From 607934e74ffabc1dcce72b29fc28e92a669e54ce Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 16 Oct 2026 16:41:51 +0200 Subject: feat(assistant): the Do row of file operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- desktop/assistant/Card.qml | 85 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 80 insertions(+), 5 deletions(-) (limited to 'desktop/assistant/Card.qml') diff --git a/desktop/assistant/Card.qml b/desktop/assistant/Card.qml index f63ce59..02ee476 100644 --- a/desktop/assistant/Card.qml +++ b/desktop/assistant/Card.qml @@ -9,10 +9,12 @@ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. +import Quickshell import QtQuick import ".." import "Files.js" as Files import "Presets.js" as Presets +import "Ops.js" as Ops // The card in its three states (Assistant.qml, mode): entry is the input // line alone; file adds the staged chips and what can be done with them; @@ -33,6 +35,8 @@ Rectangle { height: card.tall ? card.fullHeight : footer.height + 28 + (chips.visible ? chips.height + 10 : 0) + (asks.visible ? asks.height + 10 : 0) + + (dos.visible ? dos.height + 10 : 0) + + (status.visible ? status.height + 10 : 0) + (actions.visible ? actions.height + 10 : 0) Behavior on height { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } } clip: true @@ -153,7 +157,8 @@ Rectangle { id: chips visible: card.canvas.staged.length > 0 anchors { - bottom: asks.visible ? asks.top : actions.visible ? actions.top : footer.top + bottom: asks.visible ? asks.top : dos.visible ? dos.top : status.visible ? status.top + : actions.visible ? actions.top : footer.top bottomMargin: 10 left: parent.left; right: parent.right; leftMargin: 14; rightMargin: 14 } @@ -198,15 +203,14 @@ Rectangle { // --- what to do with them (file) --- // Ask: canned questions matching the staged kinds (Presets.js). A click - // sends one; Shift+click puts it in the input to edit first. The Do row - // (local operations) is its own spec and lands here too. + // sends one; Shift+click puts it in the input to edit first. Flow { id: asks readonly property var offered: Presets.offered(Presets.list(Prefs.presets), card.canvas.staged) - visible: card.mode === "file" && offered.length > 0 + visible: card.mode === "file" && offered.length > 0 && !card.canvas.opRunning anchors { - bottom: actions.top; bottomMargin: 10 + bottom: dos.visible ? dos.top : status.visible ? status.top : actions.top; bottomMargin: 10 left: parent.left; right: parent.right; leftMargin: 14; rightMargin: 14 } spacing: 8 @@ -230,6 +234,77 @@ Rectangle { } } + // Do: local operations (Ops.js, run by fileops or the daemon's write). + // Compress… swaps the row for its formats. Hidden while one runs. + property bool compressing: false + Connections { target: card.canvas; function onStagedChanged() { card.compressing = false; } } + + Flow { + id: dos + readonly property var offered: card.compressing + ? Ops.COMPRESS.concat([{ label: "Back", op: "back" }]) + : Ops.offered(card.canvas.staged) + visible: card.mode === "file" && offered.length > 0 && !card.canvas.opRunning + anchors { + bottom: status.visible ? status.top : actions.top; bottomMargin: 10 + left: parent.left; right: parent.right; leftMargin: 14; rightMargin: 14 + } + spacing: 8 + + Repeater { + model: dos.offered + Button { + required property var modelData + text: modelData.label + // Local operations need no daemon; To .md does. + enabled: modelData.op !== "md" || card.canvas.connected + onClicked: { + if (modelData.op === "compress") card.compressing = true; + else if (modelData.op === "back") card.compressing = false; + else { card.compressing = false; card.canvas.runOp(modelData); } + } + } + } + } + + // What the operation is doing, or what it made. + Row { + id: status + readonly property var c: card.canvas + visible: card.mode === "file" && (c.opRunning || c.opError !== "" || c.opOut.length > 0) + anchors { bottom: actions.top; bottomMargin: 10; left: parent.left; leftMargin: 14 } + spacing: 10 + + Text { + anchors.verticalCenter: parent.verticalCenter + text: status.c.opRunning + ? (status.c.opDaemon && status.c.opChars + ? `${status.c.opLabel}… ${(status.c.opChars / 1000).toFixed(1)}k characters` + : `Working on ${status.c.opLabel}…`) + : status.c.opError !== "" ? status.c.opError + : status.c.opOut.length === 1 ? `Saved ${status.c.opOut[0].slice(status.c.opOut[0].lastIndexOf("/") + 1)}` + : `Saved ${status.c.opOut.length} files` + color: status.c.opError !== "" && !status.c.opRunning ? Theme.red : Theme.text + font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } + } + Button { + visible: status.c.opRunning + text: "Stop" + danger: true + onClicked: status.c.stopOp() + } + Button { + visible: !status.c.opRunning && status.c.opOut.length === 1 + text: "Open" + onClicked: Quickshell.execDetached(["xdg-open", status.c.opOut[0]]) + } + Button { + visible: !status.c.opRunning && status.c.opOut.length > 0 + text: "Folder" + onClicked: Quickshell.execDetached(["xdg-open", status.c.outDir]) + } + } + Row { id: actions visible: card.mode === "file" -- cgit v1.2.3