Something went wrong. Try again.
small gleam coding and (not yet) persistent agent daemon with a detachable cli forked from okami.mom/albedo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170<script lang="ts"> import { untrack } from 'svelte'; import { SvelteMap, SvelteSet } from 'svelte/reactivity'; import { browseFiles, type Directory } from '../api';
// The project as a tree, each folder read from the daemon the first time // it opens. Changed files carry their status letter, and a folder the // number of changed files under it. type Item = Directory['items'][number];
let { root, marks, active, onopen, }: { root: string; /** Changed files by absolute path, with their status letter. */ marks: ReadonlyMap<string, string>; active: string | null; onopen: (path: string) => void; } = $props();
const listings = new SvelteMap<string, Item[] | { problem: string }>(); const opened = new SvelteSet<string>();
function load(dir: string) { if (listings.has(dir)) return; listings.set(dir, []); browseFiles(dir).then( (read) => listings.set(dir, read.items as Item[]), (failure) => listings.set(dir, { problem: String(failure?.message ?? failure) }), ); }
$effect(() => { const at = root; untrack(() => { opened.clear(); listings.clear(); load(at); }); });
function toggle(dir: string) { if (opened.has(dir)) opened.delete(dir); else { opened.add(dir); load(dir); } }
// Changed files under each folder, counted once per change of `marks`. const changedBelow = $derived.by(() => { const counts = new Map<string, number>(); for (const path of marks.keys()) { for (let slash = path.indexOf('/', 1); slash !== -1; slash = path.indexOf('/', slash + 1)) { const dir = path.slice(0, slash); counts.set(dir, (counts.get(dir) ?? 0) + 1); } } return counts; });
function changedUnder(dir: string) { return changedBelow.get(dir) ?? 0; }</script>
{#snippet level(dir: string, depth: number)} {@const items = listings.get(dir)} {#if items && 'problem' in items} <p class="quiet" style:padding-left="{10 + depth * 14}px">(。•︿•。) {items.problem}</p> {:else if items} {#each items as item (item.location)} {#if item.kind !== 'file'} {@const count = changedUnder(item.location)} <button class="row dir" class:dim={item.hidden || item.ignored} style:padding-left="{6 + depth * 14}px" aria-expanded={opened.has(item.location)} onclick={() => toggle(item.location)}> <span class="twisty" class:open={opened.has(item.location)}>▸</span> <span class="name">{item.name}</span> {#if count}<span class="count">{count}</span>{/if} </button> {#if opened.has(item.location)}{@render level(item.location, depth + 1)}{/if} {:else} {@const mark = marks.get(item.location)} <button class="row" class:dim={item.hidden || item.ignored} class:active={active === item.location} style:padding-left="{20 + depth * 14}px" onclick={() => onopen(item.location)} title={item.location}> <span class="name">{item.name}</span> {#if mark}<span class="mark mark-{mark}">{mark}</span>{/if} </button> {/if} {/each} {/if}{/snippet}
<div class="tree" role="tree">{@render level(root, 0)}</div>
<style> .tree { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1px; } .row { display: flex; align-items: center; gap: 6px; min-width: 0; padding: 3px 8px; color: var(--text); font: inherit; font-size: calc(12.5px * var(--canvas-scale)); text-align: left; background: none; border: 0; border-radius: 6px; cursor: pointer; } .row:hover { background: var(--hover); } .row.active { background: var(--selected); } .dim .name { color: var(--faint); } .name { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .twisty { flex: none; width: 10px; color: var(--muted); font-size: 10px; line-height: 1; transition: transform 150ms cubic-bezier(0.23, 1, 0.32, 1); } .twisty.open { transform: rotate(90deg); } .count, .mark { margin-left: auto; flex: none; color: var(--accent); font-size: calc(11px * var(--canvas-scale)); font-variant-numeric: tabular-nums; } .mark-A, .mark-U { color: var(--added); } .mark-D { color: var(--danger); } .quiet { margin: 4px 0; color: var(--muted); font-size: calc(12px * var(--canvas-scale)); } @media (prefers-reduced-motion: reduce) { .twisty { transition: none; } }</style>