pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import ZShell.Config import ZShell.Components import ZShell.Llm import ZShell.Blobs import qs.Components import qs.Services Item { id: root required property Item rootParent required property ChatGeneration current implicitHeight: approvalLayout.implicitHeight + approvalLayout.anchors.margins * 2 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: Tokens.font.headline.medium color: Colors.palette.m3onSurface } Item { id: toolsBg property bool open: false property int maxHeight: root.rootParent.height - Tokens.padding.medium * 2 property int openHeight: Math.min(toolsContent.item?.implicitHeight, maxHeight) property int openWidth: root.rootParent.width - Tokens.padding.medium * 2 property real openHeightProg: openHeight / maxHeight property int collapsedY: 0 implicitHeight: toolList.implicitHeight + toolList.anchors.margins * 2 z: open || dialogTransition.running ? 2 : 0 Layout.fillWidth: true function reparentWrapper(): void { const newParent = open ? root.rootParent : toolsBg; const c = toolsWrapper.parent.mapToItem(newParent, toolsWrapper.x + toolsWrapper.width / 2, toolsWrapper.y + toolsWrapper.height / 2); toolsBg.collapsedY = toolsWrapper.y; toolsWrapper.parent = newParent; toolsWrapper.x = c.x - toolsWrapper.width / 2; toolsWrapper.y = c.y - toolsWrapper.height / 2; } onOpenChanged: { if (open) root.rootParent.interactive = false; else root.rootParent.interactive = true; } 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 ? root.rootParent : toolsBg onClicked: toolsBg.open = false } Item { id: toolsWrapper width: toolsBg.width height: toolBox.implicitHeight rotation: parent === root.rootParent ? 180 : 0 z: 1 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: (root.rootParent.width - toolsBg.openWidth) / 2 toolsWrapper.y: (confirmationRow.implicitHeight + Tokens.spacing.small + Tokens.padding.medium) * (1 - toolsBg.openHeightProg) + Tokens.padding.medium 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.left: parent.left anchors.right: parent.right height: Math.min(implicitHeight, parent.height) implicitHeight: toolList.implicitHeight + Tokens.padding.medium * 2 radius: Tokens.rounding.small StateLayer { onClicked: { toolsBg.open = true; } } ColumnLayout { id: toolList anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Tokens.padding.medium Repeater { id: repeater model: root.current.pendingToolCalls?.slice(0, 4) ?? [] delegate: RowLayout { id: toolText required property var modelData required property int index spacing: Tokens.spacing.medium function toolTarget(seg: var): 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); } MaterialIcon { color: Colors.palette.m3onPrimaryContainer text: toolText.modelData.name === "webfetch" ? "web" : "text_snippet" fill: 1 } 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)) } } } CustomText { color: Colors.palette.m3onPrimaryContainer text: qsTr("And %1 more").arg(root.current.pendingToolCalls.slice(4).length) visible: repeater.count < root.current.pendingToolCalls.length } } } Loader { id: toolsContent active: opacity > 0 anchors.fill: parent asynchronous: false opacity: 0 sourceComponent: MouseArea { onWheel: event => event.accepted = true preventStealing: true implicitHeight: innerLayout.contentHeight + innerLayout.anchors.margins * 2 VerticalFadeListView { id: innerLayout anchors.fill: parent anchors.margins: Tokens.padding.small cacheBuffer: height * 10 fadeAmount: 0.1 fadeEnabled: interactive spacing: 0 clip: true interactive: contentHeight > toolsBg.maxHeight model: root.current.pendingToolCalls ?? [] delegate: Item { id: toolCall required property var modelData property bool expanded: false implicitWidth: CustomListView.view.width implicitHeight: collapsedRow.implicitHeight + collapsedRow.anchors.margins + (expanded ? expandedTool.implicitHeight + expandedTool.anchors.margins + expandedTool.anchors.topMargin : collapsedRow.anchors.margins) function toolTarget(seg: var): 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); } Behavior on implicitHeight { Anim {} } RowLayout { id: collapsedRow anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right anchors.margins: Tokens.padding.medium IconButton { id: toolExpandBtn font: Tokens.font.icon.medium icon: "keyboard_arrow_down" inactiveOnColor: hovered ? Colors.palette.m3onSurface : Colors.palette.m3outline rotation: toolCall.expanded ? 180 : 0 type: IconButton.Text Behavior on rotation { Anim {} } onClicked: toolCall.expanded = !toolCall.expanded } CustomText { id: preview Layout.fillWidth: true text: toolCall.modelData.name } } ClipRect { id: expandedTool anchors.top: collapsedRow.bottom anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: Tokens.padding.medium anchors.topMargin: Tokens.spacing.medium implicitHeight: fullToolCall.implicitHeight + fullToolCall.anchors.margins * 2 radius: Tokens.rounding.medium color: Colors.palette.m3surfaceContainerLow CustomText { id: fullToolCall anchors.fill: parent anchors.margins: Tokens.padding.medium text: toolCall.toolTarget(toolCall.modelData) wrapMode: CustomText.WrapAtWordBoundaryOrAnywhere } } } } } } } } ButtonRow { id: confirmationRow 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() } } } } }