import { sendMessage, uuidv7 } from './api'; import { artifacts } from './artifacts.svelte'; import { sessions } from './sessions.svelte'; import type { Board, NoteWindow } from './board.svelte'; import { place } from './format'; 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.windows[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.windows[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.windows[window]; if (!node || node.type === 'note') return 'a closed window'; const sid = node.data.sessionId; const name = (sid ? sessions.byId.get(sid)?.name : undefined) || '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}`; if (node.type === 'pty') return node.data.cwd ? `the terminal in ${place(node.data.cwd)}` : `the terminal 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, session: string): NoteWindow[] { const order = (window: string) => { const index = board.list.findIndex((node) => node.id === window); return board.list[index]?.type === 'session' ? -1 : index; }; const at = (note: NoteWindow) => board.placedOf(note.id) ?? board.floats.find((float) => float.tile.window === note.id) ?? { x: 0, y: 0 }; return board.list .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) || at(a).y - at(b).y || at(a).x - at(b).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 { const bySession = new Map(); 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 { const sessions = new Set(board.list.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))); }