Skip to content

fix(desktop): dedupe error UI + wire iframe error listener#14

Merged
hqhq1025 merged 3 commits intomainfrom
wt/error-iframe
Apr 18, 2026
Merged

fix(desktop): dedupe error UI + wire iframe error listener#14
hqhq1025 merged 3 commits intomainfrom
wt/error-iframe

Conversation

@hqhq1025
Copy link
Copy Markdown
Collaborator

Summary

  • 移除 sendPrompt catch 块里的 toast 错误调用,生成失败只走 ErrorState
  • App.tsx 新增 message 监听,把 iframe 抛出的 IFRAME_ERROR 推到 store
  • PreviewPane 顶部挂载 CanvasErrorBar,让 iframe 运行时错误始终可见
  • store 新增 pushIframeError action

Test plan

  • pnpm --filter @open-codesign/desktop typecheck
  • pnpm lint
  • 故意触发生成失败,确认只有 ErrorState、无 toast
  • iframe 内 throw new Error(),确认 CanvasErrorBar 显示

🤖 Generated with subagent

@hqhq1025 hqhq1025 closed this Apr 18, 2026
@hqhq1025 hqhq1025 reopened this Apr 18, 2026
@hqhq1025 hqhq1025 closed this Apr 18, 2026
@hqhq1025 hqhq1025 reopened this Apr 18, 2026
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] 旧 iframe 错误会泄漏到下一次生成结果中。CanvasErrorBar 现在会一直挂在预览区顶部,但这次改动没有在新一轮 sendPrompt 或新 iframe 建立时清空 iframeErrors;结果是上一个 preview 的运行时异常会继续显示在新的 loading / preview 上,用户会把旧错误误判成当前结果的问题。证据:apps/desktop/src/renderer/src/components/PreviewPane.tsx:52apps/desktop/src/renderer/src/store.ts:116-117apps/desktop/src/renderer/src/store.ts:155-159
    Suggested fix:

    set((s) => ({
      messages: [...s.messages, userMessage],
      isGenerating: true,
      errorMessage: null,
      iframeErrors: [],
    }));
  • [Major] iframeErrors 现在是无上限追加的。window.message listener 会把每个 IFRAME_ERROR 都推入 store,而 pushIframeError 每次都复制并扩展整个数组;AI 生成的 HTML 很容易出现 setInterval/循环重试导致的重复 throw/reject,这里会持续占用内存并放大 rerender 成本。证据:apps/desktop/src/renderer/src/App.tsx:35apps/desktop/src/renderer/src/store.ts:117
    Suggested fix:

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

Summary

  • Review mode: initial
  • 发现 2 个 Major 问题,都是这次新增的 iframe error surface 引入的:旧错误会跨 preview 泄漏,且错误缓冲区无上限增长。
  • 未发现新增依赖、直接 provider SDK import 或 UI token 字面量违规。DCO / license / install-size 变化在本次 diff 中未涉及。

Testing

  • Not run (automation)
  • 缺少 Vitest coverage:window.postMessage -> pushIframeError、重新生成时清空旧错误、重复错误的去重/上限行为。

open-codesign Bot

return (
<div className="flex flex-col min-h-0 flex-1">
<PreviewToolbar />
<CanvasErrorBar />
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.

这里把 CanvasErrorBar 变成了常驻 UI,但本 PR 没有配套清空 iframeErrors。现在只要上一个 preview 抛过一次错,这条 bar 就会一直挂到后续新的 loading / preview 上,误导用户把旧错误当成当前结果的问题。

Suggested fix:

set((s) => ({
  messages: [...s.messages, userMessage],
  isGenerating: true,
  errorMessage: null,
  iframeErrors: [],
}));

Comment thread apps/desktop/src/renderer/src/store.ts Outdated
iframeErrors: [],

pushIframeError(message: string) {
set((s) => ({ iframeErrors: [...s.iframeErrors, 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.

这里是无上限追加;如果 preview 里的脚本在定时器里反复 throw / reject,store 会持续增长,而且每次都复制整数组。对 AI 生成的任意 HTML 来说,这个路径很容易被打满。

Suggested fix:

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

@Sun-sunshine06
Copy link
Copy Markdown
Collaborator

目前不建议合并,先处理下面这些问题:

  • DCO check 失败,提交里缺少 Signed-off-by:,需要先补签名。
  • 旧 iframe 错误会泄漏到下一次生成结果,导致用户把上一次 preview 的错误误认为当前结果的问题。相关位置:apps/desktop/src/renderer/src/components/PreviewPane.tsxapps/desktop/src/renderer/src/store.ts
  • iframeErrors 现在无上限追加,重复报错场景下会持续增长并放大 rerender / 内存开销。相关位置:apps/desktop/src/renderer/src/App.tsxapps/desktop/src/renderer/src/store.ts
  • 目前缺少对应测试覆盖,至少需要补上:重新生成时清空旧错误、重复错误去重 / 限长。

CI 主体已经是绿的,但上面这些行为问题和 DCO 没处理前,还是不适合进 main

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] 重新生成期间,上一版 preview 仍会把旧的 IFRAME_ERROR 重新灌回这次新清空的错误条。sendPrompt() 现在会在开始时清空 iframeErrors,但 PreviewPane 只有在“没有旧 preview”时才切到 LoadingState;已有 preview 时,旧 iframe 会继续挂着并继续 postMessage。这样一来,新增的全局 listener 会在同一轮生成过程中重新写入旧错误,CanvasErrorBar 也会把它显示成当前结果的问题。证据:apps/desktop/src/renderer/src/App.tsx:32apps/desktop/src/renderer/src/components/PreviewPane.tsx:31apps/desktop/src/renderer/src/store.ts:159
    Suggested fix:
    const handler = (e: MessageEvent) => {
      if (!isIframeErrorMessage(e.data)) return;
      const { isGenerating, pushIframeError } = useCodesignStore.getState();
      if (isGenerating) return;
      pushIframeError(e.data.message);
    };

Summary

  • Review mode: follow-up after new commits
  • 还剩 1 个 Major 问题:这次虽然补上了清空和去重/上限,但旧 preview 在重新生成期间仍能继续污染新的 iframe error surface。
  • 未发现这次新增/修改行引入新的依赖、直接 provider SDK import、silent fallback 或 UI token 字面量违规。DCO / license / install-size 变化未涉及本次 diff。

Testing

  • Not run (automation)
  • 建议补一个集成测试:在已有 previewHtml 时触发新一轮 sendPrompt(),并在 isGenerating=true 期间模拟旧 iframe 发出 IFRAME_ERROR,断言 CanvasErrorBar 不会重新出现。

open-codesign Bot

Comment thread apps/desktop/src/renderer/src/App.tsx Outdated

useEffect(() => {
const handler = (e: MessageEvent) => {
if (isIframeErrorMessage(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.

这里仍然会把“上一版 preview 还在运行时抛出的错误”记到这次新生成里。sendPrompt() 虽然先清空了 iframeErrors,但 PreviewPane 在已有 previewHtml 时不会卸载旧 iframe,所以它在生成期间还能继续 postMessage;这个全局 listener 会把这些旧错误重新写回 store,最后让 CanvasErrorBar 看起来像是当前结果出了问题。

Suggested fix:

const handler = (e: MessageEvent) => {
  if (!isIframeErrorMessage(e.data)) return;
  const { isGenerating, pushIframeError } = useCodesignStore.getState();
  if (isGenerating) return;
  pushIframeError(e.data.message);
};

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

  • 未发现基于本次新增/修改行、且置信度足够高的问题。

Summary

  • Review mode: follow-up after new commits
  • 上一轮指出的 Major 问题已经在 apps/desktop/src/renderer/src/App.tsx:13apps/desktop/src/renderer/src/store.ts:159 的改动中得到处理:重新生成期间会忽略 IFRAME_ERROR,并在开始新一轮生成时清空旧的 iframeErrors;本轮未发现新的 diff-attributable 问题。
  • 本次 diff 未引入新依赖,也未看到直接 provider SDK import、silent fallback 或新增 UI token 字面量违规。license / install-size 变化不涉及本次 diff。
  • 残余风险是测试边界仍偏窄:当前只覆盖了辅助函数和 store action,未见覆盖 window.message 监听到 CanvasErrorBar 渲染联动的组件级测试。

Testing

  • Not run (automation)
  • 本环境缺少 pnpm,未执行 Vitest / Playwright。

open-codesign Bot

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] 旧 iframe 的晚到错误仍会污染新预览状态。新的全局 message 监听只校验 isIframeErrorMessage,不再校验 event.source 是否仍然指向当前预览 iframe;这样一来,上一轮生成在切换前后迟到的 postMessage 仍会被 pushIframeError 收下,stale runtime error 会重新出现在新预览上,证据 apps/desktop/src/renderer/src/App.tsx:37apps/desktop/src/renderer/src/components/PreviewPane.tsx:31
    Suggested fix:

    if (!isTrustedPreviewMessageSource(event.source, iframeRef.current?.contentWindow)) return;
    
    const state = useCodesignStore.getState();
    if (state.isGenerating || !isIframeErrorMessage(event.data)) return;
    state.pushIframeError(formatIframeError(event.data));
  • [Minor] iframe 运行时错误的定位信息被截断。readIframeErrorMessage() 现在只保留 message,把原先的 kind/source/lineno 都丢掉了;CanvasErrorBar 最终只能显示裸文本,和运行时消息契约提供的上下文不一致,证据 apps/desktop/src/renderer/src/App.tsx:13packages/runtime/src/iframe-errors.ts:13
    Suggested fix:

    const location = data.source && data.lineno ? ` (${data.source}:${data.lineno})` : '';
    return `${data.kind}: ${data.message}${location}`;

Summary

  • Review mode: follow-up after new commits
  • 本轮主要问题集中在 App.tsx 新增的 iframe 错误监听:它解决了“生成中忽略旧错误”的一部分场景,但同时丢掉了当前 iframe source 校验和错误位置信息。
  • 未看到本次 diff 引入新依赖、直接 provider SDK import、silent fallback,license / install-size 变化也不涉及本次修改。

Testing

  • Not run (automation)
  • 按本任务的安全约束,未执行 PR 中代码,因此未跑 Vitest / Playwright。

open-codesign Bot

Comment thread apps/desktop/src/renderer/src/App.tsx Outdated
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 的组件里。

Comment thread apps/desktop/src/renderer/src/App.tsx Outdated

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 的排障价值明显下降。建议在这里保留最少的上下文格式化。

Sun-sunshine06 and others added 2 commits April 18, 2026 21:06
Signed-off-by: Sun-sunshine06 <[email protected]>
Signed-off-by: hqhq1025 <[email protected]>
- Change pushIframeError cap from 10 to 50; use shift-oldest strategy
  so the cap is explicit and the no-op dedup path is allocation-free.
- Return early (no state update) when new message equals the last entry.
- Add store.test.ts with three focused tests: clear-on-sendPrompt,
  consecutive-duplicate dedup, and the 50-entry hard cap.
- Fix import order in App.tsx to satisfy biome organizeImports.

Signed-off-by: hqhq1025 <[email protected]>
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

  • [Blocker] App.tsx 新增的全局 message 监听会与 PreviewPane 现有监听并行消费同一条 IFRAME_ERROR,且这里没有 event.source 校验,导致两类回归:1) 同一错误被重复写入(kind: message + 裸 message);2) 非当前 preview iframe 的消息也可能被接受,重新污染当前画布错误栏。证据 apps/desktop/src/renderer/src/App.tsx:39apps/desktop/src/renderer/src/App.tsx:42apps/desktop/src/renderer/src/App.tsx:44(对比现有 source 校验 apps/desktop/src/renderer/src/components/PreviewPane.tsx:36)。
    Suggested fix:
    // App.tsx
    // 删除 App 级 iframe 错误监听,保留 PreviewPane 内部监听(已有 source 校验 + 定位信息)
    // useEffect(() => { ...window.addEventListener('message', handler)... }, []);
    // export function readIframeErrorMessage(...) { ... }

Summary

  • Review mode: follow-up after new commits
  • 本轮发现 1 个阻塞问题:App 层新增监听引入重复/越权来源接收,属于本次 diff 直接引入的行为回归。
  • 本次 diff 未引入新第三方依赖;未发现 direct provider SDK import。

Testing

  • Not run (automation): 当前环境缺少 pnpmpnpm: command not found),无法执行 Vitest/Playwright。

open-codesign Bot

Comment thread apps/desktop/src/renderer/src/App.tsx Outdated
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.

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

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

…ion info

- Delete readIframeErrorMessage helper and the global window.addEventListener
  in App.tsx that was consuming IFRAME_ERROR without source validation, causing
  duplicate pushes to iframeErrors alongside the one in PreviewPane.
- Extract formatIframeError pure helper in PreviewPane.tsx so the kind + source:lineno
  location string is testable independently.
- Replace App.test.ts (which tested the now-deleted helper) with tests covering
  formatIframeError location formatting edge cases.

Signed-off-by: hqhq1025 <[email protected]>
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 Blocker/Major/Minor/Nit findings with >=80% confidence that are introduced by the current added/modified lines.

Summary

  • Review mode: follow-up after new commits
  • No diff-attributable issues found in the latest PR diff.
  • Constraint checks in scope: no new dependencies added (license/install-size impact not introduced by this diff), no direct provider SDK imports in changed files, and no newly introduced silent fallback pattern found in changed lines.
  • Residual risk/testing gap: runtime behavior was reviewed statically only; integration behavior was not executed in this review run.

Testing

  • Not run (automation): PR-content code execution was intentionally not performed in this review run.

open-codesign Bot

@hqhq1025 hqhq1025 merged commit 3e1a71c into main Apr 18, 2026
7 checks passed
@hqhq1025 hqhq1025 deleted the wt/error-iframe branch April 18, 2026 13:30
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.

2 participants