The preview rendered, but with dead space around it. `MdImage.qml` sizes its
container to the request rather than to the image, so with no hint it is the
full pane width and 200px tall and the image is aspect-fitted inside that. A
wide capture sat in a letterbox: measured against the real captures in the
support directory, a 4:1 crop wasted about 106px of the 200px container and a
6.7:1 crop about 140px, which is what made the transcript look gappy and put
the caption far below the image.
- `readAttachment` now reads the PNG `IHDR` chunk, so `Attachment` carries the
real dimensions and they are persisted with the turn.
- `previewBox` computes an exact box from those dimensions, scaled to fit
400x200 with the scale capped at 1 so a small capture is not upscaled, and
`imageSource` emits it as the `raycast-width`/`raycast-height` hints
`parseImageSize` looks for. The path is resolved from the URL before the
query, so the hints never reach the file name.
- The alt text is now "Screenshot". It is drawn centred under the image, so the
sentence "Screenshot attached" read as a stray line of prose floating in the
middle of the pane.
- A pending note no longer stacks on top of the empty-transcript placeholder.
Both said the same thing, one line under the other, once a snip was attached.
A capture with no readable header simply falls back to the old sizing.
Verified: 293 tests, tsc, eslint, prettier, and vici build.
The transcript said "_Screenshot attached_" and left it at that, on a note in
the test file claiming Vicinae could not resolve a local image. It can. That
claim was true of a URL the model invented, not of a file this extension
captured itself.
Checked against the renderer rather than assumed:
- `imageProviderUrl` in `markdown-model.cpp` routes `file://`, and a
scheme-less string, through `ImageURL::local`. Anything it does not
recognise comes back empty, which is why a bare path is not good enough.
- `MdImage.qml` draws it block-level and centred, using the alt text as an
italic caption, and caps the rendered height at 200px regardless of the
`raycast-width`/`raycast-height` hint it also accepts.
So `attachments.ts` gains `imageSource`, which builds a `file://` URL with
`pathToFileURL` so a path with a space or non-ascii still resolves, and
`markdown.ts` gains `renderAttachments`, used for a turn's attachments and
for an unsent pending snip. That second use is the one that matters most:
the capture is now visible before the question about it exists.
The alt text carries the label, so a deleted file degrades to what the
transcript used to say. The 200px cap is a Vicinae limit, so the action panel
keeps Open Screenshot for the full image.
Tests that asserted the old behaviour are inverted, and imageSource is
covered for spaces and non-ascii.
Verified: 280 tests, tsc, eslint, prettier, and vici build.
Two changes: screenshot input gated on the model really reporting vision,
and a fix for URLs that arrived as code.
Links
- A fence whose body is all URLs, a lone `https://...` inline span, and a
line that is only a URL are rewritten into [url](url) markdown links.
Vicinae attaches only the table and strikethrough GFM extensions, so
autolink is off and a bare URL is dead text; clickability comes from
MarkdownModel turning CMARK_NODE_LINK into an <a href>. A fenced URL was
a dark panel with a Copy button and no way to open it.
- promoteStandaloneCodeSpans was itself promoting a lone URL span into a
fence, so it made this worse. Link restoration now runs first.
- The default prompt says never to fence a URL.
- Anything that is not one of those three shapes is copied through, so a
curl invocation containing an address is never rewritten.
Screenshots
- capture.ts shells out to an auto-detected tool: grimblast, grim + slurp,
spectacle, maim, scrot, flameshot, or screencapture on macOS. There is
no screenshot call in @vicinae/api and Clipboard moves no binary, so the
extension owns the capture and reads the file it wrote rather than
guessing at the clipboard. Cancel is detected by whether an image landed
on disk, because the tools disagree on exit codes.
- In chat: "Snip a Region" in the action panel (cmd+shift+S) attaches to
the current conversation. Standalone: "Snip to Lam" starts a new one.
- vision.ts caches the /api/show capability answer per host and model. A
failed check is "unknown", never "no", so a dropped connection is not
reported as a fact about the user's model. Actions still fail closed.
- Only the path is stored, base64 is read at send time, and the image goes
on its own turn only. Capture is never a model-callable tool.
Verified: 244 tests, tsc, eslint, and vici build (both entrypoints).