claude-code-rust 0.12.3 → 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/commands.js +20 -0
- package/agent-sdk/dist/bridge/events.js +15 -1
- package/agent-sdk/dist/bridge/logger.js +10 -0
- package/agent-sdk/dist/bridge/mcp_metadata.js +35 -0
- package/agent-sdk/dist/bridge/message_handlers.js +172 -1
- package/agent-sdk/dist/bridge/model_metadata.js +30 -20
- package/agent-sdk/dist/bridge/session_lifecycle.js +40 -2
- package/agent-sdk/dist/bridge/state_parsing.js +9 -0
- package/agent-sdk/dist/bridge/tooling.js +55 -8
- package/agent-sdk/dist/bridge/user_interaction.js +52 -1
- package/agent-sdk/dist/bridge.js +409 -1
- package/agent-sdk/dist/bridge.test.js +755 -7
- package/package.json +10 -5
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
|
|
|
@@ -95,6 +95,13 @@ function expectEffortLevel(record, key, context) {
|
|
|
95
95
|
}
|
|
96
96
|
return value;
|
|
97
97
|
}
|
|
98
|
+
function expectRewindRestoreMode(record, key, context) {
|
|
99
|
+
const value = expectString(record, key, context);
|
|
100
|
+
if (value === "both" || value === "conversation" || value === "code") {
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
throw new Error(`${context}.${key} must be one of both, conversation, code`);
|
|
104
|
+
}
|
|
98
105
|
function expectNonEmptyStringOrNull(record, key, context) {
|
|
99
106
|
const value = record[key];
|
|
100
107
|
if (value === null) {
|
|
@@ -281,6 +288,19 @@ export function parseCommandEnvelope(line) {
|
|
|
281
288
|
command: "get_context_usage",
|
|
282
289
|
session_id: expectString(raw, "session_id", "get_context_usage"),
|
|
283
290
|
};
|
|
291
|
+
case "get_rewind_targets":
|
|
292
|
+
return {
|
|
293
|
+
command: "get_rewind_targets",
|
|
294
|
+
session_id: expectString(raw, "session_id", "get_rewind_targets"),
|
|
295
|
+
};
|
|
296
|
+
case "rewind":
|
|
297
|
+
return {
|
|
298
|
+
command: "rewind",
|
|
299
|
+
session_id: expectString(raw, "session_id", "rewind"),
|
|
300
|
+
target_user_message_id: expectString(raw, "target_user_message_id", "rewind"),
|
|
301
|
+
restore_mode: expectRewindRestoreMode(raw, "restore_mode", "rewind"),
|
|
302
|
+
launch_settings: optionalLaunchSettings(raw, "launch_settings", "rewind"),
|
|
303
|
+
};
|
|
284
304
|
case "reload_plugins":
|
|
285
305
|
return {
|
|
286
306
|
command: "reload_plugins",
|
|
@@ -6,7 +6,9 @@ import { resolveCurrentModel } from "./session_lifecycle.js";
|
|
|
6
6
|
const SESSION_LIST_LIMIT = 50;
|
|
7
7
|
let sessionListingDir;
|
|
8
8
|
export function buildSessionListOptions(dir, limit = SESSION_LIST_LIMIT) {
|
|
9
|
-
return dir
|
|
9
|
+
return dir
|
|
10
|
+
? { dir, includeProgrammatic: true, includeWorktrees: true, limit }
|
|
11
|
+
: { includeProgrammatic: true, limit };
|
|
10
12
|
}
|
|
11
13
|
export function setSessionListingDir(dir) {
|
|
12
14
|
sessionListingDir = dir;
|
|
@@ -121,6 +123,7 @@ function buildConnectBridgeEvent(session, eventName) {
|
|
|
121
123
|
available_models: session.availableModels,
|
|
122
124
|
mode: session.mode ? buildModeState(session, session.mode) : null,
|
|
123
125
|
...(historyUpdates && historyUpdates.length > 0 ? { history_updates: historyUpdates } : {}),
|
|
126
|
+
...(session.restoredInput !== undefined ? { restored_input: session.restoredInput } : {}),
|
|
124
127
|
}
|
|
125
128
|
: {
|
|
126
129
|
event: "connected",
|
|
@@ -144,6 +147,7 @@ function logConnectEventEmission(session, eventName, requestId) {
|
|
|
144
147
|
history_update_count: session.resumeUpdates?.length ?? 0,
|
|
145
148
|
available_model_count: session.availableModels.length,
|
|
146
149
|
stale_session_count: session.sessionsToCloseAfterConnect?.length ?? 0,
|
|
150
|
+
has_restored_input: session.restoredInput !== undefined,
|
|
147
151
|
},
|
|
148
152
|
});
|
|
149
153
|
}
|
|
@@ -151,10 +155,15 @@ export function emitConnectEvent(session) {
|
|
|
151
155
|
const bridgeEvent = buildConnectBridgeEvent(session, session.connectEvent);
|
|
152
156
|
logConnectEventEmission(session, session.connectEvent, session.connectRequestId);
|
|
153
157
|
writeEvent(bridgeEvent, session.connectRequestId);
|
|
158
|
+
if (session.pendingRewindResult) {
|
|
159
|
+
writeEvent({ ...session.pendingRewindResult, session_id: session.sessionId }, session.connectRequestId);
|
|
160
|
+
session.pendingRewindResult = undefined;
|
|
161
|
+
}
|
|
154
162
|
session.connectRequestId = undefined;
|
|
155
163
|
session.connected = true;
|
|
156
164
|
session.authHintSent = false;
|
|
157
165
|
session.resumeUpdates = undefined;
|
|
166
|
+
session.restoredInput = undefined;
|
|
158
167
|
const staleSessions = session.sessionsToCloseAfterConnect;
|
|
159
168
|
session.sessionsToCloseAfterConnect = undefined;
|
|
160
169
|
if (!staleSessions || staleSessions.length === 0) {
|
|
@@ -180,7 +189,12 @@ export function emitSessionReplacedEvent(session, requestId) {
|
|
|
180
189
|
const bridgeEvent = buildConnectBridgeEvent(session, "session_replaced");
|
|
181
190
|
logConnectEventEmission(session, "session_replaced", requestId);
|
|
182
191
|
writeEvent(bridgeEvent, requestId);
|
|
192
|
+
if (session.pendingRewindResult) {
|
|
193
|
+
writeEvent({ ...session.pendingRewindResult, session_id: session.sessionId }, requestId);
|
|
194
|
+
session.pendingRewindResult = undefined;
|
|
195
|
+
}
|
|
183
196
|
session.resumeUpdates = undefined;
|
|
197
|
+
session.restoredInput = undefined;
|
|
184
198
|
refreshSessionsList();
|
|
185
199
|
}
|
|
186
200
|
export async function emitSessionsList(requestId) {
|
|
@@ -71,6 +71,10 @@ function commandSessionId(command) {
|
|
|
71
71
|
case "question_response":
|
|
72
72
|
case "elicitation_response":
|
|
73
73
|
case "get_status_snapshot":
|
|
74
|
+
case "get_context_usage":
|
|
75
|
+
case "get_rewind_targets":
|
|
76
|
+
case "rewind":
|
|
77
|
+
case "reload_plugins":
|
|
74
78
|
case "mcp_status":
|
|
75
79
|
case "mcp_reconnect":
|
|
76
80
|
case "mcp_toggle":
|
|
@@ -105,6 +109,8 @@ function commandToolCallId(command) {
|
|
|
105
109
|
case "elicitation_response":
|
|
106
110
|
case "get_status_snapshot":
|
|
107
111
|
case "get_context_usage":
|
|
112
|
+
case "get_rewind_targets":
|
|
113
|
+
case "rewind":
|
|
108
114
|
case "reload_plugins":
|
|
109
115
|
case "mcp_status":
|
|
110
116
|
case "mcp_reconnect":
|
|
@@ -141,6 +147,8 @@ function eventToolCallId(event) {
|
|
|
141
147
|
case "sessions_listed":
|
|
142
148
|
case "status_snapshot":
|
|
143
149
|
case "context_usage":
|
|
150
|
+
case "rewind_targets":
|
|
151
|
+
case "rewind_result":
|
|
144
152
|
case "mcp_snapshot":
|
|
145
153
|
return undefined;
|
|
146
154
|
}
|
|
@@ -183,6 +191,8 @@ function protocolEventLevel(event) {
|
|
|
183
191
|
case "sessions_listed":
|
|
184
192
|
case "status_snapshot":
|
|
185
193
|
case "context_usage":
|
|
194
|
+
case "rewind_targets":
|
|
195
|
+
case "rewind_result":
|
|
186
196
|
case "runtime_reload_completed":
|
|
187
197
|
case "mcp_set_servers_result":
|
|
188
198
|
case "mcp_snapshot":
|
|
@@ -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,
|
|
@@ -90,6 +90,162 @@ function emitSystemNoticeUpdate(session, severity, message) {
|
|
|
90
90
|
}
|
|
91
91
|
emitSessionUpdate(session.sessionId, { type: "system_notice_update", severity, message: trimmed });
|
|
92
92
|
}
|
|
93
|
+
const MAX_INFORMATIONAL_DEDUP_KEYS = 256;
|
|
94
|
+
function shouldEmitInformationalMessage(session, level, content, toolUseId) {
|
|
95
|
+
if (!toolUseId) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
const key = `${toolUseId}\u0000${level}\u0000${content}`;
|
|
99
|
+
if (session.informationalDedupKeys.has(key)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
session.informationalDedupKeys.add(key);
|
|
103
|
+
while (session.informationalDedupKeys.size > MAX_INFORMATIONAL_DEDUP_KEYS) {
|
|
104
|
+
const first = session.informationalDedupKeys.values().next().value;
|
|
105
|
+
if (typeof first !== "string") {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
session.informationalDedupKeys.delete(first);
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
function handleInformationalSystemMessage(session, msg) {
|
|
113
|
+
const content = typeof msg.content === "string" ? msg.content.trim() : "";
|
|
114
|
+
if (!content) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const level = typeof msg.level === "string" ? msg.level : "info";
|
|
118
|
+
const toolUseId = typeof msg.tool_use_id === "string" ? msg.tool_use_id : "";
|
|
119
|
+
if (!shouldEmitInformationalMessage(session, level, content, toolUseId)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
switch (level) {
|
|
123
|
+
case "notice":
|
|
124
|
+
emitSystemNoticeUpdate(session, "info", content);
|
|
125
|
+
return;
|
|
126
|
+
case "suggestion":
|
|
127
|
+
emitSystemNoticeUpdate(session, "info", `Suggestion: ${content}`);
|
|
128
|
+
return;
|
|
129
|
+
case "warning":
|
|
130
|
+
emitSystemNoticeUpdate(session, "warning", content);
|
|
131
|
+
return;
|
|
132
|
+
case "info":
|
|
133
|
+
if (msg.prevent_continuation === true) {
|
|
134
|
+
emitSystemNoticeUpdate(session, "warning", content);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
default:
|
|
138
|
+
bridgeLogger.debug({
|
|
139
|
+
target: LOG_TARGETS.BRIDGE_SDK,
|
|
140
|
+
eventName: "sdk_informational_level_unhandled",
|
|
141
|
+
message: "SDK informational message ignored for unknown level",
|
|
142
|
+
outcome: "ignored",
|
|
143
|
+
sessionId: session.sessionId,
|
|
144
|
+
toolCallId: toolUseId || undefined,
|
|
145
|
+
fields: {
|
|
146
|
+
informational_level: level,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function trimmedStringField(msg, field) {
|
|
152
|
+
const value = msg[field];
|
|
153
|
+
if (typeof value !== "string") {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
const trimmed = value.trim();
|
|
157
|
+
return trimmed ? trimmed : undefined;
|
|
158
|
+
}
|
|
159
|
+
function ensureSentencePunctuation(value) {
|
|
160
|
+
const trimmed = value.trim();
|
|
161
|
+
if (!trimmed) {
|
|
162
|
+
return "";
|
|
163
|
+
}
|
|
164
|
+
return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`;
|
|
165
|
+
}
|
|
166
|
+
function modelRefusalNoFallbackMessage(msg) {
|
|
167
|
+
const model = trimmedStringField(msg, "original_model") ?? "the selected model";
|
|
168
|
+
const base = `Could not continue with ${model}: model refused the request and no fallback model is configured.`;
|
|
169
|
+
const explanation = trimmedStringField(msg, "api_refusal_explanation");
|
|
170
|
+
const category = trimmedStringField(msg, "api_refusal_category");
|
|
171
|
+
const content = trimmedStringField(msg, "content");
|
|
172
|
+
const detail = explanation
|
|
173
|
+
? `Reason: ${explanation}`
|
|
174
|
+
: category
|
|
175
|
+
? `Refusal category: ${category}`
|
|
176
|
+
: content;
|
|
177
|
+
const detailSentence = detail ? ensureSentencePunctuation(detail) : "";
|
|
178
|
+
return detailSentence ? `${base} ${detailSentence}` : base;
|
|
179
|
+
}
|
|
180
|
+
function handleModelRefusalNoFallbackMessage(session, msg) {
|
|
181
|
+
const message = modelRefusalNoFallbackMessage(msg);
|
|
182
|
+
emitSystemNoticeUpdate(session, "warning", message);
|
|
183
|
+
bridgeLogger.info({
|
|
184
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
185
|
+
eventName: "sdk_model_refusal_no_fallback_received",
|
|
186
|
+
message: "SDK model refusal without fallback received",
|
|
187
|
+
outcome: "success",
|
|
188
|
+
sessionId: session.sessionId,
|
|
189
|
+
requestId: trimmedStringField(msg, "request_id"),
|
|
190
|
+
fields: {
|
|
191
|
+
original_model: trimmedStringField(msg, "original_model"),
|
|
192
|
+
api_refusal_category: trimmedStringField(msg, "api_refusal_category"),
|
|
193
|
+
refused_user_message_uuid: trimmedStringField(msg, "refused_user_message_uuid"),
|
|
194
|
+
sdk_message_uuid: trimmedStringField(msg, "uuid"),
|
|
195
|
+
sdk_message_session_id: trimmedStringField(msg, "session_id"),
|
|
196
|
+
has_api_refusal_explanation: trimmedStringField(msg, "api_refusal_explanation") !== undefined,
|
|
197
|
+
has_content: trimmedStringField(msg, "content") !== undefined,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function workerShutdownMessage(reason) {
|
|
202
|
+
const trimmed = reason.trim();
|
|
203
|
+
return trimmed ? `Claude worker is shutting down: ${trimmed}` : "Claude worker is shutting down.";
|
|
204
|
+
}
|
|
205
|
+
function handleWorkerShuttingDownSystemMessage(session, msg) {
|
|
206
|
+
const reason = typeof msg.reason === "string" ? msg.reason.trim() : "";
|
|
207
|
+
if (!session.connected) {
|
|
208
|
+
bridgeLogger.debug({
|
|
209
|
+
target: LOG_TARGETS.BRIDGE_SDK,
|
|
210
|
+
eventName: "sdk_worker_shutdown_preconnect_ignored",
|
|
211
|
+
message: "SDK worker shutdown ignored before session connect",
|
|
212
|
+
outcome: "ignored",
|
|
213
|
+
sessionId: session.sessionId,
|
|
214
|
+
fields: {
|
|
215
|
+
reason: reason || undefined,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
session.pendingWorkerShutdown = { reason };
|
|
221
|
+
}
|
|
222
|
+
function cancelPendingWorkerShutdown(session) {
|
|
223
|
+
if (!session.pendingWorkerShutdown) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
bridgeLogger.debug({
|
|
227
|
+
target: LOG_TARGETS.BRIDGE_SDK,
|
|
228
|
+
eventName: "sdk_worker_shutdown_cancelled",
|
|
229
|
+
message: "SDK worker shutdown ignored after later stream activity",
|
|
230
|
+
outcome: "ignored",
|
|
231
|
+
sessionId: session.sessionId,
|
|
232
|
+
fields: {
|
|
233
|
+
reason: session.pendingWorkerShutdown.reason || undefined,
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
session.pendingWorkerShutdown = undefined;
|
|
237
|
+
}
|
|
238
|
+
export function flushPendingWorkerShutdown(session) {
|
|
239
|
+
const pending = session.pendingWorkerShutdown;
|
|
240
|
+
if (!pending) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
session.pendingWorkerShutdown = undefined;
|
|
244
|
+
if (!session.connected) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
emitSystemNoticeUpdate(session, "warning", workerShutdownMessage(pending.reason));
|
|
248
|
+
}
|
|
93
249
|
function notificationSeverity(priority) {
|
|
94
250
|
return priority === "high" || priority === "immediate" ? "warning" : "info";
|
|
95
251
|
}
|
|
@@ -675,12 +831,19 @@ function terminalReasonFromValue(value) {
|
|
|
675
831
|
export function handleSdkMessage(session, message) {
|
|
676
832
|
const msg = message;
|
|
677
833
|
const type = typeof msg.type === "string" ? msg.type : "";
|
|
834
|
+
const subtype = type === "system" && typeof msg.subtype === "string" ? msg.subtype : "";
|
|
835
|
+
if (subtype !== "worker_shutting_down") {
|
|
836
|
+
cancelPendingWorkerShutdown(session);
|
|
837
|
+
}
|
|
678
838
|
logSdkMessageOrigin(session, msg);
|
|
679
839
|
if (type === "system") {
|
|
680
|
-
const subtype = typeof msg.subtype === "string" ? msg.subtype : "";
|
|
681
840
|
if (handleFallbackRetractionMessage(session, subtype, msg)) {
|
|
682
841
|
return;
|
|
683
842
|
}
|
|
843
|
+
if (subtype === "model_refusal_no_fallback") {
|
|
844
|
+
handleModelRefusalNoFallbackMessage(session, msg);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
684
847
|
if (subtype === "commands_changed") {
|
|
685
848
|
updateAvailableCommands(session, "commands_changed", mapSdkSlashCommands(msg.commands));
|
|
686
849
|
return;
|
|
@@ -690,6 +853,14 @@ export function handleSdkMessage(session, message) {
|
|
|
690
853
|
emitSystemNoticeUpdate(session, notificationSeverity(msg.priority), text);
|
|
691
854
|
return;
|
|
692
855
|
}
|
|
856
|
+
if (subtype === "informational") {
|
|
857
|
+
handleInformationalSystemMessage(session, msg);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
if (subtype === "worker_shutting_down") {
|
|
861
|
+
handleWorkerShuttingDownSystemMessage(session, msg);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
693
864
|
if (subtype === "mirror_error") {
|
|
694
865
|
const error = typeof msg.error === "string" ? msg.error : "";
|
|
695
866
|
const key = asRecordOrNull(msg.key);
|
|
@@ -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 {
|