import QtQuick import QtQuick.Controls import QtQuick.Layouts // The transcript: cards for the messages, a card per tool call, folded // thinking blocks and one-line notes. Follows the tail of the conversation // until the user scrolls away from it. Item { id: view property var model: null property bool running: false ListView { id: list anchors.fill: parent anchors.leftMargin: Theme.gapL anchors.rightMargin: Theme.gapL model: view.model clip: true spacing: Theme.gap topMargin: Theme.gapL bottomMargin: Theme.gapL cacheBuffer: 2000 boundsBehavior: Flickable.StopAtBounds // The delegate picks the card by row kind; the components below read // the ListModel roles directly, which is why they are declared in the // delegate's scope. delegate: Loader { id: rowLoader width: list.width sourceComponent: model.kind === "tool" ? toolComp : model.kind === "think" ? thinkComp : model.kind === "note" ? noteComp : msgComp Component { id: msgComp MessageCard { role: model.role text: model.text complete: model.complete html: (model.role === "agent" || model.role === "error") && (model.complete || model.text.length > 12000) && model.text.length > 0 ? agent.renderMarkdown(model.text) : "" } } Component { id: thinkComp ThinkBlock { text: model.text complete: model.complete } } Component { id: toolComp ToolCard { name: model.name state: model.state detail: model.detail output: model.output exitCode: model.exitCode hasResult: model.complete } } Component { id: noteComp NoteLine { text: model.text level: model.state === "warn" || model.state === "error" ? model.state : "info" } } } onMovementEnded: view.stickBottom = atYEnd onCountChanged: if (view.stickBottom) Qt.callLater(list.positionViewAtEnd) onContentHeightChanged: if (view.stickBottom) Qt.callLater(list.positionViewAtEnd) } // Follow the tail unless the user asked otherwise. property bool stickBottom: true // Empty state: what this window is and what to do with it. ColumnLayout { anchors.centerIn: parent width: Math.min(parent.width - 80, 520) spacing: Theme.gap visible: view.model ? view.model.count === 0 : true Text { Layout.alignment: Qt.AlignHCenter text: "CST-agent" color: Theme.text font.pixelSize: 22 font.bold: true } Text { Layout.alignment: Qt.AlignHCenter text: "The engine runs in this window — no server, no second process." color: Theme.textDim font.pixelSize: Theme.bodySize horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap Layout.fillWidth: true } Text { Layout.alignment: Qt.AlignHCenter Layout.topMargin: Theme.gap text: "Ask for a change, a command to run, a file to explain.\n" + "Every write, delete and command asks first: Deny / Once / Always.\n" + "Enter sends, Shift+Enter adds a line, Esc stops a running turn." color: Theme.textFaint font.pixelSize: Theme.smallSize horizontalAlignment: Text.AlignHCenter wrapMode: Text.Wrap Layout.fillWidth: true } } // Jump back to the live end of the conversation. RoundButton { anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: Theme.padL width: Theme.jumpButton height: Theme.jumpButton visible: !view.stickBottom && list.contentHeight > list.height text: "↓" onClicked: { view.stickBottom = true list.positionViewAtEnd() } } }