diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-16 16:41:51 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-16 16:41:51 +0200 |
| commit | 607934e74ffabc1dcce72b29fc28e92a669e54ce (patch) | |
| tree | 47ca9f67742c1892ff9197b5d4d31f6dd6534d36 /desktop/assistant/Assistant.qml | |
| parent | 9a2a0c93cc30af979a26d957fef603e2378676be (diff) | |
| download | quickshell-607934e74ffabc1dcce72b29fc28e92a669e54ce.tar.gz quickshell-607934e74ffabc1dcce72b29fc28e92a669e54ce.zip | |
feat(assistant): the Do row of file operations
Conversions, extraction, merge and compression on the staged files from
the file state, one at a time, with Stop; Compress… opens its formats.
To .md goes through the daemon's write, the rest through fileops. The
card keeps the result line, Saved name with Open and Folder, or the
error, so another operation can follow on the same files. Results land in
Documents/Assistant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/assistant/Assistant.qml')
| -rw-r--r-- | desktop/assistant/Assistant.qml | 84 |
1 files changed, 84 insertions, 0 deletions
diff --git a/desktop/assistant/Assistant.qml b/desktop/assistant/Assistant.qml index 0139ef9..b1329b1 100644 --- a/desktop/assistant/Assistant.qml +++ b/desktop/assistant/Assistant.qml @@ -41,6 +41,22 @@ Scope { // 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" + // The Do row's operation: one at a time, either fileops (a Process) or + // the daemon's write (To .md). Its result stays on the card. + property string opLabel: "" + property bool opRunning: false + property bool opDaemon: false + property int opChars: 0 + property var opOut: [] + property string opError: "" + // Results folder: $(xdg-user-dir DOCUMENTS)/Assistant, read once. + property string outDir: (Quickshell.env("HOME") || "") + "/Documents/Assistant" + readonly property string fileops: + decodeURIComponent(Qt.resolvedUrl("fileops").toString().replace(/^file:\/\//, "")) + readonly property string mdPrompt: "Rewrite this document as Markdown: keep its wording; " + + "use headings, lists and tables where the original has them; transcribe scanned " + + "pages. Output only the Markdown." + // A staged kind chat does not take holds the question back. readonly property bool blocked: root.staged.some(f => !Files.chats(f.kind)) @@ -102,6 +118,7 @@ Scope { const files = paths.filter(p => p !== "").map(Files.file); if (!files.length) return; root.fresh(); + root.clearOp(); root.staged = files; root.historyOpen = root.pickerOpen = false; root.open = true; @@ -114,6 +131,64 @@ Scope { root.refine(); } + function clearOp() { + if (root.opRunning) return; + root.opLabel = root.opError = ""; + root.opOut = []; + root.opChars = 0; + } + + function runOp(o) { + if (root.opRunning || !root.staged.length) return; + root.clearOp(); + root.opLabel = o.label; + root.opRunning = true; + const paths = root.staged.map(f => f.path); + if (o.op === "md") { + if (!root.connected) { root.opRunning = false; root.opError = "assistant not running"; return; } + root.opDaemon = true; + const name = root.staged[0].name.replace(/\.[^.]*$/, "") + ".md"; + 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; + } + } + + 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 + 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"; + } + } + } + } + + Process { + running: true + command: ["xdg-user-dir", "DOCUMENTS"] + stdout: StdioCollector { + onStreamFinished: if (text.trim().startsWith("/")) root.outDir = text.trim() + "/Assistant" + } + } + // A file whose name says nothing (a script with no suffix) is "other" // until file(1) has read it. One run for all of them; the daemon makes // its own call on the content when the question is sent. @@ -244,7 +319,16 @@ Scope { if (!e.stopped) root.convId = e.id; root.lastActive = Date.now(); break; + case "progress": if (root.opDaemon) root.opChars = e.chars; break; + case "written": + if (root.opDaemon && root.opRunning) { root.opRunning = false; root.opOut = [e.path]; } + break; case "error": + if (e.turn && root.opDaemon && root.opRunning) { + root.opRunning = false; + root.opError = e.message; + break; + } // Only an error marked turn ended the reply. A command error, // perhaps another client's, leaves a streaming reply alone. if (e.turn && i >= 0 && turns.get(i).live) { |
