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.
157 lines
4.6 KiB
TypeScript
157 lines
4.6 KiB
TypeScript
/**
|
|
* Shared domain types for Lam Agent.
|
|
*
|
|
* These are deliberately split into two families:
|
|
*
|
|
* - `ApiMessage` and friends mirror exactly what Ollama's `/api/chat` accepts.
|
|
* - `Session`, `ChatTurn` and friends are Lam's own storage model.
|
|
*
|
|
* Keeping them separate matters because the storage model is derived from the
|
|
* API model, not identical to it: the tool-calling messages that Ollama needs
|
|
* mid-turn are not persisted, so a reloaded session replays cleanly as plain
|
|
* user/assistant pairs.
|
|
*/
|
|
|
|
export type ChatRole = "system" | "user" | "assistant" | "tool";
|
|
|
|
export interface ToolCall {
|
|
function: {
|
|
name: string;
|
|
arguments: Record<string, unknown>;
|
|
};
|
|
}
|
|
|
|
export interface ToolDefinition {
|
|
type: "function";
|
|
function: {
|
|
name: string;
|
|
description: string;
|
|
parameters: Record<string, unknown>;
|
|
};
|
|
}
|
|
|
|
/** A message in the shape Ollama's `/api/chat` endpoint expects. */
|
|
export interface ApiMessage {
|
|
role: ChatRole;
|
|
content: string;
|
|
tool_calls?: ToolCall[];
|
|
/** Only set on `role: "tool"` messages, naming the tool that produced this content. */
|
|
tool_name?: string;
|
|
/**
|
|
* Base64 image data, for vision models.
|
|
*
|
|
* Ollama only reads these on a `user` message (`gpt-oss` style tool roles have
|
|
* no images). They are the one genuinely large thing in a request, so they are
|
|
* attached to the question they belong to and never replayed later, the same
|
|
* way tool output is not replayed.
|
|
*/
|
|
images?: string[];
|
|
}
|
|
|
|
/**
|
|
* A file the user attached to a question.
|
|
*
|
|
* Only the path is stored. A screenshot runs to megabytes, and
|
|
* `MAX_STORED_CHARS_PER_TURN` exists precisely so one turn cannot dominate
|
|
* LocalStorage, so the bytes stay on disk and are read when the turn is sent.
|
|
*/
|
|
export interface Attachment {
|
|
/** Absolute path to the image on disk. */
|
|
path: string;
|
|
/** Sent to Ollama next to the base64 payload. */
|
|
mime: string;
|
|
/**
|
|
* Pixel dimensions, read from the file's header.
|
|
*
|
|
* Kept because the transcript has to lay the preview out long after the file
|
|
* was read, and Vicinae sizes an image's container to the request rather than
|
|
* to the image, which leaves dead space unless the request has the right
|
|
* aspect ratio. Absent for a format whose header we do not parse.
|
|
*/
|
|
width?: number;
|
|
height?: number;
|
|
}
|
|
|
|
/**
|
|
* An attachment that has been read and is ready to send, held in the view while
|
|
* the user types their question.
|
|
*
|
|
* The base64 payload lives here and nowhere else. It is needed for exactly one
|
|
* request, so it is deliberately not part of `Attachment` and never reaches
|
|
* storage.
|
|
*/
|
|
export interface PendingAttachment extends Attachment {
|
|
/** Base64 of the image, with no `data:` URL prefix. Ollama wants it bare. */
|
|
base64: string;
|
|
}
|
|
/** What a tool actually returned during a turn, kept for display and debugging. */
|
|
export interface ToolCallRecord {
|
|
name: string;
|
|
arguments: Record<string, unknown>;
|
|
result: string;
|
|
isError: boolean;
|
|
}
|
|
|
|
/** One question and its answer. The unit the transcript is rendered from. */
|
|
export interface ChatTurn {
|
|
id: string;
|
|
prompt: string;
|
|
response: string;
|
|
thinking: string;
|
|
model: string;
|
|
createdAt: number;
|
|
/** Set when the turn failed, so a stored session can show what went wrong. */
|
|
error?: string;
|
|
toolCalls: ToolCallRecord[];
|
|
/** Images the user attached, kept for the transcript. The bytes are not stored. */
|
|
attachments: Attachment[];
|
|
}
|
|
|
|
export interface Session {
|
|
id: string;
|
|
title: string;
|
|
model: string;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
turns: ChatTurn[];
|
|
}
|
|
|
|
/** The lightweight record kept in the session index, so listing sessions never loads full transcripts. */
|
|
export interface SessionSummary {
|
|
id: string;
|
|
title: string;
|
|
model: string;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
turnCount: number;
|
|
preview: string;
|
|
}
|
|
|
|
/** A model as reported by `GET /api/tags`. */
|
|
export interface ModelInfo {
|
|
name: string;
|
|
parameterSize?: string;
|
|
}
|
|
|
|
/**
|
|
* One rendered line of the conversation.
|
|
*
|
|
* A turn is stored as a question/answer pair, but the transcript is displayed as
|
|
* a flat sequence of messages so each side can carry its own icon and weight.
|
|
*/
|
|
export interface ThreadMessage {
|
|
id: string;
|
|
role: "user" | "assistant";
|
|
/** Full text, used for the detail panel and for copying. */
|
|
text: string;
|
|
createdAt: number;
|
|
isStreaming: boolean;
|
|
thinking: string;
|
|
toolCalls: ToolCallRecord[];
|
|
error?: string;
|
|
/** Activity line shown while this message is streaming, e.g. "Searching the web". */
|
|
status?: string;
|
|
/** Images attached to this question. Only ever non-empty on a user message. */
|
|
attachments: Attachment[];
|
|
}
|