Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104import type { components } from './api.gen';import { readHistory, type HistoryEntry } from './api';import { hydrate } from './transcript.svelte';
// The changes one turn made, read from its history: every tool call's trace// records each file it changed with the unified diff (robot-docs/web.md).
export type Change = Omit<components['schemas']['FileDiff'], 'kind'> & Partial<Pick<components['schemas']['FileUnavailable'], 'reason'>>;
/** One file's changes in a turn, in the order they were made. */export interface FileChanges { path: string; changes: Change[]; added: number; removed: number;}
/** * The turn's entries: from its first position until the next turn begins. * With `seek`, entries of other turns before it are skipped; without, the * first one ends the read, which is how a review asks what came since. */export async function readTurn(sessionId: string, turnId: string, from: number, seek = true): Promise<HistoryEntry[]> { const turn: HistoryEntry[] = []; let after = from - 1; for (;;) { const page = await readHistory(sessionId, { after }); for (const entry of page.items) { if (entry.position <= after) continue; if (entry.turn_id !== turnId) { if (turn.length || !seek) return turn; continue; } turn.push(entry); } if (!page.newer || !page.items.length) return turn; after = page.items.at(-1)!.position; }}
/** * Every change the turn's tool calls recorded, grouped by file. `read` keeps * results already filled in, so a review that follows a running turn fetches * only the new ones. */export async function turnChanges(entries: HistoryEntry[], read = new Map<string, HistoryEntry>()): Promise<FileChanges[]> { const results = await Promise.all( entries .filter((entry) => entry.kind === 'tool_result') .map(async (entry) => { const full = read.get(entry.id) ?? (await hydrate(entry)); read.set(entry.id, full); return full; }), ); const files = new Map<string, FileChanges>(); for (const entry of results) { for (const part of entry.content) { if (part.kind !== 'trace') continue; const trace = part.trace; for (const change of trace.changes ?? []) { const file = files.get(change.path) ?? { path: change.path, changes: [], added: 0, removed: 0 }; file.changes.push(change.kind === 'diff' ? change : { path: change.path, diff: '', added: 0, removed: 0, reason: change.reason }); if (change.kind === 'diff') { file.added += change.added; file.removed += change.removed; } files.set(change.path, file); } } } return [...files.values()];}
export type DiffLine = | { kind: 'hunk'; text: string } | { kind: 'add' | 'remove' | 'context'; text: string; old: number | null; new: number | null };
/** A unified diff as rows with both line numbers, skipping its file headers. */export function parseDiff(diff: string): DiffLine[] { const lines: DiffLine[] = []; let old = 0; let next = 0; for (const line of diff.split('\n')) { const hunk = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@(.*)$/.exec(line); if (hunk) { old = Number(hunk[1]); next = Number(hunk[2]); lines.push({ kind: 'hunk', text: hunk[3].trim() }); } else if (line.startsWith('---') || line.startsWith('+++') || line.startsWith('diff ') || line.startsWith('index ')) { continue; } else if (line.startsWith('+')) { lines.push({ kind: 'add', text: line.slice(1), old: null, new: next++ }); } else if (line.startsWith('-')) { lines.push({ kind: 'remove', text: line.slice(1), old: old++, new: null }); } else if (line.startsWith(' ') || (line === '' && lines.length)) { lines.push({ kind: 'context', text: line.slice(1), old: old++, new: next++ }); } } // A diff's trailing newline leaves one empty context row. if (lines.at(-1)?.kind === 'context' && (lines.at(-1) as { text: string }).text === '') lines.pop(); return lines;}