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.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970<script lang="ts"> import type { Job } from './api'; import { ago } from './format'; import { headline, outcome, watchJobs } from './jobs.svelte'; import MenuButton from './MenuButton.svelte';
// The session's background jobs: an activity pulse (not a prompt: that is a live terminal), with how many are // running, that lists them (finished ones too, while the kernel still has // their output). Nothing shows until the session has had a job. let { sessionId, onopen, up = false, inline = false }: { sessionId: string; onopen: (job: string) => void; up?: boolean; inline?: boolean } = $props(); let jobs = $state<Job[]>([]); let listed = $state(false); const running = $derived(jobs.filter((job) => job.state === 'running').length); // When each job last wrote, as far as this list has seen: the record only // counts bytes, so a count that grew between reads is output just now. const heard = new Map<string, { bytes: number; at: number }>(); const lastOutput = (job: Job) => heard.get(job.id)?.at ?? job.started_at; const listedJobs = $derived([...jobs].sort((a, b) => Number(b.state === 'running') - Number(a.state === 'running') || lastOutput(b) - lastOutput(a)));
$effect(() => watchJobs(sessionId, (found) => { const now = Date.now(); for (const job of found) { const seen = heard.get(job.id); if (!seen || seen.bytes !== job.bytes) heard.set(job.id, { bytes: job.bytes, at: seen ? now : (job.ended_at ?? job.started_at) }); } jobs = found; }), );</script>
{#if jobs.length || inline} <MenuButton label="background jobs" count={running} live={running > 0} {up} {inline} bind:open={listed}> {#snippet icon()} <svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true"><path d="M1.5 8.5h2.8l1.9-4.5 3.2 8 1.9-4.5h3.2" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" /></svg> {/snippet} {#each listedJobs as job (job.id)} <li data-chip-target="terminal:{sessionId}:{job.id}"> <button onclick={() => ((listed = false), onopen(job.id))}> <span class="command" title={job.command}>{headline(job.command)}</span> <span class="outcome" class:running={job.state === 'running'} class:failed={job.state === 'exited' && (job.exit_code !== 0 || job.timed_out)}>{outcome(job)}</span> <span class="when">{ago(new Date(lastOutput(job)).toISOString())}</span> </button> </li> {/each} </MenuButton>{/if}
<style> .command { overflow: hidden; font-family: var(--mono); font-size: calc(12px * var(--ui-scale)); white-space: nowrap; text-overflow: ellipsis; } .outcome { color: var(--muted); font-size: calc(11.5px * var(--ui-scale)); font-variant-numeric: tabular-nums; white-space: nowrap; } .outcome.running { color: var(--accent); } .outcome.failed { color: var(--danger); }</style>