# 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 | | `Models.qml` | the model panel: local and cloud models, the chat's pick | | `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 `, 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). The header's model name (the Model button outside chat, or Ctrl+M) opens the model panel: the router's models, then each cloud provider's. A pick applies to this chat from its next question and is stored with it by the daemon; a new chat starts on the preferred model. Moving a chat with turns to a cloud model says once that its history goes to that provider. Voice is transcribed by the local model in a cloud chat too. 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, Ctrl+M the model 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).