feat(desktop): structured error recovery cards for chat#7552
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Greptile SummaryThis PR introduces a structured error-recovery layer for the macOS chat surface: a
Confidence Score: 4/5The new error card layer is additive and strictly scoped; the legacy banner remains as a fallback so no error path disappears. The main concern is that currentError is never cleared at the start of sendMessage, so a stale error card can remain visible throughout a new user-initiated send and even persist after a successful response. The BridgeError mapping, card rendering, recovery dispatch, and unit tests are otherwise well-structured. desktop/Desktop/Sources/Providers/ChatProvider.swift - the sendMessage preamble needs currentError = nil to match the existing errorMessage = nil clear-on-new-send contract. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[User sends message] --> B[sendMessage called]
B --> C{BridgeError thrown?}
C -->|No| D[Success: response shown]
C -->|Yes - stopped| F[ChatErrorState.from stopped]
C -->|Yes - other| G[ChatErrorState.from bridgeError]
F --> H{maps to card?}
G --> H
H -->|nil| I[errorMessage = localizedDescription\ncurrentError = nil]
H -->|some card| J[currentError = card\nlastFailedPrompt saved\nerrorMessage = nil]
J --> K[ChatErrorCard rendered above legacy banner]
K --> L{User action}
L -->|Taps CTA| M[recoverFromError dispatches action]
L -->|Taps X| N[dismissCurrentError - card cleared]
M --> O1[retry - sendMessage with saved prompt]
M --> O2[signIn - open omi.me]
M --> O3[installRuntime - open nodejs.org]
M --> O4[openSettings - post notification]
M --> O5[switchMode - toggle bridge mode]
M --> O6[dismiss - card cleared]
|
Greptile review on BasedHardware#7552: `.openSettings` and `.switchMode` were never returned by any arm of `ChatErrorState.primaryRecovery`, so their branches in `recoverFromError()` and `ChatErrorCard.primaryCTATitle` were dead code. The guarding test only asserted a hand-written 6-element array had 6 elements — trivially true — so it never caught the gap. - Remove `.openSettings` and `.switchMode` from `ChatErrorRecoveryAction` and their handler/CTA branches. The reachable set is now exactly `{ retry, signIn, installRuntime, dismiss }`. - Make `ChatErrorRecoveryAction` `CaseIterable` and replace the trivial count test with one that asserts every action is *reachable* — i.e. produced by some state's `primaryRecovery`. This fails if any action becomes dead code again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kodjima33
left a comment
There was a problem hiding this comment.
Desktop feature: structured error recovery cards, additive with legacy fallback. Approve only — feature >500 lines and mergeStateStatus BLOCKED.
Replace the single inline `errorMessage` banner on the chat surface with structured, recoverable error cards. `ChatErrorState` defines five user-visible failure classes (auth required, timeout, bridge unavailable, interrupted, no data found) and maps `BridgeError` onto them. Each state carries a primary recovery action; unmappable errors keep falling back to the legacy banner so no error path becomes invisible. `ChatErrorCard` renders a state as an inline, message-level card with a recovery CTA and an optional redacted "Show details" disclosure. ChatProvider wiring: - `currentError` / `lastFailedPrompt` drive the card and enable retry. - sendMessage's catch prefers the card for mappable BridgeError cases and clears the legacy banner; unmappable cases keep the banner. - `recoverFromError()` dispatches each recovery action to a concrete handler (retry, dismiss, open sign-in, open AI Chat settings, open the Node install page, toggle bridge mode). - `dismissCurrentError()` clears the card without retrying. ChatPage renders the card above the legacy banner; only one is ever active per turn. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Greptile review on BasedHardware#7552: `.openSettings` and `.switchMode` were never returned by any arm of `ChatErrorState.primaryRecovery`, so their branches in `recoverFromError()` and `ChatErrorCard.primaryCTATitle` were dead code. The guarding test only asserted a hand-written 6-element array had 6 elements — trivially true — so it never caught the gap. - Remove `.openSettings` and `.switchMode` from `ChatErrorRecoveryAction` and their handler/CTA branches. The reachable set is now exactly `{ retry, signIn, installRuntime, dismiss }`. - Make `ChatErrorRecoveryAction` `CaseIterable` and replace the trivial count test with one that asserts every action is *reachable* — i.e. produced by some state's `primaryRecovery`. This fails if any action becomes dead code again. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
da94852 to
77d101d
Compare
|
Rebased onto the latest |
Summary
errorMessagebanner — a flat red string with no recovery path. Auth lapses, timeouts, a missing bridge runtime, user interrupts, and empty results all look the same and offer the user nothing to do.ChatErrorState(five user-visible failure classes mapped fromBridgeError) andChatErrorCard, an inline message-level card with a primary recovery CTA.ChatProvidernow drives the card and dispatches each recovery action to a concrete handler.ClaudeAuthSheet,showOmiThresholdAlert, browser-extension setup) are intentionally left as-is — they are product flows, not generic error recovery. No telemetry, backend, or bridge-protocol changes. The legacyerrorMessagebanner is retained as the fallback for unmappable errors, so no error path becomes invisible.Linked Issue / Bounty
Root Cause (bug fixes only)
How It Works
ChatErrorState.from(_ bridgeError:)lifts aBridgeErrorinto one of five recoverable states (authRequired,timeout,bridgeUnavailable,interrupted,noDataFound). Cases that shouldn't preempt the existing banner (encodingError,quotaExceeded, free-formagentError) returnniland fall through.sendMessage's catch block prefers the structured card when the error maps, clearingerrorMessage; unmappable errors keep the legacy banner. Only one surface is active per turn.ChatErrorStateexposes aprimaryRecoveryaction.recoverFromError()dispatches it:.retryre-issues the captured last prompt;.signInopens the sign-in URL;.openSettingsposts the existingnavigateToAIChatSettingsnotification;.installRuntimeopens the Node install page;.switchModetoggles the bridge mode via the existingswitchBridgeMode(to:);.dismissclears.ChatPagerendersChatErrorCardabove the legacy banner so its CTA gets the prominent slot.Change Type
Scope
Security Impact
.signInopens a public URL; it does not touch tokens or the auth store..signIn/.installRuntimehand a public URL to the default browser viaNSWorkspace.open.Testing
xcrun swift build -c debug --package-path Desktop→Build complete!ChatErrorStateTests.swift(11 tests): fullBridgeError → ChatErrorStatemapping, every state has a recovery action, and a guard locking the 6-case recovery-action set so a new case forces arecoverFromErrorswitch update.Human Verification
main; all 11ChatErrorStatetests pass; full test target compiles.BridgeErrorcases correctly fall through to the legacy banner;bridgeUnavailablereason split (node-missing / runtime-missing / crashed / unknown).Evidence
Docs
Performance, Privacy, and Reliability
Migration / Backward Compatibility
Risks and Mitigations
BridgeErrorcase silently fails to map. Mitigation:testFactoryMappabilityIsStableUnderRefactorlocks the mapping; changing it forces a test update..signInopens the sign-in URL rather than triggering native in-app OAuth. This is the honest minimum; deepening it is a natural follow-up.AI Disclosure