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.
- package/README.md +7 -6
- package/agent-sdk/README.md +1 -1
- package/agent-sdk/dist/bridge/commands.js +71 -2
- package/agent-sdk/dist/bridge/events.js +11 -4
- package/agent-sdk/dist/bridge/history.js +9 -4
- package/agent-sdk/dist/bridge/logger.js +8 -0
- package/agent-sdk/dist/bridge/message_handlers.js +235 -23
- package/agent-sdk/dist/bridge/session_lifecycle.js +389 -7
- package/agent-sdk/dist/bridge/state_parsing.js +61 -0
- package/agent-sdk/dist/bridge/tool_calls.js +96 -8
- package/agent-sdk/dist/bridge/tooling.js +60 -27
- package/agent-sdk/dist/bridge.js +203 -21
- package/agent-sdk/dist/bridge.test.js +765 -23
- package/package.json +2 -2
|
@@ -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
|
-
|
|
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
|
-
...(
|
|
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 ? {
|
|
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
|
+
}
|