// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import QtQuick import ".." // One turn: the question, then the reply with its thinking line, the tool // chips, and an error or stopped mark. Both texts go through Segments, so // the question renders as markdown too. Column { id: msg required property int index required property string user required property string reply required property string think required property string tools required property string files // JSON: [{name, kind, pages, truncated}] required property string error required property bool live required property bool stopped required property bool expanded required property var model // the row, for setting expanded property string running: "" // the tool running now, if any spacing: 6 Rectangle { width: msg.width height: question.height + 16 radius: 8 color: Qt.alpha(Theme.surface, 0.7) Segments { id: question anchors { left: parent.left; right: parent.right; top: parent.top; margins: 8 } text: msg.user } } // The files that went with the question, as the daemon read them. Flow { width: msg.width spacing: 6 visible: msg.files !== "[]" Repeater { model: JSON.parse(msg.files) Rectangle { required property var modelData width: f.implicitWidth + 16 height: f.implicitHeight + 6 radius: height / 2 color: Qt.alpha(Theme.surface, 0.6) Text { id: f anchors.centerIn: parent text: parent.modelData.name + (parent.modelData.pages ? ` · ${parent.modelData.pages} pages` : "") + (parent.modelData.truncated ? " · truncated" : "") color: parent.modelData.truncated ? Theme.yellow : Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 } } } } } // Collapsed by default; a click shows the reasoning as plain text. Text { visible: msg.think !== "" text: (msg.expanded ? "▾ " : "▸ ") + (msg.live && msg.reply === "" ? "thinking…" : "thinking") color: Theme.overlay font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: msg.model.expanded = !msg.expanded } } Text { width: msg.width visible: msg.expanded && msg.think !== "" text: msg.think textFormat: Text.PlainText wrapMode: Text.Wrap color: Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } Flow { width: msg.width spacing: 6 visible: msg.tools !== "" Repeater { model: msg.tools ? msg.tools.split(",") : [] Rectangle { required property string modelData readonly property bool lit: msg.live && msg.running === modelData property int elapsed: 0 width: chip.implicitWidth + 14 height: chip.implicitHeight + 6 radius: height / 2 color: Qt.alpha(Theme.surface, 0.9) opacity: lit ? 1 : 0.55 // A lit chip alone looks frozen on a clone that takes a // minute, so the running tool counts seconds. onLitChanged: if (!lit) elapsed = 0 Timer { running: parent.lit interval: 1000 repeat: true onTriggered: parent.elapsed++ } Text { id: chip anchors.centerIn: parent text: parent.modelData + (parent.lit ? " · " + parent.elapsed + "s" : "") color: parent.lit ? Theme.text : Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 5 } } } } } Segments { width: msg.width text: msg.reply visible: msg.reply !== "" } Text { visible: msg.error !== "" || msg.stopped width: msg.width text: msg.error !== "" ? msg.error : "stopped" color: msg.error !== "" ? Theme.red : Theme.overlay wrapMode: Text.Wrap font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } }