aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/assistant/Assistant.qml
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-16 16:41:51 +0200
committerDanilo M. <danix@danix.xyz>2026-10-16 16:41:51 +0200
commit607934e74ffabc1dcce72b29fc28e92a669e54ce (patch)
tree47ca9f67742c1892ff9197b5d4d31f6dd6534d36 /desktop/assistant/Assistant.qml
parent9a2a0c93cc30af979a26d957fef603e2378676be (diff)
downloadquickshell-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.qml84
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) {