Skip to content

feat(reliability): error boundaries + cancellation + retry + iframe error reporting#11

Merged
hqhq1025 merged 1 commit intomainfrom
wt/reliability-v2
Apr 18, 2026
Merged

feat(reliability): error boundaries + cancellation + retry + iframe error reporting#11
hqhq1025 merged 1 commit intomainfrom
wt/reliability-v2

Conversation

@hqhq1025
Copy link
Copy Markdown
Collaborator

Goal

Close the seven Tier-1 reliability gaps from docs/research/09-polish-parity-backlog.md so a single rendering bug, network blip, or rate-limit never blanks the app or leaves the user staring at silence.

ID Gap Where
C1 App-shell React error boundary with Reload + Copy stack components/ErrorBoundary.tsx, App.tsx
C2 Per-pane boundaries (Sidebar / Preview / TopBar) App.tsx
C5 Generation cancellation (AbortController -> pi-ai signal) core, providers, store.ts, App.tsx
C6 Retry with exponential backoff + jitter, surfaced loudly providers/src/retry.ts
C7 429 Retry-After parsing + countdown toast store.ts, App.tsx
C10 Sandbox iframe error reporting runtime/src/overlay.ts, runtime/src/iframe-errors.ts, components/CanvasErrorBar.tsx
C11 Overlay listener defense (setInterval(reattach, 200)) runtime/src/overlay.ts

Files changed

New

  • packages/providers/src/retry.tscompleteWithRetry(), classifyError(), sleepWithAbort()
  • packages/providers/src/retry.test.ts — 12 vitest cases
  • packages/runtime/src/iframe-errors.tsIframeErrorMessage + isIframeErrorMessage guard
  • apps/desktop/src/renderer/src/components/ErrorBoundary.tsx — class boundary, copy-stack fallback
  • apps/desktop/src/renderer/src/components/CanvasErrorBar.tsx — slim red strip above iframe

Modified

  • packages/runtime/src/overlay.ts — capture-phase listeners, window.error + unhandledrejection, setInterval(reattach, 200)
  • packages/runtime/src/index.ts — re-exports IframeErrorMessage + guard
  • packages/core/src/index.tsGenerateInput.signal + onRetry, wraps complete with completeWithRetry
  • packages/core/src/generate.test.ts — mock now exports completeWithRetry passthrough
  • packages/providers/src/index.ts — re-exports retry surface
  • packages/shared/src/index.ts — adds IframeErrorEvent zod schema
  • apps/desktop/src/renderer/src/App.tsx — split into Sidebar / PreviewPane / TopBar, each wrapped in <ErrorBoundary>; CanvasErrorBar above iframe; iframe message listener; Send -> Cancel toggle while generating; rate-limit toast with countdown
  • apps/desktop/src/renderer/src/store.tscurrentAbortController, cancelGeneration(), iframeErrors[], rateLimitedUntil, statusLines[]

Files NOT touched (boundary): apps/desktop/src/main/**, apps/desktop/src/preload/**, packages/exporters/**, packages/i18n/**, apps/desktop/src/renderer/src/onboarding/**, packages/templates/**, packages/artifacts/**, packages/ui/**.

NO new prod dependencies

Confirmed — git diff main...HEAD -- '**/package.json' only touches workspace ranges, no new deps. The four PRINCIPLES §5b checks all stay green:

  • Compatibility: kept Onboarding flow + PreviewToolbar untouched
  • Upgradeability: schema-versioning continues to apply (no on-disk format changes)
  • No bloat: zero new runtime libraries, classify/retry are ~150 LOC of pure TS
  • Elegance: completeWithRetry is a thin pure wrapper — complete() itself is unchanged

Acceptance test outcomes

pnpm install        # clean
pnpm -r typecheck   # 9/9 packages pass
pnpm lint           # no errors (3 pre-existing complexity warnings remain)
pnpm -r test        # 32/32 tests passing across the workspace
                    #   providers: 21 (12 new retry + 9 validate)
                    #   core:       3 (existing — mock updated for completeWithRetry)
                    #   runtime:    3 (overlay/iframe build)
                    #   artifacts:  5

The 12 new retry tests cover: success first-try, transient retry success surfaced via onRetry, exhausted retries, non-retryable 401, 429 honouring Retry-After, AbortSignal pre-call cancellation, AbortSignal mid-backoff cancellation, plus 5 classifyError cases (5xx / 4xx / 429 / AbortError / TypeError).

Integration notes

  • Overlap with wt/preview-ux-v2: that branch is expected to restructure App.tsx into Sidebar / PreviewPane / TopBar components. This PR already does that split and wraps each in <ErrorBoundary scope="...">. When merging, keep the ErrorBoundary wrappers and let preview-ux-v2 swap the inner JSX as it likes — the boundaries are cosmetic-agnostic.
  • Cancellation through IPC: the preload IPC contract is owned by another worktree and the apps/desktop/src/main/** boundary is hands-off. So the renderer races the IPC promise against an AbortController and discards the result on cancel. Once the IPC layer learns to forward signal, the wiring in core.generate(signal) is already in place — main just needs to pass it through.
  • Loud retries through IPC: onRetry callbacks fire in main-process land today; surfacing them in the renderer needs a future IPC channel (codesign:generation-status). This PR ships the loud surface (statusLines[] + Sidebar list) ready to receive those events.

Test plan

  • pnpm dev, paste an invalid model name, confirm error lands in chat (not silent)
  • During generation, click the Cancel button — Sidebar shows "Generation cancelled" status line, isGenerating clears immediately
  • Inject <script>throw new Error('boom')</script> into a generated artifact and confirm the red CanvasErrorBar appears with "boom (about:srcdoc:LINE)"
  • Inject <script>setTimeout(()=>{throw 1},10);document.removeEventListener('click',()=>{},true)</script> and verify clicks still trigger ELEMENT_SELECTED after 200ms (overlay re-attach)
  • Force a render exception in PreviewPane (e.g. mutate previewHtml to a non-string) and confirm only the right pane shows the boundary card; Sidebar stays interactive

…rror reporting

Adds the seven reliability gaps tracked in
docs/research/09-polish-parity-backlog.md:

C1  App-shell React error boundary with Reload + Copy stack actions
C2  Per-pane boundaries (Sidebar / Preview / TopBar) so a single crash
    never blanks the window
C5  AbortController threaded through core.generate -> providers.complete
    -> pi-ai signal; renderer races IPC against abort and shows Cancel
    button while generating
C6  completeWithRetry wrapper in packages/providers: max 3 attempts,
    base 500ms with +/-20% jitter, retries only 5xx / network /
    429-with-Retry-After. Every attempt is surfaced via onRetry — no
    silent retries (PRINCIPLES §10)
C7  429 rate-limit handling: parse Retry-After, store rateLimitedUntil,
    show "Rate limited until HH:MM:SS" toast with countdown
C10 Sandbox iframe error reporting: overlay.ts catches window.error +
    unhandledrejection and postMessages IFRAME_ERROR; surfaced as a
    slim red CanvasErrorBar above the iframe
C11 Overlay defense: setInterval(reattach, 200) re-installs listeners
    in case generated HTML strips them; capture-phase listeners

New files:
  packages/providers/src/retry.ts (+12 vitest cases in retry.test.ts)
  packages/runtime/src/iframe-errors.ts
  apps/desktop/src/renderer/src/components/ErrorBoundary.tsx
  apps/desktop/src/renderer/src/components/CanvasErrorBar.tsx

No new prod dependencies. Tier 1 — no animations, no new libraries.
All UI uses var(--color-*) tokens.

Signed-off-by: hqhq1025 <1506751656@qq.com>
@hqhq1025 hqhq1025 force-pushed the wt/reliability-v2 branch from 159bce3 to 94ad787 Compare April 18, 2026 07:55
@hqhq1025 hqhq1025 merged commit b41da76 into main Apr 18, 2026
4 of 7 checks passed
@hqhq1025 hqhq1025 deleted the wt/reliability-v2 branch April 18, 2026 07:57
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