Tier 0: render what the services already decided, honestly

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
This commit is contained in:
Gabriel Brown
2026-08-25 00:24:30 -04:00
parent be0e55214b
commit 8b1205e4b8
38 changed files with 1474 additions and 260 deletions
@@ -78,17 +78,74 @@ SettingsPage {
// runs exactly as the hotkey runs it, and the words arrive here. The helper
// is addressed through the path the Dictation service already publishes
// rather than one spelled again in this file.
// True only once the helper has confirmed a recording actually started.
//
// It used to be set on the press, which made it a claim rather than a fact:
// `panama-dictate start` refuses with {"ok":false,"error":"already-recording"}
// when the hotkey is already holding the microphone, and the field said
// "Listening…" over that refusal. Worse, the button then read "Stop and
// type it" -- so the next press stopped the hotkey's recording and typed
// somebody else's words into this test field.
property bool listening: false
// Why the last press did not do what it said, in this page's words.
property string testError: ""
// The helper answers with a code, not a sentence, so the words live here.
// Anything unrecognised is shown as itself rather than swallowed.
readonly property var dictateReasons: ({
"already-recording": "Something is already listening — the dictation hotkey, most likely. Let that finish first.",
"not-recording": "Nothing was listening, so there was nothing to type.",
"not-downloaded": "The speech model has not been downloaded yet.",
"no-image": "The speech server is not installed yet.",
"server-unavailable": "The speech server did not answer.",
"transcribe-failed": "That could not be transcribed.",
"too-short": "That was too short to transcribe.",
"no-speech": "Nothing was said.",
"deliver-failed": "The words could not be typed into the field.",
"unknown-command": "The dictation helper did not understand that."
})
Process {
id: dictateRun
// Which action this run was, so its reply can be read against it.
property string action: ""
stdout: StdioCollector { onStreamFinished: root.absorbDictate(this.text) }
onExited: (exitCode, exitStatus) => Dictation.refresh()
}
// The helper's reply decides what happened. A start that was refused leaves
// this page exactly as it was, with the refusal on screen.
function absorbDictate(text: string): void {
let reply = null;
try {
reply = JSON.parse(text);
} catch (error) {
reply = null;
}
const ok = reply?.ok === true;
root.listening = dictateRun.action === "start" && ok;
if (ok) {
root.testError = "";
return;
}
const code = String(reply?.error ?? "");
if (code === "")
root.testError = "The dictation helper did not answer.";
else
root.testError = root.dictateReasons[code] !== undefined
? String(root.dictateReasons[code])
: code;
}
function runDictate(action: string): void {
if (dictateRun.running)
return;
root.testError = "";
dictateRun.action = action;
dictateRun.command = [Dictation.helper, action];
dictateRun.running = true;
}
@@ -181,7 +238,11 @@ SettingsPage {
SettingRow {
label: "Try it"
detail: "Speak a sentence and it is typed into the field here, rather than into whatever you were working on"
// The refusal takes the detail's place while there is one, so a
// press that did nothing says so where the press happened.
detail: root.testError !== ""
? root.testError
: "Speak a sentence and it is typed into the field here, rather than into whatever you were working on"
controlWidth: 340
Row {
@@ -226,19 +287,27 @@ SettingsPage {
SettingsButton {
anchors.verticalCenter: parent.verticalCenter
text: root.listening ? "Stop and type it" : "Test dictation"
// Four states, because stopping is not instant: the helper
// transcribes before it types, and a button still reading
// "Stop and type it" through those seconds invites a second
// press for a stop that has already been asked for.
text: {
if (dictateRun.running)
return root.listening ? "Transcribing…" : "Starting…";
return root.listening ? "Stop and type it" : "Test dictation";
}
tone: root.listening ? "accent" : "normal"
enabled: !dictateRun.running
onClicked: {
// Focus first, and keep it: the helper types with wtype
// into whatever holds keyboard focus when it finishes.
heard.forceActiveFocus();
if (root.listening) {
root.listening = false;
root.runDictate("stop");
return;
}
heard.text = "";
root.listening = true;
// `listening` is set by the reply, not by this press.
root.runDictate("start");
}
}