diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-16 16:52:45 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-16 16:52:45 +0200 |
| commit | bb9442a07cebe70c3492cca31f31a8270bb83134 (patch) | |
| tree | 2dff25cd4c3715b5b54d4c65ee6619e896f170aa /desktop/assistant/Assistant.qml | |
| parent | 193cd8c2e1cc7e16b1389b95036bf7f4f99f97e0 (diff) | |
| download | quickshell-bb9442a07cebe70c3492cca31f31a8270bb83134.tar.gz quickshell-bb9442a07cebe70c3492cca31f31a8270bb83134.zip | |
fix(assistant): review of the Do row and fileops
The canvas: a To .md ended any other way than its own Stop (the footer
Stop, a question, a drop, a daemon restart, an older daemon refusing the
command) left the card at Working forever. The footer Stop goes through
stopOp(), asking and dropping end the operation first, a disconnect ends
it, and an error while nothing else runs is taken as its outcome. Each
fileops run gets its own Process, so a stopped run's late answer is never
read as the next one's. To .md takes one PDF, as one file is written.
Compress... no longer flips back when file(1) refines a kind, long errors
are elided, and a desktop with no documents folder gets
Documents/Assistant rather than ~/Assistant.
fileops: an animated GIF to PNG or JPEG left one file per frame under
names the rename never found; the first frame is taken. JPEG flattens
transparency onto white. Zipping a folder holding the output folder read
its own growing archive without end; only regular files outside it are
added, which also skips FIFOs and dangling links. qpdf warnings no longer
fail a merge. A folder's free name keeps its dots, a lone .gz or .xz is
decompressed rather than refused, and a dotfile's archive is not hidden.
Tools die with fileops even when it is killed outright, a killed run's
leftovers are swept on the next run, a stop between fork and bookkeeping
cannot miss the child, and a late stop cannot add a second answer.
Error lines lose bsdtar's doubled prefix and its delayed-exit noise.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/assistant/Assistant.qml')
| -rw-r--r-- | desktop/assistant/Assistant.qml | 61 |
1 files changed, 42 insertions, 19 deletions
diff --git a/desktop/assistant/Assistant.qml b/desktop/assistant/Assistant.qml index b1329b1..f76c38d 100644 --- a/desktop/assistant/Assistant.qml +++ b/desktop/assistant/Assistant.qml @@ -73,7 +73,11 @@ Scope { readonly property bool busy: root.state !== "idle" // A daemon gone mid-reply sends no idle; its hello on return redraws. - onConnectedChanged: if (!root.connected) { root.state = "idle"; root.runningTool = ""; } + onConnectedChanged: if (!root.connected) { + root.state = "idle"; + root.runningTool = ""; + if (root.opRunning && root.opDaemon) { root.opRunning = false; root.opError = "assistant gone"; } + } // One row per turn. tools is a comma-joined string: a ListModel role // holding an array becomes a nested model, which a Repeater can read but @@ -117,6 +121,7 @@ Scope { function attach(paths) { const files = paths.filter(p => p !== "").map(Files.file); if (!files.length) return; + if (root.opRunning) root.stopOp(); // a drop is a new task root.fresh(); root.clearOp(); root.staged = files; @@ -151,31 +156,42 @@ Scope { root.send({ type: "write", files: paths, prompt: root.mdPrompt, dir: root.outDir, name: name }); } else { root.opDaemon = false; - ops.command = [root.fileops, o.op, root.outDir].concat(paths); - ops.running = false; - ops.running = true; + // A Process per run: a stopped run's late answer must not be + // read as the next run's. + root.opProc = opRun.createObject(root, { command: [root.fileops, o.op, root.outDir].concat(paths) }); } } + property var opProc: null + function stopOp() { if (!root.opRunning) return; if (root.opDaemon) root.send({ type: "stop" }); - else ops.running = false; // SIGTERM: fileops kills its tool, removes the partial + else if (root.opProc) { + root.opProc.running = false; // fileops kills its tool, removes the partial + root.opProc = null; + } root.opRunning = false; root.opError = "stopped"; } - Process { - id: ops - stdout: StdioCollector { - onStreamFinished: { - if (!root.opRunning || root.opDaemon) return; - root.opRunning = false; - try { - const r = JSON.parse(text.trim().split("\n").pop()); - if (r.ok) root.opOut = r.out; else root.opError = r.error; - } catch (e) { - root.opError = "fileops gave no answer"; + Component { + id: opRun + Process { + id: proc + running: true + stdout: StdioCollector { + onStreamFinished: { + if (root.opProc !== proc) { proc.destroy(); return; } // stopped, or superseded + root.opProc = null; + root.opRunning = false; + try { + const r = JSON.parse(text.trim().split("\n").pop()); + if (r.ok) root.opOut = r.out; else root.opError = r.error; + } catch (e) { + root.opError = "fileops gave no answer"; + } + proc.destroy(); } } } @@ -185,7 +201,11 @@ Scope { running: true command: ["xdg-user-dir", "DOCUMENTS"] stdout: StdioCollector { - onStreamFinished: if (text.trim().startsWith("/")) root.outDir = text.trim() + "/Assistant" + // With no documents folder set, xdg-user-dir answers $HOME itself. + onStreamFinished: { + const d = text.trim(); + if (d.startsWith("/") && d !== Quickshell.env("HOME")) root.outDir = d + "/Assistant"; + } } } @@ -232,7 +252,8 @@ Scope { // 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; + if (!root.connected || root.blocked || root.opRunning + || (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) }); @@ -324,7 +345,9 @@ Scope { if (root.opDaemon && root.opRunning) { root.opRunning = false; root.opOut = [e.path]; } break; case "error": - if (e.turn && root.opDaemon && root.opRunning) { + // A write's outcome. An older daemon answers it with a plain + // error and no turn; while nothing else runs, that is the write's. + if (root.opDaemon && root.opRunning && (e.turn || root.state === "idle")) { root.opRunning = false; root.opError = e.message; break; |
