pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell import ZShell.Config import ZShell.Llm import qs.Components import qs.Services MouseArea { id: root property ChatGeneration current: modelData.activeGeneration required property int index readonly property bool isUser: modelData.role === ChatMessage.Role.User required property ChatMessage modelData property bool reasoningExpanded: false readonly property var blocks: blockify(current.segments) property real savedOffset: -1 property bool stickActive: false function handleReasoningToggle(expanded: bool): void { const view = root.ListView.view; if (!view) return; if (expanded) { root.savedOffset = view.contentY - root.y; root.reasoningExpanded = true; } else { root.reasoningExpanded = false; root.stickActive = false; settleTimer.stop(); if (!isNaN(root.savedOffset)) { restoreAnim.to = root.y + root.savedOffset; restoreAnim.restart(); root.savedOffset = NaN; } } } function blockify(segs) { const blocks = []; for (let i = 0; i < segs.length; i++) { const seg = segs[i]; if (seg.type === LlmSegment.Type.Content) { blocks.push({ kind: "content", id: i, segments: [seg] }); } else { const last = blocks.length ? blocks[blocks.length - 1] : null; if (last && last.kind === "process") { last.segments.push(seg); } else { blocks.push({ kind: "process", id: i, segments: [seg] }); } } } return blocks; } implicitWidth: ListView.view.width hoverEnabled: true preventStealing: false implicitHeight: layout.implicitHeight + Tokens.padding.medium Timer { id: settleTimer interval: 16 onTriggered: { root.stickActive = false; } } onImplicitHeightChanged: { if (root.reasoningExpanded) { root.stickActive = true; settleTimer.restart(); const view = root.ListView.view; if (!view) return; const bottomEdge = root.y + root.height + Tokens.padding.large * 2; const viewportBottom = view.contentY + view.height; if (bottomEdge > viewportBottom) view.contentY = bottomEdge - view.height; } } Behavior on current { id: changeAnim property var object: targetProperty readonly property bool next: { const cond = root.modelData.generations.indexOf(current) < root.modelData.generations.indexOf(changeAnim.targetValue); return cond; } animation: SequentialAnimation { ParallelAnimation { Anim { target: root property: "x" to: changeAnim.next ? root.width / 4 : -root.width / 4 from: 0 type: Anim.FastEffects } Anim { target: root property: "opacity" from: 1 to: 0 type: Anim.FastEffects } } PropertyAction {} ParallelAnimation { Anim { target: root property: "x" from: changeAnim.next ? -root.width / 4 : root.width / 4 type: Anim.FastEffects to: 0 } Anim { target: root property: "opacity" from: 0 type: Anim.FastEffects to: 1 } } } } Anim { id: restoreAnim property: "contentY" target: root.ListView.view type: Anim.DefaultEffects } Column { id: layout anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right Repeater { id: segmentRep model: ScriptModel { values: root.blocks objectProp: "id" } delegate: DelegateChooser { role: "kind" DelegateChoice { roleValue: "process" delegate: ProcessBlock { width: root.width blocks: root.blocks onExpandedChanged: root.handleReasoningToggle(expanded) } } DelegateChoice { roleValue: "content" delegate: ContentBubble { required property var modelData isUser: root.isUser segment: modelData.segments[0] width: root.width repeater: segmentRep message: root.modelData current: root.current hovered: root.containsMouse onEdit: text => { root.modelData.edit(text); if (root.isUser) root.modelData.generate(); } } } } } } }