claude-code-rust 0.12.0 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,6 +5,7 @@ A native Rust terminal interface for Claude Code. Drop-in replacement for Anthro
5
5
  [![npm version](https://img.shields.io/npm/v/claude-code-rust)](https://www.npmjs.com/package/claude-code-rust)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/claude-code-rust)](https://www.npmjs.com/package/claude-code-rust)
7
7
  [![CI](https://github.com/srothgan/claude-code-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/srothgan/claude-code-rust/actions/workflows/ci.yml)
8
+ [![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://srothgan.github.io/claude-code-rust/)
8
9
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
9
10
  [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-green.svg)](https://nodejs.org/)
10
11
 
@@ -35,6 +36,8 @@ If `claude-rs` resolves to an older global shim, ensure your npm global bin dire
35
36
  claude-rs
36
37
  ```
37
38
 
39
+ Full documentation is available at [srothgan.github.io/claude-code-rust](https://srothgan.github.io/claude-code-rust/).
40
+
38
41
  > [!WARNING]
39
42
  > **Agent SDK billing changes on June 15, 2026.** Anthropic says Agent SDK usage, `claude -p`, Claude Code GitHub Actions, and third-party Agent SDK apps will use a separate monthly Agent SDK credit instead of normal interactive Claude or Claude Code subscription limits. Because Claude Code Rust wraps the Agent SDK, treat usage through this project as Agent SDK usage. If that credit is exhausted, continued use may require enabling extra usage billed at standard API rates, or requests may pause until the credit refreshes.
40
43
  >
@@ -54,14 +57,15 @@ The stock Claude Code TUI runs on Node.js with React Ink. This causes real probl
54
57
 
55
58
  Claude Code Rust fixes all of these by compiling to a single native binary with direct terminal control via Crossterm.
56
59
 
57
- ## Custom Commands
60
+ ## Documentation
58
61
 
59
- Claude Code Rust adds project-local slash commands that set environment variables via `.claude/settings.local.json` without leaving the TUI. Changes apply on the next session.
62
+ The manual covers installation from npm and source, help, slash commands, keyboard shortcuts, settings, diagnostics, architecture, and the changelog:
60
63
 
61
- | Command | Usage | Description |
62
- |---------|-------|-------------|
63
- | `/1m-context` | `/1m-context <enable\|disable\|status>` | Disable the 1 million token context window to improve model performance and prevent quality degradation on large context windows. |
64
- | `/opus-version` | `/opus-version <4.5\|4.6\|4.7\|default\|status>` | Pin the Opus model version for the current folder. Useful for switching to 4.6 or 4.5 to avoid 4.7's tokenization issues. Use `default` to clear the pin. |
64
+ - [Installation](https://srothgan.github.io/claude-code-rust/installation.html)
65
+ - [Usage](https://srothgan.github.io/claude-code-rust/usage.html)
66
+ - [Help](https://srothgan.github.io/claude-code-rust/help.html)
67
+ - [Slash commands](https://srothgan.github.io/claude-code-rust/commands.html)
68
+ - [Settings](https://srothgan.github.io/claude-code-rust/settings.html)
65
69
 
66
70
  ## Status
67
71
 
@@ -9,7 +9,7 @@ npm install
9
9
  npm run build
10
10
  ```
11
11
 
12
- Build output is written to `dist/bridge.mjs`.
12
+ Build output is written to `dist/bridge.js`.
13
13
 
14
14
  ## License
15
15
 
@@ -0,0 +1,44 @@
1
+ const KNOWN_API_PROVIDERS = new Set([
2
+ "firstParty",
3
+ "bedrock",
4
+ "vertex",
5
+ "foundry",
6
+ "anthropicAws",
7
+ "mantle",
8
+ "gateway",
9
+ ]);
10
+ function trimmedString(value) {
11
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
12
+ }
13
+ export function isKnownApiProvider(provider) {
14
+ return provider !== undefined && KNOWN_API_PROVIDERS.has(provider);
15
+ }
16
+ export function apiProviderIsExternal(provider) {
17
+ return provider !== undefined && provider !== "firstParty";
18
+ }
19
+ export function mapSdkAccountInfo(account) {
20
+ const apiProvider = trimmedString(account.apiProvider);
21
+ return {
22
+ ...(trimmedString(account.email) ? { email: trimmedString(account.email) } : {}),
23
+ ...(trimmedString(account.organization)
24
+ ? { organization: trimmedString(account.organization) }
25
+ : {}),
26
+ ...(trimmedString(account.subscriptionType)
27
+ ? { subscription_type: trimmedString(account.subscriptionType) }
28
+ : {}),
29
+ ...(trimmedString(account.tokenSource)
30
+ ? { token_source: trimmedString(account.tokenSource) }
31
+ : {}),
32
+ ...(trimmedString(account.apiKeySource)
33
+ ? { api_key_source: trimmedString(account.apiKeySource) }
34
+ : {}),
35
+ ...(apiProvider ? { api_provider: apiProvider } : {}),
36
+ };
37
+ }
38
+ export function shouldEmitStartupAuthRequiredForAccount(account) {
39
+ const provider = trimmedString(account.apiProvider);
40
+ if (apiProviderIsExternal(provider)) {
41
+ return false;
42
+ }
43
+ return !trimmedString(account.email) && !trimmedString(account.apiKeySource);
44
+ }
@@ -0,0 +1,129 @@
1
+ import { emitSessionUpdate } from "./events.js";
2
+ import { bridgeLogger, LOG_TARGETS } from "./logger.js";
3
+ function isDynamicSource(source) {
4
+ return source === "commands_changed" || source === "reload_plugins";
5
+ }
6
+ function commandSignature(commands) {
7
+ return JSON.stringify(commands.map((command) => [command.name, command.description, command.input_hint ?? ""]));
8
+ }
9
+ function logAvailableCommandsDecision(session, source, commands, outcome, reason, generation) {
10
+ bridgeLogger.info({
11
+ target: LOG_TARGETS.APP_SESSION,
12
+ eventName: outcome === "accepted"
13
+ ? "available_commands_snapshot_accepted"
14
+ : "available_commands_snapshot_ignored",
15
+ message: outcome === "accepted"
16
+ ? "available commands snapshot accepted"
17
+ : "available commands snapshot ignored",
18
+ outcome,
19
+ sessionId: session.sessionId,
20
+ count: commands.length,
21
+ fields: {
22
+ source,
23
+ previous_source: session.availableCommands?.source,
24
+ generation,
25
+ command_count: commands.length,
26
+ command_names: commands.map((command) => command.name),
27
+ reason,
28
+ },
29
+ });
30
+ }
31
+ function shouldAcceptAvailableCommandsSnapshot(session, source, commands) {
32
+ const current = session.availableCommands;
33
+ if (isDynamicSource(source)) {
34
+ return { accept: true, reason: "dynamic source is authoritative" };
35
+ }
36
+ if (!current) {
37
+ if (commands.length === 0) {
38
+ return { accept: false, reason: "empty bootstrap snapshot ignored before first command list" };
39
+ }
40
+ return { accept: true, reason: "initial command snapshot" };
41
+ }
42
+ if (current.dynamicSeen) {
43
+ return {
44
+ accept: false,
45
+ reason: "bootstrap source cannot replace dynamic command snapshot",
46
+ };
47
+ }
48
+ if (source === "supportedCommands" && current.commands.length > 0) {
49
+ return {
50
+ accept: false,
51
+ reason: "supportedCommands is an initialize-time fallback and current snapshot already exists",
52
+ };
53
+ }
54
+ if (commands.length === 0) {
55
+ return { accept: false, reason: "empty bootstrap snapshot ignored" };
56
+ }
57
+ return { accept: true, reason: "bootstrap refresh before dynamic command source" };
58
+ }
59
+ export function updateAvailableCommands(session, source, commands) {
60
+ const decision = shouldAcceptAvailableCommandsSnapshot(session, source, commands);
61
+ const currentGeneration = session.availableCommands?.generation ?? 0;
62
+ const nextGeneration = currentGeneration + 1;
63
+ if (!decision.accept) {
64
+ logAvailableCommandsDecision(session, source, commands, "ignored", decision.reason, currentGeneration);
65
+ return false;
66
+ }
67
+ const dynamicSeen = session.availableCommands?.dynamicSeen === true || isDynamicSource(source);
68
+ session.availableCommands = {
69
+ generation: nextGeneration,
70
+ source,
71
+ signature: commandSignature(commands),
72
+ dynamicSeen,
73
+ commands,
74
+ };
75
+ logAvailableCommandsDecision(session, source, commands, "accepted", decision.reason, nextGeneration);
76
+ bridgeLogger.info({
77
+ target: LOG_TARGETS.APP_SESSION,
78
+ eventName: "available_commands_update_emitted",
79
+ message: "available commands update emitted",
80
+ outcome: "success",
81
+ sessionId: session.sessionId,
82
+ count: commands.length,
83
+ fields: {
84
+ source,
85
+ generation: nextGeneration,
86
+ command_count: commands.length,
87
+ command_names: commands.map((command) => command.name),
88
+ },
89
+ });
90
+ emitSessionUpdate(session.sessionId, {
91
+ type: "available_commands_update",
92
+ commands,
93
+ source,
94
+ generation: nextGeneration,
95
+ });
96
+ return true;
97
+ }
98
+ export function mapSdkSlashCommand(command) {
99
+ if (!command || typeof command !== "object") {
100
+ return null;
101
+ }
102
+ const record = command;
103
+ const name = typeof record.name === "string" ? record.name : "";
104
+ if (!name) {
105
+ return null;
106
+ }
107
+ return {
108
+ name,
109
+ description: typeof record.description === "string" ? record.description : "",
110
+ input_hint: typeof record.argumentHint === "string" ? record.argumentHint : undefined,
111
+ };
112
+ }
113
+ export function mapSdkSlashCommands(commands) {
114
+ if (!Array.isArray(commands)) {
115
+ return [];
116
+ }
117
+ return commands.flatMap((command) => {
118
+ const mapped = mapSdkSlashCommand(command);
119
+ return mapped ? [mapped] : [];
120
+ });
121
+ }
122
+ export function mapInitSlashCommands(commands) {
123
+ if (!Array.isArray(commands)) {
124
+ return [];
125
+ }
126
+ return commands
127
+ .filter((entry) => typeof entry === "string")
128
+ .map((name) => ({ name, description: "", input_hint: undefined }));
129
+ }
@@ -1,3 +1,4 @@
1
+ import { parseMcpServersRecord } from "./mcp_metadata.js";
1
2
  import { resolveCurrentModel } from "./session_lifecycle.js";
2
3
  const MODE_NAMES = {
3
4
  default: "Default",
@@ -83,6 +84,27 @@ function expectString(record, key, context) {
83
84
  }
84
85
  return value;
85
86
  }
87
+ function expectEffortLevel(record, key, context) {
88
+ const value = expectString(record, key, context);
89
+ if (value !== "low" &&
90
+ value !== "medium" &&
91
+ value !== "high" &&
92
+ value !== "xhigh" &&
93
+ value !== "max") {
94
+ throw new Error(`${context}.${key} must be one of low, medium, high, xhigh, max`);
95
+ }
96
+ return value;
97
+ }
98
+ function expectNonEmptyStringOrNull(record, key, context) {
99
+ const value = record[key];
100
+ if (value === null) {
101
+ return null;
102
+ }
103
+ if (typeof value !== "string" || value.trim().length === 0) {
104
+ throw new Error(`${context}.${key} must be a non-empty string or null`);
105
+ }
106
+ return value;
107
+ }
86
108
  function optionalString(record, key, context) {
87
109
  const value = record[key];
88
110
  if (value === undefined || value === null) {
@@ -162,33 +184,12 @@ function expectBoolean(record, key, context) {
162
184
  }
163
185
  return value;
164
186
  }
165
- function parseMcpServerConfig(value, context) {
166
- const record = asRecord(value, context);
167
- const type = expectString(record, "type", context);
168
- switch (type) {
169
- case "stdio":
170
- return {
171
- type,
172
- command: expectString(record, "command", context),
173
- ...(record.args === undefined ? {} : { args: expectStringArray(record, "args", context) }),
174
- ...(record.env === undefined ? {} : { env: expectStringMap(record, "env", context) }),
175
- };
176
- case "sse":
177
- case "http":
178
- return {
179
- type,
180
- url: expectString(record, "url", context),
181
- ...(record.headers === undefined
182
- ? {}
183
- : { headers: expectStringMap(record, "headers", context) }),
184
- };
185
- default:
186
- throw new Error(`${context}.type must be one of stdio, sse, http`);
187
+ function expectRefusalFallbackPromptChoice(record, key, context) {
188
+ const value = expectString(record, key, context);
189
+ if (value !== "retry_fallback" && value !== "edit_prompt") {
190
+ throw new Error(`${context}.${key} must be 'retry_fallback' or 'edit_prompt'`);
187
191
  }
188
- }
189
- function parseMcpServersRecord(value, context) {
190
- const record = asRecord(value, context);
191
- return Object.fromEntries(Object.entries(record).map(([key, entry]) => [key, parseMcpServerConfig(entry, `${context}.${key}`)]));
192
+ return value;
192
193
  }
193
194
  export function parseCommandEnvelope(line) {
194
195
  const raw = asRecord(JSON.parse(line), "command envelope");
@@ -246,6 +247,18 @@ export function parseCommandEnvelope(line) {
246
247
  session_id: expectString(raw, "session_id", "set_mode"),
247
248
  mode: expectString(raw, "mode", "set_mode"),
248
249
  };
250
+ case "set_effort":
251
+ return {
252
+ command: "set_effort",
253
+ session_id: expectString(raw, "session_id", "set_effort"),
254
+ effort: expectEffortLevel(raw, "effort", "set_effort"),
255
+ };
256
+ case "set_agent":
257
+ return {
258
+ command: "set_agent",
259
+ session_id: expectString(raw, "session_id", "set_agent"),
260
+ agent: expectNonEmptyStringOrNull(raw, "agent", "set_agent"),
261
+ };
249
262
  case "generate_session_title":
250
263
  return {
251
264
  command: "generate_session_title",
@@ -339,6 +352,25 @@ export function parseCommandEnvelope(line) {
339
352
  outcome: parsedOutcome,
340
353
  };
341
354
  }
355
+ case "user_dialog_response": {
356
+ const outcome = asRecord(raw.outcome, "user_dialog_response.outcome");
357
+ const outcomeType = expectString(outcome, "outcome", "user_dialog_response.outcome");
358
+ if (outcomeType !== "selected" && outcomeType !== "cancelled") {
359
+ throw new Error("user_dialog_response.outcome.outcome must be 'selected' or 'cancelled'");
360
+ }
361
+ const parsedOutcome = outcomeType === "selected"
362
+ ? {
363
+ outcome: "selected",
364
+ option_id: expectRefusalFallbackPromptChoice(outcome, "option_id", "user_dialog_response.outcome"),
365
+ }
366
+ : { outcome: "cancelled" };
367
+ return {
368
+ command: "user_dialog_response",
369
+ session_id: expectString(raw, "session_id", "user_dialog_response"),
370
+ request_id: expectString(raw, "request_id", "user_dialog_response"),
371
+ outcome: parsedOutcome,
372
+ };
373
+ }
342
374
  case "elicitation_response":
343
375
  return {
344
376
  command: "elicitation_response",
@@ -388,16 +420,6 @@ function expectStringArray(record, key, context) {
388
420
  return entry;
389
421
  });
390
422
  }
391
- function expectStringMap(record, key, context) {
392
- const value = record[key];
393
- const parsed = asRecord(value, `${context}.${key}`);
394
- return Object.fromEntries(Object.entries(parsed).map(([entryKey, entryValue]) => {
395
- if (typeof entryValue !== "string") {
396
- throw new Error(`${context}.${key}.${entryKey} must be a string`);
397
- }
398
- return [entryKey, entryValue];
399
- }));
400
- }
401
423
  function parseQuestionAnnotation(value) {
402
424
  const record = asRecord(value, "question_response.outcome.annotation");
403
425
  const preview = optionalString(record, "preview", "question_response.outcome.annotation");
@@ -29,20 +29,33 @@ export function looksLikePlanLimitError(input) {
29
29
  }
30
30
  export function classifyTurnErrorKind(subtype, errors, assistantError) {
31
31
  const combined = errors.join("\n");
32
+ switch (assistantError) {
33
+ case "billing_error":
34
+ case "rate_limit":
35
+ return "plan_limit";
36
+ case "authentication_failed":
37
+ return "auth_required";
38
+ case "oauth_org_not_allowed":
39
+ return "account_access";
40
+ case "model_not_found":
41
+ return "model_unavailable";
42
+ case "overloaded":
43
+ case "server_error":
44
+ return "transient_service";
45
+ case "invalid_request":
46
+ case "max_output_tokens":
47
+ case "unknown":
48
+ case undefined:
49
+ break;
50
+ }
32
51
  if (subtype === "error_max_turns" ||
33
52
  subtype === "error_max_budget_usd" ||
34
- assistantError === "billing_error" ||
35
- assistantError === "rate_limit" ||
36
53
  (combined.length > 0 && looksLikePlanLimitError(combined))) {
37
54
  return "plan_limit";
38
55
  }
39
- if (assistantError === "authentication_failed" ||
40
- errors.some((entry) => looksLikeAuthRequired(entry))) {
56
+ if (errors.some((entry) => looksLikeAuthRequired(entry))) {
41
57
  return "auth_required";
42
58
  }
43
- if (assistantError === "server_error") {
44
- return "internal";
45
- }
46
59
  return "other";
47
60
  }
48
61
  export function emitFastModeUpdateIfChanged(session, value) {
@@ -75,6 +75,24 @@ export function emitQuestionRequestEvent(sessionId, request) {
75
75
  });
76
76
  writeEvent({ event: "question_request", session_id: sessionId, request });
77
77
  }
78
+ export function emitUserDialogRequestEvent(sessionId, request) {
79
+ bridgeLogger.info({
80
+ target: LOG_TARGETS.BRIDGE_PERMISSION,
81
+ eventName: "user_dialog_request_emitted",
82
+ message: "user dialog request emitted",
83
+ outcome: "success",
84
+ sessionId,
85
+ requestId: request.request_id,
86
+ count: request.options.length,
87
+ fields: {
88
+ dialog_kind: request.dialog_kind,
89
+ option_count: request.options.length,
90
+ original_model: request.payload.original_model,
91
+ fallback_model: request.payload.fallback_model,
92
+ },
93
+ });
94
+ writeEvent({ event: "user_dialog_request", session_id: sessionId, request });
95
+ }
78
96
  export function emitElicitationRequestEvent(sessionId, request) {
79
97
  bridgeLogger.info({
80
98
  target: LOG_TARGETS.BRIDGE_PERMISSION,
@@ -19,17 +19,166 @@ function messageCandidates(raw) {
19
19
  }
20
20
  return candidates;
21
21
  }
22
- function pushResumeTextChunk(updates, role, text) {
22
+ function jsonValue(value) {
23
+ if (value === undefined) {
24
+ return undefined;
25
+ }
26
+ try {
27
+ const text = JSON.stringify(value);
28
+ if (text === undefined) {
29
+ return undefined;
30
+ }
31
+ return JSON.parse(text);
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
37
+ function mergeTaskMetadata(existing, patch) {
38
+ if (!patch) {
39
+ return existing;
40
+ }
41
+ const existingRecord = existing && typeof existing === "object" && !Array.isArray(existing)
42
+ ? { ...existing }
43
+ : {};
44
+ for (const [key, value] of Object.entries(patch)) {
45
+ if (value === null) {
46
+ delete existingRecord[key];
47
+ }
48
+ else {
49
+ existingRecord[key] = value;
50
+ }
51
+ }
52
+ return Object.keys(existingRecord).length > 0 ? existingRecord : null;
53
+ }
54
+ function normalizeLifecycleTaskStatus(value) {
55
+ switch (value) {
56
+ case "pending":
57
+ return "pending";
58
+ case "running":
59
+ case "in_progress":
60
+ return "in_progress";
61
+ case "completed":
62
+ case "failed":
63
+ case "killed":
64
+ case "stopped":
65
+ return "completed";
66
+ default:
67
+ return undefined;
68
+ }
69
+ }
70
+ function taskSystemMetadata(msg, patch) {
71
+ const metadata = {};
72
+ const copyValue = (from, key) => {
73
+ if (!from || !Object.hasOwn(from, key)) {
74
+ return;
75
+ }
76
+ const value = jsonValue(from[key]);
77
+ if (value !== undefined) {
78
+ metadata[key] = value;
79
+ }
80
+ };
81
+ for (const key of [
82
+ "error",
83
+ "is_backgrounded",
84
+ "request_id",
85
+ "subagent_type",
86
+ "task_description",
87
+ "task_type",
88
+ "workflow_name",
89
+ "prompt",
90
+ "output_file",
91
+ "summary",
92
+ "end_time",
93
+ "total_paused_ms",
94
+ ]) {
95
+ copyValue(msg, key);
96
+ copyValue(patch, key);
97
+ }
98
+ const terminalStatus = nonEmptyTrimmed(msg.status) ?? nonEmptyTrimmed(patch?.status);
99
+ if (terminalStatus === "completed" ||
100
+ terminalStatus === "failed" ||
101
+ terminalStatus === "killed" ||
102
+ terminalStatus === "stopped") {
103
+ metadata.terminal_status = terminalStatus;
104
+ }
105
+ return Object.keys(metadata).length > 0 ? metadata : undefined;
106
+ }
107
+ function pushResumeTaskSystemUpdate(updates, tasksById, taskToolUseIds, msg) {
108
+ const subtype = nonEmptyTrimmed(msg.subtype);
109
+ if (subtype !== "task_started" &&
110
+ subtype !== "task_progress" &&
111
+ subtype !== "task_updated" &&
112
+ subtype !== "task_notification") {
113
+ return false;
114
+ }
115
+ const taskId = nonEmptyTrimmed(msg.task_id);
116
+ if (!taskId) {
117
+ return true;
118
+ }
119
+ const explicitToolUseId = nonEmptyTrimmed(msg.tool_use_id);
120
+ if (explicitToolUseId) {
121
+ taskToolUseIds.set(taskId, explicitToolUseId);
122
+ }
123
+ const existing = tasksById.get(taskId);
124
+ const patch = asRecordOrNull(msg.patch) ?? undefined;
125
+ const status = normalizeLifecycleTaskStatus(msg.status) ??
126
+ normalizeLifecycleTaskStatus(patch?.status) ??
127
+ (subtype === "task_started" || subtype === "task_progress" ? "in_progress" : undefined);
128
+ const description = nonEmptyTrimmed(patch?.description) ??
129
+ nonEmptyTrimmed(msg.description) ??
130
+ nonEmptyTrimmed(msg.summary);
131
+ const activeForm = nonEmptyTrimmed(patch?.activeForm) ?? nonEmptyTrimmed(patch?.active_form);
132
+ const subject = nonEmptyTrimmed(patch?.subject) ??
133
+ nonEmptyTrimmed(msg.subject) ??
134
+ existing?.subject ??
135
+ nonEmptyTrimmed(msg.workflow_name) ??
136
+ nonEmptyTrimmed(msg.task_description) ??
137
+ description ??
138
+ taskId;
139
+ const metadata = mergeTaskMetadata(existing?.metadata, taskSystemMetadata(msg, patch));
140
+ const sourceToolCallId = taskToolUseIds.get(taskId) ?? existing?.source_tool_call_id;
141
+ const task = {
142
+ task_id: taskId,
143
+ subject,
144
+ ...(description !== undefined ? { description } : existing?.description !== undefined ? { description: existing.description } : {}),
145
+ ...(activeForm !== undefined ? { active_form: activeForm } : existing?.active_form !== undefined ? { active_form: existing.active_form } : {}),
146
+ status: status ?? existing?.status ?? "pending",
147
+ ...(existing?.owner !== undefined ? { owner: existing.owner } : {}),
148
+ blocks: existing ? [...existing.blocks] : [],
149
+ blocked_by: existing ? [...existing.blocked_by] : [],
150
+ ...(metadata !== undefined ? { metadata } : {}),
151
+ ...(sourceToolCallId !== undefined ? { source_tool_call_id: sourceToolCallId } : {}),
152
+ };
153
+ tasksById.set(taskId, task);
154
+ updates.push({
155
+ type: "task_state_update",
156
+ source: "task_lifecycle",
157
+ tasks: [task],
158
+ removed_task_ids: [],
159
+ is_complete_snapshot: false,
160
+ });
161
+ return true;
162
+ }
163
+ function pushResumeTextChunk(updates, role, text, sourceMessageUuid) {
23
164
  if (!text.trim()) {
24
165
  return;
25
166
  }
26
167
  if (role === "assistant") {
27
- updates.push({ type: "agent_message_chunk", content: { type: "text", text } });
168
+ updates.push({
169
+ type: "agent_message_chunk",
170
+ content: { type: "text", text },
171
+ ...(sourceMessageUuid ? { source_message_uuid: sourceMessageUuid } : {}),
172
+ });
28
173
  return;
29
174
  }
30
- updates.push({ type: "user_message_chunk", content: { type: "text", text } });
175
+ updates.push({
176
+ type: "user_message_chunk",
177
+ content: { type: "text", text },
178
+ ...(sourceMessageUuid ? { source_message_uuid: sourceMessageUuid } : {}),
179
+ });
31
180
  }
32
- function pushResumeToolUse(updates, toolCalls, hiddenToolUseIds, block, parentToolUseId) {
181
+ function pushResumeToolUse(updates, toolCalls, hiddenToolUseIds, block, parentToolUseId, sourceMessageUuid) {
33
182
  const toolUseId = typeof block.id === "string" ? block.id : "";
34
183
  if (!toolUseId) {
35
184
  return;
@@ -42,10 +191,13 @@ function pushResumeToolUse(updates, toolCalls, hiddenToolUseIds, block, parentTo
42
191
  }
43
192
  const toolCall = createToolCall(toolUseId, name, input, parentToolUseId);
44
193
  toolCall.status = "in_progress";
194
+ if (sourceMessageUuid) {
195
+ toolCall.source_message_uuid = sourceMessageUuid;
196
+ }
45
197
  toolCalls.set(toolUseId, toolCall);
46
198
  updates.push({ type: "tool_call", tool_call: toolCall });
47
199
  }
48
- function pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block) {
200
+ function pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block, sourceMessageUuid) {
49
201
  const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : "";
50
202
  if (!toolUseId) {
51
203
  return;
@@ -58,7 +210,14 @@ function pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block) {
58
210
  const isError = Boolean(block.is_error);
59
211
  const base = toolCalls.get(toolUseId);
60
212
  const fields = buildToolResultFields(isError, block.content, base, block);
61
- updates.push({ type: "tool_call_update", tool_call_update: { tool_call_id: toolUseId, fields } });
213
+ updates.push({
214
+ type: "tool_call_update",
215
+ tool_call_update: {
216
+ tool_call_id: toolUseId,
217
+ ...(sourceMessageUuid ? { source_message_uuid: sourceMessageUuid } : {}),
218
+ fields,
219
+ },
220
+ });
62
221
  if (!base) {
63
222
  return;
64
223
  }
@@ -111,9 +270,22 @@ export function mapSessionMessagesToUpdates(messages) {
111
270
  const updates = [];
112
271
  const toolCalls = new Map();
113
272
  const hiddenToolUseIds = new Set();
273
+ const tasksById = new Map();
274
+ const taskToolUseIds = new Map();
114
275
  for (const entry of messages) {
115
276
  const fallbackRole = entry.type === "assistant" ? "assistant" : "user";
116
- for (const message of messageCandidates(entry.message)) {
277
+ const entrySourceMessageUuid = typeof entry.uuid === "string" ? entry.uuid : undefined;
278
+ const candidates = messageCandidates(entry.message);
279
+ if (entry.type === "system") {
280
+ for (const message of candidates) {
281
+ if (pushResumeTaskSystemUpdate(updates, tasksById, taskToolUseIds, message)) {
282
+ break;
283
+ }
284
+ }
285
+ continue;
286
+ }
287
+ for (const message of candidates) {
288
+ const sourceMessageUuid = typeof message.uuid === "string" ? message.uuid : entrySourceMessageUuid;
117
289
  const roleCandidate = message.role;
118
290
  const role = roleCandidate === "assistant" || roleCandidate === "user" ? roleCandidate : fallbackRole;
119
291
  const parentToolUseId = typeof entry.parent_tool_use_id === "string"
@@ -132,19 +304,19 @@ export function mapSessionMessagesToUpdates(messages) {
132
304
  continue;
133
305
  }
134
306
  if (blockType === "text" && typeof block.text === "string") {
135
- pushResumeTextChunk(updates, role, block.text);
307
+ pushResumeTextChunk(updates, role, block.text, sourceMessageUuid);
136
308
  continue;
137
309
  }
138
310
  if (isToolUseBlockType(blockType) && role === "assistant") {
139
- pushResumeToolUse(updates, toolCalls, hiddenToolUseIds, block, parentToolUseId);
311
+ pushResumeToolUse(updates, toolCalls, hiddenToolUseIds, block, parentToolUseId, sourceMessageUuid);
140
312
  continue;
141
313
  }
142
314
  if (TOOL_RESULT_TYPES.has(blockType)) {
143
- pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block);
315
+ pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block, sourceMessageUuid);
144
316
  continue;
145
317
  }
146
318
  if (blockType === "image") {
147
- pushResumeTextChunk(updates, role, "[image]");
319
+ pushResumeTextChunk(updates, role, "[image]", sourceMessageUuid);
148
320
  }
149
321
  }
150
322
  }