claude-code-rust 0.12.4 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -7
- package/agent-sdk/dist/bridge/mcp_metadata.js +35 -0
- package/agent-sdk/dist/bridge/model_metadata.js +30 -20
- package/agent-sdk/dist/bridge/session_lifecycle.js +3 -2
- package/agent-sdk/dist/bridge/user_interaction.js +52 -1
- package/agent-sdk/dist/bridge.js +1 -1
- package/agent-sdk/dist/bridge.test.js +134 -3
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -9,6 +9,10 @@ A native Rust terminal interface for Claude Code. Drop-in replacement for Anthro
|
|
|
9
9
|
[](https://www.apache.org/licenses/LICENSE-2.0)
|
|
10
10
|
[](https://nodejs.org/)
|
|
11
11
|
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="assets/banner.png" alt="Claude Code Rust running a Read tool call with syntax-highlighted output" width="900">
|
|
14
|
+
</p>
|
|
15
|
+
|
|
12
16
|
## About
|
|
13
17
|
|
|
14
18
|
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.
|
|
@@ -43,15 +47,17 @@ Full documentation is available at [srothgan.github.io/claude-code-rust](https:/
|
|
|
43
47
|
|
|
44
48
|
## Why
|
|
45
49
|
|
|
46
|
-
The stock Claude Code TUI runs on Node.js with React Ink. This causes real problems:
|
|
50
|
+
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:
|
|
47
51
|
|
|
48
|
-
- **
|
|
49
|
-
- **
|
|
50
|
-
- **
|
|
51
|
-
- **
|
|
52
|
-
- **
|
|
52
|
+
- **Flickering**: The whole view is redrawn on every status update, causing constant flicker — bad enough to crash editors' integrated terminals during long sessions
|
|
53
|
+
- **CPU**: Sustained high CPU even when idle, and runaway loops that spawn multiple background processes
|
|
54
|
+
- **Memory**: 200-400MB baseline (and climbing with conversation length) vs ~20-50MB for a native binary
|
|
55
|
+
- **Resize**: Window resizing leaves duplicated frames in scrollback, loses lines when shrinking, and can garble the display
|
|
56
|
+
- **Input latency**: Keystrokes echo with visible delay as context fills up, and noticeably worse on Windows
|
|
57
|
+
- **Scrollback**: Hijacks the terminal's native scrollback, erasing history you can no longer scroll back to
|
|
58
|
+
- **Paste**: Large pastes can flood stdout and freeze the terminal
|
|
53
59
|
|
|
54
|
-
Claude Code Rust
|
|
60
|
+
Claude Code Rust addresses these by compiling to a single native binary with diffed, direct terminal control via Crossterm and Ratatui — no full-frame redraws, no Node runtime overhead.
|
|
55
61
|
|
|
56
62
|
## Documentation
|
|
57
63
|
|
|
@@ -48,6 +48,16 @@ function optionalTimeout(record, context) {
|
|
|
48
48
|
}
|
|
49
49
|
return value;
|
|
50
50
|
}
|
|
51
|
+
function optionalRequestTimeoutMs(record, context) {
|
|
52
|
+
const value = record.request_timeout_ms;
|
|
53
|
+
if (value === undefined) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < 1000) {
|
|
57
|
+
throw new Error(`${context}.request_timeout_ms must be an integer >= 1000`);
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
51
61
|
function optionalAlwaysLoad(record, context) {
|
|
52
62
|
const value = record.always_load;
|
|
53
63
|
if (value === undefined) {
|
|
@@ -98,6 +108,7 @@ export function parseMcpServerConfig(value, context) {
|
|
|
98
108
|
throw new Error(`${context}.type must be a string`);
|
|
99
109
|
}
|
|
100
110
|
const timeout = optionalTimeout(record, context);
|
|
111
|
+
const requestTimeoutMs = optionalRequestTimeoutMs(record, context);
|
|
101
112
|
const alwaysLoad = optionalAlwaysLoad(record, context);
|
|
102
113
|
switch (type) {
|
|
103
114
|
case "stdio": {
|
|
@@ -114,6 +125,7 @@ export function parseMcpServerConfig(value, context) {
|
|
|
114
125
|
...(optionalStringArray(record, "args", context) ? { args: optionalStringArray(record, "args", context) } : {}),
|
|
115
126
|
...(optionalStringMap(record, "env", context) ? { env: optionalStringMap(record, "env", context) } : {}),
|
|
116
127
|
...(timeout === undefined ? {} : { timeout }),
|
|
128
|
+
...(requestTimeoutMs === undefined ? {} : { request_timeout_ms: requestTimeoutMs }),
|
|
117
129
|
...(alwaysLoad === undefined ? {} : { always_load: alwaysLoad }),
|
|
118
130
|
};
|
|
119
131
|
}
|
|
@@ -130,6 +142,7 @@ export function parseMcpServerConfig(value, context) {
|
|
|
130
142
|
...(optionalStringMap(record, "headers", context) ? { headers: optionalStringMap(record, "headers", context) } : {}),
|
|
131
143
|
...(tools === undefined ? {} : { tools }),
|
|
132
144
|
...(timeout === undefined ? {} : { timeout }),
|
|
145
|
+
...(requestTimeoutMs === undefined ? {} : { request_timeout_ms: requestTimeoutMs }),
|
|
133
146
|
...(alwaysLoad === undefined ? {} : { always_load: alwaysLoad }),
|
|
134
147
|
};
|
|
135
148
|
}
|
|
@@ -148,6 +161,9 @@ function toSdkToolPolicies(tools) {
|
|
|
148
161
|
...(tool.org_max_permission === undefined ? {} : { org_max_permission: tool.org_max_permission }),
|
|
149
162
|
}));
|
|
150
163
|
}
|
|
164
|
+
function sdkRequestTimeoutConfig(config) {
|
|
165
|
+
return config.request_timeout_ms === undefined ? {} : { requestTimeoutMs: config.request_timeout_ms };
|
|
166
|
+
}
|
|
151
167
|
export function bridgeMcpConfigToSdk(config) {
|
|
152
168
|
switch (config.type) {
|
|
153
169
|
case "stdio":
|
|
@@ -157,6 +173,7 @@ export function bridgeMcpConfigToSdk(config) {
|
|
|
157
173
|
...(config.args ? { args: config.args } : {}),
|
|
158
174
|
...(config.env ? { env: config.env } : {}),
|
|
159
175
|
...(config.timeout === undefined ? {} : { timeout: config.timeout }),
|
|
176
|
+
...sdkRequestTimeoutConfig(config),
|
|
160
177
|
...(config.always_load === undefined ? {} : { alwaysLoad: config.always_load }),
|
|
161
178
|
};
|
|
162
179
|
case "sse":
|
|
@@ -166,6 +183,7 @@ export function bridgeMcpConfigToSdk(config) {
|
|
|
166
183
|
...(config.headers ? { headers: config.headers } : {}),
|
|
167
184
|
...(config.tools ? { tools: toSdkToolPolicies(config.tools) } : {}),
|
|
168
185
|
...(config.timeout === undefined ? {} : { timeout: config.timeout }),
|
|
186
|
+
...sdkRequestTimeoutConfig(config),
|
|
169
187
|
...(config.always_load === undefined ? {} : { alwaysLoad: config.always_load }),
|
|
170
188
|
};
|
|
171
189
|
case "http":
|
|
@@ -175,6 +193,7 @@ export function bridgeMcpConfigToSdk(config) {
|
|
|
175
193
|
...(config.headers ? { headers: config.headers } : {}),
|
|
176
194
|
...(config.tools ? { tools: toSdkToolPolicies(config.tools) } : {}),
|
|
177
195
|
...(config.timeout === undefined ? {} : { timeout: config.timeout }),
|
|
196
|
+
...sdkRequestTimeoutConfig(config),
|
|
178
197
|
...(config.always_load === undefined ? {} : { alwaysLoad: config.always_load }),
|
|
179
198
|
};
|
|
180
199
|
}
|
|
@@ -250,6 +269,16 @@ export function mapMcpServerStatus(status) {
|
|
|
250
269
|
: [],
|
|
251
270
|
};
|
|
252
271
|
}
|
|
272
|
+
function sdkRequestTimeoutMs(config) {
|
|
273
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
const raw = config;
|
|
277
|
+
const value = raw.requestTimeoutMs ?? raw.request_timeout_ms;
|
|
278
|
+
return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value)
|
|
279
|
+
? value
|
|
280
|
+
: undefined;
|
|
281
|
+
}
|
|
253
282
|
export function mapMcpServerStatusConfig(config) {
|
|
254
283
|
switch (config.type) {
|
|
255
284
|
case "stdio":
|
|
@@ -259,6 +288,7 @@ export function mapMcpServerStatusConfig(config) {
|
|
|
259
288
|
...(Array.isArray(config.args) && config.args.length > 0 ? { args: config.args } : {}),
|
|
260
289
|
...(config.env ? { env: config.env } : {}),
|
|
261
290
|
...(config.timeout === undefined ? {} : { timeout: config.timeout }),
|
|
291
|
+
...(sdkRequestTimeoutMs(config) === undefined ? {} : { request_timeout_ms: sdkRequestTimeoutMs(config) }),
|
|
262
292
|
...(config.alwaysLoad === undefined ? {} : { always_load: config.alwaysLoad }),
|
|
263
293
|
};
|
|
264
294
|
case "sse": {
|
|
@@ -269,6 +299,7 @@ export function mapMcpServerStatusConfig(config) {
|
|
|
269
299
|
...(config.headers ? { headers: config.headers } : {}),
|
|
270
300
|
...(tools === undefined ? {} : { tools }),
|
|
271
301
|
...(config.timeout === undefined ? {} : { timeout: config.timeout }),
|
|
302
|
+
...(sdkRequestTimeoutMs(config) === undefined ? {} : { request_timeout_ms: sdkRequestTimeoutMs(config) }),
|
|
272
303
|
...(config.alwaysLoad === undefined ? {} : { always_load: config.alwaysLoad }),
|
|
273
304
|
};
|
|
274
305
|
}
|
|
@@ -280,6 +311,7 @@ export function mapMcpServerStatusConfig(config) {
|
|
|
280
311
|
...(config.headers ? { headers: config.headers } : {}),
|
|
281
312
|
...(tools === undefined ? {} : { tools }),
|
|
282
313
|
...(config.timeout === undefined ? {} : { timeout: config.timeout }),
|
|
314
|
+
...(sdkRequestTimeoutMs(config) === undefined ? {} : { request_timeout_ms: sdkRequestTimeoutMs(config) }),
|
|
283
315
|
...(config.alwaysLoad === undefined ? {} : { always_load: config.alwaysLoad }),
|
|
284
316
|
};
|
|
285
317
|
}
|
|
@@ -321,6 +353,7 @@ function mcpStatusConfigDiagnostics(config) {
|
|
|
321
353
|
return {
|
|
322
354
|
config_type: "stdio",
|
|
323
355
|
...(config.timeout === undefined ? {} : { timeout_ms: config.timeout }),
|
|
356
|
+
...(config.request_timeout_ms === undefined ? {} : { request_timeout_ms: config.request_timeout_ms }),
|
|
324
357
|
...(config.always_load === undefined ? {} : { always_load: config.always_load }),
|
|
325
358
|
configured_tool_policy_count: 0,
|
|
326
359
|
};
|
|
@@ -329,6 +362,7 @@ function mcpStatusConfigDiagnostics(config) {
|
|
|
329
362
|
return {
|
|
330
363
|
config_type: config.type,
|
|
331
364
|
...(config.timeout === undefined ? {} : { timeout_ms: config.timeout }),
|
|
365
|
+
...(config.request_timeout_ms === undefined ? {} : { request_timeout_ms: config.request_timeout_ms }),
|
|
332
366
|
...(config.always_load === undefined ? {} : { always_load: config.always_load }),
|
|
333
367
|
configured_tool_policy_count: config.tools?.length ?? 0,
|
|
334
368
|
};
|
|
@@ -359,6 +393,7 @@ export function summarizeMcpServersForDiagnostics(servers) {
|
|
|
359
393
|
config_type: config.config_type,
|
|
360
394
|
...(server.scope ? { scope: server.scope } : {}),
|
|
361
395
|
...(config.timeout_ms === undefined ? {} : { timeout_ms: config.timeout_ms }),
|
|
396
|
+
...(config.request_timeout_ms === undefined ? {} : { request_timeout_ms: config.request_timeout_ms }),
|
|
362
397
|
...(config.always_load === undefined ? {} : { always_load: config.always_load }),
|
|
363
398
|
tool_count: server.tools.length,
|
|
364
399
|
configured_tool_policy_count: config.configured_tool_policy_count,
|
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
const
|
|
1
|
+
const DEFAULT_MODEL_ALIAS = "fable";
|
|
2
2
|
const MAX_MODEL_VERSION_PARTS = 2;
|
|
3
3
|
const RELEASE_BUILD_TOKEN = /^20\d{6}$/;
|
|
4
|
-
function isUnavailableModelId(id) {
|
|
5
|
-
const normalized = id.trim().toLowerCase();
|
|
6
|
-
// TODO: Revisit only after a product decision if Anthropic restores Fable 5 access.
|
|
7
|
-
return normalized === "fable" || normalized.startsWith("claude-fable-5");
|
|
8
|
-
}
|
|
9
4
|
function isEffortLevel(value) {
|
|
10
5
|
return (value === "low" ||
|
|
11
6
|
value === "medium" ||
|
|
@@ -19,7 +14,7 @@ function normalizeModelKey(id) {
|
|
|
19
14
|
return { original, family: "unknown", versionParts: [], variantParts: [], buildParts: [] };
|
|
20
15
|
}
|
|
21
16
|
const lower = original.toLowerCase();
|
|
22
|
-
const contextMatch = lower.match(/\[([
|
|
17
|
+
const contextMatch = lower.match(/\[([^[\]]+)\]$/);
|
|
23
18
|
const contextSuffix = contextMatch?.[1];
|
|
24
19
|
const withoutContext = contextMatch ? lower.slice(0, contextMatch.index) : lower;
|
|
25
20
|
const withoutPrefix = withoutContext.startsWith("claude-")
|
|
@@ -27,7 +22,10 @@ function normalizeModelKey(id) {
|
|
|
27
22
|
: withoutContext;
|
|
28
23
|
const parts = withoutPrefix.split("-").filter((part) => part.length > 0);
|
|
29
24
|
const familyPart = parts[0] ?? "";
|
|
30
|
-
const family = familyPart === "
|
|
25
|
+
const family = familyPart === "fable" ||
|
|
26
|
+
familyPart === "opus" ||
|
|
27
|
+
familyPart === "sonnet" ||
|
|
28
|
+
familyPart === "haiku"
|
|
31
29
|
? familyPart
|
|
32
30
|
: "unknown";
|
|
33
31
|
const versionParts = [];
|
|
@@ -36,6 +34,10 @@ function normalizeModelKey(id) {
|
|
|
36
34
|
if (family !== "unknown") {
|
|
37
35
|
for (const part of parts.slice(1)) {
|
|
38
36
|
if (/^\d+$/.test(part)) {
|
|
37
|
+
if (versionParts.length > 0 && RELEASE_BUILD_TOKEN.test(part)) {
|
|
38
|
+
buildParts.push(part);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
39
41
|
if (versionParts.length < MAX_MODEL_VERSION_PARTS) {
|
|
40
42
|
const parsed = Number.parseInt(part, 10);
|
|
41
43
|
if (Number.isFinite(parsed)) {
|
|
@@ -120,11 +122,13 @@ function humanizeModelId(id) {
|
|
|
120
122
|
if (normalized.family === "unknown") {
|
|
121
123
|
return id;
|
|
122
124
|
}
|
|
123
|
-
const familyLabel = normalized.family === "
|
|
124
|
-
? "
|
|
125
|
-
: normalized.family === "
|
|
126
|
-
? "
|
|
127
|
-
: "
|
|
125
|
+
const familyLabel = normalized.family === "fable"
|
|
126
|
+
? "Fable"
|
|
127
|
+
: normalized.family === "opus"
|
|
128
|
+
? "Opus"
|
|
129
|
+
: normalized.family === "sonnet"
|
|
130
|
+
? "Sonnet"
|
|
131
|
+
: "Haiku";
|
|
128
132
|
const versionLabel = normalized.versionParts.length > 0 ? ` ${normalized.versionParts.join(".")}` : "";
|
|
129
133
|
const contextLabel = normalized.contextSuffix?.toLowerCase() === "1m"
|
|
130
134
|
? " [1M]"
|
|
@@ -144,19 +148,23 @@ function currentModelIsAuthoritative(resolvedId, requestedId) {
|
|
|
144
148
|
return true;
|
|
145
149
|
}
|
|
146
150
|
function resolveCatalogModel(availableModels, resolvedId, requestedId) {
|
|
147
|
-
const exactResolved = availableModels.find((entry) => entry.id === resolvedId);
|
|
151
|
+
const exactResolved = availableModels.find((entry) => entry.id === resolvedId || entry.resolved_model === resolvedId);
|
|
148
152
|
if (exactResolved) {
|
|
149
153
|
return exactResolved;
|
|
150
154
|
}
|
|
151
155
|
if (requestedId) {
|
|
152
|
-
const exactRequested = availableModels.find((entry) => entry.id === requestedId);
|
|
156
|
+
const exactRequested = availableModels.find((entry) => entry.id === requestedId || entry.resolved_model === requestedId);
|
|
153
157
|
if (exactRequested &&
|
|
154
|
-
modelKeysAreCompatible(exactRequested.id, resolvedId)
|
|
158
|
+
(modelKeysAreCompatible(exactRequested.id, resolvedId) ||
|
|
159
|
+
(exactRequested.resolved_model !== undefined &&
|
|
160
|
+
modelKeysAreCompatible(exactRequested.resolved_model, resolvedId))) &&
|
|
155
161
|
!hasVariantSiblingConflict(availableModels, exactRequested.id, resolvedId)) {
|
|
156
162
|
return exactRequested;
|
|
157
163
|
}
|
|
158
164
|
}
|
|
159
|
-
const compatible = availableModels.filter((entry) => modelKeysAreCompatible(entry.id, resolvedId)
|
|
165
|
+
const compatible = availableModels.filter((entry) => (modelKeysAreCompatible(entry.id, resolvedId) ||
|
|
166
|
+
(entry.resolved_model !== undefined &&
|
|
167
|
+
modelKeysAreCompatible(entry.resolved_model, resolvedId))) &&
|
|
160
168
|
!hasVariantSiblingConflict(availableModels, entry.id, resolvedId));
|
|
161
169
|
return compatible.length === 1 ? compatible[0] : undefined;
|
|
162
170
|
}
|
|
@@ -168,12 +176,14 @@ export function mapAvailableModels(models) {
|
|
|
168
176
|
.filter((entry) => {
|
|
169
177
|
return (typeof entry?.value === "string" &&
|
|
170
178
|
entry.value.trim().length > 0 &&
|
|
171
|
-
!isUnavailableModelId(entry.value) &&
|
|
172
179
|
typeof entry.displayName === "string" &&
|
|
173
180
|
entry.displayName.trim().length > 0);
|
|
174
181
|
})
|
|
175
182
|
.map((entry) => ({
|
|
176
183
|
id: entry.value,
|
|
184
|
+
...(typeof entry.resolvedModel === "string" && entry.resolvedModel.trim().length > 0
|
|
185
|
+
? { resolved_model: entry.resolvedModel.trim() }
|
|
186
|
+
: {}),
|
|
177
187
|
display_name: entry.displayName,
|
|
178
188
|
supports_effort: entry.supportsEffort === true,
|
|
179
189
|
supported_effort_levels: Array.isArray(entry.supportedEffortLevels)
|
|
@@ -198,9 +208,9 @@ export function resolveCurrentModel(session) {
|
|
|
198
208
|
const resolvedId = session.resolvedRuntimeModelId?.trim() ||
|
|
199
209
|
session.model.trim() ||
|
|
200
210
|
requestedId ||
|
|
201
|
-
|
|
211
|
+
DEFAULT_MODEL_ALIAS;
|
|
202
212
|
const catalogModel = resolveCatalogModel(session.availableModels, resolvedId, requestedId);
|
|
203
|
-
const runtimeDisplayId = resolvedId || requestedId ||
|
|
213
|
+
const runtimeDisplayId = resolvedId || requestedId || DEFAULT_MODEL_ALIAS;
|
|
204
214
|
const displayNameShort = shortDisplayNameForModelId(runtimeDisplayId);
|
|
205
215
|
const displayNameLong = catalogModel?.display_name ?? humanizeModelId(runtimeDisplayId);
|
|
206
216
|
return {
|
|
@@ -19,7 +19,7 @@ export { shouldEmitStartupAuthRequiredForAccount } from "./account_metadata.js";
|
|
|
19
19
|
const BRIDGE_RUNTIME_PROCESS_NAME = process.platform === "win32" ? "claude-rs-bridge-node.exe" : "claude-rs-bridge-node";
|
|
20
20
|
const BRIDGE_RUNTIME_GUARD_PROMPT = `Do not terminate the Claude Rust bridge runtime process \`${BRIDGE_RUNTIME_PROCESS_NAME}\`; ` +
|
|
21
21
|
"when cleaning up development servers, only stop processes by explicit PIDs you started in this session.";
|
|
22
|
-
const STARTUP_FALLBACK_MODEL_ALIAS = "
|
|
22
|
+
const STARTUP_FALLBACK_MODEL_ALIAS = "fable";
|
|
23
23
|
function permissionDisplayFromCanUseOptions(options) {
|
|
24
24
|
const title = typeof options.title === "string" ? options.title.trim() : "";
|
|
25
25
|
const displayName = typeof options.displayName === "string" ? options.displayName.trim() : "";
|
|
@@ -600,6 +600,7 @@ export function buildQueryOptions(params) {
|
|
|
600
600
|
const systemPrompt = systemPromptFromLaunchSettings(params.launchSettings);
|
|
601
601
|
const modelOption = startupModelOption(params.launchSettings);
|
|
602
602
|
const permissionModeOptions = startupPermissionModeOptions(params.launchSettings);
|
|
603
|
+
const shouldPassCanUseTool = permissionModeOptions.permissionMode !== "bypassPermissions";
|
|
603
604
|
const settings = normalizedSettingsFromLaunchSettings(params.launchSettings);
|
|
604
605
|
return {
|
|
605
606
|
cwd: params.cwd,
|
|
@@ -658,7 +659,7 @@ export function buildQueryOptions(params) {
|
|
|
658
659
|
settingSources: DEFAULT_SETTING_SOURCES,
|
|
659
660
|
resume: params.resume,
|
|
660
661
|
...(params.resumeSessionAt ? { resumeSessionAt: params.resumeSessionAt } : {}),
|
|
661
|
-
canUseTool: params.canUseTool,
|
|
662
|
+
...(shouldPassCanUseTool ? { canUseTool: params.canUseTool } : {}),
|
|
662
663
|
onElicitation: async (request) => {
|
|
663
664
|
const requestId = randomUUID();
|
|
664
665
|
const mode = request.mode === "form" || request.mode === "url"
|
|
@@ -142,6 +142,32 @@ function buildQuestionRequest(promptToolCall, prompt, index, total) {
|
|
|
142
142
|
function askUserQuestionTranscript(answers) {
|
|
143
143
|
return answers.map((entry) => `${entry.header}: ${entry.answer}\n ${entry.question}`).join("\n");
|
|
144
144
|
}
|
|
145
|
+
function askUserQuestionCompletedRawInput(prompts, answers, annotations, questionResults) {
|
|
146
|
+
return {
|
|
147
|
+
questions: prompts.map((prompt) => ({
|
|
148
|
+
question: prompt.question,
|
|
149
|
+
header: prompt.header,
|
|
150
|
+
multiSelect: prompt.multiSelect,
|
|
151
|
+
options: prompt.options.map((option) => ({
|
|
152
|
+
label: option.label,
|
|
153
|
+
description: option.description,
|
|
154
|
+
...(option.preview ? { preview: option.preview } : {}),
|
|
155
|
+
})),
|
|
156
|
+
})),
|
|
157
|
+
answers,
|
|
158
|
+
...(Object.keys(annotations).length > 0 ? { annotations: questionAnnotationsJson(annotations) } : {}),
|
|
159
|
+
question_results: questionResults,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function questionAnnotationsJson(annotations) {
|
|
163
|
+
return Object.fromEntries(Object.entries(annotations).map(([question, annotation]) => [
|
|
164
|
+
question,
|
|
165
|
+
{
|
|
166
|
+
...(annotation.preview ? { preview: annotation.preview } : {}),
|
|
167
|
+
...(annotation.notes ? { notes: annotation.notes } : {}),
|
|
168
|
+
},
|
|
169
|
+
]));
|
|
170
|
+
}
|
|
145
171
|
function deriveAnnotation(selectedOptions, annotation) {
|
|
146
172
|
const preview = annotation?.preview?.trim().length
|
|
147
173
|
? annotation.preview
|
|
@@ -166,6 +192,7 @@ export async function requestAskUserQuestionAnswers(session, toolUseId, inputDat
|
|
|
166
192
|
const answers = {};
|
|
167
193
|
const annotations = {};
|
|
168
194
|
const transcript = [];
|
|
195
|
+
const questionResults = [];
|
|
169
196
|
for (const [index, prompt] of prompts.entries()) {
|
|
170
197
|
const promptToolCall = askUserQuestionPromptToolCall(baseToolCall, prompt, index, prompts.length);
|
|
171
198
|
const fields = {
|
|
@@ -214,11 +241,35 @@ export async function requestAskUserQuestionAnswers(session, toolUseId, inputDat
|
|
|
214
241
|
annotations[prompt.question] = annotation;
|
|
215
242
|
}
|
|
216
243
|
transcript.push({ header: prompt.header, question: prompt.question, answer });
|
|
244
|
+
questionResults.push({
|
|
245
|
+
question: prompt.question,
|
|
246
|
+
header: prompt.header,
|
|
247
|
+
question_index: index,
|
|
248
|
+
total_questions: prompts.length,
|
|
249
|
+
selected_options: selectedOptions.map((option) => ({
|
|
250
|
+
option_id: option.option_id,
|
|
251
|
+
label: option.label,
|
|
252
|
+
...(option.description ? { description: option.description } : {}),
|
|
253
|
+
...(option.preview ? { preview: option.preview } : {}),
|
|
254
|
+
})),
|
|
255
|
+
...(annotation
|
|
256
|
+
? {
|
|
257
|
+
annotation: {
|
|
258
|
+
...(annotation.preview ? { preview: annotation.preview } : {}),
|
|
259
|
+
...(annotation.notes ? { notes: annotation.notes } : {}),
|
|
260
|
+
},
|
|
261
|
+
}
|
|
262
|
+
: {}),
|
|
263
|
+
});
|
|
217
264
|
const summary = askUserQuestionTranscript(transcript);
|
|
265
|
+
const completed = index + 1 >= prompts.length;
|
|
218
266
|
const progressFields = {
|
|
219
|
-
status:
|
|
267
|
+
status: completed ? "completed" : "in_progress",
|
|
220
268
|
raw_output: summary,
|
|
221
269
|
content: [{ type: "content", content: { type: "text", text: summary } }],
|
|
270
|
+
...(completed
|
|
271
|
+
? { raw_input: askUserQuestionCompletedRawInput(prompts, answers, annotations, questionResults) }
|
|
272
|
+
: {}),
|
|
222
273
|
};
|
|
223
274
|
emitToolCallUpdate(session, toolUseId, progressFields, "summary");
|
|
224
275
|
}
|
package/agent-sdk/dist/bridge.js
CHANGED
|
@@ -129,7 +129,7 @@ export function emitAgentConfigOptionUpdate(sessionId, agent) {
|
|
|
129
129
|
value: agent,
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
|
-
const EXPECTED_AGENT_SDK_VERSION = "0.3.
|
|
132
|
+
const EXPECTED_AGENT_SDK_VERSION = "0.3.198";
|
|
133
133
|
const require = createRequire(import.meta.url);
|
|
134
134
|
export function resolveInstalledAgentSdkVersion() {
|
|
135
135
|
try {
|
|
@@ -319,6 +319,7 @@ test("parseCommandEnvelope validates mcp_set_servers command", () => {
|
|
|
319
319
|
"X-Test": "1",
|
|
320
320
|
},
|
|
321
321
|
timeout: 5000,
|
|
322
|
+
request_timeout_ms: 30000,
|
|
322
323
|
always_load: true,
|
|
323
324
|
tools: [
|
|
324
325
|
{
|
|
@@ -351,6 +352,7 @@ test("parseCommandEnvelope validates mcp_set_servers command", () => {
|
|
|
351
352
|
"X-Test": "1",
|
|
352
353
|
},
|
|
353
354
|
timeout: 5000,
|
|
355
|
+
request_timeout_ms: 30000,
|
|
354
356
|
always_load: true,
|
|
355
357
|
tools: [
|
|
356
358
|
{
|
|
@@ -386,6 +388,17 @@ test("parseCommandEnvelope rejects invalid latest MCP config fields", () => {
|
|
|
386
388
|
session_id: "session-123",
|
|
387
389
|
servers: { bad: { type: "http", url: "https://mcp.example.com", timeout: 999 } },
|
|
388
390
|
})), /mcp_set_servers\.servers\.bad\.timeout must be an integer >= 1000/);
|
|
391
|
+
assert.throws(() => parseCommandEnvelope(JSON.stringify({
|
|
392
|
+
command: "mcp_set_servers",
|
|
393
|
+
session_id: "session-123",
|
|
394
|
+
servers: {
|
|
395
|
+
bad: {
|
|
396
|
+
type: "http",
|
|
397
|
+
url: "https://mcp.example.com",
|
|
398
|
+
request_timeout_ms: 999,
|
|
399
|
+
},
|
|
400
|
+
},
|
|
401
|
+
})), /mcp_set_servers\.servers\.bad\.request_timeout_ms must be an integer >= 1000/);
|
|
389
402
|
assert.throws(() => parseCommandEnvelope(JSON.stringify({
|
|
390
403
|
command: "mcp_set_servers",
|
|
391
404
|
session_id: "session-123",
|
|
@@ -453,6 +466,7 @@ test("handleMcpSetServersCommand emits SDK result", async () => {
|
|
|
453
466
|
type: "http",
|
|
454
467
|
url: "https://example.test/mcp",
|
|
455
468
|
always_load: true,
|
|
469
|
+
request_timeout_ms: 30000,
|
|
456
470
|
},
|
|
457
471
|
},
|
|
458
472
|
}, "req-mcp-set");
|
|
@@ -462,6 +476,7 @@ test("handleMcpSetServersCommand emits SDK result", async () => {
|
|
|
462
476
|
type: "http",
|
|
463
477
|
url: "https://example.test/mcp",
|
|
464
478
|
alwaysLoad: true,
|
|
479
|
+
requestTimeoutMs: 30000,
|
|
465
480
|
},
|
|
466
481
|
});
|
|
467
482
|
assert.deepEqual(events, [
|
|
@@ -537,6 +552,7 @@ test("bridgeMcpConfigToSdk maps latest MCP fields to SDK casing", () => {
|
|
|
537
552
|
type: "sse",
|
|
538
553
|
url: "https://mcp.example.com/sse",
|
|
539
554
|
timeout: 2500,
|
|
555
|
+
request_timeout_ms: 30000,
|
|
540
556
|
always_load: true,
|
|
541
557
|
tools: [
|
|
542
558
|
{ name: "search" },
|
|
@@ -546,6 +562,7 @@ test("bridgeMcpConfigToSdk maps latest MCP fields to SDK casing", () => {
|
|
|
546
562
|
type: "sse",
|
|
547
563
|
url: "https://mcp.example.com/sse",
|
|
548
564
|
timeout: 2500,
|
|
565
|
+
requestTimeoutMs: 30000,
|
|
549
566
|
alwaysLoad: true,
|
|
550
567
|
tools: [
|
|
551
568
|
{ name: "search" },
|
|
@@ -562,6 +579,7 @@ test("mapMcpServerStatus preserves latest MCP status config fields", () => {
|
|
|
562
579
|
url: "https://mcp.notion.com/mcp",
|
|
563
580
|
headers: { Authorization: "Bearer token" },
|
|
564
581
|
timeout: 5000,
|
|
582
|
+
requestTimeoutMs: 30000,
|
|
565
583
|
alwaysLoad: true,
|
|
566
584
|
tools: [
|
|
567
585
|
{ name: "search" },
|
|
@@ -575,6 +593,7 @@ test("mapMcpServerStatus preserves latest MCP status config fields", () => {
|
|
|
575
593
|
url: "https://mcp.notion.com/mcp",
|
|
576
594
|
headers: { Authorization: "Bearer token" },
|
|
577
595
|
timeout: 5000,
|
|
596
|
+
request_timeout_ms: 30000,
|
|
578
597
|
always_load: true,
|
|
579
598
|
tools: [
|
|
580
599
|
{ name: "search" },
|
|
@@ -1103,6 +1122,7 @@ test("buildQueryOptions enables dangerous skip flag for bypass permissions start
|
|
|
1103
1122
|
});
|
|
1104
1123
|
assert.equal(options.permissionMode, "bypassPermissions");
|
|
1105
1124
|
assert.equal(options.allowDangerouslySkipPermissions, true);
|
|
1125
|
+
assert.equal("canUseTool" in options, false);
|
|
1106
1126
|
});
|
|
1107
1127
|
test("buildQueryOptions omits optional startup overrides but keeps bridge guard prompt", () => {
|
|
1108
1128
|
const input = new AsyncQueue();
|
|
@@ -3162,6 +3182,70 @@ test("requestAskUserQuestionAnswers preserves previews and annotations in update
|
|
|
3162
3182
|
question_index: 0,
|
|
3163
3183
|
total_questions: 1,
|
|
3164
3184
|
});
|
|
3185
|
+
const completedQuestionUpdate = events
|
|
3186
|
+
.map((event) => (event.event === "session_update" ? event.update : undefined))
|
|
3187
|
+
.find((update) => {
|
|
3188
|
+
const toolCallUpdate = update?.tool_call_update;
|
|
3189
|
+
const fields = toolCallUpdate?.fields;
|
|
3190
|
+
return toolCallUpdate?.tool_call_id === "tool-question" && fields?.status === "completed";
|
|
3191
|
+
})?.tool_call_update;
|
|
3192
|
+
const completedFields = completedQuestionUpdate?.fields;
|
|
3193
|
+
assert.deepEqual(completedFields?.raw_input, {
|
|
3194
|
+
questions: [
|
|
3195
|
+
{
|
|
3196
|
+
question: "Pick deployment target",
|
|
3197
|
+
header: "Target",
|
|
3198
|
+
multiSelect: true,
|
|
3199
|
+
options: [
|
|
3200
|
+
{
|
|
3201
|
+
label: "Staging",
|
|
3202
|
+
description: "Low-risk validation",
|
|
3203
|
+
preview: "Deploy to staging first.",
|
|
3204
|
+
},
|
|
3205
|
+
{
|
|
3206
|
+
label: "Production",
|
|
3207
|
+
description: "Customer-facing rollout",
|
|
3208
|
+
preview: "Deploy to production after approval.",
|
|
3209
|
+
},
|
|
3210
|
+
],
|
|
3211
|
+
},
|
|
3212
|
+
],
|
|
3213
|
+
answers: {
|
|
3214
|
+
"Pick deployment target": "Staging, Production",
|
|
3215
|
+
},
|
|
3216
|
+
annotations: {
|
|
3217
|
+
"Pick deployment target": {
|
|
3218
|
+
preview: "Deploy to staging first.\n\nDeploy to production after approval.",
|
|
3219
|
+
notes: "Roll out in both environments",
|
|
3220
|
+
},
|
|
3221
|
+
},
|
|
3222
|
+
question_results: [
|
|
3223
|
+
{
|
|
3224
|
+
question: "Pick deployment target",
|
|
3225
|
+
header: "Target",
|
|
3226
|
+
question_index: 0,
|
|
3227
|
+
total_questions: 1,
|
|
3228
|
+
selected_options: [
|
|
3229
|
+
{
|
|
3230
|
+
option_id: "question_0",
|
|
3231
|
+
label: "Staging",
|
|
3232
|
+
description: "Low-risk validation",
|
|
3233
|
+
preview: "Deploy to staging first.",
|
|
3234
|
+
},
|
|
3235
|
+
{
|
|
3236
|
+
option_id: "question_1",
|
|
3237
|
+
label: "Production",
|
|
3238
|
+
description: "Customer-facing rollout",
|
|
3239
|
+
preview: "Deploy to production after approval.",
|
|
3240
|
+
},
|
|
3241
|
+
],
|
|
3242
|
+
annotation: {
|
|
3243
|
+
preview: "Deploy to staging first.\n\nDeploy to production after approval.",
|
|
3244
|
+
notes: "Roll out in both environments",
|
|
3245
|
+
},
|
|
3246
|
+
},
|
|
3247
|
+
],
|
|
3248
|
+
});
|
|
3165
3249
|
});
|
|
3166
3250
|
test("normalizeToolKind maps known tool names", () => {
|
|
3167
3251
|
assert.equal(normalizeToolKind("Bash"), "execute");
|
|
@@ -4329,6 +4413,7 @@ test("createToolCall maps project and artifact tools to compact titles", () => {
|
|
|
4329
4413
|
file_path: "C:/work/report.html",
|
|
4330
4414
|
favicon: "R",
|
|
4331
4415
|
label: "report-v2",
|
|
4416
|
+
description: "Quarterly report",
|
|
4332
4417
|
});
|
|
4333
4418
|
const artifactFallback = createToolCall("tc-artifact-path", "Artifact", {
|
|
4334
4419
|
file_path: "C:/work/report.html",
|
|
@@ -4341,6 +4426,7 @@ test("createToolCall maps project and artifact tools to compact titles", () => {
|
|
|
4341
4426
|
assert.equal(projectSearch.title, "Projects: search migration");
|
|
4342
4427
|
assert.equal(artifactWithLabel.kind, "other");
|
|
4343
4428
|
assert.equal(artifactWithLabel.title, "Artifact: report-v2");
|
|
4429
|
+
assert.equal(artifactWithLabel.raw_input.description, "Quarterly report");
|
|
4344
4430
|
assert.equal(artifactFallback.title, "Artifact: C:/work/report.html");
|
|
4345
4431
|
assert.equal(rolePicker.kind, "other");
|
|
4346
4432
|
assert.equal(rolePicker.title, "ShowOnboardingRolePicker");
|
|
@@ -5020,7 +5106,7 @@ test("looksLikeAuthRequired detects login hints", () => {
|
|
|
5020
5106
|
assert.equal(looksLikeAuthRequired("normal tool output"), false);
|
|
5021
5107
|
});
|
|
5022
5108
|
test("agent sdk version compatibility check matches pinned version", () => {
|
|
5023
|
-
assert.equal(resolveInstalledAgentSdkVersion(), "0.3.
|
|
5109
|
+
assert.equal(resolveInstalledAgentSdkVersion(), "0.3.198");
|
|
5024
5110
|
assert.equal(agentSdkVersionCompatibilityError(), undefined);
|
|
5025
5111
|
});
|
|
5026
5112
|
test("mapSessionMessagesToUpdates maps message content blocks", () => {
|
|
@@ -6088,6 +6174,7 @@ test("mapAvailableModels preserves optional fast and auto mode metadata", () =>
|
|
|
6088
6174
|
const mapped = mapAvailableModels([
|
|
6089
6175
|
{
|
|
6090
6176
|
value: "sonnet",
|
|
6177
|
+
resolvedModel: "claude-sonnet-5",
|
|
6091
6178
|
displayName: "Claude Sonnet",
|
|
6092
6179
|
description: "Balanced model",
|
|
6093
6180
|
supportsEffort: true,
|
|
@@ -6106,6 +6193,7 @@ test("mapAvailableModels preserves optional fast and auto mode metadata", () =>
|
|
|
6106
6193
|
assert.deepEqual(mapped, [
|
|
6107
6194
|
{
|
|
6108
6195
|
id: "sonnet",
|
|
6196
|
+
resolved_model: "claude-sonnet-5",
|
|
6109
6197
|
display_name: "Claude Sonnet",
|
|
6110
6198
|
description: "Balanced model",
|
|
6111
6199
|
supports_effort: true,
|
|
@@ -6123,12 +6211,13 @@ test("mapAvailableModels preserves optional fast and auto mode metadata", () =>
|
|
|
6123
6211
|
},
|
|
6124
6212
|
]);
|
|
6125
6213
|
});
|
|
6126
|
-
test("mapAvailableModels
|
|
6214
|
+
test("mapAvailableModels preserves Fable models and unknown ids", () => {
|
|
6127
6215
|
const mapped = mapAvailableModels([
|
|
6128
6216
|
{
|
|
6129
6217
|
value: "fable",
|
|
6218
|
+
resolvedModel: "claude-fable-5",
|
|
6130
6219
|
displayName: "Claude Fable",
|
|
6131
|
-
description: "
|
|
6220
|
+
description: "Default model alias",
|
|
6132
6221
|
supportsEffort: true,
|
|
6133
6222
|
},
|
|
6134
6223
|
{
|
|
@@ -6151,6 +6240,28 @@ test("mapAvailableModels filters unavailable Fable models while preserving unkno
|
|
|
6151
6240
|
},
|
|
6152
6241
|
]);
|
|
6153
6242
|
assert.deepEqual(mapped, [
|
|
6243
|
+
{
|
|
6244
|
+
id: "fable",
|
|
6245
|
+
resolved_model: "claude-fable-5",
|
|
6246
|
+
display_name: "Claude Fable",
|
|
6247
|
+
description: "Default model alias",
|
|
6248
|
+
supports_effort: true,
|
|
6249
|
+
supported_effort_levels: [],
|
|
6250
|
+
},
|
|
6251
|
+
{
|
|
6252
|
+
id: "claude-fable-5",
|
|
6253
|
+
display_name: "Claude Fable 5",
|
|
6254
|
+
description: "Unavailable model",
|
|
6255
|
+
supports_effort: true,
|
|
6256
|
+
supported_effort_levels: [],
|
|
6257
|
+
},
|
|
6258
|
+
{
|
|
6259
|
+
id: "claude-fable-5-20260612",
|
|
6260
|
+
display_name: "Claude Fable 5 dated",
|
|
6261
|
+
description: "Unavailable dated model",
|
|
6262
|
+
supports_effort: true,
|
|
6263
|
+
supported_effort_levels: [],
|
|
6264
|
+
},
|
|
6154
6265
|
{
|
|
6155
6266
|
id: "claude-unknown-1",
|
|
6156
6267
|
display_name: "Claude Unknown",
|
|
@@ -6160,6 +6271,26 @@ test("mapAvailableModels filters unavailable Fable models while preserving unkno
|
|
|
6160
6271
|
},
|
|
6161
6272
|
]);
|
|
6162
6273
|
});
|
|
6274
|
+
test("resolveCurrentModel matches full Fable runtime ids to the fable alias", () => {
|
|
6275
|
+
const session = makeSessionState();
|
|
6276
|
+
session.model = "fable";
|
|
6277
|
+
session.requestedModelId = "fable";
|
|
6278
|
+
session.resolvedRuntimeModelId = "claude-fable-5-20260612";
|
|
6279
|
+
session.availableModels = [
|
|
6280
|
+
{
|
|
6281
|
+
id: "fable",
|
|
6282
|
+
resolved_model: "claude-fable-5",
|
|
6283
|
+
display_name: "Claude Fable 5",
|
|
6284
|
+
supports_effort: true,
|
|
6285
|
+
supported_effort_levels: ["low", "medium", "high", "xhigh", "max"],
|
|
6286
|
+
},
|
|
6287
|
+
];
|
|
6288
|
+
const currentModel = resolveCurrentModel(session);
|
|
6289
|
+
assert.equal(currentModel.display_name_short, "Fable 5");
|
|
6290
|
+
assert.equal(currentModel.display_name_long, "Claude Fable 5");
|
|
6291
|
+
assert.equal(currentModel.catalog_id, "fable");
|
|
6292
|
+
assert.equal(currentModel.supports_effort, true);
|
|
6293
|
+
});
|
|
6163
6294
|
test("resolveCurrentModel keeps 1M context suffix in short and long display names", () => {
|
|
6164
6295
|
const session = makeSessionState();
|
|
6165
6296
|
session.resolvedRuntimeModelId = "claude-opus-4-7[1m]";
|
package/package.json
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-rust",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Claude Code Rust - native Rust terminal interface for Claude Code",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
7
7
|
"tui",
|
|
8
8
|
"claude",
|
|
9
|
+
"claude-code",
|
|
10
|
+
"anthropic",
|
|
11
|
+
"agent-sdk",
|
|
9
12
|
"ai",
|
|
10
|
-
"terminal"
|
|
13
|
+
"terminal",
|
|
14
|
+
"rust",
|
|
15
|
+
"ratatui"
|
|
11
16
|
],
|
|
12
17
|
"license": "Apache-2.0",
|
|
13
18
|
"repository": {
|
|
@@ -29,7 +34,7 @@
|
|
|
29
34
|
"README.md"
|
|
30
35
|
],
|
|
31
36
|
"dependencies": {
|
|
32
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.
|
|
37
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.198",
|
|
33
38
|
"@anthropic-ai/sdk": "0.106.0",
|
|
34
39
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
35
40
|
"zod": "4.4.3"
|