claude-code-rust 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,13 +10,74 @@ import { ensureToolCallVisible, setToolCallStatus, } from "./tool_calls.js";
10
10
  import { requestExitPlanModeApproval, requestAskUserQuestionAnswers, EXIT_PLAN_MODE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "./user_interaction.js";
11
11
  import { mapAvailableAgents, emitAvailableAgentsIfChanged, refreshAvailableAgents } from "./agents.js";
12
12
  import { emitAuthRequired, emitFastModeUpdateIfChanged } from "./error_classification.js";
13
+ function permissionDisplayFromCanUseOptions(options) {
14
+ const title = typeof options.title === "string" ? options.title.trim() : "";
15
+ const displayName = typeof options.displayName === "string" ? options.displayName.trim() : "";
16
+ const description = typeof options.description === "string" ? options.description.trim() : "";
17
+ if (!title && !displayName && !description) {
18
+ return undefined;
19
+ }
20
+ return {
21
+ ...(title ? { title } : {}),
22
+ ...(displayName ? { display_name: displayName } : {}),
23
+ ...(description ? { description } : {}),
24
+ };
25
+ }
26
+ const requestUserDialogInterceptorInstalled = Symbol("requestUserDialogInterceptorInstalled");
13
27
  export const sessions = new Map();
28
+ function nonEmptyString(value) {
29
+ return typeof value === "string" && value.trim().length > 0;
30
+ }
31
+ export function shouldEmitStartupAuthRequiredForAccount(account) {
32
+ const provider = account.apiProvider;
33
+ if (nonEmptyString(provider) && provider !== "firstParty") {
34
+ return false;
35
+ }
36
+ return !nonEmptyString(account.email) && !nonEmptyString(account.apiKeySource);
37
+ }
14
38
  const DEFAULT_SETTING_SOURCES = ["user", "project", "local"];
15
39
  const DEFAULT_MODEL_NAME = "default";
16
40
  const DEFAULT_PERMISSION_MODE = "default";
41
+ function isSdkElicitationContentValue(value) {
42
+ return (typeof value === "string" ||
43
+ typeof value === "number" ||
44
+ typeof value === "boolean" ||
45
+ (Array.isArray(value) && value.every((entry) => typeof entry === "string")));
46
+ }
47
+ function normalizeSdkElicitationContent(content) {
48
+ if (!content) {
49
+ return undefined;
50
+ }
51
+ const normalized = {};
52
+ for (const [key, value] of Object.entries(content)) {
53
+ if (isSdkElicitationContentValue(value)) {
54
+ normalized[key] = value;
55
+ }
56
+ }
57
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
58
+ }
17
59
  function settingsObjectFromLaunchSettings(launchSettings) {
18
60
  return launchSettings.settings;
19
61
  }
62
+ function normalizedSettingsFromLaunchSettings(launchSettings) {
63
+ const settings = settingsObjectFromLaunchSettings(launchSettings);
64
+ if (!settings) {
65
+ return undefined;
66
+ }
67
+ const sandbox = settings.sandbox && typeof settings.sandbox === "object" && !Array.isArray(settings.sandbox)
68
+ ? settings.sandbox
69
+ : undefined;
70
+ if (sandbox?.enabled === true && sandbox.failIfUnavailable === undefined) {
71
+ return {
72
+ ...settings,
73
+ sandbox: {
74
+ ...sandbox,
75
+ failIfUnavailable: false,
76
+ },
77
+ };
78
+ }
79
+ return settings;
80
+ }
20
81
  export function sessionById(sessionId) {
21
82
  return sessions.get(sessionId) ?? null;
22
83
  }
@@ -28,6 +89,77 @@ export function updateSessionId(session, newSessionId) {
28
89
  session.sessionId = newSessionId;
29
90
  sessions.set(newSessionId, session);
30
91
  }
92
+ function isRequestUserDialogControlRequest(value) {
93
+ if (!value || typeof value !== "object") {
94
+ return false;
95
+ }
96
+ const record = value;
97
+ const request = record.request;
98
+ if (!request || typeof request !== "object") {
99
+ return false;
100
+ }
101
+ const inner = request;
102
+ const payload = inner.payload;
103
+ return (typeof record.request_id === "string" &&
104
+ inner.subtype === "request_user_dialog" &&
105
+ typeof inner.dialog_kind === "string" &&
106
+ Boolean(payload && typeof payload === "object" && !Array.isArray(payload)));
107
+ }
108
+ export function attachRequestUserDialogInterceptor(query, sessionIdForLogs) {
109
+ const internalQuery = query;
110
+ if (internalQuery[requestUserDialogInterceptorInstalled]) {
111
+ return true;
112
+ }
113
+ if (typeof internalQuery.processControlRequest !== "function") {
114
+ bridgeLogger.warn({
115
+ target: LOG_TARGETS.APP_SESSION,
116
+ eventName: "request_user_dialog_interceptor_unavailable",
117
+ message: "request_user_dialog interceptor could not be installed",
118
+ outcome: "failure",
119
+ sessionId: sessionIdForLogs(),
120
+ });
121
+ return false;
122
+ }
123
+ const originalProcessControlRequest = internalQuery.processControlRequest.bind(query);
124
+ internalQuery.processControlRequest = async (request, signal) => {
125
+ // SDK 0.2.104 also added cancel_async_message and seed_read_state control
126
+ // requests. Keep those delegated to the SDK internals: claude-rs does not
127
+ // own the SDK async-message queue or read-state cache, so TUI-level commands
128
+ // for them would add unsupported host behavior without user-visible value.
129
+ if (isRequestUserDialogControlRequest(request)) {
130
+ bridgeLogger.warn({
131
+ target: LOG_TARGETS.APP_SESSION,
132
+ eventName: "request_user_dialog_received",
133
+ message: "request_user_dialog control request received",
134
+ outcome: "failure",
135
+ sessionId: sessionIdForLogs(),
136
+ requestId: request.request_id,
137
+ ...(typeof request.request.tool_use_id === "string"
138
+ ? { toolCallId: request.request.tool_use_id }
139
+ : {}),
140
+ fields: {
141
+ dialog_kind: request.request.dialog_kind,
142
+ raw_payload: request.request.payload,
143
+ raw_request: request.request,
144
+ },
145
+ });
146
+ // TODO(request_user_dialog): Revisit this when a real claude-rs host flow needs it.
147
+ // For now we only log the full control request and reject it explicitly because
148
+ // normal TUI sessions do not appear to exercise these dialog kinds.
149
+ throw new Error(`request_user_dialog is not supported by claude-rs yet (dialog_kind: ${request.request.dialog_kind})`);
150
+ }
151
+ return await originalProcessControlRequest(request, signal);
152
+ };
153
+ internalQuery[requestUserDialogInterceptorInstalled] = true;
154
+ bridgeLogger.info({
155
+ target: LOG_TARGETS.APP_SESSION,
156
+ eventName: "request_user_dialog_interceptor_installed",
157
+ message: "request_user_dialog interceptor installed",
158
+ outcome: "success",
159
+ sessionId: sessionIdForLogs(),
160
+ });
161
+ return true;
162
+ }
31
163
  export async function closeSession(session) {
32
164
  session.input.close();
33
165
  session.query.close();
@@ -79,6 +211,7 @@ export async function createSession(params) {
79
211
  const provisionalSessionId = params.resume ?? randomUUID();
80
212
  const initialModel = initialSessionModel(params.launchSettings);
81
213
  const initialMode = initialSessionMode(params.launchSettings);
214
+ const supportsBypassPermissionsMode = startupPermissionModeOptions(params.launchSettings).allowDangerouslySkipPermissions === true;
82
215
  const historyUpdateCount = params.resumeUpdates?.length ?? 0;
83
216
  const staleSessionCount = params.sessionsToCloseAfterConnect?.length ?? 0;
84
217
  let session;
@@ -93,9 +226,11 @@ export async function createSession(params) {
93
226
  if (toolName === ASK_USER_QUESTION_TOOL_NAME) {
94
227
  return await requestAskUserQuestionAnswers(session, toolUseId, inputData, existing);
95
228
  }
229
+ const display = permissionDisplayFromCanUseOptions(options);
96
230
  const request = {
97
231
  tool_call: existing,
98
232
  options: permissionOptionsFromSuggestions(options.suggestions),
233
+ ...(display ? { display } : {}),
99
234
  };
100
235
  bridgeLogger.info({
101
236
  target: LOG_TARGETS.BRIDGE_PERMISSION,
@@ -107,6 +242,7 @@ export async function createSession(params) {
107
242
  count: request.options.length,
108
243
  fields: {
109
244
  tool_name: toolName,
245
+ agent_id: options.agentID,
110
246
  blocked_path: options.blockedPath ?? "<none>",
111
247
  decision_reason: options.decisionReason ?? "<none>",
112
248
  },
@@ -161,6 +297,7 @@ export async function createSession(params) {
161
297
  sessionIdForLogs,
162
298
  }),
163
299
  });
300
+ attachRequestUserDialogInterceptor(queryHandle, sessionIdForLogs);
164
301
  }
165
302
  catch (error) {
166
303
  const message = error instanceof Error ? error.message : String(error);
@@ -185,8 +322,12 @@ export async function createSession(params) {
185
322
  sessionId: provisionalSessionId,
186
323
  cwd: params.cwd,
187
324
  model: initialModel,
325
+ ...(initialModel ? { requestedModelId: initialModel } : {}),
188
326
  availableModels: [],
189
327
  mode: initialMode,
328
+ supportedModeIds: [],
329
+ runtimeUnavailableModeIds: [],
330
+ supportsBypassPermissionsMode,
190
331
  fastModeState: "off",
191
332
  query: queryHandle,
192
333
  input,
@@ -207,6 +348,9 @@ export async function createSession(params) {
207
348
  ? { sessionsToCloseAfterConnect: params.sessionsToCloseAfterConnect }
208
349
  : {}),
209
350
  };
351
+ refreshCurrentModel(session);
352
+ const { refreshSupportedModesForSession } = await import("./commands.js");
353
+ refreshSupportedModesForSession(session);
210
354
  sessions.set(provisionalSessionId, session);
211
355
  bridgeLogger.info({
212
356
  target: LOG_TARGETS.APP_SESSION,
@@ -239,7 +383,7 @@ export async function createSession(params) {
239
383
  // before the first user prompt.
240
384
  void session.query
241
385
  .initializationResult()
242
- .then((result) => {
386
+ .then(async (result) => {
243
387
  bridgeLogger.info({
244
388
  target: LOG_TARGETS.APP_SESSION,
245
389
  eventName: "session_initialization_completed",
@@ -254,15 +398,26 @@ export async function createSession(params) {
254
398
  },
255
399
  });
256
400
  session.availableModels = mapAvailableModels(result.models);
401
+ const currentModelChanged = refreshCurrentModel(session);
402
+ const { buildModeState, refreshSupportedModesForSession } = await import("./commands.js");
403
+ refreshSupportedModesForSession(session);
257
404
  if (!session.connected) {
258
405
  emitConnectEvent(session);
259
406
  }
407
+ else {
408
+ if (currentModelChanged) {
409
+ emitCurrentModelUpdate(session);
410
+ }
411
+ if (session.mode) {
412
+ emitSessionUpdate(session.sessionId, {
413
+ type: "mode_state_update",
414
+ mode: buildModeState(session, session.mode),
415
+ });
416
+ }
417
+ }
260
418
  // Proactively detect missing auth from account info so the UI can
261
419
  // show the login hint immediately, without waiting for the first prompt.
262
- const acct = result.account;
263
- const hasCredentials = (typeof acct.email === "string" && acct.email.trim().length > 0) ||
264
- (typeof acct.apiKeySource === "string" && acct.apiKeySource.trim().length > 0);
265
- if (!hasCredentials) {
420
+ if (shouldEmitStartupAuthRequiredForAccount(result.account)) {
266
421
  emitAuthRequired(session);
267
422
  }
268
423
  emitFastModeUpdateIfChanged(session, result.fast_mode_state);
@@ -378,6 +533,7 @@ function permissionModeFromSettingsValue(rawMode) {
378
533
  }
379
534
  switch (rawMode) {
380
535
  case "default":
536
+ case "auto":
381
537
  case "acceptEdits":
382
538
  case "bypassPermissions":
383
539
  case "plan":
@@ -436,12 +592,14 @@ export function buildQueryOptions(params) {
436
592
  const systemPrompt = systemPromptFromLaunchSettings(params.launchSettings);
437
593
  const modelOption = startupModelOption(params.launchSettings);
438
594
  const permissionModeOptions = startupPermissionModeOptions(params.launchSettings);
595
+ const settings = normalizedSettingsFromLaunchSettings(params.launchSettings);
439
596
  return {
440
597
  cwd: params.cwd,
441
598
  includePartialMessages: true,
599
+ promptSuggestions: true,
442
600
  executable: "node",
443
601
  ...(params.resume ? {} : { sessionId: params.provisionalSessionId }),
444
- ...(params.launchSettings.settings ? { settings: params.launchSettings.settings } : {}),
602
+ ...(settings ? { settings } : {}),
445
603
  ...modelOption,
446
604
  ...permissionModeOptions,
447
605
  toolConfig: { askUserQuestion: { previewFormat: "markdown" } },
@@ -795,6 +953,230 @@ export function handleElicitationResponse(command) {
795
953
  });
796
954
  pending.resolve({
797
955
  action: command.action,
798
- ...(command.content ? { content: command.content } : {}),
956
+ ...(normalizeSdkElicitationContent(command.content) ? {
957
+ content: normalizeSdkElicitationContent(command.content),
958
+ } : {}),
799
959
  });
800
960
  }
961
+ function normalizeModelKey(id) {
962
+ const original = id.trim();
963
+ if (!original || original === DEFAULT_MODEL_NAME) {
964
+ return { original, family: "default", versionParts: [], variantParts: [] };
965
+ }
966
+ const lower = original.toLowerCase();
967
+ const contextMatch = lower.match(/\[([^\]]+)\]$/);
968
+ const contextSuffix = contextMatch?.[1];
969
+ const withoutContext = contextMatch ? lower.slice(0, contextMatch.index) : lower;
970
+ const withoutPrefix = withoutContext.startsWith("claude-")
971
+ ? withoutContext.slice("claude-".length)
972
+ : withoutContext;
973
+ const parts = withoutPrefix.split("-").filter((part) => part.length > 0);
974
+ const familyPart = parts[0] ?? "";
975
+ const family = familyPart === "opus" || familyPart === "sonnet" || familyPart === "haiku"
976
+ ? familyPart
977
+ : "unknown";
978
+ const versionParts = family === "unknown"
979
+ ? []
980
+ : parts
981
+ .slice(1)
982
+ .filter((part) => /^\d+$/.test(part))
983
+ .map((part) => Number.parseInt(part, 10))
984
+ .filter((part) => Number.isFinite(part));
985
+ const variantParts = family === "unknown"
986
+ ? []
987
+ : parts
988
+ .slice(1)
989
+ .filter((part) => !/^\d+$/.test(part));
990
+ return {
991
+ original,
992
+ family,
993
+ versionParts,
994
+ variantParts,
995
+ ...(contextSuffix ? { contextSuffix } : {}),
996
+ };
997
+ }
998
+ function modelKeysAreCompatible(leftId, rightId) {
999
+ const left = normalizeModelKey(leftId);
1000
+ const right = normalizeModelKey(rightId);
1001
+ if (left.family === "default" || right.family === "default") {
1002
+ return false;
1003
+ }
1004
+ if (left.family === "unknown" || right.family === "unknown") {
1005
+ return left.original.toLowerCase() === right.original.toLowerCase();
1006
+ }
1007
+ if (left.family !== right.family) {
1008
+ return false;
1009
+ }
1010
+ if (left.variantParts.join(".") !== right.variantParts.join(".")) {
1011
+ return false;
1012
+ }
1013
+ if (left.versionParts.length === 0 || right.versionParts.length === 0) {
1014
+ return true;
1015
+ }
1016
+ return left.versionParts.join(".") === right.versionParts.join(".");
1017
+ }
1018
+ function sameContextSuffix(leftId, rightId) {
1019
+ const left = normalizeModelKey(leftId);
1020
+ const right = normalizeModelKey(rightId);
1021
+ return (left.contextSuffix?.toLowerCase() ?? "") === (right.contextSuffix?.toLowerCase() ?? "");
1022
+ }
1023
+ function sameFamilyAndVersion(leftId, rightId) {
1024
+ const left = normalizeModelKey(leftId);
1025
+ const right = normalizeModelKey(rightId);
1026
+ if (left.family === "default" || right.family === "default") {
1027
+ return false;
1028
+ }
1029
+ if (left.family === "unknown" || right.family === "unknown") {
1030
+ return left.original.toLowerCase() === right.original.toLowerCase();
1031
+ }
1032
+ if (left.family !== right.family) {
1033
+ return false;
1034
+ }
1035
+ if (left.versionParts.length === 0 || right.versionParts.length === 0) {
1036
+ return left.versionParts.length === right.versionParts.length;
1037
+ }
1038
+ return left.versionParts.join(".") === right.versionParts.join(".");
1039
+ }
1040
+ function hasVariantSiblingConflict(availableModels, candidateId, resolvedId) {
1041
+ if (sameContextSuffix(candidateId, resolvedId)) {
1042
+ return false;
1043
+ }
1044
+ const resolvedContext = normalizeModelKey(resolvedId).contextSuffix?.toLowerCase() ?? "";
1045
+ if (!resolvedContext) {
1046
+ return false;
1047
+ }
1048
+ return availableModels.some((entry) => {
1049
+ if (entry.id === candidateId) {
1050
+ return false;
1051
+ }
1052
+ if (!sameFamilyAndVersion(entry.id, resolvedId)) {
1053
+ return false;
1054
+ }
1055
+ const entryContext = normalizeModelKey(entry.id).contextSuffix?.toLowerCase() ?? "";
1056
+ return entryContext === resolvedContext;
1057
+ });
1058
+ }
1059
+ function humanizeModelId(id) {
1060
+ const normalized = normalizeModelKey(id);
1061
+ if (normalized.family === "default") {
1062
+ return "Default";
1063
+ }
1064
+ if (normalized.family === "unknown") {
1065
+ return id;
1066
+ }
1067
+ const familyLabel = normalized.family === "opus"
1068
+ ? "Opus"
1069
+ : normalized.family === "sonnet"
1070
+ ? "Sonnet"
1071
+ : "Haiku";
1072
+ const versionLabel = normalized.versionParts.length > 0 ? ` ${normalized.versionParts.join(".")}` : "";
1073
+ const contextLabel = normalized.contextSuffix?.toLowerCase() === "1m"
1074
+ ? " [1M]"
1075
+ : normalized.contextSuffix
1076
+ ? ` [${normalized.contextSuffix}]`
1077
+ : "";
1078
+ return `${familyLabel}${versionLabel}${contextLabel}`;
1079
+ }
1080
+ function shortDisplayNameForModelId(id) {
1081
+ const normalized = normalizeModelKey(id);
1082
+ if (normalized.family === "default") {
1083
+ return "Default";
1084
+ }
1085
+ if (normalized.family === "unknown") {
1086
+ return id;
1087
+ }
1088
+ const familyLabel = normalized.family === "opus"
1089
+ ? "Opus"
1090
+ : normalized.family === "sonnet"
1091
+ ? "Sonnet"
1092
+ : "Haiku";
1093
+ const contextLabel = normalized.contextSuffix?.toLowerCase() === "1m"
1094
+ ? " [1M]"
1095
+ : normalized.contextSuffix
1096
+ ? ` [${normalized.contextSuffix}]`
1097
+ : "";
1098
+ return `${familyLabel}${contextLabel}`;
1099
+ }
1100
+ function currentModelIsAuthoritative(resolvedId, requestedId) {
1101
+ const resolved = resolvedId.trim();
1102
+ if (!resolved || resolved === DEFAULT_MODEL_NAME || resolved === "Connecting...") {
1103
+ return Boolean(requestedId?.trim() && requestedId.trim() !== DEFAULT_MODEL_NAME);
1104
+ }
1105
+ return true;
1106
+ }
1107
+ function resolveCatalogModel(availableModels, resolvedId, requestedId) {
1108
+ const exactResolved = availableModels.find((entry) => entry.id === resolvedId);
1109
+ if (exactResolved) {
1110
+ return exactResolved;
1111
+ }
1112
+ if (requestedId) {
1113
+ const exactRequested = availableModels.find((entry) => entry.id === requestedId);
1114
+ if (exactRequested &&
1115
+ modelKeysAreCompatible(exactRequested.id, resolvedId) &&
1116
+ !hasVariantSiblingConflict(availableModels, exactRequested.id, resolvedId)) {
1117
+ return exactRequested;
1118
+ }
1119
+ }
1120
+ const compatible = availableModels.filter((entry) => modelKeysAreCompatible(entry.id, resolvedId) &&
1121
+ !hasVariantSiblingConflict(availableModels, entry.id, resolvedId));
1122
+ return compatible.length === 1 ? compatible[0] : undefined;
1123
+ }
1124
+ export function resolveCurrentModel(session) {
1125
+ const requestedId = session.requestedModelId?.trim() || undefined;
1126
+ const resolvedId = session.resolvedRuntimeModelId?.trim() ||
1127
+ session.model.trim() ||
1128
+ requestedId ||
1129
+ DEFAULT_MODEL_NAME;
1130
+ const catalogModel = resolveCatalogModel(session.availableModels, resolvedId, requestedId);
1131
+ const runtimeDisplayId = resolvedId || requestedId || DEFAULT_MODEL_NAME;
1132
+ const displayNameShort = shortDisplayNameForModelId(runtimeDisplayId);
1133
+ const displayNameLong = humanizeModelId(runtimeDisplayId);
1134
+ const currentModel = {
1135
+ resolved_id: resolvedId,
1136
+ display_name_short: displayNameShort,
1137
+ display_name_long: displayNameLong,
1138
+ supports_effort: catalogModel?.supports_effort === true,
1139
+ supported_effort_levels: catalogModel?.supported_effort_levels ?? [],
1140
+ is_authoritative: currentModelIsAuthoritative(resolvedId, requestedId),
1141
+ ...(requestedId ? { requested_id: requestedId } : {}),
1142
+ ...(catalogModel ? { catalog_id: catalogModel.id } : {}),
1143
+ ...(catalogModel?.supports_fast_mode !== undefined
1144
+ ? { supports_fast_mode: catalogModel.supports_fast_mode }
1145
+ : {}),
1146
+ ...(catalogModel?.supports_auto_mode !== undefined
1147
+ ? { supports_auto_mode: catalogModel.supports_auto_mode }
1148
+ : {}),
1149
+ ...(catalogModel?.supports_adaptive_thinking !== undefined
1150
+ ? { supports_adaptive_thinking: catalogModel.supports_adaptive_thinking }
1151
+ : {}),
1152
+ };
1153
+ return currentModel;
1154
+ }
1155
+ export function shouldInvalidateResolvedRuntimeModel(previousRequestedId, previousSessionModel, nextRequestedId) {
1156
+ const previousRequested = previousRequestedId?.trim() || previousSessionModel.trim();
1157
+ return previousRequested !== nextRequestedId.trim();
1158
+ }
1159
+ function currentModelsEqual(left, right) {
1160
+ return JSON.stringify(left) === JSON.stringify(right);
1161
+ }
1162
+ export function emitCurrentModelUpdate(session) {
1163
+ if (!session.connected || !session.currentModel) {
1164
+ return false;
1165
+ }
1166
+ emitSessionUpdate(session.sessionId, {
1167
+ type: "current_model_update",
1168
+ current_model: session.currentModel,
1169
+ });
1170
+ return true;
1171
+ }
1172
+ export function refreshCurrentModel(session, emitUpdate = false) {
1173
+ const nextModel = resolveCurrentModel(session);
1174
+ if (currentModelsEqual(session.currentModel, nextModel)) {
1175
+ return false;
1176
+ }
1177
+ session.currentModel = nextModel;
1178
+ if (emitUpdate) {
1179
+ emitCurrentModelUpdate(session);
1180
+ }
1181
+ return true;
1182
+ }
@@ -20,6 +20,25 @@ export function parseRateLimitStatus(value) {
20
20
  }
21
21
  return null;
22
22
  }
23
+ export function parseRuntimeSessionState(value) {
24
+ if (value === "idle" || value === "running" || value === "requires_action") {
25
+ return value;
26
+ }
27
+ return null;
28
+ }
29
+ export function parseApiRetryError(value) {
30
+ switch (value) {
31
+ case "authentication_failed":
32
+ case "billing_error":
33
+ case "rate_limit":
34
+ case "invalid_request":
35
+ case "server_error":
36
+ case "max_output_tokens":
37
+ return value;
38
+ default:
39
+ return "unknown";
40
+ }
41
+ }
23
42
  export function buildRateLimitUpdate(rateLimitInfo) {
24
43
  const info = asRecordOrNull(rateLimitInfo);
25
44
  if (!info) {
@@ -64,3 +83,45 @@ export function buildRateLimitUpdate(rateLimitInfo) {
64
83
  }
65
84
  return update;
66
85
  }
86
+ export function buildApiRetryUpdate(message) {
87
+ const attempt = numberField(message, "attempt");
88
+ const maxRetries = numberField(message, "max_retries", "maxRetries");
89
+ const retryDelayMs = numberField(message, "retry_delay_ms", "retryDelayMs");
90
+ if (attempt === undefined || maxRetries === undefined || retryDelayMs === undefined) {
91
+ return null;
92
+ }
93
+ const rawStatus = message.error_status ?? message.errorStatus;
94
+ const errorStatus = typeof rawStatus === "number" && Number.isFinite(rawStatus) ? rawStatus : null;
95
+ return {
96
+ type: "api_retry_update",
97
+ attempt,
98
+ max_retries: maxRetries,
99
+ retry_delay_ms: retryDelayMs,
100
+ error_status: errorStatus,
101
+ error: parseApiRetryError(message.error),
102
+ };
103
+ }
104
+ export function normalizeSettingsParseError(value) {
105
+ const record = asRecordOrNull(value);
106
+ if (!record) {
107
+ return null;
108
+ }
109
+ const message = typeof record.message === "string" ? record.message.trim() : "";
110
+ if (!message) {
111
+ return null;
112
+ }
113
+ const path = typeof record.path === "string" ? record.path : "";
114
+ const file = typeof record.file === "string" && record.file.trim() ? record.file : undefined;
115
+ return {
116
+ ...(file ? { file } : {}),
117
+ path,
118
+ message,
119
+ };
120
+ }
121
+ export function normalizeSettingsParseErrors(value) {
122
+ const entries = Array.isArray(value) ? value : [value];
123
+ return entries.flatMap((entry) => {
124
+ const normalized = normalizeSettingsParseError(entry);
125
+ return normalized ? [normalized] : [];
126
+ });
127
+ }