feat(electrobun): add xterm.js terminal with image addon (Sixel/iTerm2)

- Terminal.svelte component with @xterm/xterm + Canvas + Fit + Image addons
- Catppuccin Mocha terminal theme matching main app
- Sixel, iTerm2 inline image protocol support via xterm-addon-image
- ResizeObserver for responsive terminal sizing
- Demo cargo test output in terminal section below agent messages
This commit is contained in:
Hibryda 2026-03-20 01:40:24 +01:00
parent b79fbf688e
commit f97ea95373
10 changed files with 241 additions and 23 deletions

View file

@ -1,4 +1,6 @@
<script lang="ts">
import Terminal from './Terminal.svelte';
// ── Types ────────────────────────────────────────────────────
type AgentStatus = 'running' | 'idle' | 'stalled';
type MsgRole = 'user' | 'assistant' | 'tool-call' | 'tool-result';
@ -165,16 +167,22 @@
role="tabpanel"
aria-label="Model"
>
{#each project.messages as msg (msg.id)}
<div class="msg">
<span class="msg-role {msg.role.split('-')[0]}">{msg.role}</span>
<div
class="msg-body"
class:tool-call={msg.role === 'tool-call'}
class:tool-result={msg.role === 'tool-result'}
>{msg.content}</div>
</div>
{/each}
<div class="agent-messages">
{#each project.messages as msg (msg.id)}
<div class="msg">
<span class="msg-role {msg.role.split('-')[0]}">{msg.role}</span>
<div
class="msg-body"
class:tool-call={msg.role === 'tool-call'}
class:tool-result={msg.role === 'tool-result'}
>{msg.content}</div>
</div>
{/each}
</div>
<!-- Terminal section -->
<div class="terminal-section">
<Terminal />
</div>
</div>
<!-- Docs tab placeholder -->

View file

@ -0,0 +1,97 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { Terminal } from '@xterm/xterm';
import { CanvasAddon } from '@xterm/addon-canvas';
import { FitAddon } from '@xterm/addon-fit';
import { ImageAddon } from '@xterm/addon-image';
// Catppuccin Mocha terminal theme
const THEME = {
background: '#1e1e2e',
foreground: '#cdd6f4',
cursor: '#f5e0dc',
cursorAccent: '#1e1e2e',
selectionBackground: '#585b7066',
black: '#45475a',
red: '#f38ba8',
green: '#a6e3a1',
yellow: '#f9e2af',
blue: '#89b4fa',
magenta: '#f5c2e7',
cyan: '#94e2d5',
white: '#bac2de',
brightBlack: '#585b70',
brightRed: '#f38ba8',
brightGreen: '#a6e3a1',
brightYellow: '#f9e2af',
brightBlue: '#89b4fa',
brightMagenta: '#f5c2e7',
brightCyan: '#94e2d5',
brightWhite: '#a6adc8',
};
let termEl: HTMLDivElement;
let term: Terminal;
let fitAddon: FitAddon;
onMount(() => {
term = new Terminal({
theme: THEME,
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
fontSize: 13,
cursorBlink: true,
allowProposedApi: true,
scrollback: 5000,
});
fitAddon = new FitAddon();
term.loadAddon(fitAddon);
term.loadAddon(new CanvasAddon());
// Image addon — enables Sixel, iTerm2, and Kitty inline image protocols
term.loadAddon(new ImageAddon({
enableSizeReports: true,
sixelSupport: true,
sixelScrolling: true,
sixelPaletteLimit: 4096,
showPlaceholder: true,
}));
term.open(termEl);
fitAddon.fit();
// Demo content with ANSI colors
term.writeln('\x1b[1;34m~/code/ai/agent-orchestrator\x1b[0m \x1b[32m$\x1b[0m cargo test --workspace');
term.writeln(' \x1b[1;32mCompiling\x1b[0m agor-core v0.1.0');
term.writeln(' \x1b[1;32mCompiling\x1b[0m agor-gpui v0.1.0');
term.writeln(' \x1b[1;32mRunning\x1b[0m tests/unit.rs');
term.writeln('test result: ok. \x1b[32m47 passed\x1b[0m; 0 failed');
term.writeln('');
term.writeln('\x1b[1;34m~/code/ai/agent-orchestrator\x1b[0m \x1b[32m$\x1b[0m \x1b[5m▊\x1b[0m');
// Resize on window resize
const ro = new ResizeObserver(() => fitAddon.fit());
ro.observe(termEl);
return () => ro.disconnect();
});
onDestroy(() => {
term?.dispose();
});
</script>
<div class="terminal-container" bind:this={termEl}></div>
<style>
.terminal-container {
width: 100%;
height: 100%;
min-height: 10rem;
}
/* xterm.js base styles */
:global(.xterm) {
padding: 0.5rem;
}
</style>

View file

@ -401,3 +401,20 @@ html, body {
color: var(--ctp-text);
font-weight: 500;
}
/* ── Terminal section ─────────────────────────────────────── */
.agent-messages {
flex: 1;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.terminal-section {
height: 12rem;
min-height: 8rem;
border-top: 1px solid var(--ctp-surface0);
flex-shrink: 0;
}

View file

@ -1,4 +1,5 @@
import "./app.css";
import "@xterm/xterm/css/xterm.css";
import App from "./App.svelte";
import { mount } from "svelte";