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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399<script lang="ts"> import { onMount, tick } from 'svelte'; import { Terminal } from '@xterm/xterm'; import { FitAddon } from '@xterm/addon-fit'; import { WebLinksAddon } from '@xterm/addon-web-links'; import '@xterm/xterm/css/xterm.css'; import { PtyClient, type TerminalReady } from '../terminals.svelte'; import { prefs } from '../prefs.svelte';
let { terminalId, cwd = '', sessionId = '', onready, onexit, }: { terminalId: string; cwd?: string; sessionId?: string; onready?: (ready: TerminalReady) => void; onexit?: (code: number) => void; } = $props();
let container = $state<HTMLElement>(); let term: Terminal | null = null; let fitAddon: FitAddon | null = null; let client: PtyClient | null = null;
let cols = $state(80); let rows = $state(24); let running = $state(true); let exitCode = $state<number | null>(null); let shell = $state('sh'); // svelte-ignore state_referenced_locally let currentCwd = $state(cwd); let error = $state<string | null>(null);
function resolveTheme() { if (!container) return undefined; const style = getComputedStyle(container); const well = style.getPropertyValue('--well').trim() || '#161217'; const text = style.getPropertyValue('--text').trim() || '#e8e2eb'; const accent = style.getPropertyValue('--accent').trim() || '#9d4edd'; const surface = style.getPropertyValue('--surface').trim() || '#221a24'; const danger = style.getPropertyValue('--danger').trim() || '#ef4444'; const added = style.getPropertyValue('--added').trim() || '#10b981'; const muted = style.getPropertyValue('--muted').trim() || '#a398a8'; const shade = style.getPropertyValue('--shade').trim() || '#0a080b';
return { background: well, foreground: text, cursor: accent, cursorAccent: surface, selectionBackground: 'rgba(157, 78, 221, 0.35)', selectionForeground: text, selectionInactiveBackground: 'rgba(157, 78, 221, 0.18)', black: shade, red: danger, green: added, yellow: '#f59e0b', blue: '#3b82f6', magenta: accent, cyan: '#06b6d4', white: text, brightBlack: muted, brightRed: danger, brightGreen: added, brightYellow: '#fbbf24', brightBlue: '#60a5fa', brightMagenta: accent, brightCyan: '#22d3ee', brightWhite: '#ffffff', }; }
function applyTheme() { if (!term || !container) return; const theme = resolveTheme(); if (theme) term.options.theme = theme; if (prefs.fonts.code) term.options.fontFamily = prefs.fonts.code; }
function fitTerminal() { if (!fitAddon || !term || !container) return; try { fitAddon.fit(); if (term.cols !== cols || term.rows !== rows) { cols = term.cols; rows = term.rows; client?.resize(cols, rows); } } catch {} }
export function restart() { if (!client) return; exitCode = null; running = true; term?.reset(); client.restart(cols, rows); fitTerminal(); }
export function clear() { term?.clear(); }
export function interrupt() { client?.signal('SIGINT'); }
onMount(() => { if (!container) return;
client = new PtyClient(terminalId, { cwd, sessionId, cols: 80, rows: 24 });
const terminal = new Terminal({ cursorBlink: true, cursorStyle: 'bar', cursorWidth: 2, fontFamily: prefs.fonts.code || 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace', fontSize: 13, lineHeight: 1.35, allowTransparency: true, drawBoldTextInBrightColors: true, scrollback: 10000, }); term = terminal;
const fit = new FitAddon(); fitAddon = fit; terminal.loadAddon(fit); terminal.loadAddon(new WebLinksAddon());
terminal.open(container); applyTheme();
terminal.onData((data) => { client?.sendInput(data); });
terminal.onBinary((data) => { client?.sendInput(new Uint8Array(data.split('').map((c) => c.charCodeAt(0)))); });
client.onOutput((chunk) => { terminal.write(chunk); });
client.onExit((code) => { running = false; exitCode = code; onexit?.(code); });
client.connect();
// Initial fit after next frame requestAnimationFrame(() => { fitTerminal(); terminal.focus(); });
const observer = new ResizeObserver(() => { requestAnimationFrame(fitTerminal); }); observer.observe(container);
return () => { observer.disconnect(); client?.dispose(); terminal.dispose(); term = null; fitAddon = null; client = null; }; });
// Follow client state reactively $effect(() => { if (!client) return; if (client.ready) { currentCwd = client.ready.cwd; shell = client.ready.shell; running = client.ready.state === 'running'; if (client.ready.exit_code !== null) exitCode = client.ready.exit_code; onready?.(client.ready); } error = client.error; });
// Follow theme changes $effect(() => { prefs.resolved; prefs.fonts.code; JSON.stringify(prefs.palettes[prefs.resolved]); applyTheme(); });</script>
<div class="pty-view holds-wheel"> <div class="terminal-container" bind:this={container}></div>
{#if error} <div class="error-banner"> <span>(。•︿•。) {error}</span> <button class="retry-btn" onclick={restart}>retry</button> </div> {/if}
{#if !running && exitCode !== null} <div class="exit-banner"> <span>(・_・ ) process exited (code {exitCode})</span> <button class="restart-btn" onclick={restart}>restart shell</button> </div> {/if}
<footer class="terminal-bar"> <span class="dims">{cols} × {rows}</span> <div class="actions"> <button class="action-btn" title="Send Ctrl+C to running process" onclick={interrupt}>^C</button> <button class="action-btn" title="Clear terminal screen" onclick={clear}>clear</button> <button class="action-btn" title="Restart shell session" onclick={restart}>restart</button> </div> </footer></div>
<style> .pty-view { position: relative; display: flex; flex: 1; flex-direction: column; min-height: 0; width: 100%; height: 100%; background: var(--well); overflow: hidden; border-radius: 0 0 14px 14px; }
.terminal-container { position: relative; flex: 1; min-height: 0; width: 100%; overflow: hidden; }
:global(.terminal-container .xterm) { position: absolute; inset: 0; padding: 8px 12px; }
:global(.pty-view .xterm .xterm-viewport) { background-color: transparent !important; scrollbar-width: thin; overscroll-behavior: contain; }
:global(.pty-view .xterm .xterm-screen) { /* crisp text rendering */ -webkit-font-smoothing: subpixel-antialiased; }
.error-banner, .exit-banner { position: absolute; top: 10px; right: 14px; z-index: 20; display: flex; align-items: center; gap: 10px; padding: 6px 12px; font-size: calc(12px * var(--canvas-scale)); background: var(--surface); border: 1px solid var(--border); border-radius: 8px; box-shadow: var(--shadow); animation: fadeIn 150ms ease-out; }
.error-banner { color: var(--danger); border-color: color-mix(in oklab, var(--danger) 30%, transparent); }
.exit-banner { color: var(--muted); }
.restart-btn, .retry-btn { height: 22px; padding: 0 8px; font: inherit; font-size: calc(11px * var(--canvas-scale)); font-weight: 560; color: var(--surface); background: var(--accent); border: 0; border-radius: 5px; cursor: pointer; transition: opacity 120ms ease; }
.restart-btn:hover, .retry-btn:hover { opacity: 0.9; }
.terminal-bar { display: flex; flex: none; align-items: center; justify-content: space-between; height: 28px; padding: 0 12px; background: var(--title-bar); border-top: 1px solid color-mix(in oklab, var(--border) 40%, transparent); font-size: calc(11.5px * var(--canvas-scale)); color: var(--muted); user-select: none; }
.indicator { display: flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: calc(11px * var(--canvas-scale)); }
.indicator .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); }
.indicator.running .dot { background: var(--added); box-shadow: 0 0 6px color-mix(in oklab, var(--added) 60%, transparent); }
.indicator.exited .dot { background: var(--danger); }
.dims { font-variant-numeric: tabular-nums; opacity: 0.75; }
.actions { display: flex; align-items: center; gap: 6px; }
.action-btn { height: 20px; padding: 0 6px; font: inherit; font-size: calc(10.5px * var(--canvas-scale)); font-family: var(--mono); color: var(--muted); background: none; border: 1px solid color-mix(in oklab, var(--border) 60%, transparent); border-radius: 4px; cursor: pointer; transition: all 120ms ease; }
.action-btn:hover { color: var(--text); background: var(--hover); border-color: var(--border); }
.action-btn:active { transform: scale(0.96); }
@keyframes fadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }</style>