Skip to content

feat(desktop): full-page Settings + fix API switching pipeline#28

Merged
hqhq1025 merged 5 commits intomainfrom
wt/settings-fullpage-api
Apr 18, 2026
Merged

feat(desktop): full-page Settings + fix API switching pipeline#28
hqhq1025 merged 5 commits intomainfrom
wt/settings-fullpage-api

Conversation

@hqhq1025
Copy link
Copy Markdown
Collaborator

@hqhq1025 hqhq1025 commented Apr 18, 2026

Summary

  • Settings changed from modal to full-page view (store.view enum + App.tsx switch, no Router)
  • ESC key in settings view goes back to workspace instead of closing a modal
  • AddProvider kept as a local modal dialog (makes sense for that flow)
  • Audit + fix API config/switching end-to-end chain: `ActiveModelSelector` syncs local model state when config prop changes (fixes stale model after re-activate)
  • Part C: Settings → Appearance language select now actually applies the locale change immediately via `i18next.changeLanguage` (not just persist to disk)

Changes

Part A — Settings as full-page view

File What changed
`store.ts` Remove `settingsOpen`/`openSettings`/`closeSettings`; add `view: AppView` + `setView(v)`
`App.tsx` Conditional render `` vs workspace grid based on `view`; no modal overlay
`Settings.tsx` Remove modal shell + `settingsOpen` guard; add `← Workspace` back button; fix `StorageTab` to call `setView('workspace')` instead of `closeSettings()`
`TopBar.tsx` Gear button → `setView('settings')`
`CommandPalette.tsx` Open Settings action → `setView('settings')`

Part B — API switching pipeline fix

File What fixed
`Settings.tsx` — `ActiveModelSelector` Added `useEffect` to sync local `primary`/`fast` state from `config` prop changes (prevents stale model dropdowns after provider switch and re-activate)

Part C — Settings Appearance language switch fix

Root cause: `AppearanceTab.handleLocaleChange` called only `window.codesign.locale.set()` (writes to disk) but never called `i18next.changeLanguage()`, so React components did not re-render after language selection.

Fix: Extract `applyLocaleChange(locale, localeApi)` helper that does both:

  1. Calls `localeApi.set(locale)` → persists to `~/.config/open-codesign/locale.json`
  2. Calls `setLocale(persisted)` (from `@open-codesign/i18n`) → calls `i18next.changeLanguage()` → all `useTranslation` hooks re-render

This matches exactly what `LanguageToggle.tsx` does (both paths are now consistent).

Button / IPC chain audit (Task D)

# Entry point IPC channel Wired? Fixed this PR?
1 Onboarding → provider select + fill key + fill baseUrl + Next `onboarding:save-key`
2 Onboarding → Validate key step `onboarding:validate-key`
3 Settings → Models → Add provider → Validate `onboarding:validate-key`
4 Settings → Models → Add provider → Save `settings:v1:add-provider`
5 Settings → Models → ProviderCard → Re-enter key reuses AddProviderModal → `settings:v1:add-provider`
6 Settings → Models → ProviderCard → Delete `settings:v1:delete-provider`
7 Settings → Models → ProviderCard → Set active `settings:v1:set-active-provider`
8 Settings → Models → ActiveModelSelector dropdowns `settings:v1:set-active-provider` (debounced 400ms) ✅ fixed stale-state Part B
9 Settings → Appearance → Theme light/dark buttons `localStorage` + DOM class (no IPC, intentional)
10 Settings → Appearance → Language select `locale:set` + `i18next.changeLanguage` ✅ was broken Part C
11 Settings → Storage → Open folder (Config) `settings:v1:open-folder`
12 Settings → Storage → Open folder (Logs) `settings:v1:open-folder`
13 Settings → Storage → Open folder (Data) `settings:v1:open-folder`
14 Settings → Storage → Reset onboarding `settings:v1:reset-onboarding` + `onboarding:get-state`
15 Settings → Advanced → Update channel toggle `preferences:v1:update`
16 Settings → Advanced → Generation timeout `preferences:v1:update`
17 Settings → Advanced → Toggle DevTools `settings:v1:toggle-devtools`
18 TopBar → gear button → open Settings `setView('settings')` (no IPC, store action) was broken (openSettings removed) Part A
19 TopBar → cmd+k → command palette `openCommandPalette()` (store action)
20 TopBar → LanguageToggle `locale:set` + `i18next.changeLanguage`
21 TopBar → ThemeToggle `localStorage` + DOM class
22 CommandPalette → Open Settings `setView('settings')` (store action) was broken (openSettings removed) Part A
23 Sidebar → Send button / Cmd+Enter `codesign:v1:generate`
24 Sidebar → Stop/Cancel button `codesign:v1:cancel-generation`
25 Sidebar → Attach files (Paperclip) `codesign:pick-input-files`
26 Sidebar → Reference URL input no IPC (passed as arg to generate)
27 Sidebar → Design system entry (Link2) `codesign:pick-design-system-directory`
28 Preview → ErrorState retry button `retryLastPrompt()` → `codesign:v1:generate`
29 Preview → CanvasErrorBar dismiss `clearIframeErrors()` (store action)

Summary: 3 broken entries found and fixed in this PR (entries 8, 10, 18/22). No TODO-for-later items; all regressions are addressed here.

Test plan

  • `pnpm typecheck` — clean (10/10 packages)
  • `pnpm lint` — clean (0 errors, 6 pre-existing complexity warnings unchanged)
  • all tests — 48/48 pass (+7 new tests vs base)

New tests added

  • `store.test.ts`: `setView("settings")` switches view + closes command palette
  • `store.test.ts`: `setView("workspace")` round-trip
  • `store.test.ts`: `sendPrompt` uses updated provider/model after `completeOnboarding`
  • `onboarding-ipc.test.ts`: `getApiKeyForProvider` returns decrypted key when secret exists
  • `onboarding-ipc.test.ts`: `getApiKeyForProvider` throws on missing provider secret
  • `Settings.test.ts`: `applyLocaleChange` calls IPC set then i18next changeLanguage
  • `Settings.test.ts`: `applyLocaleChange` skips IPC and applies locale directly when no API provided

Manual verification steps

  • Gear button → full-page Settings appears (no modal overlay)
  • `← Workspace` button returns to main view
  • ESC in settings → returns to workspace
  • Mod+, opens settings page
  • Command palette "Open Settings" → settings page
  • Add provider → appears in list
  • "Set active" → store config updates, active badge moves to new provider
  • Send prompt after switching provider → uses correct provider/key/baseUrl
  • Settings → Appearance → change language → UI immediately reflects new locale (no app restart needed for the live switch; restart still needed to persist for next boot)

Compatibility ✅ | Upgradeability ✅ | No bloat ✅ | Elegance ✅

@hqhq1025 hqhq1025 force-pushed the wt/settings-fullpage-api branch from 09be189 to 3658ef9 Compare April 18, 2026 16:26
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Major] Escape key closes the entire Settings page when Command Palette is open, instead of dismissing the palette first. This changes expected modal/overlay priority and can cause accidental navigation loss while issuing commands. Evidence apps/desktop/src/renderer/src/App.tsx:70.
    Suggested fix:
    {
      combo: 'escape',
      handler: () => {
        if (commandPaletteOpen) closeCommandPalette();
        else if (view === 'settings') setView('workspace');
      },
      preventDefault: false,
    }

Summary

  • Review mode: initial
  • One major regression found in Escape handling priority.
  • Required context docs docs/VISION.md and docs/PRINCIPLES.md: Not found in repo/docs.

Testing

  • Not run (automation)

open-codesign Bot

Comment thread apps/desktop/src/renderer/src/App.tsx Outdated
combo: 'escape',
handler: () => {
if (settingsOpen) closeSettings();
if (view === 'settings') setView('workspace');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Escape currently checks view === 'settings' before commandPaletteOpen, so when palette is open on Settings it exits the page instead of only dismissing the palette.

if (commandPaletteOpen) closeCommandPalette();
else if (view === 'settings') setView('workspace');

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Minor] Locale persistence now has a silent fallback when the bridge is missing, so a broken preload can make the UI switch language without actually saving it. That hides the failure instead of surfacing it, which conflicts with the repo's no-silent-fallback rule. Evidence apps/desktop/src/renderer/src/components/Settings.tsx:777 and apps/desktop/src/renderer/src/components/Settings.tsx:802.
    Suggested fix:
    export async function applyLocaleChange(
      locale: string,
      localeApi: { set: (locale: string) => Promise<string> },
    ): Promise<string> {
      const persisted = await localeApi.set(locale);
      return applyLocale(persisted);
    }
    
    async function handleLocaleChange(v: string) {
      try {
        if (!window.codesign?.locale) throw new Error('Locale bridge unavailable');
        const applied = await applyLocaleChange(v, window.codesign.locale);
        setLocale(applied);
      } catch (err) {
        ...
      }
    }

Summary

  • Review mode: follow-up after new commits
  • Required context docs docs/VISION.md and docs/PRINCIPLES.md: Not found in repo/docs.
  • One minor issue found; no other high-confidence regressions in the touched renderer flow.

Testing

  • Not run (automation)
  • Vitest coverage was added for the store/settings helpers, but I did not find Playwright coverage for the renderer-level settings navigation or locale switch.

open-codesign Bot

locale: string,
localeApi: { set: (locale: string) => Promise<string> } | undefined,
): Promise<string> {
const persisted = localeApi ? await localeApi.set(locale) : locale;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fallback makes the helper look successful even if the locale bridge is missing, which means the UI can switch language without persistence and with no surfaced error. That conflicts with the repo's no-silent-fallback rule.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Minor] Newly introduced Settings strings bypass i18n, so non-English users can still see English text and the new locale error keys added in this PR remain unused. Evidence: apps/desktop/src/renderer/src/components/Settings.tsx:810, apps/desktop/src/renderer/src/components/Settings.tsx:1132, packages/i18n/src/locales/en.json:191, packages/i18n/src/locales/zh-CN.json:191.
    Suggested fix:
    const t = useT();
    
    pushToast({
      variant: 'error',
      title: t('errors.localePersistFailed'),
      description: t('errors.rendererDisconnected'),
    });
    
    <button aria-label={t('navigation.backToWorkspace')}>
      {t('navigation.workspace')}
    </button>

Summary

  • Review mode: follow-up after new commits
  • 1 minor issue found in changed lines.
  • docs/VISION.md and docs/PRINCIPLES.md: Not found in repo/docs in this checkout.

Testing

  • Not run (automation).

open-codesign Bot

pushToast({
variant: 'error',
title: 'Failed to save language preference',
description: 'Renderer is not connected to the main process.',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This newly introduced toast title is hardcoded English while this PR also adds locale keys (errors.localePersistFailed) in both locale files. Please use useT() here (and for the new Workspace back label) so language switching is consistent.

@hqhq1025 hqhq1025 force-pushed the wt/settings-fullpage-api branch from 6829276 to 0d232fe Compare April 18, 2026 17:51
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • [Minor] Full-page Settings header introduces hardcoded English text, so locale switching remains partially untranslated in this new flow. Evidence: apps/desktop/src/renderer/src/components/Settings.tsx:1140, apps/desktop/src/renderer/src/components/Settings.tsx:1143.
    Suggested fix:
    const t = useT();
    
    <button aria-label={t('settings.backToWorkspace')}>
      <ArrowLeft className="w-4 h-4" />
      {t('settings.workspace')}
    </button>

Summary

  • Review mode: follow-up after new commits
  • 1 minor issue found in changed lines.
  • docs/VISION.md and docs/PRINCIPLES.md: Not found in repo/docs.

Testing

  • Not run (automation).

open-codesign Bot

aria-label="Back to workspace"
>
<ArrowLeft className="w-4 h-4" />
Workspace
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Minor] Hardcoded in the newly added full-page Settings header bypasses i18n and leaves this flow partially untranslated after language switch. Please read from locale keys instead.

Suggested fix:

@hqhq1025 hqhq1025 force-pushed the wt/settings-fullpage-api branch from 0d232fe to fcab129 Compare April 18, 2026 18:31
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • No high-confidence issues found in added/modified lines.

Summary

  • Review mode: follow-up after new commits
  • No issues found in the latest diff.
  • docs/VISION.md and docs/PRINCIPLES.md: Not found in repo/docs.
  • Residual risk: UI-level regressions in full-page settings flow are only partially covered by unit tests (no E2E signal in this run).

Testing

  • Not run (automation)

open-codesign Bot

Part A — Settings full-page view:
- store: replace settingsOpen/openSettings/closeSettings with view enum
  (AppView = 'workspace' | 'settings') and setView(v) action
- App.tsx: switch main area based on view instead of rendering a modal overlay
- Settings.tsx: remove modal shell, add ← Workspace back button; keep
  AddProviderModal as a local dialog (not a page-level route)
- TopBar.tsx: gear button calls setView('settings')
- CommandPalette.tsx: Open Settings action calls setView('settings')
- ESC keybinding: view==='settings' → setView('workspace')

Part B — API switching pipeline audit + fix:
- ActiveModelSelector: add useEffect to sync local primary/fast state
  when config prop changes, preventing stale models after provider switch
- The main pipeline (getApiKeyForProvider, getBaseUrlForProvider,
  cachedConfig updates) audited and confirmed correct end-to-end

Tests (+6 new):
- store: setView switches view and closes command palette
- store: setView workspace/settings round-trip
- store: sendPrompt uses updated provider after completeOnboarding
- onboarding-ipc: getApiKeyForProvider returns decrypted key when secret exists
- onboarding-ipc: getApiKeyForProvider throws on missing provider secret

Signed-off-by: hqhq1025 <[email protected]>
When Settings is open and the command palette is also open, pressing ESC
now closes the palette without navigating back to workspace. The escape
handler in App.tsx previously checked `view === 'settings'` first; flip
the priority so `commandPaletteOpen` is evaluated first.

Three new store-level tests cover: palette consumed (view stays), plain
settings-back, and no-op on workspace.

Signed-off-by: hqhq1025 <[email protected]>
Replace the silent fallback in applyLocaleChange / handleLocaleChange with
an error toast when window.codesign.locale is absent, so users know the
language preference was not saved rather than silently losing it on restart.

Signed-off-by: hqhq1025 <[email protected]>
Wire the previously-introduced errors.localePersistFailed and
errors.rendererDisconnected locale keys into AppearanceTab's
handleLocaleChange toasts (was hardcoded English). Addresses Codex
review note that the new locale error keys remained unused.

Signed-off-by: hqhq1025 <[email protected]>
…iome errors

- Wire useT through every Settings.tsx subcomponent (AddProviderModal,
  ProviderCard, ActiveModelSelector, all four tabs, shell). Replaces
  every user-facing English literal with a t() call.
- Add settings.{shell,common,providers,appearance,storage,advanced}
  namespaces to en.json + native zh-CN.json (~90 keys per locale).
- Replace non-null assertions in store.ts and store.generationStage.test.ts
  with explicit null guards so pre-push lint stays clean without --no-verify.
- Add useT to the Settings.test.ts mock so Vitest can import the module.

Signed-off-by: hqhq1025 <[email protected]>
@hqhq1025 hqhq1025 force-pushed the wt/settings-fullpage-api branch from fcab129 to 17780be Compare April 18, 2026 18:41
@hqhq1025 hqhq1025 merged commit aa7abd5 into main Apr 18, 2026
5 checks passed
@hqhq1025 hqhq1025 deleted the wt/settings-fullpage-api branch April 18, 2026 18:42
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings

  • No high-confidence issues found in added/modified lines.

Summary

  • Review mode: follow-up after new commits
  • No issues found in the latest diff.
  • docs/VISION.md and docs/PRINCIPLES.md: Not found in repo/docs.
  • Residual risk: full-page Settings flow still lacks Playwright/E2E coverage for keyboard navigation and view transitions (Mod+,, Esc, command palette open-settings action).

Testing

  • Not run (automation)

open-codesign Bot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant