From cb16fa6c17298a6f2f4aa390c01ff8eacd53a996 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 16 Oct 2026 15:28:48 +0200 Subject: fix(assistant): keep state across reloads, refusals and long replies From review. A shell reload reconnected with lastActive at 0, so the next SUPER+Home started a new conversation thirty seconds into one; hello with turns now counts as activity. show() could send new under a reply still running, which stops it unsaved; it waits while busy. ask() cleared the chips before the daemon accepted them, so a refused file (unreadable, too big, a model without vision) dropped them and collapsed the card to the entry line; the user event clears them now, and chatting holds the layout while a PDF is read. In the picker, folders ignore name filters and sorted first, so filter then Enter opened a folder; folders hide while filtering. Folder URLs are encoded, so a '#' in a name no longer cuts the path, and glob characters typed in the filter are taken literally. The card says the assistant is not running in every state, panels sit under the chat header instead of over it, and History and the picker no longer open together. Dropped file://localhost/ URLs are accepted, other hosts refused, and the island receptor keeps the sliver's width at rest so it takes no clicks beside it. Co-Authored-By: Claude Opus 5.5 --- desktop/assistant/Assistant.qml | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) (limited to 'desktop/assistant/Assistant.qml') diff --git a/desktop/assistant/Assistant.qml b/desktop/assistant/Assistant.qml index b038dd6..977b4dd 100644 --- a/desktop/assistant/Assistant.qml +++ b/desktop/assistant/Assistant.qml @@ -78,7 +78,9 @@ Scope { // 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(); + // Never while a reply runs: new would stop it, and a stopped turn is + // not saved. + if (turns.count && !root.busy && Date.now() - root.lastActive > 10 * 60 * 1000) root.fresh(); root.open = true; } @@ -120,15 +122,20 @@ Scope { link.item.flush(); } + // The chips stay until the daemon's user event takes them: a refused + // file (unreadable, too big, a model without vision) comes back as an + // error, and the chips must still be there to fix it. chatting keeps the + // chat layout meanwhile, so the card does not shrink while a PDF is read. function ask(text) { if (!root.connected || root.blocked || (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.staged = []; return true; } function showHistory() { + root.pickerOpen = false; root.historyOpen = true; root.send({ type: "list" }); } @@ -172,15 +179,21 @@ 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(); + // 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(); 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(); + // Emptied (New, or the current one deleted) with nothing staged: + // back to the entry line. + else if (!root.staged.length) root.chatting = false; break; case "user": turns.append(root.row({ user: e.text, files: e.files }, true)); + root.staged = []; root.lastActive = Date.now(); break; case "think": root.bufThink += e.delta; break; -- cgit v1.2.3