C++ / fmt (pull_request) Failing after 4s
C++ / build (pull_request) Failing after 2m4s
C++ / clang-tidy (pull_request) Failing after 2m2s
JS/TS / fmt (pull_request) Failing after 6s
JS/TS / lint (pull_request) Successful in 9s
Python / static (pull_request) Successful in 27s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Successful in 1m35s
Rust / fmt (pull_request) Successful in 27s
Rust / clippy (pull_request) Successful in 49s
473 lines
12 KiB
QML
473 lines
12 KiB
QML
import QtQuick
|
|
import QtQuick.Layouts
|
|
import ZShell.Blobs
|
|
import ZShell.Config
|
|
import ZShell.Components
|
|
import ZShell.Llm
|
|
import qs.Components
|
|
import qs.Services
|
|
|
|
Item {
|
|
id: root
|
|
|
|
required property int index
|
|
required property var modelData
|
|
required property var blocks
|
|
readonly property var segments: modelData.segments
|
|
required property Item rootParent
|
|
required property ChatGeneration current
|
|
readonly property bool isActive: index === blocks.length - 1
|
|
property bool expanded: false
|
|
readonly property LlmSegment lastSegment: root.segments[root.segments.length - 1]
|
|
readonly property real totalElapsedMs: root.segments.reduce((sum, s) => sum + (s.elapsedMs ?? 0), 0)
|
|
readonly property bool pendingApproval: current.toolApprovalPending && segments.filter(s => s.type === LlmSegment.Type.ToolCall).some(s => current.pendingToolCalls.includes(s))
|
|
|
|
clip: true
|
|
implicitHeight: {
|
|
const expand = expandedRect.implicitHeight + layout.implicitHeight + expandedRect.anchors.topMargin;
|
|
const collapsed = layout.implicitHeight;
|
|
const approval = layout.implicitHeight + approvalPrompt.implicitHeight + approvalPrompt.anchors.topMargin;
|
|
|
|
if (expanded)
|
|
return expand;
|
|
else if (pendingApproval)
|
|
return approval;
|
|
else
|
|
return collapsed;
|
|
}
|
|
|
|
Behavior on implicitHeight {
|
|
Anim {
|
|
type: Anim.DefaultEffects
|
|
}
|
|
}
|
|
|
|
RowLayout {
|
|
id: layout
|
|
|
|
anchors.top: parent.top
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
|
|
Item {
|
|
implicitHeight: expandBtn.implicitHeight
|
|
implicitWidth: expandBtn.implicitWidth
|
|
|
|
LoadingIndicator {
|
|
id: spinnerReasoning
|
|
|
|
implicitSize: collapsedText.implicitHeight
|
|
anchors.centerIn: parent
|
|
opacity: root.isActive ? 1 : 0
|
|
visible: opacity > 0
|
|
|
|
Behavior on opacity {
|
|
Anim {}
|
|
}
|
|
}
|
|
|
|
IconButton {
|
|
id: expandBtn
|
|
|
|
font.pointSize: Tokens.font.size.large
|
|
icon: "keyboard_arrow_down"
|
|
inactiveOnColor: hovered ? Colors.palette.m3onSurface : Colors.palette.m3outline
|
|
anchors.centerIn: parent
|
|
opacity: root.isActive ? 0 : 1
|
|
enabled: opacity > 0
|
|
rotation: root.expanded ? 180 : 0
|
|
type: IconButton.Text
|
|
|
|
Behavior on rotation {
|
|
Anim {}
|
|
}
|
|
|
|
onClicked: root.expanded = !root.expanded
|
|
}
|
|
}
|
|
|
|
CustomText {
|
|
id: collapsedText
|
|
|
|
color: Colors.palette.m3outline
|
|
font.pointSize: Tokens.font.size.small
|
|
Layout.fillWidth: true
|
|
Layout.leftMargin: Tokens.spacing.medium
|
|
text: {
|
|
if (root.isActive) {
|
|
if (root.pendingApproval)
|
|
return qsTr("Waiting for approval...");
|
|
|
|
if (root.lastSegment.type === LlmSegment.Type.ToolCall)
|
|
return qsTr("Using %1...").arg(root.lastSegment.name);
|
|
return qsTr("Thinking...");
|
|
}
|
|
return qsTr("Worked for %1s").arg((root.totalElapsedMs / 1000).toFixed(1));
|
|
}
|
|
|
|
Behavior on opacity {
|
|
Anim {}
|
|
}
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: approvalPrompt
|
|
anchors.top: layout.bottom
|
|
anchors.topMargin: Tokens.spacing.small
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
implicitHeight: approvalLayout.implicitHeight + approvalLayout.anchors.margins * 2
|
|
visible: root.pendingApproval
|
|
|
|
CustomRect {
|
|
id: approvalBg
|
|
|
|
anchors.fill: parent
|
|
radius: Tokens.rounding.medium
|
|
color: Colors.palette.m3surface
|
|
border.width: 1
|
|
border.color: Colors.palette.m3outlineVariant
|
|
|
|
ColumnLayout {
|
|
id: approvalLayout
|
|
anchors.fill: parent
|
|
anchors.margins: Tokens.padding.medium
|
|
spacing: Tokens.spacing.small
|
|
|
|
CustomText {
|
|
id: requestHeader
|
|
text: qsTr("Tool call request")
|
|
font.pointSize: Tokens.font.size.large
|
|
color: Colors.palette.m3onSurface
|
|
}
|
|
|
|
Item {
|
|
id: toolsBg
|
|
|
|
property bool open: false
|
|
property Item rootParent: root.rootParent
|
|
property int openHeight: toolsBg.rootParent.height - Tokens.padding.medium * 2
|
|
property int openWidth: toolsBg.rootParent.width - Tokens.padding.medium * 2
|
|
|
|
implicitHeight: toolList.implicitHeight + toolList.anchors.margins * 2
|
|
Layout.fillWidth: true
|
|
|
|
function reparentWrapper(): void {
|
|
const newParent = open ? rootParent : toolsBg;
|
|
const pos = toolsWrapper.mapToItem(newParent, 0, 0);
|
|
toolsWrapper.parent = newParent;
|
|
toolsWrapper.x = pos.x;
|
|
toolsWrapper.y = pos.y;
|
|
}
|
|
|
|
BlobGroup {
|
|
id: blobGroup
|
|
|
|
color: toolsBg.open ? Colors.palette.m3surfaceContainerHighest : Colors.tPalette.m3primaryContainer
|
|
|
|
Behavior on color {
|
|
CAnim {}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: backdrop
|
|
|
|
anchors.fill: parent
|
|
enabled: false
|
|
hoverEnabled: enabled
|
|
preventStealing: true
|
|
parent: toolsBg.open ? toolsBg.rootParent : toolsBg
|
|
|
|
onClicked: toolsBg.open = false
|
|
}
|
|
|
|
Item {
|
|
id: toolsWrapper
|
|
|
|
width: toolsBg.width
|
|
height: toolList.implicitHeight + toolList.anchors.margins * 2
|
|
|
|
states: State {
|
|
name: "open"
|
|
when: toolsBg.open
|
|
|
|
PropertyChanges {
|
|
backdrop.enabled: true
|
|
toolsBackground.bottomLeftRadius: Tokens.rounding.largeIncreased
|
|
toolsBackground.bottomRightRadius: Tokens.rounding.largeIncreased
|
|
toolsBackground.topRightRadius: Tokens.rounding.largeIncreased
|
|
toolsBackground.topLeftRadius: Tokens.rounding.largeIncreased
|
|
// toolsContent.opacity: 1
|
|
toolsWrapper.height: toolsBg.openHeight
|
|
toolsWrapper.width: toolsBg.openWidth
|
|
toolsWrapper.x: (toolsBg.rootParent.width - toolsBg.openWidth) / 2
|
|
toolsWrapper.y: (toolsBg.rootParent.height - toolsBg.openHeight) / 2
|
|
elevation.opacity: 1
|
|
toolBox.opacity: 0
|
|
}
|
|
}
|
|
transitions: Transition {
|
|
id: dialogTransition
|
|
|
|
SequentialAnimation {
|
|
ScriptAction {
|
|
script: toolsBg.reparentWrapper()
|
|
}
|
|
|
|
Anim {
|
|
properties: "x,y"
|
|
}
|
|
}
|
|
|
|
PropertyAction {
|
|
property: "enabled"
|
|
}
|
|
|
|
Anim {
|
|
properties: "opacity,topLeftRadius,topRightRadius,bottomLeftRadius,bottomRightRadius"
|
|
type: Anim.DefaultEffects
|
|
}
|
|
|
|
Anim {
|
|
properties: "width,height"
|
|
}
|
|
}
|
|
|
|
Elevation {
|
|
id: elevation
|
|
|
|
anchors.fill: parent
|
|
bottomLeftRadius: toolsBackground.bottomLeftRadius
|
|
bottomRightRadius: toolsBackground.bottomRightRadius
|
|
level: 4
|
|
opacity: 0
|
|
radius: toolsBackground.radius
|
|
|
|
transform: Matrix4x4 {
|
|
matrix: toolsBackground.deformMatrix
|
|
}
|
|
}
|
|
|
|
BlobRect {
|
|
id: toolsBackground
|
|
|
|
anchors.fill: parent
|
|
bottomLeftRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
|
|
bottomRightRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
|
|
topRightRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
|
|
topLeftRadius: toolsBg.open ? Tokens.rounding.largeIncreased : Tokens.rounding.small
|
|
deformScale: 0
|
|
group: blobGroup
|
|
opacity: blobGroup.color.a * (root.enabled ? 1 : 0.5)
|
|
}
|
|
|
|
CustomRect {
|
|
id: toolBox
|
|
anchors.fill: parent
|
|
radius: Tokens.rounding.small
|
|
|
|
StateLayer {
|
|
onClicked: toolsBg.open = true
|
|
}
|
|
|
|
ColumnLayout {
|
|
id: toolList
|
|
anchors.fill: parent
|
|
anchors.margins: Tokens.padding.medium
|
|
|
|
Repeater {
|
|
model: root.current.pendingToolCalls ?? []
|
|
delegate: RowLayout {
|
|
id: toolText
|
|
|
|
required property LlmSegment modelData
|
|
required property int index
|
|
|
|
spacing: Tokens.spacing.medium
|
|
|
|
function toolTarget(seg: LlmSegment): string {
|
|
var a = null;
|
|
try {
|
|
a = JSON.parse(seg.arguments);
|
|
} catch (e) {}
|
|
if (a && typeof a === "object") {
|
|
const v = a.url ?? a.path;
|
|
if (v !== undefined && v !== null)
|
|
return String(v);
|
|
}
|
|
return String(seg.arguments);
|
|
}
|
|
|
|
CustomText {
|
|
color: Colors.palette.m3onPrimaryContainer
|
|
text: String(toolText.index + 1)
|
|
font.pointSize: Tokens.font.size.large
|
|
}
|
|
|
|
CustomText {
|
|
color: Colors.palette.m3onPrimaryContainer
|
|
text: qsTr("%1").arg(toolText.modelData.name)
|
|
}
|
|
|
|
MaterialIcon {
|
|
color: Colors.palette.m3onPrimaryContainer
|
|
text: "arrow_right_alt"
|
|
}
|
|
|
|
CustomText {
|
|
color: Colors.palette.m3onPrimaryContainer
|
|
text: qsTr("%1").arg(toolText.toolTarget(toolText.modelData))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ButtonRow {
|
|
Layout.alignment: Qt.AlignRight
|
|
spacing: Tokens.spacing.medium
|
|
|
|
IconTextButton {
|
|
icon: "check_circle"
|
|
text: qsTr("Allow")
|
|
horizontalPadding: Tokens.padding.large
|
|
verticalPadding: Tokens.padding.small
|
|
isRound: true
|
|
shapeMorph: true
|
|
onClicked: root.current.approveTools()
|
|
}
|
|
|
|
IconTextButton {
|
|
icon: "cancel"
|
|
text: qsTr("Deny")
|
|
isRound: true
|
|
shapeMorph: true
|
|
inactiveColor: Colors.palette.m3error
|
|
inactiveOnColor: Colors.palette.m3onError
|
|
horizontalPadding: Tokens.padding.large
|
|
verticalPadding: Tokens.padding.small
|
|
onClicked: root.current.denyTools()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
CustomRect {
|
|
id: expandedRect
|
|
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: layout.bottom
|
|
anchors.bottom: parent.bottom
|
|
anchors.topMargin: Tokens.spacing.small
|
|
color: Colors.palette.m3surfaceContainerLow
|
|
implicitHeight: expandedContent.contentHeight + expandedContent.anchors.margins * 2
|
|
opacity: root.expanded ? 1 : 0
|
|
radius: Tokens.rounding.medium
|
|
visible: opacity > 0
|
|
|
|
Behavior on opacity {
|
|
Anim {}
|
|
}
|
|
|
|
ListView {
|
|
id: expandedContent
|
|
|
|
anchors.fill: parent
|
|
anchors.margins: Tokens.padding.medium
|
|
model: root.segments
|
|
spacing: Tokens.spacing.medium
|
|
|
|
delegate: Item {
|
|
id: segment
|
|
|
|
required property LlmSegment modelData
|
|
required property int index
|
|
|
|
implicitWidth: ListView.view.width
|
|
implicitHeight: header.implicitHeight + content.implicitHeight + content.anchors.topMargin
|
|
|
|
RowLayout {
|
|
id: header
|
|
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
spacing: Tokens.spacing.small
|
|
|
|
MaterialIcon {
|
|
text: segment.modelData.type === LlmSegment.Type.Reasoning ? "cognition" : "build"
|
|
fill: 1
|
|
}
|
|
|
|
CustomText {
|
|
Layout.fillWidth: true
|
|
text: segment.modelData.type === LlmSegment.Type.Reasoning ? qsTr("Thought for %1s").arg((segment.modelData.elapsedMs / 1000).toFixed(1)) : qsTr("Used %1 for %2s").arg(segment.modelData.name).arg((segment.modelData.elapsedMs / 1000).toFixed(1))
|
|
}
|
|
}
|
|
|
|
CustomText {
|
|
id: content
|
|
|
|
function toolTarget(seg: LlmSegment): string {
|
|
var a = null;
|
|
try {
|
|
a = JSON.parse(seg.arguments);
|
|
} catch (e) {}
|
|
if (a && typeof a === "object") {
|
|
const v = a.url ?? a.path;
|
|
if (v !== undefined && v !== null)
|
|
return String(v);
|
|
}
|
|
return String(seg.arguments);
|
|
}
|
|
|
|
function prettyTool(seg: LlmSegment): string {
|
|
if (seg.name === "webfetch") {
|
|
switch (seg.status) {
|
|
case LlmSegment.Status.Running:
|
|
return qsTr("Fetching");
|
|
case LlmSegment.Status.Pending:
|
|
return qsTr("Pending fetch");
|
|
case LlmSegment.Status.Success:
|
|
return qsTr("Fetched");
|
|
case LlmSegment.Status.Error:
|
|
return qsTr("Failed to fetch");
|
|
default:
|
|
return qsTr("Fetched website");
|
|
}
|
|
} else if (seg.name === "readfile") {
|
|
switch (seg.status) {
|
|
case LlmSegment.Status.Running:
|
|
return qsTr("Reading");
|
|
case LlmSegment.Status.Pending:
|
|
return qsTr("Pending read");
|
|
case LlmSegment.Status.Success:
|
|
return qsTr("Read");
|
|
case LlmSegment.Status.Error:
|
|
return qsTr("Failed to read");
|
|
default:
|
|
return qsTr("Read file");
|
|
}
|
|
}
|
|
}
|
|
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Tokens.padding.small
|
|
anchors.top: header.bottom
|
|
anchors.topMargin: Tokens.spacing.small
|
|
anchors.right: parent.right
|
|
color: Colors.palette.m3outline
|
|
font.pointSize: Tokens.font.size.small
|
|
wrapMode: CustomText.WrapAtWordBoundaryOrAnywhere
|
|
text: segment.modelData.type === LlmSegment.Type.Reasoning ? segment.modelData.text : qsTr("%1 %2").arg(content.prettyTool(segment.modelData)).arg(content.toolTarget(segment.modelData))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|