diff options
Diffstat (limited to 'desktop/assistant/Assistant.qml')
| -rw-r--r-- | desktop/assistant/Assistant.qml | 54 |
1 files changed, 49 insertions, 5 deletions
diff --git a/desktop/assistant/Assistant.qml b/desktop/assistant/Assistant.qml index 33616ad..0b7026d 100644 --- a/desktop/assistant/Assistant.qml +++ b/desktop/assistant/Assistant.qml @@ -66,6 +66,18 @@ Scope { // The preferred model being loaded on open, shown in the header until // the router reports it loaded. Set from shell.qml. property string swapping: "" + // The model this chat is asked with: its stored pick (hello.chat_model, + // conversation.model), "" for none, then the preferred one (set from + // shell.qml). "" for both: the daemon uses whatever the router has + // loaded. Spec: desktop-assistant, + // docs/superpowers/specs/2026-10-23-cloud-models-design.md. + property string chatModel: "" + property string preferred: "" + readonly property string wanted: root.chatModel || root.preferred + // The daemon's models event: [{id, status, cloud: false} | {id, provider, cloud: true}]. + property var models: [] + readonly property bool cloud: root.models.some(m => m.cloud && m.id === root.wanted) + property bool modelsOpen: false property int ctxUsed: 0 property int ctxMax: 0 property var convId: null @@ -74,6 +86,9 @@ Scope { property var convs: [] property var results: [] readonly property bool busy: root.state !== "idle" + // A reply finished (not stopped). The canvas notifies through notifyDone; + // the desktop companion waves. + signal replied() // A daemon gone mid-reply sends no idle; its hello on return redraws. onConnectedChanged: if (!root.connected) { @@ -110,11 +125,12 @@ Scope { function close() { root.endRecording(false); - root.open = false; root.historyOpen = false; root.pickerOpen = false; + root.open = false; root.historyOpen = false; root.pickerOpen = false; root.modelsOpen = false; } // Escape: a recording, the picker, then the history panel, then the card. function back() { if (root.recording) root.endRecording(false); + else if (root.modelsOpen) root.modelsOpen = false; else if (root.pickerOpen) root.pickerOpen = false; else if (root.historyOpen) root.historyOpen = false; else root.close(); @@ -255,8 +271,8 @@ Scope { root.notice = "recording failed"; } else if (rec.send && root.connected) { root.chatting = true; - root.send({ type: "ask", text: "", audio: root.micPath, - files: root.staged.map(f => f.path) }); + root.send(root.withModel({ type: "ask", text: "", audio: root.micPath, + files: root.staged.map(f => f.path) })); } rec.destroy(); } @@ -322,16 +338,37 @@ Scope { || (text.trim() === "" && !root.staged.length)) return false; root.notice = ""; root.chatting = true; - root.send({ type: "ask", text: text, files: root.staged.map(f => f.path) }); + root.send(root.withModel({ type: "ask", text: text, files: root.staged.map(f => f.path) })); return true; } function showHistory() { root.pickerOpen = false; + root.modelsOpen = false; root.historyOpen = true; root.send({ type: "list" }); } + function showModels() { + root.historyOpen = root.pickerOpen = false; + root.modelsOpen = true; + root.send({ type: "models" }); + } + // Applies from the next ask; the daemon stores it with the chat then. + // Moving a chat with turns to the cloud sends its history there, so + // that is said once, on the switch. + function pickModel(id) { + const m = root.models.find(x => x.id === id); + if (m && m.cloud && !root.cloud && turns.count) + root.notice = `History in this chat goes to ${m.provider}.`; + root.chatModel = id; + root.modelsOpen = false; + } + function withModel(cmd) { + if (root.wanted) cmd.model = root.wanted; + return cmd; + } + function row(t, live) { return { user: t.user ?? "", reply: t.reply ?? "", think: t.think ?? "", tools: (t.tools ?? []).join(","), error: "", live: live, @@ -368,15 +405,18 @@ Scope { root.state = e.state; root.runningTool = e.state === "tool" ? e.tool : ""; root.model = e.model ?? ""; + root.chatModel = e.chat_model ?? ""; root.ctxUsed = e.ctx_used; root.ctxMax = e.ctx_max; root.load(e.id, e.turns, e.pending); // A restart or reload counts as active: the conversation it // reconnects to is not stale just because this process is new. if (e.pending || e.turns.length) root.lastActive = Date.now(); + root.send({ type: "models" }); break; case "conversation": root.load(e.id, e.turns, null); + root.chatModel = e.model ?? ""; // Opened from History on purpose: it counts as active now. if (e.turns.length) root.lastActive = Date.now(); // Emptied (New, or the current one deleted) with nothing staged: @@ -405,7 +445,10 @@ Scope { } if (!e.stopped) root.convId = e.id; root.lastActive = Date.now(); - if (!e.stopped && i >= 0) root.notifyDone(turns.get(i).reply); + if (!e.stopped && i >= 0) { + root.notifyDone(turns.get(i).reply); + root.replied(); + } break; case "progress": if (root.opDaemon) root.opChars = e.chars; break; case "written": @@ -439,6 +482,7 @@ Scope { case "ctx": root.ctxUsed = e.used; root.ctxMax = e.max; if (e.model) root.model = e.model; break; case "conversations": root.convs = e.items; break; case "results": root.results = e.items; break; + case "models": root.models = e.items; break; } } |
