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.
@@ -51,6 +51,7 @@ function updateOutcome(status) {
51
51
  case "completed":
52
52
  return "success";
53
53
  case "failed":
54
+ case "killed":
54
55
  return "failure";
55
56
  case "in_progress":
56
57
  return "partial";
@@ -60,6 +61,25 @@ function updateOutcome(status) {
60
61
  return "partial";
61
62
  }
62
63
  }
64
+ function parentToolUseIdFromMeta(meta) {
65
+ if (!meta || typeof meta !== "object") {
66
+ return null;
67
+ }
68
+ const claudeCode = "claudeCode" in meta && meta.claudeCode && typeof meta.claudeCode === "object" ? meta.claudeCode : undefined;
69
+ const parentToolUseId = claudeCode && "parentToolUseId" in claudeCode && typeof claudeCode.parentToolUseId === "string"
70
+ ? claudeCode.parentToolUseId
71
+ : null;
72
+ return parentToolUseId;
73
+ }
74
+ function mergeTaskMetadata(current, update) {
75
+ if (update === undefined) {
76
+ return current;
77
+ }
78
+ return {
79
+ ...(current ?? {}),
80
+ ...update,
81
+ };
82
+ }
63
83
  function applyFieldsToBase(base, fields) {
64
84
  if (fields.title !== undefined) {
65
85
  base.title = fields.title;
@@ -82,6 +102,9 @@ function applyFieldsToBase(base, fields) {
82
102
  if (fields.output_metadata !== undefined) {
83
103
  base.output_metadata = fields.output_metadata;
84
104
  }
105
+ if (fields.task_metadata !== undefined) {
106
+ base.task_metadata = mergeTaskMetadata(base.task_metadata, fields.task_metadata);
107
+ }
85
108
  if (fields.meta !== undefined) {
86
109
  base.meta = fields.meta;
87
110
  }
@@ -132,10 +155,11 @@ function logToolCallUpdateEmitted(sessionId, toolUseId, fields, base, updateKind
132
155
  location_count: fields.locations?.length,
133
156
  raw_output_chars: rawOutput?.length,
134
157
  has_output_metadata: fields.output_metadata !== undefined || base?.output_metadata !== undefined,
158
+ has_task_metadata: fields.task_metadata !== undefined || base?.task_metadata !== undefined,
135
159
  failure_kind: failureKind,
136
160
  },
137
161
  };
138
- if (nextStatus === "failed") {
162
+ if (nextStatus === "failed" || nextStatus === "killed") {
139
163
  bridgeLogger.warn(commonEvent);
140
164
  return;
141
165
  }
@@ -165,11 +189,12 @@ export function emitToolCallUpdate(session, toolUseId, fields, updateKind) {
165
189
  applyFieldsToBase(base, fields);
166
190
  }
167
191
  }
168
- export function emitToolCall(session, toolUseId, name, input) {
169
- const toolCall = createToolCall(toolUseId, name, input);
192
+ export function emitToolCall(session, toolUseId, name, input, parentToolUseId = null) {
193
+ const existing = session.toolCalls.get(toolUseId);
194
+ const resolvedParentToolUseId = parentToolUseId ?? parentToolUseIdFromMeta(existing?.meta);
195
+ const toolCall = createToolCall(toolUseId, name, input, resolvedParentToolUseId);
170
196
  const status = "in_progress";
171
197
  toolCall.status = status;
172
- const existing = session.toolCalls.get(toolUseId);
173
198
  if (!existing) {
174
199
  emitInitialToolCall(session, toolCall);
175
200
  return;
@@ -187,12 +212,17 @@ export function emitToolCall(session, toolUseId, name, input) {
187
212
  }
188
213
  emitToolCallUpdate(session, toolUseId, fields, "refresh");
189
214
  }
190
- export function ensureToolCallVisible(session, toolUseId, toolName, input) {
215
+ export function ensureToolCallVisible(session, toolUseId, toolName, input, parentToolUseId = null) {
191
216
  const existing = session.toolCalls.get(toolUseId);
192
217
  if (existing) {
218
+ const existingParentToolUseId = parentToolUseIdFromMeta(existing.meta);
219
+ if (parentToolUseId && existingParentToolUseId !== parentToolUseId) {
220
+ const refreshed = createToolCall(toolUseId, toolName, input, parentToolUseId);
221
+ emitToolCallUpdate(session, toolUseId, { meta: refreshed.meta }, "refresh");
222
+ }
193
223
  return existing;
194
224
  }
195
- const toolCall = createToolCall(toolUseId, toolName, input);
225
+ const toolCall = createToolCall(toolUseId, toolName, input, parentToolUseId);
196
226
  emitInitialToolCall(session, toolCall);
197
227
  return toolCall;
198
228
  }
@@ -238,7 +268,8 @@ export function emitToolProgressUpdate(session, toolUseId, toolName) {
238
268
  }
239
269
  if (existing.status === "in_progress" ||
240
270
  existing.status === "completed" ||
241
- existing.status === "failed") {
271
+ existing.status === "failed" ||
272
+ existing.status === "killed") {
242
273
  return;
243
274
  }
244
275
  emitToolCallUpdate(session, toolUseId, { status: "in_progress" }, "progress");
@@ -249,7 +280,7 @@ export function emitToolSummaryUpdate(session, toolUseId, summary) {
249
280
  return;
250
281
  }
251
282
  const fields = {
252
- status: base.status === "failed" ? "failed" : "completed",
283
+ status: base.status === "failed" || base.status === "killed" ? base.status : "completed",
253
284
  raw_output: summary,
254
285
  content: [{ type: "content", content: { type: "text", text: summary } }],
255
286
  };
@@ -290,3 +321,60 @@ export function taskProgressText(msg) {
290
321
  }
291
322
  return description || lastTool;
292
323
  }
324
+ function taskPatchStatus(value) {
325
+ switch (value) {
326
+ case "pending":
327
+ return "pending";
328
+ case "running":
329
+ return "in_progress";
330
+ case "completed":
331
+ return "completed";
332
+ case "failed":
333
+ return "failed";
334
+ case "killed":
335
+ return "killed";
336
+ default:
337
+ return undefined;
338
+ }
339
+ }
340
+ function buildTaskMetadata(patch) {
341
+ const taskMetadata = {};
342
+ if (typeof patch.error === "string" && patch.error.length > 0) {
343
+ taskMetadata.error = patch.error;
344
+ }
345
+ if (typeof patch.is_backgrounded === "boolean") {
346
+ taskMetadata.is_backgrounded = patch.is_backgrounded;
347
+ }
348
+ if (typeof patch.end_time === "number" && Number.isFinite(patch.end_time) && patch.end_time >= 0) {
349
+ taskMetadata.end_time = Math.trunc(patch.end_time);
350
+ }
351
+ if (typeof patch.total_paused_ms === "number" &&
352
+ Number.isFinite(patch.total_paused_ms) &&
353
+ patch.total_paused_ms >= 0) {
354
+ taskMetadata.total_paused_ms = Math.trunc(patch.total_paused_ms);
355
+ }
356
+ return Object.keys(taskMetadata).length > 0 ? taskMetadata : undefined;
357
+ }
358
+ export function taskUpdatedFields(msg) {
359
+ const patch = msg.patch && typeof msg.patch === "object" ? msg.patch : {};
360
+ const fields = {};
361
+ const status = taskPatchStatus(patch.status);
362
+ const description = typeof patch.description === "string" ? patch.description : "";
363
+ const error = typeof patch.error === "string" ? patch.error : "";
364
+ if (status) {
365
+ fields.status = status;
366
+ }
367
+ if (description) {
368
+ fields.raw_output = description;
369
+ fields.content = [{ type: "content", content: { type: "text", text: description } }];
370
+ }
371
+ else if ((status === "failed" || status === "killed") && error) {
372
+ fields.raw_output = error;
373
+ fields.content = [{ type: "content", content: { type: "text", text: error } }];
374
+ }
375
+ const taskMetadata = buildTaskMetadata(patch);
376
+ if (taskMetadata) {
377
+ fields.task_metadata = taskMetadata;
378
+ }
379
+ return fields;
380
+ }
@@ -110,7 +110,7 @@ function editDiffContent(name, input) {
110
110
  }
111
111
  return [];
112
112
  }
113
- export function createToolCall(toolUseId, name, input) {
113
+ export function createToolCall(toolUseId, name, input, parentToolUseId = null) {
114
114
  return {
115
115
  tool_call_id: toolUseId,
116
116
  title: toolTitle(name, input),
@@ -122,6 +122,7 @@ export function createToolCall(toolUseId, name, input) {
122
122
  meta: {
123
123
  claudeCode: {
124
124
  toolName: name,
125
+ parentToolUseId,
125
126
  },
126
127
  },
127
128
  };
@@ -134,18 +135,33 @@ function resultRecordCandidates(rawResult, rawContent) {
134
135
  candidates.push(record);
135
136
  }
136
137
  };
138
+ const pushRecords = (value) => {
139
+ if (Array.isArray(value)) {
140
+ for (const entry of value) {
141
+ pushRecord(entry);
142
+ }
143
+ return;
144
+ }
145
+ pushRecord(value);
146
+ };
137
147
  const pushNestedRecords = (value) => {
148
+ if (Array.isArray(value)) {
149
+ for (const entry of value) {
150
+ pushNestedRecords(entry);
151
+ }
152
+ return;
153
+ }
138
154
  const record = asRecordOrNull(value);
139
155
  if (!record) {
140
156
  return;
141
157
  }
142
- pushRecord(record.result);
143
- pushRecord(record.data);
144
- pushRecord(record.content);
158
+ pushRecords(record.result);
159
+ pushRecords(record.data);
160
+ pushRecords(record.content);
145
161
  };
146
- pushRecord(rawResult);
162
+ pushRecords(rawResult);
147
163
  pushNestedRecords(rawResult);
148
- pushRecord(rawContent);
164
+ pushRecords(rawContent);
149
165
  pushNestedRecords(rawContent);
150
166
  return candidates;
151
167
  }
@@ -230,15 +246,9 @@ function extractToolOutputMetadata(toolName, rawResult, rawContent) {
230
246
  if (toolName === "Bash") {
231
247
  for (const candidate of candidates) {
232
248
  const hasAssistantAutoBackgrounded = typeof candidate.assistantAutoBackgrounded === "boolean";
233
- const hasTokenSaverOutput = typeof candidate.tokenSaverOutput === "string" && candidate.tokenSaverOutput.length > 0;
234
- if (hasAssistantAutoBackgrounded || hasTokenSaverOutput) {
249
+ if (hasAssistantAutoBackgrounded) {
235
250
  const bashMetadata = {};
236
- if (hasAssistantAutoBackgrounded) {
237
- bashMetadata.assistant_auto_backgrounded = candidate.assistantAutoBackgrounded;
238
- }
239
- if (hasTokenSaverOutput) {
240
- bashMetadata.token_saver_active = true;
241
- }
251
+ bashMetadata.assistant_auto_backgrounded = candidate.assistantAutoBackgrounded;
242
252
  return {
243
253
  bash: bashMetadata,
244
254
  };
@@ -246,14 +256,6 @@ function extractToolOutputMetadata(toolName, rawResult, rawContent) {
246
256
  }
247
257
  return undefined;
248
258
  }
249
- if (toolName === "ExitPlanMode") {
250
- for (const candidate of candidates) {
251
- if (typeof candidate.isUltraplan === "boolean") {
252
- return { exit_plan_mode: { is_ultraplan: candidate.isUltraplan } };
253
- }
254
- }
255
- return undefined;
256
- }
257
259
  if (toolName === "TodoWrite") {
258
260
  for (const candidate of candidates) {
259
261
  if (typeof candidate.verificationNudgeNeeded === "boolean") {
@@ -476,8 +478,7 @@ function findBashResultRecord(rawResult, rawContent) {
476
478
  "stderr" in candidate ||
477
479
  "backgroundTaskId" in candidate ||
478
480
  "backgroundedByUser" in candidate ||
479
- "assistantAutoBackgrounded" in candidate ||
480
- "tokenSaverOutput" in candidate);
481
+ "assistantAutoBackgrounded" in candidate);
481
482
  }
482
483
  function bashBackgroundMessage(record) {
483
484
  const backgroundTaskId = typeof record.backgroundTaskId === "string" ? record.backgroundTaskId : "";
@@ -511,16 +512,48 @@ function buildBashDisplayOutput(record) {
511
512
  }
512
513
  return segments.join("\n");
513
514
  }
515
+ function fileUnchangedResultText(rawResult, rawContent) {
516
+ for (const candidate of resultRecordCandidates(rawResult, rawContent)) {
517
+ if (candidate.type !== "file_unchanged") {
518
+ continue;
519
+ }
520
+ const file = asRecordOrNull(candidate.file);
521
+ const filePath = typeof file?.filePath === "string" ? file.filePath.trim() : "";
522
+ if (filePath) {
523
+ return `File unchanged: ${filePath}`;
524
+ }
525
+ }
526
+ return "";
527
+ }
528
+ function agentTitleFromAgentOutput(rawResult, rawContent) {
529
+ for (const candidate of resultRecordCandidates(rawResult, rawContent)) {
530
+ const agentType = typeof candidate.agentType === "string" ? candidate.agentType.trim() : "";
531
+ if (agentType) {
532
+ return agentType;
533
+ }
534
+ }
535
+ return "";
536
+ }
514
537
  export function buildToolResultFields(isError, rawContent, base, rawResult) {
515
538
  const toolName = resolveToolName(base);
539
+ const fields = {
540
+ status: isError ? "failed" : "completed",
541
+ };
542
+ const fileUnchangedText = !isError && toolName === "Read" ? fileUnchangedResultText(rawResult, rawContent) : "";
543
+ if (fileUnchangedText) {
544
+ fields.raw_output = fileUnchangedText;
545
+ fields.content = [{ type: "content", content: { type: "text", text: fileUnchangedText } }];
546
+ return fields;
547
+ }
548
+ const agentTitle = !isError && toolName === "Agent" ? agentTitleFromAgentOutput(rawResult, rawContent) : "";
549
+ if (agentTitle) {
550
+ fields.title = agentTitle;
551
+ }
516
552
  const bashResultRecord = toolName === "Bash" ? findBashResultRecord(rawResult, rawContent) : undefined;
517
553
  const normalizedRawOutput = normalizeToolResultText(rawContent, isError);
518
554
  const rawOutput = bashResultRecord
519
555
  ? buildBashDisplayOutput(bashResultRecord)
520
556
  : normalizedRawOutput || JSON.stringify(rawContent);
521
- const fields = {
522
- status: isError ? "failed" : "completed",
523
- };
524
557
  if (rawOutput) {
525
558
  fields.raw_output = rawOutput;
526
559
  }
@@ -4,11 +4,12 @@ import { dirname, join } from "node:path";
4
4
  import readline from "node:readline";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { getSessionMessages, listSessions, renameSession, } from "@anthropic-ai/claude-agent-sdk";
7
- import { parseCommandEnvelope, toPermissionMode } from "./bridge/commands.js";
8
- import { writeEvent, failConnection, slashError, emitSessionUpdate, emitSessionsList, currentSessionListOptions, setSessionListingDir, } from "./bridge/events.js";
7
+ import { buildModeState, markModeUnavailableForSession, parseCommandEnvelope, permissionModeFailureLooksUnsupported, refreshSupportedModesForSession, toPermissionMode, } from "./bridge/commands.js";
8
+ import { writeEvent, failConnection, slashError, emitRuntimeReloadCompleted, emitRuntimeReloadFailed, emitSessionUpdate, emitSessionsList, currentSessionListOptions, setSessionListingDir, } from "./bridge/events.js";
9
9
  import { contentFromPrompt } from "./bridge/message_handlers.js";
10
- import { sessions, sessionById, createSession, closeAllSessions, handleElicitationResponse, handlePermissionResponse, handleQuestionResponse, } from "./bridge/session_lifecycle.js";
10
+ import { sessions, sessionById, createSession, closeAllSessions, handleElicitationResponse, handlePermissionResponse, handleQuestionResponse, emitCurrentModelUpdate, refreshCurrentModel, shouldInvalidateResolvedRuntimeModel, } from "./bridge/session_lifecycle.js";
11
11
  import { mapSessionMessagesToUpdates } from "./bridge/history.js";
12
+ import { emitAvailableAgentsIfChanged, mapAvailableAgents } from "./bridge/agents.js";
12
13
  import { MCP_STALE_STATUS_REVALIDATION_COOLDOWN_MS, handleMcpAuthenticateCommand, handleMcpClearAuthCommand, handleMcpOauthCallbackUrlCommand, handleMcpReconnectCommand, handleMcpSetServersCommand, handleMcpStatusCommand, handleMcpToggleCommand, staleMcpAuthCandidates, } from "./bridge/mcp.js";
13
14
  import { bridgeLogger, LOG_TARGETS, logBridgeCommandReceived } from "./bridge/logger.js";
14
15
  // Re-exports: all symbols that tests and external consumers import from bridge.js.
@@ -21,10 +22,10 @@ export { parseCommandEnvelope } from "./bridge/commands.js";
21
22
  export { buildSessionListOptions } from "./bridge/events.js";
22
23
  export { permissionOptionsFromSuggestions, permissionResultFromOutcome, } from "./bridge/permissions.js";
23
24
  export { mapSessionMessagesToUpdates, mapSdkSessions, } from "./bridge/history.js";
24
- export { handleTaskSystemMessage } from "./bridge/message_handlers.js";
25
+ export { handleSdkMessage, handleTaskSystemMessage } from "./bridge/message_handlers.js";
25
26
  export { mapAvailableAgents } from "./bridge/agents.js";
26
- export { buildQueryOptions, mapAvailableModels } from "./bridge/session_lifecycle.js";
27
- export { parseFastModeState, parseRateLimitStatus, buildRateLimitUpdate, } from "./bridge/state_parsing.js";
27
+ export { attachRequestUserDialogInterceptor, buildQueryOptions, mapAvailableModels, } from "./bridge/session_lifecycle.js";
28
+ export { parseFastModeState, parseRateLimitStatus, parseRuntimeSessionState, parseApiRetryError, buildRateLimitUpdate, buildApiRetryUpdate, normalizeSettingsParseError, normalizeSettingsParseErrors, } from "./bridge/state_parsing.js";
28
29
  export { MCP_STALE_STATUS_REVALIDATION_COOLDOWN_MS, staleMcpAuthCandidates };
29
30
  export function buildSessionMutationOptions(cwd) {
30
31
  return cwd ? { dir: cwd } : undefined;
@@ -42,7 +43,7 @@ export async function generatePersistedSessionTitle(query, description) {
42
43
  }
43
44
  return title;
44
45
  }
45
- const EXPECTED_AGENT_SDK_VERSION = "0.2.74";
46
+ const EXPECTED_AGENT_SDK_VERSION = "0.2.104";
46
47
  const require = createRequire(import.meta.url);
47
48
  export function resolveInstalledAgentSdkVersion() {
48
49
  try {
@@ -238,7 +239,7 @@ async function handleCommand(command, requestId) {
238
239
  if (content.length === 0) {
239
240
  return;
240
241
  }
241
- session.input.enqueue({
242
+ const message = {
242
243
  type: "user",
243
244
  session_id: session.sessionId,
244
245
  parent_tool_use_id: null,
@@ -246,7 +247,8 @@ async function handleCommand(command, requestId) {
246
247
  role: "user",
247
248
  content,
248
249
  },
249
- });
250
+ };
251
+ session.input.enqueue(message);
250
252
  return;
251
253
  }
252
254
  case "cancel_turn": {
@@ -264,13 +266,80 @@ async function handleCommand(command, requestId) {
264
266
  slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
265
267
  return;
266
268
  }
267
- await session.query.setModel(command.model);
268
- session.model = command.model;
269
- emitSessionUpdate(session.sessionId, {
270
- type: "config_option_update",
271
- option_id: "model",
272
- value: command.model,
269
+ bridgeLogger.info({
270
+ target: LOG_TARGETS.APP_SESSION,
271
+ eventName: "set_model_started",
272
+ message: "set model started",
273
+ outcome: "start",
274
+ sessionId: session.sessionId,
275
+ requestId,
276
+ fields: {
277
+ requested_model: command.model,
278
+ previous_requested_model: session.requestedModelId,
279
+ previous_session_model: session.model,
280
+ previous_resolved_runtime_model: session.resolvedRuntimeModelId,
281
+ previous_current_model: session.currentModel?.resolved_id,
282
+ },
273
283
  });
284
+ try {
285
+ const previousRequestedModel = session.requestedModelId;
286
+ const previousSessionModel = session.model;
287
+ await session.query.setModel(command.model);
288
+ session.requestedModelId = command.model;
289
+ session.model = command.model;
290
+ const invalidatedResolvedRuntimeModel = shouldInvalidateResolvedRuntimeModel(previousRequestedModel, previousSessionModel, command.model);
291
+ if (invalidatedResolvedRuntimeModel) {
292
+ session.resolvedRuntimeModelId = undefined;
293
+ }
294
+ const changed = refreshCurrentModel(session, true);
295
+ const forcedCurrentModelUpdate = !changed && emitCurrentModelUpdate(session);
296
+ bridgeLogger.info({
297
+ target: LOG_TARGETS.APP_SESSION,
298
+ eventName: "set_model_succeeded",
299
+ message: "set model completed",
300
+ outcome: "success",
301
+ sessionId: session.sessionId,
302
+ requestId,
303
+ fields: {
304
+ requested_model: command.model,
305
+ session_model_after: session.model,
306
+ resolved_runtime_model_after: session.resolvedRuntimeModelId,
307
+ current_model_after: session.currentModel?.resolved_id,
308
+ current_model_display_short: session.currentModel?.display_name_short,
309
+ current_model_display_long: session.currentModel?.display_name_long,
310
+ current_model_update_emitted: changed || forcedCurrentModelUpdate,
311
+ current_model_update_forced: forcedCurrentModelUpdate,
312
+ resolved_runtime_model_invalidated: invalidatedResolvedRuntimeModel,
313
+ },
314
+ });
315
+ refreshSupportedModesForSession(session);
316
+ if (session.mode) {
317
+ emitSessionUpdate(session.sessionId, {
318
+ type: "mode_state_update",
319
+ mode: buildModeState(session, session.mode),
320
+ });
321
+ }
322
+ }
323
+ catch (error) {
324
+ const message = error instanceof Error ? error.message : String(error);
325
+ bridgeLogger.warn({
326
+ target: LOG_TARGETS.APP_SESSION,
327
+ eventName: "set_model_failed",
328
+ message: "set model failed",
329
+ outcome: "failure",
330
+ sessionId: session.sessionId,
331
+ requestId,
332
+ fields: {
333
+ requested_model: command.model,
334
+ error_message: message,
335
+ previous_requested_model: session.requestedModelId,
336
+ previous_session_model: session.model,
337
+ previous_resolved_runtime_model: session.resolvedRuntimeModelId,
338
+ previous_current_model: session.currentModel?.resolved_id,
339
+ },
340
+ });
341
+ slashError(command.session_id, `failed to set model: ${message}`, requestId);
342
+ }
274
343
  return;
275
344
  }
276
345
  case "set_mode": {
@@ -284,12 +353,28 @@ async function handleCommand(command, requestId) {
284
353
  slashError(command.session_id, `unsupported mode: ${command.mode}`, requestId);
285
354
  return;
286
355
  }
287
- await session.query.setPermissionMode(mode);
288
- session.mode = mode;
289
- emitSessionUpdate(session.sessionId, {
290
- type: "current_mode_update",
291
- current_mode_id: mode,
292
- });
356
+ try {
357
+ await session.query.setPermissionMode(mode);
358
+ session.mode = mode;
359
+ refreshSupportedModesForSession(session);
360
+ emitSessionUpdate(session.sessionId, {
361
+ type: "current_mode_update",
362
+ current_mode_id: mode,
363
+ });
364
+ }
365
+ catch (error) {
366
+ const message = error instanceof Error ? error.message : String(error);
367
+ if (permissionModeFailureLooksUnsupported(mode, message)) {
368
+ const changed = markModeUnavailableForSession(session, mode);
369
+ if (changed && session.mode) {
370
+ emitSessionUpdate(session.sessionId, {
371
+ type: "mode_state_update",
372
+ mode: buildModeState(session, session.mode),
373
+ });
374
+ }
375
+ }
376
+ slashError(command.session_id, `failed to set mode to ${mode}: ${message}`, requestId);
377
+ }
293
378
  return;
294
379
  }
295
380
  case "generate_session_title": {
@@ -347,6 +432,7 @@ async function handleCommand(command, requestId) {
347
432
  subscription_type: account.subscriptionType,
348
433
  token_source: account.tokenSource,
349
434
  api_key_source: account.apiKeySource,
435
+ api_provider: account.apiProvider,
350
436
  },
351
437
  });
352
438
  writeEvent({
@@ -358,6 +444,7 @@ async function handleCommand(command, requestId) {
358
444
  subscription_type: account.subscriptionType,
359
445
  token_source: account.tokenSource,
360
446
  api_key_source: account.apiKeySource,
447
+ api_provider: account.apiProvider,
361
448
  },
362
449
  }, requestId);
363
450
  }
@@ -376,6 +463,100 @@ async function handleCommand(command, requestId) {
376
463
  }
377
464
  return;
378
465
  }
466
+ case "get_context_usage": {
467
+ const session = sessionById(command.session_id);
468
+ if (!session) {
469
+ slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
470
+ return;
471
+ }
472
+ try {
473
+ const usage = await session.query.getContextUsage();
474
+ if (typeof usage.model === "string" && usage.model.trim().length > 0) {
475
+ session.resolvedRuntimeModelId = usage.model.trim();
476
+ refreshCurrentModel(session, true);
477
+ }
478
+ const rawPercentage = typeof usage.percentage === "number" ? usage.percentage : undefined;
479
+ const normalizedPercentage = rawPercentage === undefined || !Number.isFinite(rawPercentage)
480
+ ? undefined
481
+ : Math.max(0, Math.min(100, Math.round(rawPercentage)));
482
+ bridgeLogger.debug({
483
+ target: LOG_TARGETS.APP_SESSION,
484
+ eventName: "context_usage_succeeded",
485
+ message: "session context usage received from SDK",
486
+ outcome: "success",
487
+ ...(requestId ? { requestId } : {}),
488
+ sessionId: session.sessionId,
489
+ fields: {
490
+ raw_percentage: rawPercentage,
491
+ normalized_percentage: normalizedPercentage,
492
+ total_tokens: typeof usage.totalTokens === "number" ? usage.totalTokens : undefined,
493
+ max_tokens: typeof usage.maxTokens === "number" ? usage.maxTokens : undefined,
494
+ raw_max_tokens: typeof usage.rawMaxTokens === "number" ? usage.rawMaxTokens : undefined,
495
+ model: typeof usage.model === "string" ? usage.model : undefined,
496
+ },
497
+ });
498
+ writeEvent({
499
+ event: "context_usage",
500
+ session_id: session.sessionId,
501
+ ...(normalizedPercentage !== undefined ? { percentage: normalizedPercentage } : {}),
502
+ }, requestId);
503
+ }
504
+ catch (error) {
505
+ const message = error instanceof Error ? error.message : String(error);
506
+ bridgeLogger.warn({
507
+ target: LOG_TARGETS.APP_SESSION,
508
+ eventName: "context_usage_failed",
509
+ message: "failed to get session context usage",
510
+ outcome: "failure",
511
+ ...(requestId ? { requestId } : {}),
512
+ sessionId: session.sessionId,
513
+ fields: { error_message: message },
514
+ });
515
+ writeEvent({
516
+ event: "context_usage",
517
+ session_id: session.sessionId,
518
+ }, requestId);
519
+ }
520
+ return;
521
+ }
522
+ case "reload_plugins": {
523
+ const session = sessionById(command.session_id);
524
+ if (!session) {
525
+ slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
526
+ return;
527
+ }
528
+ try {
529
+ const result = await session.query.reloadPlugins();
530
+ const commands = Array.isArray(result.commands)
531
+ ? result.commands.map((entry) => ({
532
+ name: entry.name,
533
+ description: entry.description ?? "",
534
+ input_hint: entry.argumentHint ?? undefined,
535
+ }))
536
+ : [];
537
+ emitSessionUpdate(session.sessionId, {
538
+ type: "available_commands_update",
539
+ commands,
540
+ });
541
+ emitAvailableAgentsIfChanged(session, mapAvailableAgents(result.agents));
542
+ await handleMcpStatusCommand(session, requestId);
543
+ emitRuntimeReloadCompleted(session.sessionId, requestId);
544
+ }
545
+ catch (error) {
546
+ const message = error instanceof Error ? error.message : String(error);
547
+ bridgeLogger.warn({
548
+ target: LOG_TARGETS.APP_SESSION,
549
+ eventName: "reload_plugins_failed",
550
+ message: "failed to reload session plugins",
551
+ outcome: "failure",
552
+ ...(requestId ? { requestId } : {}),
553
+ sessionId: session.sessionId,
554
+ fields: { error_message: message },
555
+ });
556
+ emitRuntimeReloadFailed(session.sessionId, message, requestId);
557
+ }
558
+ return;
559
+ }
379
560
  case "mcp_status": {
380
561
  const session = sessionById(command.session_id);
381
562
  if (!session) {
@@ -465,6 +646,7 @@ async function handleCommand(command, requestId) {
465
646
  ...(requestId ? { requestId } : {}),
466
647
  });
467
648
  process.exit(0);
649
+ return;
468
650
  default:
469
651
  bridgeLogger.error({
470
652
  target: LOG_TARGETS.BRIDGE_PROTOCOL,