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

@ -1,4 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { appRpc } from '../main.ts';
import { PROVIDER_CAPABILITIES, type ProviderId } from '../provider-capabilities';
type PermMode = 'bypassPermissions' | 'default' | 'plan';
@ -14,10 +16,7 @@
let permissionMode = $state<PermMode>('bypassPermissions');
let systemPrompt = $state('');
interface ProviderState {
enabled: boolean;
model: string;
}
interface ProviderState { enabled: boolean; model: string; }
let providerState = $state<Record<string, ProviderState>>({
claude: { enabled: true, model: 'claude-opus-4-5' },
codex: { enabled: false, model: 'gpt-5.4' },
@ -26,15 +25,48 @@
let expandedProvider = $state<string | null>(null);
// ── Persistence helpers ──────────────────────────────────────────────────
function persist(key: string, value: string) {
appRpc?.request['settings.set']({ key, value }).catch(console.error);
}
function persistProviders() {
persist('provider_settings', JSON.stringify(providerState));
}
// ── Actions ──────────────────────────────────────────────────────────────
function setShell(v: string) { defaultShell = v; persist('default_shell', v); }
function setCwd(v: string) { defaultCwd = v; persist('default_cwd', v); }
function setPermMode(v: PermMode) { permissionMode = v; persist('permission_mode', v); }
function setPrompt(v: string) { systemPrompt = v; persist('system_prompt_template', v); }
function toggleProvider(id: string) {
providerState[id] = { ...providerState[id], enabled: !providerState[id].enabled };
providerState = { ...providerState };
persistProviders();
}
function setModel(id: string, model: string) {
providerState[id] = { ...providerState[id], model };
providerState = { ...providerState };
persistProviders();
}
// ── Restore on mount ─────────────────────────────────────────────────────
onMount(async () => {
if (!appRpc) return;
const { settings } = await appRpc.request['settings.getAll']({}).catch(() => ({ settings: {} }));
if (settings['default_shell']) defaultShell = settings['default_shell'];
if (settings['default_cwd']) defaultCwd = settings['default_cwd'];
if (settings['permission_mode']) permissionMode = settings['permission_mode'] as PermMode;
if (settings['system_prompt_template']) systemPrompt = settings['system_prompt_template'];
if (settings['provider_settings']) {
try { providerState = JSON.parse(settings['provider_settings']); } catch { /* ignore */ }
}
});
</script>
<div class="section">
@ -42,23 +74,27 @@
<div class="field">
<label class="lbl" for="ag-shell">Shell</label>
<input id="ag-shell" class="text-in" bind:value={defaultShell} placeholder="/bin/bash" />
<input id="ag-shell" class="text-in" value={defaultShell} placeholder="/bin/bash"
onchange={e => setShell((e.target as HTMLInputElement).value)} />
</div>
<div class="field">
<label class="lbl" for="ag-cwd">Working directory</label>
<input id="ag-cwd" class="text-in" bind:value={defaultCwd} placeholder="~" />
<input id="ag-cwd" class="text-in" value={defaultCwd} placeholder="~"
onchange={e => setCwd((e.target as HTMLInputElement).value)} />
</div>
<h3 class="sh" style="margin-top: 0.75rem;">Permission mode</h3>
<div class="seg">
<button class:active={permissionMode === 'bypassPermissions'} onclick={() => permissionMode = 'bypassPermissions'}>Bypass</button>
<button class:active={permissionMode === 'default'} onclick={() => permissionMode = 'default'}>Default</button>
<button class:active={permissionMode === 'plan'} onclick={() => permissionMode = 'plan'}>Plan</button>
<button class:active={permissionMode === 'bypassPermissions'} onclick={() => setPermMode('bypassPermissions')}>Bypass</button>
<button class:active={permissionMode === 'default'} onclick={() => setPermMode('default')}>Default</button>
<button class:active={permissionMode === 'plan'} onclick={() => setPermMode('plan')}>Plan</button>
</div>
<h3 class="sh" style="margin-top: 0.75rem;">System prompt template</h3>
<textarea class="prompt" bind:value={systemPrompt} rows="3" placeholder="Optional prompt prepended to all agent sessions..."></textarea>
<textarea class="prompt" value={systemPrompt} rows="3"
placeholder="Optional prompt prepended to all agent sessions..."
onchange={e => setPrompt((e.target as HTMLTextAreaElement).value)}></textarea>
<h3 class="sh" style="margin-top: 0.75rem;">Providers</h3>
<div class="prov-list">
@ -74,24 +110,15 @@
<div class="prov-body">
<label class="toggle-row">
<span class="lbl">Enabled</span>
<button
class="toggle"
class:on={state.enabled}
role="switch"
aria-checked={state.enabled}
aria-label="Toggle {prov.label} provider"
onclick={() => toggleProvider(prov.id)}
><span class="thumb"></span></button>
<button class="toggle" class:on={state.enabled} role="switch"
aria-checked={state.enabled} aria-label="Toggle {prov.label} provider"
onclick={() => toggleProvider(prov.id)}><span class="thumb"></span></button>
</label>
<div class="field">
<label class="lbl" for="model-{prov.id}">Default model</label>
<input
id="model-{prov.id}"
class="text-in"
value={state.model}
<input id="model-{prov.id}" class="text-in" value={state.model}
placeholder={PROVIDER_CAPABILITIES[prov.id].defaultModel}
onchange={e => setModel(prov.id, (e.target as HTMLInputElement).value)}
/>
onchange={e => setModel(prov.id, (e.target as HTMLInputElement).value)} />
</div>
<div class="caps">
{#if PROVIDER_CAPABILITIES[prov.id].images}<span class="cap">Images</span>{/if}