forked from BasedHardware/omi
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathChatErrorState.swift
More file actions
191 lines (178 loc) · 7.87 KB
/
Copy pathChatErrorState.swift
File metadata and controls
191 lines (178 loc) · 7.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import Foundation
// MARK: - ChatErrorState
//
// Defines the five user-visible failure classes the chat surface needs
// explicit, recoverable UI for, and maps `BridgeError` onto them.
//
// Deliberately out of scope (kept as their own dedicated sheets, since
// they are product flows rather than generic error recovery):
// - `ClaudeAuthSheet` (Claude paywall flow)
// - `showOmiThresholdAlert` (usage-cap upgrade alert)
/// Why the bridge process is unavailable. Used to drive copy and choose
/// whether the primary recovery opens runtime install docs or retries.
enum BridgeUnavailableReason: Equatable, Sendable {
/// Node.js binary not found on PATH (e.g. fresh install, dev build before
/// `./run.sh`). Maps from `BridgeError.nodeNotFound`.
case nodeMissing
/// Bridge JS / AI components not on disk. Maps from
/// `BridgeError.bridgeScriptNotFound` and
/// `BridgeError.agentRuntimePayloadIncomplete` — both mean this install cannot
/// run chat until it is repaired, so both take the install-runtime recovery.
case runtimeMissing
/// Bridge process started but exited / OOM'd. Maps from
/// `BridgeError.processExited` and `.outOfMemory`.
case crashed
/// A typed launch or handshake failure. The retry flow retains this cause
/// instead of collapsing all startup failures into generic unavailability.
case failedToStart(AgentRuntimeBridgeLifecycle.StartFailure)
/// Catch-all for "we don't know why it's not running"; maps from
/// `BridgeError.notRunning`, `.restarting`, and any other un-classified
/// start failure.
case unknown
}
/// The five recoverable error states the chat UI renders inline.
///
/// Anything that does NOT map to a case here (e.g. `BridgeError.encodingError`,
/// `.quotaExceeded`, opaque `.agentError`) is intentionally left to the existing
/// `errorMessage` banner / sheets. The `from(_:)` factory returns `nil` in
/// those cases so callers can fall through.
enum ChatErrorState: Equatable, Sendable {
/// Token expired or the bridge emitted `auth_required` mid-turn. Recovery:
/// re-sign-in. Distinct from the Claude OAuth paywall.
case authRequired
/// Per-turn or per-tool timeout. `toolName` is the offending tool
/// when the timeout was scoped (nil = full-turn timeout).
case timeout(toolName: String?)
/// Bridge process can't run. Reason picks the recovery: nodeMissing /
/// runtimeMissing open runtime install docs; crashed / unknown retry.
case bridgeUnavailable(reason: BridgeUnavailableReason)
/// User pressed Stop / Cancel mid-turn. Recovery: dismiss; the user can
/// type and send a new message when they want to continue.
case interrupted
/// Tools returned empty payloads and the model produced no text. Recovery:
/// nudge the user to try a different question instead of an infinite spinner.
/// Intentionally has no `BridgeError` mapping yet; empty-result detection
/// should set `currentError = .noDataFound` directly when that signal exists.
case noDataFound
}
// MARK: - Recovery actions
/// One primary recovery action per error card. Multiple cases may share the
/// same recovery — that's intentional.
enum ChatErrorRecoveryAction: Equatable, Sendable, CaseIterable {
/// Replay the last failed user turn with a fresh `turnId`.
case retry
/// Open the sign-in flow (Firebase / OAuth, NOT the Claude paywall).
case signIn
/// Show installation instructions for the bridge runtime (Node.js / AI
/// components). Currently routes to a docs URL.
case installRuntime
/// Dismiss the card with no further action.
case dismiss
}
extension ChatErrorState {
/// The single primary CTA shown on the error card.
///
/// Design note: we deliberately surface only ONE recovery per card. A
/// "Show details" disclosure can offer secondary affordances, but the card
/// itself stays scannable.
var primaryRecovery: ChatErrorRecoveryAction {
switch self {
case .authRequired:
return .signIn
case .timeout:
return .retry
case .bridgeUnavailable(let reason):
switch reason {
case .nodeMissing, .runtimeMissing:
return .installRuntime
case .crashed, .failedToStart, .unknown:
return .retry
}
case .interrupted:
return .dismiss
case .noDataFound:
return .dismiss
}
}
/// Compact summary for surfaces that only show a single line (floating bar).
var userFacingSummary: String {
switch self {
case .authRequired:
return "Please sign in to continue."
case .timeout:
return "AI took too long to respond."
case .bridgeUnavailable(let reason):
switch reason {
case .failedToStart(let failure):
switch failure {
case .handshakeTimedOut: return "AI took too long to start. Try again."
case .incompatibleHandshake: return "AI needs to restart before it can respond. Try again."
case .exitedDuringStartup, .launchFailed: return "AI couldn't start. Try again."
}
// Repairing the install is the only way out, so even the one-line
// floating-bar summary has to say that rather than "not available".
case .nodeMissing, .runtimeMissing:
return "AI components aren't installed."
case .crashed, .unknown:
return "AI isn't available right now."
}
case .interrupted:
return "Response stopped."
case .noDataFound:
return "No matching data found."
}
}
}
// MARK: - BridgeError mapping
extension ChatErrorState {
/// Lift a `BridgeError` into a `ChatErrorState` when one of the five
/// recoverable cases applies. Returns `nil` for errors that should keep
/// flowing into the existing `errorMessage` banner (encoding errors,
/// quota / paywall, generic agent errors).
///
/// Cases handled:
/// - `.timeout` → `.timeout(toolName: nil)`
/// - `.nodeNotFound` → `.bridgeUnavailable(.nodeMissing)`
/// - `.bridgeScriptNotFound` → `.bridgeUnavailable(.runtimeMissing)`
/// - `.agentRuntimePayloadIncomplete` → `.bridgeUnavailable(.runtimeMissing)`
/// - `.processExited` → `.bridgeUnavailable(.crashed)`
/// - `.outOfMemory` → `.bridgeUnavailable(.crashed)`
/// - `.failedToStart` → `.bridgeUnavailable(.unknown)` with retry
/// - `.notRunning` → `.bridgeUnavailable(.unknown)`
/// - `.restarting` → `.bridgeUnavailable(.unknown)`
/// - `.authMissing` → `.authRequired`
///
/// Cases conditionally handled:
/// - `.agentError` session-token auth strings → `.authRequired`
///
/// Cases intentionally returning `nil` (fall through to existing banner):
/// - `.encodingError` (internal error, retry won't help)
/// - `.quotaExceeded` (paywall — kept as separate sheet)
/// - opaque `.agentError` (varied; existing banner already classifies)
/// - `.agentRuntimeFailure` (already carries runtime-specific copy)
/// - `.requestAlreadyActive` (the existing banner explains the active turn)
static func from(_ bridgeError: BridgeError) -> ChatErrorState? {
switch bridgeError {
case .timeout:
return .timeout(toolName: nil)
case .stopped:
return nil
case .nodeNotFound:
return .bridgeUnavailable(reason: .nodeMissing)
case .bridgeScriptNotFound, .agentRuntimePayloadIncomplete:
return .bridgeUnavailable(reason: .runtimeMissing)
case .processExited, .outOfMemory:
return .bridgeUnavailable(reason: .crashed)
case .failedToStart(let failure):
return .bridgeUnavailable(reason: .failedToStart(failure))
case .notRunning, .restarting:
return .bridgeUnavailable(reason: .unknown)
case .authMissing:
return .authRequired
case .agentError(let message):
return BridgeError.agentError(message).isSessionAuthenticationFailure ? .authRequired : nil
case .encodingError, .quotaExceeded, .agentRuntimeFailure, .requestAlreadyActive:
return nil
}
}
}