Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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: 28 additions & 0 deletions apps/desktop/src/renderer/src/App.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest';
import { readIframeErrorMessage } from './App';

describe('readIframeErrorMessage', () => {
it('ignores iframe errors while a new generation is in progress', () => {
const data = {
__codesign: true,
type: 'IFRAME_ERROR',
kind: 'error',
message: 'old preview failed',
timestamp: Date.now(),
};

expect(readIframeErrorMessage(true, data)).toBeNull();
});

it('returns the iframe error message when generation is idle', () => {
const data = {
__codesign: true,
type: 'IFRAME_ERROR',
kind: 'error',
message: 'current preview failed',
timestamp: Date.now(),
};

expect(readIframeErrorMessage(false, data)).toBe('current preview failed');
});
});
17 changes: 17 additions & 0 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 { isIframeErrorMessage } from '@open-codesign/runtime';
import { useEffect, useMemo, useState } from 'react';
import { CommandPalette } from './components/CommandPalette';
import { PreviewPane } from './components/PreviewPane';
Expand All @@ -10,6 +11,11 @@ import { useKeyboard } from './hooks/useKeyboard';
import { Onboarding } from './onboarding';
import { useCodesignStore } from './store';

export function readIframeErrorMessage(isGenerating: boolean, data: unknown): string | null {
if (isGenerating || !isIframeErrorMessage(data)) return null;
return data.message;

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.

readIframeErrorMessage() 现在只返回 data.message,把 kind/source/lineno 都丢掉了。当前 runtime 合同仍然会提供这些字段,所以这会把原本可定位的 error: boom (foo.js:12) 退化成裸 message,CanvasErrorBar 的排障价值明显下降。建议在这里保留最少的上下文格式化。

}

export function App() {
const t = useT();
const config = useCodesignStore((s) => s.config);
Expand All @@ -30,6 +36,17 @@ export function App() {
void loadConfig();
}, [loadConfig]);

useEffect(() => {
const handler = (e: MessageEvent) => {
const state = useCodesignStore.getState();
const message = readIframeErrorMessage(state.isGenerating, e.data);

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.

这里少了对 event.source 的约束。上一版错误监听虽然会把 stale error 带进来,但至少只接受当前 preview iframe 发出的消息;现在改成 App 级全局监听后,旧 iframe 在切换边界上迟到的 IFRAME_ERROR 也会被收下,导致新预览重新出现上一轮的 runtime error。更稳妥的做法是继续复用 isTrustedPreviewMessageSource(...),或者把 error 监听留在持有 iframeRef 的组件里。

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.

这里直接消费 window.message 的 e.data,但没有任何 event.source 约束。由于 PreviewPane 仍在监听并已做 source 校验,这里会形成重复消费并允许非当前 preview 源消息进入 store。建议删除 App 级监听,或至少把 source 校验前置到与 preview iframe 绑定的上下文中。

// App.tsx
// remove this listener and keep PreviewPane's onMessage handler only

if (message === null) return;
state.pushIframeError(message);
};
window.addEventListener('message', handler);
return () => window.removeEventListener('message', handler);
}, []);

function submit(): void {
const trimmed = prompt.trim();
if (!trimmed || isGenerating) return;
Expand Down
97 changes: 97 additions & 0 deletions apps/desktop/src/renderer/src/store.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { OnboardingState } from '@open-codesign/shared';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { useCodesignStore } from './store';

const READY_CONFIG: OnboardingState = {
hasKey: true,
provider: 'anthropic',
modelPrimary: 'claude-sonnet-4-6',
modelFast: 'claude-haiku-3',
baseUrl: null,
designSystem: null,
};

const initialState = useCodesignStore.getState();

function resetStore() {
useCodesignStore.setState({
...initialState,
messages: [],
previewHtml: null,
isGenerating: false,
errorMessage: null,
lastError: null,
config: READY_CONFIG,
configLoaded: true,
toastMessage: null,
iframeErrors: [],
toasts: [],
});
}

beforeEach(() => {
resetStore();
});

afterEach(() => {
vi.unstubAllGlobals();
});

describe('useCodesignStore iframe error handling', () => {
it('clears stale iframe errors when starting a new generation', async () => {
let resolveGenerate: ((value: unknown) => void) | undefined;
const generate = vi.fn(
() =>
new Promise((resolve) => {
resolveGenerate = resolve;
}),
);

vi.stubGlobal('window', {
codesign: {
generate,
},
});

useCodesignStore.setState({ iframeErrors: ['old iframe error'] });

const sendPromise = useCodesignStore.getState().sendPrompt({ prompt: 'make a landing page' });

expect(useCodesignStore.getState().iframeErrors).toEqual([]);
expect(useCodesignStore.getState().isGenerating).toBe(true);

resolveGenerate?.({
artifacts: [{ content: '<html></html>' }],
message: 'Done.',
});
await sendPromise;

expect(generate).toHaveBeenCalledOnce();
});

it('deduplicates consecutive identical iframe errors', () => {
const { pushIframeError } = useCodesignStore.getState();

pushIframeError('first');
pushIframeError('first'); // duplicate — should be skipped
pushIframeError('second');
pushIframeError('second'); // duplicate — should be skipped
pushIframeError('third');

expect(useCodesignStore.getState().iframeErrors).toEqual(['first', 'second', 'third']);
});

it('caps iframeErrors at 50 entries and drops the oldest when exceeded', () => {
const { pushIframeError } = useCodesignStore.getState();

for (let i = 0; i < 55; i++) {
pushIframeError(`error-${i}`);
}

const errors = useCodesignStore.getState().iframeErrors;
expect(errors).toHaveLength(50);
// oldest (0-4) should have been shifted out; newest (5-54) remain
expect(errors[0]).toBe('error-5');
expect(errors[49]).toBe('error-54');
});
});
9 changes: 6 additions & 3 deletions apps/desktop/src/renderer/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,12 @@ export const useCodesignStore = create<CodesignState>((set, get) => ({
},

pushIframeError(message) {
set((s) => ({
iframeErrors: [...s.iframeErrors.slice(-9), message],
}));
set((s) => {
const last = s.iframeErrors[s.iframeErrors.length - 1];
if (last === message) return {};
const next = [...s.iframeErrors, message];
return { iframeErrors: next.length > 50 ? next.slice(1) : next };
});
},

async loadConfig() {
Expand Down
Loading