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 +7 -7
- package/agent-sdk/dist/bridge/command_dispatch.js +6 -0
- package/agent-sdk/dist/bridge/command_session_control.js +29 -3
- package/agent-sdk/dist/bridge/command_session_data.js +1 -1
- package/agent-sdk/dist/bridge/commands.js +1 -0
- package/agent-sdk/dist/bridge/error_classification.js +1 -0
- package/agent-sdk/dist/bridge/history.js +1 -0
- package/agent-sdk/dist/bridge/logger.js +8 -0
- package/agent-sdk/dist/bridge/mcp.js +1 -0
- package/agent-sdk/dist/bridge/message_handlers.js +154 -3
- package/agent-sdk/dist/bridge/resource_links.js +102 -0
- package/agent-sdk/dist/bridge/session_lifecycle.js +17 -8
- package/agent-sdk/dist/bridge/shared.js +3 -2
- package/agent-sdk/dist/bridge/state_parsing.js +1 -0
- package/agent-sdk/dist/bridge/tasks.js +4 -0
- package/agent-sdk/dist/bridge/tool_calls.js +17 -1
- package/agent-sdk/dist/bridge/tooling.js +139 -31
- package/agent-sdk/dist/bridge.js +2 -1
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -8,12 +8,12 @@ A native Rust terminal interface for Claude Code. Drop-in replacement for Anthro
|
|
|
8
8
|
[](https://www.apache.org/licenses/LICENSE-2.0)
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
11
|
-
<img src="assets/
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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":
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
959
|
-
outcome: "
|
|
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
|
|
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) {
|
|
@@ -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 (
|
|
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
|
-
|
|
630
|
+
const seeded = artifactRead?.seeded === false ? false : undefined;
|
|
631
|
+
if (slug) {
|
|
631
632
|
const webFetchMetadata = {
|
|
632
|
-
artifact_read: {
|
|
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
|
|
842
|
-
|
|
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 =
|
|
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 ||
|
|
875
|
+
if (!filePath || originalRaw === undefined) {
|
|
865
876
|
continue;
|
|
866
877
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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
|
|
2299
|
-
if (
|
|
2300
|
-
fields.content =
|
|
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);
|
package/agent-sdk/dist/bridge.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
36
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.258"
|
|
37
37
|
},
|
|
38
38
|
"optionalDependencies": {
|
|
39
|
-
"@srothgan/claude-code-rust-darwin-arm64": "0.14.
|
|
40
|
-
"@srothgan/claude-code-rust-darwin-x64": "0.14.
|
|
41
|
-
"@srothgan/claude-code-rust-linux-x64-gnu": "0.14.
|
|
42
|
-
"@srothgan/claude-code-rust-linux-arm64-gnu": "0.14.
|
|
43
|
-
"@srothgan/claude-code-rust-win32-x64-msvc": "0.14.
|
|
44
|
-
"@srothgan/claude-code-rust-win32-arm64-msvc": "0.14.
|
|
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"
|