claude-code-rust 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,96 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { AsyncQueue, CACHE_SPLIT_POLICY, buildRateLimitUpdate, buildQueryOptions, buildToolResultFields, createToolCall, mapAvailableAgents, mapSessionMessagesToUpdates, mapSdkSessions, agentSdkVersionCompatibilityError, looksLikeAuthRequired, normalizeToolResultText, parseFastModeState, parseRateLimitStatus, normalizeToolKind, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, previewKilobyteLabel, resolveInstalledAgentSdkVersion, unwrapToolUseResult, } from "./bridge.js";
3
+ import { AsyncQueue, CACHE_SPLIT_POLICY, buildRateLimitUpdate, buildQueryOptions, canGenerateSessionTitle, generatePersistedSessionTitle, buildSessionMutationOptions, buildSessionListOptions, buildToolResultFields, createToolCall, handleTaskSystemMessage, mapAvailableAgents, mapAvailableModels, mapSessionMessagesToUpdates, mapSdkSessions, agentSdkVersionCompatibilityError, looksLikeAuthRequired, normalizeToolResultText, parseFastModeState, parseRateLimitStatus, normalizeToolKind, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, previewKilobyteLabel, staleMcpAuthCandidates, resolveInstalledAgentSdkVersion, unwrapToolUseResult, } from "./bridge.js";
4
+ import { emitToolProgressUpdate } from "./bridge/tool_calls.js";
5
+ import { requestAskUserQuestionAnswers } from "./bridge/user_interaction.js";
6
+ function makeSessionState() {
7
+ const input = new AsyncQueue();
8
+ return {
9
+ sessionId: "session-1",
10
+ cwd: "C:/work",
11
+ model: "haiku",
12
+ availableModels: [],
13
+ mode: null,
14
+ fastModeState: "off",
15
+ query: {},
16
+ input,
17
+ connected: true,
18
+ connectEvent: "connected",
19
+ toolCalls: new Map(),
20
+ taskToolUseIds: new Map(),
21
+ pendingPermissions: new Map(),
22
+ pendingQuestions: new Map(),
23
+ pendingElicitations: new Map(),
24
+ mcpStatusRevalidatedAt: new Map(),
25
+ authHintSent: false,
26
+ };
27
+ }
28
+ function captureBridgeEvents(run) {
29
+ const writes = [];
30
+ const originalWrite = process.stdout.write;
31
+ process.stdout.write = (chunk) => {
32
+ if (typeof chunk === "string") {
33
+ writes.push(chunk);
34
+ }
35
+ else if (Buffer.isBuffer(chunk)) {
36
+ writes.push(chunk.toString("utf8"));
37
+ }
38
+ else {
39
+ writes.push(String(chunk));
40
+ }
41
+ return true;
42
+ };
43
+ try {
44
+ run();
45
+ }
46
+ finally {
47
+ process.stdout.write = originalWrite;
48
+ }
49
+ return writes
50
+ .map((line) => line.trim())
51
+ .filter((line) => line.startsWith("{"))
52
+ .flatMap((line) => {
53
+ try {
54
+ return [JSON.parse(line)];
55
+ }
56
+ catch {
57
+ return [];
58
+ }
59
+ });
60
+ }
61
+ async function captureBridgeEventsAsync(run) {
62
+ const writes = [];
63
+ const originalWrite = process.stdout.write;
64
+ process.stdout.write = (chunk) => {
65
+ if (typeof chunk === "string") {
66
+ writes.push(chunk);
67
+ }
68
+ else if (Buffer.isBuffer(chunk)) {
69
+ writes.push(chunk.toString("utf8"));
70
+ }
71
+ else {
72
+ writes.push(String(chunk));
73
+ }
74
+ return true;
75
+ };
76
+ try {
77
+ await run();
78
+ }
79
+ finally {
80
+ process.stdout.write = originalWrite;
81
+ }
82
+ return writes
83
+ .map((line) => line.trim())
84
+ .filter((line) => line.startsWith("{"))
85
+ .flatMap((line) => {
86
+ try {
87
+ return [JSON.parse(line)];
88
+ }
89
+ catch {
90
+ return [];
91
+ }
92
+ });
93
+ }
4
94
  test("parseCommandEnvelope validates initialize command", () => {
5
95
  const parsed = parseCommandEnvelope(JSON.stringify({
6
96
  request_id: "req-1",
@@ -20,11 +110,18 @@ test("parseCommandEnvelope validates resume_session command without cwd", () =>
20
110
  command: "resume_session",
21
111
  session_id: "session-123",
22
112
  launch_settings: {
23
- model: "haiku",
24
113
  language: "German",
25
- permission_mode: "plan",
26
- thinking_mode: "adaptive",
27
- effort_level: "high",
114
+ settings: {
115
+ alwaysThinkingEnabled: true,
116
+ model: "haiku",
117
+ permissions: { defaultMode: "plan" },
118
+ fastMode: false,
119
+ effortLevel: "high",
120
+ outputStyle: "Default",
121
+ spinnerTipsEnabled: true,
122
+ terminalProgressBarEnabled: true,
123
+ },
124
+ agent_progress_summaries: true,
28
125
  },
29
126
  }));
30
127
  assert.equal(parsed.requestId, "req-2");
@@ -33,22 +130,176 @@ test("parseCommandEnvelope validates resume_session command without cwd", () =>
33
130
  throw new Error("unexpected command variant");
34
131
  }
35
132
  assert.equal(parsed.command.session_id, "session-123");
36
- assert.equal(parsed.command.launch_settings.model, "haiku");
37
133
  assert.equal(parsed.command.launch_settings.language, "German");
38
- assert.equal(parsed.command.launch_settings.permission_mode, "plan");
39
- assert.equal(parsed.command.launch_settings.thinking_mode, "adaptive");
40
- assert.equal(parsed.command.launch_settings.effort_level, "high");
134
+ assert.deepEqual(parsed.command.launch_settings.settings, {
135
+ alwaysThinkingEnabled: true,
136
+ model: "haiku",
137
+ permissions: { defaultMode: "plan" },
138
+ fastMode: false,
139
+ effortLevel: "high",
140
+ outputStyle: "Default",
141
+ spinnerTipsEnabled: true,
142
+ terminalProgressBarEnabled: true,
143
+ });
144
+ assert.equal(parsed.command.launch_settings.agent_progress_summaries, true);
145
+ });
146
+ test("parseCommandEnvelope validates rename_session command", () => {
147
+ const parsed = parseCommandEnvelope(JSON.stringify({
148
+ request_id: "req-rename",
149
+ command: "rename_session",
150
+ session_id: "session-123",
151
+ title: "Renamed session",
152
+ }));
153
+ assert.equal(parsed.requestId, "req-rename");
154
+ assert.equal(parsed.command.command, "rename_session");
155
+ if (parsed.command.command !== "rename_session") {
156
+ throw new Error("unexpected command variant");
157
+ }
158
+ assert.equal(parsed.command.session_id, "session-123");
159
+ assert.equal(parsed.command.title, "Renamed session");
160
+ });
161
+ test("parseCommandEnvelope validates generate_session_title command", () => {
162
+ const parsed = parseCommandEnvelope(JSON.stringify({
163
+ request_id: "req-generate",
164
+ command: "generate_session_title",
165
+ session_id: "session-123",
166
+ description: "Current custom title",
167
+ }));
168
+ assert.equal(parsed.requestId, "req-generate");
169
+ assert.equal(parsed.command.command, "generate_session_title");
170
+ if (parsed.command.command !== "generate_session_title") {
171
+ throw new Error("unexpected command variant");
172
+ }
173
+ assert.equal(parsed.command.session_id, "session-123");
174
+ assert.equal(parsed.command.description, "Current custom title");
175
+ });
176
+ test("parseCommandEnvelope validates mcp_toggle command", () => {
177
+ const parsed = parseCommandEnvelope(JSON.stringify({
178
+ request_id: "req-mcp-toggle",
179
+ command: "mcp_toggle",
180
+ session_id: "session-123",
181
+ server_name: "notion",
182
+ enabled: false,
183
+ }));
184
+ assert.equal(parsed.requestId, "req-mcp-toggle");
185
+ assert.equal(parsed.command.command, "mcp_toggle");
186
+ if (parsed.command.command !== "mcp_toggle") {
187
+ throw new Error("unexpected command variant");
188
+ }
189
+ assert.equal(parsed.command.session_id, "session-123");
190
+ assert.equal(parsed.command.server_name, "notion");
191
+ assert.equal(parsed.command.enabled, false);
192
+ });
193
+ test("parseCommandEnvelope validates mcp_set_servers command", () => {
194
+ const parsed = parseCommandEnvelope(JSON.stringify({
195
+ request_id: "req-mcp-set",
196
+ command: "mcp_set_servers",
197
+ session_id: "session-123",
198
+ servers: {
199
+ notion: {
200
+ type: "http",
201
+ url: "https://mcp.notion.com/mcp",
202
+ headers: {
203
+ "X-Test": "1",
204
+ },
205
+ },
206
+ },
207
+ }));
208
+ assert.equal(parsed.requestId, "req-mcp-set");
209
+ assert.equal(parsed.command.command, "mcp_set_servers");
210
+ if (parsed.command.command !== "mcp_set_servers") {
211
+ throw new Error("unexpected command variant");
212
+ }
213
+ assert.equal(parsed.command.session_id, "session-123");
214
+ assert.deepEqual(parsed.command.servers, {
215
+ notion: {
216
+ type: "http",
217
+ url: "https://mcp.notion.com/mcp",
218
+ headers: {
219
+ "X-Test": "1",
220
+ },
221
+ },
222
+ });
223
+ });
224
+ test("staleMcpAuthCandidates selects previously connected servers that regressed to needs-auth", () => {
225
+ const candidates = staleMcpAuthCandidates([
226
+ {
227
+ name: "supabase",
228
+ status: "needs-auth",
229
+ server_info: undefined,
230
+ error: undefined,
231
+ config: undefined,
232
+ scope: undefined,
233
+ tools: [],
234
+ },
235
+ {
236
+ name: "notion",
237
+ status: "needs-auth",
238
+ server_info: undefined,
239
+ error: undefined,
240
+ config: undefined,
241
+ scope: undefined,
242
+ tools: [],
243
+ },
244
+ ], new Set(["supabase"]), new Map(), 10_000, 1_000);
245
+ assert.deepEqual(candidates, ["supabase"]);
246
+ });
247
+ test("staleMcpAuthCandidates respects the revalidation cooldown", () => {
248
+ const candidates = staleMcpAuthCandidates([
249
+ {
250
+ name: "supabase",
251
+ status: "needs-auth",
252
+ server_info: undefined,
253
+ error: undefined,
254
+ config: undefined,
255
+ scope: undefined,
256
+ tools: [],
257
+ },
258
+ ], new Set(["supabase"]), new Map([["supabase", 9_500]]), 10_000, 1_000);
259
+ assert.deepEqual(candidates, []);
260
+ });
261
+ test("buildSessionMutationOptions scopes rename requests to the session cwd", () => {
262
+ assert.deepEqual(buildSessionMutationOptions("C:/worktree"), { dir: "C:/worktree" });
263
+ assert.equal(buildSessionMutationOptions(undefined), undefined);
264
+ });
265
+ test("canGenerateSessionTitle detects supported query objects", () => {
266
+ const query = {
267
+ async generateSessionTitle() {
268
+ return "Generated";
269
+ },
270
+ };
271
+ assert.equal(canGenerateSessionTitle(query), true);
272
+ assert.equal(canGenerateSessionTitle({}), false);
273
+ });
274
+ test("generatePersistedSessionTitle calls sdk query with persist true", async () => {
275
+ const calls = [];
276
+ const query = {
277
+ async generateSessionTitle(description, options) {
278
+ calls.push({ description, persist: options?.persist });
279
+ return "Generated title";
280
+ },
281
+ };
282
+ const title = await generatePersistedSessionTitle(query, "Current summary");
283
+ assert.equal(title, "Generated title");
284
+ assert.deepEqual(calls, [{ description: "Current summary", persist: true }]);
41
285
  });
42
286
  test("buildQueryOptions maps launch settings into sdk query options", () => {
43
287
  const input = new AsyncQueue();
44
288
  const options = buildQueryOptions({
45
289
  cwd: "C:/work",
46
290
  launchSettings: {
47
- model: "haiku",
48
291
  language: "German",
49
- permission_mode: "plan",
50
- thinking_mode: "adaptive",
51
- effort_level: "medium",
292
+ settings: {
293
+ alwaysThinkingEnabled: true,
294
+ model: "haiku",
295
+ permissions: { defaultMode: "plan" },
296
+ fastMode: false,
297
+ effortLevel: "medium",
298
+ outputStyle: "Default",
299
+ spinnerTipsEnabled: true,
300
+ terminalProgressBarEnabled: true,
301
+ },
302
+ agent_progress_summaries: true,
52
303
  },
53
304
  provisionalSessionId: "session-1",
54
305
  input,
@@ -57,26 +308,47 @@ test("buildQueryOptions maps launch settings into sdk query options", () => {
57
308
  enableSpawnDebug: false,
58
309
  sessionIdForLogs: () => "session-1",
59
310
  });
60
- assert.equal(options.model, "haiku");
311
+ assert.deepEqual(options.settings, {
312
+ alwaysThinkingEnabled: true,
313
+ model: "haiku",
314
+ permissions: { defaultMode: "plan" },
315
+ fastMode: false,
316
+ effortLevel: "medium",
317
+ outputStyle: "Default",
318
+ spinnerTipsEnabled: true,
319
+ terminalProgressBarEnabled: true,
320
+ });
61
321
  assert.deepEqual(options.systemPrompt, {
62
322
  type: "preset",
63
323
  preset: "claude_code",
64
324
  append: "Always respond to the user in German unless the user explicitly asks for a different language. " +
65
325
  "Keep code, shell commands, file paths, API names, tool names, and raw error text unchanged unless the user explicitly asks for translation.",
66
326
  });
67
- assert.equal(options.permissionMode, "plan");
68
- assert.deepEqual(options.thinking, { type: "adaptive" });
69
- assert.equal(options.effort, "medium");
327
+ assert.equal("model" in options, false);
328
+ assert.equal("permissionMode" in options, false);
329
+ assert.equal("thinking" in options, false);
330
+ assert.equal("effort" in options, false);
331
+ assert.equal(options.agentProgressSummaries, true);
70
332
  assert.equal(options.sessionId, "session-1");
71
333
  assert.deepEqual(options.settingSources, ["user", "project", "local"]);
334
+ assert.deepEqual(options.toolConfig, {
335
+ askUserQuestion: { previewFormat: "markdown" },
336
+ });
72
337
  });
73
- test("buildQueryOptions maps disabled thinking mode into sdk query options", () => {
338
+ test("buildQueryOptions forwards settings without direct model and permission flags", () => {
74
339
  const input = new AsyncQueue();
75
340
  const options = buildQueryOptions({
76
341
  cwd: "C:/work",
77
342
  launchSettings: {
78
- thinking_mode: "disabled",
79
- effort_level: "high",
343
+ settings: {
344
+ alwaysThinkingEnabled: false,
345
+ permissions: { defaultMode: "default" },
346
+ fastMode: true,
347
+ effortLevel: "high",
348
+ outputStyle: "Learning",
349
+ spinnerTipsEnabled: false,
350
+ terminalProgressBarEnabled: false,
351
+ },
80
352
  },
81
353
  provisionalSessionId: "session-3",
82
354
  input,
@@ -85,7 +357,18 @@ test("buildQueryOptions maps disabled thinking mode into sdk query options", ()
85
357
  enableSpawnDebug: false,
86
358
  sessionIdForLogs: () => "session-3",
87
359
  });
88
- assert.deepEqual(options.thinking, { type: "disabled" });
360
+ assert.deepEqual(options.settings, {
361
+ alwaysThinkingEnabled: false,
362
+ permissions: { defaultMode: "default" },
363
+ fastMode: true,
364
+ effortLevel: "high",
365
+ outputStyle: "Learning",
366
+ spinnerTipsEnabled: false,
367
+ terminalProgressBarEnabled: false,
368
+ });
369
+ assert.equal("model" in options, false);
370
+ assert.equal("permissionMode" in options, false);
371
+ assert.equal("thinking" in options, false);
89
372
  assert.equal("effort" in options, false);
90
373
  });
91
374
  test("buildQueryOptions omits startup overrides for default logout path", () => {
@@ -103,6 +386,133 @@ test("buildQueryOptions omits startup overrides for default logout path", () =>
103
386
  assert.equal("model" in options, false);
104
387
  assert.equal("permissionMode" in options, false);
105
388
  assert.equal("systemPrompt" in options, false);
389
+ assert.equal("agentProgressSummaries" in options, false);
390
+ });
391
+ test("handleTaskSystemMessage prefers task_progress summary over fallback text", () => {
392
+ const session = makeSessionState();
393
+ const events = captureBridgeEvents(() => {
394
+ handleTaskSystemMessage(session, "task_started", {
395
+ task_id: "task-1",
396
+ tool_use_id: "tool-1",
397
+ description: "Initial task description",
398
+ });
399
+ handleTaskSystemMessage(session, "task_progress", {
400
+ task_id: "task-1",
401
+ summary: "Analyzing authentication flow",
402
+ description: "Should not be shown",
403
+ last_tool_name: "Read",
404
+ });
405
+ });
406
+ const lastEvent = events.at(-1);
407
+ assert.ok(lastEvent);
408
+ assert.equal(lastEvent.event, "session_update");
409
+ assert.deepEqual(lastEvent.update, {
410
+ type: "tool_call_update",
411
+ tool_call_update: {
412
+ tool_call_id: "tool-1",
413
+ fields: {
414
+ status: "in_progress",
415
+ raw_output: "Analyzing authentication flow",
416
+ content: [
417
+ {
418
+ type: "content",
419
+ content: { type: "text", text: "Analyzing authentication flow" },
420
+ },
421
+ ],
422
+ },
423
+ },
424
+ });
425
+ });
426
+ test("handleTaskSystemMessage falls back to description and last tool when progress summary is absent", () => {
427
+ const session = makeSessionState();
428
+ const events = captureBridgeEvents(() => {
429
+ handleTaskSystemMessage(session, "task_started", {
430
+ task_id: "task-1",
431
+ tool_use_id: "tool-1",
432
+ description: "Initial task description",
433
+ });
434
+ handleTaskSystemMessage(session, "task_progress", {
435
+ task_id: "task-1",
436
+ description: "Inspecting auth code",
437
+ last_tool_name: "Read",
438
+ });
439
+ });
440
+ const lastEvent = events.at(-1);
441
+ assert.ok(lastEvent);
442
+ assert.equal(lastEvent.event, "session_update");
443
+ assert.deepEqual(lastEvent.update, {
444
+ type: "tool_call_update",
445
+ tool_call_update: {
446
+ tool_call_id: "tool-1",
447
+ fields: {
448
+ status: "in_progress",
449
+ raw_output: "Inspecting auth code (last tool: Read)",
450
+ content: [
451
+ {
452
+ type: "content",
453
+ content: { type: "text", text: "Inspecting auth code (last tool: Read)" },
454
+ },
455
+ ],
456
+ },
457
+ },
458
+ });
459
+ });
460
+ test("handleTaskSystemMessage final summary replaces prior task content and finalizes status", () => {
461
+ const session = makeSessionState();
462
+ const events = captureBridgeEvents(() => {
463
+ handleTaskSystemMessage(session, "task_started", {
464
+ task_id: "task-1",
465
+ tool_use_id: "tool-1",
466
+ description: "Initial task description",
467
+ });
468
+ handleTaskSystemMessage(session, "task_progress", {
469
+ task_id: "task-1",
470
+ summary: "Analyzing authentication flow",
471
+ description: "Should not be shown",
472
+ });
473
+ handleTaskSystemMessage(session, "task_notification", {
474
+ task_id: "task-1",
475
+ status: "completed",
476
+ summary: "Found the auth bug and prepared the fix",
477
+ });
478
+ });
479
+ const lastEvent = events.at(-1);
480
+ assert.ok(lastEvent);
481
+ assert.equal(lastEvent.event, "session_update");
482
+ assert.deepEqual(lastEvent.update, {
483
+ type: "tool_call_update",
484
+ tool_call_update: {
485
+ tool_call_id: "tool-1",
486
+ fields: {
487
+ status: "completed",
488
+ raw_output: "Found the auth bug and prepared the fix",
489
+ content: [
490
+ {
491
+ type: "content",
492
+ content: { type: "text", text: "Found the auth bug and prepared the fix" },
493
+ },
494
+ ],
495
+ },
496
+ },
497
+ });
498
+ assert.equal(session.taskToolUseIds.has("task-1"), false);
499
+ });
500
+ test("emitToolProgressUpdate does not reopen completed tools", () => {
501
+ const session = makeSessionState();
502
+ session.toolCalls.set("tool-1", {
503
+ tool_call_id: "tool-1",
504
+ title: "Bash",
505
+ kind: "execute",
506
+ status: "completed",
507
+ content: [],
508
+ locations: [],
509
+ meta: { claudeCode: { toolName: "Bash" } },
510
+ });
511
+ const events = captureBridgeEvents(() => {
512
+ emitToolProgressUpdate(session, "tool-1", "Bash");
513
+ });
514
+ assert.equal(events.length, 0);
515
+ assert.equal(session.toolCalls.get("tool-1")?.status, "completed");
106
516
  });
107
517
  test("buildQueryOptions trims language before appending system prompt", () => {
108
518
  const input = new AsyncQueue();
@@ -128,6 +538,172 @@ test("buildQueryOptions trims language before appending system prompt", () => {
128
538
  test("parseCommandEnvelope rejects missing required fields", () => {
129
539
  assert.throws(() => parseCommandEnvelope(JSON.stringify({ command: "set_model", session_id: "s1" })), /set_model\.model must be a string/);
130
540
  });
541
+ test("parseCommandEnvelope validates question_response command", () => {
542
+ const parsed = parseCommandEnvelope(JSON.stringify({
543
+ request_id: "req-question",
544
+ command: "question_response",
545
+ session_id: "session-1",
546
+ tool_call_id: "tool-1",
547
+ outcome: {
548
+ outcome: "answered",
549
+ selected_option_ids: ["question_0", "question_2"],
550
+ annotation: {
551
+ preview: "Rendered preview",
552
+ notes: "User note",
553
+ },
554
+ },
555
+ }));
556
+ assert.equal(parsed.requestId, "req-question");
557
+ assert.equal(parsed.command.command, "question_response");
558
+ if (parsed.command.command !== "question_response") {
559
+ throw new Error("unexpected command variant");
560
+ }
561
+ assert.deepEqual(parsed.command.outcome, {
562
+ outcome: "answered",
563
+ selected_option_ids: ["question_0", "question_2"],
564
+ annotation: {
565
+ preview: "Rendered preview",
566
+ notes: "User note",
567
+ },
568
+ });
569
+ });
570
+ test("requestAskUserQuestionAnswers preserves previews and annotations in updated input", async () => {
571
+ const session = makeSessionState();
572
+ const baseToolCall = {
573
+ tool_call_id: "tool-question",
574
+ title: "AskUserQuestion",
575
+ kind: "other",
576
+ status: "in_progress",
577
+ content: [],
578
+ locations: [],
579
+ meta: { claudeCode: { toolName: "AskUserQuestion" } },
580
+ };
581
+ const events = await captureBridgeEventsAsync(async () => {
582
+ const resultPromise = requestAskUserQuestionAnswers(session, "tool-question", {
583
+ questions: [
584
+ {
585
+ question: "Pick deployment target",
586
+ header: "Target",
587
+ multiSelect: true,
588
+ options: [
589
+ {
590
+ label: "Staging",
591
+ description: "Low-risk validation",
592
+ preview: "Deploy to staging first.",
593
+ },
594
+ {
595
+ label: "Production",
596
+ description: "Customer-facing rollout",
597
+ preview: "Deploy to production after approval.",
598
+ },
599
+ ],
600
+ },
601
+ ],
602
+ }, baseToolCall);
603
+ await new Promise((resolve) => setImmediate(resolve));
604
+ const pending = session.pendingQuestions.get("tool-question");
605
+ assert.ok(pending, "expected pending question");
606
+ pending.onOutcome({
607
+ outcome: "answered",
608
+ selected_option_ids: ["question_0", "question_1"],
609
+ annotation: {
610
+ notes: "Roll out in both environments",
611
+ },
612
+ });
613
+ const result = await resultPromise;
614
+ assert.equal(result.behavior, "allow");
615
+ if (result.behavior !== "allow") {
616
+ throw new Error("expected allow result");
617
+ }
618
+ assert.deepEqual(result.updatedInput, {
619
+ questions: [
620
+ {
621
+ question: "Pick deployment target",
622
+ header: "Target",
623
+ multiSelect: true,
624
+ options: [
625
+ {
626
+ label: "Staging",
627
+ description: "Low-risk validation",
628
+ preview: "Deploy to staging first.",
629
+ },
630
+ {
631
+ label: "Production",
632
+ description: "Customer-facing rollout",
633
+ preview: "Deploy to production after approval.",
634
+ },
635
+ ],
636
+ },
637
+ ],
638
+ answers: {
639
+ "Pick deployment target": "Staging, Production",
640
+ },
641
+ annotations: {
642
+ "Pick deployment target": {
643
+ preview: "Deploy to staging first.\n\nDeploy to production after approval.",
644
+ notes: "Roll out in both environments",
645
+ },
646
+ },
647
+ });
648
+ });
649
+ const questionEvent = events.find((event) => event.event === "question_request");
650
+ assert.ok(questionEvent, "expected question request event");
651
+ assert.deepEqual(questionEvent.request, {
652
+ tool_call: {
653
+ tool_call_id: "tool-question",
654
+ title: "Pick deployment target",
655
+ kind: "other",
656
+ status: "in_progress",
657
+ content: [],
658
+ locations: [],
659
+ meta: { claudeCode: { toolName: "AskUserQuestion" } },
660
+ raw_input: {
661
+ prompt: {
662
+ question: "Pick deployment target",
663
+ header: "Target",
664
+ multi_select: true,
665
+ options: [
666
+ {
667
+ option_id: "question_0",
668
+ label: "Staging",
669
+ description: "Low-risk validation",
670
+ preview: "Deploy to staging first.",
671
+ },
672
+ {
673
+ option_id: "question_1",
674
+ label: "Production",
675
+ description: "Customer-facing rollout",
676
+ preview: "Deploy to production after approval.",
677
+ },
678
+ ],
679
+ },
680
+ question_index: 0,
681
+ total_questions: 1,
682
+ },
683
+ },
684
+ prompt: {
685
+ question: "Pick deployment target",
686
+ header: "Target",
687
+ multi_select: true,
688
+ options: [
689
+ {
690
+ option_id: "question_0",
691
+ label: "Staging",
692
+ description: "Low-risk validation",
693
+ preview: "Deploy to staging first.",
694
+ },
695
+ {
696
+ option_id: "question_1",
697
+ label: "Production",
698
+ description: "Customer-facing rollout",
699
+ preview: "Deploy to production after approval.",
700
+ },
701
+ ],
702
+ },
703
+ question_index: 0,
704
+ total_questions: 1,
705
+ });
706
+ });
131
707
  test("normalizeToolKind maps known tool names", () => {
132
708
  assert.equal(normalizeToolKind("Bash"), "execute");
133
709
  assert.equal(normalizeToolKind("Delete"), "delete");
@@ -324,6 +900,9 @@ test("buildToolResultFields maps structured Write output to diff content", () =>
324
900
  content: "new",
325
901
  originalFile: "old",
326
902
  structuredPatch: [],
903
+ gitDiff: {
904
+ repository: "acme/project",
905
+ },
327
906
  }, base);
328
907
  assert.equal(fields.status, "completed");
329
908
  assert.deepEqual(fields.content, [
@@ -333,16 +912,24 @@ test("buildToolResultFields maps structured Write output to diff content", () =>
333
912
  new_path: "src/main.ts",
334
913
  old: "old",
335
914
  new: "new",
915
+ repository: "acme/project",
336
916
  },
337
917
  ]);
338
918
  });
339
- test("buildToolResultFields preserves Edit diff content from input", () => {
919
+ test("buildToolResultFields preserves Edit diff content from input and structured repository", () => {
340
920
  const base = createToolCall("tc-e", "Edit", {
341
921
  file_path: "src/main.ts",
342
922
  old_string: "old",
343
923
  new_string: "new",
344
924
  });
345
- const fields = buildToolResultFields(false, [{ text: "Updated successfully" }], base);
925
+ const fields = buildToolResultFields(false, [{ text: "Updated successfully" }], base, {
926
+ result: {
927
+ filePath: "src/main.ts",
928
+ gitDiff: {
929
+ repository: "acme/project",
930
+ },
931
+ },
932
+ });
346
933
  assert.equal(fields.status, "completed");
347
934
  assert.deepEqual(fields.content, [
348
935
  {
@@ -351,6 +938,120 @@ test("buildToolResultFields preserves Edit diff content from input", () => {
351
938
  new_path: "src/main.ts",
352
939
  old: "old",
353
940
  new: "new",
941
+ repository: "acme/project",
942
+ },
943
+ ]);
944
+ });
945
+ test("buildToolResultFields prefers structured Bash stdout over token-saver output", () => {
946
+ const base = createToolCall("tc-bash", "Bash", { command: "npm test" });
947
+ const fields = buildToolResultFields(false, {
948
+ stdout: "real stdout",
949
+ stderr: "",
950
+ interrupted: false,
951
+ tokenSaverOutput: "compressed output for model",
952
+ }, base, {
953
+ result: {
954
+ stdout: "real stdout",
955
+ stderr: "",
956
+ interrupted: false,
957
+ tokenSaverOutput: "compressed output for model",
958
+ },
959
+ });
960
+ assert.equal(fields.raw_output, "real stdout");
961
+ assert.deepEqual(fields.output_metadata, {
962
+ bash: {
963
+ token_saver_active: true,
964
+ },
965
+ });
966
+ });
967
+ test("buildToolResultFields adds Bash auto-backgrounded metadata and message", () => {
968
+ const base = createToolCall("tc-bash-bg", "Bash", { command: "npm run watch" });
969
+ const fields = buildToolResultFields(false, {
970
+ stdout: "",
971
+ stderr: "",
972
+ interrupted: false,
973
+ backgroundTaskId: "task-42",
974
+ assistantAutoBackgrounded: true,
975
+ }, base, {
976
+ result: {
977
+ stdout: "",
978
+ stderr: "",
979
+ interrupted: false,
980
+ backgroundTaskId: "task-42",
981
+ assistantAutoBackgrounded: true,
982
+ },
983
+ });
984
+ assert.equal(fields.raw_output, "Command was auto-backgrounded by assistant mode with ID: task-42.");
985
+ assert.deepEqual(fields.output_metadata, {
986
+ bash: {
987
+ assistant_auto_backgrounded: true,
988
+ },
989
+ });
990
+ });
991
+ test("buildToolResultFields maps structured ReadMcpResource output to typed resource content", () => {
992
+ const base = createToolCall("tc-mcp", "ReadMcpResource", {
993
+ server: "docs",
994
+ uri: "file://manual.pdf",
995
+ });
996
+ const fields = buildToolResultFields(false, {
997
+ contents: [
998
+ {
999
+ uri: "file://manual.pdf",
1000
+ mimeType: "application/pdf",
1001
+ text: "[Resource from docs at file://manual.pdf] Saved to C:\\tmp\\manual.pdf",
1002
+ blobSavedTo: "C:\\tmp\\manual.pdf",
1003
+ },
1004
+ ],
1005
+ }, base, {
1006
+ result: {
1007
+ contents: [
1008
+ {
1009
+ uri: "file://manual.pdf",
1010
+ mimeType: "application/pdf",
1011
+ text: "[Resource from docs at file://manual.pdf] Saved to C:\\tmp\\manual.pdf",
1012
+ blobSavedTo: "C:\\tmp\\manual.pdf",
1013
+ },
1014
+ ],
1015
+ },
1016
+ });
1017
+ assert.equal(fields.status, "completed");
1018
+ assert.deepEqual(fields.content, [
1019
+ {
1020
+ type: "mcp_resource",
1021
+ uri: "file://manual.pdf",
1022
+ mime_type: "application/pdf",
1023
+ text: "[Resource from docs at file://manual.pdf] Saved to C:\\tmp\\manual.pdf",
1024
+ blob_saved_to: "C:\\tmp\\manual.pdf",
1025
+ },
1026
+ ]);
1027
+ });
1028
+ test("buildToolResultFields restores ReadMcpResource blob paths from transcript JSON text", () => {
1029
+ const base = createToolCall("tc-mcp-history", "ReadMcpResource", {
1030
+ server: "docs",
1031
+ uri: "file://manual.pdf",
1032
+ });
1033
+ const transcriptJson = JSON.stringify({
1034
+ contents: [
1035
+ {
1036
+ uri: "file://manual.pdf",
1037
+ mimeType: "application/pdf",
1038
+ text: "[Resource from docs at file://manual.pdf] Saved to C:\\tmp\\manual.pdf",
1039
+ blobSavedTo: "C:\\tmp\\manual.pdf",
1040
+ },
1041
+ ],
1042
+ });
1043
+ const fields = buildToolResultFields(false, transcriptJson, base, {
1044
+ type: "tool_result",
1045
+ tool_use_id: "tc-mcp-history",
1046
+ content: transcriptJson,
1047
+ });
1048
+ assert.deepEqual(fields.content, [
1049
+ {
1050
+ type: "mcp_resource",
1051
+ uri: "file://manual.pdf",
1052
+ mime_type: "application/pdf",
1053
+ text: "[Resource from docs at file://manual.pdf] Saved to C:\\tmp\\manual.pdf",
1054
+ blob_saved_to: "C:\\tmp\\manual.pdf",
354
1055
  },
355
1056
  ]);
356
1057
  });
@@ -492,7 +1193,7 @@ test("looksLikeAuthRequired detects login hints", () => {
492
1193
  assert.equal(looksLikeAuthRequired("normal tool output"), false);
493
1194
  });
494
1195
  test("agent sdk version compatibility check matches pinned version", () => {
495
- assert.equal(resolveInstalledAgentSdkVersion(), "0.2.63");
1196
+ assert.equal(resolveInstalledAgentSdkVersion(), "0.2.74");
496
1197
  assert.equal(agentSdkVersionCompatibilityError(), undefined);
497
1198
  });
498
1199
  test("mapSessionMessagesToUpdates maps message content blocks", () => {
@@ -607,3 +1308,83 @@ test("mapSdkSessions normalizes and sorts sessions", () => {
607
1308
  },
608
1309
  ]);
609
1310
  });
1311
+ test("buildSessionListOptions scopes repo-local listings to worktrees", () => {
1312
+ assert.deepEqual(buildSessionListOptions("C:/repo"), {
1313
+ dir: "C:/repo",
1314
+ includeWorktrees: true,
1315
+ limit: 50,
1316
+ });
1317
+ assert.deepEqual(buildSessionListOptions(undefined), {
1318
+ limit: 50,
1319
+ });
1320
+ });
1321
+ test("buildToolResultFields extracts ExitPlanMode ultraplan metadata from structured results", () => {
1322
+ const base = createToolCall("tc-plan", "ExitPlanMode", {});
1323
+ const fields = buildToolResultFields(false, [{ text: "Plan ready for approval" }], base, {
1324
+ result: {
1325
+ plan: "Plan contents",
1326
+ isUltraplan: true,
1327
+ },
1328
+ });
1329
+ assert.deepEqual(fields.output_metadata, {
1330
+ exit_plan_mode: {
1331
+ is_ultraplan: true,
1332
+ },
1333
+ });
1334
+ });
1335
+ test("buildToolResultFields extracts TodoWrite verification metadata from structured results", () => {
1336
+ const base = createToolCall("tc-todo", "TodoWrite", {
1337
+ todos: [{ content: "Verify changes", status: "pending", activeForm: "Verifying changes" }],
1338
+ });
1339
+ const fields = buildToolResultFields(false, [{ text: "Todos have been modified successfully." }], base, {
1340
+ data: {
1341
+ oldTodos: [],
1342
+ newTodos: [],
1343
+ verificationNudgeNeeded: true,
1344
+ },
1345
+ });
1346
+ assert.deepEqual(fields.output_metadata, {
1347
+ todo_write: {
1348
+ verification_nudge_needed: true,
1349
+ },
1350
+ });
1351
+ });
1352
+ test("mapAvailableModels preserves optional fast and auto mode metadata", () => {
1353
+ const mapped = mapAvailableModels([
1354
+ {
1355
+ value: "sonnet",
1356
+ displayName: "Claude Sonnet",
1357
+ description: "Balanced model",
1358
+ supportsEffort: true,
1359
+ supportedEffortLevels: ["low", "medium", "high", "max"],
1360
+ supportsAdaptiveThinking: true,
1361
+ supportsFastMode: true,
1362
+ supportsAutoMode: false,
1363
+ },
1364
+ {
1365
+ value: "haiku",
1366
+ displayName: "Claude Haiku",
1367
+ description: "Fast model",
1368
+ supportsEffort: false,
1369
+ },
1370
+ ]);
1371
+ assert.deepEqual(mapped, [
1372
+ {
1373
+ id: "sonnet",
1374
+ display_name: "Claude Sonnet",
1375
+ description: "Balanced model",
1376
+ supports_effort: true,
1377
+ supported_effort_levels: ["low", "medium", "high"],
1378
+ supports_adaptive_thinking: true,
1379
+ supports_fast_mode: true,
1380
+ supports_auto_mode: false,
1381
+ },
1382
+ {
1383
+ id: "haiku",
1384
+ display_name: "Claude Haiku",
1385
+ description: "Fast model",
1386
+ supports_effort: false,
1387
+ supported_effort_levels: [],
1388
+ },
1389
+ ]);
1390
+ });