Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion apps/desktop/src/main/onboarding-ipc.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,6 @@ describe('registerOnboardingIpc — channel versioning', () => {
}
});
});

describe('registerOnboardingIpc — validate-key passes baseUrl to pingProvider', () => {
it('forwards baseUrl to pingProvider when provided', async () => {
const { pingProvider } = await import('@open-codesign/providers');
Expand Down Expand Up @@ -144,3 +143,30 @@ describe('registerOnboardingIpc — validate-key passes baseUrl to pingProvider'
expect(pingProvider).toHaveBeenCalledWith('anthropic', 'sk-ant-test', undefined);
});
});

describe('getApiKeyForProvider — API key retrieval', () => {
it('returns the decrypted key when the provider secret exists in config', async () => {
const { loadConfigOnBoot, getApiKeyForProvider } = await import('./onboarding-ipc');

// Override readConfig to return a config with an anthropic secret.
const { readConfig } = await import('./config');
vi.mocked(readConfig).mockResolvedValueOnce({
version: 1,
provider: 'anthropic',
modelPrimary: 'claude-sonnet-4-6',
modelFast: 'claude-haiku-3',
secrets: { anthropic: { ciphertext: 'enc:sk-ant-test' } },
baseUrls: {},
});

await loadConfigOnBoot();
const key = getApiKeyForProvider('anthropic');
// decryptSecret mock strips the 'enc:' prefix.
expect(key).toBe('sk-ant-test');
});

it('throws PROVIDER_KEY_MISSING when provider has no stored secret', async () => {
const { getApiKeyForProvider } = await import('./onboarding-ipc');
expect(() => getApiKeyForProvider('openai')).toThrow(/PROVIDER_KEY_MISSING|No API key stored/);
});
});
38 changes: 23 additions & 15 deletions apps/desktop/src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@ export function App() {
const loadConfig = useCodesignStore((s) => s.loadConfig);
const sendPrompt = useCodesignStore((s) => s.sendPrompt);
const isGenerating = useCodesignStore((s) => s.isGenerating);
const openSettings = useCodesignStore((s) => s.openSettings);
const closeSettings = useCodesignStore((s) => s.closeSettings);
const setView = useCodesignStore((s) => s.setView);
const openCommandPalette = useCodesignStore((s) => s.openCommandPalette);
const closeCommandPalette = useCodesignStore((s) => s.closeCommandPalette);
const settingsOpen = useCodesignStore((s) => s.settingsOpen);
const view = useCodesignStore((s) => s.view);
const commandPaletteOpen = useCodesignStore((s) => s.commandPaletteOpen);

const [prompt, setPrompt] = useState('');
Expand Down Expand Up @@ -55,7 +54,7 @@ export function App() {
combo: 'mod+,',
handler: () => {
if (!ready) return;
openSettings();
setView('settings');
},
},
{
Expand All @@ -68,8 +67,13 @@ export function App() {
{
combo: 'escape',
handler: () => {
if (settingsOpen) closeSettings();
else if (commandPaletteOpen) closeCommandPalette();
if (commandPaletteOpen) {
closeCommandPalette();
return;
}
if (view === 'settings') {
setView('workspace');
}
},
preventDefault: false,
},
Expand All @@ -79,11 +83,10 @@ export function App() {
isGenerating,
ready,
sendPrompt,
settingsOpen,
view,
commandPaletteOpen,
openSettings,
setView,
openCommandPalette,
closeSettings,
closeCommandPalette,
],
);
Expand All @@ -104,13 +107,18 @@ export function App() {
return (
<div className="h-full flex flex-col bg-[var(--color-background)]">
<TopBar />
<div className="flex-1 grid grid-cols-[360px_1fr] min-h-0">
<Sidebar prompt={prompt} setPrompt={setPrompt} onSubmit={submit} />
<main className="flex flex-col min-h-0">
<PreviewPane onPickStarter={(p) => setPrompt(p)} />
</main>
<div className="flex-1 min-h-0">
{view === 'settings' ? (
<Settings />
) : (
<div className="h-full grid grid-cols-[360px_1fr]">
<Sidebar prompt={prompt} setPrompt={setPrompt} onSubmit={submit} />
<main className="flex flex-col min-h-0">
<PreviewPane onPickStarter={(p) => setPrompt(p)} />
</main>
</div>
)}
</div>
<Settings />
<CommandPalette />
<ToastViewport />
</div>
Expand Down
6 changes: 3 additions & 3 deletions apps/desktop/src/renderer/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function CommandPalette() {
const t = useT();
const open = useCodesignStore((s) => s.commandPaletteOpen);
const close = useCodesignStore((s) => s.closeCommandPalette);
const openSettings = useCodesignStore((s) => s.openSettings);
const setView = useCodesignStore((s) => s.setView);
const toggleTheme = useCodesignStore((s) => s.toggleTheme);
const pushToast = useCodesignStore((s) => s.pushToast);

Expand Down Expand Up @@ -52,7 +52,7 @@ export function CommandPalette() {
label: t('commands.items.openSettings'),
hint: t('commands.hints.openSettings'),
icon: SettingsIcon,
run: openSettings,
run: () => setView('settings'),
},
{
id: 'export',
Expand All @@ -67,7 +67,7 @@ export function CommandPalette() {
}),
},
],
[openSettings, pushToast, t, toggleTheme],
[setView, pushToast, t, toggleTheme],
);

const filtered = useMemo(() => {
Expand Down
38 changes: 36 additions & 2 deletions apps/desktop/src/renderer/src/components/Settings.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { describe, expect, it } from 'vitest';
import { applyValidateResult, canSaveProvider } from './Settings';
import { describe, expect, it, vi } from 'vitest';
import { applyLocaleChange, applyValidateResult, canSaveProvider } from './Settings';

vi.mock('@open-codesign/i18n', () => ({
setLocale: vi.fn((locale: string) => Promise.resolve(locale)),
useT: () => (key: string) => key,
}));

describe('canSaveProvider', () => {
it('requires a validated API key before enabling save', () => {
Expand Down Expand Up @@ -79,3 +84,32 @@ describe('applyValidateResult', () => {
expect(next.validated).toBe(false);
});
});

describe('applyLocaleChange', () => {
it('calls locale IPC set, then applies the persisted locale via i18next', async () => {
const { setLocale: mockSetLocale } = await import('@open-codesign/i18n');
const mockLocaleApi = {
set: vi.fn((_locale: string) => Promise.resolve('zh-CN')),
};

const result = await applyLocaleChange('zh-CN', mockLocaleApi);

expect(mockLocaleApi.set).toHaveBeenCalledWith('zh-CN');
expect(mockSetLocale).toHaveBeenCalledWith('zh-CN');
expect(result).toBe('zh-CN');
});

it('applies the locale returned by the IPC bridge, not the requested locale', async () => {
const { setLocale: mockSetLocale } = await import('@open-codesign/i18n');
// Bridge normalises 'zh' → 'zh-CN'
const mockLocaleApi = {
set: vi.fn((_locale: string) => Promise.resolve('zh-CN')),
};

const result = await applyLocaleChange('zh', mockLocaleApi);

expect(mockLocaleApi.set).toHaveBeenCalledWith('zh');
expect(mockSetLocale).toHaveBeenCalledWith('zh-CN');
expect(result).toBe('zh-CN');
});
});
Loading
Loading