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.
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.
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`.
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).