feat(settings): add provider config UI and sidecar routing (Phase 2+3)
Add Providers section to SettingsTab with collapsible per-provider config panels and per-project provider dropdown. Implement provider-based sidecar runner selection and multi-provider env var stripping in Rust.
This commit is contained in:
parent
1efcb13869
commit
11b00f18f8
1 changed files with 236 additions and 0 deletions
|
|
@ -18,6 +18,8 @@
|
|||
import { THEME_LIST, getPalette, type ThemeId } from '../../styles/themes';
|
||||
import { listProfiles, type ClaudeProfile } from '../../adapters/claude-bridge';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { getProviders } from '../../providers/registry.svelte';
|
||||
import type { ProviderId, ProviderSettings } from '../../providers/types';
|
||||
|
||||
const PROJECT_ICONS = [
|
||||
'📁', '🚀', '🤖', '🌐', '🔧', '🎮', '📱', '💻',
|
||||
|
|
@ -28,6 +30,12 @@
|
|||
// Claude profiles for account selector
|
||||
let profiles = $state<ClaudeProfile[]>([]);
|
||||
|
||||
// Provider settings (keyed by ProviderId)
|
||||
let providerSettings = $state<Record<string, ProviderSettings>>({});
|
||||
let expandedProvider = $state<string | null>(null);
|
||||
let registeredProviders = $derived(getProviders());
|
||||
let providerDropdownOpenFor = $state<string | null>(null);
|
||||
|
||||
let activeGroupId = $derived(getActiveGroupId());
|
||||
let activeGroup = $derived(getActiveGroup());
|
||||
let activeProjectId = $derived(getActiveProjectId());
|
||||
|
|
@ -131,6 +139,14 @@
|
|||
} catch {
|
||||
profiles = [];
|
||||
}
|
||||
|
||||
// Load provider settings
|
||||
try {
|
||||
const raw = await getSetting('provider_settings');
|
||||
if (raw) providerSettings = JSON.parse(raw);
|
||||
} catch {
|
||||
providerSettings = {};
|
||||
}
|
||||
});
|
||||
|
||||
function applyCssProp(prop: string, value: string) {
|
||||
|
|
@ -201,12 +217,35 @@
|
|||
await setTheme(themeId);
|
||||
}
|
||||
|
||||
async function saveProviderSettings() {
|
||||
await saveGlobalSetting('provider_settings', JSON.stringify(providerSettings));
|
||||
}
|
||||
|
||||
function toggleProviderEnabled(providerId: string) {
|
||||
const current = providerSettings[providerId] ?? { enabled: true, config: {} };
|
||||
providerSettings[providerId] = { ...current, enabled: !current.enabled };
|
||||
providerSettings = { ...providerSettings };
|
||||
saveProviderSettings();
|
||||
}
|
||||
|
||||
function setProviderModel(providerId: string, model: string) {
|
||||
const current = providerSettings[providerId] ?? { enabled: true, config: {} };
|
||||
providerSettings[providerId] = { ...current, defaultModel: model || undefined };
|
||||
providerSettings = { ...providerSettings };
|
||||
saveProviderSettings();
|
||||
}
|
||||
|
||||
function isProviderEnabled(providerId: string): boolean {
|
||||
return providerSettings[providerId]?.enabled ?? true;
|
||||
}
|
||||
|
||||
function handleClickOutside(e: MouseEvent) {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.closest('.custom-dropdown')) {
|
||||
themeDropdownOpen = false;
|
||||
uiFontDropdownOpen = false;
|
||||
termFontDropdownOpen = false;
|
||||
providerDropdownOpenFor = null;
|
||||
}
|
||||
if (!target.closest('.icon-field')) {
|
||||
iconPickerOpenFor = null;
|
||||
|
|
@ -504,6 +543,63 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<h2>Providers</h2>
|
||||
<div class="provider-list">
|
||||
{#each registeredProviders as provider}
|
||||
<div class="provider-panel" class:disabled={!isProviderEnabled(provider.id)}>
|
||||
<button
|
||||
class="provider-header"
|
||||
onclick={() => { expandedProvider = expandedProvider === provider.id ? null : provider.id; }}
|
||||
>
|
||||
<span class="provider-name">{provider.name}</span>
|
||||
<span class="provider-desc">{provider.description}</span>
|
||||
<span class="provider-chevron">{expandedProvider === provider.id ? '\u25B4' : '\u25BE'}</span>
|
||||
</button>
|
||||
{#if expandedProvider === provider.id}
|
||||
<div class="provider-body">
|
||||
<div class="setting-field">
|
||||
<label class="setting-label toggle-label">
|
||||
<span>Enabled</span>
|
||||
<button
|
||||
class="toggle-switch"
|
||||
class:on={isProviderEnabled(provider.id)}
|
||||
role="switch"
|
||||
aria-checked={isProviderEnabled(provider.id)}
|
||||
onclick={() => toggleProviderEnabled(provider.id)}
|
||||
>
|
||||
<span class="toggle-thumb"></span>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
{#if provider.capabilities.hasModelSelection}
|
||||
<div class="setting-field">
|
||||
<span class="setting-label">Default model</span>
|
||||
<input
|
||||
value={providerSettings[provider.id]?.defaultModel ?? provider.defaultModel ?? ''}
|
||||
placeholder={provider.defaultModel ?? 'default'}
|
||||
onchange={e => setProviderModel(provider.id, (e.target as HTMLInputElement).value)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="provider-caps">
|
||||
<span class="setting-label">Capabilities</span>
|
||||
<div class="caps-grid">
|
||||
{#if provider.capabilities.hasProfiles}<span class="cap-badge">Profiles</span>{/if}
|
||||
{#if provider.capabilities.hasSkills}<span class="cap-badge">Skills</span>{/if}
|
||||
{#if provider.capabilities.supportsSubagents}<span class="cap-badge">Subagents</span>{/if}
|
||||
{#if provider.capabilities.supportsCost}<span class="cap-badge">Cost tracking</span>{/if}
|
||||
{#if provider.capabilities.supportsResume}<span class="cap-badge">Resume</span>{/if}
|
||||
{#if provider.capabilities.hasSandbox}<span class="cap-badge">Sandbox</span>{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<h2>Groups</h2>
|
||||
<div class="group-list">
|
||||
|
|
@ -637,6 +733,44 @@
|
|||
{/if}
|
||||
</div>
|
||||
|
||||
{#if registeredProviders.length > 1}
|
||||
<div class="card-field">
|
||||
<span class="card-field-label">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>
|
||||
Provider
|
||||
</span>
|
||||
<div class="custom-dropdown">
|
||||
<button
|
||||
class="dropdown-trigger provider-trigger"
|
||||
onclick={() => { providerDropdownOpenFor = providerDropdownOpenFor === project.id ? null : project.id; }}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={providerDropdownOpenFor === project.id}
|
||||
>
|
||||
<span class="dropdown-label">{registeredProviders.find(p => p.id === (project.provider ?? 'claude'))?.name ?? 'Claude Code'}</span>
|
||||
<span class="dropdown-arrow">{providerDropdownOpenFor === project.id ? '\u25B4' : '\u25BE'}</span>
|
||||
</button>
|
||||
{#if providerDropdownOpenFor === project.id}
|
||||
<div class="dropdown-menu" role="listbox">
|
||||
{#each registeredProviders.filter(p => isProviderEnabled(p.id)) as prov}
|
||||
<button
|
||||
class="dropdown-option"
|
||||
class:active={(project.provider ?? 'claude') === prov.id}
|
||||
role="option"
|
||||
aria-selected={(project.provider ?? 'claude') === prov.id}
|
||||
onclick={() => {
|
||||
updateProject(activeGroupId, project.id, { provider: prov.id });
|
||||
providerDropdownOpenFor = null;
|
||||
}}
|
||||
>
|
||||
<span class="dropdown-option-label">{prov.name}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="card-footer">
|
||||
<button class="btn-remove" onclick={() => removeProject(activeGroupId, project.id)}>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>
|
||||
|
|
@ -1394,4 +1528,106 @@
|
|||
.toggle-switch.on .toggle-thumb {
|
||||
transform: translateX(0.875rem);
|
||||
}
|
||||
|
||||
/* Provider section */
|
||||
.provider-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
|
||||
.provider-panel {
|
||||
background: var(--ctp-surface0);
|
||||
border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 0.375rem;
|
||||
overflow: hidden;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.provider-panel.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.provider-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.625rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--ctp-text);
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.provider-header:hover {
|
||||
background: var(--ctp-base);
|
||||
}
|
||||
|
||||
.provider-name {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.provider-desc {
|
||||
flex: 1;
|
||||
color: var(--ctp-overlay0);
|
||||
font-size: 0.7rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.provider-chevron {
|
||||
color: var(--ctp-overlay0);
|
||||
font-size: 0.7rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.provider-body {
|
||||
padding: 0.5rem 0.625rem;
|
||||
border-top: 1px solid var(--ctp-surface1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.provider-body > .setting-field > input {
|
||||
padding: 0.375rem 0.625rem;
|
||||
background: var(--ctp-base);
|
||||
border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 0.25rem;
|
||||
color: var(--ctp-text);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.provider-caps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.caps-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.cap-badge {
|
||||
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;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.provider-trigger {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: var(--ctp-base);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue