feat: let the model answer with tables, lists and headings
The model refused to write a table and described one instead: a bulleted list of rows with the column names in inline code. That was our instruction, not a rendering limit. The default prompt asked for brevity in a narrow window and then said "skip large headings and wide tables", so the model obeyed and flattened the answer. Vicinae renders tables properly. The screenshot that prompted this is a rendered one: `MdTable.qml` is wired to block type 5, and `markdown-model.cpp` attaches the `table` GFM extension. Nothing was missing. - The prompt now names what actually renders (tables, bulleted and numbered lists, headings, blockquotes, bold, italics, links, code) and asks for the shape that fits, with a cap of about four narrow columns because the detail pane really is narrow. It also says outright not to hand back a table as a bulleted list of rows. - `separateTables` adds the blank line a table needs in front of it. GFM tables cannot interrupt a paragraph, so a table written straight under a line of prose renders as a paragraph of pipes and dashes. Only a genuine table start is touched, which is a row followed by a delimiter row; prose with a pipe, a table already at a block boundary, and a fenced example are all left alone. - A regression test asserts a real table survives the whole repair pass untouched: pipes are not fences, not links, and not lone spans. Verified: 304 tests, tsc, eslint, prettier, and vici build.
This commit is contained in:
@@ -15,6 +15,7 @@ Hit a global shortcut, type a question, get a streamed answer without leaving wh
|
||||
- **Streaming answers.** Tokens render as they arrive, in a markdown side panel.
|
||||
- **Persistent conversations.** Every chat is saved locally. The sidebar lists them by recency, and reopening the command picks up where you left off, unless it has been idle longer than you allow.
|
||||
- **Readable code.** Fenced blocks render in a dark panel with the language named in a header, syntax colouring, and a Copy button.
|
||||
- **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.
|
||||
- **Tools.** Lam decides when to call them: current time, location context, web search, and page fetching.
|
||||
- **Screenshots, two ways.** Snip a region into the conversation you are already in from the action panel, or run **Snip to Lam** to start a new chat about it. The launcher window gets out of the way first, so it never ends up in the shot, and the capture appears in the transcript as a thumbnail with Open Screenshot for the full image. Neither is offered unless the current model can actually see images.
|
||||
- **Chat only.** Lam answers questions. It does not touch your files or run commands. The one exception is capture, and it only ever happens because you pressed the key for it.
|
||||
@@ -199,6 +200,10 @@ The rendered height stays capped at 200px whatever is asked for (`Math.min(_rawH
|
||||
|
||||
**A URL is only clickable as a markdown link, so `code.ts` takes them back out of code.** Vicinae's parser attaches the `table` and `strikethrough` GFM extensions and nothing else, so the `autolink` extension is off: a bare URL is plain dead text. Clickability comes from `MarkdownModel` turning `CMARK_NODE_LINK` into an `<a href>`, which the selection controller opens on release. A URL the model fenced is therefore the worst possible presentation, a dark panel with a Copy button and no way to open the address. `restoreUrlLinks` recovers three narrow shapes (a fence whose entire body is URLs, a lone `` `https://...` `` span, and a line that is only a URL) into `[url](url)` links, and the default prompt now says never to fence a URL in the first place. Anything that is not one of those three shapes is copied through, so a `curl` invocation containing an address is never rewritten.
|
||||
|
||||
**A table needs a blank line in front of it, and the prompt alone does not guarantee one.** Vicinae renders tables properly (`MdTable.qml`, block type 5), but GFM tables cannot interrupt a paragraph, so a table written directly under a line of prose is not a table at all: it renders as a paragraph of pipes and dashes. The blank line looks unnecessary, so models leave it out. `separateTables` adds it, and only in front of a genuine table start, which is a row followed by a delimiter row. Prose containing a pipe is left alone, a table already at a block boundary is left alone, and a fenced example is never touched.
|
||||
|
||||
**The prompt used to forbid tables, which is worth not repeating.** It asked for brevity in a narrow window and then said "skip large headings and wide tables", and a model follows that instruction by describing the table it was about to write instead of writing it: a bulleted list of rows, with the column names in inline code. Brevity is about length, not about which markdown is allowed, so the guidance now names what renders and asks for the shape that fits, with a cap of about four narrow columns because the detail pane really is narrow.
|
||||
|
||||
**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.
|
||||
|
||||
Reference in New Issue
Block a user