feat: source citations with footnotes in the transcript (#21)

This commit was merged in pull request #21.
This commit is contained in:
2026-10-07 08:12:48 +00:00
parent 72a63e69e6
commit fcc6d26c9d
16 changed files with 2716 additions and 205 deletions
+9
View File
@@ -18,6 +18,7 @@ Hit a global shortcut, type a question, get a streamed answer without leaving wh
- **Real markdown.** Tables, bulleted and numbered lists, headings, blockquotes, bold, italics and links all render natively, and the default prompt asks for the shape that fits the answer rather than flattening everything into prose.
- **Maths, real HTML, and diagrams.** LaTeX renders, so an equation arrives as an equation instead of a wall of backslashes. A small amount of HTML draws as well, for the status chip or tiny grid a sentence is worse at, and a diagram drawn with box characters keeps its alignment instead of being reflowed. See [Rendering](#rendering).
- **Tools.** Lam decides when to call them: current time, location context, web search, and page fetching. A turn that uses up its tool budget still ends in an answer, written from what it gathered, rather than a notice that it stopped.
- **Sources you can check.** A search result and a fetched page each get a number, and those numbers are the ones Lam cites, so `[1]` in the answer and `[1]` under it are the same page. The list sits under the answer it belongs to, each number opens the page it names, and a number Lam was never shown stays as plain text rather than becoming a link to somewhere else.
- **Tool calls you can watch and stop.** What Lam is doing right now stays on screen, including when it asks the same question twice, the round limit is a preference, Force Stop is always one keystroke away, and a call that repeats itself with identical arguments ends the turn instead of running forever.
- **Models, from a list.** Browse every model your host can reach, pick one and start using it immediately, and on a server you run, pull new ones or delete old ones. Tag names are picked, never typed.
- **Optional auto-deletion.** Set a retention window and Lam clears out the conversations you have stopped using the next time you open a command, taking their screenshots with them and collecting any capture you never sent. Off by default, and nothing at all is read or written while it is off. The setup screen also has both tidy-ups as explicit actions, confirmed first, for when you would rather press the button yourself.
@@ -295,6 +296,14 @@ The rest of the pass is defensive, and stays away from guessing what the model m
The whole pass is plain TypeScript with no Vicinae imports, so it unit tests without a runtime, and copying a conversation to the clipboard uses the source text rather than the rendering.
### Sources
Footnotes deserve a note of their own, because the window cannot draw them. Vicinae parses markdown with `CMARK_OPT_DEFAULT` and attaches only the table and strikethrough extensions, so a `[^1]` arrives as literal text. The citation pass therefore does the whole job: it links the marker, drops a footnote definition that would only repeat an entry the tools already registered, and builds the list from those entries, which is also what stops the list from being something a model can quietly invent.
Numbers are handed out where the tools run, once per turn, and printed in the tool result the model reads. So `[3]` means one page for the model and for the reader, three searches in one turn do not each start again at `1`, and a page found by a search and read again later keeps the number it already had. A marker stays as written unless every number in it is a source with a destination this window should open: half a link reads as a citation that works, and a `javascript:` address out of a search result is a real vulnerability rather than a rendering quirk.
The list shows the sources the answer cited, falling back to everything the turn consulted when it cited nothing, since that is the one case where the list is the only evidence of what was read. It waits for a citation before appearing at all, so it never arrives in full and then shrinks.
## Architecture
```