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).
64 lines
2.1 KiB
TypeScript
64 lines
2.1 KiB
TypeScript
import type { ChatTurn, ThreadMessage } from "./types";
|
|
|
|
/**
|
|
* Flattens stored turns into the flat message list the transcript renders.
|
|
*
|
|
* Storage keeps a question and its answer together as a turn, because that is
|
|
* what gets replayed to the model. The transcript needs a flat sequence, newest
|
|
* message first. This is the one place that translation happens.
|
|
*/
|
|
|
|
export interface LiveState {
|
|
isStreaming: boolean;
|
|
/** Partial answer for the turn currently in flight. */
|
|
response: string;
|
|
thinking: string;
|
|
/** Activity line, e.g. "Searching the web". Only meaningful mid-turn. */
|
|
status?: string;
|
|
}
|
|
|
|
export function buildThread(turns: ChatTurn[], live: LiveState): ThreadMessage[] {
|
|
const messages: ThreadMessage[] = [];
|
|
|
|
// Strictly newest message first. The search bar sits at the top of the window,
|
|
// so the most recent thing, an answer once one has been written, should be
|
|
// the first line under it. A consequence is that within an exchange the answer
|
|
// comes before its question, which is the intended reading order here.
|
|
for (let index = turns.length - 1; index >= 0; index--) {
|
|
const turn = turns[index];
|
|
|
|
// Only the newest turn can be mid-flight.
|
|
const isLive = index === turns.length - 1 && live.isStreaming;
|
|
const text = isLive ? live.response : turn.response;
|
|
|
|
messages.push({
|
|
id: `${turn.id}:assistant`,
|
|
role: "assistant",
|
|
text,
|
|
createdAt: turn.createdAt,
|
|
isStreaming: isLive,
|
|
thinking: isLive ? live.thinking : turn.thinking,
|
|
toolCalls: turn.toolCalls,
|
|
error: isLive ? undefined : turn.error,
|
|
// Only while there is nothing to read yet. Once tokens land, the
|
|
// placeholder it feeds is gone anyway.
|
|
status: isLive && text.length === 0 ? live.status : undefined,
|
|
// Only a question carries attachments, so the answer never has one.
|
|
attachments: [],
|
|
});
|
|
|
|
messages.push({
|
|
id: `${turn.id}:user`,
|
|
role: "user",
|
|
text: turn.prompt,
|
|
createdAt: turn.createdAt,
|
|
isStreaming: false,
|
|
thinking: "",
|
|
toolCalls: [],
|
|
attachments: turn.attachments,
|
|
});
|
|
}
|
|
|
|
return messages;
|
|
}
|