Commit Graph
22 Commits
Author SHA1 Message Date
adrianbonpin 400f81fa6c fix: stop reporting a timezone's name as the user's city
get_location printed "City: Manila", derived from the last segment of the
IANA zone name. That is the timezone database's representative place, not
the user's: Asia/Manila is one zone covering the entire Philippines,
Asia/Kolkata covers all of India, Europe/Oslo all of Norway. It was wrong
for anyone outside the zone's namesake, and confidently so, because a model
asked about the weather uses it without hesitating. The README claimed the
value was region-level, so the output also contradicted its own docs.

- The zone's label is still reported, as `Timezone area`, and named for what
  it is. Both the field and the tool note say outright that it is not the
  user's city, and the tool description repeats it.
- The locale region subtag is now used, which is a declaration rather than an
  inference: a locale of en-PH is the Philippines because the user set it.
- A Location preference was added. It is the only exact place this extension
  has, it is reported verbatim and first, and when it is set the note tells
  the model to prefer it over anything inferred.
- The continent grouping from the zone name was dropped: "Asia" next to a
  region of "Philippines" reads as two answers to the same question.
- ToolContext takes the place as an injected value, keeping tools.ts free of
  Vicinae imports as the module wiring test requires.

Verified: 316 tests, tsc, eslint, prettier, and vici build.
2026-10-04 15:50:45 +08:00
adrianbonpin 35d88f635c 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.
2026-10-04 15:44:29 +08:00
adrianbonpin 2c0653c23c fix: lay the screenshot preview out to the image, not to the request
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.
2026-10-04 15:41:45 +08:00
adrianbonpin c1212192c5 feat: render the attached screenshot instead of just naming it
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.
2026-10-04 15:37:14 +08:00
adrianbonpin e00b49bbe5 fix: stop the chat view hanging on "Loading conversations…" after a snip
The hand-off launch deadlocked its own startup. `resumed` was initialised to
`Boolean(initialSession)`, which is false for a snip, and the idle-resume
effect returned early whenever there was a hand-off, so nothing ever set it.
The render gate needed both `resumed` and `prepared`, so a successful snip
left the view spinning and there was no way to reach the composer.

Two effects guarding two booleans had already produced one bug here, so this
fixes the shape rather than the value:

- One startup effect and one `startup` flag, cleared in a `finally`. Every
  branch ends in the same place, including a thrown one, so no path can leave
  the view loading.
- The decision tree moves to `lib/startup.ts` as a pure `resolveStartup`,
  which takes its storage and file reads as injected ports. It is now covered
  by 16 tests, including the case that broke: a hand-off must not fall through
  to the idle resume, and must still finish startup.
- `startup.ts` joins the guard that keeps the logic layer free of Vicinae
  imports.
- ChatView is prettier-clean; it had been carrying pre-existing drift and has
  been rewritten twice this session anyway.

Verified: 273 tests, tsc, eslint, prettier, and vici build.
2026-10-04 15:34:09 +08:00
adrianbonpin ada98968ac fix: tell a refused screen capture apart from a cancelled one
The snip failed with "could not create image from rect". That message is
macOS's signature for a denied Screen Recording permission, not a cancel,
and it is the only thing distinguishing the two: both write no file, and
screencapture -i has been reported to exit 0 when the permission is
missing. The previous code could not tell them apart, so a permission
problem would either look like Escape or, as here, surface as a bare
"Command failed".

- `RunCommand` now returns stdout and stderr, and `captureRegion`
  collects every message a tool produced, clean exit or not.
- Success is still decided by the file landing on disk, so a tool that
  warns on stderr and writes the image anyway is still a success.
- Failure is triaged from stderr: the macOS denial message becomes
  `CapturePermissionError` with the setting to change (TCC attributes the
  request to Vicinae, since it launched the extension runtime, so the
  permission belongs to Vicinae and needs an app restart). Any other
  complaint becomes a `CaptureToolError` rather than being swallowed.
  Silence with no image is a cancel, which is the common case.
- `describeCaptureFailure` in `lib/snip.ts` keeps both actionable hints,
  which the old catch clause dropped for everything but a missing tool.

Diagnosed from the process tree: the extension runtime (pid 66329) is a
child of Vicinae (pid 66327), so the grant goes on Vicinae.

Verified: 257 tests, tsc, eslint, prettier, and vici build.
2026-10-04 15:28:57 +08:00
adrianbonpin 5f359778b1 fix: take the launcher window out of the shot when snipping
The snip was selecting a region around the launcher panel sitting in the
middle of the screen. `activateEntrypoint` calls `showWindow()` when it
activates a command from a closed launcher, so a view command's window is
already up before any extension code runs. Capturing from inside the chat
view could never have worked.

- `snip` is now a `no-view` command. It calls `closeMainWindow()` first,
  waits out the 50ms hide that `ui-service.hpp` schedules, then captures
  with nothing on screen, then brings the window back by launching `chat`
  with the file. `settleWindow()` exists because a capture started the
  instant the close resolves could still find the window there.
- The chat's action no longer captures inline. It launches `snip` and
  passes the session id, so there is one capture path, one capability
  check, and one place that deals with the window.
- The hand-off relays through `snip` rather than going straight back to
  `chat` on purpose: `ActivateEntrypointOptions::toggleIfAlreadyActive`
  defaults to true, so launching the *already active* view command closes
  the window instead of reopening it. A launch cannot override that flag,
  but relaying through a different no-view command makes the toggle branch
  unreachable.
- The action is hidden while streaming, since the hand-off unloads the
  view and would abandon an answer mid-sentence.
- Vision is settled before the selector appears, so a model that cannot
  read images never costs a selection gesture. `no` and `unknown` get
  different HUD wording.
- `lib/snip.ts` holds the hand-off contract; `lib/hooks/useSnip.ts` is
  gone, as nothing captures from a view any more.

Verified: 252 tests, tsc, eslint, prettier, and vici build. The deployed
manifest now reports `snip mode=no-view`.
2026-10-04 15:26:55 +08:00
adrianbonpin 5e083d8142 feat: snip a region into chat, and make links clickable
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).
2026-10-04 15:13:15 +08:00
adrianbonpin f5d1ced4d6 chore: sweep em dashes from our own source and README (#13) 2026-10-03 22:03:00 +00:00
adrianbonpin 95edbcd20b fix: fence lone commands and give the prompt some warmth (#12) 2026-10-03 22:00:38 +00:00
adrianbonpin 75fc914eca feat: cmd+shift+delete deletes the highlighted conversation (#11) 2026-10-03 21:45:09 +00:00
adrianbonpin ac2da73669 feat: label code fences so Vicinae can colour them (#10) 2026-10-03 21:37:11 +00:00
adrianbonpin c49ffeb274 feat: strip speaker labels from the transcript, add a plain-prose prompt (#9) 2026-10-03 21:28:38 +00:00
adrianbonpin ee96f1ee61 feat: display toggles, smart session resume, fix stale transcript pane (#8) 2026-10-03 21:22:04 +00:00
adrianbonpin ffe2c6b7ed feat: newest message first, Raycast-style conversation sidebar (#7) 2026-10-03 21:15:44 +00:00
adrianbonpin 9b35fcef11 feat: conversations on the left, the transcript on the right (#6) 2026-10-03 21:09:36 +00:00
adrianbonpin d448a4377b feat: mark each speaker with a role icon in the transcript (#5) 2026-10-03 21:03:26 +00:00
adrianbonpin 31eb688455 feat: message-per-row transcript, session resume, drop the composer (#4) 2026-10-03 20:59:01 +00:00
adrianbonpin af0c75a65d style: strip the multi-line composer down to a bare editor (#3) 2026-10-03 20:45:29 +00:00
adrianbonpin 7afafdaf32 test: cover the Ollama web tools, mark trimmed snippets, new icon (#2) 2026-10-03 20:36:38 +00:00
adrianbonpin 8b507e7953 feat: initial Lam Agent extension (#1) 2026-10-03 20:32:40 +00:00
adrianbonpin 1d39aabb50 Initial commit 2026-10-03 20:11:02 +00:00