aboutsummaryrefslogtreecommitdiffstats
path: root/desktop/assistant/Assistant.qml
diff options
context:
space:
mode:
Diffstat (limited to 'desktop/assistant/Assistant.qml')
-rw-r--r--desktop/assistant/Assistant.qml66
1 files changed, 63 insertions, 3 deletions
diff --git a/desktop/assistant/Assistant.qml b/desktop/assistant/Assistant.qml
index 340c0cf..865f157 100644
--- a/desktop/assistant/Assistant.qml
+++ b/desktop/assistant/Assistant.qml
@@ -79,6 +79,7 @@ Scope {
onConnectedChanged: if (!root.connected) {
root.state = "idle";
root.runningTool = "";
+ root.endRecording(false);
if (root.opRunning && root.opDaemon) { root.opRunning = false; root.opError = "assistant gone"; }
}
@@ -107,10 +108,14 @@ Scope {
root.open = true;
}
- function close() { root.open = false; root.historyOpen = false; root.pickerOpen = false; }
- // Escape: the picker, then the history panel, then the card.
+ function close() {
+ root.endRecording(false);
+ root.open = false; root.historyOpen = false; root.pickerOpen = false;
+ }
+ // Escape: a recording, the picker, then the history panel, then the card.
function back() {
- if (root.pickerOpen) root.pickerOpen = false;
+ if (root.recording) root.endRecording(false);
+ else if (root.pickerOpen) root.pickerOpen = false;
else if (root.historyOpen) root.historyOpen = false;
else root.close();
}
@@ -200,6 +205,61 @@ Scope {
}
}
+ // The mic: pw-record into one file, overwritten by each recording and
+ // never deleted (the runtime dir goes at logout). The daemon has the
+ // model transcribe it. Spec: docs/superpowers/specs/2026-10-19-assistant-mic-design.md.
+ readonly property string micPath: `${Quickshell.env("XDG_RUNTIME_DIR") || "/tmp"}/assistant-mic.wav`
+ property var mic: null // the recording's Process, null when not recording
+ property int micSecs: 0
+ readonly property bool recording: root.mic !== null
+
+ function record() {
+ if (root.recording || !root.connected || root.busy || root.opRunning || root.blocked) return;
+ root.notice = "";
+ root.micSecs = 0;
+ root.mic = micRun.createObject(root);
+ }
+
+ // SIGTERM: pw-record finishes the header, then exited sends the ask.
+ // ponytail: a recording started before the last one's exit shares the
+ // file; pw-record exits within milliseconds of the signal.
+ function endRecording(send) {
+ if (!root.mic) return;
+ root.mic.send = send;
+ root.mic.running = false;
+ root.mic = null;
+ }
+
+ // 59, not 60: the daemon refuses over 60 s by file size, and the stop
+ // lands a little after the tick.
+ Timer {
+ running: root.recording
+ interval: 1000
+ repeat: true
+ onTriggered: if (++root.micSecs >= 59) root.endRecording(true);
+ }
+
+ Component {
+ id: micRun
+ Process {
+ id: rec
+ property bool send: false
+ running: true
+ command: ["pw-record", "--rate", "16000", "--channels", "1", "--format", "s16", root.micPath]
+ onExited: {
+ if (root.mic === rec) { // ended on its own: no mic, PipeWire down
+ root.mic = null;
+ root.notice = "recording failed";
+ } else if (rec.send && root.connected) {
+ root.chatting = true;
+ root.send({ type: "ask", text: "", audio: root.micPath,
+ files: root.staged.map(f => f.path) });
+ }
+ rec.destroy();
+ }
+ }
+ }
+
Process {
running: true
command: ["xdg-user-dir", "DOCUMENTS"]