aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/assistant/Message.qml
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-16 14:11:17 +0200
committerDanilo M. <danix@danix.xyz>2026-10-16 14:11:17 +0200
commitb7f33e54c93c5e3c764aa083c2f354fb8751e166 (patch)
tree6b9b66af1f0a5897137d13df66b19b504270da21 /desktop/assistant/Message.qml
parent72d3ff1f0c8c2cadf315214f31312895154cd20e (diff)
downloadquickshell-b7f33e54c93c5e3c764aa083c2f354fb8751e166.tar.gz
quickshell-b7f33e54c93c5e3c764aa083c2f354fb8751e166.zip
feat(assistant): the canvas, a view over the assistant daemon
The socket, the turn model and the window live in Assistant.qml, outside the window's loader, so closing the card loses nothing and a reply keeps streaming while it is closed. Tokens reach the model on a 50 ms tick rather than per token. Prose goes through Notify.sanitize, because model output can quote a search result and a remote image in it would be fetched on render. Code blocks get a copy button once their fence closes. The history panel lists conversations and searches them through the daemon's full-text index. Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/assistant/Message.qml')
-rw-r--r--desktop/assistant/Message.qml107
1 files changed, 107 insertions, 0 deletions
diff --git a/desktop/assistant/Message.qml b/desktop/assistant/Message.qml
new file mode 100644
index 0000000..c724c2e
--- /dev/null
+++ b/desktop/assistant/Message.qml
@@ -0,0 +1,107 @@
+// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+//
+// 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 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
+ }
+ }
+
+ // 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 on: msg.live && msg.running === modelData
+ width: chip.implicitWidth + 14
+ height: chip.implicitHeight + 6
+ radius: height / 2
+ color: Qt.alpha(Theme.surface, 0.9)
+ opacity: on ? 1 : 0.55
+ Text {
+ id: chip
+ anchors.centerIn: parent
+ text: parent.modelData
+ color: parent.on ? 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 }
+ }
+}