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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416import { uuidv7 as newId } from '../api';
// A board's layout: a tree whose leaves name windows and whose inner tiles// arrange them. A split lays its children side by side (row) or stacked// (column), a tabs tile shows one child at a time, and a canvas lets each// child sit anywhere on a plane that pans and zooms, another canvas too. A// window is in a tree at most once. Any tile may have a// name: a named split or tabs is a section, and a named tab shows its name.//// Everything here is pure: every change returns a new tree, normalized.
export type Axis = 'row' | 'column';export type Side = 'left' | 'right' | 'top' | 'bottom';export type Viewport = { x: number; y: number; zoom: number };export type Rect = { x: number; y: number; width: number; height: number };
export type Leaf = { id: string; kind: 'leaf'; window: string; name?: string };export type Split = { id: string; kind: 'split'; axis: Axis; children: Tile[]; sizes: number[]; name?: string };/** `active` is the id of the child shown. */export type Tabs = { id: string; kind: 'tabs'; children: Tile[]; active: string; name?: string };/** A child on a canvas; without a height it takes the height of what it holds. */export type Placed = { tile: Tile; x: number; y: number; width: number; height?: number; z: number };/** `map` is whether its overview map shows. */export type Canvas = { id: string; kind: 'canvas'; children: Placed[]; viewport: Viewport; map?: boolean; name?: string };export type Tile = Leaf | Split | Tabs | Canvas;export type Kind = Tile['kind'];
/** The least a tile may be across or down: a gutter can't squeeze one smaller, nor can a drop. */export const LEAST = 160;/** The gap between a split's tiles, which is its resize handle (SplitView.svelte). */export const GUTTER = 6;/** Where a new canvas's view starts. */export const VIEW: Viewport = { x: 40, y: 72, zoom: 1 };
/** What a section's frame takes along an axis: its padding, and across the top its header (TileView.svelte). */const FRAME = { row: 12, column: 30 };/** What a tabs tile's strip and, round a tab that is not a window, its padding take (TabsView.svelte). */const STRIP = { row: 0, column: 38 };const PAGE = 12;
/** * How short `tile` can be along `axis` with every window in it at LEAST or * more. `headed` is whether a section's frame is drawn, as in TileView. */export function leastAlong(tile: Tile, axis: Axis, headed = true): number { if (tile.kind === 'leaf' || tile.kind === 'canvas') return LEAST; const frame = headed && tile.name ? FRAME[axis] : 0; if (tile.kind === 'tabs') { const pages = tile.children.map((child) => leastAlong(child, axis, false) + (child.kind === 'leaf' ? 0 : PAGE)); return Math.max(LEAST, ...pages) + STRIP[axis] + frame; } const each = tile.children.map((child) => leastAlong(child, axis)); if (tile.axis === axis) return each.reduce((sum, length) => sum + length, GUTTER * (each.length - 1) + frame); return Math.max(LEAST, ...each) + frame;}
type Extent = { tile: Tile; width: number; height: number };
/** The size of each tile laid out side by side in `width` by `height`, a leaf's by its window, so a window moved keeps its key. */function extents(tile: Tile, width: number, height: number, into = new Map<string, Extent>()): Map<string, Extent> { if (tile.kind !== 'split') return into.set(tile.kind === 'leaf' ? tile.window : tile.id, { tile, width, height }); const row = tile.axis === 'row'; const length = (row ? width : height) - GUTTER * (tile.children.length - 1); tile.children.forEach((child, at) => extents(child, row ? length * tile.sizes[at] : width, row ? height : length * tile.sizes[at], into)); return into;}
export type Squeezed = { tile: Tile; way: 'narrower' | 'shorter' };
/** * A tile `next` squeezes under LEAST one way, as a gutter would not, and * which way, laid out in `width` by `height`. One already under it in * `before` only counts if it shrinks further, so a board that has a cramped * tile can still take a change elsewhere. */export function squeezed(before: Tile, next: Tile, width: number, height: number): Squeezed | null { const was = extents(before, width, height); for (const [key, now] of extents(next, width, height)) { const old = was.get(key); if (now.width < LEAST && now.width < (old?.width ?? Infinity) - 1) return { tile: now.tile, way: 'narrower' }; if (now.height < LEAST && now.height < (old?.height ?? Infinity) - 1) return { tile: now.tile, way: 'shorter' }; } return null;}
export function leaf(window: string): Leaf { return { id: newId(), kind: 'leaf', window };}
export function split(axis: Axis, children: Tile[], sizes?: number[]): Split { return { id: newId(), kind: 'split', axis, children, sizes: sizes ?? children.map(() => 1 / children.length) };}
export function tabs(children: Tile[], active = children[0]?.id ?? ''): Tabs { return { id: newId(), kind: 'tabs', children, active };}
export function canvas(children: Placed[] = [], viewport: Viewport = VIEW): Canvas { return { id: newId(), kind: 'canvas', children, viewport };}
export function childrenOf(tile: Tile): Tile[] { if (tile.kind === 'leaf') return []; if (tile.kind === 'canvas') return tile.children.map((placed) => placed.tile); return tile.children;}
/** Every window in the tree, in order. */export function windows(tile: Tile): string[] { return tile.kind === 'leaf' ? [tile.window] : childrenOf(tile).flatMap(windows);}
/** The tiles from the root down to the leaf showing `window`, or null when it is not in the tree. */export function pathTo(root: Tile, window: string): Tile[] | null { if (root.kind === 'leaf') return root.window === window ? [root] : null; for (const child of childrenOf(root)) { const path = pathTo(child, window); if (path) return [root, ...path]; } return null;}
/** The tile `window` sits directly in, or null for a missing window or a root leaf. */export function parentOf(root: Tile, window: string): Exclude<Tile, Leaf> | null { const path = pathTo(root, window); return path && path.length > 1 ? (path[path.length - 2] as Exclude<Tile, Leaf>) : null;}
/** The tiles from the root down to pane `id`: a window's leaf, or the tile with that id; null when it is neither. */export function paneTrail(root: Tile, id: string): Tile[] | null { const path = pathTo(root, id) ?? trail(root, id); return path.length ? path : null;}
/** The tile pane `id` (a window, or a tile by its id) is shown in: its tabs block when it is a tab, else its own. */export function frameOf(root: Tile, id: string): Tile | null { const path = paneTrail(root, id); return (path?.at(-2)?.kind === 'tabs' ? path.at(-2) : path?.at(-1)) ?? null;}
/** The nearest canvas above pane `id`, which is where it is placed freely, if anywhere. */export function canvasOf(root: Tile, id: string): Canvas | null { const path = paneTrail(root, id) ?? []; for (let at = path.length - 2; at >= 0; at--) if (path[at].kind === 'canvas') return path[at] as Canvas; return null;}
/** The tiles from the root down to the one with `id`. */export function trail(root: Tile, id: string): Tile[] { if (root.id === id) return [root]; for (const child of childrenOf(root)) { const path = trail(child, id); if (path.length) return [root, ...path]; } return [];}
export function find(root: Tile, id: string): Tile | null { return trail(root, id).at(-1) ?? null;}
/** An empty root of a kind; an emptied split or tabs root stays as one, so the board keeps its layout. */export function empty(kind: Kind): Tile { if (kind === 'canvas') return canvas(); return kind === 'tabs' ? tabs([]) : split('row', []);}
/** * Stacks `items` back to front as 1..n with the one `top` picks in front, so * raising never grows the numbers. Unchanged items come back as the same array. */export function raised<T extends { z: number }>(items: T[], top: (item: T) => boolean): T[] { const order = [...items].sort((a, b) => a.z - b.z); const stacked = [...order.filter((item) => !top(item)), ...order.filter(top)]; if (stacked.every((item, at) => item.z === at + 1)) return items; const z = new Map(stacked.map((item, at) => [item, at + 1])); return items.map((item) => (z.get(item) === item.z ? item : { ...item, z: z.get(item)! }));}
/** Replaces the tile with `id` by what `change` makes of it, then normalizes. */export function update(root: Tile, id: string, change: (tile: Tile) => Tile | null): Tile { return normalize(replace(root, id, change)) ?? empty(root.kind);}
function same<T>(before: T[], after: T[]) { return before.length === after.length && before.every((item, at) => item === after[at]);}
function replace(tile: Tile, id: string, change: (tile: Tile) => Tile | null): Tile | null { if (tile.id === id) return change(tile); if (tile.kind === 'leaf') return tile; if (tile.kind === 'canvas') { const children = tile.children.flatMap((placed) => { const next = replace(placed.tile, id, change); if (next === placed.tile) return [placed]; return next ? [{ ...placed, tile: next }] : []; }); return same(tile.children, children) ? tile : { ...tile, children }; } if (tile.kind === 'tabs') { let active = tile.active; const children = tile.children.flatMap((child) => { const next = replace(child, id, change); if (next && child.id === tile.active) active = next.id; return next ? [next] : []; }); return same(tile.children, children) ? tile : { ...tile, children, active }; } const kept = tile.children.map((child) => replace(child, id, change)); if (same(tile.children, kept)) return tile; return { ...tile, children: kept.filter((child) => child !== null), sizes: tile.sizes.filter((_, at) => kept[at] !== null) };}
/** * Drops empty splits and tabs, collapses an unnamed split or tabs of one into * its child, folds an unnamed split into a parent split of the same axis and * unnamed tabs into parent tabs, keeps sizes summing to 1, and points a tabs * tile whose shown child left at one that stayed. A section (a named split or * tabs) stays while it holds anything, so a window moved within it stays in * it. An empty canvas stays: it is somewhere to put things. */export function normalize(tile: Tile | null): Tile | null { if (!tile || tile.kind === 'leaf') return tile; if (tile.kind === 'canvas') { const children = tile.children.flatMap((placed) => { const next = normalize(placed.tile); if (next === placed.tile) return [placed]; return next ? [{ ...placed, tile: next }] : []; }); return same(tile.children, children) ? tile : { ...tile, children }; } if (tile.kind === 'tabs') { let active = tile.active; const children = tile.children.flatMap((child) => { const next = normalize(child); if (!next) return []; const inner = next.kind === 'tabs' && !next.name; if (child.id === tile.active) active = inner ? next.active : next.id; return inner ? next.children : [next]; }); if (children.length === 0) return null; if (children.length === 1 && !tile.name) return children[0]; if (!children.some((child) => child.id === active)) active = children[0].id; return same(tile.children, children) && active === tile.active ? tile : { ...tile, children, active }; } const children: Tile[] = []; const sizes: number[] = []; tile.children.forEach((child, at) => { const next = normalize(child); if (!next) return; const size = tile.sizes[at] ?? 0; if (next.kind === 'split' && next.axis === tile.axis && !next.name) { children.push(...next.children); sizes.push(...next.sizes.map((inner) => size * inner)); } else { children.push(next); sizes.push(size); } }); if (children.length === 0) return null; if (children.length === 1 && !tile.name) return children[0]; const total = sizes.reduce((sum, size) => sum + size, 0); if (same(tile.children, children) && Math.abs(total - 1) < 1e-9) return tile; return { ...tile, children, sizes: sizes.map((size) => (total > 0 ? size / total : 1 / sizes.length)) };}
/** Takes `window` out of the tree. */export function remove(root: Tile, window: string): Tile { const path = pathTo(root, window); return path ? update(root, path[path.length - 1].id, () => null) : root;}
/** Splits the tile with `id` in two, putting `add` on `side` of it; a tab splits its whole tabs tile. */export function splitBeside(root: Tile, id: string, add: Tile, side: Side): Tile { const axis: Axis = side === 'left' || side === 'right' ? 'row' : 'column'; const first = side === 'left' || side === 'top'; const holder = trail(root, id).at(-2); return update(root, holder?.kind === 'tabs' ? holder.id : id, (tile) => split(axis, first ? [add, tile] : [tile, add]));}
/** Adds `add` as a tab of the tabs tile `id` (or of any other tile, which becomes tabs with it) at `at`, last by default, and shows it. */export function addTab(root: Tile, id: string, add: Tile, at?: number): Tile { return update(root, id, (tile) => { if (tile.kind !== 'tabs') return tabs(at === 0 ? [add, tile] : [tile, add], add.id); const index = Math.min(at ?? tile.children.length, tile.children.length); return { ...tile, children: [...tile.children.slice(0, index), add, ...tile.children.slice(index)], active: add.id }; });}
/** Trades the places of windows `a` and `b`. */export function swap(root: Tile, a: string, b: string): Tile { const other = (window: string) => (window === a ? b : window === b ? a : window); return swapped(root, other);}
function swapped(tile: Tile, other: (window: string) => string): Tile { if (tile.kind === 'leaf') { const window = other(tile.window); return window === tile.window ? tile : { ...tile, window }; } if (tile.kind === 'canvas') { const children = tile.children.map((placed) => { const next = swapped(placed.tile, other); return next === placed.tile ? placed : { ...placed, tile: next }; }); return same(tile.children, children) ? tile : { ...tile, children }; } const children = tile.children.map((child) => swapped(child, other)); return same(tile.children, children) ? tile : { ...tile, children };}
/** Puts `tile` on the canvas `id`, above everything already there. */export function place(root: Tile, id: string, tile: Tile, rect: Omit<Placed, 'tile' | 'z'>): Tile { return update(root, id, (target) => { if (target.kind !== 'canvas') return target; const z = Math.max(0, ...target.children.map((placed) => placed.z)) + 1; return { ...target, children: [...target.children, { tile, ...rect, z }] }; });}
/** A tree's arrangement without sizes or ids (a canvas keeps its id), to tell whether a change moves anything. */export function shape(tile: Tile): string { if (tile.kind === 'leaf') return tile.window; const inner = childrenOf(tile).map(shape).join(','); if (tile.kind === 'canvas') return `canvas:${tile.id}(${inner})`; return tile.kind === 'split' ? `${tile.axis}(${inner})` : `tabs(${inner})`;}
/** Whether the tree keeps its rule: no window twice. */export function valid(root: Tile): boolean { const seen = windows(root); return new Set(seen).size === seen.length;}
/** * Tiles a canvas's children by where they sat: the widest clear gap between * them, across or down, cuts them in two, and each side is tiled the same * way. Children that overlap with no gap left fall back to a row by x. */export function tileOut(placed: Placed[]): Tile | null { if (placed.length === 0) return null; if (placed.length === 1) return placed[0].tile; const box = (child: Placed): Rect => ({ x: child.x, y: child.y, width: child.width, height: child.height ?? child.width }); const cut = (axis: Axis) => { const start = (rect: Rect) => (axis === 'row' ? rect.x : rect.y); const end = (rect: Rect) => (axis === 'row' ? rect.x + rect.width : rect.y + rect.height); const sorted = [...placed].sort((a, b) => start(box(a)) - start(box(b))); let best: { at: number; gap: number } | null = null; let reach = end(box(sorted[0])); for (let at = 1; at < sorted.length; at++) { const gap = start(box(sorted[at])) - reach; if (gap >= 0 && (!best || gap > best.gap)) best = { at, gap }; reach = Math.max(reach, end(box(sorted[at]))); } return best && { ...best, axis, sorted }; }; const across = cut('row'); const down = cut('column'); const chosen = across && (!down || across.gap >= down.gap) ? across : down; if (!chosen) return split('row', [...placed].sort((a, b) => a.x - b.x).map((child) => child.tile)); const span = (side: Placed[]) => { const rects = side.map(box); const start = (rect: Rect) => (chosen.axis === 'row' ? rect.x : rect.y); const end = (rect: Rect) => (chosen.axis === 'row' ? rect.x + rect.width : rect.y + rect.height); return Math.max(...rects.map(end)) - Math.min(...rects.map(start)); }; const before = chosen.sorted.slice(0, chosen.at); const after = chosen.sorted.slice(chosen.at); return normalize(split(chosen.axis, [tileOut(before)!, tileOut(after)!], [span(before), span(after)]));}
/** * Lays a tile's windows out on a canvas where they were shown, `rects` being * each window's box as last measured; a tabs block or a section stays one, * where what it showed was. */export function spreadOut(tile: Tile, rects: Map<string, Rect>, viewport?: Viewport): Canvas { const children = pieces(tile).map((piece, z) => { const boxes = shownWindows(piece).flatMap((window) => rects.get(window) ?? []); const x = Math.min(...boxes.map((box) => box.x)); const y = Math.min(...boxes.map((box) => box.y)); const rect = boxes.length ? { x, y, width: Math.max(...boxes.map((box) => box.x + box.width)) - x, height: Math.max(...boxes.map((box) => box.y + box.height)) - y } : { x: z * 40, y: z * 40, width: 560, height: 720 }; return { tile: piece, ...rect, z: z + 1 }; }); return canvas(children, viewport);}
function pieces(tile: Tile): Tile[] { const whole = tile.kind === 'leaf' || ((tile.kind === 'tabs' || tile.name) && !holdsCanvas(tile)); return whole ? [tile] : childrenOf(tile).flatMap(pieces);}
function holdsCanvas(tile: Tile): boolean { return tile.kind === 'canvas' || childrenOf(tile).some(holdsCanvas);}
/** The windows a tile shows: of a tabs tile, only its shown child's. */function shownWindows(tile: Tile): string[] { if (tile.kind === 'tabs') { const shown = tile.children.find((child) => child.id === tile.active); return shown ? shownWindows(shown) : []; } return tile.kind === 'leaf' ? [tile.window] : childrenOf(tile).flatMap(shownWindows);}
/** The same windows under a root of another kind. */export function rearrange(root: Tile, kind: Exclude<Kind, 'leaf'>, rects: Map<string, Rect>): Tile { if (windows(root).length === 0) return empty(kind); if (kind === 'canvas') return root.kind === 'canvas' ? root : spreadOut(root, rects); if (kind === 'tabs') return tabs(windows(root).map(leaf)); if (root.kind === 'canvas') return tileOut(root.children) ?? empty('split'); return root.kind === 'tabs' ? (normalize(split('row', root.children)) ?? empty('split')) : root;}