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
35 changes: 30 additions & 5 deletions apps/desktop/src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useT } from '@open-codesign/i18n';
import { ChevronLeft } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { CommandPalette } from './components/CommandPalette';
import { PreviewPane } from './components/PreviewPane';
Expand All @@ -9,6 +10,7 @@ import { TopBar } from './components/TopBar';
import { useKeyboard } from './hooks/useKeyboard';
import { Onboarding } from './onboarding';
import { useCodesignStore } from './store';
import { HubView } from './views/HubView';

export function App() {
const t = useT();
Expand Down Expand Up @@ -73,6 +75,10 @@ export function App() {
}
if (view === 'settings') {
setView('workspace');
return;
}
if (view === 'workspace') {
setView('hub');
}
},
preventDefault: false,
Expand Down Expand Up @@ -110,12 +116,31 @@ export function App() {
<div className="flex-1 min-h-0">
{view === 'settings' ? (
<Settings />
) : view === 'hub' ? (
<HubView
onUseExamplePrompt={(p) => {
setPrompt(p);
setView('workspace');
}}
/>
) : (
<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 className="h-full flex flex-col">
<div className="px-[var(--space-5)] py-[var(--space-2)] border-b border-[var(--color-border-muted)] bg-[var(--color-background-secondary)]">
<button
type="button"
onClick={() => setView('hub')}
className="inline-flex items-center gap-[var(--space-1)] text-[var(--text-xs)] text-[var(--color-text-secondary)] hover:text-[var(--color-text-primary)] transition-colors"
>
<ChevronLeft className="w-[var(--size-icon-sm)] h-[var(--size-icon-sm)]" />
{t('hub.backToHub')}
</button>
</div>
<div className="flex-1 min-h-0 grid grid-cols-[var(--size-hub-sidebar)_1fr]">
<Sidebar prompt={prompt} setPrompt={setPrompt} onSubmit={submit} />
<main className="flex flex-col min-h-0">
<PreviewPane onPickStarter={(p) => setPrompt(p)} />
</main>
</div>
</div>
)}
</div>
Expand Down
111 changes: 108 additions & 3 deletions apps/desktop/src/renderer/src/store.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { initI18n } from '@open-codesign/i18n';
import type { OnboardingState } from '@open-codesign/shared';
import type { LocalInputFile, OnboardingState, SelectedElement } from '@open-codesign/shared';
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import type { GenerationStage } from './store';
import { useCodesignStore } from './store';

const READY_CONFIG: OnboardingState = {
Expand Down Expand Up @@ -231,8 +232,8 @@ describe('useCodesignStore generation cancellation', () => {
});

describe('useCodesignStore view navigation', () => {
it('starts on workspace view', () => {
expect(useCodesignStore.getState().view).toBe('workspace');
it('starts on hub view', () => {
expect(useCodesignStore.getState().view).toBe('hub');
});

it('setView("settings") switches to settings and closes command palette', () => {
Expand Down Expand Up @@ -331,3 +332,107 @@ describe('useCodesignStore active provider routing', () => {
expect(payload.model.modelId).toBe('gpt-4o');
});
});

describe('useCodesignStore project storage error surfacing', () => {
beforeAll(async () => {
await initI18n('en');
});

it('createProject pushes a toast when localStorage.setItem throws and keeps the project in memory', () => {
const setItem = vi.fn(() => {
throw new Error('QuotaExceededError');
});
const getItem = vi.fn(() => null);

vi.stubGlobal('window', {
localStorage: { setItem, getItem, removeItem: vi.fn(), clear: vi.fn() },
setTimeout,
});
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});

const projectsBefore = useCodesignStore.getState().projects;
const toastsBefore = useCodesignStore.getState().toasts.length;

const project = useCodesignStore
.getState()
.createProject({ name: 'My Project', type: 'slideDeck' });

const state = useCodesignStore.getState();

// In-memory state stays consistent — the project was added even though persist failed.
expect(state.projects[0]).toEqual(project);
expect(state.projects).toHaveLength(projectsBefore.length + 1);
expect(state.currentProjectId).toBe(project.id);

// Toast surfaced with the i18n title and the underlying error message.
expect(state.toasts.length).toBe(toastsBefore + 1);
expect(state.toasts.at(-1)).toMatchObject({
variant: 'error',
description: 'QuotaExceededError',
});
expect(state.toasts.at(-1)?.title).toBeTruthy();

expect(setItem).toHaveBeenCalledOnce();
expect(warnSpy).toHaveBeenCalled();

warnSpy.mockRestore();
});

it('createProject resets project-scoped workspace state when switching to the new project', () => {
vi.stubGlobal('window', {
localStorage: {
setItem: vi.fn(),
getItem: vi.fn(() => null),
removeItem: vi.fn(),
clear: vi.fn(),
},
setTimeout,
});

const staleFile: LocalInputFile = {
path: '/tmp/old.png',
name: 'old.png',
size: 1,
};
const staleSelection: SelectedElement = {
selector: '.stale',
tag: 'div',
outerHTML: '<div class="stale">old</div>',
rect: { top: 0, left: 0, width: 10, height: 10 },
};

useCodesignStore.setState({
messages: [{ role: 'user', content: 'old prompt' }],
previewHtml: '<html>old</html>',
inputFiles: [staleFile],
referenceUrl: 'https://example.com/old',
selectedElement: staleSelection,
lastPromptInput: { prompt: 'old prompt', attachments: [staleFile] },
isGenerating: true,
activeGenerationId: 'gen-old',
errorMessage: 'stale error',
lastError: 'stale error',
generationStage: 'streaming' as GenerationStage,
});

const project = useCodesignStore
.getState()
.createProject({ name: 'Fresh Project', type: 'prototype' });

const state = useCodesignStore.getState();
expect(state.currentProjectId).toBe(project.id);
expect(state.view).toBe('workspace');
expect(state.createProjectModalOpen).toBe(false);
expect(state.messages).toEqual([]);
expect(state.previewHtml).toBeNull();
expect(state.inputFiles).toEqual([]);
expect(state.referenceUrl).toBe('');
expect(state.selectedElement).toBeNull();
expect(state.lastPromptInput).toBeNull();
expect(state.isGenerating).toBe(false);
expect(state.activeGenerationId).toBeNull();
expect(state.errorMessage).toBeNull();
expect(state.lastError).toBeNull();
expect(state.generationStage).toBe('idle');
});
});
Loading
Loading