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.
@@ -0,0 +1,203 @@
1
+ import { getSessionMessages, renameSession, } from "@anthropic-ai/claude-agent-sdk";
2
+ import { mapSdkAccountInfo } from "./account_metadata.js";
3
+ import { emitSessionsList, setSessionListingDir, slashError, writeEvent, } from "./events.js";
4
+ import { bridgeLogger, LOG_TARGETS } from "./logger.js";
5
+ import { refreshCurrentModel, sessionById, } from "./session_lifecycle.js";
6
+ export async function handleSessionDataCommand(command, requestId, deps) {
7
+ switch (command.command) {
8
+ case "generate_session_title":
9
+ await generateTitle(command, requestId, deps);
10
+ return;
11
+ case "rename_session":
12
+ await rename(command, requestId, deps);
13
+ return;
14
+ case "get_status_snapshot":
15
+ await getStatusSnapshot(command, requestId);
16
+ return;
17
+ case "get_context_usage":
18
+ await getContextUsage(command, requestId);
19
+ return;
20
+ case "get_rewind_targets":
21
+ await getRewindTargets(command, requestId, deps);
22
+ return;
23
+ case "rewind":
24
+ await deps.handleRewind(command, requestId);
25
+ }
26
+ }
27
+ async function generateTitle(command, requestId, deps) {
28
+ const session = requireSession(command.session_id, requestId);
29
+ if (!session) {
30
+ return;
31
+ }
32
+ try {
33
+ await deps.generatePersistedSessionTitle(session.query, command.description);
34
+ setSessionListingDir(session.cwd);
35
+ await emitSessionsList(requestId);
36
+ }
37
+ catch (error) {
38
+ const message = error instanceof Error ? error.message : String(error);
39
+ slashError(command.session_id, `failed to generate session title: ${message}`, requestId);
40
+ }
41
+ }
42
+ async function rename(command, requestId, deps) {
43
+ const session = requireSession(command.session_id, requestId);
44
+ if (!session) {
45
+ return;
46
+ }
47
+ try {
48
+ await renameSession(command.session_id, command.title, deps.buildSessionMutationOptions(session.cwd));
49
+ setSessionListingDir(session.cwd);
50
+ await emitSessionsList(requestId);
51
+ }
52
+ catch (error) {
53
+ const message = error instanceof Error ? error.message : String(error);
54
+ slashError(command.session_id, `failed to rename session: ${message}`, requestId);
55
+ }
56
+ }
57
+ async function getStatusSnapshot(command, requestId) {
58
+ const session = requireSession(command.session_id, requestId);
59
+ if (!session) {
60
+ return;
61
+ }
62
+ try {
63
+ const account = await session.query.accountInfo();
64
+ bridgeLogger.info({
65
+ target: LOG_TARGETS.APP_AUTH,
66
+ eventName: "status_snapshot_emitted",
67
+ message: "status snapshot emitted",
68
+ outcome: "success",
69
+ ...(requestId ? { requestId } : {}),
70
+ sessionId: session.sessionId,
71
+ fields: {
72
+ has_email: typeof account.email === "string" && account.email.trim().length > 0,
73
+ has_organization: account.organization !== undefined,
74
+ subscription_type: account.subscriptionType,
75
+ token_source: account.tokenSource,
76
+ api_key_source: account.apiKeySource,
77
+ api_provider: account.apiProvider,
78
+ },
79
+ });
80
+ writeEvent({
81
+ event: "status_snapshot",
82
+ session_id: session.sessionId,
83
+ account: mapSdkAccountInfo(account),
84
+ }, requestId);
85
+ }
86
+ catch (error) {
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ bridgeLogger.warn({
89
+ target: LOG_TARGETS.APP_AUTH,
90
+ eventName: "status_snapshot_failed",
91
+ message: "failed to build status snapshot",
92
+ outcome: "failure",
93
+ ...(requestId ? { requestId } : {}),
94
+ sessionId: session.sessionId,
95
+ fields: { error_message: message },
96
+ });
97
+ throw error;
98
+ }
99
+ }
100
+ async function getContextUsage(command, requestId) {
101
+ const session = requireSession(command.session_id, requestId);
102
+ if (!session) {
103
+ return;
104
+ }
105
+ try {
106
+ const usage = await session.query.getContextUsage();
107
+ if (typeof usage.model === "string" && usage.model.trim().length > 0) {
108
+ session.resolvedRuntimeModelId = usage.model.trim();
109
+ refreshCurrentModel(session, true);
110
+ }
111
+ const rawPercentage = typeof usage.percentage === "number" ? usage.percentage : undefined;
112
+ const normalizedPercentage = rawPercentage === undefined || !Number.isFinite(rawPercentage)
113
+ ? undefined
114
+ : Math.max(0, Math.min(100, Math.round(rawPercentage)));
115
+ bridgeLogger.debug({
116
+ target: LOG_TARGETS.APP_SESSION,
117
+ eventName: "context_usage_succeeded",
118
+ message: "session context usage received from SDK",
119
+ outcome: "success",
120
+ ...(requestId ? { requestId } : {}),
121
+ sessionId: session.sessionId,
122
+ fields: {
123
+ raw_percentage: rawPercentage,
124
+ normalized_percentage: normalizedPercentage,
125
+ total_tokens: typeof usage.totalTokens === "number" ? usage.totalTokens : undefined,
126
+ max_tokens: typeof usage.maxTokens === "number" ? usage.maxTokens : undefined,
127
+ raw_max_tokens: typeof usage.rawMaxTokens === "number" ? usage.rawMaxTokens : undefined,
128
+ model: typeof usage.model === "string" ? usage.model : undefined,
129
+ },
130
+ });
131
+ writeEvent({
132
+ event: "context_usage",
133
+ session_id: session.sessionId,
134
+ ...(normalizedPercentage !== undefined ? { percentage: normalizedPercentage } : {}),
135
+ }, requestId);
136
+ }
137
+ catch (error) {
138
+ const message = error instanceof Error ? error.message : String(error);
139
+ bridgeLogger.warn({
140
+ target: LOG_TARGETS.APP_SESSION,
141
+ eventName: "context_usage_failed",
142
+ message: "failed to get session context usage",
143
+ outcome: "failure",
144
+ ...(requestId ? { requestId } : {}),
145
+ sessionId: session.sessionId,
146
+ fields: { error_message: message },
147
+ });
148
+ writeEvent({
149
+ event: "context_usage",
150
+ session_id: session.sessionId,
151
+ }, requestId);
152
+ }
153
+ }
154
+ async function getRewindTargets(command, requestId, deps) {
155
+ const session = requireSession(command.session_id, requestId);
156
+ if (!session) {
157
+ return;
158
+ }
159
+ try {
160
+ const historyMessages = await getSessionMessages(command.session_id, {
161
+ dir: session.cwd,
162
+ includeSystemMessages: true,
163
+ });
164
+ const targets = deps.rewindTargetsFromSessionMessages(historyMessages);
165
+ bridgeLogger.info({
166
+ target: LOG_TARGETS.APP_SESSION,
167
+ eventName: "rewind_targets_loaded",
168
+ message: "rewind targets loaded from session history",
169
+ outcome: "success",
170
+ ...(requestId ? { requestId } : {}),
171
+ sessionId: session.sessionId,
172
+ fields: {
173
+ history_message_count: historyMessages.length,
174
+ target_count: targets.length,
175
+ },
176
+ });
177
+ writeEvent({
178
+ event: "rewind_targets",
179
+ session_id: session.sessionId,
180
+ targets,
181
+ }, requestId);
182
+ }
183
+ catch (error) {
184
+ const message = error instanceof Error ? error.message : String(error);
185
+ bridgeLogger.warn({
186
+ target: LOG_TARGETS.APP_SESSION,
187
+ eventName: "rewind_targets_failed",
188
+ message: "failed to load rewind targets",
189
+ outcome: "failure",
190
+ ...(requestId ? { requestId } : {}),
191
+ sessionId: session.sessionId,
192
+ fields: { error_message: message },
193
+ });
194
+ slashError(command.session_id, `failed to load rewind targets: ${message}`, requestId);
195
+ }
196
+ }
197
+ function requireSession(sessionId, requestId) {
198
+ const session = sessionById(sessionId);
199
+ if (!session) {
200
+ slashError(sessionId, `unknown session: ${sessionId}`, requestId);
201
+ }
202
+ return session;
203
+ }
@@ -266,6 +266,12 @@ export function parseCommandEnvelope(line) {
266
266
  session_id: expectString(raw, "session_id", "set_agent"),
267
267
  agent: expectNonEmptyStringOrNull(raw, "agent", "set_agent"),
268
268
  };
269
+ case "set_fast_mode":
270
+ return {
271
+ command: "set_fast_mode",
272
+ session_id: expectString(raw, "session_id", "set_fast_mode"),
273
+ enabled: expectBoolean(raw, "enabled", "set_fast_mode"),
274
+ };
269
275
  case "generate_session_title":
270
276
  return {
271
277
  command: "generate_session_title",
@@ -1,7 +1,7 @@
1
1
  import { looksLikeAuthRequired } from "./auth.js";
2
2
  import { writeEvent } from "./events.js";
3
3
  import { emitSessionUpdate } from "./events.js";
4
- import { parseFastModeState } from "./state_parsing.js";
4
+ import { parseFastModeDisabledReason, parseFastModeState } from "./state_parsing.js";
5
5
  export function emitAuthRequired(session, detail) {
6
6
  if (session.authHintSent) {
7
7
  return;
@@ -58,11 +58,28 @@ export function classifyTurnErrorKind(subtype, errors, assistantError) {
58
58
  }
59
59
  return "other";
60
60
  }
61
- export function emitFastModeUpdateIfChanged(session, value) {
62
- const next = parseFastModeState(value);
63
- if (!next || next === session.fastModeState) {
64
- return;
61
+ export function setFastModeSnapshotIfChanged(session, stateValue, disabledReasonValue) {
62
+ const nextState = parseFastModeState(stateValue) ?? session.fastModeState;
63
+ const nextDisabledReason = parseFastModeDisabledReason(disabledReasonValue);
64
+ if (nextState === session.fastModeState &&
65
+ nextDisabledReason === session.fastModeDisabledReason) {
66
+ return false;
67
+ }
68
+ session.fastModeState = nextState;
69
+ session.fastModeDisabledReason = nextDisabledReason;
70
+ return true;
71
+ }
72
+ export function emitFastModeUpdate(session) {
73
+ emitSessionUpdate(session.sessionId, {
74
+ type: "fast_mode_update",
75
+ fast_mode_state: session.fastModeState,
76
+ ...(session.fastModeDisabledReason
77
+ ? { fast_mode_disabled_reason: session.fastModeDisabledReason }
78
+ : {}),
79
+ });
80
+ }
81
+ export function emitFastModeUpdateIfChanged(session, stateValue, disabledReasonValue) {
82
+ if (setFastModeSnapshotIfChanged(session, stateValue, disabledReasonValue)) {
83
+ emitFastModeUpdate(session);
65
84
  }
66
- session.fastModeState = next;
67
- emitSessionUpdate(session.sessionId, { type: "fast_mode_update", fast_mode_state: next });
68
85
  }
@@ -1,10 +1,30 @@
1
1
  import { listSessions } from "@anthropic-ai/claude-agent-sdk";
2
+ import { writeSync } from "node:fs";
2
3
  import { buildModeState } from "./commands.js";
3
4
  import { mapSdkSessions } from "./history.js";
4
5
  import { bridgeLogger, LOG_TARGETS, logBridgeEventSent } from "./logger.js";
5
- import { resolveCurrentModel } from "./session_lifecycle.js";
6
+ import { detachSessionForClose, resolveCurrentModel, trackSessionCloseTask, } from "./session_lifecycle.js";
6
7
  const SESSION_LIST_LIMIT = 50;
7
8
  let sessionListingDir;
9
+ function writeProtocolEventToStdout(line) {
10
+ const payload = Buffer.from(line);
11
+ let offset = 0;
12
+ while (offset < payload.length) {
13
+ const written = writeSync(process.stdout.fd, payload, offset, payload.length - offset);
14
+ if (written <= 0) {
15
+ throw new Error("bridge stdout write made no progress");
16
+ }
17
+ offset += written;
18
+ }
19
+ }
20
+ let protocolEventWriter = writeProtocolEventToStdout;
21
+ export function replaceProtocolEventWriter(writer) {
22
+ const previous = protocolEventWriter;
23
+ protocolEventWriter = writer;
24
+ return () => {
25
+ protocolEventWriter = previous;
26
+ };
27
+ }
8
28
  export function buildSessionListOptions(dir, limit = SESSION_LIST_LIMIT) {
9
29
  return dir
10
30
  ? { dir, includeProgrammatic: true, includeWorktrees: true, limit }
@@ -23,7 +43,7 @@ export function writeEvent(event, requestId) {
23
43
  };
24
44
  const serialized = JSON.stringify(envelope);
25
45
  logBridgeEventSent(event, requestId, Buffer.byteLength(serialized) + 1);
26
- process.stdout.write(`${serialized}\n`);
46
+ protocolEventWriter(`${serialized}\n`);
27
47
  }
28
48
  export function failConnection(message, requestId) {
29
49
  writeEvent({ event: "connection_failed", message }, requestId);
@@ -112,7 +132,7 @@ export function emitElicitationRequestEvent(sessionId, request) {
112
132
  });
113
133
  writeEvent({ event: "elicitation_request", session_id: sessionId, request });
114
134
  }
115
- function buildConnectBridgeEvent(session, eventName) {
135
+ export function buildConnectBridgeEvent(session, eventName) {
116
136
  const historyUpdates = session.resumeUpdates;
117
137
  return eventName === "session_replaced"
118
138
  ? {
@@ -122,6 +142,10 @@ function buildConnectBridgeEvent(session, eventName) {
122
142
  current_model: session.currentModel ?? resolveCurrentModel(session),
123
143
  available_models: session.availableModels,
124
144
  mode: session.mode ? buildModeState(session, session.mode) : null,
145
+ fast_mode_state: session.fastModeState,
146
+ ...(session.fastModeDisabledReason
147
+ ? { fast_mode_disabled_reason: session.fastModeDisabledReason }
148
+ : {}),
125
149
  ...(historyUpdates && historyUpdates.length > 0 ? { history_updates: historyUpdates } : {}),
126
150
  ...(session.restoredInput !== undefined ? { restored_input: session.restoredInput } : {}),
127
151
  }
@@ -132,6 +156,10 @@ function buildConnectBridgeEvent(session, eventName) {
132
156
  current_model: session.currentModel ?? resolveCurrentModel(session),
133
157
  available_models: session.availableModels,
134
158
  mode: session.mode ? buildModeState(session, session.mode) : null,
159
+ fast_mode_state: session.fastModeState,
160
+ ...(session.fastModeDisabledReason
161
+ ? { fast_mode_disabled_reason: session.fastModeDisabledReason }
162
+ : {}),
135
163
  ...(historyUpdates && historyUpdates.length > 0 ? { history_updates: historyUpdates } : {}),
136
164
  };
137
165
  }
@@ -152,6 +180,14 @@ function logConnectEventEmission(session, eventName, requestId) {
152
180
  });
153
181
  }
154
182
  export function emitConnectEvent(session) {
183
+ const staleSessions = session.sessionsToCloseAfterConnect;
184
+ if (staleSessions) {
185
+ for (const stale of staleSessions) {
186
+ if (stale !== session) {
187
+ detachSessionForClose(stale);
188
+ }
189
+ }
190
+ }
155
191
  const bridgeEvent = buildConnectBridgeEvent(session, session.connectEvent);
156
192
  logConnectEventEmission(session, session.connectEvent, session.connectRequestId);
157
193
  writeEvent(bridgeEvent, session.connectRequestId);
@@ -164,26 +200,23 @@ export function emitConnectEvent(session) {
164
200
  session.authHintSent = false;
165
201
  session.resumeUpdates = undefined;
166
202
  session.restoredInput = undefined;
167
- const staleSessions = session.sessionsToCloseAfterConnect;
168
203
  session.sessionsToCloseAfterConnect = undefined;
169
204
  if (!staleSessions || staleSessions.length === 0) {
170
205
  refreshSessionsList();
171
206
  return;
172
207
  }
173
- void (async () => {
208
+ const closeTask = (async () => {
174
209
  // Lazy import to break circular dependency at module-evaluation time.
175
- const { sessions, closeSessionWithLogging } = await import("./session_lifecycle.js");
210
+ const { closeSessionWithLogging } = await import("./session_lifecycle.js");
176
211
  for (const stale of staleSessions) {
177
212
  if (stale === session) {
178
213
  continue;
179
214
  }
180
- if (sessions.get(stale.sessionId) === stale) {
181
- sessions.delete(stale.sessionId);
182
- }
183
215
  await closeSessionWithLogging(stale, { reason: "stale_after_connect" });
184
216
  }
185
217
  refreshSessionsList();
186
218
  })();
219
+ trackSessionCloseTask(closeTask);
187
220
  }
188
221
  export function emitSessionReplacedEvent(session, requestId) {
189
222
  const bridgeEvent = buildConnectBridgeEvent(session, "session_replaced");
@@ -1,5 +1,5 @@
1
1
  import { asRecordOrNull } from "./shared.js";
2
- import { TOOL_RESULT_TYPES, buildToolResultFields, createToolCall, isToolSearchToolName, isToolSearchToolResultType, isToolUseBlockType, } from "./tooling.js";
2
+ import { applyToolNonExecutionMetadata, TOOL_RESULT_TYPES, buildToolResultFields, createToolCall, isToolSearchToolName, isToolSearchToolResultType, isToolUseBlockType, parseToolNonExecutionMetadata, } from "./tooling.js";
3
3
  function nonEmptyTrimmed(value) {
4
4
  if (typeof value !== "string") {
5
5
  return undefined;
@@ -197,7 +197,7 @@ function pushResumeToolUse(updates, toolCalls, hiddenToolUseIds, block, parentTo
197
197
  toolCalls.set(toolUseId, toolCall);
198
198
  updates.push({ type: "tool_call", tool_call: toolCall });
199
199
  }
200
- function pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block, sourceMessageUuid) {
200
+ function pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block, nonExecutionByToolUseId, sourceMessageUuid) {
201
201
  const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : "";
202
202
  if (!toolUseId) {
203
203
  return;
@@ -210,6 +210,7 @@ function pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block, sourc
210
210
  const isError = Boolean(block.is_error);
211
211
  const base = toolCalls.get(toolUseId);
212
212
  const fields = buildToolResultFields(isError, block.content, base, block);
213
+ applyToolNonExecutionMetadata(fields, nonExecutionByToolUseId.get(toolUseId));
213
214
  updates.push({
214
215
  type: "tool_call_update",
215
216
  tool_call_update: {
@@ -294,6 +295,9 @@ export function mapSessionMessagesToUpdates(messages) {
294
295
  ? message.parent_tool_use_id
295
296
  : null;
296
297
  const content = Array.isArray(message.content) ? message.content : [];
298
+ const nonExecutionByToolUseId = parseToolNonExecutionMetadata(Object.hasOwn(entry, "tool_result_meta")
299
+ ? entry.tool_result_meta
300
+ : message.tool_result_meta);
297
301
  for (const item of content) {
298
302
  const block = asRecordOrNull(item);
299
303
  if (!block) {
@@ -312,7 +316,7 @@ export function mapSessionMessagesToUpdates(messages) {
312
316
  continue;
313
317
  }
314
318
  if (TOOL_RESULT_TYPES.has(blockType)) {
315
- pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block, sourceMessageUuid);
319
+ pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block, nonExecutionByToolUseId, sourceMessageUuid);
316
320
  continue;
317
321
  }
318
322
  if (blockType === "image") {
@@ -65,6 +65,7 @@ function commandSessionId(command) {
65
65
  case "cancel_turn":
66
66
  case "set_model":
67
67
  case "set_mode":
68
+ case "set_fast_mode":
68
69
  case "generate_session_title":
69
70
  case "rename_session":
70
71
  case "permission_response":
@@ -103,6 +104,7 @@ function commandToolCallId(command) {
103
104
  case "cancel_turn":
104
105
  case "set_model":
105
106
  case "set_mode":
107
+ case "set_fast_mode":
106
108
  case "generate_session_title":
107
109
  case "rename_session":
108
110
  case "new_session":