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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326<script lang="ts"> import type { Session } from '../api'; import { summarize, type Step } from '../burst'; import { jsonField, loadedIn, madeIn, type Block, type Made, type Transcript } from '../transcript.svelte'; import Markdown from './Markdown.svelte'; import ToolStep from './ToolStep.svelte'; import { traceLink } from '../tracelink.svelte'; import { scrollToShow } from '../anchors';
// A turn's thinking and tool use between two messages, read as one line of // what it did, the way the terminal writes it. The run still going stays // open, with albedo's face, so it can be watched. let { block, steps, live, transcript, session, onreview, onartifact, }: { /** The thread's key for this stretch, which the chat finds its place by. */ block: string; steps: Block[]; live: boolean; transcript: Transcript; session: Session | undefined; onreview: (turnId: string, from: number, path?: string) => void; onartifact: (made: Made) => void; } = $props();
let chosen = $state<boolean | null>(null); let element = $state<HTMLElement>(); // Thoughts show their first lines; a click shows the rest, and keeps this // stretch open when a tool call or prose follows. let whole = $state(new Set<number>()); const open = $derived(chosen ?? (live || whole.size > 0)); const keyed = $derived.by(() => { let thoughts = 0; return steps.map((step) => (step.kind === 'thinking' ? { step, key: `thought:${thoughts}`, thought: thoughts++ } : { step, key: step.key, thought: -1 })); }); const tools = $derived(steps.flatMap((step) => (step.kind === 'tool' ? [step.entry] : []))); const summary = $derived( summarize( steps.flatMap((step): Step[] => { if (step.kind === 'thinking') return [{ kind: 'thinking', ms: step.ms }]; if (step.kind === 'tool') return [{ kind: 'tool', name: step.name, title: step.entry.tool?.title, args: jsonField(step.entry, 'arguments') as Record<string, unknown> | undefined, trace: step.trace, failed: step.failed }]; return []; }), session?.workspace ?? '', ), );
// What a call touched lives in its trace, which a page may only reference; // fetch it once the line comes into view. $effect(() => { if (!element) return; const observer = new IntersectionObserver(([entry]) => entry.isIntersecting && transcript.expand(...tools), { rootMargin: '200px' }); observer.observe(element); return () => observer.disconnect(); });
// The counts open the turn's review, on the first file this stretch changed. const turnId = $derived(tools.find((entry) => entry.turn_id)?.turn_id ?? null); function review() { const from = turnId ? transcript.turnStart(turnId) : null; const path = steps.flatMap((step) => (step.kind === 'tool' ? (step.trace?.changes ?? []) : [])).find((change) => change.path)?.path; if (turnId && from !== null) onreview(turnId, from, path); }
// Each artifact this stretch made or revised, at its newest version, as a // line that opens it. const made = $derived.by(() => { const newest = new Map<string, Made>(); for (const step of steps) if (step.kind === 'tool') for (const item of madeIn(step.trace)) newest.set(item.id, item); return [...newest.values()]; });
// Each skill this stretch's cells loaded, once. const loaded = $derived([...new Set(steps.flatMap((step) => (step.kind === 'tool' ? loadedIn(step.trace) : [])))]);
// A call picked in a trace opens this stretch and is brought into the chat's view. $effect(() => { const entry = traceLink.reveal; if (!entry || !tools.some((tool) => tool.id === entry)) return; chosen = true; traceLink.reveal = null; requestAnimationFrame(() => { const step = element?.querySelector<HTMLElement>(`[data-entry="${CSS.escape(entry)}"]`); const scroller = step?.closest<HTMLElement>('.scroller'); if (step && scroller) scrollToShow(scroller, step.getBoundingClientRect()); }); });
function toggle(thought: number) { const next = new Set(whole); if (!next.delete(thought)) next.add(thought); whole = next; }
function progressLabel(step: { name: string; title?: string | null; preview: string }): string { if (step.title) return step.title; return step.preview.split('\n').find((line) => line.trim())?.trim() ?? ''; }</script>
<div class="work" bind:this={element} data-block={block} data-tools={tools.map((tool) => tool.id).join(' ')}> <div class="head"> <button class="summary" onclick={() => (chosen = !open)} aria-expanded={open} title={summary.text}> <svg class="chevron" class:open viewBox="0 0 16 16" width="12" height="12" aria-hidden="true"><path d="M6 4l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" /></svg> <span class="line">{summary.text || 'worked'}</span> {#if summary.failed}<span class="failed">{summary.failed} failed</span>{/if} </button> {#if summary.added || summary.removed} <button class="counts" onclick={review} title="show these changes in the review"><span class="added">+{summary.added}</span> <span class="removed">−{summary.removed}</span></button> {/if} </div> {#each made as item (item.id)} <button class="artifact" onclick={() => onartifact(item)} title="open {item.title}"> <svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><rect x="2" y="3" width="12" height="10" rx="2" fill="none" stroke="currentColor" stroke-width="1.4" /><path d="M2 6h12" stroke="currentColor" stroke-width="1.4" /></svg> <span class="verb">{item.version === 1 ? 'made' : 'updated'}</span> <span class="title">{item.title}</span> {#if item.version > 1}<span class="version">v{item.version}</span>{/if} </button> {/each} {#each loaded as name (name)} <p class="skill" title="loaded the {name} skill"><span class="verb">skill</span> · <span class="name">{name}</span></p> {/each} {#if open} <div class="steps"> {#each keyed as { step, key, thought } (key)} {#if step.kind === 'thinking'} <button class="thought" data-entry={step.key} class:whole={whole.has(thought)} onclick={() => toggle(thought)}><Markdown text={step.text} /></button> {:else if step.kind === 'tool'} <ToolStep entry={step.entry} name={step.name} done={step.done} {transcript} /> {:else if step.kind === 'progress'} <p class="running"><span class="spinner" aria-label="running"></span><span class="name">{step.name}</span><span class="preview">{progressLabel(step)}</span></p> {/if} {/each} </div> {/if}</div>
<style> .summary { display: flex; align-items: center; gap: 6px; width: 100%; min-width: 0; text-align: left; padding: 2px 6px; margin: 0 -6px; color: var(--muted); font: inherit; font-size: calc(12.5px * var(--canvas-scale)); background: none; border: 0; border-radius: 6px; cursor: pointer; transition: background-color 100ms ease, color 100ms ease; } .summary:hover { color: var(--text); background: var(--hover); } .chevron { transition: transform 160ms cubic-bezier(0.23, 1, 0.32, 1); } .chevron.open { transform: rotate(90deg); } .chevron { flex: none; } .line { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .failed { flex: none; color: var(--danger); } .head { display: flex; align-items: center; gap: 2px; min-width: 0; } .head .summary { flex: 1; width: auto; } .counts { flex: none; padding: 2px 6px; font: inherit; font-variant-numeric: tabular-nums; background: none; border: 0; border-radius: 6px; cursor: pointer; transition: background-color 120ms ease; } .counts:hover { background: var(--hover); } .added { color: var(--added); } .removed { color: var(--danger); } .steps { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2px; margin: 6px 0 0; padding: 8px 12px; background: var(--well); border-radius: 10px; } .artifact { display: flex; align-items: center; gap: 6px; max-width: 100%; margin: 4px -6px 0; padding: 4px 8px 4px 6px; color: var(--muted); font: inherit; font-size: calc(12.5px * var(--canvas-scale)); text-align: left; background: none; border: 0; border-radius: 6px; cursor: pointer; transition: background-color 100ms ease, color 100ms ease; } .artifact:hover { color: var(--text); background: var(--hover); } .artifact svg { flex: none; color: var(--accent); } .artifact .title { min-width: 0; overflow: hidden; color: var(--accent); font-weight: 560; white-space: nowrap; text-overflow: ellipsis; } .artifact .version { flex: none; color: var(--faint); font-variant-numeric: tabular-nums; } .skill { max-width: 100%; margin: 2px 0 0; overflow: hidden; color: var(--muted); font-size: calc(12.5px * var(--canvas-scale)); white-space: nowrap; text-overflow: ellipsis; } .running { display: flex; align-items: center; gap: 8px; min-width: 0; margin: 0; padding: 3px 0; color: var(--muted); font-size: calc(12.5px * var(--canvas-scale)); } .running .name { flex: none; font-weight: 560; } .running .preview { min-width: 0; overflow: hidden; font-family: var(--mono); font-size: calc(11.5px * var(--canvas-scale)); white-space: nowrap; text-overflow: ellipsis; } .spinner { flex: none; width: 9px; height: 9px; border: 1.5px solid color-mix(in oklab, var(--accent) 25%, transparent); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.8s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } .thought { display: -webkit-box; margin: 2px 0 6px; padding: 0; overflow: hidden; color: var(--muted); font: inherit; font-size: calc(12.5px * var(--canvas-scale)); text-align: left; background: none; border: 0; cursor: pointer; -webkit-box-orient: vertical; -webkit-line-clamp: 3; line-clamp: 3; } .thought.whole { display: block; }</style>