feat(electrobun): port all Tauri features — full settings, popup menus, provider capabilities

New components (8):
- provider-capabilities.ts: per-provider feature flags (claude/codex/ollama)
- settings/AppearanceSettings.svelte: theme, fonts, cursor, scrollback
- settings/AgentSettings.svelte: shell, CWD, permissions, providers
- settings/SecuritySettings.svelte: keyring, secrets, branch policies
- settings/ProjectSettings.svelte: per-project provider/model/worktree/sandbox
- settings/OrchestrationSettings.svelte: wake strategy, notifications, anchors
- settings/AdvancedSettings.svelte: logging, OTLP, plugins, import/export

Updated:
- ChatInput: radial context indicator (78% demo, color-coded arc),
  4 popup menus (upload/context/web/slash), provider-gated icons
- SettingsDrawer: 6-category sidebar shell
- AgentPane: passes provider + contextPct to ChatInput
This commit is contained in:
Hibryda 2026-03-20 04:50:57 +01:00
parent 54d6f0b94a
commit 0b9e8b305a
15 changed files with 1510 additions and 441 deletions

View file

@ -0,0 +1,152 @@
<script lang="ts">
import { PROVIDER_CAPABILITIES, type ProviderId } from '../provider-capabilities';
type PermMode = 'bypassPermissions' | 'default' | 'plan';
const PROVIDERS: { id: ProviderId; label: string; desc: string }[] = [
{ id: 'claude', label: 'Claude', desc: 'Anthropic — claude-opus/sonnet/haiku' },
{ id: 'codex', label: 'Codex', desc: 'OpenAI — gpt-5.4' },
{ id: 'ollama', label: 'Ollama', desc: 'Local — qwen3, llama3, etc.' },
];
let defaultShell = $state('/bin/bash');
let defaultCwd = $state('~');
let permissionMode = $state<PermMode>('bypassPermissions');
let systemPrompt = $state('');
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' },
ollama: { enabled: false, model: 'qwen3:8b' },
});
let expandedProvider = $state<string | null>(null);
function toggleProvider(id: string) {
providerState[id] = { ...providerState[id], enabled: !providerState[id].enabled };
providerState = { ...providerState };
}
function setModel(id: string, model: string) {
providerState[id] = { ...providerState[id], model };
providerState = { ...providerState };
}
</script>
<div class="section">
<h3 class="sh">Defaults</h3>
<div class="field">
<label class="lbl" for="ag-shell">Shell</label>
<input id="ag-shell" class="text-in" bind:value={defaultShell} placeholder="/bin/bash" />
</div>
<div class="field">
<label class="lbl" for="ag-cwd">Working directory</label>
<input id="ag-cwd" class="text-in" bind:value={defaultCwd} placeholder="~" />
</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>
</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>
<h3 class="sh" style="margin-top: 0.75rem;">Providers</h3>
<div class="prov-list">
{#each PROVIDERS as prov}
{@const state = providerState[prov.id]}
<div class="prov-panel" class:disabled={!state.enabled}>
<button class="prov-hdr" onclick={() => expandedProvider = expandedProvider === prov.id ? null : prov.id}>
<span class="prov-name">{prov.label}</span>
<span class="prov-desc">{prov.desc}</span>
<span class="prov-chev">{expandedProvider === prov.id ? '▴' : '▾'}</span>
</button>
{#if expandedProvider === prov.id}
<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>
</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}
placeholder={PROVIDER_CAPABILITIES[prov.id].defaultModel}
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}
{#if PROVIDER_CAPABILITIES[prov.id].web}<span class="cap">Web</span>{/if}
{#if PROVIDER_CAPABILITIES[prov.id].upload}<span class="cap">Upload</span>{/if}
</div>
</div>
{/if}
</div>
{/each}
</div>
</div>
<style>
.section { display: flex; flex-direction: column; gap: 0.5rem; }
.sh { margin: 0.125rem 0; font-size: 0.6875rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: var(--ctp-overlay0); }
.field { display: flex; flex-direction: column; gap: 0.2rem; }
.lbl { font-size: 0.75rem; color: var(--ctp-subtext0); }
.text-in {
padding: 0.3rem 0.5rem; background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1);
border-radius: 0.25rem; color: var(--ctp-text); font-size: 0.8125rem; font-family: var(--ui-font-family);
}
.text-in:focus { outline: none; border-color: var(--ctp-blue); }
.prompt {
padding: 0.375rem 0.5rem; background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1);
border-radius: 0.25rem; color: var(--ctp-text); font-size: 0.8rem;
font-family: var(--term-font-family, monospace); resize: vertical; min-height: 3rem; line-height: 1.4;
}
.prompt:focus { outline: none; border-color: var(--ctp-blue); }
.prompt::placeholder { color: var(--ctp-overlay0); }
.seg { display: flex; border-radius: 0.25rem; overflow: hidden; border: 1px solid var(--ctp-surface1); }
.seg button { flex: 1; padding: 0.25rem 0.5rem; background: var(--ctp-surface0); border: none; color: var(--ctp-overlay1); font-size: 0.75rem; cursor: pointer; font-family: var(--ui-font-family); }
.seg button:not(:last-child) { border-right: 1px solid var(--ctp-surface1); }
.seg button:hover { background: var(--ctp-surface1); color: var(--ctp-subtext1); }
.seg button.active { background: color-mix(in srgb, var(--ctp-blue) 20%, var(--ctp-surface0)); color: var(--ctp-blue); font-weight: 600; }
.prov-list { display: flex; flex-direction: column; gap: 0.3rem; }
.prov-panel { background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1); border-radius: 0.3rem; overflow: hidden; transition: opacity 0.15s; }
.prov-panel.disabled { opacity: 0.5; }
.prov-hdr { display: flex; align-items: center; gap: 0.5rem; width: 100%; padding: 0.45rem 0.625rem; background: transparent; border: none; color: var(--ctp-text); cursor: pointer; text-align: left; font-size: 0.8rem; font-family: var(--ui-font-family); }
.prov-hdr:hover { background: var(--ctp-base); }
.prov-name { font-weight: 600; white-space: nowrap; }
.prov-desc { flex: 1; color: var(--ctp-overlay0); font-size: 0.7rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.prov-chev { color: var(--ctp-overlay0); font-size: 0.7rem; flex-shrink: 0; }
.prov-body { padding: 0.5rem 0.625rem; border-top: 1px solid var(--ctp-surface1); display: flex; flex-direction: column; gap: 0.5rem; }
.toggle-row { display: flex; align-items: center; justify-content: space-between; cursor: pointer; }
.toggle { position: relative; width: 2rem; height: 1.125rem; border: none; border-radius: 0.5625rem; background: var(--ctp-surface1); cursor: pointer; transition: background 0.2s; padding: 0; flex-shrink: 0; }
.toggle.on { background: var(--ctp-blue); }
.thumb { position: absolute; top: 0.125rem; left: 0.125rem; width: 0.875rem; height: 0.875rem; border-radius: 50%; background: var(--ctp-text); transition: transform 0.2s; }
.toggle.on .thumb { transform: translateX(0.875rem); }
.caps { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.cap { padding: 0.125rem 0.5rem; background: color-mix(in srgb, var(--ctp-blue) 10%, transparent); color: var(--ctp-blue); border-radius: 0.75rem; font-size: 0.65rem; font-weight: 500; }
</style>