diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-16 15:23:41 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-16 15:23:41 +0200 |
| commit | f5da8d8ee2cb4ace1fa6841437b4798e7799c100 (patch) | |
| tree | 9a1d51d8e61782852de2779cc77bfa5d2ca94a65 /desktop/assistant/Assistant.qml | |
| parent | 13a10999f53802858f87883db381842d741eab69 (diff) | |
| download | quickshell-f5da8d8ee2cb4ace1fa6841437b4798e7799c100.tar.gz quickshell-f5da8d8ee2cb4ace1fa6841437b4798e7799c100.zip | |
feat(assistant): entry line, file state, staged files and a picker
The card has three states: an entry line, a file state for staged files with Chat about it, and the chat. SUPER+Home resumes a conversation active in the last 10 minutes and otherwise starts a new one. The paperclip opens a picker drawn in the card, since a system dialog would open beneath the overlay that holds the keyboard. Button gains a font alias for the paperclip glyph, and the canvas a socketPath property so a check can point it at a scratch daemon.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/assistant/Assistant.qml')
| -rw-r--r-- | desktop/assistant/Assistant.qml | 80 |
1 files changed, 71 insertions, 9 deletions
diff --git a/desktop/assistant/Assistant.qml b/desktop/assistant/Assistant.qml index d143219..b038dd6 100644 --- a/desktop/assistant/Assistant.qml +++ b/desktop/assistant/Assistant.qml @@ -14,6 +14,7 @@ import Quickshell.Io import Quickshell.Wayland import QtQuick import ".." +import "Files.js" as Files // The assistant canvas: a view over desktop-assistant's socket. The daemon // owns the model, the tools and the history; this keeps the conversation it @@ -23,8 +24,25 @@ Scope { id: root property string monitor: "DP-1" + // The daemon's socket; a check against a scratch daemon points it elsewhere. + property string socketPath: `${Quickshell.env("XDG_RUNTIME_DIR") || "/tmp"}/desktop-assistant.sock` property bool open: false property bool historyOpen: false + property bool pickerOpen: false + property string pickDir: "" // where the picker opened last; "" is home + + // Files waiting to go with the next question: [{path, name, kind}]. + property var staged: [] + // Chat about it: the chat layout before any turn exists. + property bool chatting: false + // When the conversation last moved, for the resume rule in show(). + property double lastActive: 0 + + // entry: one line. file: files staged, nothing asked yet. chat: turns. + readonly property string mode: turns.count || root.chatting ? "chat" + : root.staged.length ? "file" : "entry" + // A staged kind chat does not take holds the question back. + readonly property bool blocked: root.staged.some(f => !Files.chats(f.kind)) readonly property bool connected: link.item?.connected ?? false property string state: "idle" @@ -55,10 +73,46 @@ Scope { readonly property var screenObj: Quickshell.screens.find(s => s.name === root.monitor) ?? Quickshell.screens[0] - function toggle() { if (root.open) root.close(); else root.open = true; } - function close() { root.open = false; root.historyOpen = false; } - // Escape: the history panel first, then the card. - function back() { if (root.historyOpen) root.historyOpen = false; else root.close(); } + function toggle() { if (root.open) root.close(); else root.show(); } + + // A conversation idle for 10 minutes is over: opening starts a new one, + // the old one staying in History. ponytail: a constant, not a setting. + function show() { + if (turns.count && Date.now() - root.lastActive > 10 * 60 * 1000) root.fresh(); + root.open = true; + } + + function close() { root.open = false; root.historyOpen = false; root.pickerOpen = false; } + // Escape: the picker, then the history panel, then the card. + function back() { + if (root.pickerOpen) root.pickerOpen = false; + else if (root.historyOpen) root.historyOpen = false; + else root.close(); + } + + function fresh() { + root.send({ type: "new" }); + root.chatting = false; + } + + // A drop is a new task: a new conversation with these files staged. + function attach(paths) { + const files = paths.filter(p => p !== "").map(Files.file); + if (!files.length) return; + root.fresh(); + root.staged = files; + root.historyOpen = root.pickerOpen = false; + root.open = true; + } + + function stage(path) { + if (!root.staged.some(f => f.path === path)) root.staged = root.staged.concat([Files.file(path)]); + } + function unstage(i) { + const s = root.staged.slice(); + s.splice(i, 1); + root.staged = s; + } function send(cmd) { if (!link.item) return; @@ -67,9 +121,10 @@ Scope { } function ask(text) { - if (!root.connected || text.trim() === "") return false; + if (!root.connected || root.blocked || (text.trim() === "" && !root.staged.length)) return false; root.notice = ""; - root.send({ type: "ask", text: text }); + root.send({ type: "ask", text: text, files: root.staged.map(f => f.path) }); + root.staged = []; return true; } @@ -81,6 +136,8 @@ Scope { function row(t, live) { return { user: t.user ?? "", reply: t.reply ?? "", think: t.think ?? "", tools: (t.tools ?? []).join(","), error: "", live: live, + // JSON for the same reason tools is a string. + files: JSON.stringify(t.files ?? []), stopped: false, expanded: false }; } @@ -115,12 +172,16 @@ Scope { root.ctxUsed = e.ctx_used; root.ctxMax = e.ctx_max; root.load(e.id, e.turns, e.pending); + if (e.pending) root.lastActive = Date.now(); break; case "conversation": root.load(e.id, e.turns, null); + // Opened from History on purpose: it counts as active now. + if (e.turns.length) root.lastActive = Date.now(); break; case "user": - turns.append(root.row({ user: e.text }, true)); + turns.append(root.row({ user: e.text, files: e.files }, true)); + root.lastActive = Date.now(); break; case "think": root.bufThink += e.delta; break; case "token": root.bufReply += e.delta; break; @@ -138,6 +199,7 @@ Scope { turns.setProperty(i, "stopped", e.stopped === true); } if (!e.stopped) root.convId = e.id; + root.lastActive = Date.now(); break; case "error": // Only an error marked turn ended the reply. A command error, @@ -175,7 +237,7 @@ Scope { id: link active: true Socket { - path: `${Quickshell.env("XDG_RUNTIME_DIR") || "/tmp"}/desktop-assistant.sock` + path: root.socketPath connected: true parser: SplitParser { onRead: line => root.handle(line) } } @@ -205,7 +267,7 @@ Scope { Card { anchors.centerIn: parent width: Math.min(760, parent.width - 48) - height: Math.round(parent.height * 0.7) + fullHeight: Math.round(parent.height * 0.7) canvas: root turnModel: turns } |
