aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/assistant/Assistant.qml
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/assistant/Assistant.qml')
-rw-r--r--desktop/assistant/Assistant.qml80
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
}