From b7f33e54c93c5e3c764aa083c2f354fb8751e166 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 16 Oct 2026 14:11:17 +0200 Subject: 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 --- desktop/assistant/Segments.qml | 90 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 desktop/assistant/Segments.qml (limited to 'desktop/assistant/Segments.qml') diff --git a/desktop/assistant/Segments.qml b/desktop/assistant/Segments.qml new file mode 100644 index 0000000..2b68b64 --- /dev/null +++ b/desktop/assistant/Segments.qml @@ -0,0 +1,90 @@ +// 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 Quickshell +import QtQuick +import ".." +import "Md.js" as Md + +// Markdown with a copy button per code block. Prose is a read-only TextEdit, +// selectable unlike Text. It goes through Notify.sanitize, which blocks +// remote images: model output can quote a search result, and an image URL +// in it would be fetched the moment it rendered. +Column { + id: seg + property string text + spacing: 8 + + Repeater { + model: Md.split(seg.text) + + Item { + id: part + required property var modelData + width: seg.width + height: modelData.kind === "code" ? block.height : prose.height + + TextEdit { + id: prose + visible: part.modelData.kind === "prose" + width: part.width + height: visible ? implicitHeight : 0 + readOnly: true + selectByMouse: true + textFormat: TextEdit.MarkdownText + text: visible ? Notify.sanitize(part.modelData.text) : "" + wrapMode: TextEdit.Wrap + color: Theme.text + selectionColor: Qt.alpha(Theme.accent, 0.4) + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } + onLinkActivated: link => Qt.openUrlExternally(link) + HoverHandler { cursorShape: prose.hoveredLink ? Qt.PointingHandCursor : Qt.IBeamCursor } + } + + Rectangle { + id: block + visible: part.modelData.kind === "code" + width: part.width + height: visible ? code.height + 16 : 0 + radius: 8 + color: Qt.alpha(Theme.crust, 0.6) + + TextEdit { + id: code + anchors { left: parent.left; right: copy.left; top: parent.top; margins: 8 } + readOnly: true + selectByMouse: true + textFormat: TextEdit.PlainText + text: part.modelData.text ?? "" + wrapMode: TextEdit.Wrap + color: Theme.text + selectionColor: Qt.alpha(Theme.accent, 0.4) + font { family: Theme.iconFamily; pixelSize: Theme.fontSize - 3 } + } + + // Copy only a finished block: a half-streamed one is not the + // command the model meant. + Button { + id: copy + anchors { right: parent.right; top: parent.top; margins: 6 } + visible: !part.modelData.open + width: visible ? implicitWidth : 0 + text: copied.running ? "Copied" : (part.modelData.lang || "Copy") + onClicked: { + Quickshell.execDetached(["wl-copy", "--", part.modelData.text]); + copied.restart(); + } + Timer { id: copied; interval: 1200 } + } + } + } + } +} -- cgit v1.2.3