feat(settings): Sprint 2-3 — extract Orchestration (238) + Advanced (321) settings
This commit is contained in:
parent
438f986a08
commit
9769e7f29a
2 changed files with 559 additions and 0 deletions
321
src/lib/settings/categories/AdvancedSettings.svelte
Normal file
321
src/lib/settings/categories/AdvancedSettings.svelte
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { getSetting, setSetting } from '../../adapters/settings-bridge';
|
||||
import { getPluginEntries, setPluginEnabled, reloadAllPlugins } from '../../stores/plugins.svelte';
|
||||
import { checkForUpdates, getCurrentVersion, getLastCheckTimestamp } from '../../utils/updater';
|
||||
import type { UpdateInfo } from '../../utils/updater';
|
||||
|
||||
let pluginEntries = $derived(getPluginEntries());
|
||||
let pluginAutoUpdate = $state(false);
|
||||
|
||||
let appVersion = $state('');
|
||||
let updateLastCheck = $state('');
|
||||
let updateChecking = $state(false);
|
||||
let updateCheckResult = $state<UpdateInfo | null>(null);
|
||||
|
||||
let relayUrls = $state('');
|
||||
let connectionTimeout = $state(30);
|
||||
|
||||
let logLevel = $state<'trace' | 'debug' | 'info' | 'warn' | 'error'>('info');
|
||||
let otlpEndpoint = $state('');
|
||||
|
||||
let importFileInput: HTMLInputElement | undefined = $state();
|
||||
|
||||
onMount(async () => {
|
||||
const [version, rawAutoUpdate, rawRelays, rawTimeout, rawLog, rawOtlp] = await Promise.all([
|
||||
getCurrentVersion(),
|
||||
getSetting('plugin_auto_update'),
|
||||
getSetting('relay_urls'),
|
||||
getSetting('connection_timeout'),
|
||||
getSetting('log_level'),
|
||||
getSetting('otlp_endpoint'),
|
||||
]);
|
||||
appVersion = version;
|
||||
pluginAutoUpdate = rawAutoUpdate === 'true';
|
||||
relayUrls = rawRelays ?? '';
|
||||
connectionTimeout = rawTimeout ? parseInt(rawTimeout, 10) : 30;
|
||||
if (rawLog === 'trace' || rawLog === 'debug' || rawLog === 'info' || rawLog === 'warn' || rawLog === 'error') logLevel = rawLog;
|
||||
otlpEndpoint = rawOtlp ?? '';
|
||||
|
||||
const ts = getLastCheckTimestamp();
|
||||
if (ts) updateLastCheck = new Date(ts).toLocaleString();
|
||||
});
|
||||
|
||||
async function save(key: string, value: string) {
|
||||
try { await setSetting(key, value); } catch (e) { console.error(`Failed to save ${key}:`, e); }
|
||||
}
|
||||
|
||||
async function handleCheckForUpdates() {
|
||||
updateChecking = true;
|
||||
try {
|
||||
updateCheckResult = await checkForUpdates();
|
||||
const ts = getLastCheckTimestamp();
|
||||
if (ts) updateLastCheck = new Date(ts).toLocaleString();
|
||||
} catch (e) {
|
||||
console.error('Update check failed:', e);
|
||||
} finally {
|
||||
updateChecking = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReloadPlugins() {
|
||||
try { await reloadAllPlugins(); } catch (e) { console.error('Plugin reload failed:', e); }
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
const keys = [
|
||||
'default_shell', 'default_cwd', 'files_save_on_blur', 'theme', 'ui_font_family',
|
||||
'ui_font_size', 'term_font_family', 'term_font_size', 'default_permission_mode',
|
||||
'system_prompt_template', 'context_pressure_warn', 'context_pressure_critical',
|
||||
'budget_monthly_tokens', 'router_profile', 'provider_settings', 'plugin_auto_update',
|
||||
'relay_urls', 'connection_timeout', 'log_level', 'otlp_endpoint',
|
||||
];
|
||||
const settings: Record<string, string> = {};
|
||||
await Promise.all(keys.map(async (k) => {
|
||||
const v = await getSetting(k);
|
||||
if (v != null) settings[k] = v;
|
||||
}));
|
||||
const blob = new Blob([JSON.stringify(settings, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'bterminal-settings.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function handleImport(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
const text = await file.text();
|
||||
const imported = JSON.parse(text) as Record<string, string>;
|
||||
const sensitive = ['secrets', 'api_key', 'token', 'password'];
|
||||
for (const [key, value] of Object.entries(imported)) {
|
||||
if (sensitive.some(s => key.toLowerCase().includes(s))) continue;
|
||||
if (typeof value === 'string') await setSetting(key, value);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Settings import failed:', err);
|
||||
}
|
||||
input.value = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="section">
|
||||
<h2>Plugins</h2>
|
||||
{#if pluginEntries.length === 0}
|
||||
<p class="empty">No plugins found in ~/.config/agor/plugins/</p>
|
||||
{:else}
|
||||
<div class="plug-list">
|
||||
{#each pluginEntries as entry (entry.meta.id)}
|
||||
<div class="plug-row">
|
||||
<div class="plug-top">
|
||||
<div class="plug-info">
|
||||
<span class="plug-name">{entry.meta.name}</span>
|
||||
<span class="plug-ver">v{entry.meta.version}</span>
|
||||
{#if entry.status === 'loaded'}
|
||||
<span class="plug-badge loaded">loaded</span>
|
||||
{:else if entry.status === 'error'}
|
||||
<span class="plug-badge err" title={entry.error ?? 'Error'}>error</span>
|
||||
{:else if entry.status === 'disabled'}
|
||||
<span class="plug-badge off">disabled</span>
|
||||
{:else}
|
||||
<span class="plug-badge disc">discovered</span>
|
||||
{/if}
|
||||
</div>
|
||||
<button class="toggle" class:on={entry.status !== 'disabled'} role="switch"
|
||||
aria-checked={entry.status !== 'disabled'} aria-label="Toggle {entry.meta.name}"
|
||||
onclick={async () => { await setPluginEnabled(entry.meta.id, entry.status === 'disabled'); }}>
|
||||
<span class="thumb"></span>
|
||||
</button>
|
||||
</div>
|
||||
{#if entry.meta.description}
|
||||
<p class="plug-desc">{entry.meta.description}</p>
|
||||
{/if}
|
||||
{#if entry.error}
|
||||
<p class="plug-err">{entry.error}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="fields" style="margin-top: 0.5rem;">
|
||||
<div class="field" id="setting-plugin-auto-update">
|
||||
<label class="lbl toggle-row">
|
||||
<span>Auto-update plugins</span>
|
||||
<button class="toggle" class:on={pluginAutoUpdate} role="switch" aria-checked={pluginAutoUpdate}
|
||||
onclick={() => { pluginAutoUpdate = !pluginAutoUpdate; save('plugin_auto_update', String(pluginAutoUpdate)); }}>
|
||||
<span class="thumb"></span>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
<button class="btn" onclick={handleReloadPlugins}>Reload Plugins</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Updates</h2>
|
||||
<div class="fields">
|
||||
<div class="field">
|
||||
<span class="lbl">Current version</span>
|
||||
<span class="val">{appVersion || '...'}</span>
|
||||
</div>
|
||||
{#if updateLastCheck}
|
||||
<div class="field">
|
||||
<span class="lbl">Last checked</span>
|
||||
<span class="val muted">{updateLastCheck}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if updateCheckResult?.available}
|
||||
<div class="field">
|
||||
<span class="lbl">Available</span>
|
||||
<span class="val available">v{updateCheckResult.version}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<button class="btn" onclick={handleCheckForUpdates} disabled={updateChecking}>
|
||||
{updateChecking ? 'Checking...' : 'Check for Updates'}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Multi-Machine</h2>
|
||||
<div class="fields">
|
||||
<div class="field" id="setting-relay-urls">
|
||||
<label for="adv-relays" class="lbl">Relay URLs (one per line)</label>
|
||||
<textarea id="adv-relays" class="ta" value={relayUrls} placeholder="wss://relay.example.com:8443" rows={3}
|
||||
onchange={e => { relayUrls = (e.target as HTMLTextAreaElement).value; save('relay_urls', relayUrls); }}></textarea>
|
||||
<span class="hint">WebSocket relay endpoints for remote machines</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="adv-timeout" class="lbl">Connection timeout</label>
|
||||
<div class="num-row">
|
||||
<input id="adv-timeout" type="number" min="5" max="120" value={connectionTimeout} class="num"
|
||||
onchange={e => { const n = parseInt((e.target as HTMLInputElement).value, 10); if (!isNaN(n) && n >= 5 && n <= 120) { connectionTimeout = n; save('connection_timeout', String(n)); } }} />
|
||||
<span class="unit">seconds</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Developer / Debug</h2>
|
||||
<div class="fields">
|
||||
<div class="field" id="setting-log-level">
|
||||
<span class="lbl">Log level</span>
|
||||
<div class="seg-group">
|
||||
{#each ['trace', 'debug', 'info', 'warn', 'error'] as level}
|
||||
<button class="seg-btn" class:active={logLevel === level}
|
||||
onclick={() => { logLevel = level as typeof logLevel; save('log_level', logLevel); }}>{level}</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field" id="setting-otlp">
|
||||
<label for="adv-otlp" class="lbl">OTLP endpoint</label>
|
||||
<input id="adv-otlp" value={otlpEndpoint} placeholder="http://localhost:4318"
|
||||
onchange={e => { otlpEndpoint = (e.target as HTMLInputElement).value; save('otlp_endpoint', otlpEndpoint); }} />
|
||||
<span class="hint">OpenTelemetry HTTP endpoint for trace export</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Import / Export</h2>
|
||||
<div class="fields">
|
||||
<div class="btn-row">
|
||||
<button class="btn" onclick={handleExport}>Export Settings</button>
|
||||
<button class="btn" onclick={() => importFileInput?.click()}>Import Settings</button>
|
||||
<input bind:this={importFileInput} type="file" accept=".json" style="display:none" onchange={handleImport} />
|
||||
</div>
|
||||
<span class="hint">Export downloads a JSON file. Import restores non-sensitive settings.</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Keyboard Shortcuts</h2>
|
||||
<p class="hint-block">Keyboard shortcuts can be customized in <code>~/.claude/keybindings.json</code></p>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
h2 { font-size: 0.875rem; font-weight: 600; color: var(--ctp-text); margin: 0 0 0.625rem; padding-bottom: 0.375rem; border-bottom: 1px solid var(--ctp-surface0); }
|
||||
.section { margin-bottom: 1.25rem; }
|
||||
.fields { display: flex; flex-direction: column; gap: 0.625rem; }
|
||||
.field { display: flex; flex-direction: column; gap: 0.25rem; }
|
||||
.lbl { font-size: 0.7rem; color: var(--ctp-subtext0); text-transform: uppercase; letter-spacing: 0.03em; }
|
||||
.hint { font-size: 0.625rem; color: var(--ctp-overlay0); }
|
||||
.val { font-size: 0.8rem; color: var(--ctp-text); }
|
||||
.muted { color: var(--ctp-overlay0); }
|
||||
.available { color: var(--ctp-green); font-weight: 600; }
|
||||
.empty { font-size: 0.75rem; color: var(--ctp-overlay0); margin: 0.25rem 0; }
|
||||
|
||||
input, .ta {
|
||||
padding: 0.375rem 0.625rem; background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 0.25rem; color: var(--ctp-text); font-size: 0.8rem;
|
||||
}
|
||||
.ta { font-family: var(--term-font-family, monospace); resize: vertical; min-height: 3rem; line-height: 1.4; }
|
||||
.ta:focus, input:focus { outline: none; border-color: var(--ctp-blue); }
|
||||
.ta::placeholder, input::placeholder { color: var(--ctp-overlay0); }
|
||||
|
||||
.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); }
|
||||
|
||||
.seg-group { display: flex; border-radius: 0.25rem; overflow: hidden; border: 1px solid var(--ctp-surface1); }
|
||||
.seg-btn {
|
||||
flex: 1; padding: 0.25rem 0.5rem; border: none; background: var(--ctp-surface0);
|
||||
color: var(--ctp-overlay1); font-size: 0.7rem; font-weight: 500; cursor: pointer; transition: background 0.12s, color 0.12s;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.seg-btn:not(:last-child) { border-right: 1px solid var(--ctp-surface1); }
|
||||
.seg-btn:hover { background: var(--ctp-surface1); color: var(--ctp-subtext1); }
|
||||
.seg-btn.active { background: color-mix(in srgb, var(--ctp-blue) 20%, var(--ctp-surface0)); color: var(--ctp-blue); font-weight: 600; }
|
||||
|
||||
.num-row { display: flex; align-items: center; gap: 0.25rem; }
|
||||
.num {
|
||||
width: 4.5rem; padding: 0.375rem 0.625rem; background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 0.25rem; color: var(--ctp-text); font-size: 0.8rem; -moz-appearance: textfield;
|
||||
}
|
||||
.num::-webkit-inner-spin-button, .num::-webkit-outer-spin-button { -webkit-appearance: none; }
|
||||
.unit { font-size: 0.7rem; color: var(--ctp-overlay0); }
|
||||
|
||||
.btn {
|
||||
padding: 0.375rem 0.75rem; background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 0.25rem; color: var(--ctp-text); font-size: 0.75rem; cursor: pointer; transition: background 0.12s;
|
||||
}
|
||||
.btn:hover { background: var(--ctp-surface1); }
|
||||
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.btn-row { display: flex; gap: 0.5rem; }
|
||||
|
||||
.plug-list { display: flex; flex-direction: column; gap: 0.375rem; }
|
||||
.plug-row { background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1); border-radius: 0.375rem; padding: 0.5rem 0.625rem; }
|
||||
.plug-top { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
|
||||
.plug-info { display: flex; align-items: center; gap: 0.375rem; flex: 1; min-width: 0; }
|
||||
.plug-name { font-size: 0.8rem; font-weight: 600; color: var(--ctp-text); white-space: nowrap; }
|
||||
.plug-ver { font-size: 0.65rem; color: var(--ctp-overlay0); }
|
||||
.plug-badge {
|
||||
padding: 0.0625rem 0.375rem; border-radius: 0.75rem; font-size: 0.575rem; font-weight: 600;
|
||||
text-transform: uppercase; letter-spacing: 0.03em; white-space: nowrap;
|
||||
}
|
||||
.plug-badge.loaded { background: color-mix(in srgb, var(--ctp-green) 15%, transparent); color: var(--ctp-green); }
|
||||
.plug-badge.err { background: color-mix(in srgb, var(--ctp-red) 15%, transparent); color: var(--ctp-red); }
|
||||
.plug-badge.off { background: color-mix(in srgb, var(--ctp-overlay0) 15%, transparent); color: var(--ctp-overlay0); }
|
||||
.plug-badge.disc { background: color-mix(in srgb, var(--ctp-blue) 15%, transparent); color: var(--ctp-blue); }
|
||||
.plug-desc { font-size: 0.7rem; color: var(--ctp-overlay1); margin: 0.25rem 0 0; }
|
||||
.plug-err { font-size: 0.65rem; color: var(--ctp-red); margin: 0.25rem 0 0; }
|
||||
|
||||
.hint-block { font-size: 0.75rem; color: var(--ctp-overlay1); margin: 0; line-height: 1.5; }
|
||||
.hint-block code {
|
||||
padding: 0.125rem 0.375rem; background: var(--ctp-surface0); border-radius: 0.1875rem;
|
||||
font-family: var(--term-font-family, monospace); font-size: 0.7rem; color: var(--ctp-text);
|
||||
}
|
||||
</style>
|
||||
238
src/lib/settings/categories/OrchestrationSettings.svelte
Normal file
238
src/lib/settings/categories/OrchestrationSettings.svelte
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { getSetting, setSetting } from '../../adapters/settings-bridge';
|
||||
import { ANCHOR_BUDGET_SCALES, ANCHOR_BUDGET_SCALE_LABELS, type AnchorBudgetScale } from '../../types/anchors';
|
||||
import { WAKE_STRATEGIES, WAKE_STRATEGY_LABELS, WAKE_STRATEGY_DESCRIPTIONS, type WakeStrategy } from '../../types/wake';
|
||||
|
||||
let stallThreshold = $state(15);
|
||||
let anchorBudget = $state<AnchorBudgetScale>('medium');
|
||||
let autoAnchor = $state(true);
|
||||
let wakeStrategy = $state<WakeStrategy>('persistent');
|
||||
let wakeThreshold = $state(50);
|
||||
let notifDesktop = $state(true);
|
||||
let notifTypes = $state<Set<string>>(new Set(['complete', 'error', 'crash', 'stall']));
|
||||
let memoryTtl = $state('');
|
||||
let memoryExtract = $state(false);
|
||||
|
||||
const NOTIF_TYPE_OPTIONS = ['complete', 'error', 'crash', 'stall'] as const;
|
||||
|
||||
onMount(async () => {
|
||||
const [rawStall, rawBudget, rawAutoAnchor, rawWake, rawThresh, rawDesktop, rawTypes, rawTtl, rawExtract] = await Promise.all([
|
||||
getSetting('stall_threshold'), getSetting('anchor_budget_scale'), getSetting('auto_anchor'),
|
||||
getSetting('wake_strategy'), getSetting('wake_threshold'), getSetting('notif_desktop'),
|
||||
getSetting('notif_types'), getSetting('memory_ttl'), getSetting('memory_extract'),
|
||||
]);
|
||||
if (rawStall) { const n = parseInt(rawStall, 10); if (!isNaN(n)) stallThreshold = n; }
|
||||
if (rawBudget && ANCHOR_BUDGET_SCALES.includes(rawBudget as AnchorBudgetScale)) anchorBudget = rawBudget as AnchorBudgetScale;
|
||||
autoAnchor = rawAutoAnchor !== 'false';
|
||||
if (rawWake && WAKE_STRATEGIES.includes(rawWake as WakeStrategy)) wakeStrategy = rawWake as WakeStrategy;
|
||||
if (rawThresh) { const n = parseInt(rawThresh, 10); if (!isNaN(n)) wakeThreshold = n; }
|
||||
notifDesktop = rawDesktop !== 'false';
|
||||
if (rawTypes) { try { const arr = JSON.parse(rawTypes); if (Array.isArray(arr)) notifTypes = new Set(arr); } catch { /* keep default */ } }
|
||||
memoryTtl = rawTtl ?? '';
|
||||
memoryExtract = rawExtract === 'true';
|
||||
});
|
||||
|
||||
async function save(key: string, value: string) {
|
||||
try { await setSetting(key, value); } catch (e) { console.error(`Failed to save ${key}:`, e); }
|
||||
}
|
||||
|
||||
function toggleNotifType(type: string) {
|
||||
const next = new Set(notifTypes);
|
||||
if (next.has(type)) next.delete(type); else next.add(type);
|
||||
notifTypes = next;
|
||||
save('notif_types', JSON.stringify([...notifTypes]));
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="section">
|
||||
<h2>Health Monitoring</h2>
|
||||
<div class="fields">
|
||||
<div class="field" id="setting-stall-threshold">
|
||||
<label for="orch-stall" class="lbl">Stall threshold</label>
|
||||
<div class="slider-row">
|
||||
<input id="orch-stall" type="range" min="5" max="60" step="5" bind:value={stallThreshold}
|
||||
onchange={() => save('stall_threshold', String(stallThreshold))} />
|
||||
<span class="slider-val">{stallThreshold} min</span>
|
||||
</div>
|
||||
<span class="hint">Agent marked as stalled after this idle duration</span>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="lbl">Context pressure thresholds</span>
|
||||
<span class="hint">Configure warning and critical % in Agent Settings</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Session Anchors</h2>
|
||||
<div class="fields">
|
||||
<div class="field" id="setting-anchor-budget">
|
||||
<span class="lbl">Anchor budget scale</span>
|
||||
<div class="seg-group">
|
||||
{#each ANCHOR_BUDGET_SCALES as scale}
|
||||
<button class="seg-btn" class:active={anchorBudget === scale}
|
||||
onclick={() => { anchorBudget = scale; save('anchor_budget_scale', scale); }}>
|
||||
{ANCHOR_BUDGET_SCALE_LABELS[scale]}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<span class="hint">Token budget reserved for re-injected anchor turns</span>
|
||||
</div>
|
||||
<div class="field" id="setting-auto-anchor">
|
||||
<label class="lbl toggle-row">
|
||||
<span>Auto-anchor on compaction</span>
|
||||
<button class="toggle" class:on={autoAnchor} role="switch" aria-checked={autoAnchor}
|
||||
onclick={() => { autoAnchor = !autoAnchor; save('auto_anchor', String(autoAnchor)); }}>
|
||||
<span class="thumb"></span>
|
||||
</button>
|
||||
</label>
|
||||
<span class="hint">Automatically anchor top turns when context compacts</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Wake Scheduler</h2>
|
||||
<div class="fields">
|
||||
<div class="field" id="setting-wake-strategy">
|
||||
<span class="lbl">Wake strategy</span>
|
||||
<div class="seg-group">
|
||||
{#each WAKE_STRATEGIES as strategy}
|
||||
<button class="seg-btn" class:active={wakeStrategy === strategy}
|
||||
onclick={() => { wakeStrategy = strategy; save('wake_strategy', strategy); }}>
|
||||
{WAKE_STRATEGY_LABELS[strategy]}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<span class="hint">{WAKE_STRATEGY_DESCRIPTIONS[wakeStrategy]}</span>
|
||||
</div>
|
||||
{#if wakeStrategy === 'smart'}
|
||||
<div class="field" id="setting-wake-threshold">
|
||||
<label for="orch-wake-thresh" class="lbl">Wake threshold</label>
|
||||
<div class="slider-row">
|
||||
<input id="orch-wake-thresh" type="range" min="0" max="100" step="1" bind:value={wakeThreshold}
|
||||
onchange={() => save('wake_threshold', String(wakeThreshold))} />
|
||||
<span class="slider-val">{wakeThreshold}%</span>
|
||||
</div>
|
||||
<span class="hint">Manager only wakes when signal score exceeds this threshold</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Notifications</h2>
|
||||
<div class="fields">
|
||||
<div class="field" id="setting-notif-desktop">
|
||||
<label class="lbl toggle-row">
|
||||
<span>Desktop notifications</span>
|
||||
<button class="toggle" class:on={notifDesktop} role="switch" aria-checked={notifDesktop}
|
||||
onclick={() => { notifDesktop = !notifDesktop; save('notif_desktop', String(notifDesktop)); }}>
|
||||
<span class="thumb"></span>
|
||||
</button>
|
||||
</label>
|
||||
<span class="hint">Show OS-level notifications for agent events</span>
|
||||
</div>
|
||||
<div class="field" id="setting-notif-types">
|
||||
<span class="lbl">Notification types</span>
|
||||
<div class="chip-group">
|
||||
{#each NOTIF_TYPE_OPTIONS as type}
|
||||
<button class="chip" class:selected={notifTypes.has(type)} onclick={() => toggleNotifType(type)}>
|
||||
{type}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<span class="hint">Which agent events trigger notifications</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h2>Agent Memory</h2>
|
||||
<div class="fields">
|
||||
<div class="field" id="setting-memory-ttl">
|
||||
<label for="orch-ttl" class="lbl">Memory TTL <span class="pro">Pro</span></label>
|
||||
<div class="num-row">
|
||||
<input id="orch-ttl" type="number" min="1" max="365" value={memoryTtl} class="num" placeholder="30"
|
||||
onchange={e => { memoryTtl = (e.target as HTMLInputElement).value; save('memory_ttl', memoryTtl); }} />
|
||||
<span class="unit">days</span>
|
||||
</div>
|
||||
<span class="hint">Auto-expire extracted memories after this duration</span>
|
||||
</div>
|
||||
<div class="field" id="setting-memory-extract">
|
||||
<label class="lbl toggle-row">
|
||||
<span>Auto-extract insights <span class="pro">Pro</span></span>
|
||||
<button class="toggle" class:on={memoryExtract} role="switch" aria-checked={memoryExtract}
|
||||
onclick={() => { memoryExtract = !memoryExtract; save('memory_extract', String(memoryExtract)); }}>
|
||||
<span class="thumb"></span>
|
||||
</button>
|
||||
</label>
|
||||
<span class="hint">Automatically extract reusable insights from agent sessions</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
h2 { font-size: 0.875rem; font-weight: 600; color: var(--ctp-text); margin: 0 0 0.625rem; padding-bottom: 0.375rem; border-bottom: 1px solid var(--ctp-surface0); }
|
||||
.section { margin-bottom: 1.25rem; }
|
||||
.fields { display: flex; flex-direction: column; gap: 0.625rem; }
|
||||
.field { display: flex; flex-direction: column; gap: 0.25rem; }
|
||||
.lbl { font-size: 0.7rem; color: var(--ctp-subtext0); text-transform: uppercase; letter-spacing: 0.03em; }
|
||||
.hint { font-size: 0.625rem; color: var(--ctp-overlay0); }
|
||||
|
||||
.seg-group { display: flex; border-radius: 0.25rem; overflow: hidden; border: 1px solid var(--ctp-surface1); }
|
||||
.seg-btn {
|
||||
flex: 1; padding: 0.25rem 0.5rem; border: none; background: var(--ctp-surface0);
|
||||
color: var(--ctp-overlay1); font-size: 0.7rem; font-weight: 500; cursor: pointer; transition: background 0.12s, color 0.12s;
|
||||
}
|
||||
.seg-btn:not(:last-child) { border-right: 1px solid var(--ctp-surface1); }
|
||||
.seg-btn:hover { background: var(--ctp-surface1); color: var(--ctp-subtext1); }
|
||||
.seg-btn.active { background: color-mix(in srgb, var(--ctp-blue) 20%, var(--ctp-surface0)); color: var(--ctp-blue); font-weight: 600; }
|
||||
|
||||
.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); }
|
||||
|
||||
.slider-row { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.slider-row input[type="range"] {
|
||||
flex: 1; height: 0.25rem; -webkit-appearance: none; appearance: none;
|
||||
background: var(--ctp-surface1); border-radius: 0.125rem; outline: none;
|
||||
}
|
||||
.slider-row input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none; width: 0.875rem; height: 0.875rem; border-radius: 50%;
|
||||
background: var(--ctp-blue); cursor: pointer; border: none;
|
||||
}
|
||||
.slider-val { font-size: 0.7rem; color: var(--ctp-text); font-weight: 600; min-width: 3rem; text-align: right; }
|
||||
|
||||
.num-row { display: flex; align-items: center; gap: 0.25rem; }
|
||||
.num {
|
||||
width: 4.5rem; padding: 0.375rem 0.625rem; background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 0.25rem; color: var(--ctp-text); font-size: 0.8rem; -moz-appearance: textfield;
|
||||
}
|
||||
.num::-webkit-inner-spin-button, .num::-webkit-outer-spin-button { -webkit-appearance: none; }
|
||||
.unit { font-size: 0.7rem; color: var(--ctp-overlay0); }
|
||||
|
||||
.chip-group { display: flex; flex-wrap: wrap; gap: 0.25rem; }
|
||||
.chip {
|
||||
padding: 0.1875rem 0.5rem; border: 1px solid var(--ctp-surface1); border-radius: 0.75rem;
|
||||
background: var(--ctp-surface0); color: var(--ctp-overlay1); font-size: 0.65rem; font-weight: 500;
|
||||
cursor: pointer; transition: background 0.12s, color 0.12s, border-color 0.12s; text-transform: capitalize;
|
||||
}
|
||||
.chip:hover { background: var(--ctp-surface1); color: var(--ctp-subtext1); }
|
||||
.chip.selected { background: color-mix(in srgb, var(--ctp-blue) 15%, var(--ctp-surface0)); color: var(--ctp-blue); border-color: var(--ctp-blue); }
|
||||
|
||||
.pro {
|
||||
display: inline-block; font-size: 0.5625rem; padding: 0.0625rem 0.3125rem; background: var(--ctp-peach);
|
||||
color: var(--ctp-base); border-radius: 0.125rem; font-weight: 700; text-transform: uppercase;
|
||||
letter-spacing: 0.04em; vertical-align: middle; margin-left: 0.375rem;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue