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.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889<script lang="ts"> import type { Session } from './api'; import { pokeFamily, stateOf, watchFamily } from './agents'; import { descendants } from './agentTree'; import { ago } from './format'; import MenuButton from './MenuButton.svelte';
// The agents a session started, below the one the window shows: a branching // icon, with how many are working, that lists them (closed ones too) and // opens the tree of the whole family. Nothing shows until the session has // started an agent. let { sessionId, rootId, busy = false, up = false, inline = false, onopen, ontree }: { sessionId: string; rootId: string; busy?: boolean; up?: boolean; inline?: boolean; onopen: (agent: Session) => void; ontree: () => void } = $props(); let family = $state.raw<Session[]>([]); let listed = $state(false); const order = { working: 0, idle: 1, closed: 2 }; const agents = $derived(descendants(family, sessionId, (agent) => [order[stateOf(agent)], agent.activity_at ? -Date.parse(agent.activity_at) : 0])); /** How far down the family this window's own session is, so the list indents what is below it, not what is below the root. */ const own = $derived(family.find((session) => session.id === sessionId)?.depth ?? 0); const working = $derived(agents.filter((agent) => stateOf(agent) === 'working').length);
$effect(() => watchFamily(rootId, (found) => found && (family = found)));
// The root starts its agents during a turn, so its turn starting or ending is when to look, not a long wait later. // An agent's own turns follow from a read, which is already the look. let turn: boolean | undefined; $effect(() => { if (sessionId === rootId && turn !== undefined && turn !== busy) pokeFamily(rootId); turn = busy; });</script>
{#if agents.length || inline} <MenuButton label="agents" count={working} live={working > 0} {up} {inline} bind:open={listed}> {#snippet icon()} <svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true"><path d="M3.5 8h3m0 0c2 0 1-4 3.5-4H12.5M6.5 8c2 0 1 4 3.5 4h2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /><circle cx="3" cy="8" r="1.4" fill="currentColor" /><circle cx="13" cy="4" r="1.4" fill="currentColor" /><circle cx="13" cy="12" r="1.4" fill="currentColor" /></svg> {/snippet} <li class="tree" data-chip-target="agents:{rootId}"> <button class="open" onclick={() => ((listed = false), ontree())}> <span class="name">open tree</span> <span class="state">{agents.length} {agents.length === 1 ? 'agent' : 'agents'}</span> </button> </li> {#each agents as agent (agent.id)} <li data-chip-target="session:{agent.id}"> <button onclick={() => ((listed = false), onopen(agent))}> <span class="name" title="{agent.name || 'untitled session'} · {agent.model ?? 'no model yet'}" style:padding-left="{(agent.depth - own - 1) * 12}px">{agent.name || 'untitled session'}</span> <span class="state" class:working={stateOf(agent) === 'working'}>{stateOf(agent)}</span> <span class="when">{ago(agent.activity_at)}</span> </button> </li> {/each} </MenuButton>{/if}
<style> .name { overflow: hidden; font-size: calc(12.5px * var(--ui-scale)); white-space: nowrap; text-overflow: ellipsis; } .state { color: var(--muted); font-size: calc(11.5px * var(--ui-scale)); white-space: nowrap; } .state.working { color: var(--accent); } /* Opening the tree is an action, not one of the agents: it stays on top and reads as a button. */ .tree { position: sticky; top: 0; z-index: 1; padding-bottom: 4px; background: var(--surface); } .tree > .open { color: var(--accent); background: color-mix(in oklab, var(--accent) 10%, transparent); } .tree > .open:hover { background: color-mix(in oklab, var(--accent) 16%, transparent); } .tree .state { color: inherit; }</style>