claude-code-rust 0.14.5 → 0.14.6

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
@@ -13,7 +13,7 @@ A native Rust terminal interface for Claude Code. Drop-in replacement for Anthro
13
13
 
14
14
  ## About
15
15
 
16
- 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.
16
+ 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 works unchanged, including tool calls, file editing, terminal commands, and permissions.
17
17
 
18
18
  ## Prerequisite
19
19
 
@@ -52,13 +52,13 @@ claude-rs
52
52
  Full documentation is available at [srothgan.github.io/claude-code-rust](https://srothgan.github.io/claude-code-rust/).
53
53
 
54
54
  > [!NOTE]
55
- > **Agent SDK billing unchanged.** Anthropic has paused the previously announced Agent SDK credit change. For now nothing changes: Claude Agent SDK usage including `claude -p` and third-party apps like this one still draws from your normal Claude subscription limits. See [Use the Claude Agent SDK with your Claude plan](https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan).
55
+ > **Agent SDK billing unchanged.** Anthropic has paused the previously announced Agent SDK credit change. For now nothing changes: Claude Agent SDK usage (including `claude -p` and third-party apps like this one) still draws from your normal Claude subscription limits. See [Use the Claude Agent SDK with your Claude plan](https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan).
56
56
 
57
57
  ## Why
58
58
 
59
59
  The stock Claude Code TUI runs on Node.js with React Ink, which renders by redrawing full frames over raw ANSI escape codes. This causes real, widely-reported problems:
60
60
 
61
- - **Flickering**: The whole view is redrawn on every status update, causing constant flicker bad enough to crash editors' integrated terminals during long sessions
61
+ - **Flickering**: The whole view is redrawn on every status update, causing constant flicker (bad enough to crash editors' integrated terminals during long sessions)
62
62
  - **CPU**: Sustained high CPU even when idle, and runaway loops that spawn multiple background processes
63
63
  - **Memory**: 200-400MB baseline (and climbing with conversation length) vs ~20-50MB for a native binary
64
64
  - **Resize**: Window resizing leaves duplicated frames in scrollback, loses lines when shrinking, and can garble the display
@@ -66,7 +66,7 @@ The stock Claude Code TUI runs on Node.js with React Ink, which renders by redra
66
66
  - **Scrollback**: Hijacks the terminal's native scrollback, erasing history you can no longer scroll back to
67
67
  - **Paste**: Large pastes can flood stdout and freeze the terminal
68
68
 
69
- Claude Code Rust addresses these with a native terminal UI that uses diffed, direct terminal control via Crossterm and Ratatui -- no full-frame redraws and no React Ink rendering loop.
69
+ Claude Code Rust addresses these with a native terminal UI that uses diffed, direct terminal control via Crossterm and Ratatui, with no full-frame redraws and no React Ink rendering loop.
70
70
 
71
71
  ## Documentation
72
72
 
@@ -96,9 +96,9 @@ This project is licensed under the [Apache License 2.0](LICENSE). Apache-2.0 was
96
96
 
97
97
  This project is not affiliated with, endorsed by, or supported by Anthropic.
98
98
 
99
- A quick note on where this project stands, since I know people worry about this kind of thing: claude-code-rust is a terminal UI that I wrote from scratch in Rust. It is not a fork, copy or port of the latest Claude Code source leak -- it talks to Anthropic's official [Agent SDK](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/agent-sdk) as a runtime dependency instead, the same way any other third-party tool would. No Anthropic source code was read or used as reference at any point during development.
99
+ A quick note on where this project stands, since I know people worry about this kind of thing: claude-code-rust is a terminal UI that I wrote from scratch in Rust. It is not a fork, copy or port of the latest Claude Code source leak. It talks to Anthropic's official [Agent SDK](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/agent-sdk) as a runtime dependency instead, the same way any other third-party tool would. No Anthropic source code was read or used as reference at any point during development.
100
100
 
101
- The project authenticates through your existing Claude Code account via the Agent SDK, and the Agent SDK's terms allow building on top of it. Billing, credits, limits, and overage behavior are controlled by Anthropic, including any future changes Anthropic may make to how Agent SDK usage is metered. Other community projects do the same. As far as I can tell, using this project is fine -- but I am a single maintainer, not a lawyer. If anything changes on Anthropic's end, I will update this section and adjust the project accordingly.
101
+ The project authenticates through your existing Claude Code account via the Agent SDK, and the Agent SDK's terms allow building on top of it. Billing, credits, limits, and overage behavior are controlled by Anthropic, including any future changes Anthropic may make to how Agent SDK usage is metered. Other community projects do the same. As far as I can tell, using this project is fine, but I am a single maintainer, not a lawyer. If anything changes on Anthropic's end, I will update this section and adjust the project accordingly.
102
102
 
103
103
  This project's source code is licensed under [Apache-2.0](LICENSE). The Agent SDK itself is proprietary and governed by [Anthropic's Commercial Terms of Service](https://www.anthropic.com/legal/commercial-terms).
104
104
 
@@ -1,3 +1,4 @@
1
+ import { writeEvent } from "./events.js";
1
2
  import { bridgeLogger, LOG_TARGETS } from "./logger.js";
2
3
  export async function dispatchCancelTurnCommand(command, deps) {
3
4
  const session = deps.sessionById(command.session_id);
@@ -9,6 +10,11 @@ export async function dispatchCancelTurnCommand(command, deps) {
9
10
  const stillQueued = Array.isArray(receipt?.still_queued)
10
11
  ? receipt.still_queued.filter((entry) => typeof entry === "string")
11
12
  : [];
13
+ writeEvent({
14
+ event: "turn_interrupt_receipt",
15
+ session_id: command.session_id,
16
+ still_queued: stillQueued,
17
+ }, deps.requestId);
12
18
  if (stillQueued.length > 0) {
13
19
  bridgeLogger.info({
14
20
  target: LOG_TARGETS.APP_SESSION,
@@ -1,7 +1,7 @@
1
1
  import { buildModeState, markModeUnavailableForSession, permissionModeFailureLooksUnsupported, refreshSupportedModesForSession, toPermissionMode, } from "./commands.js";
2
2
  import { dispatchCancelTurnCommand } from "./command_dispatch.js";
3
3
  import { emitFastModeUpdate } from "./error_classification.js";
4
- import { emitSessionUpdate, slashError } from "./events.js";
4
+ import { emitSessionUpdate, slashError, writeEvent } from "./events.js";
5
5
  import { bridgeLogger, LOG_TARGETS } from "./logger.js";
6
6
  import { emitCurrentModelUpdate, refreshCurrentModel, sessionById, shouldInvalidateResolvedRuntimeModel, } from "./session_lifecycle.js";
7
7
  export async function handleSessionControlCommand(command, requestId, deps) {
@@ -38,12 +38,38 @@ export async function handleSessionControlCommand(command, requestId, deps) {
38
38
  function handlePrompt(command, requestId, deps) {
39
39
  const session = requireSession(command.session_id, requestId);
40
40
  if (!session) {
41
+ writeEvent({
42
+ event: "user_message_rejected",
43
+ session_id: command.session_id,
44
+ message_uuid: command.message_uuid,
45
+ reason: "unknown session",
46
+ }, requestId);
41
47
  return;
42
48
  }
43
49
  const message = deps.buildPromptUserMessage(command, session.sessionId);
44
- if (message) {
45
- session.input.enqueue(message);
50
+ if (!message) {
51
+ writeEvent({
52
+ event: "user_message_rejected",
53
+ session_id: session.sessionId,
54
+ message_uuid: command.message_uuid,
55
+ reason: "prompt contained no supported content",
56
+ }, requestId);
57
+ return;
58
+ }
59
+ if (!session.input.enqueue(message)) {
60
+ writeEvent({
61
+ event: "user_message_rejected",
62
+ session_id: session.sessionId,
63
+ message_uuid: command.message_uuid,
64
+ reason: "session input is closed",
65
+ }, requestId);
66
+ return;
46
67
  }
68
+ writeEvent({
69
+ event: "user_message_queued",
70
+ session_id: session.sessionId,
71
+ message_uuid: command.message_uuid,
72
+ }, requestId);
47
73
  }
48
74
  async function setModel(command, requestId) {
49
75
  const session = requireSession(command.session_id, requestId);
@@ -107,7 +107,7 @@ async function getContextUsage(command, requestId) {
107
107
  return;
108
108
  }
109
109
  try {
110
- const usage = await session.query.getContextUsage();
110
+ const usage = await session.query.getContextUsage({ detail: "summary" });
111
111
  if (typeof usage.model === "string" && usage.model.trim().length > 0) {
112
112
  session.resolvedRuntimeModelId = usage.model.trim();
113
113
  refreshCurrentModel(session, true);
@@ -262,6 +262,7 @@ export function parseCommandEnvelope(line) {
262
262
  return {
263
263
  command: "prompt",
264
264
  session_id: expectString(raw, "session_id", "prompt"),
265
+ message_uuid: expectString(raw, "message_uuid", "prompt"),
265
266
  chunks: parsePromptChunks(raw, "prompt"),
266
267
  };
267
268
  case "cancel_turn":
@@ -146,6 +146,10 @@ function eventToolCallId(event) {
146
146
  case "mcp_auth_redirect":
147
147
  case "mcp_operation_error":
148
148
  case "mcp_set_servers_result":
149
+ case "user_message_queued":
150
+ case "user_message_started":
151
+ case "user_message_rejected":
152
+ case "turn_interrupt_receipt":
149
153
  case "turn_complete":
150
154
  case "turn_error":
151
155
  case "slash_error":
@@ -199,6 +203,7 @@ function protocolEventLevel(event) {
199
203
  case "elicitation_request":
200
204
  case "elicitation_complete":
201
205
  case "mcp_auth_redirect":
206
+ case "user_message_started":
202
207
  case "turn_complete":
203
208
  return "trace";
204
209
  case "sessions_listed":
@@ -209,6 +214,9 @@ function protocolEventLevel(event) {
209
214
  case "rewind_result":
210
215
  case "runtime_reload_completed":
211
216
  case "mcp_set_servers_result":
217
+ case "user_message_queued":
218
+ case "user_message_rejected":
219
+ case "turn_interrupt_receipt":
212
220
  case "mcp_snapshot":
213
221
  return "debug";
214
222
  }
@@ -289,6 +289,7 @@ export async function handleMcpSetServersCommand(session, command, requestId) {
289
289
  logMcpFailure("mcp_servers_set_failed", "failed to update MCP server configuration", command.session_id, message, requestId, { server_count: Object.keys(command.servers).length });
290
290
  emitMcpOperationError(command.session_id, { operation: "set-servers", message }, requestId);
291
291
  slashError(command.session_id, `failed to set MCP servers: ${message}`, requestId);
292
+ await handleMcpStatusCommand(session, requestId, "mcp_set_servers");
292
293
  }
293
294
  }
294
295
  export async function handleMcpAuthenticateCommand(session, command, requestId) {
@@ -2,7 +2,7 @@ import { asRecordOrNull } from "./shared.js";
2
2
  import { toPermissionMode, buildModeState, refreshSupportedModesForSession, } from "./commands.js";
3
3
  import { writeEvent, emitSessionUpdate, emitConnectEvent, emitSessionReplacedEvent, } from "./events.js";
4
4
  import { TOOL_RESULT_TYPES, isToolSearchToolName, isToolSearchToolResultType, unwrapToolUseResult, parseToolNonExecutionMetadata, } from "./tooling.js";
5
- import { emitToolCall, emitToolCallUpdate, emitToolResultUpdate, finalizeOpenToolCalls, emitToolProgressUpdate, emitToolSummaryUpdate, ensureToolCallVisible, resolveTaskToolUseId, defersTaskNotificationCompletion, toolAcceptsTaskLifecycle, taskProgressText, taskUpdatedFields, } from "./tool_calls.js";
5
+ import { emitToolCall, emitToolCallUpdate, emitToolResultUpdate, finalizeOpenToolCalls, emitToolProgressUpdate, emitToolSummaryUpdate, ensureToolCallVisible, resolveTaskToolUseId, defersTaskNotificationCompletion, toolAcceptsTaskLifecycle, toolAcceptsTerminalTaskNotification, toolPreservesTaskNotificationOutput, taskProgressText, taskUpdatedFields, } from "./tool_calls.js";
6
6
  import { applyBackgroundTasksChanged, applyTaskLifecycleState, } from "./tasks.js";
7
7
  import { linkTaskToolUse, unlinkTaskToolUse } from "./task_links.js";
8
8
  import { emitAuthRequired, classifyTurnErrorKind, emitFastModeUpdate, emitFastModeUpdateIfChanged, setFastModeSnapshotIfChanged, } from "./error_classification.js";
@@ -13,6 +13,7 @@ import { looksLikeAuthRequired } from "./auth.js";
13
13
  import { emitCurrentModelUpdate, refreshCurrentModel, updateSessionId, } from "./session_lifecycle.js";
14
14
  import { bridgeLogger, LOG_TARGETS } from "./logger.js";
15
15
  import { emitMcpSnapshotFromStatuses } from "./mcp.js";
16
+ import { appendResourceLinks } from "./resource_links.js";
16
17
  export function textFromPrompt(command) {
17
18
  const chunks = command.chunks ?? [];
18
19
  return chunks
@@ -88,6 +89,7 @@ function sdkTaskMetadata(msg) {
88
89
  ...(typeof msg.is_backgrounded === "boolean"
89
90
  ? { is_backgrounded: msg.is_backgrounded }
90
91
  : {}),
92
+ ...(typeof msg.ambient === "boolean" ? { ambient: msg.ambient } : {}),
91
93
  ...(spawnDepth !== undefined ? { spawn_depth: spawnDepth } : {}),
92
94
  };
93
95
  return Object.keys(taskMetadata).length > 0 ? taskMetadata : undefined;
@@ -98,6 +100,44 @@ function sdkMessageOriginKind(msg) {
98
100
  : null;
99
101
  return typeof origin?.kind === "string" ? origin.kind : undefined;
100
102
  }
103
+ function diagnosticToken(value) {
104
+ return typeof value === "string" && /^[a-z][a-z0-9_-]{0,63}$/i.test(value)
105
+ ? value
106
+ : undefined;
107
+ }
108
+ export function sdkMessageDiagnosticFields(msg) {
109
+ const event = asRecordOrNull(msg.event);
110
+ const delta = asRecordOrNull(event?.delta);
111
+ const uuidFields = Object.fromEntries(Object.entries(msg).filter(([key, value]) => key.toLowerCase().includes("uuid") && typeof value === "string"));
112
+ const hasParentToolUseId = Object.hasOwn(msg, "parent_tool_use_id");
113
+ const parentToolUseId = trimmedStringField(msg, "parent_tool_use_id");
114
+ return {
115
+ sdk_type: typeof msg.type === "string" ? msg.type : undefined,
116
+ sdk_subtype: typeof msg.subtype === "string" ? msg.subtype : undefined,
117
+ sdk_uuid: trimmedStringField(msg, "uuid"),
118
+ user_message_uuid: trimmedStringField(msg, "user_message_uuid"),
119
+ parent_tool_use_id: parentToolUseId,
120
+ parent_tool_use_scope: !hasParentToolUseId
121
+ ? "absent"
122
+ : msg.parent_tool_use_id === null
123
+ ? "root"
124
+ : parentToolUseId
125
+ ? "child"
126
+ : "other",
127
+ request_id: trimmedStringField(msg, "request_id"),
128
+ origin_kind: sdkMessageOriginKind(msg),
129
+ inner_event_type: diagnosticToken(event?.type),
130
+ inner_delta_type: diagnosticToken(delta?.type),
131
+ lifecycle_status: diagnosticToken(msg.status),
132
+ lifecycle_state: diagnosticToken(msg.state),
133
+ lifecycle_operation: diagnosticToken(msg.operation),
134
+ lifecycle_reason: diagnosticToken(msg.reason),
135
+ sdk_keys: Object.keys(msg).sort(),
136
+ ...(Object.keys(uuidFields).length > 0
137
+ ? { sdk_uuid_fields: uuidFields }
138
+ : {}),
139
+ };
140
+ }
101
141
  function externalMessageUpdateFromSdkUser(msg) {
102
142
  const origin = asRecordOrNull(msg.origin);
103
143
  if (!origin) {
@@ -490,7 +530,10 @@ export function handleTaskSystemMessage(session, subtype, msg) {
490
530
  return true;
491
531
  }
492
532
  const toolCall = ensureToolCallVisible(session, toolUseId, "Agent", {});
493
- if (!toolAcceptsTaskLifecycle(toolCall)) {
533
+ const acceptsLifecycle = toolAcceptsTaskLifecycle(toolCall);
534
+ const acceptsTerminalNotification = subtype === "task_notification" &&
535
+ toolAcceptsTerminalTaskNotification(toolCall);
536
+ if (!acceptsLifecycle && !acceptsTerminalNotification) {
494
537
  if (taskId) {
495
538
  unlinkTaskToolUse(session, taskId);
496
539
  }
@@ -576,12 +619,16 @@ export function handleTaskSystemMessage(session, subtype, msg) {
576
619
  if (messageTaskMetadata) {
577
620
  fields.task_metadata = messageTaskMetadata;
578
621
  }
579
- if (summary) {
622
+ if (summary && !toolPreservesTaskNotificationOutput(toolCall)) {
580
623
  fields.raw_output = summary;
581
624
  fields.content = [
582
625
  { type: "content", content: { type: "text", text: summary } },
583
626
  ];
584
627
  }
628
+ const contentWithResourceLinks = appendResourceLinks(fields.content, msg.resource_links);
629
+ if (contentWithResourceLinks !== undefined) {
630
+ fields.content = contentWithResourceLinks;
631
+ }
585
632
  if (Object.keys(fields).length > 0) {
586
633
  emitToolCallUpdate(session, toolUseId, fields, "task_notification");
587
634
  }
@@ -908,6 +955,40 @@ export function handleAssistantMessage(session, message) {
908
955
  }
909
956
  }
910
957
  }
958
+ function emitUserMessageStarted(session, message, source) {
959
+ const messageUuid = trimmedStringField(message, "user_message_uuid");
960
+ if (!messageUuid) {
961
+ return;
962
+ }
963
+ emitUserMessageStartedForUuid(session, messageUuid, source);
964
+ }
965
+ function emitUserMessageStartedForUuid(session, messageUuid, source) {
966
+ writeEvent({
967
+ event: "user_message_started",
968
+ session_id: session.sessionId,
969
+ message_uuid: messageUuid,
970
+ source,
971
+ });
972
+ }
973
+ function handleCommandLifecycleMessage(session, message) {
974
+ const state = trimmedStringField(message, "state");
975
+ const commandUuid = trimmedStringField(message, "command_uuid");
976
+ if (state !== "started") {
977
+ return;
978
+ }
979
+ if (!commandUuid) {
980
+ bridgeLogger.warn({
981
+ target: LOG_TARGETS.BRIDGE_SDK,
982
+ eventName: "sdk_command_lifecycle_start_invalid",
983
+ message: "SDK command lifecycle start omitted its command UUID",
984
+ outcome: "ignored",
985
+ sessionId: session.sessionId,
986
+ fields: sdkMessageDiagnosticFields(message),
987
+ });
988
+ return;
989
+ }
990
+ emitUserMessageStartedForUuid(session, commandUuid, "command_lifecycle");
991
+ }
911
992
  function messageToolUseResult(message) {
912
993
  if (Object.hasOwn(message, "toolUseResult")) {
913
994
  return message.toolUseResult;
@@ -960,8 +1041,13 @@ export function handleUserToolResultBlocks(session, message) {
960
1041
  return handled;
961
1042
  }
962
1043
  export function handleResultMessage(session, message) {
1044
+ if (message.parent_tool_use_id === null ||
1045
+ message.parent_tool_use_id === undefined) {
1046
+ emitUserMessageStarted(session, message, "result");
1047
+ }
963
1048
  emitFastModeUpdateIfChanged(session, message.fast_mode_state, message.fast_mode_disabled_reason);
964
1049
  const terminalReason = terminalReasonFromValue(message.terminal_reason);
1050
+ const queuedTurnCount = nonNegativeIntegerField(message, "queued_turn_count");
965
1051
  const subtype = typeof message.subtype === "string" ? message.subtype : "";
966
1052
  if (subtype === "success") {
967
1053
  session.lastAssistantError = undefined;
@@ -969,6 +1055,9 @@ export function handleResultMessage(session, message) {
969
1055
  writeEvent({
970
1056
  event: "turn_complete",
971
1057
  session_id: session.sessionId,
1058
+ ...(queuedTurnCount !== undefined
1059
+ ? { queued_turn_count: queuedTurnCount }
1060
+ : {}),
972
1061
  ...(terminalReason ? { terminal_reason: terminalReason } : {}),
973
1062
  });
974
1063
  return;
@@ -1013,6 +1102,9 @@ export function handleResultMessage(session, message) {
1013
1102
  event: "turn_error",
1014
1103
  session_id: session.sessionId,
1015
1104
  message: errors.length > 0 ? errors.join("\n") : fallback,
1105
+ ...(queuedTurnCount !== undefined
1106
+ ? { queued_turn_count: queuedTurnCount }
1107
+ : {}),
1016
1108
  error_kind: errorKind,
1017
1109
  ...(subtype ? { sdk_result_subtype: subtype } : {}),
1018
1110
  ...(assistantError ? { assistant_error: assistantError } : {}),
@@ -1406,6 +1498,10 @@ export function handleSdkMessage(session, message) {
1406
1498
  return;
1407
1499
  }
1408
1500
  if (type === "stream_event") {
1501
+ if (msg.parent_tool_use_id === null ||
1502
+ msg.parent_tool_use_id === undefined) {
1503
+ emitUserMessageStarted(session, msg, "stream_event");
1504
+ }
1409
1505
  if (msg.event && typeof msg.event === "object") {
1410
1506
  const parentToolUseId = typeof msg.parent_tool_use_id === "string"
1411
1507
  ? msg.parent_tool_use_id
@@ -1580,10 +1676,29 @@ export function handleSdkMessage(session, message) {
1580
1676
  if (msg.error === "authentication_failed") {
1581
1677
  emitAuthRequired(session);
1582
1678
  }
1679
+ if (msg.parent_tool_use_id === null ||
1680
+ msg.parent_tool_use_id === undefined) {
1681
+ emitUserMessageStarted(session, msg, "assistant");
1682
+ }
1583
1683
  handleAssistantMessage(session, msg);
1584
1684
  return;
1585
1685
  }
1586
1686
  if (type === "result") {
1587
1687
  handleResultMessage(session, msg);
1688
+ return;
1588
1689
  }
1690
+ // Bundled Claude Code 2.1.258 emits this runtime frame even though the
1691
+ // corresponding SDK 0.3.258 TypeScript union does not declare it.
1692
+ if (type === "command_lifecycle") {
1693
+ handleCommandLifecycleMessage(session, msg);
1694
+ return;
1695
+ }
1696
+ bridgeLogger.debug({
1697
+ target: LOG_TARGETS.BRIDGE_SDK,
1698
+ eventName: "sdk_message_unhandled",
1699
+ message: "SDK message ignored by explicit top-level fallback policy",
1700
+ outcome: "ignored",
1701
+ sessionId: session.sessionId,
1702
+ fields: sdkMessageDiagnosticFields(msg),
1703
+ });
1589
1704
  }
@@ -0,0 +1,102 @@
1
+ import { asRecordOrNull } from "./shared.js";
2
+ const MAX_RESOURCE_LINKS = 50;
3
+ const MAX_RESOURCE_LINK_BYTES = 64 * 1024;
4
+ function nonEmptyString(value) {
5
+ return typeof value === "string" && value.trim().length > 0
6
+ ? value.trim()
7
+ : undefined;
8
+ }
9
+ function jsonValue(value) {
10
+ if (value === null ||
11
+ typeof value === "string" ||
12
+ typeof value === "boolean") {
13
+ return value;
14
+ }
15
+ if (typeof value === "number") {
16
+ return Number.isFinite(value) ? value : undefined;
17
+ }
18
+ if (Array.isArray(value)) {
19
+ const entries = [];
20
+ for (const entry of value) {
21
+ const parsed = jsonValue(entry);
22
+ if (parsed === undefined) {
23
+ return undefined;
24
+ }
25
+ entries.push(parsed);
26
+ }
27
+ return entries;
28
+ }
29
+ const record = asRecordOrNull(value);
30
+ if (!record) {
31
+ return undefined;
32
+ }
33
+ const parsed = [];
34
+ for (const [key, entry] of Object.entries(record)) {
35
+ const value = jsonValue(entry);
36
+ if (value === undefined) {
37
+ return undefined;
38
+ }
39
+ parsed.push([key, value]);
40
+ }
41
+ return Object.fromEntries(parsed);
42
+ }
43
+ function resourceLinkContent(value) {
44
+ const link = asRecordOrNull(value);
45
+ const uri = nonEmptyString(link?.uri);
46
+ const name = nonEmptyString(link?.name);
47
+ if (!link || !uri || !name) {
48
+ return undefined;
49
+ }
50
+ const title = nonEmptyString(link.title);
51
+ const description = nonEmptyString(link.description);
52
+ const mimeType = nonEmptyString(link.mimeType);
53
+ const size = typeof link.size === "number" &&
54
+ Number.isSafeInteger(link.size) &&
55
+ link.size >= 0
56
+ ? link.size
57
+ : undefined;
58
+ const annotationsValue = link.annotations === undefined ? undefined : jsonValue(link.annotations);
59
+ const annotations = asRecordOrNull(annotationsValue);
60
+ if (link.annotations !== undefined && !annotations) {
61
+ return undefined;
62
+ }
63
+ return {
64
+ type: "resource_link",
65
+ uri,
66
+ name,
67
+ ...(title ? { title } : {}),
68
+ ...(description ? { description } : {}),
69
+ ...(mimeType ? { mime_type: mimeType } : {}),
70
+ ...(size !== undefined ? { size } : {}),
71
+ ...(annotations ? { annotations } : {}),
72
+ };
73
+ }
74
+ export function resourceLinkContents(value) {
75
+ if (!Array.isArray(value)) {
76
+ return [];
77
+ }
78
+ const links = [];
79
+ let serializedBytes = 2;
80
+ for (const entry of value) {
81
+ if (links.length >= MAX_RESOURCE_LINKS) {
82
+ break;
83
+ }
84
+ const link = resourceLinkContent(entry);
85
+ if (!link) {
86
+ continue;
87
+ }
88
+ const linkBytes = Buffer.byteLength(JSON.stringify(link));
89
+ const separatorBytes = links.length > 0 ? 1 : 0;
90
+ if (serializedBytes + separatorBytes + linkBytes >
91
+ MAX_RESOURCE_LINK_BYTES) {
92
+ continue;
93
+ }
94
+ serializedBytes += separatorBytes + linkBytes;
95
+ links.push(link);
96
+ }
97
+ return links;
98
+ }
99
+ export function appendResourceLinks(content, value) {
100
+ const links = resourceLinkContents(value);
101
+ return links.length > 0 ? [...(content ?? []), ...links] : content;
102
+ }
@@ -344,6 +344,9 @@ export async function createSession(params) {
344
344
  history_update_count: historyUpdateCount,
345
345
  stale_session_count: staleSessionCount,
346
346
  stale_session_before_register_count: staleSessionBeforeRegisterCount,
347
+ sdk_debug_enabled: enableSdkDebug,
348
+ sdk_debug_file_configured: Boolean(sdkDebugFile),
349
+ sdk_spawn_debug_enabled: enableSpawnDebug,
347
350
  },
348
351
  });
349
352
  try {
@@ -608,7 +611,7 @@ export async function awaitSessionInitialization(session) {
608
611
  // the protocol barrier proving that startup messages have reached the SDK; yield
609
612
  // once more so the bridge's query consumer can process the already-enqueued result.
610
613
  try {
611
- await session.query.getContextUsage();
614
+ await session.query.getContextUsage({ detail: "summary" });
612
615
  }
613
616
  catch (error) {
614
617
  const message = error instanceof Error ? error.message : String(error);
@@ -762,6 +765,9 @@ function systemPromptFromLaunchSettings(launchSettings) {
762
765
  type: "preset",
763
766
  preset: "claude_code",
764
767
  append: appendLines.join(" "),
768
+ // Keep the prompt prefix stable across resume. Updated host language or guard text
769
+ // intentionally takes effect after SDK compaction or in a new session.
770
+ snapshot: true,
765
771
  };
766
772
  }
767
773
  export function buildQueryOptions(params) {
@@ -10,14 +10,15 @@ export class AsyncQueue {
10
10
  closed = false;
11
11
  enqueue(item) {
12
12
  if (this.closed) {
13
- return;
13
+ return false;
14
14
  }
15
15
  const waiter = this.waiters.shift();
16
16
  if (waiter) {
17
17
  waiter({ value: item, done: false });
18
- return;
18
+ return true;
19
19
  }
20
20
  this.items.push(item);
21
+ return true;
21
22
  }
22
23
  close() {
23
24
  if (this.closed) {
@@ -844,6 +844,7 @@ function lifecycleMetadata(msg) {
844
844
  "total_paused_ms",
845
845
  "blocked",
846
846
  "parent_agent_id",
847
+ "ambient",
847
848
  ]) {
848
849
  copyValue(msg, key);
849
850
  copyValue(patch, key);
@@ -912,6 +913,7 @@ export function applyBackgroundTasksChanged(session, msg) {
912
913
  }
913
914
  const taskType = nonEmptyString(task.task_type);
914
915
  const description = nonEmptyString(task.description);
916
+ const ambient = typeof task.ambient === "boolean" ? task.ambient : undefined;
915
917
  return {
916
918
  task_id: taskId,
917
919
  subject: description ?? taskType ?? taskId,
@@ -922,6 +924,7 @@ export function applyBackgroundTasksChanged(session, msg) {
922
924
  metadata: {
923
925
  [BACKGROUND_TASK_METADATA_KEY]: true,
924
926
  ...(taskType ? { task_type: taskType } : {}),
927
+ ...(ambient !== undefined ? { ambient } : {}),
925
928
  },
926
929
  };
927
930
  })
@@ -4,6 +4,7 @@ import { asRecordOrNull } from "./shared.js";
4
4
  import { applyTaskToolResult } from "./tasks.js";
5
5
  import { activeTaskIdForToolUse, linkTaskToolUse, unlinkTaskToolUse, } from "./task_links.js";
6
6
  import { applyToolNonExecutionMetadata, backgroundToolLaunchTaskIdFromResult, buildToolResultFields, createToolCall, } from "./tooling.js";
7
+ import { appendResourceLinks } from "./resource_links.js";
7
8
  const TOOL_SUMMARY_TOOL_NAMES = new Set([
8
9
  "Agent",
9
10
  "Task",
@@ -62,6 +63,16 @@ export function toolAcceptsTaskLifecycle(base) {
62
63
  const baseToolName = toolName(base);
63
64
  return Boolean(baseToolName && TASK_LIFECYCLE_TOOL_NAMES.has(baseToolName));
64
65
  }
66
+ export function toolAcceptsTerminalTaskNotification(base) {
67
+ const baseToolName = toolName(base);
68
+ return Boolean(baseToolName &&
69
+ (TASK_LIFECYCLE_TOOL_NAMES.has(baseToolName) ||
70
+ baseToolName === "Bash" ||
71
+ baseToolName.startsWith("mcp__")));
72
+ }
73
+ export function toolPreservesTaskNotificationOutput(base) {
74
+ return toolName(base) === "Bash";
75
+ }
65
76
  export function defersTaskNotificationCompletion(base) {
66
77
  const baseToolName = toolName(base);
67
78
  return baseToolName === "Agent" || baseToolName === "Task";
@@ -353,6 +364,11 @@ export function emitToolResultUpdate(session, toolUseId, isError, rawContent, ra
353
364
  const base = session.toolCalls.get(toolUseId);
354
365
  const baseToolName = toolNameFromMeta(base?.meta) ?? "";
355
366
  const fields = buildToolResultFields(isError, rawContent, base, rawResult, taskTitleContext(session, baseToolName, asRecordOrNull(base?.raw_input) ?? {}));
367
+ const resultRecord = asRecordOrNull(rawResult);
368
+ const contentWithResourceLinks = appendResourceLinks(fields.content, resultRecord?.resourceLinks);
369
+ if (contentWithResourceLinks !== undefined) {
370
+ fields.content = contentWithResourceLinks;
371
+ }
356
372
  applyToolNonExecutionMetadata(fields, nonExecutionMetadata);
357
373
  if (!isError && !nonExecutionMetadata) {
358
374
  const taskId = backgroundToolLaunchTaskIdFromResult(baseToolName, rawResult, rawContent);
@@ -374,7 +390,7 @@ export function finalizeOpenToolCalls(session, status) {
374
390
  if (toolCall.status !== "pending" && toolCall.status !== "in_progress") {
375
391
  continue;
376
392
  }
377
- if (toolAcceptsTaskLifecycle(toolCall) &&
393
+ if (toolAcceptsTerminalTaskNotification(toolCall) &&
378
394
  activeTaskIdForToolUse(session, toolUseId)) {
379
395
  continue;
380
396
  }
@@ -843,49 +843,87 @@ function editDiffFromInput(rawInput) {
843
843
  },
844
844
  ];
845
845
  }
846
- function writeDiffFromResult(rawContent) {
847
- const candidates = Array.isArray(rawContent) ? rawContent : [rawContent];
846
+ function boundedDisplayText(value, maxCharacters = 240) {
847
+ return value.length > maxCharacters
848
+ ? `${value.slice(0, maxCharacters - 3)}...`
849
+ : value;
850
+ }
851
+ function writeResultPresentation(rawContent) {
852
+ const candidates = resultRecordCandidates(rawContent, undefined);
853
+ const parsed = parseJsonCandidate(rawContent);
854
+ if (parsed !== undefined) {
855
+ candidates.push(...resultRecordCandidates(parsed, undefined));
856
+ }
848
857
  for (const candidate of candidates) {
849
- const record = asRecordOrNull(candidate);
850
- if (!record) {
851
- continue;
852
- }
858
+ const record = candidate;
853
859
  const filePath = typeof record.filePath === "string"
854
860
  ? record.filePath
855
861
  : typeof record.file_path === "string"
856
862
  ? record.file_path
857
863
  : "";
858
- const content = typeof record.content === "string" ? record.content : "";
864
+ const content = typeof record.content === "string" ? record.content : undefined;
859
865
  const originalRaw = "originalFile" in record
860
866
  ? record.originalFile
861
867
  : "original_file" in record
862
868
  ? record.original_file
863
869
  : undefined;
864
- const gitDiff = asRecordOrNull(record.gitDiff);
870
+ const gitDiff = asRecordOrNull(record.gitDiff ?? record.git_diff);
865
871
  const repository = typeof gitDiff?.repository === "string" &&
866
872
  gitDiff.repository.trim().length > 0
867
873
  ? gitDiff.repository.trim()
868
874
  : undefined;
869
- if (!filePath || !content || originalRaw === undefined) {
875
+ if (!filePath || originalRaw === undefined) {
870
876
  continue;
871
877
  }
872
- const original = typeof originalRaw === "string"
873
- ? originalRaw
874
- : originalRaw === null
875
- ? ""
876
- : "";
877
- return [
878
- {
879
- type: "diff",
880
- old_path: filePath,
881
- new_path: filePath,
882
- old: original,
883
- new: content,
884
- ...(repository ? { repository } : {}),
885
- },
886
- ];
878
+ if (originalRaw === null) {
879
+ if (record.type === "create" && content !== undefined) {
880
+ return {
881
+ kind: "diff",
882
+ content: [
883
+ {
884
+ type: "diff",
885
+ old_path: filePath,
886
+ new_path: filePath,
887
+ old: "",
888
+ new: content,
889
+ ...(repository ? { repository } : {}),
890
+ },
891
+ ],
892
+ };
893
+ }
894
+ return {
895
+ kind: "text",
896
+ text: record.type === "create"
897
+ ? `Created ${boundedDisplayText(filePath)}; the written content was unavailable, so a diff could not be displayed.`
898
+ : `Updated ${boundedDisplayText(filePath)}; the previous content was unavailable, so a diff could not be displayed.`,
899
+ };
900
+ }
901
+ if (content === undefined) {
902
+ continue;
903
+ }
904
+ if (typeof originalRaw === "string") {
905
+ if (originalRaw === content) {
906
+ return {
907
+ kind: "text",
908
+ text: `No changes to ${boundedDisplayText(filePath)}.`,
909
+ };
910
+ }
911
+ return {
912
+ kind: "diff",
913
+ content: [
914
+ {
915
+ type: "diff",
916
+ old_path: filePath,
917
+ new_path: filePath,
918
+ old: originalRaw,
919
+ new: content,
920
+ ...(repository ? { repository } : {}),
921
+ },
922
+ ],
923
+ };
924
+ }
887
925
  }
888
- return [];
926
+ return undefined;
889
927
  }
890
928
  function editDiffFromResult(rawResult, rawInput) {
891
929
  const input = asRecordOrNull(rawInput);
@@ -1741,7 +1779,11 @@ function hasConcreteReplField(record) {
1741
1779
  "stderr" in record ||
1742
1780
  "registeredTools" in record ||
1743
1781
  "images" in record ||
1744
- "documents" in record);
1782
+ "documents" in record ||
1783
+ "imagesOmitted" in record ||
1784
+ "imagePagesFailed" in record ||
1785
+ "imagePagesFailedOmitted" in record ||
1786
+ "documentsOmitted" in record);
1745
1787
  }
1746
1788
  function isReplWrapperRecord(record) {
1747
1789
  const nestedResult = asRecordOrNull(record.result);
@@ -1801,6 +1843,46 @@ function replResultFields(toolName, rawResult, rawContent) {
1801
1843
  if (Array.isArray(candidate.documents) && candidate.documents.length > 0) {
1802
1844
  lines.push(`Documents: ${candidate.documents.length}`);
1803
1845
  }
1846
+ const imagesOmitted = nonNegativeInteger(candidate.imagesOmitted);
1847
+ if (imagesOmitted !== undefined && imagesOmitted > 0) {
1848
+ lines.push(`Images omitted: ${imagesOmitted}`);
1849
+ }
1850
+ const failedPages = Array.isArray(candidate.imagePagesFailed)
1851
+ ? candidate.imagePagesFailed
1852
+ : [];
1853
+ const failedPageLimit = 20;
1854
+ let renderedFailedPages = 0;
1855
+ let locallyOmittedFailedPages = 0;
1856
+ for (const value of failedPages) {
1857
+ const failedPage = asRecordOrNull(value);
1858
+ const page = nonNegativeInteger(failedPage?.page);
1859
+ if (!failedPage || page === undefined) {
1860
+ continue;
1861
+ }
1862
+ if (renderedFailedPages >= failedPageLimit) {
1863
+ locallyOmittedFailedPages += 1;
1864
+ continue;
1865
+ }
1866
+ const file = nonEmptyString(failedPage.file);
1867
+ const error = nonEmptyString(failedPage.error);
1868
+ const context = [
1869
+ file ? `file ${boundedDisplayText(file)}` : undefined,
1870
+ error ? boundedDisplayText(error) : undefined,
1871
+ ]
1872
+ .filter((value) => value !== undefined)
1873
+ .join(": ");
1874
+ lines.push(`Failed image page: ${page}${context ? ` (${context})` : ""}`);
1875
+ renderedFailedPages += 1;
1876
+ }
1877
+ const upstreamFailedPagesOmitted = nonNegativeInteger(candidate.imagePagesFailedOmitted) ?? 0;
1878
+ const failedPagesOmitted = upstreamFailedPagesOmitted + locallyOmittedFailedPages;
1879
+ if (failedPagesOmitted > 0) {
1880
+ lines.push(`Failed image pages omitted: ${failedPagesOmitted}`);
1881
+ }
1882
+ const documentsOmitted = nonNegativeInteger(candidate.documentsOmitted);
1883
+ if (documentsOmitted !== undefined && documentsOmitted > 0) {
1884
+ lines.push(`Documents omitted: ${documentsOmitted}`);
1885
+ }
1804
1886
  return { output: lines.length > 0 ? lines.join("\n") : undefined, failed };
1805
1887
  }
1806
1888
  return undefined;
@@ -1987,6 +2069,20 @@ function workflowResultFields(toolName, rawResult, rawContent) {
1987
2069
  return undefined;
1988
2070
  }
1989
2071
  function backgroundLaunchResultFields(toolName, rawResult, rawContent) {
2072
+ if (isShellToolName(toolName)) {
2073
+ const record = findShellResultRecord(rawResult, rawContent);
2074
+ const taskId = typeof record?.backgroundTaskId === "string"
2075
+ ? record.backgroundTaskId.trim()
2076
+ : "";
2077
+ if (record && taskId) {
2078
+ return {
2079
+ output: buildShellDisplayOutput(record),
2080
+ taskId,
2081
+ failed: false,
2082
+ keepRunning: true,
2083
+ };
2084
+ }
2085
+ }
1990
2086
  return (monitorResultFields(toolName, rawResult, rawContent) ??
1991
2087
  workflowResultFields(toolName, rawResult, rawContent));
1992
2088
  }
@@ -2300,9 +2396,16 @@ export function buildToolResultFields(isError, rawContent, base, rawResult, _con
2300
2396
  }
2301
2397
  }
2302
2398
  if (!isError && toolName === "Write") {
2303
- const structuredDiff = writeDiffFromResult(rawContent);
2304
- if (structuredDiff.length > 0) {
2305
- fields.content = structuredDiff;
2399
+ const presentation = writeResultPresentation(rawContent);
2400
+ if (presentation?.kind === "diff") {
2401
+ fields.content = presentation.content;
2402
+ return fields;
2403
+ }
2404
+ if (presentation?.kind === "text") {
2405
+ fields.raw_output = presentation.text;
2406
+ fields.content = [
2407
+ { type: "content", content: { type: "text", text: presentation.text } },
2408
+ ];
2306
2409
  return fields;
2307
2410
  }
2308
2411
  const inputDiff = writeDiffFromInput(base?.raw_input);
@@ -167,6 +167,7 @@ export function buildPromptUserMessage(command, sessionId) {
167
167
  }
168
168
  return {
169
169
  type: "user",
170
+ uuid: command.message_uuid,
170
171
  session_id: sessionId,
171
172
  parent_tool_use_id: null,
172
173
  origin: { kind: "human" },
@@ -194,7 +195,7 @@ export function emitAgentConfigOptionUpdate(sessionId, agent) {
194
195
  value: agent,
195
196
  });
196
197
  }
197
- const EXPECTED_AGENT_SDK_VERSION = "0.3.239";
198
+ const EXPECTED_AGENT_SDK_VERSION = "0.3.258";
198
199
  const require = createRequire(import.meta.url);
199
200
  export function resolveInstalledAgentSdkVersion() {
200
201
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-rust",
3
- "version": "0.14.5",
3
+ "version": "0.14.6",
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.239"
36
+ "@anthropic-ai/claude-agent-sdk": "0.3.258"
37
37
  },
38
38
  "optionalDependencies": {
39
- "@srothgan/claude-code-rust-darwin-arm64": "0.14.5",
40
- "@srothgan/claude-code-rust-darwin-x64": "0.14.5",
41
- "@srothgan/claude-code-rust-linux-x64-gnu": "0.14.5",
42
- "@srothgan/claude-code-rust-linux-arm64-gnu": "0.14.5",
43
- "@srothgan/claude-code-rust-win32-x64-msvc": "0.14.5",
44
- "@srothgan/claude-code-rust-win32-arm64-msvc": "0.14.5"
39
+ "@srothgan/claude-code-rust-darwin-arm64": "0.14.6",
40
+ "@srothgan/claude-code-rust-darwin-x64": "0.14.6",
41
+ "@srothgan/claude-code-rust-linux-x64-gnu": "0.14.6",
42
+ "@srothgan/claude-code-rust-linux-arm64-gnu": "0.14.6",
43
+ "@srothgan/claude-code-rust-win32-x64-msvc": "0.14.6",
44
+ "@srothgan/claude-code-rust-win32-arm64-msvc": "0.14.6"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=24"