feat: mark each speaker with a role icon in the transcript

Nothing in the transcript said who was speaking. Each message is now
prefixed with a role marker: person for the user, sparkle for Lam.

Emoji rather than icon images, because Vicinae's markdown renderer is
minimal and there is no reliable way to inline an `icon://` reference
into markdown, whereas plain text always renders.

Also records in the README why the transcript needs both panes, since
that came up: Detail accepts no typed input, so a detail-only chat
command would have nothing to type into, and list rows are a fixed
single line, so full text cannot live in the list. Input needs a List,
full text needs a Detail.

Verified on screen that both markers render.
This commit is contained in:
2026-10-04 05:03:11 +08:00
parent 31eb688455
commit 03a7b30ebf
3 changed files with 30 additions and 2 deletions
+4
View File
@@ -150,6 +150,10 @@ A few decisions worth knowing about before you change things:
**Messages render oldest first, and the detail panel shows the whole thread.** Vicinae resets list selection to the first row on every model change, and streaming changes the model on every flush, so the selected row cannot be relied on to stay put while an answer arrives. Showing the thread keeps the live answer on screen regardless.
**Both panes are required, not a layout preference.** `Detail` accepts no typed input — `onSearchTextChange` exists only on `List`, `Grid`, `Form` and `Dropdown` — so a detail-only chat command would have nothing to type into. And list rows are a fixed single line, so full message text cannot render in the list. Input needs a `List`; full text needs a `Detail`. Dropping either pane breaks the feature.
**Speakers are marked with emoji, not icon images.** `👤` and `✨` prefix each message in the transcript. Vicinae's markdown renderer is minimal and there is no reliable way to inline an `icon://` reference into markdown, whereas plain text always renders.
**Streamed deltas are buffered in refs and flushed to state on a timer** (`FLUSH_INTERVAL_MS`). Re-rendering the transcript per token makes the launcher visibly janky.
**The logic layer has no Vicinae imports.** `agent.ts`, `ollama.ts`, `tools.ts` and friends are plain TypeScript, which is why they unit test without a runtime. A test enforces this.