feat: snip a region into chat, and make links clickable (#14)

This commit was merged in pull request #14.
This commit is contained in:
2026-10-05 08:38:50 +00:00
parent f5d1ced4d6
commit 576c5946f7
44 changed files with 3890 additions and 120 deletions
+91
View File
@@ -0,0 +1,91 @@
import { LaunchType, closeMainWindow, launchCommand, showHUD, type LaunchProps } from "@vicinae/api";
import { captureRegion } from "./lib/capture";
import { getLamPreferences } from "./lib/preferences";
import { describeCaptureFailure, readSnipContext, snipDirectory } from "./lib/snip";
import { visionSupport } from "./lib/vision";
/**
* Vicinae's `closeMainWindow` defers the actual hide to a 50ms timer
* (`closeWindow(settings, 50ms)` in `ui-service.hpp`), so a capture started the
* instant the call resolves can still find the window on screen and select a
* region around it. Waiting a little past that timer is the difference between
* "the window is closed" and "the window is usually closed".
*/
const WINDOW_SETTLE_MS = 100;
function settleWindow(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, WINDOW_SETTLE_MS));
}
/**
* Entrypoint for the "snip" command.
*
* `no-view`, and it has to be. Vicinae calls `showWindow()` when it activates a
* command from a closed launcher, so the window is up by the time any extension
* code runs; capturing from a view means selecting a region around a panel
* sitting in the middle of the screen. This command closes the window first,
* captures with nothing in the way, and only then hands the image to the chat,
* which is the command that brings the window back.
*
* It is also the *only* capture path. The chat's own action launches this command
* rather than capturing inline, so there is one implementation, one capability
* check, and one place where the window is dealt with.
*
* Bind it to a global shortcut for one-key "snip and ask".
*/
export default async function SnipCommand(props: LaunchProps) {
const preferences = getLamPreferences();
const context = readSnipContext(props.launchContext);
// Before anything else. Everything below is pointless if the selector is
// competing with a window for the middle of the screen.
await closeMainWindow();
await settleWindow();
// Settled before the capture, because taking a screenshot for a model that
// cannot read it throws away the selection gesture the user just performed.
// "unknown" says so in those words: a dropped check is not a verdict on the
// model, and reporting it as one would send the user shopping for another.
const vision = await visionSupport({
host: preferences.host,
apiKey: preferences.apiKey,
model: preferences.model,
});
if (vision !== "yes") {
await showHUD(
vision === "no"
? `${preferences.model} cannot see images. Pick a vision model in preferences.`
: `Could not check whether ${preferences.model} can see images. Run Set Up Lam Agent.`,
);
return;
}
let snipPath: string | undefined;
let snipError: string | undefined;
try {
const capture = await captureRegion({ outputDir: snipDirectory() });
snipPath = capture?.path;
} catch (thrown) {
snipError = describeCaptureFailure(thrown);
}
// An abandoned selection says nothing at all: the user pressed Escape. But a
// snip started from an open conversation has to come back, because the window
// is now closed and the hotkey is otherwise the only way back.
if (!snipPath && !snipError && !context.sessionId) return;
// Only the fields that exist are sent. `launchCommand` types its context as a
// string map, and a key left undefined carries no information anyway.
const payload: Record<string, string> = {};
if (context.sessionId) payload.sessionId = context.sessionId;
if (snipPath) payload.snipPath = snipPath;
if (snipError) payload.snipError = snipError;
await launchCommand({
name: "chat",
type: LaunchType.UserInitiated,
context: payload,
});
}