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

@ -1,4 +1,11 @@
<script lang="ts">
import AppearanceSettings from './settings/AppearanceSettings.svelte';
import AgentSettings from './settings/AgentSettings.svelte';
import SecuritySettings from './settings/SecuritySettings.svelte';
import ProjectSettings from './settings/ProjectSettings.svelte';
import OrchestrationSettings from './settings/OrchestrationSettings.svelte';
import AdvancedSettings from './settings/AdvancedSettings.svelte';
interface Props {
open: boolean;
onClose: () => void;
@ -6,40 +13,24 @@
let { open, onClose }: Props = $props();
// Local settings state (demo — no persistence in prototype)
const THEMES = [
{ id: 'mocha', label: 'Catppuccin Mocha' },
{ id: 'macchiato', label: 'Catppuccin Macchiato' },
{ id: 'frappe', label: 'Catppuccin Frappé' },
{ id: 'latte', label: 'Catppuccin Latte' },
];
let themeId = $state('mocha');
let themeDropdownOpen = $state(false);
let selectedThemeLabel = $derived(THEMES.find(t => t.id === themeId)?.label ?? 'Catppuccin Mocha');
type CategoryId = 'appearance' | 'agents' | 'security' | 'projects' | 'orchestration' | 'advanced';
function selectTheme(id: string) {
themeId = id;
themeDropdownOpen = false;
}
let uiFontSize = $state(14);
let termFontSize = $state(13);
interface Provider {
id: string;
interface Category {
id: CategoryId;
label: string;
enabled: boolean;
icon: string;
}
let providers = $state<Provider[]>([
{ id: 'claude', label: 'Claude (Anthropic)', enabled: true },
{ id: 'codex', label: 'Codex (OpenAI)', enabled: false },
{ id: 'ollama', label: 'Ollama (local)', enabled: false },
]);
const CATEGORIES: Category[] = [
{ id: 'appearance', label: 'Appearance', icon: '🎨' },
{ id: 'agents', label: 'Agents', icon: '🤖' },
{ id: 'security', label: 'Security', icon: '🔒' },
{ id: 'projects', label: 'Projects', icon: '📁' },
{ id: 'orchestration', label: 'Orchestration', icon: '⚙' },
{ id: 'advanced', label: 'Advanced', icon: '🔧' },
];
function toggleProvider(id: string) {
providers = providers.map(p => p.id === id ? { ...p, enabled: !p.enabled } : p);
}
let activeCategory = $state<CategoryId>('appearance');
function handleBackdropClick(e: MouseEvent) {
if (e.target === e.currentTarget) onClose();
@ -50,7 +41,6 @@
}
</script>
<!-- Backdrop -->
{#if open}
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<div
@ -62,109 +52,50 @@
onclick={handleBackdropClick}
onkeydown={handleKeydown}
>
<aside class="drawer-panel">
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<aside class="drawer-panel" onclick={e => e.stopPropagation()} onkeydown={e => e.stopPropagation()}>
<!-- Header -->
<header class="drawer-header">
<h2 class="drawer-title">Settings</h2>
<button class="drawer-close" onclick={onClose} aria-label="Close settings">×</button>
</header>
<!-- Body: sidebar + content -->
<div class="drawer-body">
<!-- Appearance -->
<section class="settings-section">
<h3 class="section-heading">Appearance</h3>
<div class="setting-row">
<label class="setting-label" for="theme-dropdown-btn">Theme</label>
<div class="theme-dropdown">
<button
id="theme-dropdown-btn"
class="theme-dropdown-btn"
onclick={() => themeDropdownOpen = !themeDropdownOpen}
aria-haspopup="listbox"
aria-expanded={themeDropdownOpen}
>
<span class="theme-dropdown-label">{selectedThemeLabel}</span>
<svg class="theme-chevron" class:open={themeDropdownOpen} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
{#if themeDropdownOpen}
<ul class="theme-dropdown-list" role="listbox" aria-label="Theme options">
{#each THEMES as t}
<li
class="theme-option"
class:selected={themeId === t.id}
role="option"
aria-selected={themeId === t.id}
onclick={() => selectTheme(t.id)}
onkeydown={(e) => (e.key === 'Enter' || e.key === ' ') && selectTheme(t.id)}
tabindex="0"
>{t.label}</li>
{/each}
</ul>
{/if}
</div>
</div>
<div class="setting-row">
<span class="setting-label" id="ui-font-label">UI font size</span>
<div class="font-stepper" role="group" aria-labelledby="ui-font-label">
<button
class="stepper-btn"
onclick={() => uiFontSize = Math.max(10, uiFontSize - 1)}
aria-label="Decrease UI font size"
></button>
<span class="stepper-value">{uiFontSize}px</span>
<button
class="stepper-btn"
onclick={() => uiFontSize = Math.min(24, uiFontSize + 1)}
aria-label="Increase UI font size"
>+</button>
</div>
</div>
<div class="setting-row">
<span class="setting-label" id="term-font-label">Terminal font size</span>
<div class="font-stepper" role="group" aria-labelledby="term-font-label">
<button
class="stepper-btn"
onclick={() => termFontSize = Math.max(8, termFontSize - 1)}
aria-label="Decrease terminal font size"
></button>
<span class="stepper-value">{termFontSize}px</span>
<button
class="stepper-btn"
onclick={() => termFontSize = Math.min(24, termFontSize + 1)}
aria-label="Increase terminal font size"
>+</button>
</div>
</div>
</section>
<!-- Providers -->
<section class="settings-section">
<h3 class="section-heading">Providers</h3>
{#each providers as prov (prov.id)}
<div class="setting-row">
<label class="setting-label" for="prov-{prov.id}">{prov.label}</label>
<button
id="prov-{prov.id}"
class="toggle-btn"
class:enabled={prov.enabled}
onclick={() => toggleProvider(prov.id)}
role="switch"
aria-checked={prov.enabled}
aria-label="{prov.label} {prov.enabled ? 'enabled' : 'disabled'}"
>
<span class="toggle-track">
<span class="toggle-thumb"></span>
</span>
<span class="toggle-label">{prov.enabled ? 'on' : 'off'}</span>
</button>
</div>
<!-- Category nav -->
<nav class="cat-nav" aria-label="Settings categories">
{#each CATEGORIES as cat}
<button
class="cat-btn"
class:active={activeCategory === cat.id}
onclick={() => activeCategory = cat.id}
aria-current={activeCategory === cat.id ? 'page' : undefined}
>
<span class="cat-icon" aria-hidden="true">{cat.icon}</span>
<span class="cat-label">{cat.label}</span>
</button>
{/each}
</section>
</nav>
<!-- Category content -->
<div class="cat-content">
{#if activeCategory === 'appearance'}
<AppearanceSettings />
{:else if activeCategory === 'agents'}
<AgentSettings />
{:else if activeCategory === 'security'}
<SecuritySettings />
{:else if activeCategory === 'projects'}
<ProjectSettings />
{:else if activeCategory === 'orchestration'}
<OrchestrationSettings />
{:else if activeCategory === 'advanced'}
<AdvancedSettings />
{/if}
</div>
</div>
</aside>
</div>
{/if}
@ -180,8 +111,8 @@
}
.drawer-panel {
width: 18rem;
max-width: 90vw;
width: 30rem;
max-width: 95vw;
background: var(--ctp-mantle);
border-right: 1px solid var(--ctp-surface0);
display: flex;
@ -195,6 +126,7 @@
to { transform: translateX(0); opacity: 1; }
}
/* Header */
.drawer-header {
height: 3rem;
display: flex;
@ -232,199 +164,77 @@
color: var(--ctp-text);
}
/* Body: two-column layout */
.drawer-body {
flex: 1;
min-height: 0;
display: flex;
overflow: hidden;
}
/* Category nav sidebar */
.cat-nav {
width: 8.5rem;
flex-shrink: 0;
background: var(--ctp-crust);
border-right: 1px solid var(--ctp-surface0);
display: flex;
flex-direction: column;
padding: 0.375rem 0;
gap: 0.0625rem;
overflow-y: auto;
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.drawer-body::-webkit-scrollbar { width: 0.375rem; }
.drawer-body::-webkit-scrollbar-track { background: transparent; }
.drawer-body::-webkit-scrollbar-thumb { background: var(--ctp-surface1); border-radius: 0.25rem; }
.cat-nav::-webkit-scrollbar { display: none; }
/* Sections */
.settings-section {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.section-heading {
margin: 0 0 0.25rem;
font-size: 0.6875rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ctp-overlay0);
}
.setting-row {
.cat-btn {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.25rem 0;
}
.setting-label {
font-size: 0.8125rem;
color: var(--ctp-subtext1);
}
/* Theme dropdown */
.theme-dropdown {
position: relative;
}
.theme-dropdown-btn {
display: flex;
align-items: center;
gap: 0.375rem;
background: var(--ctp-surface0);
border: 1px solid var(--ctp-surface1);
border-radius: 0.3rem;
padding: 0.2rem 0.5rem;
color: var(--ctp-mauve);
padding: 0.5rem 0.75rem;
background: transparent;
border: none;
color: var(--ctp-subtext0);
font-family: var(--ui-font-family);
font-size: 0.75rem;
font-size: 0.8125rem;
cursor: pointer;
transition: border-color 0.12s;
white-space: nowrap;
text-align: left;
border-radius: 0;
transition: background 0.1s, color 0.1s;
}
.theme-dropdown-btn:hover { border-color: var(--ctp-surface2); }
.cat-btn:hover {
background: var(--ctp-surface0);
color: var(--ctp-text);
}
.theme-dropdown-label { flex: 1; }
.cat-btn.active {
background: var(--ctp-surface0);
color: var(--ctp-text);
border-left: 2px solid var(--ctp-mauve);
padding-left: calc(0.75rem - 2px);
}
.theme-chevron {
width: 0.75rem;
height: 0.75rem;
color: var(--ctp-overlay1);
transition: transform 0.15s;
.cat-icon {
font-size: 0.875rem;
flex-shrink: 0;
}
.theme-chevron.open { transform: rotate(180deg); }
.theme-dropdown-list {
position: absolute;
right: 0;
top: calc(100% + 0.25rem);
z-index: 10;
list-style: none;
margin: 0;
padding: 0.25rem;
background: var(--ctp-mantle);
border: 1px solid var(--ctp-surface1);
border-radius: 0.375rem;
min-width: 11rem;
box-shadow: 0 0.5rem 1.25rem color-mix(in srgb, var(--ctp-crust) 60%, transparent);
display: flex;
flex-direction: column;
gap: 0.0625rem;
.cat-label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.theme-option {
padding: 0.35rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.8125rem;
color: var(--ctp-subtext1);
cursor: pointer;
transition: background 0.08s, color 0.08s;
outline: none;
/* Content area */
.cat-content {
flex: 1;
min-width: 0;
overflow-y: auto;
padding: 0.875rem;
}
.theme-option:hover,
.theme-option:focus { background: var(--ctp-surface0); color: var(--ctp-text); }
.theme-option.selected {
background: color-mix(in srgb, var(--ctp-mauve) 15%, transparent);
color: var(--ctp-mauve);
font-weight: 500;
}
/* Font stepper */
.font-stepper {
display: flex;
align-items: center;
gap: 0.375rem;
}
.stepper-btn {
width: 1.375rem;
height: 1.375rem;
background: var(--ctp-surface0);
border: 1px solid var(--ctp-surface1);
border-radius: 0.25rem;
color: var(--ctp-text);
font-size: 0.875rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.1s;
}
.stepper-btn:hover { background: var(--ctp-surface1); }
.stepper-value {
font-size: 0.8125rem;
color: var(--ctp-text);
min-width: 2.5rem;
text-align: center;
}
/* Toggle switch */
.toggle-btn {
display: flex;
align-items: center;
gap: 0.375rem;
background: transparent;
border: none;
cursor: pointer;
padding: 0;
}
.toggle-track {
width: 2rem;
height: 1.125rem;
background: var(--ctp-surface1);
border-radius: 0.5625rem;
position: relative;
transition: background 0.15s;
display: block;
}
.toggle-btn.enabled .toggle-track {
background: var(--ctp-mauve);
}
.toggle-thumb {
position: absolute;
top: 0.1875rem;
left: 0.1875rem;
width: 0.75rem;
height: 0.75rem;
background: var(--ctp-base);
border-radius: 50%;
transition: transform 0.15s;
display: block;
}
.toggle-btn.enabled .toggle-thumb {
transform: translateX(0.875rem);
}
.toggle-label {
font-size: 0.75rem;
color: var(--ctp-subtext0);
min-width: 1.5rem;
}
.toggle-btn.enabled .toggle-label {
color: var(--ctp-mauve);
}
.cat-content::-webkit-scrollbar { width: 0.375rem; }
.cat-content::-webkit-scrollbar-track { background: transparent; }
.cat-content::-webkit-scrollbar-thumb { background: var(--ctp-surface1); border-radius: 0.25rem; }
</style>