claude-code-rust 0.14.1 → 0.14.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.
@@ -11,7 +11,7 @@ import { isToolSearchToolName } from "./tooling.js";
11
11
  import { requestExitPlanModeApproval, requestAskUserQuestionAnswers, EXIT_PLAN_MODE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "./user_interaction.js";
12
12
  import { mapAvailableAgents, emitAvailableAgentsIfChanged, refreshAvailableAgents } from "./agents.js";
13
13
  import { mapSdkSlashCommands, updateAvailableCommands, } from "./available_commands.js";
14
- import { emitAuthRequired, emitFastModeUpdateIfChanged } from "./error_classification.js";
14
+ import { emitAuthRequired, emitFastModeUpdate, setFastModeSnapshotIfChanged, } from "./error_classification.js";
15
15
  import { mapAvailableModels, resolveCurrentModel, currentModelsEqual, } from "./model_metadata.js";
16
16
  import { shouldEmitStartupAuthRequiredForAccount } from "./account_metadata.js";
17
17
  export { mapAvailableModels, resolveCurrentModel } from "./model_metadata.js";
@@ -34,6 +34,7 @@ function permissionDisplayFromCanUseOptions(options) {
34
34
  };
35
35
  }
36
36
  export const sessions = new Map();
37
+ const pendingSessionCloseTasks = new Set();
37
38
  const DEFAULT_SETTING_SOURCES = ["user", "project", "local"];
38
39
  const DEFAULT_PERMISSION_MODE = "default";
39
40
  function isSdkElicitationContentValue(value) {
@@ -134,7 +135,44 @@ export function updateSessionId(session, newSessionId) {
134
135
  session.sessionId = newSessionId;
135
136
  sessions.set(newSessionId, session);
136
137
  }
138
+ export function beginSessionClose(session) {
139
+ session.closing = true;
140
+ for (const monitor of session.mcpAuthMonitors.values()) {
141
+ monitor.controller.abort();
142
+ }
143
+ }
144
+ export function detachSessionForClose(session) {
145
+ beginSessionClose(session);
146
+ if (sessions.get(session.sessionId) === session) {
147
+ sessions.delete(session.sessionId);
148
+ }
149
+ }
150
+ export function trackSessionCloseTask(task) {
151
+ const ownedTask = task.catch((error) => {
152
+ bridgeLogger.error({
153
+ target: LOG_TARGETS.APP_SESSION,
154
+ eventName: "session_close_task_failed",
155
+ message: "background session cleanup failed",
156
+ outcome: "failure",
157
+ fields: {
158
+ error_message: error instanceof Error ? error.message : String(error),
159
+ },
160
+ });
161
+ });
162
+ pendingSessionCloseTasks.add(ownedTask);
163
+ void ownedTask.then(() => {
164
+ pendingSessionCloseTasks.delete(ownedTask);
165
+ });
166
+ }
167
+ async function waitForPendingSessionCloseTasks() {
168
+ while (pendingSessionCloseTasks.size > 0) {
169
+ await Promise.all(Array.from(pendingSessionCloseTasks));
170
+ }
171
+ }
137
172
  export async function closeSession(session) {
173
+ beginSessionClose(session);
174
+ const mcpAuthMonitors = Array.from(session.mcpAuthMonitors.values());
175
+ session.mcpAuthMonitors.clear();
138
176
  session.input.close();
139
177
  session.query.close();
140
178
  for (const pending of session.pendingPermissions.values()) {
@@ -154,6 +192,11 @@ export async function closeSession(session) {
154
192
  pending.resolve({ action: "cancel" });
155
193
  }
156
194
  session.pendingElicitations.clear();
195
+ await Promise.all([
196
+ session.initializationTask,
197
+ session.queryConsumerTask,
198
+ ...mcpAuthMonitors.map((monitor) => monitor.task),
199
+ ].filter((task) => task !== undefined));
157
200
  }
158
201
  export async function closeSessionWithLogging(session, options = {}) {
159
202
  await closeSession(session);
@@ -191,6 +234,7 @@ export async function closeAllSessions(options = {}) {
191
234
  reason: options.reason ?? "bulk_close",
192
235
  requestId: options.requestId,
193
236
  })));
237
+ await waitForPendingSessionCloseTasks();
194
238
  bridgeLogger.info({
195
239
  target: LOG_TARGETS.APP_SESSION,
196
240
  eventName: "all_sessions_closed",
@@ -335,6 +379,7 @@ export async function createSession(params) {
335
379
  query: queryHandle,
336
380
  input,
337
381
  connected: false,
382
+ closing: false,
338
383
  connectEvent: params.connectEvent,
339
384
  connectRequestId: params.requestId,
340
385
  toolCalls: new Map(),
@@ -347,7 +392,9 @@ export async function createSession(params) {
347
392
  pendingUserDialogs: new Map(),
348
393
  pendingElicitations: new Map(),
349
394
  informationalDedupKeys: new Set(),
395
+ knownConnectedMcpServers: new Set(),
350
396
  mcpStatusRevalidatedAt: new Map(),
397
+ mcpAuthMonitors: new Map(),
351
398
  hiddenToolUseIds: new Set(),
352
399
  authHintSent: false,
353
400
  ...(params.resumeUpdates && params.resumeUpdates.length > 0
@@ -396,7 +443,7 @@ export async function createSession(params) {
396
443
  // In stream-input mode the SDK may defer init until input arrives.
397
444
  // Trigger initialization explicitly so the Rust UI can receive `connected`
398
445
  // before the first user prompt.
399
- void session.query
446
+ session.initializationTask = session.query
400
447
  .initializationResult()
401
448
  .then(async (result) => {
402
449
  bridgeLogger.info({
@@ -416,6 +463,7 @@ export async function createSession(params) {
416
463
  const currentModelChanged = refreshCurrentModel(session);
417
464
  const { buildModeState, refreshSupportedModesForSession } = await import("./commands.js");
418
465
  refreshSupportedModesForSession(session);
466
+ const fastModeChanged = setFastModeSnapshotIfChanged(session, result.fast_mode_state, result.fast_mode_disabled_reason);
419
467
  if (!session.connected) {
420
468
  emitConnectEvent(session);
421
469
  }
@@ -429,13 +477,15 @@ export async function createSession(params) {
429
477
  mode: buildModeState(session, session.mode),
430
478
  });
431
479
  }
480
+ if (fastModeChanged) {
481
+ emitFastModeUpdate(session);
482
+ }
432
483
  }
433
484
  // Proactively detect missing auth from account info so the UI can
434
485
  // show the login hint immediately, without waiting for the first prompt.
435
486
  if (shouldEmitStartupAuthRequiredForAccount(result.account)) {
436
487
  emitAuthRequired(session);
437
488
  }
438
- emitFastModeUpdateIfChanged(session, result.fast_mode_state);
439
489
  updateAvailableCommands(session, "session_result_commands", mapSdkSlashCommands(result.commands));
440
490
  emitAvailableAgentsIfChanged(session, mapAvailableAgents(result.agents));
441
491
  refreshAvailableAgents(session);
@@ -457,7 +507,7 @@ export async function createSession(params) {
457
507
  failConnection(`agent initialization failed: ${message}`, session.connectRequestId);
458
508
  session.connectRequestId = undefined;
459
509
  });
460
- void (async () => {
510
+ session.queryConsumerTask = (async () => {
461
511
  try {
462
512
  for await (const message of session.query) {
463
513
  // Lazy import to break circular dependency at module-evaluation time.
@@ -15,7 +15,7 @@ function nonNegativeNumberField(record, ...keys) {
15
15
  }
16
16
  return value;
17
17
  }
18
- function nonNegativeIntegerField(record, ...keys) {
18
+ export function nonNegativeIntegerField(record, ...keys) {
19
19
  const value = numberField(record, ...keys);
20
20
  return value !== undefined && value >= 0 && Number.isInteger(value) ? value : undefined;
21
21
  }
@@ -49,6 +49,13 @@ export function parseFastModeState(value) {
49
49
  }
50
50
  return null;
51
51
  }
52
+ export function parseFastModeDisabledReason(value) {
53
+ if (typeof value !== "string") {
54
+ return undefined;
55
+ }
56
+ const reason = value.trim();
57
+ return reason.length > 0 ? reason : undefined;
58
+ }
52
59
  export function parseRateLimitStatus(value) {
53
60
  if (value === "allowed" || value === "allowed_warning" || value === "rejected") {
54
61
  return value;
@@ -3,7 +3,7 @@ import { bridgeLogger, LOG_TARGETS } from "./logger.js";
3
3
  import { asRecordOrNull } from "./shared.js";
4
4
  import { applyTaskToolResult } from "./tasks.js";
5
5
  import { activeTaskIdForToolUse, linkTaskToolUse, unlinkTaskToolUse } from "./task_links.js";
6
- import { backgroundToolLaunchTaskIdFromResult, buildToolResultFields, createToolCall, } from "./tooling.js";
6
+ import { applyToolNonExecutionMetadata, backgroundToolLaunchTaskIdFromResult, buildToolResultFields, createToolCall, } from "./tooling.js";
7
7
  const TOOL_SUMMARY_TOOL_NAMES = new Set(["Agent", "Task", "WebSearch", "WebFetch", "ExitPlanMode"]);
8
8
  const TASK_LIFECYCLE_TOOL_NAMES = new Set(["Agent", "Task", "Monitor", "Workflow"]);
9
9
  function jsonSize(value) {
@@ -308,18 +308,19 @@ export function ensureToolCallVisible(session, toolUseId, toolName, input, paren
308
308
  emitInitialToolCall(session, toolCall);
309
309
  return toolCall;
310
310
  }
311
- export function emitToolResultUpdate(session, toolUseId, isError, rawContent, rawResult = rawContent, sourceMessageUuid) {
311
+ export function emitToolResultUpdate(session, toolUseId, isError, rawContent, rawResult = rawContent, sourceMessageUuid, nonExecutionMetadata) {
312
312
  const base = session.toolCalls.get(toolUseId);
313
313
  const baseToolName = toolNameFromMeta(base?.meta) ?? "";
314
314
  const fields = buildToolResultFields(isError, rawContent, base, rawResult, taskTitleContext(session, baseToolName, asRecordOrNull(base?.raw_input) ?? {}));
315
- if (!isError) {
315
+ applyToolNonExecutionMetadata(fields, nonExecutionMetadata);
316
+ if (!isError && !nonExecutionMetadata) {
316
317
  const taskId = backgroundToolLaunchTaskIdFromResult(baseToolName, rawResult, rawContent);
317
318
  if (taskId) {
318
319
  linkTaskToolUse(session, taskId, toolUseId);
319
320
  }
320
321
  }
321
322
  emitToolCallUpdate(session, toolUseId, fields, "result", sourceMessageUuid);
322
- applyTaskToolResult(session, toolUseId, isError, rawContent, rawResult);
323
+ applyTaskToolResult(session, toolUseId, isError || nonExecutionMetadata !== undefined, rawContent, rawResult);
323
324
  if (baseToolName === "Agent" || baseToolName === "Task") {
324
325
  const taskId = activeTaskIdForToolUse(session, toolUseId);
325
326
  if (taskId) {
@@ -338,14 +339,15 @@ export function finalizeOpenToolCalls(session, status) {
338
339
  emitToolCallUpdate(session, toolUseId, { status }, "finalize");
339
340
  }
340
341
  }
341
- export function emitToolProgressUpdate(session, toolUseId, toolName, progress = {}) {
342
- let existing = session.toolCalls.get(toolUseId);
342
+ /**
343
+ * Apply advisory progress only to a tool created by an authoritative tool-use event.
344
+ */
345
+ export function emitToolProgressUpdate(session, toolUseId, progress = {}) {
346
+ const existing = session.toolCalls.get(toolUseId);
343
347
  if (!existing) {
344
- emitToolCall(session, toolUseId, toolName, {});
345
- existing = session.toolCalls.get(toolUseId);
348
+ return;
346
349
  }
347
- if (!existing ||
348
- existing.status === "completed" ||
350
+ if (existing.status === "completed" ||
349
351
  existing.status === "failed" ||
350
352
  existing.status === "killed") {
351
353
  return;
@@ -23,6 +23,19 @@ const WORKFLOW_TOOL_NAME = "Workflow";
23
23
  const PROJECTS_TOOL_NAME = "Projects";
24
24
  const ARTIFACT_TOOL_NAME = "Artifact";
25
25
  const SHOW_ONBOARDING_ROLE_PICKER_TOOL_NAME = "ShowOnboardingRolePicker";
26
+ const SKILL_TOOL_NAME = "Skill";
27
+ const SKILL_WORD_OVERRIDES = {
28
+ api: "API",
29
+ ci: "CI",
30
+ cli: "CLI",
31
+ gh: "GH",
32
+ github: "GitHub",
33
+ mcp: "MCP",
34
+ pdf: "PDF",
35
+ sdk: "SDK",
36
+ ui: "UI",
37
+ ux: "UX",
38
+ };
26
39
  const READ_MCP_RESOURCE_TOOL_NAME = "ReadMcpResource";
27
40
  const READ_MCP_RESOURCE_DIR_TOOL_NAME = "ReadMcpResourceDir";
28
41
  const SEARCH_OUTPUT_MODES = new Set(["content", "files_with_matches", "count"]);
@@ -183,6 +196,7 @@ export function normalizeToolKind(name) {
183
196
  case "Projects":
184
197
  case "Artifact":
185
198
  case "ShowOnboardingRolePicker":
199
+ case SKILL_TOOL_NAME:
186
200
  return "other";
187
201
  case "Task":
188
202
  case "Agent":
@@ -194,6 +208,34 @@ export function normalizeToolKind(name) {
194
208
  return "think";
195
209
  }
196
210
  }
211
+ function skillDisplayName(rawName) {
212
+ const trimmed = rawName.trim();
213
+ const segments = trimmed.split(":");
214
+ if (segments.length > 2 ||
215
+ segments.some((segment) => !segment || !/^[a-zA-Z0-9]+(?:[-_][a-zA-Z0-9]+)*$/.test(segment))) {
216
+ return trimmed;
217
+ }
218
+ const displaySegments = segments.map((segment) => segment
219
+ .split(/[-_]/)
220
+ .map((word) => skillDisplayWord(word))
221
+ .join(" "));
222
+ if (displaySegments.length === 2 &&
223
+ displaySegments[0].toLowerCase() === displaySegments[1].toLowerCase()) {
224
+ return displaySegments[1];
225
+ }
226
+ return displaySegments.join(" / ");
227
+ }
228
+ function skillDisplayWord(word) {
229
+ const override = SKILL_WORD_OVERRIDES[word.toLowerCase()];
230
+ if (override) {
231
+ return override;
232
+ }
233
+ if (/^[A-Z0-9]+$/.test(word) ||
234
+ (/[a-z]/.test(word) && /[A-Z]/.test(word) && !/^[A-Z][a-z0-9]*$/.test(word))) {
235
+ return word;
236
+ }
237
+ return `${word.charAt(0).toUpperCase()}${word.slice(1).toLowerCase()}`;
238
+ }
197
239
  export function toolTitle(name, input, context = {}) {
198
240
  const agentTitle = agentInputTitle(name, input);
199
241
  if (agentTitle) {
@@ -263,6 +305,12 @@ export function toolTitle(name, input, context = {}) {
263
305
  if (name === SHOW_ONBOARDING_ROLE_PICKER_TOOL_NAME) {
264
306
  return SHOW_ONBOARDING_ROLE_PICKER_TOOL_NAME;
265
307
  }
308
+ if (name === SKILL_TOOL_NAME) {
309
+ const skillName = nonEmptyString(input.skill);
310
+ return skillName
311
+ ? `${SKILL_TOOL_NAME}: ${skillDisplayName(skillName)}`
312
+ : SKILL_TOOL_NAME;
313
+ }
266
314
  if (name === "EnterWorktree") {
267
315
  const worktreeName = typeof input.name === "string" ? input.name.trim() : "";
268
316
  return worktreeName || "EnterWorktree";
@@ -369,6 +417,17 @@ function resultRecordCandidates(rawResult, rawContent) {
369
417
  pushNestedRecords(rawContent);
370
418
  return candidates;
371
419
  }
420
+ function imageReadResultText(rawResult, rawContent, rawInput) {
421
+ const isImage = resultRecordCandidates(rawResult, rawContent).some((candidate) => candidate.type === "image");
422
+ if (!isImage) {
423
+ return undefined;
424
+ }
425
+ const input = asRecordOrNull(rawInput);
426
+ const filePath = typeof input?.file_path === "string" ? input.file_path.trim() : "";
427
+ const normalizedPath = filePath.replaceAll("\\", "/");
428
+ const fileName = normalizedPath.slice(normalizedPath.lastIndexOf("/") + 1);
429
+ return fileName ? `Viewed Image ${fileName}` : "Viewed Image";
430
+ }
372
431
  function parseJsonCandidate(value) {
373
432
  const text = typeof value === "string" ? value : extractText(value);
374
433
  const trimmed = text.trim();
@@ -528,7 +587,59 @@ function extractToolOutputMetadata(toolName, rawResult, rawContent) {
528
587
  }
529
588
  }
530
589
  }
531
- return metadata.bash || metadata.agent || metadata.web_fetch ? metadata : undefined;
590
+ if (toolName === "Skill") {
591
+ for (const candidate of candidates) {
592
+ if (candidate.background === true) {
593
+ metadata.skill = { background: true };
594
+ break;
595
+ }
596
+ }
597
+ }
598
+ return metadata.bash || metadata.agent || metadata.web_fetch || metadata.skill
599
+ ? metadata
600
+ : undefined;
601
+ }
602
+ export function parseToolNonExecutionMetadata(value) {
603
+ const byToolUseId = new Map();
604
+ if (!Array.isArray(value)) {
605
+ return byToolUseId;
606
+ }
607
+ for (const entry of value) {
608
+ const record = asRecordOrNull(entry);
609
+ const id = nonEmptyString(record?.id);
610
+ const kind = nonEmptyString(record?.non_execution_kind);
611
+ if (!id || !kind || byToolUseId.has(id)) {
612
+ continue;
613
+ }
614
+ const userFeedback = nonEmptyString(record?.user_feedback);
615
+ byToolUseId.set(id, {
616
+ kind,
617
+ ...(userFeedback ? { user_feedback: userFeedback } : {}),
618
+ });
619
+ }
620
+ return byToolUseId;
621
+ }
622
+ export function applyToolNonExecutionMetadata(fields, metadata) {
623
+ if (!metadata) {
624
+ return;
625
+ }
626
+ fields.output_metadata = {
627
+ ...(fields.output_metadata ?? {}),
628
+ non_execution: metadata,
629
+ };
630
+ switch (metadata.kind) {
631
+ case "user-rejected":
632
+ case "permission-rule":
633
+ case "automode-unavailable":
634
+ case "automode-parsing-error":
635
+ case "automode-blocked":
636
+ fields.status = "failed";
637
+ break;
638
+ case "cancelled":
639
+ case "interrupted":
640
+ fields.status = "killed";
641
+ break;
642
+ }
532
643
  }
533
644
  export function extractText(value) {
534
645
  if (typeof value === "string") {
@@ -1541,6 +1652,51 @@ function collectResultCandidates(rawResult, rawContent) {
1541
1652
  }
1542
1653
  return candidates;
1543
1654
  }
1655
+ function parseSkillResult(toolName, rawResult, rawContent) {
1656
+ if (toolName !== SKILL_TOOL_NAME) {
1657
+ return undefined;
1658
+ }
1659
+ for (const candidate of collectResultCandidates(rawResult, rawContent)) {
1660
+ const commandName = nonEmptyString(candidate.commandName);
1661
+ if (!commandName || typeof candidate.success !== "boolean") {
1662
+ continue;
1663
+ }
1664
+ if (candidate.status === "forked") {
1665
+ const agentId = nonEmptyString(candidate.agentId);
1666
+ if (!agentId || typeof candidate.result !== "string") {
1667
+ continue;
1668
+ }
1669
+ if (candidate.background !== undefined && typeof candidate.background !== "boolean") {
1670
+ continue;
1671
+ }
1672
+ return {
1673
+ success: candidate.success,
1674
+ commandName,
1675
+ status: "forked",
1676
+ agentId,
1677
+ result: candidate.result,
1678
+ ...(typeof candidate.background === "boolean"
1679
+ ? { background: candidate.background }
1680
+ : {}),
1681
+ };
1682
+ }
1683
+ if (candidate.status === undefined || candidate.status === "inline") {
1684
+ const allowedToolsValid = candidate.allowedTools === undefined ||
1685
+ (Array.isArray(candidate.allowedTools) &&
1686
+ candidate.allowedTools.every((tool) => typeof tool === "string"));
1687
+ const modelValid = candidate.model === undefined || typeof candidate.model === "string";
1688
+ if (!allowedToolsValid || !modelValid) {
1689
+ continue;
1690
+ }
1691
+ return {
1692
+ success: candidate.success,
1693
+ commandName,
1694
+ status: "inline",
1695
+ };
1696
+ }
1697
+ }
1698
+ return undefined;
1699
+ }
1544
1700
  function monitorResultFields(toolName, rawResult, rawContent) {
1545
1701
  if (toolName !== MONITOR_TOOL_NAME) {
1546
1702
  return undefined;
@@ -1745,6 +1901,32 @@ export function buildToolResultFields(isError, rawContent, base, rawResult, _con
1745
1901
  if (outputMetadata) {
1746
1902
  fields.output_metadata = outputMetadata;
1747
1903
  }
1904
+ const skillResult = !isError ? parseSkillResult(toolName, rawResult, rawContent) : undefined;
1905
+ if (skillResult) {
1906
+ fields.title = `${SKILL_TOOL_NAME}: ${skillDisplayName(skillResult.commandName)}`;
1907
+ if (!skillResult.success) {
1908
+ fields.status = "failed";
1909
+ }
1910
+ else if (skillResult.status === "inline") {
1911
+ return fields;
1912
+ }
1913
+ else {
1914
+ const output = skillResult.result.trim();
1915
+ if (output) {
1916
+ fields.raw_output = output;
1917
+ fields.content = [{ type: "content", content: { type: "text", text: output } }];
1918
+ }
1919
+ return fields;
1920
+ }
1921
+ }
1922
+ const imageReadText = !isError && toolName === "Read"
1923
+ ? imageReadResultText(rawResult, rawContent, base?.raw_input)
1924
+ : undefined;
1925
+ if (imageReadText !== undefined) {
1926
+ fields.raw_output = imageReadText;
1927
+ fields.content = [{ type: "content", content: { type: "text", text: imageReadText } }];
1928
+ return fields;
1929
+ }
1748
1930
  const fileUnchangedText = !isError && toolName === "Read" ? fileUnchangedResultText(rawResult, rawContent) : "";
1749
1931
  if (fileUnchangedText) {
1750
1932
  fields.raw_output = fileUnchangedText;