1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
# assistant
The assistant canvas: a centred card on DP-1, on SUPER+Home, for typed
conversations with the local text assistant. It is only a view.
desktop-assistant (the repo next to this one) is the daemon that owns the
model, the tools and the history; this connects to its socket,
`$XDG_RUNTIME_DIR/desktop-assistant.sock`, draws what it is told and sends
what is typed. The protocol is specified in that repo,
`docs/superpowers/specs/2026-10-16-text-daemon-design.md`.
qs -p ./desktop ipc call assistant toggle
| File | Does |
| --- | --- |
| `Assistant.qml` | the socket, the turn model, the window |
| `Card.qml` | header, transcript, input, the history panel's place |
| `Message.qml` | one turn: question, thinking line, tool chips, reply |
| `Segments.qml` | markdown with a copy button per code block |
| `History.qml` | conversation list and full-text search |
| `Picker.qml` | the in-card file picker behind the paperclip |
| `Md.js` | splits a message into prose and fenced code |
| `Files.js` | a path's kind, and a dropped URL to a path |
| `Presets.js` | the Ask row's canned questions and which kinds they fit |
| `Ops.js` | the Do row's operations and which kinds they fit |
| `fileops` | runs a Do operation: `fileops <op> <outdir> <file...>`, one JSON line back |
Three states (`Assistant.qml`, `mode`): **entry**, one line, "How can I
help you?"; **file**, files staged and nothing asked yet, with what can be
done with them; **chat**, the transcript. SUPER+Home resumes a conversation
active in the last 10 minutes and otherwise starts a new one. A file
dropped on the island (`../island/`) starts a new one in the file state.
The paperclip opens a picker drawn in the card: a system dialog would open
beneath the overlay that holds the keyboard.
The mic records with `pw-record` into `$XDG_RUNTIME_DIR/assistant-mic.wav`
until clicked again (at most 59 s), then asks with it: the daemon has the
model transcribe it and answers the words. The mic is greyed out while the
model in use reports no `audio` input (`AssistantModule.hears`, from the
router's `/models`, read on every open).
Keys: Enter sends, Shift+Enter is a newline, Escape cancels a recording,
then closes the picker, the history panel, then the card. Ctrl+N starts a
new chat, Ctrl+H toggles the history panel. Stop replaces
Send while a reply streams.
Notes:
- **The state outlives the card.** The socket and the turn model live in
`Assistant.qml`, outside the window's loader, so closing the card loses
nothing, and a reply keeps streaming into it while it is closed.
- **Tokens are batched.** They collect in a buffer and reach the model on a
50 ms tick, so markdown is re-rendered twenty times a second at most
rather than per token.
- **Prose goes through `Notify.sanitize`**, which blocks remote images.
Model output can quote a search result, and an image URL in it would be
fetched the moment it rendered.
- **A code block's copy button appears once the fence closes.** A
half-streamed block is not the command the model meant.
- **The daemon may be down.** The card says so and disables the input; the
socket retries every 2 s with a fresh `Socket` (see AGENTS.md), logging a
`ServerNotFoundError` warning each time.
- **`tools` is a comma-joined string role.** A ListModel role holding an
array becomes a nested model, which `setProperty` cannot append to.
- **A reply that ends while the card is closed notifies.** Its action,
Open, brings the card back on that conversation. A stopped reply does
not notify; a failed one does, with the error as its body.
- **A stopped reply is shown, not kept.** The daemon does not save a
partial turn, so it is gone after the next reopen or restart.
- **Only an error marked `turn` ends a reply.** Every client sees every
event, so a command error (another canvas opening a deleted
conversation) shows above the input instead.
- **Search snippets** mark matches with `**`, rendered as bold; backticks
are stripped first, since a match inside a code span would show the
markers literally.
- **File kinds are decided twice.** `Files.js` decides what the card
offers; the daemon decides what chat accepts. Both go by the name first
and by the content when the name says nothing: the card runs
`file -b --mime-type` on staged "other" files, the daemon reads the first
4 KB. A script with no suffix is therefore code here and text there.
- **`socketPath`** is a property so a check can point the canvas at a
scratch daemon; the desktop uses the default.
- **Ask presets** live in `Prefs.qml` (`assistant.presets`) once saved and
are `Presets.js`'s defaults until then; an invalid saved list reads as the
defaults. There is no editor yet: a drawer settings page will call
`Prefs.setPresets`. Each prompt asks for an answer in the document's
language, since the model answers in the language it is asked in.
- **Do operations** write to `$(xdg-user-dir DOCUMENTS)/Assistant`, under
the first free name, through a temporary file renamed on success; Stop
sends SIGTERM and fileops kills its tool's process group and removes the
partial result. To .md goes through the daemon's `write` instead. This
ffmpeg has no libx264 and no AAC encoder: To MP4 is `h264_vaapi` with MP3
audio, and Extract audio copies AAC, MP3 and Opus tracks as they are.
Tests: `node desktop/assistant/test_md.js`, `test_files.js`,
`test_presets.js`, `test_ops.js`, and `/usr/bin/python3
desktop/assistant/test_fileops.py` (same directory).
|