Files
lam-agent-vicinae/src/lib/types.ts
T
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

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[];
}