Files
z-bar-qt/Modules/Notifications/Sidebar/Chat/Content/ProcessBlock.qml
T
zach 34dde4bbfb
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
fix: tool call prompt
2026-09-03 23:45:07 +02:00

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))
}
}
}
}
}