claude-code-rust 0.14.4 → 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
@@ -8,12 +8,12 @@ A native Rust terminal interface for Claude Code. Drop-in replacement for Anthro
8
8
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](https://www.apache.org/licenses/LICENSE-2.0)
9
9
 
10
10
  <p align="center">
11
- <img src="assets/banner.png" alt="Claude Code Rust running a Read tool call with syntax-highlighted output" width="900">
11
+ <img src="assets/demo.gif" alt="Claude Code Rust explaining the terminal problems its native Rust interface solves" width="900">
12
12
  </p>
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":
@@ -39,6 +39,7 @@ export function classifyTurnErrorKind(subtype, errors, assistantError) {
39
39
  return "plan_limit";
40
40
  case "authentication_failed":
41
41
  return "auth_required";
42
+ case "account_on_hold":
42
43
  case "oauth_org_not_allowed":
43
44
  return "account_access";
44
45
  case "model_not_found":
@@ -81,6 +81,7 @@ function taskSystemMetadata(msg, patch) {
81
81
  for (const key of [
82
82
  "error",
83
83
  "is_backgrounded",
84
+ "spawn_depth",
84
85
  "request_id",
85
86
  "subagent_type",
86
87
  "task_description",
@@ -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
@@ -64,6 +65,11 @@ function sdkTaskMetadata(msg) {
64
65
  const summary = status && typeof msg.summary === "string" && msg.summary.length > 0
65
66
  ? msg.summary
66
67
  : undefined;
68
+ const spawnDepth = typeof msg.spawn_depth === "number" &&
69
+ Number.isSafeInteger(msg.spawn_depth) &&
70
+ msg.spawn_depth > 0
71
+ ? msg.spawn_depth
72
+ : undefined;
67
73
  const taskMetadata = {
68
74
  ...(metadata.requestId ? { request_id: metadata.requestId } : {}),
69
75
  ...(metadata.subagentType ? { subagent_type: metadata.subagentType } : {}),
@@ -80,6 +86,11 @@ function sdkTaskMetadata(msg) {
80
86
  ...(summary ? { summary } : {}),
81
87
  ...(status ? { terminal_status: status } : {}),
82
88
  ...(typeof msg.blocked === "boolean" ? { blocked: msg.blocked } : {}),
89
+ ...(typeof msg.is_backgrounded === "boolean"
90
+ ? { is_backgrounded: msg.is_backgrounded }
91
+ : {}),
92
+ ...(typeof msg.ambient === "boolean" ? { ambient: msg.ambient } : {}),
93
+ ...(spawnDepth !== undefined ? { spawn_depth: spawnDepth } : {}),
83
94
  };
84
95
  return Object.keys(taskMetadata).length > 0 ? taskMetadata : undefined;
85
96
  }
@@ -89,6 +100,44 @@ function sdkMessageOriginKind(msg) {
89
100
  : null;
90
101
  return typeof origin?.kind === "string" ? origin.kind : undefined;
91
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
+ }
92
141
  function externalMessageUpdateFromSdkUser(msg) {
93
142
  const origin = asRecordOrNull(msg.origin);
94
143
  if (!origin) {
@@ -140,6 +189,9 @@ function externalMessageUpdateFromSdkUser(msg) {
140
189
  origin.verifiedPeerPid >= 0
141
190
  ? { verified_peer_pid: origin.verifiedPeerPid }
142
191
  : {}),
192
+ ...(origin.fromMode === "bypass" || origin.fromMode === "prompting"
193
+ ? { from_mode: origin.fromMode }
194
+ : {}),
143
195
  },
144
196
  };
145
197
  }
@@ -168,6 +220,9 @@ function logSdkMessageOrigin(session, msg) {
168
220
  origin_verified_peer_pid: typeof origin?.verifiedPeerPid === "number"
169
221
  ? origin.verifiedPeerPid
170
222
  : undefined,
223
+ origin_from_mode: origin?.fromMode === "bypass" || origin?.fromMode === "prompting"
224
+ ? origin.fromMode
225
+ : undefined,
171
226
  },
172
227
  });
173
228
  }
@@ -475,7 +530,10 @@ export function handleTaskSystemMessage(session, subtype, msg) {
475
530
  return true;
476
531
  }
477
532
  const toolCall = ensureToolCallVisible(session, toolUseId, "Agent", {});
478
- if (!toolAcceptsTaskLifecycle(toolCall)) {
533
+ const acceptsLifecycle = toolAcceptsTaskLifecycle(toolCall);
534
+ const acceptsTerminalNotification = subtype === "task_notification" &&
535
+ toolAcceptsTerminalTaskNotification(toolCall);
536
+ if (!acceptsLifecycle && !acceptsTerminalNotification) {
479
537
  if (taskId) {
480
538
  unlinkTaskToolUse(session, taskId);
481
539
  }
@@ -561,12 +619,16 @@ export function handleTaskSystemMessage(session, subtype, msg) {
561
619
  if (messageTaskMetadata) {
562
620
  fields.task_metadata = messageTaskMetadata;
563
621
  }
564
- if (summary) {
622
+ if (summary && !toolPreservesTaskNotificationOutput(toolCall)) {
565
623
  fields.raw_output = summary;
566
624
  fields.content = [
567
625
  { type: "content", content: { type: "text", text: summary } },
568
626
  ];
569
627
  }
628
+ const contentWithResourceLinks = appendResourceLinks(fields.content, msg.resource_links);
629
+ if (contentWithResourceLinks !== undefined) {
630
+ fields.content = contentWithResourceLinks;
631
+ }
570
632
  if (Object.keys(fields).length > 0) {
571
633
  emitToolCallUpdate(session, toolUseId, fields, "task_notification");
572
634
  }
@@ -850,6 +912,27 @@ export function handleAssistantMessage(session, message) {
850
912
  const content = Array.isArray(messageObject.content)
851
913
  ? messageObject.content
852
914
  : [];
915
+ if (asRecordOrNull(message.context_usage)) {
916
+ const markdown = content
917
+ .flatMap((block) => {
918
+ const record = asRecordOrNull(block);
919
+ return record?.type === "text" &&
920
+ typeof record.text === "string" &&
921
+ record.text.trim().length > 0
922
+ ? [record.text]
923
+ : [];
924
+ })
925
+ .join("\n\n");
926
+ if (markdown.length > 0) {
927
+ emitSessionUpdate(session.sessionId, {
928
+ type: "agent_message_chunk",
929
+ content: { type: "text", text: markdown },
930
+ ...(assistantMessageUuid
931
+ ? { source_message_uuid: assistantMessageUuid }
932
+ : {}),
933
+ });
934
+ }
935
+ }
853
936
  for (const block of content) {
854
937
  if (!block || typeof block !== "object") {
855
938
  continue;
@@ -872,6 +955,40 @@ export function handleAssistantMessage(session, message) {
872
955
  }
873
956
  }
874
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
+ }
875
992
  function messageToolUseResult(message) {
876
993
  if (Object.hasOwn(message, "toolUseResult")) {
877
994
  return message.toolUseResult;
@@ -924,8 +1041,13 @@ export function handleUserToolResultBlocks(session, message) {
924
1041
  return handled;
925
1042
  }
926
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
+ }
927
1048
  emitFastModeUpdateIfChanged(session, message.fast_mode_state, message.fast_mode_disabled_reason);
928
1049
  const terminalReason = terminalReasonFromValue(message.terminal_reason);
1050
+ const queuedTurnCount = nonNegativeIntegerField(message, "queued_turn_count");
929
1051
  const subtype = typeof message.subtype === "string" ? message.subtype : "";
930
1052
  if (subtype === "success") {
931
1053
  session.lastAssistantError = undefined;
@@ -933,6 +1055,9 @@ export function handleResultMessage(session, message) {
933
1055
  writeEvent({
934
1056
  event: "turn_complete",
935
1057
  session_id: session.sessionId,
1058
+ ...(queuedTurnCount !== undefined
1059
+ ? { queued_turn_count: queuedTurnCount }
1060
+ : {}),
936
1061
  ...(terminalReason ? { terminal_reason: terminalReason } : {}),
937
1062
  });
938
1063
  return;
@@ -977,6 +1102,9 @@ export function handleResultMessage(session, message) {
977
1102
  event: "turn_error",
978
1103
  session_id: session.sessionId,
979
1104
  message: errors.length > 0 ? errors.join("\n") : fallback,
1105
+ ...(queuedTurnCount !== undefined
1106
+ ? { queued_turn_count: queuedTurnCount }
1107
+ : {}),
980
1108
  error_kind: errorKind,
981
1109
  ...(subtype ? { sdk_result_subtype: subtype } : {}),
982
1110
  ...(assistantError ? { assistant_error: assistantError } : {}),
@@ -1370,6 +1498,10 @@ export function handleSdkMessage(session, message) {
1370
1498
  return;
1371
1499
  }
1372
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
+ }
1373
1505
  if (msg.event && typeof msg.event === "object") {
1374
1506
  const parentToolUseId = typeof msg.parent_tool_use_id === "string"
1375
1507
  ? msg.parent_tool_use_id
@@ -1544,10 +1676,29 @@ export function handleSdkMessage(session, message) {
1544
1676
  if (msg.error === "authentication_failed") {
1545
1677
  emitAuthRequired(session);
1546
1678
  }
1679
+ if (msg.parent_tool_use_id === null ||
1680
+ msg.parent_tool_use_id === undefined) {
1681
+ emitUserMessageStarted(session, msg, "assistant");
1682
+ }
1547
1683
  handleAssistantMessage(session, msg);
1548
1684
  return;
1549
1685
  }
1550
1686
  if (type === "result") {
1551
1687
  handleResultMessage(session, msg);
1688
+ return;
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;
1552
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
+ });
1553
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) {
@@ -802,11 +808,15 @@ export function buildQueryOptions(params) {
802
808
  },
803
809
  spawnClaudeCodeProcess: (options) => {
804
810
  const command = resolveClaudeCodeSpawnCommand(options.command);
805
- const spawnOptions = { ...options, command };
811
+ const env = { ...options.env };
812
+ if (env.CLAUDE_CODE_ENABLE_TODO_TOOLS === undefined) {
813
+ env.CLAUDE_CODE_ENABLE_TODO_TOOLS = "1";
814
+ }
815
+ const spawnOptions = { ...options, command, env };
806
816
  logSdkProcessSpawnStarted(spawnOptions, params.enableSpawnDebug);
807
817
  const child = spawnChild(command, options.args, {
808
818
  cwd: options.cwd,
809
- env: options.env,
819
+ env,
810
820
  signal: options.signal,
811
821
  stdio: ["pipe", "pipe", "pipe"],
812
822
  windowsHide: true,
@@ -945,8 +955,7 @@ export function buildQueryOptions(params) {
945
955
  // the CLI emits `request_user_dialog` and the host renders the chooser below.
946
956
  supportedDialogKinds: [REFUSAL_FALLBACK_DIALOG_KIND],
947
957
  // Host policy for `request_user_dialog` control requests. Unknown kinds are
948
- // logged and answered `{ behavior: "cancelled" }` (the spec-required answer
949
- // for unrecognized kinds). For `refusal_fallback_prompt`, we surface an
958
+ // logged and left unsettled by returning `null`. For `refusal_fallback_prompt`, we surface an
950
959
  // interactive chooser in the TUI and route the user's decision back to the
951
960
  // CLI as `{ behavior: "completed", result: <choice> }` (or cancelled on
952
961
  // decline/abort/teardown).
@@ -955,15 +964,15 @@ export function buildQueryOptions(params) {
955
964
  bridgeLogger.warn({
956
965
  target: LOG_TARGETS.APP_SESSION,
957
966
  eventName: "user_dialog_received",
958
- message: "request_user_dialog received for unknown kind; cancelled",
959
- outcome: "cancelled",
967
+ message: "request_user_dialog received for unknown kind; unsupported",
968
+ outcome: "unsupported",
960
969
  sessionId: params.sessionIdForLogs(),
961
970
  ...(typeof request.toolUseID === "string"
962
971
  ? { toolCallId: request.toolUseID }
963
972
  : {}),
964
973
  fields: { dialog_kind: request.dialogKind },
965
974
  });
966
- return { behavior: "cancelled" };
975
+ return null;
967
976
  }
968
977
  const payload = normalizeRefusalFallbackPayload(request.payload);
969
978
  const requestId = options.requestId;
@@ -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) {
@@ -77,6 +77,7 @@ export function parseRuntimeSessionState(value) {
77
77
  export function parseApiRetryError(value) {
78
78
  switch (value) {
79
79
  case "authentication_failed":
80
+ case "account_on_hold":
80
81
  case "oauth_org_not_allowed":
81
82
  case "billing_error":
82
83
  case "rate_limit":
@@ -831,6 +831,7 @@ function lifecycleMetadata(msg) {
831
831
  for (const key of [
832
832
  "error",
833
833
  "is_backgrounded",
834
+ "spawn_depth",
834
835
  "request_id",
835
836
  "subagent_type",
836
837
  "task_description",
@@ -843,6 +844,7 @@ function lifecycleMetadata(msg) {
843
844
  "total_paused_ms",
844
845
  "blocked",
845
846
  "parent_agent_id",
847
+ "ambient",
846
848
  ]) {
847
849
  copyValue(msg, key);
848
850
  copyValue(patch, key);
@@ -911,6 +913,7 @@ export function applyBackgroundTasksChanged(session, msg) {
911
913
  }
912
914
  const taskType = nonEmptyString(task.task_type);
913
915
  const description = nonEmptyString(task.description);
916
+ const ambient = typeof task.ambient === "boolean" ? task.ambient : undefined;
914
917
  return {
915
918
  task_id: taskId,
916
919
  subject: description ?? taskType ?? taskId,
@@ -921,6 +924,7 @@ export function applyBackgroundTasksChanged(session, msg) {
921
924
  metadata: {
922
925
  [BACKGROUND_TASK_METADATA_KEY]: true,
923
926
  ...(taskType ? { task_type: taskType } : {}),
927
+ ...(ambient !== undefined ? { ambient } : {}),
924
928
  },
925
929
  };
926
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
  }
@@ -627,9 +627,14 @@ function extractToolOutputMetadata(toolName, rawResult, rawContent) {
627
627
  const artifactRead = asRecordOrNull(candidate.artifactRead);
628
628
  const slug = nonEmptyString(artifactRead?.slug);
629
629
  const ver = nonEmptyString(artifactRead?.ver);
630
- if (slug && ver) {
630
+ const seeded = artifactRead?.seeded === false ? false : undefined;
631
+ if (slug) {
631
632
  const webFetchMetadata = {
632
- artifact_read: { slug, ver },
633
+ artifact_read: {
634
+ slug,
635
+ ...(ver ? { ver } : {}),
636
+ ...(seeded === false ? { seeded } : {}),
637
+ },
633
638
  };
634
639
  metadata.web_fetch = webFetchMetadata;
635
640
  break;
@@ -838,49 +843,87 @@ function editDiffFromInput(rawInput) {
838
843
  },
839
844
  ];
840
845
  }
841
- function writeDiffFromResult(rawContent) {
842
- 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
+ }
843
857
  for (const candidate of candidates) {
844
- const record = asRecordOrNull(candidate);
845
- if (!record) {
846
- continue;
847
- }
858
+ const record = candidate;
848
859
  const filePath = typeof record.filePath === "string"
849
860
  ? record.filePath
850
861
  : typeof record.file_path === "string"
851
862
  ? record.file_path
852
863
  : "";
853
- const content = typeof record.content === "string" ? record.content : "";
864
+ const content = typeof record.content === "string" ? record.content : undefined;
854
865
  const originalRaw = "originalFile" in record
855
866
  ? record.originalFile
856
867
  : "original_file" in record
857
868
  ? record.original_file
858
869
  : undefined;
859
- const gitDiff = asRecordOrNull(record.gitDiff);
870
+ const gitDiff = asRecordOrNull(record.gitDiff ?? record.git_diff);
860
871
  const repository = typeof gitDiff?.repository === "string" &&
861
872
  gitDiff.repository.trim().length > 0
862
873
  ? gitDiff.repository.trim()
863
874
  : undefined;
864
- if (!filePath || !content || originalRaw === undefined) {
875
+ if (!filePath || originalRaw === undefined) {
865
876
  continue;
866
877
  }
867
- const original = typeof originalRaw === "string"
868
- ? originalRaw
869
- : originalRaw === null
870
- ? ""
871
- : "";
872
- return [
873
- {
874
- type: "diff",
875
- old_path: filePath,
876
- new_path: filePath,
877
- old: original,
878
- new: content,
879
- ...(repository ? { repository } : {}),
880
- },
881
- ];
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
+ }
882
925
  }
883
- return [];
926
+ return undefined;
884
927
  }
885
928
  function editDiffFromResult(rawResult, rawInput) {
886
929
  const input = asRecordOrNull(rawInput);
@@ -1736,7 +1779,11 @@ function hasConcreteReplField(record) {
1736
1779
  "stderr" in record ||
1737
1780
  "registeredTools" in record ||
1738
1781
  "images" in record ||
1739
- "documents" in record);
1782
+ "documents" in record ||
1783
+ "imagesOmitted" in record ||
1784
+ "imagePagesFailed" in record ||
1785
+ "imagePagesFailedOmitted" in record ||
1786
+ "documentsOmitted" in record);
1740
1787
  }
1741
1788
  function isReplWrapperRecord(record) {
1742
1789
  const nestedResult = asRecordOrNull(record.result);
@@ -1796,6 +1843,46 @@ function replResultFields(toolName, rawResult, rawContent) {
1796
1843
  if (Array.isArray(candidate.documents) && candidate.documents.length > 0) {
1797
1844
  lines.push(`Documents: ${candidate.documents.length}`);
1798
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
+ }
1799
1886
  return { output: lines.length > 0 ? lines.join("\n") : undefined, failed };
1800
1887
  }
1801
1888
  return undefined;
@@ -1982,6 +2069,20 @@ function workflowResultFields(toolName, rawResult, rawContent) {
1982
2069
  return undefined;
1983
2070
  }
1984
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
+ }
1985
2086
  return (monitorResultFields(toolName, rawResult, rawContent) ??
1986
2087
  workflowResultFields(toolName, rawResult, rawContent));
1987
2088
  }
@@ -2295,9 +2396,16 @@ export function buildToolResultFields(isError, rawContent, base, rawResult, _con
2295
2396
  }
2296
2397
  }
2297
2398
  if (!isError && toolName === "Write") {
2298
- const structuredDiff = writeDiffFromResult(rawContent);
2299
- if (structuredDiff.length > 0) {
2300
- 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
+ ];
2301
2409
  return fields;
2302
2410
  }
2303
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.227";
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.4",
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.227"
36
+ "@anthropic-ai/claude-agent-sdk": "0.3.258"
37
37
  },
38
38
  "optionalDependencies": {
39
- "@srothgan/claude-code-rust-darwin-arm64": "0.14.4",
40
- "@srothgan/claude-code-rust-darwin-x64": "0.14.4",
41
- "@srothgan/claude-code-rust-linux-x64-gnu": "0.14.4",
42
- "@srothgan/claude-code-rust-linux-arm64-gnu": "0.14.4",
43
- "@srothgan/claude-code-rust-win32-x64-msvc": "0.14.4",
44
- "@srothgan/claude-code-rust-win32-arm64-msvc": "0.14.4"
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"