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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146<script lang="ts"> import ShellDelivery from './ShellDelivery.svelte'; import { copyText, readShellOutput, stopShellCommand, type ShellCommand as CommandRecord } from '../api'; let { sessionId, command, onopen, openLabel = 'Open command output' }: { sessionId: string; command: CommandRecord; onopen?: () => void; openLabel?: string } = $props(); let expanded = $state(false); let output = $state(''); let next = $state<number | null>(null); let paged = $state(false); let loading = $state(false); let error = $state(''); let stopping = $state(false); let copied = $state<'command' | 'output' | null>(null); let pre = $state<HTMLPreElement>(); let clipped = $state(false); const captured = $derived(new TextEncoder().encode(output).length); // Bound work as well as height: a minified response may be one enormous line. const preview = $derived(output.slice(0, 4096).replace(/[\uD800-\uDBFF]$/, '').split(/\r?\n/, 5).join('\n')); const expandable = $derived(expanded || clipped || output.length > preview.length || next !== null); const success = $derived(command.status === 'completed' && command.exit_code === 0); const failed = $derived(command.status === 'failed' || (command.status === 'completed' && command.exit_code !== null && command.exit_code !== 0)); const status = $derived(command.status === 'completed' ? (success ? 'done' : command.exit_code !== null ? `exit ${command.exit_code}` : 'completed') : command.status === 'cancelled' ? 'stopped' : command.status); const partial = $derived(captured < command.output_bytes);
$effect(() => { if (!paged && !loading) output = command.output ?? ''; next = command.output_bytes > captured ? captured : null; }); $effect(() => { if (!pre) return; const element = pre; // Remeasure after text changes and pane/phone resizing, not on a timer. preview; expanded; const measure = () => (clipped = element.scrollHeight > element.clientHeight + 1); measure(); const observer = new ResizeObserver(measure); observer.observe(element); return () => observer.disconnect(); }); $effect(() => { if (!copied) return; const timer = setTimeout(() => (copied = null), 1500); return () => clearTimeout(timer); });
function size(bytes: number): string { return bytes < 1024 ? `${bytes} B` : bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(1)} KiB` : `${(bytes / (1024 * 1024)).toFixed(1)} MiB`; } async function copy(which: 'command' | 'output') { error = ''; try { await copyText(which === 'command' ? command.command : output); copied = which; } catch (reason) { error = String((reason as Error).message ?? reason); } } async function more() { if (loading || next === null) return; loading = true; paged = true; error = ''; try { const page = await readShellOutput(sessionId, command.id, next); output += page.text; next = page.next_offset; } catch (reason) { error = String((reason as Error).message ?? reason); } finally { loading = false; } } async function stop() { stopping = true; error = ''; try { await stopShellCommand(sessionId, command.id); } catch (reason) { error = String((reason as Error).message ?? reason); } finally { stopping = false; } }</script>
{#snippet copyIcon(done: boolean)} <svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> {#if done}<path d="m3 8 3 3 7-7" />{:else}<rect x="5.5" y="5.5" width="7.5" height="7.5" rx="1.5" /><path d="M10.5 3H4a1 1 0 0 0-1 1v6.5" />{/if} </svg>{/snippet}
<section class="shell-entry" aria-label="Shell command"> <div class="author"><svg viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="currentColor" stroke-width="1.2" aria-hidden="true"><circle cx="8" cy="5" r="2.5" /><path d="M3 14v-1a5 5 0 0 1 10 0v1" /></svg>you · shell</div> <div class="rail"> <div class="command-head"> <span class="prompt">$</span><code>{command.command}</code> <div class="command-meta"> <span class="status" class:success class:failed class:running={command.status === 'running'} role="status" title={`${command.status}${command.exit_code !== null ? ` · exit ${command.exit_code}` : ''}`}> {#if success}<svg viewBox="0 0 16 16" width="11" height="11" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="m3 8 3 3 7-7" /></svg>{:else if command.status === 'running'}<span class="running-dot"></span>{/if}{status} </span> <ShellDelivery {command} /> {#if onopen}<button class="copy-command" aria-label={openLabel} title={openLabel} onclick={onopen}><svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 3h4v4M13 3L7 9M11 10v2a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h2" /></svg></button>{/if} <button class="copy-command" aria-label="Copy command" title={copied === 'command' ? 'Copied' : 'Copy command'} onclick={() => copy('command')}>{@render copyIcon(copied === 'command')}</button> </div> </div> {#if output} <!-- svelte-ignore a11y_no_noninteractive_tabindex (expanded output must be keyboard-scrollable) --> <pre role="region" bind:this={pre} class:expanded tabindex={expanded ? 0 : undefined} aria-label="Command output">{expanded ? output : preview}</pre> {:else} <p class="empty">{command.status === 'running' ? 'Waiting for output…' : 'No output.'}</p> {/if} {#if command.error}<p class="error">{command.error}</p>{/if} {#if command.truncated}<p class="warning">Capture hit the 16 MiB retention limit; later output wasn’t retained.</p>{/if} {#if error}<p class="error" role="alert">{error}</p>{/if} {#if output || next !== null || command.status === 'running'} <div class="actions"> {#if expandable}<button class="expand" aria-expanded={expanded} onclick={() => (expanded = !expanded)}><svg class:up={expanded} viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="1.3" aria-hidden="true"><path d="m4 6 4 4 4-4" /></svg>{expanded ? 'Collapse' : 'Expand output'}</button>{/if} {#if expanded && next !== null}<button disabled={loading} onclick={more}>{loading ? 'Loading…' : 'Load more output'}</button>{/if} {#if output}<button title={partial ? 'Copy loaded output' : 'Copy output'} aria-label={partial ? 'Copy loaded output' : 'Copy output'} onclick={() => copy('output')}>{@render copyIcon(copied === 'output')}{copied === 'output' ? 'Copied' : partial ? 'Copy loaded' : 'Copy'}</button>{/if} {#if command.status === 'running'}<button class="stop" disabled={stopping} onclick={stop}><svg viewBox="0 0 16 16" width="10" height="10" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="3" y="3" width="10" height="10" rx="1" /></svg>{stopping ? 'Stopping…' : 'Stop'}</button>{/if} {#if command.output_bytes > 0}<span class="bytes">{partial ? `${size(captured)} of ` : ''}{size(command.output_bytes)}</span>{/if} </div> {/if} </div></section>
<style> .shell-entry { width: min(100%, 760px); min-width: 0; align-self: flex-start; color: var(--text); font: calc(12px * var(--canvas-scale)) / 1.6 var(--mono); } .author { display: flex; align-items: center; gap: 5px; margin-bottom: 7px; color: var(--faint); font: calc(10px * var(--canvas-scale)) / 1.5 var(--sans); } .rail { min-width: 0; border-left: 2px solid color-mix(in oklab, var(--accent) 24%, transparent); padding-left: 12px; } .command-head { display: flex; align-items: flex-start; gap: 8px; min-width: 0; } .prompt { color: var(--accent); } code { flex: 1; min-width: 0; white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; } .command-meta { display: flex; flex: none; align-items: center; gap: 8px; } .status { display: inline-flex; flex: none; align-items: center; gap: 4px; color: var(--muted); white-space: nowrap; font: calc(10px * var(--canvas-scale)) / 1.6 var(--sans); } .success svg { color: var(--added); } .failed { color: var(--danger); } .running { color: var(--accent); } .running-dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; animation: pulse 1.5s ease-in-out infinite; } pre { margin: 10px 0 0; padding: 0; max-height: 8em; overflow: hidden; white-space: pre-wrap; overflow-wrap: anywhere; color: var(--text); font: inherit; tab-size: 2; } pre.expanded { max-height: min(55vh, 520px); overflow: auto; overscroll-behavior: contain; scrollbar-width: thin; } .empty { margin: 10px 0 0; color: var(--faint); font: inherit; } button { display: inline-flex; align-items: center; gap: 4px; border: 0; padding: 3px 0; background: none; color: var(--muted); cursor: pointer; font: calc(11px * var(--canvas-scale)) / 1.5 var(--sans); } button:hover { color: var(--text); } button:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 2px; } button:disabled { opacity: .5; cursor: default; } .copy-command { flex: none; padding: 3px; margin: 0 -3px 0 0; color: var(--faint); } .actions { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; margin-top: 9px; } .up { transform: rotate(180deg); } .stop { color: var(--danger); } .bytes { margin-left: auto; color: var(--faint); font-size: calc(10px * var(--canvas-scale)); } .error, .warning { margin: 10px 0 0; overflow-wrap: anywhere; font: calc(11px * var(--canvas-scale)) / 1.6 var(--sans); } .error { color: var(--danger); } .warning { color: var(--muted); } @keyframes pulse { 50% { opacity: .35; } } @media (prefers-reduced-motion: reduce) { .running-dot { animation: none; } }</style>