Skip to content

Commit 4a07c39

Browse files
committed
feat(settings): per-row model selector + cleaner IPC error toasts
- RowModelSelector replaces ActiveModelSelector: any provider row with an API key now shows a model dropdown. Non-active rows persist the pick to the per-provider defaultModel so "Set as current" later picks it up — lets users pre-stage models across providers and one-click switch. - cleanIpcError() strips the "Error invoking remote method '<channel>': XxxError:" wrapper and, for "en / zh" bilingual error messages, picks the side matching the active locale. Applied to 11 toast description sites. - Mark the 4 import-failure toasts (Codex/Gemini/OpenCode/ClaudeCode) reportable: false — these are user-config errors, not bugs to file. - Add reportable?: boolean to ReportableErrorToastSpec; when false, skip createReportableError so the Toast UI hides the Report button. Signed-off-by: hqhq1025 <1506751656@qq.com>
1 parent 80c72e2 commit 4a07c39

2 files changed

Lines changed: 89 additions & 35 deletions

File tree

apps/desktop/src/renderer/src/components/Settings.tsx

Lines changed: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { setLocale as applyLocale, useT } from '@open-codesign/i18n';
1+
import { setLocale as applyLocale, getCurrentLocale, useT } from '@open-codesign/i18n';
22
import type { OnboardingState, ReasoningLevel, WireApi } from '@open-codesign/shared';
33
import {
44
PROVIDER_SHORTLIST as SHORTLIST,
@@ -323,7 +323,7 @@ function ProviderCard({
323323
code: 'CONNECTION_TEST_FAILED',
324324
scope: 'settings',
325325
title: t('settings.providers.toast.connectionFailed'),
326-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
326+
description: cleanIpcError(err) || t('settings.common.unknownError'),
327327
...(err instanceof Error && err.stack !== undefined ? { stack: err.stack } : {}),
328328
context: { provider: row.provider },
329329
});
@@ -384,8 +384,8 @@ function ProviderCard({
384384
</div>
385385
</div>
386386

387-
{row.isActive && !hasError && config !== null && (
388-
<ActiveModelSelector config={config} provider={row.provider} />
387+
{!hasError && row.hasKey !== false && config !== null && (
388+
<RowModelSelector config={config} row={row} onRowChanged={onRowChanged} />
389389
)}
390390
{!hasError && row.hasKey !== false && (
391391
<ReasoningDepthSelector
@@ -398,24 +398,34 @@ function ProviderCard({
398398
);
399399
}
400400

401-
function ActiveModelSelector({
401+
function RowModelSelector({
402402
config,
403-
provider,
403+
row,
404+
onRowChanged,
404405
}: {
405406
config: OnboardingState;
406-
provider: string;
407+
row: ProviderRow;
408+
onRowChanged: (row: ProviderRow) => void;
407409
}) {
408410
const t = useT();
409411
const setConfig = useCodesignStore((s) => s.completeOnboarding);
410412
const reportableErrorToast = useCodesignStore((s) => s.reportableErrorToast);
411413

412-
const [primary, setPrimary] = useState(config.modelPrimary ?? '');
414+
const provider = row.provider;
415+
const isActive = row.isActive;
416+
417+
const initial = isActive
418+
? (config.modelPrimary ?? row.defaultModel ?? '')
419+
: (row.defaultModel ?? '');
420+
const [primary, setPrimary] = useState(initial);
413421
const [models, setModels] = useState<string[] | null>(null);
414422
const [loadingModels, setLoadingModels] = useState(false);
415423

416424
useEffect(() => {
417-
setPrimary(config.modelPrimary ?? '');
418-
}, [config.modelPrimary]);
425+
setPrimary(
426+
isActive ? (config.modelPrimary ?? row.defaultModel ?? '') : (row.defaultModel ?? ''),
427+
);
428+
}, [isActive, config.modelPrimary, row.defaultModel]);
419429

420430
// Fetch models immediately on mount
421431
useEffect(() => {
@@ -437,19 +447,26 @@ function ActiveModelSelector({
437447
async function save(next: string): Promise<boolean> {
438448
if (!window.codesign) return false;
439449
try {
440-
const updated = await window.codesign.settings.setActiveProvider({
441-
provider,
442-
modelPrimary: next,
443-
});
444-
recordAction({ type: 'provider.switch', data: { provider, modelId: next } });
445-
setConfig(updated);
450+
if (isActive) {
451+
const updated = await window.codesign.settings.setActiveProvider({
452+
provider,
453+
modelPrimary: next,
454+
});
455+
recordAction({ type: 'provider.switch', data: { provider, modelId: next } });
456+
setConfig(updated);
457+
} else {
458+
// Inactive row: persist the per-provider default so that clicking
459+
// "Set as current" later picks it up (see handleActivate → currentRow.defaultModel).
460+
await window.codesign.config.updateProvider({ id: provider, defaultModel: next });
461+
onRowChanged({ ...row, defaultModel: next });
462+
}
446463
return true;
447464
} catch (err) {
448465
reportableErrorToast({
449466
code: 'PROVIDER_MODEL_SAVE_FAILED',
450467
scope: 'settings',
451468
title: t('settings.providers.toast.modelSaveFailed'),
452-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
469+
description: cleanIpcError(err) || t('settings.common.unknownError'),
453470
...(err instanceof Error && err.stack !== undefined ? { stack: err.stack } : {}),
454471
context: { provider },
455472
});
@@ -537,7 +554,7 @@ function ReasoningDepthSelector({
537554
code: 'PROVIDER_REASONING_SAVE_FAILED',
538555
scope: 'settings',
539556
title: t('settings.providers.toast.reasoningSaveFailed'),
540-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
557+
description: cleanIpcError(err) || t('settings.common.unknownError'),
541558
...(err instanceof Error && err.stack !== undefined ? { stack: err.stack } : {}),
542559
context: { provider },
543560
});
@@ -579,6 +596,24 @@ const PARSE_REASON_NOT_JSON_OBJECT = '__parse_reason_not_json_object__';
579596

580597
const DISMISSED_BANNER_PREFIX = 'open-codesign:settings:dismissed-import-banner:';
581598

599+
/**
600+
* Electron IPC wraps thrown errors as
601+
* `Error invoking remote method '<channel>': <ErrorName>: <message>`.
602+
* Strip the wrapper and, for bilingual messages formatted as `en / zh`,
603+
* pick the side matching the active locale so toast descriptions read
604+
* like natural sentences instead of framework tracebacks.
605+
*/
606+
function cleanIpcError(err: unknown): string {
607+
if (!(err instanceof Error)) return String(err ?? '');
608+
const raw = err.message;
609+
const stripped = raw.replace(/^Error invoking remote method '[^']*':\s*[A-Za-z]*Error:\s*/, '');
610+
const parts = stripped.split(' / ');
611+
if (parts.length >= 2) {
612+
return getCurrentLocale() === 'zh-CN' ? (parts[1] ?? stripped) : (parts[0] ?? stripped);
613+
}
614+
return stripped;
615+
}
616+
582617
/**
583618
* Strip any user:pass@ credentials from a URL before putting it into
584619
* visible copy (banner, toast, screenshot). Preserves the full URL for
@@ -883,7 +918,7 @@ function ModelsTab() {
883918
pushToast({
884919
variant: 'error',
885920
title: t('settings.providers.toast.loadFailed'),
886-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
921+
description: cleanIpcError(err) || t('settings.common.unknownError'),
887922
});
888923
})
889924
.finally(() => setLoading(false));
@@ -973,7 +1008,8 @@ function ModelsTab() {
9731008
code: 'CODEX_IMPORT_FAILED',
9741009
scope: 'onboarding',
9751010
title: t('settings.providers.import.failed'),
976-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
1011+
description: cleanIpcError(err) || t('settings.common.unknownError'),
1012+
reportable: false,
9771013
...(err instanceof Error && err.stack !== undefined ? { stack: err.stack } : {}),
9781014
});
9791015
}
@@ -1002,7 +1038,8 @@ function ModelsTab() {
10021038
code: 'GEMINI_IMPORT_FAILED',
10031039
scope: 'onboarding',
10041040
title: t('settings.providers.import.failed'),
1005-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
1041+
description: cleanIpcError(err) || t('settings.common.unknownError'),
1042+
reportable: false,
10061043
...(err instanceof Error && err.stack !== undefined ? { stack: err.stack } : {}),
10071044
});
10081045
}
@@ -1033,7 +1070,8 @@ function ModelsTab() {
10331070
code: 'OPENCODE_IMPORT_FAILED',
10341071
scope: 'onboarding',
10351072
title: t('settings.providers.import.failed'),
1036-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
1073+
description: cleanIpcError(err) || t('settings.common.unknownError'),
1074+
reportable: false,
10371075
...(err instanceof Error && err.stack !== undefined ? { stack: err.stack } : {}),
10381076
});
10391077
}
@@ -1076,7 +1114,8 @@ function ModelsTab() {
10761114
code: 'CLAUDECODE_IMPORT_FAILED',
10771115
scope: 'onboarding',
10781116
title: t('settings.providers.import.failed'),
1079-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
1117+
description: cleanIpcError(err) || t('settings.common.unknownError'),
1118+
reportable: false,
10801119
...(err instanceof Error && err.stack !== undefined ? { stack: err.stack } : {}),
10811120
});
10821121
}
@@ -1146,7 +1185,7 @@ function ModelsTab() {
11461185
code: 'PROVIDER_DELETE_FAILED',
11471186
scope: 'settings',
11481187
title: t('settings.providers.toast.deleteFailed'),
1149-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
1188+
description: cleanIpcError(err) || t('settings.common.unknownError'),
11501189
...(err instanceof Error && err.stack !== undefined ? { stack: err.stack } : {}),
11511190
});
11521191
}
@@ -1188,7 +1227,7 @@ function ModelsTab() {
11881227
code: 'PROVIDER_ACTIVATE_FAILED',
11891228
scope: 'settings',
11901229
title: t('settings.providers.toast.switchFailed'),
1191-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
1230+
description: cleanIpcError(err) || t('settings.common.unknownError'),
11921231
...(err instanceof Error && err.stack !== undefined ? { stack: err.stack } : {}),
11931232
});
11941233
}
@@ -1560,7 +1599,7 @@ function AppearanceTab() {
15601599
pushToast({
15611600
variant: 'error',
15621601
title: t('settings.appearance.languageLoadFailed'),
1563-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
1602+
description: cleanIpcError(err) || t('settings.common.unknownError'),
15641603
});
15651604
});
15661605
}, [pushToast, t]);
@@ -1581,7 +1620,7 @@ function AppearanceTab() {
15811620
pushToast({
15821621
variant: 'error',
15831622
title: t('errors.localePersistFailed'),
1584-
description: err instanceof Error ? err.message : t('errors.unknown'),
1623+
description: cleanIpcError(err) || t('errors.unknown'),
15851624
});
15861625
}
15871626
}
@@ -1737,7 +1776,7 @@ function StorageTab() {
17371776
pushToast({
17381777
variant: 'error',
17391778
title: t('settings.storage.pathsLoadFailed'),
1740-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
1779+
description: cleanIpcError(err) || t('settings.common.unknownError'),
17411780
});
17421781
});
17431782
}, [pushToast, t]);
@@ -1749,7 +1788,7 @@ function StorageTab() {
17491788
pushToast({
17501789
variant: 'error',
17511790
title: t('settings.storage.openFolderFailed'),
1752-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
1791+
description: cleanIpcError(err) || t('settings.common.unknownError'),
17531792
});
17541793
}
17551794
}
@@ -1765,7 +1804,7 @@ function StorageTab() {
17651804
pushToast({
17661805
variant: 'error',
17671806
title: t('settings.storage.locationSaveFailed'),
1768-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
1807+
description: cleanIpcError(err) || t('settings.common.unknownError'),
17691808
});
17701809
} finally {
17711810
setChoosing(null);
@@ -1790,7 +1829,7 @@ function StorageTab() {
17901829
pushToast({
17911830
variant: 'error',
17921831
title: t('settings.storage.openFolderFailed'),
1793-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
1832+
description: cleanIpcError(err) || t('settings.common.unknownError'),
17941833
});
17951834
}
17961835
}
@@ -1809,7 +1848,7 @@ function StorageTab() {
18091848
pushToast({
18101849
variant: 'error',
18111850
title: t('settings.storage.diagnosticsExportFailed'),
1812-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
1851+
description: cleanIpcError(err) || t('settings.common.unknownError'),
18131852
});
18141853
} finally {
18151854
setExporting(false);
@@ -1944,7 +1983,7 @@ function AdvancedTab() {
19441983
pushToast({
19451984
variant: 'error',
19461985
title: t('settings.advanced.prefsLoadFailed'),
1947-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
1986+
description: cleanIpcError(err) || t('settings.common.unknownError'),
19481987
});
19491988
});
19501989
}, [pushToast, t]);
@@ -1958,7 +1997,7 @@ function AdvancedTab() {
19581997
pushToast({
19591998
variant: 'error',
19601999
title: t('settings.advanced.prefsSaveFailed'),
1961-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
2000+
description: cleanIpcError(err) || t('settings.common.unknownError'),
19622001
});
19632002
}
19642003
}
@@ -1971,7 +2010,7 @@ function AdvancedTab() {
19712010
pushToast({
19722011
variant: 'error',
19732012
title: t('settings.advanced.devtoolsFailed'),
1974-
description: err instanceof Error ? err.message : t('settings.common.unknownError'),
2013+
description: cleanIpcError(err) || t('settings.common.unknownError'),
19752014
});
19762015
}
19772016
}

apps/desktop/src/renderer/src/store.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,13 @@ export interface ReportableErrorToastSpec {
100100
stack?: string;
101101
runId?: string;
102102
context?: Record<string, unknown>;
103+
/**
104+
* When false, the toast is shown without recording a ReportableError,
105+
* so the Toast UI does NOT render the "Report" button. Use this for
106+
* expected user-facing errors (missing config files, declined imports)
107+
* where prompting the user to file a bug report would just be noise.
108+
*/
109+
reportable?: boolean;
103110
}
104111

105112
export type Theme = 'light' | 'dark';
@@ -2074,6 +2081,14 @@ export const useCodesignStore = create<CodesignState>((set, get) => ({
20742081
},
20752082

20762083
reportableErrorToast(spec) {
2084+
if (spec.reportable === false) {
2085+
return get().pushToast({
2086+
variant: 'error',
2087+
title: spec.title,
2088+
...(spec.description !== undefined ? { description: spec.description } : {}),
2089+
...(spec.action !== undefined ? { action: spec.action } : {}),
2090+
});
2091+
}
20772092
const localId = get().createReportableError({
20782093
code: spec.code,
20792094
scope: spec.scope,

0 commit comments

Comments
 (0)