fix(settings): replace console.error with handleError + Promise.allSettled

- All 6 settings components: save handlers use handleError with user intent
- onMount loaders migrated from Promise.all to Promise.allSettled (partial recovery)
- loadError $state + inline warning banner on full load failure
- JSON parse catches use handleInfraError with explicit fallback comments
- Secret operations (reveal/store/delete) use handleError for user feedback
This commit is contained in:
Hibryda 2026-03-18 01:21:48 +01:00
parent c7292e9e54
commit 365c420901
6 changed files with 106 additions and 60 deletions

View file

@ -4,6 +4,7 @@
import { getPluginEntries, setPluginEnabled, reloadAllPlugins } from '../../stores/plugins.svelte'; import { getPluginEntries, setPluginEnabled, reloadAllPlugins } from '../../stores/plugins.svelte';
import { checkForUpdates, getCurrentVersion, getLastCheckTimestamp } from '../../utils/updater'; import { checkForUpdates, getCurrentVersion, getLastCheckTimestamp } from '../../utils/updater';
import type { UpdateInfo } from '../../utils/updater'; import type { UpdateInfo } from '../../utils/updater';
import { handleError } from '../../utils/handle-error';
let pluginEntries = $derived(getPluginEntries()); let pluginEntries = $derived(getPluginEntries());
let pluginAutoUpdate = $state(false); let pluginAutoUpdate = $state(false);
@ -20,9 +21,10 @@
let otlpEndpoint = $state(''); let otlpEndpoint = $state('');
let importFileInput: HTMLInputElement | undefined = $state(); let importFileInput: HTMLInputElement | undefined = $state();
let loadError = $state('');
onMount(async () => { onMount(async () => {
const [version, rawAutoUpdate, rawRelays, rawTimeout, rawLog, rawOtlp] = await Promise.all([ const results = await Promise.allSettled([
getCurrentVersion(), getCurrentVersion(),
getSetting('plugin_auto_update'), getSetting('plugin_auto_update'),
getSetting('relay_urls'), getSetting('relay_urls'),
@ -30,19 +32,21 @@
getSetting('log_level'), getSetting('log_level'),
getSetting('otlp_endpoint'), getSetting('otlp_endpoint'),
]); ]);
appVersion = version; let failCount = 0;
pluginAutoUpdate = rawAutoUpdate === 'true'; if (results[0].status === 'fulfilled') appVersion = results[0].value; else failCount++;
relayUrls = rawRelays ?? ''; if (results[1].status === 'fulfilled') pluginAutoUpdate = results[1].value === 'true'; else failCount++;
connectionTimeout = rawTimeout ? parseInt(rawTimeout, 10) : 30; if (results[2].status === 'fulfilled') relayUrls = results[2].value ?? ''; else failCount++;
if (rawLog === 'trace' || rawLog === 'debug' || rawLog === 'info' || rawLog === 'warn' || rawLog === 'error') logLevel = rawLog; if (results[3].status === 'fulfilled') connectionTimeout = results[3].value ? parseInt(results[3].value, 10) : 30; else failCount++;
otlpEndpoint = rawOtlp ?? ''; if (results[4].status === 'fulfilled') { const v = results[4].value; if (v === 'trace' || v === 'debug' || v === 'info' || v === 'warn' || v === 'error') logLevel = v; } else failCount++;
if (results[5].status === 'fulfilled') otlpEndpoint = results[5].value ?? ''; else failCount++;
if (failCount === results.length) loadError = 'Could not load settings. Displaying defaults.';
const ts = getLastCheckTimestamp(); const ts = getLastCheckTimestamp();
if (ts) updateLastCheck = new Date(ts).toLocaleString(); if (ts) updateLastCheck = new Date(ts).toLocaleString();
}); });
async function save(key: string, value: string) { async function save(key: string, value: string) {
try { await setSetting(key, value); } catch (e) { console.error(`Failed to save ${key}:`, e); } try { await setSetting(key, value); } catch (e) { handleError(e, `settings.save.${key}`, 'save your settings'); }
} }
async function handleCheckForUpdates() { async function handleCheckForUpdates() {
@ -52,14 +56,14 @@
const ts = getLastCheckTimestamp(); const ts = getLastCheckTimestamp();
if (ts) updateLastCheck = new Date(ts).toLocaleString(); if (ts) updateLastCheck = new Date(ts).toLocaleString();
} catch (e) { } catch (e) {
console.error('Update check failed:', e); handleError(e, 'settings.updates.check', 'check for updates');
} finally { } finally {
updateChecking = false; updateChecking = false;
} }
} }
async function handleReloadPlugins() { async function handleReloadPlugins() {
try { await reloadAllPlugins(); } catch (e) { console.error('Plugin reload failed:', e); } try { await reloadAllPlugins(); } catch (e) { handleError(e, 'settings.plugins.reload', 'reload plugins'); }
} }
async function handleExport() { async function handleExport() {
@ -97,12 +101,13 @@
if (typeof value === 'string') await setSetting(key, value); if (typeof value === 'string') await setSetting(key, value);
} }
} catch (err) { } catch (err) {
console.error('Settings import failed:', err); handleError(err, 'settings.import', 'import settings');
} }
input.value = ''; input.value = '';
} }
</script> </script>
{#if loadError}<p class="load-error">{loadError}</p>{/if}
<section class="section"> <section class="section">
<h2>Plugins</h2> <h2>Plugins</h2>
{#if pluginEntries.length === 0} {#if pluginEntries.length === 0}
@ -239,6 +244,7 @@
</section> </section>
<style> <style>
.load-error { font-size: 0.75rem; color: var(--ctp-peach); margin: 0 0 0.25rem; }
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); } 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; } .section { margin-bottom: 1.25rem; }
.fields { display: flex; flex-direction: column; gap: 0.625rem; } .fields { display: flex; flex-direction: column; gap: 0.625rem; }

View file

@ -4,6 +4,7 @@
import { getProviders } from '../../providers/registry.svelte'; import { getProviders } from '../../providers/registry.svelte';
import type { ProviderSettings } from '../../providers/types'; import type { ProviderSettings } from '../../providers/types';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { handleError, handleInfraError } from '../../utils/handle-error';
let defaultShell = $state(''); let defaultShell = $state('');
let defaultCwd = $state(''); let defaultCwd = $state('');
@ -17,29 +18,35 @@
let contextPressureCritical = $state(90); let contextPressureCritical = $state(90);
let monthlyTokenBudget = $state(''); let monthlyTokenBudget = $state('');
let routerProfile = $state<'cost_saver' | 'balanced' | 'quality_first'>('balanced'); let routerProfile = $state<'cost_saver' | 'balanced' | 'quality_first'>('balanced');
let loadError = $state('');
onMount(async () => { onMount(async () => {
const [shell, cwd, saveOnBlur, rawProv, rawPerm, rawPrompt, rawWarn, rawCrit, rawBudget, rawRouter] = await Promise.all([ const results = await Promise.allSettled([
getSetting('default_shell'), getSetting('default_cwd'), getSetting('files_save_on_blur'), getSetting('default_shell'), getSetting('default_cwd'), getSetting('files_save_on_blur'),
getSetting('provider_settings'), getSetting('default_permission_mode'), getSetting('provider_settings'), getSetting('default_permission_mode'),
getSetting('system_prompt_template'), getSetting('context_pressure_warn'), getSetting('system_prompt_template'), getSetting('context_pressure_warn'),
getSetting('context_pressure_critical'), getSetting('budget_monthly_tokens'), getSetting('context_pressure_critical'), getSetting('budget_monthly_tokens'),
getSetting('router_profile'), getSetting('router_profile'),
]); ]);
defaultShell = shell ?? ''; let failCount = 0;
defaultCwd = cwd ?? ''; if (results[0].status === 'fulfilled') defaultShell = results[0].value ?? ''; else failCount++;
filesSaveOnBlur = saveOnBlur === 'true'; if (results[1].status === 'fulfilled') defaultCwd = results[1].value ?? ''; else failCount++;
try { if (rawProv) providerSettings = JSON.parse(rawProv); } catch { providerSettings = {}; } if (results[2].status === 'fulfilled') filesSaveOnBlur = results[2].value === 'true'; else failCount++;
if (rawPerm === 'bypassPermissions' || rawPerm === 'default' || rawPerm === 'plan') permissionMode = rawPerm; if (results[3].status === 'fulfilled') {
systemPromptTemplate = rawPrompt ?? ''; const rawProv = results[3].value;
contextPressureWarn = rawWarn ? parseInt(rawWarn, 10) : 75; try { if (rawProv) providerSettings = JSON.parse(rawProv); } catch (e) { handleInfraError(e, 'settings.parse.provider_settings'); providerSettings = {}; }
contextPressureCritical = rawCrit ? parseInt(rawCrit, 10) : 90; } else failCount++;
monthlyTokenBudget = rawBudget ?? ''; if (results[4].status === 'fulfilled') { const v = results[4].value; if (v === 'bypassPermissions' || v === 'default' || v === 'plan') permissionMode = v; } else failCount++;
if (rawRouter === 'cost_saver' || rawRouter === 'balanced' || rawRouter === 'quality_first') routerProfile = rawRouter; if (results[5].status === 'fulfilled') systemPromptTemplate = results[5].value ?? ''; else failCount++;
if (results[6].status === 'fulfilled') contextPressureWarn = results[6].value ? parseInt(results[6].value, 10) : 75; else failCount++;
if (results[7].status === 'fulfilled') contextPressureCritical = results[7].value ? parseInt(results[7].value, 10) : 90; else failCount++;
if (results[8].status === 'fulfilled') monthlyTokenBudget = results[8].value ?? ''; else failCount++;
if (results[9].status === 'fulfilled') { const v = results[9].value; if (v === 'cost_saver' || v === 'balanced' || v === 'quality_first') routerProfile = v; } else failCount++;
if (failCount === results.length) loadError = 'Could not load settings. Displaying defaults.';
}); });
async function save(key: string, value: string) { async function save(key: string, value: string) {
try { await setSetting(key, value); } catch (e) { console.error(`Failed to save ${key}:`, e); } try { await setSetting(key, value); } catch (e) { handleError(e, `settings.save.${key}`, 'save your settings'); }
} }
async function browseDirectory(): Promise<string | null> { async function browseDirectory(): Promise<string | null> {
@ -65,6 +72,7 @@
function isProviderEnabled(id: string): boolean { return providerSettings[id]?.enabled ?? true; } function isProviderEnabled(id: string): boolean { return providerSettings[id]?.enabled ?? true; }
</script> </script>
{#if loadError}<p class="load-error">{loadError}</p>{/if}
<section class="section"> <section class="section">
<h2>Defaults</h2> <h2>Defaults</h2>
<div class="fields"> <div class="fields">
@ -206,6 +214,7 @@
</section> </section>
<style> <style>
.load-error { font-size: 0.75rem; color: var(--ctp-peach); margin: 0 0 0.25rem; }
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); } 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; } .section { margin-bottom: 1.25rem; }
.fields { display: flex; flex-direction: column; gap: 0.625rem; } .fields { display: flex; flex-direction: column; gap: 0.625rem; }

View file

@ -3,6 +3,7 @@
import { getCurrentTheme, setTheme } from '../../stores/theme.svelte'; import { getCurrentTheme, setTheme } from '../../stores/theme.svelte';
import { THEME_LIST, getPalette, type ThemeId } from '../../styles/themes'; import { THEME_LIST, getPalette, type ThemeId } from '../../styles/themes';
import { getSetting, setSetting } from '../../adapters/settings-bridge'; import { getSetting, setSetting } from '../../adapters/settings-bridge';
import { handleError } from '../../utils/handle-error';
const UI_FONTS = [ const UI_FONTS = [
{ value: '', label: 'System Default' }, { value: '', label: 'System Default' },
@ -36,6 +37,7 @@
let cursorStyle = $state('block'); let cursorStyle = $state('block');
let cursorBlink = $state(true); let cursorBlink = $state(true);
let scrollbackLines = $state('1000'); let scrollbackLines = $state('1000');
let loadError = $state('');
let themeOpen = $state(false); let themeOpen = $state(false);
let uiFontOpen = $state(false); let uiFontOpen = $state(false);
let termFontOpen = $state(false); let termFontOpen = $state(false);
@ -55,48 +57,53 @@
function css(prop: string, val: string) { document.documentElement.style.setProperty(prop, val); } function css(prop: string, val: string) { document.documentElement.style.setProperty(prop, val); }
onMount(async () => { onMount(async () => {
const [font, size, tfont, tsize, cursor, blink, scroll] = await Promise.all([ const results = await Promise.allSettled([
getSetting('ui_font_family'), getSetting('ui_font_size'), getSetting('ui_font_family'), getSetting('ui_font_size'),
getSetting('term_font_family'), getSetting('term_font_size'), getSetting('term_font_family'), getSetting('term_font_size'),
getSetting('term_cursor_style'), getSetting('term_cursor_blink'), getSetting('term_cursor_style'), getSetting('term_cursor_blink'),
getSetting('term_scrollback'), getSetting('term_scrollback'),
]); ]);
if (font) uiFont = font; let failCount = 0;
if (size) uiFontSize = size; if (results[0].status === 'fulfilled' && results[0].value) uiFont = results[0].value; else failCount++;
if (tfont) termFont = tfont; if (results[1].status === 'fulfilled' && results[1].value) uiFontSize = results[1].value; else failCount++;
if (tsize) termFontSize = tsize; if (results[2].status === 'fulfilled' && results[2].value) termFont = results[2].value; else failCount++;
if (cursor) cursorStyle = cursor; if (results[3].status === 'fulfilled' && results[3].value) termFontSize = results[3].value; else failCount++;
if (blink !== null) cursorBlink = blink !== 'false'; if (results[4].status === 'fulfilled' && results[4].value) cursorStyle = results[4].value; else failCount++;
if (scroll) scrollbackLines = scroll; if (results[5].status === 'fulfilled') cursorBlink = results[5].value !== 'false'; else failCount++;
if (results[6].status === 'fulfilled' && results[6].value) scrollbackLines = results[6].value; else failCount++;
if (failCount === results.length) loadError = 'Could not load settings. Displaying defaults.';
}); });
function pickTheme(id: ThemeId) { selectedTheme = id; themeOpen = false; setTheme(id); } function pickTheme(id: ThemeId) { selectedTheme = id; themeOpen = false; setTheme(id); }
async function save(key: string, value: string) {
try { await setSetting(key, value); } catch (e) { handleError(e, `settings.save.${key}`, 'save your settings'); }
}
function pickUiFont(val: string) { function pickUiFont(val: string) {
uiFont = val; uiFontOpen = false; uiFont = val; uiFontOpen = false;
css('--ui-font-family', val ? `'${val}', sans-serif` : 'system-ui, sans-serif'); css('--ui-font-family', val ? `'${val}', sans-serif` : 'system-ui, sans-serif');
setSetting('ui_font_family', val); save('ui_font_family', val);
} }
function pickTermFont(val: string) { function pickTermFont(val: string) {
termFont = val; termFontOpen = false; termFont = val; termFontOpen = false;
css('--term-font-family', val ? `'${val}', monospace` : `'JetBrains Mono', monospace`); css('--term-font-family', val ? `'${val}', monospace` : `'JetBrains Mono', monospace`);
setSetting('term_font_family', val); save('term_font_family', val);
} }
function stepUiSize(delta: number) { function stepUiSize(delta: number) {
const n = parseInt(uiFontSize, 10) + delta; const n = parseInt(uiFontSize, 10) + delta;
if (n < 8 || n > 24) return; if (n < 8 || n > 24) return;
uiFontSize = String(n); css('--ui-font-size', `${n}px`); setSetting('ui_font_size', String(n)); uiFontSize = String(n); css('--ui-font-size', `${n}px`); save('ui_font_size', String(n));
} }
function stepTermSize(delta: number) { function stepTermSize(delta: number) {
const n = parseInt(termFontSize, 10) + delta; const n = parseInt(termFontSize, 10) + delta;
if (n < 8 || n > 24) return; if (n < 8 || n > 24) return;
termFontSize = String(n); css('--term-font-size', `${n}px`); setSetting('term_font_size', String(n)); termFontSize = String(n); css('--term-font-size', `${n}px`); save('term_font_size', String(n));
} }
function setCursor(style: string) { cursorStyle = style; setSetting('term_cursor_style', style); } function setCursor(style: string) { cursorStyle = style; save('term_cursor_style', style); }
function toggleBlink() { cursorBlink = !cursorBlink; setSetting('term_cursor_blink', String(cursorBlink)); } function toggleBlink() { cursorBlink = !cursorBlink; save('term_cursor_blink', String(cursorBlink)); }
function setScrollback(val: string) { function setScrollback(val: string) {
const n = parseInt(val, 10); const n = parseInt(val, 10);
if (isNaN(n) || n < 100 || n > 100000) return; if (isNaN(n) || n < 100 || n > 100000) return;
scrollbackLines = val; setSetting('term_scrollback', val); scrollbackLines = val; save('term_scrollback', val);
} }
function closeDropdowns() { themeOpen = false; uiFontOpen = false; termFontOpen = false; } function closeDropdowns() { themeOpen = false; uiFontOpen = false; termFontOpen = false; }
@ -112,6 +119,7 @@
</script> </script>
<div class="appearance" onclick={handleClick} onkeydown={handleKey}> <div class="appearance" onclick={handleClick} onkeydown={handleKey}>
{#if loadError}<p class="load-error">{loadError}</p>{/if}
<h3>Theme</h3> <h3>Theme</h3>
<div class="field" id="setting-theme"> <div class="field" id="setting-theme">
<div class="custom-dropdown"> <div class="custom-dropdown">
@ -206,6 +214,7 @@
</div> </div>
<style> <style>
.load-error { font-size: 0.75rem; color: var(--ctp-peach); margin: 0 0 0.25rem; }
.appearance { display: flex; flex-direction: column; gap: 0.75rem; } .appearance { display: flex; flex-direction: column; gap: 0.75rem; }
h3 { font-size: 0.75rem; font-weight: 600; color: var(--ctp-subtext0); text-transform: uppercase; letter-spacing: 0.05em; margin: 0.5rem 0 0.125rem; } h3 { font-size: 0.75rem; font-weight: 600; color: var(--ctp-subtext0); text-transform: uppercase; letter-spacing: 0.05em; margin: 0.5rem 0 0.125rem; }
.field { position: relative; } .field { position: relative; }

View file

@ -3,6 +3,7 @@
import { getSetting, setSetting } from '../../adapters/settings-bridge'; import { getSetting, setSetting } from '../../adapters/settings-bridge';
import { ANCHOR_BUDGET_SCALES, ANCHOR_BUDGET_SCALE_LABELS, type AnchorBudgetScale } from '../../types/anchors'; 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'; import { WAKE_STRATEGIES, WAKE_STRATEGY_LABELS, WAKE_STRATEGY_DESCRIPTIONS, type WakeStrategy } from '../../types/wake';
import { handleError, handleInfraError } from '../../utils/handle-error';
let stallThreshold = $state(15); let stallThreshold = $state(15);
let anchorBudget = $state<AnchorBudgetScale>('medium'); let anchorBudget = $state<AnchorBudgetScale>('medium');
@ -13,28 +14,34 @@
let notifTypes = $state<Set<string>>(new Set(['complete', 'error', 'crash', 'stall'])); let notifTypes = $state<Set<string>>(new Set(['complete', 'error', 'crash', 'stall']));
let memoryTtl = $state(''); let memoryTtl = $state('');
let memoryExtract = $state(false); let memoryExtract = $state(false);
let loadError = $state('');
const NOTIF_TYPE_OPTIONS = ['complete', 'error', 'crash', 'stall'] as const; const NOTIF_TYPE_OPTIONS = ['complete', 'error', 'crash', 'stall'] as const;
onMount(async () => { onMount(async () => {
const [rawStall, rawBudget, rawAutoAnchor, rawWake, rawThresh, rawDesktop, rawTypes, rawTtl, rawExtract] = await Promise.all([ const results = await Promise.allSettled([
getSetting('stall_threshold'), getSetting('anchor_budget_scale'), getSetting('auto_anchor'), getSetting('stall_threshold'), getSetting('anchor_budget_scale'), getSetting('auto_anchor'),
getSetting('wake_strategy'), getSetting('wake_threshold'), getSetting('notif_desktop'), getSetting('wake_strategy'), getSetting('wake_threshold'), getSetting('notif_desktop'),
getSetting('notif_types'), getSetting('memory_ttl'), getSetting('memory_extract'), getSetting('notif_types'), getSetting('memory_ttl'), getSetting('memory_extract'),
]); ]);
if (rawStall) { const n = parseInt(rawStall, 10); if (!isNaN(n)) stallThreshold = n; } let failCount = 0;
if (rawBudget && ANCHOR_BUDGET_SCALES.includes(rawBudget as AnchorBudgetScale)) anchorBudget = rawBudget as AnchorBudgetScale; if (results[0].status === 'fulfilled') { const v = results[0].value; if (v) { const n = parseInt(v, 10); if (!isNaN(n)) stallThreshold = n; } } else failCount++;
autoAnchor = rawAutoAnchor !== 'false'; if (results[1].status === 'fulfilled') { const v = results[1].value; if (v && ANCHOR_BUDGET_SCALES.includes(v as AnchorBudgetScale)) anchorBudget = v as AnchorBudgetScale; } else failCount++;
if (rawWake && WAKE_STRATEGIES.includes(rawWake as WakeStrategy)) wakeStrategy = rawWake as WakeStrategy; if (results[2].status === 'fulfilled') autoAnchor = results[2].value !== 'false'; else failCount++;
if (rawThresh) { const n = parseInt(rawThresh, 10); if (!isNaN(n)) wakeThreshold = n; } if (results[3].status === 'fulfilled') { const v = results[3].value; if (v && WAKE_STRATEGIES.includes(v as WakeStrategy)) wakeStrategy = v as WakeStrategy; } else failCount++;
notifDesktop = rawDesktop !== 'false'; if (results[4].status === 'fulfilled') { const v = results[4].value; if (v) { const n = parseInt(v, 10); if (!isNaN(n)) wakeThreshold = n; } } else failCount++;
if (rawTypes) { try { const arr = JSON.parse(rawTypes); if (Array.isArray(arr)) notifTypes = new Set(arr); } catch { /* keep default */ } } if (results[5].status === 'fulfilled') notifDesktop = results[5].value !== 'false'; else failCount++;
memoryTtl = rawTtl ?? ''; if (results[6].status === 'fulfilled') {
memoryExtract = rawExtract === 'true'; const v = results[6].value;
if (v) { try { const arr = JSON.parse(v); if (Array.isArray(arr)) notifTypes = new Set(arr); } catch (e) { handleInfraError(e, 'settings.parse.notif_types'); /* keep default */ } }
} else failCount++;
if (results[7].status === 'fulfilled') memoryTtl = results[7].value ?? ''; else failCount++;
if (results[8].status === 'fulfilled') memoryExtract = results[8].value === 'true'; else failCount++;
if (failCount === results.length) loadError = 'Could not load settings. Displaying defaults.';
}); });
async function save(key: string, value: string) { async function save(key: string, value: string) {
try { await setSetting(key, value); } catch (e) { console.error(`Failed to save ${key}:`, e); } try { await setSetting(key, value); } catch (e) { handleError(e, `settings.save.${key}`, 'save your settings'); }
} }
function toggleNotifType(type: string) { function toggleNotifType(type: string) {
@ -45,6 +52,7 @@
} }
</script> </script>
{#if loadError}<p class="load-error">{loadError}</p>{/if}
<section class="section"> <section class="section">
<h2>Health Monitoring</h2> <h2>Health Monitoring</h2>
<div class="fields"> <div class="fields">
@ -174,6 +182,7 @@
</section> </section>
<style> <style>
.load-error { font-size: 0.75rem; color: var(--ctp-peach); margin: 0 0 0.25rem; }
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); } 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; } .section { margin-bottom: 1.25rem; }
.fields { display: flex; flex-direction: column; gap: 0.625rem; } .fields { display: flex; flex-direction: column; gap: 0.625rem; }

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { handleError } from '../../utils/handle-error';
import { import {
getActiveGroupId, getActiveGroup, getAllGroups, getActiveGroupId, getActiveGroup, getAllGroups,
addProject, removeProject, addGroup, removeGroup, switchGroup, addProject, removeProject, addGroup, removeGroup, switchGroup,
@ -41,7 +41,7 @@
async function browseDir(): Promise<string | null> { async function browseDir(): Promise<string | null> {
try { return await invoke<string | null>('pick_directory'); } try { return await invoke<string | null>('pick_directory'); }
catch { return null; } catch (e) { handleError(e, 'settings.browseDir', 'browse for a directory'); return null; }
} }
async function browseProjectCwd() { async function browseProjectCwd() {

View file

@ -2,6 +2,7 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { getSetting, setSetting } from '../../adapters/settings-bridge'; import { getSetting, setSetting } from '../../adapters/settings-bridge';
import { storeSecret, getSecret, deleteSecret, listSecrets, hasKeyring, knownSecretKeys, SECRET_KEY_LABELS } from '../../adapters/secrets-bridge'; import { storeSecret, getSecret, deleteSecret, listSecrets, hasKeyring, knownSecretKeys, SECRET_KEY_LABELS } from '../../adapters/secrets-bridge';
import { handleError, handleInfraError } from '../../utils/handle-error';
let keyringAvailable = $state(false); let keyringAvailable = $state(false);
let storedKeys = $state<string[]>([]); let storedKeys = $state<string[]>([]);
@ -17,6 +18,7 @@
let newBranchAction = $state<'block' | 'warn'>('warn'); let newBranchAction = $state<'block' | 'warn'>('warn');
let telemetryEnabled = $state(false); let telemetryEnabled = $state(false);
let retentionDays = $state(90); let retentionDays = $state(90);
let loadError = $state('');
let availableKeysForAdd = $derived(knownKeys.filter(k => !storedKeys.includes(k))); let availableKeysForAdd = $derived(knownKeys.filter(k => !storedKeys.includes(k)));
let newSecretKeyLabel = $derived(newSecretKey ? getSecretKeyLabel(newSecretKey) : 'Select key...'); let newSecretKeyLabel = $derived(newSecretKey ? getSecretKeyLabel(newSecretKey) : 'Select key...');
@ -25,35 +27,44 @@
try { try {
keyringAvailable = await hasKeyring(); keyringAvailable = await hasKeyring();
if (keyringAvailable) { storedKeys = await listSecrets(); knownKeys = await knownSecretKeys(); } if (keyringAvailable) { storedKeys = await listSecrets(); knownKeys = await knownSecretKeys(); }
} catch { keyringAvailable = false; } } catch (e) {
const [rawPolicies, rawTelemetry, rawRetention] = await Promise.all([ // Keyring unavailable is expected on some systems — set state explicitly
handleInfraError(e, 'settings.keyring.init');
keyringAvailable = false;
}
const results = await Promise.allSettled([
getSetting('branch_policies'), getSetting('telemetry_enabled'), getSetting('data_retention_days'), getSetting('branch_policies'), getSetting('telemetry_enabled'), getSetting('data_retention_days'),
]); ]);
if (rawPolicies) { try { branchPolicies = JSON.parse(rawPolicies); } catch { branchPolicies = []; } } let failCount = 0;
telemetryEnabled = rawTelemetry === 'true'; if (results[0].status === 'fulfilled') {
retentionDays = rawRetention ? parseInt(rawRetention, 10) : 90; const v = results[0].value;
if (v) { try { branchPolicies = JSON.parse(v); } catch (e) { handleInfraError(e, 'settings.parse.branch_policies'); branchPolicies = []; } }
} else failCount++;
if (results[1].status === 'fulfilled') telemetryEnabled = results[1].value === 'true'; else failCount++;
if (results[2].status === 'fulfilled') retentionDays = results[2].value ? parseInt(results[2].value, 10) : 90; else failCount++;
if (failCount === results.length) loadError = 'Could not load settings. Displaying defaults.';
}); });
async function save(key: string, value: string) { async function save(key: string, value: string) {
try { await setSetting(key, value); } catch (e) { console.error(`Failed to save ${key}:`, e); } try { await setSetting(key, value); } catch (e) { handleError(e, `settings.save.${key}`, 'save your settings'); }
} }
function getSecretKeyLabel(key: string): string { return SECRET_KEY_LABELS[key] ?? key; } function getSecretKeyLabel(key: string): string { return SECRET_KEY_LABELS[key] ?? key; }
async function handleRevealSecret(key: string) { async function handleRevealSecret(key: string) {
if (revealedKey === key) { revealedKey = null; revealedValue = ''; return; } if (revealedKey === key) { revealedKey = null; revealedValue = ''; return; }
try { const val = await getSecret(key); revealedKey = key; revealedValue = val ?? ''; } try { const val = await getSecret(key); revealedKey = key; revealedValue = val ?? ''; }
catch (e) { console.error(`Failed to reveal secret '${key}':`, e); } catch (e) { handleError(e, `settings.secrets.reveal.${key}`, 'reveal the secret'); }
} }
async function handleSaveSecret() { async function handleSaveSecret() {
if (!newSecretKey || !newSecretValue) return; if (!newSecretKey || !newSecretValue) return;
secretsSaving = true; secretsSaving = true;
try { await storeSecret(newSecretKey, newSecretValue); storedKeys = await listSecrets(); newSecretKey = ''; newSecretValue = ''; revealedKey = null; revealedValue = ''; } try { await storeSecret(newSecretKey, newSecretValue); storedKeys = await listSecrets(); newSecretKey = ''; newSecretValue = ''; revealedKey = null; revealedValue = ''; }
catch (e) { console.error('Failed to store secret:', e); } catch (e) { handleError(e, 'settings.secrets.store', 'store the secret'); }
finally { secretsSaving = false; } finally { secretsSaving = false; }
} }
async function handleDeleteSecret(key: string) { async function handleDeleteSecret(key: string) {
try { await deleteSecret(key); storedKeys = await listSecrets(); if (revealedKey === key) { revealedKey = null; revealedValue = ''; } } try { await deleteSecret(key); storedKeys = await listSecrets(); if (revealedKey === key) { revealedKey = null; revealedValue = ''; } }
catch (e) { console.error(`Failed to delete secret '${key}':`, e); } catch (e) { handleError(e, `settings.secrets.delete.${key}`, 'delete the secret'); }
} }
function addBranchPolicy() { function addBranchPolicy() {
if (!newBranchPattern.trim()) return; if (!newBranchPattern.trim()) return;
@ -71,6 +82,7 @@
<svelte:window onclick={handleClickOutside} onkeydown={handleKeydown} /> <svelte:window onclick={handleClickOutside} onkeydown={handleKeydown} />
{#if loadError}<p class="load-error">{loadError}</p>{/if}
<section class="section"> <section class="section">
<h2>Secrets</h2> <h2>Secrets</h2>
<div class="secrets-status"> <div class="secrets-status">
@ -194,6 +206,7 @@
</section> </section>
<style> <style>
.load-error { font-size: 0.75rem; color: var(--ctp-peach); margin: 0 0 0.25rem; }
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); } 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; } .section { margin-bottom: 1.25rem; }
.fields { display: flex; flex-direction: column; gap: 0.625rem; } .fields { display: flex; flex-direction: column; gap: 0.625rem; }