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.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798import { sendMessage, uuidv7 } from './api';import { artifacts } from './artifacts.svelte';import { sessions } from './sessions.svelte';import type { Board, NoteWindow } from './board.svelte';import { tidy } from './quote';
// Sending notes: each says what it was tied to, then what was written. Notes// for one session go as one message, after whatever the person typed; a note// tied to a chat window itself is just a message, written somewhere roomier,// and one tied to an artifact says which artifact and version it is about,// and which part of its page.
function quoted(quote: string): string { return tidy(quote) .split('\n') .map((line) => `> ${line}`) .join('\n');}
/** Which artifact a window shows, and the version: the one tied to, else the one on screen. */function artifactOf(board: Board, window: string, version?: number): string { const node = board.nodes.find((candidate) => candidate.id === window); if (node?.type !== 'artifact') return 'the artifact'; const shown = artifacts.get(node.data.artifact); const at = version ?? node.data.version ?? shown?.version; return `the artifact “${shown?.title ?? node.data.artifact}”${at ? ` (v${at})` : ''}`;}
function part(board: Board, note: NoteWindow): string { const { target, text } = note.data; if (!target) return text.trim(); if (target.kind === 'window') { const window = board.nodes.find((node) => node.id === target.window); return window?.type === 'artifact' ? `On ${artifactOf(board, target.window)}:\n\n${text.trim()}` : text.trim(); } if (target.kind === 'element' || target.kind === 'passage') return `On ${artifactOf(board, target.window, target.version)}, at \`${target.selector}\`:\n${quoted(target.quote)}\n\n${text.trim()}`; if (target.kind === 'entry' || target.kind === 'text') return `On this:\n${quoted(target.quote)}\n\n${text.trim()}`; const lines = target.start === target.end ? `${target.start}` : `${target.start}-${target.end}`; return `${target.path}:${lines}${target.side === 'deletions' ? ' (removed lines)' : ''}\n\`\`\`diff\n${target.quote}\n\`\`\`\n${text.trim()}`;}
/** What a window on the board is called where a note names it. */export function windowName(board: Board, window: string): string { const node = board.nodes.find((candidate) => candidate.id === window); if (!node || node.type === 'note') return 'a closed window'; const name = sessions.byId.get(node.data.sessionId)?.name || 'untitled session'; if (node.type === 'artifact') return `the artifact “${artifacts.get(node.data.artifact)?.title ?? '…'}”`; if (node.type === 'trace') return `the trace of ${name}`; if (node.type === 'agents') return `the agents of ${name}`; if (node.type === 'chip') return `the ${node.data.chip === 'model' ? 'context' : node.data.chip} of ${name}`; return node.type === 'review' ? `the review of ${name}` : node.type === 'terminal' ? `a terminal of ${name}` : name;}
/** Whether a note can be sent: it has words and is tied into some session. */export function sendable(board: Board, note: NoteWindow): boolean { return !!note.data.text.trim() && !!note.data.target && !!board.sessionOf(note.data.target.window);}
/** * The notes waiting to go with a session's next message, ones kept `alone` * among them: unsent, written, tied into any of its windows. Notes on the * chat itself come first, then each other window's in board order, each * window's from the top. */export function drafts(board: Pick<Board, 'nodes' | 'sessionOf'>, session: string): NoteWindow[] { const order = (window: string) => { const index = board.nodes.findIndex((node) => node.id === window); return board.nodes[index]?.type === 'session' ? -1 : index; }; return board.nodes .filter((node): node is NoteWindow => node.type === 'note' && !node.data.sent && !!node.data.text.trim() && !!node.data.target && board.sessionOf(node.data.target.window) === session) .sort((a, b) => order(a.data.target!.window) - order(b.data.target!.window) || a.position.y - b.position.y || a.position.x - b.position.x);}
/** One message: an opening line, then each note with what it is on. */export function compose(board: Board, notes: NoteWindow[], lead = ''): string { return [lead.trim(), ...notes.map((note) => part(board, note))].filter(Boolean).join('\n\n');}
/** Sends notes, one message per session, with an optional opening line; marks them sent. */export async function sendNotes(board: Board, notes: NoteWindow[], lead = ''): Promise<void> { const bySession = new Map<string, NoteWindow[]>(); for (const note of notes.filter((candidate) => sendable(board, candidate))) { const session = board.sessionOf(note.data.target!.window)!; bySession.set(session, [...(bySession.get(session) ?? []), note]); } for (const [session, group] of bySession) { await sendMessage(session, uuidv7(), compose(board, group, lead)); for (const note of group) board.updateNote(note.id, { sent: true, alone: false }); }}
/** Sends every note waiting on the board that is not kept alone, one message per session. */export function sendAllNotes(board: Board): Promise<void> { const sessions = new Set(board.nodes.flatMap((node) => (node.type === 'note' && node.data.target ? (board.sessionOf(node.data.target.window) ?? []) : []))); return sendNotes(board, [...sessions].flatMap((session) => drafts(board, session).filter((note) => !note.data.alone)));}