claude-code-rust 0.12.0 → 0.12.2

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.
@@ -5,15 +5,21 @@ import { query, } from "@anthropic-ai/claude-agent-sdk";
5
5
  import { bridgeLogger, LOG_TARGETS, logSdkStderrLine } from "./logger.js";
6
6
  import { AsyncQueue } from "./shared.js";
7
7
  import { permissionOptionsFromSuggestions, permissionResultFromOutcome, } from "./permissions.js";
8
- import { failConnection, emitSessionUpdate, emitConnectEvent, emitPermissionRequestEvent, emitElicitationRequestEvent, } from "./events.js";
8
+ import { failConnection, emitSessionUpdate, emitConnectEvent, emitPermissionRequestEvent, emitElicitationRequestEvent, emitUserDialogRequestEvent, } from "./events.js";
9
9
  import { ensureToolCallVisible, setToolCallStatus, } from "./tool_calls.js";
10
10
  import { isToolSearchToolName } from "./tooling.js";
11
11
  import { requestExitPlanModeApproval, requestAskUserQuestionAnswers, EXIT_PLAN_MODE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "./user_interaction.js";
12
12
  import { mapAvailableAgents, emitAvailableAgentsIfChanged, refreshAvailableAgents } from "./agents.js";
13
+ import { mapSdkSlashCommands, updateAvailableCommands, } from "./available_commands.js";
13
14
  import { emitAuthRequired, emitFastModeUpdateIfChanged } from "./error_classification.js";
15
+ import { mapAvailableModels, resolveCurrentModel, currentModelsEqual, } from "./model_metadata.js";
16
+ import { shouldEmitStartupAuthRequiredForAccount } from "./account_metadata.js";
17
+ export { mapAvailableModels, resolveCurrentModel } from "./model_metadata.js";
18
+ export { shouldEmitStartupAuthRequiredForAccount } from "./account_metadata.js";
14
19
  const BRIDGE_RUNTIME_PROCESS_NAME = process.platform === "win32" ? "claude-rs-bridge-node.exe" : "claude-rs-bridge-node";
15
20
  const BRIDGE_RUNTIME_GUARD_PROMPT = `Do not terminate the Claude Rust bridge runtime process \`${BRIDGE_RUNTIME_PROCESS_NAME}\`; ` +
16
21
  "when cleaning up development servers, only stop processes by explicit PIDs you started in this session.";
22
+ const STARTUP_FALLBACK_MODEL_ALIAS = "opus";
17
23
  function permissionDisplayFromCanUseOptions(options) {
18
24
  const title = typeof options.title === "string" ? options.title.trim() : "";
19
25
  const displayName = typeof options.displayName === "string" ? options.displayName.trim() : "";
@@ -27,20 +33,8 @@ function permissionDisplayFromCanUseOptions(options) {
27
33
  ...(description ? { description } : {}),
28
34
  };
29
35
  }
30
- const requestUserDialogInterceptorInstalled = Symbol("requestUserDialogInterceptorInstalled");
31
36
  export const sessions = new Map();
32
- function nonEmptyString(value) {
33
- return typeof value === "string" && value.trim().length > 0;
34
- }
35
- export function shouldEmitStartupAuthRequiredForAccount(account) {
36
- const provider = account.apiProvider;
37
- if (nonEmptyString(provider) && provider !== "firstParty") {
38
- return false;
39
- }
40
- return !nonEmptyString(account.email) && !nonEmptyString(account.apiKeySource);
41
- }
42
37
  const DEFAULT_SETTING_SOURCES = ["user", "project", "local"];
43
- const OPUS_MODEL_ALIAS = "opus";
44
38
  const DEFAULT_PERMISSION_MODE = "default";
45
39
  function isSdkElicitationContentValue(value) {
46
40
  return (typeof value === "string" ||
@@ -60,6 +54,48 @@ function normalizeSdkElicitationContent(content) {
60
54
  }
61
55
  return Object.keys(normalized).length > 0 ? normalized : undefined;
62
56
  }
57
+ const REFUSAL_FALLBACK_DIALOG_KIND = "refusal_fallback_prompt";
58
+ function optionalString(value) {
59
+ return typeof value === "string" && value.length > 0 ? value : undefined;
60
+ }
61
+ function optionalStringArray(value) {
62
+ if (!Array.isArray(value)) {
63
+ return undefined;
64
+ }
65
+ const entries = value.filter((entry) => typeof entry === "string");
66
+ return entries.length > 0 ? entries : undefined;
67
+ }
68
+ /**
69
+ * Normalize the camelCase `refusal_fallback_prompt` payload built by the CLI
70
+ * into the snake-case host wire shape. The dialog descriptor requires
71
+ * `originalModel`/`fallbackModel`; the rest are optional metadata.
72
+ */
73
+ function normalizeRefusalFallbackPayload(payload) {
74
+ return {
75
+ original_model: typeof payload.originalModel === "string" ? payload.originalModel : "",
76
+ fallback_model: typeof payload.fallbackModel === "string" ? payload.fallbackModel : "",
77
+ ...(optionalString(payload.apiRefusalCategory) !== undefined
78
+ ? { api_refusal_category: optionalString(payload.apiRefusalCategory) }
79
+ : {}),
80
+ ...(optionalString(payload.guidanceText) !== undefined
81
+ ? { guidance_text: optionalString(payload.guidanceText) }
82
+ : {}),
83
+ ...(optionalStringArray(payload.retractedMessageUuids) !== undefined
84
+ ? { retracted_message_uuids: optionalStringArray(payload.retractedMessageUuids) }
85
+ : {}),
86
+ };
87
+ }
88
+ /**
89
+ * Build the selectable options the host renders, mirroring the labels the CLI
90
+ * itself produces (`function Wg$`). `cancelled` is the Esc/decline default and
91
+ * is not a listed option.
92
+ */
93
+ function buildRefusalFallbackOptions(payload) {
94
+ return [
95
+ { option_id: "retry_fallback", label: `Switch to ${payload.fallback_model}` },
96
+ { option_id: "edit_prompt", label: `Edit prompt and retry with ${payload.original_model}` },
97
+ ];
98
+ }
63
99
  function settingsObjectFromLaunchSettings(launchSettings) {
64
100
  return launchSettings.settings;
65
101
  }
@@ -93,77 +129,6 @@ export function updateSessionId(session, newSessionId) {
93
129
  session.sessionId = newSessionId;
94
130
  sessions.set(newSessionId, session);
95
131
  }
96
- function isRequestUserDialogControlRequest(value) {
97
- if (!value || typeof value !== "object") {
98
- return false;
99
- }
100
- const record = value;
101
- const request = record.request;
102
- if (!request || typeof request !== "object") {
103
- return false;
104
- }
105
- const inner = request;
106
- const payload = inner.payload;
107
- return (typeof record.request_id === "string" &&
108
- inner.subtype === "request_user_dialog" &&
109
- typeof inner.dialog_kind === "string" &&
110
- Boolean(payload && typeof payload === "object" && !Array.isArray(payload)));
111
- }
112
- export function attachRequestUserDialogInterceptor(query, sessionIdForLogs) {
113
- const internalQuery = query;
114
- if (internalQuery[requestUserDialogInterceptorInstalled]) {
115
- return true;
116
- }
117
- if (typeof internalQuery.processControlRequest !== "function") {
118
- bridgeLogger.warn({
119
- target: LOG_TARGETS.APP_SESSION,
120
- eventName: "request_user_dialog_interceptor_unavailable",
121
- message: "request_user_dialog interceptor could not be installed",
122
- outcome: "failure",
123
- sessionId: sessionIdForLogs(),
124
- });
125
- return false;
126
- }
127
- const originalProcessControlRequest = internalQuery.processControlRequest.bind(query);
128
- internalQuery.processControlRequest = async (request, signal) => {
129
- // SDK 0.2.104 also added cancel_async_message and seed_read_state control
130
- // requests. Keep those delegated to the SDK internals: claude-rs does not
131
- // own the SDK async-message queue or read-state cache, so TUI-level commands
132
- // for them would add unsupported host behavior without user-visible value.
133
- if (isRequestUserDialogControlRequest(request)) {
134
- bridgeLogger.warn({
135
- target: LOG_TARGETS.APP_SESSION,
136
- eventName: "request_user_dialog_received",
137
- message: "request_user_dialog control request received",
138
- outcome: "failure",
139
- sessionId: sessionIdForLogs(),
140
- requestId: request.request_id,
141
- ...(typeof request.request.tool_use_id === "string"
142
- ? { toolCallId: request.request.tool_use_id }
143
- : {}),
144
- fields: {
145
- dialog_kind: request.request.dialog_kind,
146
- raw_payload: request.request.payload,
147
- raw_request: request.request,
148
- },
149
- });
150
- // TODO(request_user_dialog): Revisit this when a real claude-rs host flow needs it.
151
- // For now we only log the full control request and reject it explicitly because
152
- // normal TUI sessions do not appear to exercise these dialog kinds.
153
- throw new Error(`request_user_dialog is not supported by claude-rs yet (dialog_kind: ${request.request.dialog_kind})`);
154
- }
155
- return await originalProcessControlRequest(request, signal);
156
- };
157
- internalQuery[requestUserDialogInterceptorInstalled] = true;
158
- bridgeLogger.info({
159
- target: LOG_TARGETS.APP_SESSION,
160
- eventName: "request_user_dialog_interceptor_installed",
161
- message: "request_user_dialog interceptor installed",
162
- outcome: "success",
163
- sessionId: sessionIdForLogs(),
164
- });
165
- return true;
166
- }
167
132
  export async function closeSession(session) {
168
133
  session.input.close();
169
134
  session.query.close();
@@ -176,6 +141,10 @@ export async function closeSession(session) {
176
141
  pending.onOutcome({ outcome: "cancelled" });
177
142
  }
178
143
  session.pendingQuestions.clear();
144
+ for (const pending of session.pendingUserDialogs.values()) {
145
+ pending.resolve("cancelled");
146
+ }
147
+ session.pendingUserDialogs.clear();
179
148
  for (const pending of session.pendingElicitations.values()) {
180
149
  pending.resolve({ action: "cancel" });
181
150
  }
@@ -305,7 +274,6 @@ export async function createSession(params) {
305
274
  sessionIdForLogs,
306
275
  }),
307
276
  });
308
- attachRequestUserDialogInterceptor(queryHandle, sessionIdForLogs);
309
277
  }
310
278
  catch (error) {
311
279
  const message = error instanceof Error ? error.message : String(error);
@@ -343,9 +311,13 @@ export async function createSession(params) {
343
311
  connectEvent: params.connectEvent,
344
312
  connectRequestId: params.requestId,
345
313
  toolCalls: new Map(),
314
+ tasksById: new Map(),
315
+ taskOrder: [],
346
316
  taskToolUseIds: new Map(),
317
+ taskIdsByToolUseId: new Map(),
347
318
  pendingPermissions: new Map(),
348
319
  pendingQuestions: new Map(),
320
+ pendingUserDialogs: new Map(),
349
321
  pendingElicitations: new Map(),
350
322
  mcpStatusRevalidatedAt: new Map(),
351
323
  hiddenToolUseIds: new Set(),
@@ -430,16 +402,7 @@ export async function createSession(params) {
430
402
  emitAuthRequired(session);
431
403
  }
432
404
  emitFastModeUpdateIfChanged(session, result.fast_mode_state);
433
- const commands = Array.isArray(result.commands)
434
- ? result.commands.map((command) => ({
435
- name: command.name,
436
- description: command.description ?? "",
437
- input_hint: command.argumentHint ?? undefined,
438
- }))
439
- : [];
440
- if (commands.length > 0) {
441
- emitSessionUpdate(session.sessionId, { type: "available_commands_update", commands });
442
- }
405
+ updateAvailableCommands(session, "session_result_commands", mapSdkSlashCommands(result.commands));
443
406
  emitAvailableAgentsIfChanged(session, mapAvailableAgents(result.agents));
444
407
  refreshAvailableAgents(session);
445
408
  })
@@ -555,7 +518,7 @@ function permissionModeFromSettingsValue(rawMode) {
555
518
  function initialSessionModel(launchSettings) {
556
519
  const settings = settingsObjectFromLaunchSettings(launchSettings);
557
520
  const model = typeof settings?.model === "string" ? settings.model.trim() : "";
558
- return model || OPUS_MODEL_ALIAS;
521
+ return model || STARTUP_FALLBACK_MODEL_ALIAS;
559
522
  }
560
523
  function startupModelOption(launchSettings) {
561
524
  const settings = settingsObjectFromLaunchSettings(launchSettings);
@@ -721,40 +684,92 @@ export function buildQueryOptions(params) {
721
684
  });
722
685
  });
723
686
  },
687
+ // The SDK "fails closed" and never emits a dialog kind unless it is declared
688
+ // here. We declare the one refusal-related kind we render: when the API
689
+ // returns a hard `stop_reason: "refusal"` and a fallback model is configured,
690
+ // the CLI emits `request_user_dialog` and the host renders the chooser below.
691
+ supportedDialogKinds: [REFUSAL_FALLBACK_DIALOG_KIND],
692
+ // Host policy for `request_user_dialog` control requests. Unknown kinds are
693
+ // logged and answered `{ behavior: "cancelled" }` (the spec-required answer
694
+ // for unrecognized kinds). For `refusal_fallback_prompt`, we surface an
695
+ // interactive chooser in the TUI and route the user's decision back to the
696
+ // CLI as `{ behavior: "completed", result: <choice> }` (or cancelled on
697
+ // decline/abort/teardown).
698
+ onUserDialog: async (request, options) => {
699
+ if (request.dialogKind !== REFUSAL_FALLBACK_DIALOG_KIND) {
700
+ bridgeLogger.warn({
701
+ target: LOG_TARGETS.APP_SESSION,
702
+ eventName: "user_dialog_received",
703
+ message: "request_user_dialog received for unknown kind; cancelled",
704
+ outcome: "cancelled",
705
+ sessionId: params.sessionIdForLogs(),
706
+ ...(typeof request.toolUseID === "string" ? { toolCallId: request.toolUseID } : {}),
707
+ fields: { dialog_kind: request.dialogKind },
708
+ });
709
+ return { behavior: "cancelled" };
710
+ }
711
+ const payload = normalizeRefusalFallbackPayload(request.payload);
712
+ const requestId = randomUUID();
713
+ const dialogRequest = {
714
+ request_id: requestId,
715
+ dialog_kind: REFUSAL_FALLBACK_DIALOG_KIND,
716
+ payload,
717
+ options: buildRefusalFallbackOptions(payload),
718
+ };
719
+ bridgeLogger.info({
720
+ target: LOG_TARGETS.APP_SESSION,
721
+ eventName: "user_dialog_received",
722
+ message: "request_user_dialog received; awaiting host decision",
723
+ outcome: "start",
724
+ sessionId: params.sessionIdForLogs(),
725
+ requestId,
726
+ ...(typeof request.toolUseID === "string" ? { toolCallId: request.toolUseID } : {}),
727
+ fields: {
728
+ dialog_kind: request.dialogKind,
729
+ original_model: payload.original_model,
730
+ fallback_model: payload.fallback_model,
731
+ },
732
+ });
733
+ const choice = await new Promise((resolve) => {
734
+ const currentSession = sessions.get(params.sessionIdForLogs());
735
+ if (!currentSession) {
736
+ bridgeLogger.warn({
737
+ target: LOG_TARGETS.APP_SESSION,
738
+ eventName: "user_dialog_request_dropped",
739
+ message: "user dialog request dropped without an active session",
740
+ outcome: "dropped",
741
+ sessionId: params.sessionIdForLogs(),
742
+ requestId,
743
+ fields: { reason: "unknown_session" },
744
+ });
745
+ resolve("cancelled");
746
+ return;
747
+ }
748
+ let settled = false;
749
+ const settle = (value) => {
750
+ if (settled) {
751
+ return;
752
+ }
753
+ settled = true;
754
+ currentSession.pendingUserDialogs.delete(requestId);
755
+ options.signal.removeEventListener("abort", onAbort);
756
+ resolve(value);
757
+ };
758
+ const onAbort = () => settle("cancelled");
759
+ if (options.signal.aborted) {
760
+ settle("cancelled");
761
+ return;
762
+ }
763
+ options.signal.addEventListener("abort", onAbort);
764
+ currentSession.pendingUserDialogs.set(requestId, { resolve: settle });
765
+ emitUserDialogRequestEvent(params.sessionIdForLogs(), dialogRequest);
766
+ });
767
+ return choice === "cancelled"
768
+ ? { behavior: "cancelled" }
769
+ : { behavior: "completed", result: choice };
770
+ },
724
771
  };
725
772
  }
726
- export function mapAvailableModels(models) {
727
- if (!Array.isArray(models)) {
728
- return [];
729
- }
730
- return models
731
- .filter((entry) => {
732
- return (typeof entry?.value === "string" &&
733
- entry.value.trim().length > 0 &&
734
- typeof entry.displayName === "string" &&
735
- entry.displayName.trim().length > 0);
736
- })
737
- .map((entry) => ({
738
- id: entry.value,
739
- display_name: entry.displayName,
740
- supports_effort: entry.supportsEffort === true,
741
- supported_effort_levels: Array.isArray(entry.supportedEffortLevels)
742
- ? entry.supportedEffortLevels.filter((level) => level === "low" || level === "medium" || level === "high")
743
- : [],
744
- ...(typeof entry.supportsAdaptiveThinking === "boolean"
745
- ? { supports_adaptive_thinking: entry.supportsAdaptiveThinking }
746
- : {}),
747
- ...(typeof entry.supportsFastMode === "boolean"
748
- ? { supports_fast_mode: entry.supportsFastMode }
749
- : {}),
750
- ...(typeof entry.supportsAutoMode === "boolean"
751
- ? { supports_auto_mode: entry.supportsAutoMode }
752
- : {}),
753
- ...(typeof entry.description === "string" && entry.description.trim().length > 0
754
- ? { description: entry.description }
755
- : {}),
756
- }));
757
- }
758
773
  export function handlePermissionResponse(command) {
759
774
  bridgeLogger.info({
760
775
  target: LOG_TARGETS.BRIDGE_PERMISSION,
@@ -908,6 +923,60 @@ export function handleQuestionResponse(command) {
908
923
  });
909
924
  resolver.onOutcome(command.outcome);
910
925
  }
926
+ export function handleUserDialogResponse(command) {
927
+ bridgeLogger.info({
928
+ target: LOG_TARGETS.BRIDGE_PERMISSION,
929
+ eventName: "user_dialog_response_received",
930
+ message: "user dialog response received",
931
+ outcome: "success",
932
+ sessionId: command.session_id,
933
+ requestId: command.request_id,
934
+ fields: {
935
+ response_kind: command.outcome.outcome,
936
+ selected_option: command.outcome.outcome === "selected" ? command.outcome.option_id : "cancelled",
937
+ },
938
+ });
939
+ const session = sessionById(command.session_id);
940
+ if (!session) {
941
+ bridgeLogger.warn({
942
+ target: LOG_TARGETS.BRIDGE_PERMISSION,
943
+ eventName: "user_dialog_response_dropped",
944
+ message: "user dialog response dropped for unknown session",
945
+ outcome: "dropped",
946
+ sessionId: command.session_id,
947
+ requestId: command.request_id,
948
+ fields: { reason: "unknown_session" },
949
+ });
950
+ return;
951
+ }
952
+ const pending = session.pendingUserDialogs.get(command.request_id);
953
+ if (!pending) {
954
+ // Idempotent: a late or duplicate response for an already-resolved request
955
+ // (e.g. the dialog was cancelled on abort/teardown first) is a no-op.
956
+ bridgeLogger.warn({
957
+ target: LOG_TARGETS.BRIDGE_PERMISSION,
958
+ eventName: "user_dialog_response_dropped",
959
+ message: "user dialog response dropped without a pending request",
960
+ outcome: "dropped",
961
+ sessionId: command.session_id,
962
+ requestId: command.request_id,
963
+ fields: { reason: "missing_pending_request" },
964
+ });
965
+ return;
966
+ }
967
+ session.pendingUserDialogs.delete(command.request_id);
968
+ const choice = command.outcome.outcome === "selected" ? command.outcome.option_id : "cancelled";
969
+ bridgeLogger.info({
970
+ target: LOG_TARGETS.BRIDGE_PERMISSION,
971
+ eventName: "user_dialog_response_applied",
972
+ message: "user dialog response applied",
973
+ outcome: "success",
974
+ sessionId: command.session_id,
975
+ requestId: command.request_id,
976
+ fields: { choice },
977
+ });
978
+ pending.resolve(choice);
979
+ }
911
980
  export function handleElicitationResponse(command) {
912
981
  bridgeLogger.info({
913
982
  target: LOG_TARGETS.BRIDGE_PERMISSION,
@@ -968,208 +1037,10 @@ export function handleElicitationResponse(command) {
968
1037
  } : {}),
969
1038
  });
970
1039
  }
971
- const MAX_MODEL_VERSION_PARTS = 2;
972
- const RELEASE_BUILD_TOKEN = /^20\d{6}$/;
973
- function normalizeModelKey(id) {
974
- const original = id.trim();
975
- if (!original) {
976
- return { original, family: "unknown", versionParts: [], variantParts: [], buildParts: [] };
977
- }
978
- const lower = original.toLowerCase();
979
- const contextMatch = lower.match(/\[([^\]]+)\]$/);
980
- const contextSuffix = contextMatch?.[1];
981
- const withoutContext = contextMatch ? lower.slice(0, contextMatch.index) : lower;
982
- const withoutPrefix = withoutContext.startsWith("claude-")
983
- ? withoutContext.slice("claude-".length)
984
- : withoutContext;
985
- const parts = withoutPrefix.split("-").filter((part) => part.length > 0);
986
- const familyPart = parts[0] ?? "";
987
- const family = familyPart === "opus" || familyPart === "sonnet" || familyPart === "haiku"
988
- ? familyPart
989
- : "unknown";
990
- const versionParts = [];
991
- const variantParts = [];
992
- const buildParts = [];
993
- if (family !== "unknown") {
994
- for (const part of parts.slice(1)) {
995
- if (/^\d+$/.test(part)) {
996
- if (versionParts.length < MAX_MODEL_VERSION_PARTS) {
997
- const parsed = Number.parseInt(part, 10);
998
- if (Number.isFinite(parsed)) {
999
- versionParts.push(parsed);
1000
- }
1001
- continue;
1002
- }
1003
- if (RELEASE_BUILD_TOKEN.test(part)) {
1004
- buildParts.push(part);
1005
- continue;
1006
- }
1007
- }
1008
- variantParts.push(part);
1009
- }
1010
- }
1011
- return {
1012
- original,
1013
- family,
1014
- versionParts,
1015
- variantParts,
1016
- buildParts,
1017
- ...(contextSuffix ? { contextSuffix } : {}),
1018
- };
1019
- }
1020
- function modelKeysAreCompatible(leftId, rightId) {
1021
- const left = normalizeModelKey(leftId);
1022
- const right = normalizeModelKey(rightId);
1023
- if (left.family === "unknown" || right.family === "unknown") {
1024
- return left.original.toLowerCase() === right.original.toLowerCase();
1025
- }
1026
- if (left.family !== right.family) {
1027
- return false;
1028
- }
1029
- if (left.variantParts.join(".") !== right.variantParts.join(".")) {
1030
- return false;
1031
- }
1032
- if (left.versionParts.length === 0 || right.versionParts.length === 0) {
1033
- return true;
1034
- }
1035
- return left.versionParts.join(".") === right.versionParts.join(".");
1036
- }
1037
- function sameContextSuffix(leftId, rightId) {
1038
- const left = normalizeModelKey(leftId);
1039
- const right = normalizeModelKey(rightId);
1040
- return (left.contextSuffix?.toLowerCase() ?? "") === (right.contextSuffix?.toLowerCase() ?? "");
1041
- }
1042
- function sameFamilyAndVersion(leftId, rightId) {
1043
- const left = normalizeModelKey(leftId);
1044
- const right = normalizeModelKey(rightId);
1045
- if (left.family === "unknown" || right.family === "unknown") {
1046
- return left.original.toLowerCase() === right.original.toLowerCase();
1047
- }
1048
- if (left.family !== right.family) {
1049
- return false;
1050
- }
1051
- if (left.versionParts.length === 0 || right.versionParts.length === 0) {
1052
- return left.versionParts.length === right.versionParts.length;
1053
- }
1054
- return left.versionParts.join(".") === right.versionParts.join(".");
1055
- }
1056
- function hasVariantSiblingConflict(availableModels, candidateId, resolvedId) {
1057
- if (sameContextSuffix(candidateId, resolvedId)) {
1058
- return false;
1059
- }
1060
- const resolvedContext = normalizeModelKey(resolvedId).contextSuffix?.toLowerCase() ?? "";
1061
- if (!resolvedContext) {
1062
- return false;
1063
- }
1064
- return availableModels.some((entry) => {
1065
- if (entry.id === candidateId) {
1066
- return false;
1067
- }
1068
- if (!sameFamilyAndVersion(entry.id, resolvedId)) {
1069
- return false;
1070
- }
1071
- const entryContext = normalizeModelKey(entry.id).contextSuffix?.toLowerCase() ?? "";
1072
- return entryContext === resolvedContext;
1073
- });
1074
- }
1075
- function humanizeModelId(id) {
1076
- const normalized = normalizeModelKey(id);
1077
- if (normalized.family === "unknown") {
1078
- return id;
1079
- }
1080
- const familyLabel = normalized.family === "opus"
1081
- ? "Opus"
1082
- : normalized.family === "sonnet"
1083
- ? "Sonnet"
1084
- : "Haiku";
1085
- const versionLabel = normalized.versionParts.length > 0 ? ` ${normalized.versionParts.join(".")}` : "";
1086
- const contextLabel = normalized.contextSuffix?.toLowerCase() === "1m"
1087
- ? " [1M]"
1088
- : normalized.contextSuffix
1089
- ? ` [${normalized.contextSuffix}]`
1090
- : "";
1091
- return `${familyLabel}${versionLabel}${contextLabel}`;
1092
- }
1093
- function shortDisplayNameForModelId(id) {
1094
- const normalized = normalizeModelKey(id);
1095
- if (normalized.family === "unknown") {
1096
- return id;
1097
- }
1098
- const familyLabel = normalized.family === "opus"
1099
- ? "Opus"
1100
- : normalized.family === "sonnet"
1101
- ? "Sonnet"
1102
- : "Haiku";
1103
- const versionLabel = normalized.versionParts.length > 0 ? ` ${normalized.versionParts.join(".")}` : "";
1104
- const contextLabel = normalized.contextSuffix?.toLowerCase() === "1m"
1105
- ? " [1M]"
1106
- : normalized.contextSuffix
1107
- ? ` [${normalized.contextSuffix}]`
1108
- : "";
1109
- return `${familyLabel}${versionLabel}${contextLabel}`;
1110
- }
1111
- function currentModelIsAuthoritative(resolvedId, requestedId) {
1112
- const resolved = resolvedId.trim();
1113
- if (!resolved || resolved === "Connecting...") {
1114
- return Boolean(requestedId?.trim());
1115
- }
1116
- return true;
1117
- }
1118
- function resolveCatalogModel(availableModels, resolvedId, requestedId) {
1119
- const exactResolved = availableModels.find((entry) => entry.id === resolvedId);
1120
- if (exactResolved) {
1121
- return exactResolved;
1122
- }
1123
- if (requestedId) {
1124
- const exactRequested = availableModels.find((entry) => entry.id === requestedId);
1125
- if (exactRequested &&
1126
- modelKeysAreCompatible(exactRequested.id, resolvedId) &&
1127
- !hasVariantSiblingConflict(availableModels, exactRequested.id, resolvedId)) {
1128
- return exactRequested;
1129
- }
1130
- }
1131
- const compatible = availableModels.filter((entry) => modelKeysAreCompatible(entry.id, resolvedId) &&
1132
- !hasVariantSiblingConflict(availableModels, entry.id, resolvedId));
1133
- return compatible.length === 1 ? compatible[0] : undefined;
1134
- }
1135
- export function resolveCurrentModel(session) {
1136
- const requestedId = session.requestedModelId?.trim() || undefined;
1137
- const resolvedId = session.resolvedRuntimeModelId?.trim() ||
1138
- session.model.trim() ||
1139
- requestedId ||
1140
- OPUS_MODEL_ALIAS;
1141
- const catalogModel = resolveCatalogModel(session.availableModels, resolvedId, requestedId);
1142
- const runtimeDisplayId = resolvedId || requestedId || OPUS_MODEL_ALIAS;
1143
- const displayNameShort = shortDisplayNameForModelId(runtimeDisplayId);
1144
- const displayNameLong = humanizeModelId(runtimeDisplayId);
1145
- const currentModel = {
1146
- resolved_id: resolvedId,
1147
- display_name_short: displayNameShort,
1148
- display_name_long: displayNameLong,
1149
- supports_effort: catalogModel?.supports_effort === true,
1150
- supported_effort_levels: catalogModel?.supported_effort_levels ?? [],
1151
- is_authoritative: currentModelIsAuthoritative(resolvedId, requestedId),
1152
- ...(requestedId ? { requested_id: requestedId } : {}),
1153
- ...(catalogModel ? { catalog_id: catalogModel.id } : {}),
1154
- ...(catalogModel?.supports_fast_mode !== undefined
1155
- ? { supports_fast_mode: catalogModel.supports_fast_mode }
1156
- : {}),
1157
- ...(catalogModel?.supports_auto_mode !== undefined
1158
- ? { supports_auto_mode: catalogModel.supports_auto_mode }
1159
- : {}),
1160
- ...(catalogModel?.supports_adaptive_thinking !== undefined
1161
- ? { supports_adaptive_thinking: catalogModel.supports_adaptive_thinking }
1162
- : {}),
1163
- };
1164
- return currentModel;
1165
- }
1166
1040
  export function shouldInvalidateResolvedRuntimeModel(previousRequestedId, previousSessionModel, nextRequestedId) {
1167
1041
  const previousRequested = previousRequestedId?.trim() || previousSessionModel.trim();
1168
1042
  return previousRequested !== nextRequestedId.trim();
1169
1043
  }
1170
- function currentModelsEqual(left, right) {
1171
- return JSON.stringify(left) === JSON.stringify(right);
1172
- }
1173
1044
  export function emitCurrentModelUpdate(session) {
1174
1045
  if (!session.connected || !session.currentModel) {
1175
1046
  return false;
@@ -36,9 +36,12 @@ export function parseRuntimeSessionState(value) {
36
36
  export function parseApiRetryError(value) {
37
37
  switch (value) {
38
38
  case "authentication_failed":
39
+ case "oauth_org_not_allowed":
39
40
  case "billing_error":
40
41
  case "rate_limit":
42
+ case "overloaded":
41
43
  case "invalid_request":
44
+ case "model_not_found":
42
45
  case "server_error":
43
46
  case "max_output_tokens":
44
47
  return value;
@@ -81,7 +84,10 @@ export function buildRateLimitUpdate(rateLimitInfo) {
81
84
  if (typeof info.overageDisabledReason === "string" && info.overageDisabledReason.length > 0) {
82
85
  update.overage_disabled_reason = info.overageDisabledReason;
83
86
  }
84
- if (typeof info.isUsingOverage === "boolean") {
87
+ if (typeof info.overageInUse === "boolean") {
88
+ update.is_using_overage = info.overageInUse;
89
+ }
90
+ else if (typeof info.isUsingOverage === "boolean") {
85
91
  update.is_using_overage = info.isUsingOverage;
86
92
  }
87
93
  const surpassedThreshold = numberField(info, "surpassedThreshold");
@@ -0,0 +1,34 @@
1
+ export function linkTaskToolUse(session, taskId, toolUseId) {
2
+ if (!taskId || !toolUseId) {
3
+ return;
4
+ }
5
+ const previousToolUseId = session.taskToolUseIds.get(taskId);
6
+ if (previousToolUseId && previousToolUseId !== toolUseId) {
7
+ const reverseTaskId = session.taskIdsByToolUseId.get(previousToolUseId);
8
+ if (reverseTaskId === taskId) {
9
+ session.taskIdsByToolUseId.delete(previousToolUseId);
10
+ }
11
+ }
12
+ const previousTaskId = session.taskIdsByToolUseId.get(toolUseId);
13
+ if (previousTaskId && previousTaskId !== taskId) {
14
+ const forwardToolUseId = session.taskToolUseIds.get(previousTaskId);
15
+ if (forwardToolUseId === toolUseId) {
16
+ session.taskToolUseIds.delete(previousTaskId);
17
+ }
18
+ }
19
+ session.taskToolUseIds.set(taskId, toolUseId);
20
+ session.taskIdsByToolUseId.set(toolUseId, taskId);
21
+ }
22
+ export function unlinkTaskToolUse(session, taskId) {
23
+ if (!taskId) {
24
+ return;
25
+ }
26
+ const toolUseId = session.taskToolUseIds.get(taskId);
27
+ session.taskToolUseIds.delete(taskId);
28
+ if (toolUseId && session.taskIdsByToolUseId.get(toolUseId) === taskId) {
29
+ session.taskIdsByToolUseId.delete(toolUseId);
30
+ }
31
+ }
32
+ export function activeTaskIdForToolUse(session, toolUseId) {
33
+ return toolUseId ? session.taskIdsByToolUseId.get(toolUseId) : undefined;
34
+ }