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

View file

@ -4,6 +4,7 @@
import { getProviders } from '../../providers/registry.svelte';
import type { ProviderSettings } from '../../providers/types';
import { invoke } from '@tauri-apps/api/core';
import { handleError, handleInfraError } from '../../utils/handle-error';
let defaultShell = $state('');
let defaultCwd = $state('');
@ -17,29 +18,35 @@
let contextPressureCritical = $state(90);
let monthlyTokenBudget = $state('');
let routerProfile = $state<'cost_saver' | 'balanced' | 'quality_first'>('balanced');
let loadError = $state('');
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('provider_settings'), getSetting('default_permission_mode'),
getSetting('system_prompt_template'), getSetting('context_pressure_warn'),
getSetting('context_pressure_critical'), getSetting('budget_monthly_tokens'),
getSetting('router_profile'),
]);
defaultShell = shell ?? '';
defaultCwd = cwd ?? '';
filesSaveOnBlur = saveOnBlur === 'true';
try { if (rawProv) providerSettings = JSON.parse(rawProv); } catch { providerSettings = {}; }
if (rawPerm === 'bypassPermissions' || rawPerm === 'default' || rawPerm === 'plan') permissionMode = rawPerm;
systemPromptTemplate = rawPrompt ?? '';
contextPressureWarn = rawWarn ? parseInt(rawWarn, 10) : 75;
contextPressureCritical = rawCrit ? parseInt(rawCrit, 10) : 90;
monthlyTokenBudget = rawBudget ?? '';
if (rawRouter === 'cost_saver' || rawRouter === 'balanced' || rawRouter === 'quality_first') routerProfile = rawRouter;
let failCount = 0;
if (results[0].status === 'fulfilled') defaultShell = results[0].value ?? ''; else failCount++;
if (results[1].status === 'fulfilled') defaultCwd = results[1].value ?? ''; else failCount++;
if (results[2].status === 'fulfilled') filesSaveOnBlur = results[2].value === 'true'; else failCount++;
if (results[3].status === 'fulfilled') {
const rawProv = results[3].value;
try { if (rawProv) providerSettings = JSON.parse(rawProv); } catch (e) { handleInfraError(e, 'settings.parse.provider_settings'); providerSettings = {}; }
} else failCount++;
if (results[4].status === 'fulfilled') { const v = results[4].value; if (v === 'bypassPermissions' || v === 'default' || v === 'plan') permissionMode = v; } else failCount++;
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) {
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> {
@ -65,6 +72,7 @@
function isProviderEnabled(id: string): boolean { return providerSettings[id]?.enabled ?? true; }
</script>
{#if loadError}<p class="load-error">{loadError}</p>{/if}
<section class="section">
<h2>Defaults</h2>
<div class="fields">
@ -206,6 +214,7 @@
</section>
<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); }
.section { margin-bottom: 1.25rem; }
.fields { display: flex; flex-direction: column; gap: 0.625rem; }

View file

@ -3,6 +3,7 @@
import { getCurrentTheme, setTheme } from '../../stores/theme.svelte';
import { THEME_LIST, getPalette, type ThemeId } from '../../styles/themes';
import { getSetting, setSetting } from '../../adapters/settings-bridge';
import { handleError } from '../../utils/handle-error';
const UI_FONTS = [
{ value: '', label: 'System Default' },
@ -36,6 +37,7 @@
let cursorStyle = $state('block');
let cursorBlink = $state(true);
let scrollbackLines = $state('1000');
let loadError = $state('');
let themeOpen = $state(false);
let uiFontOpen = $state(false);
let termFontOpen = $state(false);
@ -55,48 +57,53 @@
function css(prop: string, val: string) { document.documentElement.style.setProperty(prop, val); }
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('term_font_family'), getSetting('term_font_size'),
getSetting('term_cursor_style'), getSetting('term_cursor_blink'),
getSetting('term_scrollback'),
]);
if (font) uiFont = font;
if (size) uiFontSize = size;
if (tfont) termFont = tfont;
if (tsize) termFontSize = tsize;
if (cursor) cursorStyle = cursor;
if (blink !== null) cursorBlink = blink !== 'false';
if (scroll) scrollbackLines = scroll;
let failCount = 0;
if (results[0].status === 'fulfilled' && results[0].value) uiFont = results[0].value; else failCount++;
if (results[1].status === 'fulfilled' && results[1].value) uiFontSize = results[1].value; else failCount++;
if (results[2].status === 'fulfilled' && results[2].value) termFont = results[2].value; else failCount++;
if (results[3].status === 'fulfilled' && results[3].value) termFontSize = results[3].value; else failCount++;
if (results[4].status === 'fulfilled' && results[4].value) cursorStyle = results[4].value; else failCount++;
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); }
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) {
uiFont = val; uiFontOpen = false;
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) {
termFont = val; termFontOpen = false;
css('--term-font-family', val ? `'${val}', monospace` : `'JetBrains Mono', monospace`);
setSetting('term_font_family', val);
save('term_font_family', val);
}
function stepUiSize(delta: number) {
const n = parseInt(uiFontSize, 10) + delta;
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) {
const n = parseInt(termFontSize, 10) + delta;
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 toggleBlink() { cursorBlink = !cursorBlink; setSetting('term_cursor_blink', String(cursorBlink)); }
function setCursor(style: string) { cursorStyle = style; save('term_cursor_style', style); }
function toggleBlink() { cursorBlink = !cursorBlink; save('term_cursor_blink', String(cursorBlink)); }
function setScrollback(val: string) {
const n = parseInt(val, 10);
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; }
@ -112,6 +119,7 @@
</script>
<div class="appearance" onclick={handleClick} onkeydown={handleKey}>
{#if loadError}<p class="load-error">{loadError}</p>{/if}
<h3>Theme</h3>
<div class="field" id="setting-theme">
<div class="custom-dropdown">
@ -206,6 +214,7 @@
</div>
<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; }
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; }

View file

@ -3,6 +3,7 @@
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';
import { handleError, handleInfraError } from '../../utils/handle-error';
let stallThreshold = $state(15);
let anchorBudget = $state<AnchorBudgetScale>('medium');
@ -13,28 +14,34 @@
let notifTypes = $state<Set<string>>(new Set(['complete', 'error', 'crash', 'stall']));
let memoryTtl = $state('');
let memoryExtract = $state(false);
let loadError = $state('');
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([
const results = await Promise.allSettled([
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';
let failCount = 0;
if (results[0].status === 'fulfilled') { const v = results[0].value; if (v) { const n = parseInt(v, 10); if (!isNaN(n)) stallThreshold = n; } } else failCount++;
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 (results[2].status === 'fulfilled') autoAnchor = results[2].value !== 'false'; else failCount++;
if (results[3].status === 'fulfilled') { const v = results[3].value; if (v && WAKE_STRATEGIES.includes(v as WakeStrategy)) wakeStrategy = v as WakeStrategy; } else failCount++;
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 (results[5].status === 'fulfilled') notifDesktop = results[5].value !== 'false'; else failCount++;
if (results[6].status === 'fulfilled') {
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) {
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) {
@ -45,6 +52,7 @@
}
</script>
{#if loadError}<p class="load-error">{loadError}</p>{/if}
<section class="section">
<h2>Health Monitoring</h2>
<div class="fields">
@ -174,6 +182,7 @@
</section>
<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); }
.section { margin-bottom: 1.25rem; }
.fields { display: flex; flex-direction: column; gap: 0.625rem; }

View file

@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import { invoke } from '@tauri-apps/api/core';
import { handleError } from '../../utils/handle-error';
import {
getActiveGroupId, getActiveGroup, getAllGroups,
addProject, removeProject, addGroup, removeGroup, switchGroup,
@ -41,7 +41,7 @@
async function browseDir(): Promise<string | null> {
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() {

View file

@ -2,6 +2,7 @@
import { onMount } from 'svelte';
import { getSetting, setSetting } from '../../adapters/settings-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 storedKeys = $state<string[]>([]);
@ -17,6 +18,7 @@
let newBranchAction = $state<'block' | 'warn'>('warn');
let telemetryEnabled = $state(false);
let retentionDays = $state(90);
let loadError = $state('');
let availableKeysForAdd = $derived(knownKeys.filter(k => !storedKeys.includes(k)));
let newSecretKeyLabel = $derived(newSecretKey ? getSecretKeyLabel(newSecretKey) : 'Select key...');
@ -25,35 +27,44 @@
try {
keyringAvailable = await hasKeyring();
if (keyringAvailable) { storedKeys = await listSecrets(); knownKeys = await knownSecretKeys(); }
} catch { keyringAvailable = false; }
const [rawPolicies, rawTelemetry, rawRetention] = await Promise.all([
} catch (e) {
// 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'),
]);
if (rawPolicies) { try { branchPolicies = JSON.parse(rawPolicies); } catch { branchPolicies = []; } }
telemetryEnabled = rawTelemetry === 'true';
retentionDays = rawRetention ? parseInt(rawRetention, 10) : 90;
let failCount = 0;
if (results[0].status === 'fulfilled') {
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) {
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; }
async function handleRevealSecret(key: string) {
if (revealedKey === key) { revealedKey = null; revealedValue = ''; return; }
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() {
if (!newSecretKey || !newSecretValue) return;
secretsSaving = true;
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; }
}
async function handleDeleteSecret(key: string) {
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() {
if (!newBranchPattern.trim()) return;
@ -71,6 +82,7 @@
<svelte:window onclick={handleClickOutside} onkeydown={handleKeydown} />
{#if loadError}<p class="load-error">{loadError}</p>{/if}
<section class="section">
<h2>Secrets</h2>
<div class="secrets-status">
@ -194,6 +206,7 @@
</section>
<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); }
.section { margin-bottom: 1.25rem; }
.fields { display: flex; flex-direction: column; gap: 0.625rem; }