claude-code-rust 0.12.1 → 0.12.3

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
@@ -38,12 +38,8 @@ claude-rs
38
38
 
39
39
  Full documentation is available at [srothgan.github.io/claude-code-rust](https://srothgan.github.io/claude-code-rust/).
40
40
 
41
- > [!WARNING]
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.
43
- >
44
- > Sources:
45
- > - [Anthropic support: Use the Claude Agent SDK with your Claude plan](https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan)
46
- > - [ClaudeDevs announcement](https://x.com/ClaudeDevs/status/2054610152817619388)
41
+ > [!NOTE]
42
+ > **Agent SDK billing unchanged.** Anthropic has paused the previously announced Agent SDK credit change. For now nothing changes: Claude Agent SDK usage — including `claude -p` and third-party apps like this one still draws from your normal Claude subscription limits. See [Use the Claude Agent SDK with your Claude plan](https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan).
47
43
 
48
44
  ## Why
49
45
 
@@ -85,7 +81,7 @@ This project is not affiliated with, endorsed by, or supported by Anthropic.
85
81
 
86
82
  A quick note on where this project stands, since I know people worry about this kind of thing: claude-code-rust is a terminal UI that I wrote from scratch in Rust. It is not a fork, copy or port of the latest Claude Code source leak -- it talks to Anthropic's official [Agent SDK](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/agent-sdk) as a runtime dependency instead, the same way any other third-party tool would. No Anthropic source code was read or used as reference at any point during development.
87
83
 
88
- The project authenticates through your existing Claude Code account via the Agent SDK, and the Agent SDK's terms allow building on top of it. Billing, credits, limits, and overage behavior are controlled by Anthropic, including the Agent SDK credit change noted above. Other community projects do the same. As far as I can tell, using this project is fine -- but I am a single maintainer, not a lawyer. If anything changes on Anthropic's end, I will update this section and adjust the project accordingly.
84
+ The project authenticates through your existing Claude Code account via the Agent SDK, and the Agent SDK's terms allow building on top of it. Billing, credits, limits, and overage behavior are controlled by Anthropic, including any future changes Anthropic may make to how Agent SDK usage is metered. Other community projects do the same. As far as I can tell, using this project is fine -- but I am a single maintainer, not a lawyer. If anything changes on Anthropic's end, I will update this section and adjust the project accordingly.
89
85
 
90
86
  This project's source code is licensed under [Apache-2.0](LICENSE). The Agent SDK itself is proprietary and governed by [Anthropic's Commercial Terms of Service](https://www.anthropic.com/legal/commercial-terms).
91
87
 
@@ -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
  }