feat(electrobun): wire EVERYTHING — all settings persist, theme editor, marketplace

All settings wired to SQLite persistence:
- AgentSettings: shell, CWD, permissions, providers (JSON blob)
- SecuritySettings: branch policies (JSON array)
- ProjectSettings: per-project via setProject RPC
- OrchestrationSettings: wake, anchors, notifications
- AdvancedSettings: logging, OTLP, plugins, import/export JSON

Theme Editor:
- 26 color pickers (14 Accents + 12 Neutrals)
- Live CSS var preview as you pick colors
- Save custom theme to SQLite, cancel reverts
- Import/export theme as JSON
- Custom themes in dropdown with delete button

Extensions Marketplace:
- 8-plugin demo catalog (Browse/Installed tabs)
- Search/filter by name or tag
- Install/uninstall with SQLite persistence
- Plugin cards with emoji icons, tags, version

Terminal font hot-swap:
- fontStore.onTermFontChange() → xterm.js options update + fitAddon.fit()
- Resize notification to PTY daemon after font change

All 7 settings categories functional. Every control persists and takes effect.
This commit is contained in:
Hibryda 2026-03-20 05:45:10 +01:00
parent 6002a379e4
commit 5032021915
20 changed files with 1005 additions and 271 deletions

View file

@ -5,6 +5,9 @@
import { FitAddon } from '@xterm/addon-fit';
import { ImageAddon } from '@xterm/addon-image';
import { electrobun } from './main.ts';
import { fontStore } from './font-store.svelte.ts';
import { themeStore } from './theme-store.svelte.ts';
import { getXtermTheme } from './themes.ts';
interface Props {
sessionId: string;
@ -14,34 +17,11 @@
let { sessionId, cwd }: Props = $props();
// 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;
let unsubFont: (() => void) | null = null;
let ro: ResizeObserver | null = null;
/** Decode a base64 string from the daemon into a Uint8Array. */
function decodeBase64(b64: string): Uint8Array {
@ -51,11 +31,15 @@
return bytes;
}
onMount(async () => {
onMount(() => {
const currentTheme = themeStore.currentTheme;
const termFamily = fontStore.termFontFamily || 'JetBrains Mono, Fira Code, monospace';
const termSize = fontStore.termFontSize || 13;
term = new Terminal({
theme: THEME,
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
fontSize: 13,
theme: getXtermTheme(currentTheme),
fontFamily: termFamily,
fontSize: termSize,
cursorBlink: true,
allowProposedApi: true,
scrollback: 5000,
@ -65,7 +49,6 @@
term.loadAddon(fitAddon);
term.loadAddon(new CanvasAddon());
// Sixel / iTerm2 / Kitty inline image support
term.loadAddon(new ImageAddon({
enableSizeReports: true,
sixelSupport: true,
@ -77,25 +60,34 @@
term.open(termEl);
fitAddon.fit();
// ── Connect to PTY daemon via Bun RPC ──────────────────────────────────
// ── Subscribe to terminal font changes ─────────────────────────────────
const { cols, rows } = term;
const result = await electrobun.rpc?.request["pty.create"]({ sessionId, cols, rows, cwd });
if (!result?.ok) {
term.writeln(`\x1b[31m[agor] Failed to connect to PTY daemon: ${result?.error ?? 'unknown error'}\x1b[0m`);
term.writeln('\x1b[33m[agor] Is agor-ptyd running? Start it with: agor-ptyd\x1b[0m');
}
unsubFont = fontStore.onTermFontChange((family: string, size: number) => {
term.options.fontFamily = family || 'JetBrains Mono, Fira Code, monospace';
term.options.fontSize = size;
fitAddon.fit();
electrobun.rpc?.request['pty.resize']({ sessionId, cols: term.cols, rows: term.rows }).catch(() => {});
});
// ── Receive output from daemon (via Bun) ───────────────────────────────
// ── Connect to PTY daemon (fire-and-forget from onMount) ───────────────
// "pty.output" messages are pushed by Bun whenever the PTY produces data.
electrobun.rpc?.addMessageListener("pty.output", ({ sessionId: sid, data }) => {
void (async () => {
const { cols, rows } = term;
const result = await electrobun.rpc?.request['pty.create']({ sessionId, cols, rows, cwd });
if (!result?.ok) {
term.writeln(`\x1b[31m[agor] Failed to connect to PTY daemon: ${result?.error ?? 'unknown error'}\x1b[0m`);
term.writeln('\x1b[33m[agor] Is agor-ptyd running? Start it with: agor-ptyd\x1b[0m');
}
})();
// ── Receive output from daemon ─────────────────────────────────────────
electrobun.rpc?.addMessageListener('pty.output', ({ sessionId: sid, data }: { sessionId: string; data: string }) => {
if (sid !== sessionId) return;
term.write(decodeBase64(data));
});
// "pty.closed" fires when the shell exits.
electrobun.rpc?.addMessageListener("pty.closed", ({ sessionId: sid, exitCode }) => {
electrobun.rpc?.addMessageListener('pty.closed', ({ sessionId: sid, exitCode }: { sessionId: string; exitCode: number | null }) => {
if (sid !== sessionId) return;
term.writeln(`\r\n\x1b[90m[Process exited${exitCode !== null ? ` with code ${exitCode}` : ''}]\x1b[0m`);
});
@ -103,7 +95,7 @@
// ── Send user input to daemon ──────────────────────────────────────────
term.onData((data: string) => {
electrobun.rpc?.request["pty.write"]({ sessionId, data }).catch((err) => {
electrobun.rpc?.request['pty.write']({ sessionId, data }).catch((err: unknown) => {
console.error('[pty.write] error:', err);
});
});
@ -111,22 +103,17 @@
// ── Sync resize events to daemon ───────────────────────────────────────
term.onResize(({ cols: c, rows: r }) => {
electrobun.rpc?.request["pty.resize"]({ sessionId, cols: c, rows: r }).catch(() => {});
electrobun.rpc?.request['pty.resize']({ sessionId, cols: c, rows: r }).catch(() => {});
});
// Refit on container resize and notify the daemon.
const ro = new ResizeObserver(() => {
fitAddon.fit();
});
ro = new ResizeObserver(() => { fitAddon.fit(); });
ro.observe(termEl);
return () => ro.disconnect();
});
onDestroy(() => {
// Unsubscribe from daemon output (session stays alive so it can be
// reconnected if the tab is re-opened, matching the "persists" requirement).
electrobun.rpc?.request["pty.unsubscribe"]({ sessionId }).catch(() => {});
unsubFont?.();
ro?.disconnect();
electrobun.rpc?.request['pty.unsubscribe']({ sessionId }).catch(() => {});
term?.dispose();
});
</script>
@ -140,7 +127,6 @@
min-height: 10rem;
}
/* xterm.js base styles */
:global(.xterm) {
padding: 0.5rem;
}