claude-code-rust 0.13.4 → 0.14.1

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 CHANGED
@@ -16,28 +16,33 @@ A native Rust terminal interface for Claude Code. Drop-in replacement for Anthro
16
16
 
17
17
  Claude Code Rust replaces the stock Claude Code terminal interface with a native Rust binary built on [Ratatui](https://ratatui.rs/). It connects to the same Claude API through a local Agent SDK bridge. Core Claude Code functionality - tool calls, file editing, terminal commands, and permissions - works unchanged.
18
18
 
19
- ## Requisites
19
+ ## Prerequisite
20
20
 
21
- - Existing Claude Code authentication (`~/.claude/config.json`)
21
+ - The Claude Code CLI must be installed as fallback for some SDK-unsupported features.
22
22
 
23
23
  ## Install
24
24
 
25
- ### npm (global, recommended)
25
+ ### Install script (recommended, v0.14.0+)
26
+
27
+ **macOS/Linux:**
26
28
 
27
29
  ```bash
28
- npm install -g claude-code-rust
30
+ curl -fsSL https://raw.githubusercontent.com/srothgan/claude-code-rust/main/scripts/install/install.sh | sh
29
31
  ```
30
32
 
31
- The npm package installs a small launcher plus a platform-specific optional dependency containing the prebuilt Rust binary for your OS and architecture.
33
+ **Windows PowerShell:**
34
+
35
+ ```powershell
36
+ powershell -NoProfile -ExecutionPolicy Bypass -Command "irm 'https://raw.githubusercontent.com/srothgan/claude-code-rust/main/scripts/install/install.ps1' | iex"
37
+ ```
32
38
 
33
- If `claude-rs` reports a missing platform package, check whether optional dependencies were omitted:
39
+ ### npm (global)
34
40
 
35
41
  ```bash
36
- npm config get omit
37
42
  npm install -g claude-code-rust
38
43
  ```
39
44
 
40
- If `claude-rs` resolves to an older global shim, ensure your npm global bin directory comes first on `PATH` or remove the stale shim before retrying.
45
+ See the [installation guide](https://srothgan.github.io/claude-code-rust/installation.html) for release pinning, custom install locations, switching install methods, uninstall, and troubleshooting.
41
46
 
42
47
  ## Usage
43
48
 
@@ -66,13 +71,15 @@ Claude Code Rust addresses these with a native terminal UI that uses diffed, dir
66
71
 
67
72
  ## Documentation
68
73
 
69
- The manual covers installation from npm and source, help, slash commands, keyboard shortcuts, settings, diagnostics, architecture, and the changelog:
74
+ The manual covers installation with scripts and npm, plus help, slash commands, keyboard shortcuts, settings, diagnostics, troubleshooting, building from source, architecture, and the changelog:
70
75
 
71
76
  - [Installation](https://srothgan.github.io/claude-code-rust/installation.html)
72
77
  - [Usage](https://srothgan.github.io/claude-code-rust/usage.html)
73
78
  - [Help](https://srothgan.github.io/claude-code-rust/help.html)
74
79
  - [Slash commands](https://srothgan.github.io/claude-code-rust/commands.html)
75
80
  - [Settings](https://srothgan.github.io/claude-code-rust/settings.html)
81
+ - [Troubleshooting](https://srothgan.github.io/claude-code-rust/troubleshooting.html)
82
+ - [Development](https://srothgan.github.io/claude-code-rust/development.html)
76
83
 
77
84
  ## Status
78
85
 
@@ -4,6 +4,7 @@ const KNOWN_API_PROVIDERS = new Set([
4
4
  "vertex",
5
5
  "foundry",
6
6
  "anthropicAws",
7
+ "anthropicGoogleCloud",
7
8
  "mantle",
8
9
  "gateway",
9
10
  ]);
@@ -1,8 +1,26 @@
1
+ import { bridgeLogger, LOG_TARGETS } from "./logger.js";
1
2
  export async function dispatchCancelTurnCommand(command, deps) {
2
3
  const session = deps.sessionById(command.session_id);
3
4
  if (!session) {
4
5
  deps.slashError(command.session_id, `unknown session: ${command.session_id}`, deps.requestId);
5
6
  return;
6
7
  }
7
- await session.query.interrupt();
8
+ const receipt = await session.query.interrupt();
9
+ const stillQueued = Array.isArray(receipt?.still_queued)
10
+ ? receipt.still_queued.filter((entry) => typeof entry === "string")
11
+ : [];
12
+ if (stillQueued.length > 0) {
13
+ bridgeLogger.info({
14
+ target: LOG_TARGETS.APP_SESSION,
15
+ eventName: "interrupt_receipt_still_queued",
16
+ message: "interrupt receipt reported queued async messages",
17
+ outcome: "success",
18
+ sessionId: command.session_id,
19
+ ...(deps.requestId ? { requestId: deps.requestId } : {}),
20
+ fields: {
21
+ still_queued_count: stillQueued.length,
22
+ still_queued: stillQueued,
23
+ },
24
+ });
25
+ }
8
26
  }
@@ -307,6 +307,7 @@ export function parseCommandEnvelope(line) {
307
307
  session_id: expectString(raw, "session_id", "reload_plugins"),
308
308
  };
309
309
  case "mcp_status":
310
+ // Rust historically sends `get_mcp_snapshot`; normalize it to the bridge-internal command.
310
311
  case "get_mcp_snapshot":
311
312
  return {
312
313
  command: "mcp_status",
@@ -450,6 +451,9 @@ function parseQuestionAnnotation(value) {
450
451
  };
451
452
  }
452
453
  export function toPermissionMode(mode) {
454
+ if (mode === "manual") {
455
+ return "default";
456
+ }
453
457
  if (mode === "default" ||
454
458
  mode === "auto" ||
455
459
  mode === "acceptEdits" ||
@@ -3,12 +3,12 @@ import { toPermissionMode, buildModeState, refreshSupportedModesForSession } fro
3
3
  import { writeEvent, emitSessionUpdate, emitConnectEvent, emitSessionReplacedEvent, } from "./events.js";
4
4
  import { TOOL_RESULT_TYPES, isToolSearchToolName, isToolSearchToolResultType, unwrapToolUseResult, } from "./tooling.js";
5
5
  import { emitToolCall, emitToolCallUpdate, emitToolResultUpdate, finalizeOpenToolCalls, emitToolProgressUpdate, emitToolSummaryUpdate, ensureToolCallVisible, resolveTaskToolUseId, defersTaskNotificationCompletion, toolAcceptsTaskLifecycle, taskProgressText, taskUpdatedFields, } from "./tool_calls.js";
6
- import { applyTaskLifecycleState } from "./tasks.js";
6
+ import { applyBackgroundTasksChanged, applyTaskLifecycleState } from "./tasks.js";
7
7
  import { linkTaskToolUse, unlinkTaskToolUse } from "./task_links.js";
8
8
  import { emitAuthRequired, classifyTurnErrorKind, emitFastModeUpdateIfChanged } from "./error_classification.js";
9
9
  import { mapAvailableAgentsFromNames, emitAvailableAgentsIfChanged, refreshAvailableAgents } from "./agents.js";
10
10
  import { mapInitSlashCommands, mapSdkSlashCommands, updateAvailableCommands, } from "./available_commands.js";
11
- import { buildApiRetryUpdate, buildRateLimitUpdate, normalizeSettingsParseErrors, numberField, parseApiRetryError, parseRuntimeSessionState, } from "./state_parsing.js";
11
+ import { buildApiRetryUpdate, buildRateLimitUpdate, buildSubagentRetryUpdate, normalizeSettingsParseErrors, numberField, parseApiRetryError, parseRuntimeSessionState, } from "./state_parsing.js";
12
12
  import { looksLikeAuthRequired } from "./auth.js";
13
13
  import { emitCurrentModelUpdate, refreshCurrentModel, updateSessionId } from "./session_lifecycle.js";
14
14
  import { bridgeLogger, LOG_TARGETS } from "./logger.js";
@@ -39,6 +39,7 @@ function sdkCorrelationMetadata(msg) {
39
39
  requestId: typeof msg.request_id === "string" ? msg.request_id : undefined,
40
40
  subagentType: typeof msg.subagent_type === "string" ? msg.subagent_type : undefined,
41
41
  taskDescription: typeof msg.task_description === "string" ? msg.task_description : undefined,
42
+ parentAgentId: typeof msg.parent_agent_id === "string" ? msg.parent_agent_id : undefined,
42
43
  };
43
44
  }
44
45
  function sdkTaskMetadata(msg) {
@@ -53,12 +54,14 @@ function sdkTaskMetadata(msg) {
53
54
  ...(metadata.requestId ? { request_id: metadata.requestId } : {}),
54
55
  ...(metadata.subagentType ? { subagent_type: metadata.subagentType } : {}),
55
56
  ...(metadata.taskDescription ? { task_description: metadata.taskDescription } : {}),
57
+ ...(metadata.parentAgentId ? { parent_agent_id: metadata.parentAgentId } : {}),
56
58
  ...(taskType ? { task_type: taskType } : {}),
57
59
  ...(workflowName ? { workflow_name: workflowName } : {}),
58
60
  ...(prompt ? { prompt } : {}),
59
61
  ...(outputFile ? { output_file: outputFile } : {}),
60
62
  ...(summary ? { summary } : {}),
61
63
  ...(status ? { terminal_status: status } : {}),
64
+ ...(typeof msg.blocked === "boolean" ? { blocked: msg.blocked } : {}),
62
65
  };
63
66
  return Object.keys(taskMetadata).length > 0 ? taskMetadata : undefined;
64
67
  }
@@ -822,6 +825,13 @@ function terminalReasonFromValue(value) {
822
825
  case "hook_stopped":
823
826
  case "tool_deferred":
824
827
  case "max_turns":
828
+ case "background_requested":
829
+ case "api_error":
830
+ case "malformed_tool_use_exhausted":
831
+ case "budget_exhausted":
832
+ case "structured_output_retry_exhausted":
833
+ case "tool_deferred_unavailable":
834
+ case "turn_setup_failed":
825
835
  case "completed":
826
836
  return value;
827
837
  default:
@@ -848,6 +858,10 @@ export function handleSdkMessage(session, message) {
848
858
  updateAvailableCommands(session, "commands_changed", mapSdkSlashCommands(msg.commands));
849
859
  return;
850
860
  }
861
+ if (subtype === "background_tasks_changed") {
862
+ applyBackgroundTasksChanged(session, msg);
863
+ return;
864
+ }
851
865
  if (subtype === "notification") {
852
866
  const text = typeof msg.text === "string" ? msg.text : "";
853
867
  emitSystemNoticeUpdate(session, notificationSeverity(msg.priority), text);
@@ -1145,7 +1159,29 @@ export function handleSdkMessage(session, message) {
1145
1159
  },
1146
1160
  });
1147
1161
  if (resolvedToolUseId) {
1148
- emitToolProgressUpdate(session, resolvedToolUseId, toolName);
1162
+ const hasSubagentRetry = Object.hasOwn(msg, "subagent_retry");
1163
+ const subagentRetry = hasSubagentRetry ? buildSubagentRetryUpdate(msg) : null;
1164
+ if (hasSubagentRetry && !subagentRetry) {
1165
+ bridgeLogger.warn({
1166
+ target: LOG_TARGETS.APP_TOOL,
1167
+ eventName: "sdk_subagent_retry_rejected",
1168
+ message: "ignored malformed SDK subagent retry progress",
1169
+ outcome: "invalid_payload",
1170
+ sessionId: session.sessionId,
1171
+ toolCallId: resolvedToolUseId,
1172
+ });
1173
+ }
1174
+ const subagentType = typeof msg.subagent_type === "string" && msg.subagent_type.trim()
1175
+ ? msg.subagent_type.trim()
1176
+ : undefined;
1177
+ emitToolProgressUpdate(session, resolvedToolUseId, toolName, {
1178
+ ...(subagentRetry
1179
+ ? { subagentRetry }
1180
+ : hasSubagentRetry
1181
+ ? {}
1182
+ : { subagentRetry: { state: "clear" } }),
1183
+ ...(subagentType ? { subagentType } : {}),
1184
+ });
1149
1185
  }
1150
1186
  return;
1151
1187
  }
@@ -100,23 +100,28 @@ function settingsObjectFromLaunchSettings(launchSettings) {
100
100
  return launchSettings.settings;
101
101
  }
102
102
  function normalizedSettingsFromLaunchSettings(launchSettings) {
103
- const settings = settingsObjectFromLaunchSettings(launchSettings);
104
- if (!settings) {
105
- return undefined;
106
- }
103
+ const settings = settingsObjectFromLaunchSettings(launchSettings) ?? {};
104
+ // SendFeedback queues a local draft that can only be reviewed, edited, and
105
+ // discarded through the native /feedback surface. Agent SDK command
106
+ // snapshots do not expose that command to this host, so enabling drafts
107
+ // would create content that claude-rs cannot safely let the user approve.
108
+ const hostSettings = {
109
+ ...settings,
110
+ feedbackDrafts: "off",
111
+ };
107
112
  const sandbox = settings.sandbox && typeof settings.sandbox === "object" && !Array.isArray(settings.sandbox)
108
113
  ? settings.sandbox
109
114
  : undefined;
110
115
  if (sandbox?.enabled === true && sandbox.failIfUnavailable === undefined) {
111
116
  return {
112
- ...settings,
117
+ ...hostSettings,
113
118
  sandbox: {
114
119
  ...sandbox,
115
120
  failIfUnavailable: false,
116
121
  },
117
122
  };
118
123
  }
119
- return settings;
124
+ return hostSettings;
120
125
  }
121
126
  export function sessionById(sessionId) {
122
127
  return sessions.get(sessionId) ?? null;
@@ -542,6 +547,8 @@ function permissionModeFromSettingsValue(rawMode) {
542
547
  return undefined;
543
548
  }
544
549
  switch (rawMode) {
550
+ case "manual":
551
+ return "default";
545
552
  case "default":
546
553
  case "auto":
547
554
  case "acceptEdits":
@@ -610,9 +617,13 @@ export function buildQueryOptions(params) {
610
617
  includePartialMessages: true,
611
618
  promptSuggestions: true,
612
619
  enableFileCheckpointing: true,
620
+ // ProposeSkills reports only a proposal count and expects a native review
621
+ // surface. Keep it out of this host until claude-rs can display the actual
622
+ // proposal input and accept/reject it without losing content.
623
+ disallowedTools: ["ProposeSkills"],
613
624
  executable: "bun",
614
625
  ...(params.resume ? {} : { sessionId: params.provisionalSessionId }),
615
- ...(settings ? { settings } : {}),
626
+ settings,
616
627
  ...modelOption,
617
628
  ...permissionModeOptions,
618
629
  toolConfig: { askUserQuestion: { previewFormat: "markdown" } },
@@ -15,6 +15,34 @@ function nonNegativeNumberField(record, ...keys) {
15
15
  }
16
16
  return value;
17
17
  }
18
+ function nonNegativeIntegerField(record, ...keys) {
19
+ const value = numberField(record, ...keys);
20
+ return value !== undefined && value >= 0 && Number.isInteger(value) ? value : undefined;
21
+ }
22
+ export function buildSubagentRetryUpdate(message) {
23
+ const retry = asRecordOrNull(message.subagent_retry);
24
+ if (!retry) {
25
+ return null;
26
+ }
27
+ const attempt = nonNegativeIntegerField(retry, "attempt");
28
+ const maxRetries = nonNegativeIntegerField(retry, "max_retries", "maxRetries");
29
+ const retryDelayMs = nonNegativeIntegerField(retry, "retry_delay_ms", "retryDelayMs");
30
+ if (attempt === undefined || maxRetries === undefined || retryDelayMs === undefined) {
31
+ return null;
32
+ }
33
+ const agentId = typeof retry.agent_id === "string" ? retry.agent_id.trim() : "";
34
+ const errorCategory = typeof retry.error_category === "string" ? retry.error_category.trim() : "";
35
+ const errorStatus = nonNegativeIntegerField(retry, "error_status", "errorStatus");
36
+ return {
37
+ state: "waiting",
38
+ ...(agentId ? { agent_id: agentId } : {}),
39
+ attempt,
40
+ max_retries: maxRetries,
41
+ retry_delay_ms: retryDelayMs,
42
+ ...(errorStatus !== undefined ? { error_status: errorStatus } : {}),
43
+ ...(errorCategory ? { error_category: errorCategory } : {}),
44
+ };
45
+ }
18
46
  export function parseFastModeState(value) {
19
47
  if (value === "off" || value === "cooldown" || value === "on") {
20
48
  return value;
@@ -9,6 +9,7 @@ const TASK_TOOL_NAMES = new Set([
9
9
  "TaskOutput",
10
10
  "TaskStop",
11
11
  ]);
12
+ const BACKGROUND_TASK_METADATA_KEY = "sdk_background_task";
12
13
  export function isTaskToolName(name) {
13
14
  return TASK_TOOL_NAMES.has(name);
14
15
  }
@@ -806,6 +807,8 @@ function lifecycleMetadata(msg) {
806
807
  "summary",
807
808
  "end_time",
808
809
  "total_paused_ms",
810
+ "blocked",
811
+ "parent_agent_id",
809
812
  ]) {
810
813
  copyValue(msg, key);
811
814
  copyValue(patch, key);
@@ -857,6 +860,43 @@ export function applyTaskLifecycleState(session, subtype, msg) {
857
860
  }),
858
861
  ]);
859
862
  }
863
+ export function applyBackgroundTasksChanged(session, msg) {
864
+ const rawTasks = Array.isArray(msg.tasks) ? msg.tasks : [];
865
+ const nextTasks = rawTasks
866
+ .map((entry) => {
867
+ const task = asRecordOrNull(entry);
868
+ const taskId = nonEmptyString(task?.task_id);
869
+ if (!task || !taskId) {
870
+ return undefined;
871
+ }
872
+ const taskType = nonEmptyString(task.task_type);
873
+ const description = nonEmptyString(task.description);
874
+ return {
875
+ task_id: taskId,
876
+ subject: description ?? taskType ?? taskId,
877
+ description,
878
+ status: "in_progress",
879
+ blocks: [],
880
+ blocked_by: [],
881
+ metadata: {
882
+ [BACKGROUND_TASK_METADATA_KEY]: true,
883
+ ...(taskType ? { task_type: taskType } : {}),
884
+ },
885
+ };
886
+ })
887
+ .filter((task) => Boolean(task));
888
+ const nextIds = new Set(nextTasks.map((task) => task.task_id));
889
+ const removedTaskIds = session.taskOrder.filter((taskId) => {
890
+ if (nextIds.has(taskId)) {
891
+ return false;
892
+ }
893
+ const metadata = asRecordOrNull(session.tasksById.get(taskId)?.metadata);
894
+ return metadata?.[BACKGROUND_TASK_METADATA_KEY] === true;
895
+ });
896
+ const tasks = nextTasks.map((task) => upsertTask(session, task));
897
+ const removed = removeTasks(session, removedTaskIds);
898
+ emitTaskStateUpdate(session, "background_tasks", tasks, removed, true);
899
+ }
860
900
  export function currentTaskSnapshot(session) {
861
901
  return orderedTasks(session);
862
902
  }
@@ -112,10 +112,14 @@ function mergeTaskMetadata(current, update) {
112
112
  if (update === undefined) {
113
113
  return current;
114
114
  }
115
- return {
115
+ const merged = {
116
116
  ...(current ?? {}),
117
117
  ...update,
118
118
  };
119
+ if (update.subagent_retry?.state === "clear") {
120
+ delete merged.subagent_retry;
121
+ }
122
+ return merged;
119
123
  }
120
124
  function applyFieldsToBase(base, fields) {
121
125
  if (fields.title !== undefined) {
@@ -237,6 +241,16 @@ function taskTitleContext(session, name, input) {
237
241
  }
238
242
  export function emitToolCallUpdate(session, toolUseId, fields, updateKind, sourceMessageUuid) {
239
243
  const base = session.toolCalls.get(toolUseId);
244
+ const nextStatus = fields.status ?? base?.status;
245
+ const terminal = nextStatus === "completed" || nextStatus === "failed" || nextStatus === "killed";
246
+ const hasActiveRetry = base?.task_metadata?.subagent_retry?.state === "waiting" ||
247
+ fields.task_metadata?.subagent_retry?.state === "waiting";
248
+ if (terminal && hasActiveRetry) {
249
+ fields.task_metadata = {
250
+ ...(fields.task_metadata ?? {}),
251
+ subagent_retry: { state: "clear" },
252
+ };
253
+ }
240
254
  logToolCallUpdateEmitted(session.sessionId, toolUseId, fields, base, updateKind);
241
255
  emitSessionUpdate(session.sessionId, {
242
256
  type: "tool_call_update",
@@ -324,19 +338,39 @@ export function finalizeOpenToolCalls(session, status) {
324
338
  emitToolCallUpdate(session, toolUseId, { status }, "finalize");
325
339
  }
326
340
  }
327
- export function emitToolProgressUpdate(session, toolUseId, toolName) {
328
- const existing = session.toolCalls.get(toolUseId);
341
+ export function emitToolProgressUpdate(session, toolUseId, toolName, progress = {}) {
342
+ let existing = session.toolCalls.get(toolUseId);
329
343
  if (!existing) {
330
344
  emitToolCall(session, toolUseId, toolName, {});
331
- return;
345
+ existing = session.toolCalls.get(toolUseId);
332
346
  }
333
- if (existing.status === "in_progress" ||
347
+ if (!existing ||
334
348
  existing.status === "completed" ||
335
349
  existing.status === "failed" ||
336
350
  existing.status === "killed") {
337
351
  return;
338
352
  }
339
- emitToolCallUpdate(session, toolUseId, { status: "in_progress" }, "progress");
353
+ const taskMetadata = {};
354
+ if (progress.subagentRetry?.state === "waiting") {
355
+ taskMetadata.subagent_retry = progress.subagentRetry;
356
+ }
357
+ else if (progress.subagentRetry?.state === "clear" &&
358
+ existing.task_metadata?.subagent_retry?.state === "waiting") {
359
+ taskMetadata.subagent_retry = progress.subagentRetry;
360
+ }
361
+ if (progress.subagentType && progress.subagentType !== existing.task_metadata?.subagent_type) {
362
+ taskMetadata.subagent_type = progress.subagentType;
363
+ }
364
+ const fields = {};
365
+ if (existing.status !== "in_progress") {
366
+ fields.status = "in_progress";
367
+ }
368
+ if (Object.keys(taskMetadata).length > 0) {
369
+ fields.task_metadata = taskMetadata;
370
+ }
371
+ if (Object.keys(fields).length > 0) {
372
+ emitToolCallUpdate(session, toolUseId, fields, "progress");
373
+ }
340
374
  }
341
375
  export function emitToolSummaryUpdate(session, toolUseId, summary) {
342
376
  const base = session.toolCalls.get(toolUseId);
@@ -423,6 +457,12 @@ function buildTaskMetadata(patch) {
423
457
  if (typeof patch.status === "string" && ["completed", "failed", "killed"].includes(patch.status)) {
424
458
  taskMetadata.terminal_status = patch.status;
425
459
  }
460
+ if (typeof patch.blocked === "boolean") {
461
+ taskMetadata.blocked = patch.blocked;
462
+ }
463
+ if (typeof patch.parent_agent_id === "string" && patch.parent_agent_id.length > 0) {
464
+ taskMetadata.parent_agent_id = patch.parent_agent_id;
465
+ }
426
466
  return Object.keys(taskMetadata).length > 0 ? taskMetadata : undefined;
427
467
  }
428
468
  export function taskUpdatedFields(msg) {
@@ -261,7 +261,6 @@ export function toolTitle(name, input, context = {}) {
261
261
  return label ? `${ARTIFACT_TOOL_NAME}: ${label}` : ARTIFACT_TOOL_NAME;
262
262
  }
263
263
  if (name === SHOW_ONBOARDING_ROLE_PICKER_TOOL_NAME) {
264
- // TODO: The TUI accepts this SDK tool call but does not implement an onboarding role flow yet.
265
264
  return SHOW_ONBOARDING_ROLE_PICKER_TOOL_NAME;
266
265
  }
267
266
  if (name === "EnterWorktree") {
@@ -483,9 +482,19 @@ function extractToolOutputMetadata(toolName, rawResult, rawContent) {
483
482
  if (toolName === "Bash") {
484
483
  for (const candidate of candidates) {
485
484
  const hasAssistantAutoBackgrounded = typeof candidate.assistantAutoBackgrounded === "boolean";
486
- if (hasAssistantAutoBackgrounded) {
485
+ const timedOutAfterMs = nonNegativeInteger(candidate.timedOutAfterMs);
486
+ const backgroundCwdHint = nonEmptyString(candidate.backgroundCwdHint);
487
+ if (hasAssistantAutoBackgrounded || timedOutAfterMs !== undefined || backgroundCwdHint) {
487
488
  const bashMetadata = {};
488
- bashMetadata.assistant_auto_backgrounded = candidate.assistantAutoBackgrounded;
489
+ if (hasAssistantAutoBackgrounded) {
490
+ bashMetadata.assistant_auto_backgrounded = candidate.assistantAutoBackgrounded;
491
+ }
492
+ if (timedOutAfterMs !== undefined) {
493
+ bashMetadata.timed_out_after_ms = timedOutAfterMs;
494
+ }
495
+ if (backgroundCwdHint) {
496
+ bashMetadata.background_cwd_hint = backgroundCwdHint;
497
+ }
489
498
  metadata.bash = bashMetadata;
490
499
  break;
491
500
  }
@@ -494,9 +503,11 @@ function extractToolOutputMetadata(toolName, rawResult, rawContent) {
494
503
  if (toolName === "Agent" || toolName === "Task") {
495
504
  for (const candidate of candidates) {
496
505
  const resolvedModel = nonEmptyString(candidate.resolvedModel);
497
- if (resolvedModel) {
506
+ const modelsUsed = orderedNonEmptyStrings(candidate.modelsUsed);
507
+ if (resolvedModel || modelsUsed) {
498
508
  const agentMetadata = {
499
- resolved_model: resolvedModel,
509
+ ...(resolvedModel ? { resolved_model: resolvedModel } : {}),
510
+ ...(modelsUsed ? { models_used: modelsUsed } : {}),
500
511
  };
501
512
  metadata.agent = agentMetadata;
502
513
  break;
@@ -737,6 +748,10 @@ function shellBackgroundMessage(record) {
737
748
  if (!backgroundTaskId) {
738
749
  return "";
739
750
  }
751
+ const timedOutAfterMs = nonNegativeInteger(record.timedOutAfterMs);
752
+ if (timedOutAfterMs !== undefined) {
753
+ return `Command was auto-backgrounded after ${timedOutAfterMs.toLocaleString("en-US")} ms with ID: ${backgroundTaskId}.`;
754
+ }
740
755
  if (record.assistantAutoBackgrounded === true) {
741
756
  return `Command was auto-backgrounded by assistant mode with ID: ${backgroundTaskId}.`;
742
757
  }
@@ -813,6 +828,9 @@ function recordNumber(record, key) {
813
828
  const value = record[key];
814
829
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
815
830
  }
831
+ function recordNonNegativeInteger(record, key) {
832
+ return nonNegativeInteger(record[key]);
833
+ }
816
834
  function recordString(record, key) {
817
835
  const value = record[key];
818
836
  return typeof value === "string" ? value : undefined;
@@ -858,8 +876,13 @@ function globResultText(record) {
858
876
  function grepResultText(record) {
859
877
  const filenames = searchFilenames(record);
860
878
  const content = recordString(record, "content") ?? "";
861
- const numFiles = recordNumber(record, "numFiles") ?? filenames.length;
862
- const numLines = recordNumber(record, "numLines");
879
+ const hasVisibleMatches = content.trim().length > 0 || filenames.length > 0;
880
+ const totalFiles = recordNonNegativeInteger(record, "totalFiles");
881
+ const legacyNumFiles = recordNonNegativeInteger(record, "numFiles");
882
+ const numFiles = totalFiles ??
883
+ (legacyNumFiles !== 0 || !hasVisibleMatches ? legacyNumFiles : undefined) ??
884
+ (filenames.length > 0 ? filenames.length : undefined);
885
+ const numLines = recordNonNegativeInteger(record, "totalLines") ?? recordNonNegativeInteger(record, "numLines");
863
886
  const numMatches = recordNumber(record, "numMatches");
864
887
  const appliedLimit = recordNumber(record, "appliedLimit");
865
888
  const appliedOffset = recordNumber(record, "appliedOffset");
@@ -879,7 +902,9 @@ function grepResultText(record) {
879
902
  lines.push("No matches found");
880
903
  }
881
904
  const summaryParts = [];
882
- summaryParts.push(`${numFiles} ${pluralize(numFiles, "file")}`);
905
+ if (numFiles !== undefined) {
906
+ summaryParts.push(`${numFiles} ${pluralize(numFiles, "file")}`);
907
+ }
883
908
  if (numMatches !== undefined) {
884
909
  summaryParts.push(`${numMatches} ${pluralize(numMatches, "match", "matches")}`);
885
910
  }
@@ -938,6 +963,26 @@ function pushBooleanField(lines, label, value) {
938
963
  function nonEmptyString(value) {
939
964
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
940
965
  }
966
+ function nonNegativeInteger(value) {
967
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
968
+ ? Math.trunc(value)
969
+ : undefined;
970
+ }
971
+ function orderedNonEmptyStrings(value) {
972
+ if (!Array.isArray(value)) {
973
+ return undefined;
974
+ }
975
+ const seen = new Set();
976
+ const values = [];
977
+ for (const item of value) {
978
+ const normalized = nonEmptyString(item);
979
+ if (normalized && !seen.has(normalized)) {
980
+ seen.add(normalized);
981
+ values.push(normalized);
982
+ }
983
+ }
984
+ return values.length > 0 ? values : undefined;
985
+ }
941
986
  const CRON_MONTH_NAMES = [
942
987
  "January",
943
988
  "February",
@@ -108,9 +108,23 @@ export async function generatePersistedSessionTitle(query, description) {
108
108
  return title;
109
109
  }
110
110
  export async function applySessionEffort(query, effort) {
111
- const settings = { effortLevel: effort };
112
- // applyFlagSettings controls live session settings; SDK Settings typings model persisted effort levels.
113
- await query.applyFlagSettings(settings);
111
+ await query.applyFlagSettings({ effortLevel: effort });
112
+ }
113
+ export function buildPromptUserMessage(command, sessionId) {
114
+ const content = contentFromPrompt(command);
115
+ if (content.length === 0) {
116
+ return undefined;
117
+ }
118
+ return {
119
+ type: "user",
120
+ session_id: sessionId,
121
+ parent_tool_use_id: null,
122
+ origin: { kind: "human" },
123
+ message: {
124
+ role: "user",
125
+ content,
126
+ },
127
+ };
114
128
  }
115
129
  export async function applySessionAgent(query, agent) {
116
130
  const settings = { agent };
@@ -130,7 +144,7 @@ export function emitAgentConfigOptionUpdate(sessionId, agent) {
130
144
  value: agent,
131
145
  });
132
146
  }
133
- const EXPECTED_AGENT_SDK_VERSION = "0.3.198";
147
+ const EXPECTED_AGENT_SDK_VERSION = "0.3.214";
134
148
  const require = createRequire(import.meta.url);
135
149
  export function resolveInstalledAgentSdkVersion() {
136
150
  try {
@@ -662,19 +676,10 @@ async function handleCommand(command, requestId) {
662
676
  slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
663
677
  return;
664
678
  }
665
- const content = contentFromPrompt(command);
666
- if (content.length === 0) {
679
+ const message = buildPromptUserMessage(command, session.sessionId);
680
+ if (!message) {
667
681
  return;
668
682
  }
669
- const message = {
670
- type: "user",
671
- session_id: session.sessionId,
672
- parent_tool_use_id: null,
673
- message: {
674
- role: "user",
675
- content,
676
- },
677
- };
678
683
  session.input.enqueue(message);
679
684
  return;
680
685
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-rust",
3
- "version": "0.13.4",
3
+ "version": "0.14.1",
4
4
  "description": "Claude Code Rust - native Rust terminal interface for Claude Code",
5
5
  "keywords": [
6
6
  "cli",
@@ -33,15 +33,15 @@
33
33
  "LICENSE"
34
34
  ],
35
35
  "dependencies": {
36
- "@anthropic-ai/claude-agent-sdk": "0.3.198"
36
+ "@anthropic-ai/claude-agent-sdk": "0.3.214"
37
37
  },
38
38
  "optionalDependencies": {
39
- "@srothgan/claude-code-rust-darwin-arm64": "0.13.4",
40
- "@srothgan/claude-code-rust-darwin-x64": "0.13.4",
41
- "@srothgan/claude-code-rust-linux-x64-gnu": "0.13.4",
42
- "@srothgan/claude-code-rust-linux-arm64-gnu": "0.13.4",
43
- "@srothgan/claude-code-rust-win32-x64-msvc": "0.13.4",
44
- "@srothgan/claude-code-rust-win32-arm64-msvc": "0.13.4"
39
+ "@srothgan/claude-code-rust-darwin-arm64": "0.14.1",
40
+ "@srothgan/claude-code-rust-darwin-x64": "0.14.1",
41
+ "@srothgan/claude-code-rust-linux-x64-gnu": "0.14.1",
42
+ "@srothgan/claude-code-rust-linux-arm64-gnu": "0.14.1",
43
+ "@srothgan/claude-code-rust-win32-x64-msvc": "0.14.1",
44
+ "@srothgan/claude-code-rust-win32-arm64-msvc": "0.14.1"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=24"