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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261<script lang="ts"> import type { NodeProps } from '@xyflow/svelte'; import { describe, targetAt } from '../anchors'; import type { NoteWindow } from '../board.svelte'; import { boards } from '../boards.svelte'; import { sendable, sendNotes, windowName } from '../notes'; import { tying } from '../tie.svelte';
// A note on the canvas. Drag its top edge to move it; drag the tie from // its corner onto a window, a message, a block or the selected text in a // chat, lines in a diff, or a part of a page to make it about that (drop // it on empty canvas to set it loose). A written, tied note goes with its // chat's next message unless it is kept on its own, which the footer // toggles; ⌘↵ sends it alone. A sent note stays, faded, until it is // dismissed. let { id, data, selected }: NodeProps<NoteWindow> = $props(); const board = $derived(boards.board); let field = $state<HTMLTextAreaElement>(); let sending = $state(false); let problem = $state('');
const about = $derived(describe(data.target, (window) => (board ? windowName(board, window) : 'a closed window'))); const ready = $derived(!!board && !data.sent && sendable(board, { id, data } as NoteWindow));
// A note just made is for writing in. The canvas keeps a node hidden // until it has measured it, and a hidden field cannot take focus, so try // for a few frames. $effect(() => { if (!field || !board?.fresh.delete(id)) return; let tries = 10; let frame = 0; const attempt = () => { field?.focus(); if (document.activeElement !== field && --tries > 0) frame = requestAnimationFrame(attempt); }; frame = requestAnimationFrame(attempt); return () => cancelAnimationFrame(frame); });
async function send() { if (!board || !ready) return; sending = true; problem = ''; try { await sendNotes(board, [{ id, data } as NoteWindow]); } catch (error) { problem = String((error as Error).message ?? error); } finally { sending = false; } }
function keydown(event: KeyboardEvent) { event.stopPropagation(); if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) send(); if (event.key === 'Escape') field?.blur(); }
// The tie follows the pointer on the whole document, not the grip: the // note may redraw mid-drag and drop the grip's capture, and a tie that // never heard its pointer go up would stay drawn after the note is gone. function tie(event: PointerEvent) { event.preventDefault(); event.stopPropagation(); const pointer = event.pointerId; Object.assign(tying, { note: id, x: event.clientX, y: event.clientY }); const move = (moved: PointerEvent) => moved.pointerId === pointer && Object.assign(tying, { x: moved.clientX, y: moved.clientY }); const stop = () => { document.removeEventListener('pointermove', move, true); document.removeEventListener('pointerup', end, true); document.removeEventListener('pointercancel', stop, true); tying.note = null; }; const end = (ended: PointerEvent) => { if (ended.pointerId !== pointer) return; stop(); board?.updateNote(id, { target: targetAt(ended.clientX, ended.clientY), sent: false }); }; document.addEventListener('pointermove', move, true); document.addEventListener('pointerup', end, true); document.addEventListener('pointercancel', stop, true); }
// A note dismissed mid-tie takes its tie with it. $effect(() => () => { if (tying.note === id) tying.note = null; });</script>
<article class="note" class:selected class:sent={data.sent} class:loose={!data.target}> <header class="note-handle"> <button class="grip nodrag" title="drag onto what this note is about" aria-label="tie this note to something" onpointerdown={tie}> <svg viewBox="0 0 16 16" width="13" height="13" aria-hidden="true"><circle cx="4" cy="12" r="2" fill="currentColor" /><path d="M5.5 10.5 12 4M8.5 4H12v3.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg> </button> <span class="about" title={data.target?.kind === 'window' ? about : (data.target?.quote ?? about)}>{about}</span> <button class="dismiss nodrag" title="dismiss" aria-label="dismiss note" onclick={() => board?.remove(id)}> <svg viewBox="0 0 16 16" width="12" height="12" aria-hidden="true"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" /></svg> </button> </header> <textarea bind:this={field} class="nodrag nowheel" style:zoom={data.zoom && data.zoom !== 1 ? data.zoom : undefined} value={data.text} readonly={data.sent} oninput={(event) => board?.updateNote(id, { text: event.currentTarget.value })} onkeydown={keydown} rows="2" placeholder={data.target ? 'what about it?' : 'a note… tie it to something to send it'} ></textarea> {#if data.sent} <footer><span class="state">sent ✓</span></footer> {:else if data.target} <footer> {#if problem}<span class="problem" title={problem}>not sent</span> {:else if ready} <button class="state nodrag" title={data.alone ? 'only its own send button sends it; click to send it with your next message in the chat' : 'goes with your next message in the chat; click to send it only with its own button'} onclick={() => board?.updateNote(id, { alone: !data.alone })} > {data.alone ? 'on its own' : 'with next message'} </button> {/if} <span class="spacer"></span> <button class="send nodrag" disabled={!ready || sending} onclick={send} title="send it to albedo now, on its own (⌘↵)">send</button> </footer> {/if}</article>
<style> .note { display: flex; flex-direction: column; width: 100%; min-height: 0; padding: 6px 10px 8px; color: var(--text); font-size: calc(13px * var(--canvas-scale)); background: var(--surface); border-radius: 12px; box-shadow: var(--shadow-raised); transition: opacity 200ms ease; } .note.selected { box-shadow: 0 0 0 2px var(--focus-ring), var(--shadow-raised); } .note.loose { background: color-mix(in oklab, var(--surface), var(--accent) 6%); } /* A sent note steps back but stays readable over whatever it sits on. */ .note.sent { color: var(--muted); background: var(--title-bar); box-shadow: var(--shadow); } header { display: flex; align-items: center; gap: 6px; min-width: 0; margin: 0 -4px 2px; cursor: grab; } .about { flex: 1; min-width: 0; overflow: hidden; color: var(--muted); font-size: calc(11px * var(--canvas-scale)); white-space: nowrap; text-overflow: ellipsis; } header button { display: grid; place-items: center; width: 22px; height: 22px; padding: 0; color: var(--muted); background: none; border: 0; border-radius: 6px; cursor: pointer; } header button:hover { color: var(--text); background: var(--hover); } .grip { color: var(--accent) !important; cursor: crosshair !important; } textarea { width: 100%; min-height: 44px; max-height: 320px; padding: 2px 0; color: var(--text); font: inherit; line-height: 1.45; background: none; border: 0; outline: 0; resize: none; field-sizing: content; cursor: text; } footer { display: flex; align-items: center; gap: 6px; margin-top: 4px; } .spacer { flex: 1; } .state { color: var(--muted); font-size: calc(11.5px * var(--canvas-scale)); } button.state { min-width: 0; padding: 2px 4px; margin-left: -4px; overflow: hidden; font: inherit; font-size: calc(11.5px * var(--canvas-scale)); white-space: nowrap; text-overflow: ellipsis; background: none; border: 0; border-radius: 5px; cursor: pointer; } button.state:hover { color: var(--text); background: var(--hover); } .problem { color: var(--danger); font-size: calc(11.5px * var(--canvas-scale)); } .send { height: 24px; padding: 0 10px; color: var(--surface); font: inherit; font-size: calc(12px * var(--canvas-scale)); font-weight: 560; background: var(--accent); border: 0; border-radius: 7px; cursor: pointer; } .send:disabled { opacity: 0.35; cursor: default; }</style>