wave-code 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/acp/agent.ts CHANGED
@@ -1,20 +1,45 @@
1
- import { Agent as WaveAgent, AgentOptions } from "wave-agent-sdk";
1
+ import {
2
+ Agent as WaveAgent,
3
+ AgentOptions,
4
+ PermissionDecision,
5
+ ToolPermissionContext,
6
+ AgentToolBlockUpdateParams,
7
+ Task,
8
+ listSessions as listWaveSessions,
9
+ listAllSessions as listAllWaveSessions,
10
+ deleteSession as deleteWaveSession,
11
+ truncateContent,
12
+ } from "wave-agent-sdk";
13
+ import * as fs from "node:fs/promises";
14
+ import * as path from "node:path";
2
15
  import { logger } from "../utils/logger.js";
3
- import type {
4
- Agent as AcpAgent,
5
- AgentSideConnection,
6
- InitializeResponse,
7
- NewSessionRequest,
8
- NewSessionResponse,
9
- LoadSessionRequest,
10
- LoadSessionResponse,
11
- PromptRequest,
12
- PromptResponse,
13
- CancelNotification,
14
- AuthenticateResponse,
15
- SessionId as AcpSessionId,
16
- ToolCallStatus,
17
- StopReason,
16
+ import {
17
+ type Agent as AcpAgent,
18
+ type AgentSideConnection,
19
+ type InitializeResponse,
20
+ type NewSessionRequest,
21
+ type NewSessionResponse,
22
+ type LoadSessionRequest,
23
+ type LoadSessionResponse,
24
+ type ListSessionsRequest,
25
+ type ListSessionsResponse,
26
+ type PromptRequest,
27
+ type PromptResponse,
28
+ type CancelNotification,
29
+ type AuthenticateResponse,
30
+ type SessionId as AcpSessionId,
31
+ type ToolCallStatus,
32
+ type StopReason,
33
+ type PermissionOption,
34
+ type SessionInfo,
35
+ type ToolCallContent,
36
+ type ToolCallLocation,
37
+ type ToolKind,
38
+ type SessionConfigOption,
39
+ type SetSessionModeRequest,
40
+ type SetSessionConfigOptionRequest,
41
+ type SetSessionConfigOptionResponse,
42
+ AGENT_METHODS,
18
43
  } from "@agentclientprotocol/sdk";
19
44
 
20
45
  export class WaveAcpAgent implements AcpAgent {
@@ -25,8 +50,65 @@ export class WaveAcpAgent implements AcpAgent {
25
50
  this.connection = connection;
26
51
  }
27
52
 
53
+ private getSessionModeState(agent: WaveAgent) {
54
+ return {
55
+ currentModeId: agent.getPermissionMode(),
56
+ availableModes: [
57
+ {
58
+ id: "default",
59
+ name: "Default",
60
+ description: "Ask for permission for restricted tools",
61
+ },
62
+ {
63
+ id: "acceptEdits",
64
+ name: "Accept Edits",
65
+ description: "Automatically accept file edits",
66
+ },
67
+ {
68
+ id: "plan",
69
+ name: "Plan",
70
+ description: "Plan mode for complex tasks",
71
+ },
72
+ {
73
+ id: "bypassPermissions",
74
+ name: "Bypass Permissions",
75
+ description: "Automatically accept all tool calls",
76
+ },
77
+ ],
78
+ };
79
+ }
80
+
81
+ private getSessionConfigOptions(agent: WaveAgent): SessionConfigOption[] {
82
+ return [
83
+ {
84
+ id: "permission_mode",
85
+ name: "Permission Mode",
86
+ type: "select",
87
+ category: "mode",
88
+ currentValue: agent.getPermissionMode(),
89
+ options: [
90
+ { value: "default", name: "Default" },
91
+ { value: "acceptEdits", name: "Accept Edits" },
92
+ { value: "plan", name: "Plan" },
93
+ { value: "bypassPermissions", name: "Bypass Permissions" },
94
+ ],
95
+ },
96
+ ];
97
+ }
98
+
99
+ private async cleanupAllAgents() {
100
+ logger.info("Cleaning up all active agents due to connection closure");
101
+ const destroyPromises = Array.from(this.agents.values()).map((agent) =>
102
+ agent.destroy(),
103
+ );
104
+ await Promise.all(destroyPromises);
105
+ this.agents.clear();
106
+ }
107
+
28
108
  async initialize(): Promise<InitializeResponse> {
29
109
  logger.info("Initializing WaveAcpAgent");
110
+ // Setup cleanup on connection closure
111
+ this.connection.closed.then(() => this.cleanupAllAgents());
30
112
  return {
31
113
  protocolVersion: 1,
32
114
  agentInfo: {
@@ -35,6 +117,10 @@ export class WaveAcpAgent implements AcpAgent {
35
117
  },
36
118
  agentCapabilities: {
37
119
  loadSession: true,
120
+ sessionCapabilities: {
121
+ list: {},
122
+ close: {},
123
+ },
38
124
  },
39
125
  };
40
126
  }
@@ -43,12 +129,26 @@ export class WaveAcpAgent implements AcpAgent {
43
129
  // No authentication required for now
44
130
  }
45
131
 
46
- async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
47
- const { cwd } = params;
48
- logger.info(`Creating new session in ${cwd}`);
132
+ private async createAgent(
133
+ sessionId: string | undefined,
134
+ cwd: string,
135
+ ): Promise<WaveAgent> {
49
136
  const callbacks: AgentOptions["callbacks"] = {};
137
+ const agentRef: { instance?: WaveAgent } = {};
138
+
50
139
  const agent = await WaveAgent.create({
51
140
  workdir: cwd,
141
+ restoreSessionId: sessionId,
142
+ stream: false,
143
+ canUseTool: (context) => {
144
+ if (!agentRef.instance) {
145
+ throw new Error("Agent instance not yet initialized");
146
+ }
147
+ return this.handlePermissionRequest(
148
+ agentRef.instance.sessionId,
149
+ context,
150
+ );
151
+ },
52
152
  callbacks: {
53
153
  onAssistantContentUpdated: (chunk: string) =>
54
154
  callbacks.onAssistantContentUpdated?.(chunk, ""),
@@ -60,62 +160,142 @@ export class WaveAcpAgent implements AcpAgent {
60
160
  | undefined;
61
161
  cb?.(params);
62
162
  },
63
- onTasksChange: (tasks: unknown[]) => {
64
- const cb = callbacks.onTasksChange as
65
- | ((tasks: unknown[]) => void)
66
- | undefined;
67
- cb?.(tasks);
68
- },
163
+ onTasksChange: (tasks) => callbacks.onTasksChange?.(tasks as Task[]),
164
+ onPermissionModeChange: (mode) =>
165
+ callbacks.onPermissionModeChange?.(mode),
69
166
  },
70
167
  });
71
168
 
72
- const sessionId = agent.sessionId;
73
- logger.info(`New session created: ${sessionId}`);
74
- this.agents.set(sessionId, agent);
169
+ agentRef.instance = agent;
170
+ const actualSessionId = agent.sessionId;
171
+ this.agents.set(actualSessionId, agent);
75
172
 
76
173
  // Update the callbacks object with the correct sessionId
77
- Object.assign(callbacks, this.createCallbacks(sessionId));
174
+ Object.assign(callbacks, this.createCallbacks(actualSessionId));
175
+
176
+ // Send initial available commands after agent creation
177
+ // Use setImmediate to ensure the client receives the session response before the update
178
+ setImmediate(() => {
179
+ this.connection.sessionUpdate({
180
+ sessionId: actualSessionId as AcpSessionId,
181
+ update: {
182
+ sessionUpdate: "available_commands_update",
183
+ availableCommands: agent.getSlashCommands().map((cmd) => ({
184
+ name: cmd.name,
185
+ description: cmd.description,
186
+ input: {
187
+ hint: "Enter arguments...",
188
+ },
189
+ })),
190
+ },
191
+ });
192
+ });
193
+
194
+ return agent;
195
+ }
196
+
197
+ async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
198
+ const { cwd } = params;
199
+ logger.info(`Creating new session in ${cwd}`);
200
+ const agent = await this.createAgent(undefined, cwd);
201
+ logger.info(`New session created with ID: ${agent.sessionId}`);
78
202
 
79
203
  return {
80
- sessionId: sessionId as AcpSessionId,
204
+ sessionId: agent.sessionId as AcpSessionId,
205
+ modes: this.getSessionModeState(agent),
206
+ configOptions: this.getSessionConfigOptions(agent),
81
207
  };
82
208
  }
83
209
 
84
210
  async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
85
- const { sessionId } = params;
86
- logger.info(`Loading session: ${sessionId}`);
87
- const callbacks: AgentOptions["callbacks"] = {};
88
- const agent = await WaveAgent.create({
89
- restoreSessionId: sessionId,
90
- callbacks: {
91
- onAssistantContentUpdated: (chunk: string) =>
92
- callbacks.onAssistantContentUpdated?.(chunk, ""),
93
- onAssistantReasoningUpdated: (chunk: string) =>
94
- callbacks.onAssistantReasoningUpdated?.(chunk, ""),
95
- onToolBlockUpdated: (params: unknown) => {
96
- const cb = callbacks.onToolBlockUpdated as
97
- | ((params: unknown) => void)
98
- | undefined;
99
- cb?.(params);
100
- },
101
- onTasksChange: (tasks: unknown[]) => {
102
- const cb = callbacks.onTasksChange as
103
- | ((tasks: unknown[]) => void)
104
- | undefined;
105
- cb?.(tasks);
106
- },
107
- },
108
- });
211
+ const { sessionId, cwd } = params;
212
+ logger.info(`Loading session: ${sessionId} in ${cwd}`);
213
+ const agent = await this.createAgent(sessionId, cwd);
109
214
 
110
- this.agents.set(sessionId, agent);
111
- logger.info(`Session loaded: ${sessionId}`);
215
+ return {
216
+ modes: this.getSessionModeState(agent),
217
+ configOptions: this.getSessionConfigOptions(agent),
218
+ };
219
+ }
112
220
 
113
- // Update the callbacks object with the correct sessionId
114
- Object.assign(callbacks, this.createCallbacks(sessionId));
221
+ async listSessions(
222
+ params: ListSessionsRequest,
223
+ ): Promise<ListSessionsResponse> {
224
+ const { cwd } = params;
225
+ logger.info(`listSessions called with params: ${JSON.stringify(params)}`);
115
226
 
227
+ let waveSessions;
228
+ if (!cwd) {
229
+ logger.info("listSessions called without cwd, listing all sessions");
230
+ waveSessions = await listAllWaveSessions();
231
+ } else {
232
+ logger.info(`Listing sessions for ${cwd}`);
233
+ waveSessions = await listWaveSessions(cwd);
234
+ }
235
+
236
+ logger.info(`Found ${waveSessions.length} sessions`);
237
+ const sessions: SessionInfo[] = waveSessions.map((meta) => ({
238
+ sessionId: meta.id as AcpSessionId,
239
+ cwd: meta.workdir,
240
+ title: meta.firstMessage ? truncateContent(meta.firstMessage) : undefined,
241
+ updatedAt: meta.lastActiveAt.toISOString(),
242
+ }));
243
+ return { sessions };
244
+ }
245
+
246
+ async unstable_closeSession(
247
+ params: Record<string, unknown>,
248
+ ): Promise<Record<string, unknown>> {
249
+ const sessionId = params.sessionId as string;
250
+ logger.info(`Stopping session ${sessionId}`);
251
+ const agent = this.agents.get(sessionId);
252
+ if (agent) {
253
+ const workdir = agent.workingDirectory;
254
+ await agent.destroy();
255
+ this.agents.delete(sessionId);
256
+ // Delete the session file so it doesn't show up in listSessions
257
+ await deleteWaveSession(sessionId, workdir);
258
+ }
116
259
  return {};
117
260
  }
118
261
 
262
+ async extMethod(
263
+ method: string,
264
+ params: Record<string, unknown>,
265
+ ): Promise<Record<string, unknown>> {
266
+ if (method === AGENT_METHODS.session_close) {
267
+ return this.unstable_closeSession(params);
268
+ }
269
+ throw new Error(`Method ${method} not implemented`);
270
+ }
271
+
272
+ async setSessionMode(params: SetSessionModeRequest): Promise<void> {
273
+ const { sessionId, modeId } = params;
274
+ const agent = this.agents.get(sessionId);
275
+ if (!agent) throw new Error(`Session ${sessionId} not found`);
276
+ agent.setPermissionMode(
277
+ modeId as "default" | "acceptEdits" | "plan" | "bypassPermissions",
278
+ );
279
+ }
280
+
281
+ async setSessionConfigOption(
282
+ params: SetSessionConfigOptionRequest,
283
+ ): Promise<SetSessionConfigOptionResponse> {
284
+ const { sessionId, configId, value } = params;
285
+ const agent = this.agents.get(sessionId);
286
+ if (!agent) throw new Error(`Session ${sessionId} not found`);
287
+
288
+ if (configId === "permission_mode") {
289
+ agent.setPermissionMode(
290
+ value as "default" | "acceptEdits" | "plan" | "bypassPermissions",
291
+ );
292
+ }
293
+
294
+ return {
295
+ configOptions: this.getSessionConfigOptions(agent),
296
+ };
297
+ }
298
+
119
299
  async prompt(params: PromptRequest): Promise<PromptResponse> {
120
300
  const { sessionId, prompt } = params;
121
301
  logger.info(`Received prompt for session ${sessionId}`);
@@ -149,12 +329,6 @@ export class WaveAcpAgent implements AcpAgent {
149
329
  textContent,
150
330
  images.length > 0 ? images : undefined,
151
331
  );
152
- // Force save session so it can be loaded later
153
- await (
154
- agent as unknown as {
155
- messageManager: { saveSession: () => Promise<void> };
156
- }
157
- ).messageManager.saveSession();
158
332
  logger.info(`Message sent successfully for session ${sessionId}`);
159
333
  return {
160
334
  stopReason: "end_turn" as StopReason,
@@ -180,7 +354,297 @@ export class WaveAcpAgent implements AcpAgent {
180
354
  }
181
355
  }
182
356
 
357
+ private async handlePermissionRequest(
358
+ sessionId: string,
359
+ context: ToolPermissionContext,
360
+ ): Promise<PermissionDecision> {
361
+ logger.info(
362
+ `Handling permission request for ${context.toolName} in session ${sessionId}`,
363
+ );
364
+
365
+ const agent = this.agents.get(sessionId);
366
+ const workdir = agent?.workingDirectory || process.cwd();
367
+
368
+ const toolCallId =
369
+ context.toolCallId ||
370
+ "perm-" + Math.random().toString(36).substring(2, 9);
371
+
372
+ let effectiveName = context.toolName;
373
+ let effectiveCompactParams: string | undefined = undefined;
374
+
375
+ if (agent?.messages && context.toolCallId) {
376
+ const toolBlock = agent.messages
377
+ .flatMap((m) => m.blocks)
378
+ .find((b) => b.type === "tool" && b.id === context.toolCallId) as
379
+ | import("wave-agent-sdk").ToolBlock
380
+ | undefined;
381
+ if (toolBlock) {
382
+ effectiveName = toolBlock.name || effectiveName;
383
+ effectiveCompactParams =
384
+ toolBlock.compactParams || effectiveCompactParams;
385
+ }
386
+ }
387
+
388
+ const displayTitle =
389
+ effectiveName && effectiveCompactParams
390
+ ? `${effectiveName}: ${effectiveCompactParams}`
391
+ : effectiveName || "Tool Call";
392
+
393
+ const options: PermissionOption[] = [
394
+ {
395
+ optionId: "allow_once",
396
+ name: "Allow Once",
397
+ kind: "allow_once",
398
+ },
399
+ {
400
+ optionId: "allow_always",
401
+ name: "Allow Always",
402
+ kind: "allow_always",
403
+ },
404
+ {
405
+ optionId: "reject_once",
406
+ name: "Reject Once",
407
+ kind: "reject_once",
408
+ },
409
+ ];
410
+
411
+ const content = context.toolName
412
+ ? await this.getToolContentAsync(
413
+ context.toolName,
414
+ context.toolInput,
415
+ workdir,
416
+ )
417
+ : undefined;
418
+ const locations = context.toolName
419
+ ? this.getToolLocations(context.toolName, context.toolInput)
420
+ : undefined;
421
+ const kind = context.toolName
422
+ ? this.getToolKind(context.toolName)
423
+ : undefined;
424
+
425
+ try {
426
+ const response = await this.connection.requestPermission({
427
+ sessionId: sessionId as AcpSessionId,
428
+ toolCall: {
429
+ toolCallId,
430
+ title: displayTitle,
431
+ status: "pending",
432
+ rawInput: context.toolInput,
433
+ content,
434
+ locations,
435
+ kind,
436
+ },
437
+ options,
438
+ });
439
+
440
+ if (response.outcome.outcome === "cancelled") {
441
+ return { behavior: "deny", message: "Cancelled by user" };
442
+ }
443
+
444
+ const selectedOptionId = response.outcome.optionId;
445
+ logger.info(`User selected permission option: ${selectedOptionId}`);
446
+
447
+ switch (selectedOptionId) {
448
+ case "allow_always":
449
+ return {
450
+ behavior: "allow",
451
+ newPermissionRule: `${context.toolName}(*)`,
452
+ };
453
+ case "allow_once":
454
+ return { behavior: "allow" };
455
+ case "reject_once":
456
+ return { behavior: "deny", message: "Rejected by user" };
457
+ default:
458
+ return { behavior: "deny", message: "Unknown option selected" };
459
+ }
460
+ } catch (error) {
461
+ logger.error("Error requesting permission via ACP:", error);
462
+ return {
463
+ behavior: "deny",
464
+ message: `Error requesting permission: ${error instanceof Error ? error.message : String(error)}`,
465
+ };
466
+ }
467
+ }
468
+
469
+ private async getToolContentAsync(
470
+ name: string,
471
+ parameters: Record<string, unknown> | undefined,
472
+ workdir: string,
473
+ ): Promise<ToolCallContent[] | undefined> {
474
+ if (!parameters) return undefined;
475
+ if (name === "Write") {
476
+ let oldText: string | null = null;
477
+ try {
478
+ const filePath = (parameters.file_path ||
479
+ parameters.filePath) as string;
480
+ const fullPath = path.isAbsolute(filePath)
481
+ ? filePath
482
+ : path.join(workdir, filePath);
483
+ oldText = await fs.readFile(fullPath, "utf-8");
484
+ } catch {
485
+ // File might not exist, which is fine for Write
486
+ }
487
+ return [
488
+ {
489
+ type: "diff",
490
+ path: (parameters.file_path || parameters.filePath) as string,
491
+ oldText,
492
+ newText: parameters.content as string,
493
+ },
494
+ ];
495
+ }
496
+ if (name === "Edit") {
497
+ let oldText: string | null = null;
498
+ let newText: string | null = null;
499
+ try {
500
+ const filePath = (parameters.file_path ||
501
+ parameters.filePath) as string;
502
+ const fullPath = path.isAbsolute(filePath)
503
+ ? filePath
504
+ : path.join(workdir, filePath);
505
+ oldText = await fs.readFile(fullPath, "utf-8");
506
+ if (oldText) {
507
+ if (parameters.replace_all) {
508
+ newText = oldText
509
+ .split(parameters.old_string as string)
510
+ .join(parameters.new_string as string);
511
+ } else {
512
+ newText = oldText.replace(
513
+ parameters.old_string as string,
514
+ parameters.new_string as string,
515
+ );
516
+ }
517
+ }
518
+ } catch {
519
+ logger.error("Failed to read file for Edit diff");
520
+ }
521
+
522
+ if (oldText && newText) {
523
+ return [
524
+ {
525
+ type: "diff",
526
+ path: (parameters.file_path || parameters.filePath) as string,
527
+ oldText,
528
+ newText,
529
+ },
530
+ ];
531
+ }
532
+
533
+ // Fallback to snippets if file reading fails
534
+ return [
535
+ {
536
+ type: "diff",
537
+ path: (parameters.file_path || parameters.filePath) as string,
538
+ oldText: parameters.old_string as string,
539
+ newText: parameters.new_string as string,
540
+ },
541
+ ];
542
+ }
543
+ return this.getToolContent(name, parameters, undefined);
544
+ }
545
+
546
+ private getToolContent(
547
+ name: string,
548
+ parameters: Record<string, unknown> | undefined,
549
+ shortResult: string | undefined,
550
+ ): ToolCallContent[] | undefined {
551
+ const contents: ToolCallContent[] = [];
552
+ if (parameters) {
553
+ if (name === "Write") {
554
+ contents.push({
555
+ type: "diff",
556
+ path: (parameters.file_path || parameters.filePath) as string,
557
+ oldText: null,
558
+ newText: parameters.content as string,
559
+ });
560
+ } else if (name === "Edit") {
561
+ contents.push({
562
+ type: "diff",
563
+ path: (parameters.file_path || parameters.filePath) as string,
564
+ oldText: parameters.old_string as string,
565
+ newText: parameters.new_string as string,
566
+ });
567
+ }
568
+ }
569
+
570
+ if (shortResult) {
571
+ contents.push({
572
+ type: "content",
573
+ content: {
574
+ type: "text",
575
+ text: shortResult,
576
+ },
577
+ });
578
+ }
579
+
580
+ return contents.length > 0 ? contents : undefined;
581
+ }
582
+
583
+ private getToolLocations(
584
+ name: string,
585
+ parameters: Record<string, unknown> | undefined,
586
+ extraStartLineNumber?: number,
587
+ ): ToolCallLocation[] | undefined {
588
+ if (!parameters) return undefined;
589
+ if (
590
+ name === "Write" ||
591
+ name === "Edit" ||
592
+ name === "Read" ||
593
+ name === "LSP"
594
+ ) {
595
+ const filePath = (parameters.file_path || parameters.filePath) as string;
596
+ let line =
597
+ extraStartLineNumber ??
598
+ (parameters.startLineNumber as number) ??
599
+ (parameters.line as number) ??
600
+ (parameters.offset as number);
601
+
602
+ if (name === "Write" && line === undefined) {
603
+ line = 1;
604
+ }
605
+
606
+ if (filePath) {
607
+ return [
608
+ {
609
+ path: filePath,
610
+ line: line,
611
+ },
612
+ ];
613
+ }
614
+ }
615
+ return undefined;
616
+ }
617
+
618
+ private getToolKind(name: string): ToolKind {
619
+ switch (name) {
620
+ case "Read":
621
+ case "Glob":
622
+ case "Grep":
623
+ case "LSP":
624
+ return "read";
625
+ case "Write":
626
+ case "Edit":
627
+ return "edit";
628
+ case "Bash":
629
+ return "execute";
630
+ case "Agent":
631
+ return "other";
632
+ default:
633
+ return "other";
634
+ }
635
+ }
636
+
183
637
  private createCallbacks(sessionId: string): AgentOptions["callbacks"] {
638
+ const getAgent = () => this.agents.get(sessionId);
639
+ const toolStates = new Map<
640
+ string,
641
+ {
642
+ name?: string;
643
+ compactParams?: string;
644
+ shortResult?: string;
645
+ startLineNumber?: number;
646
+ }
647
+ >();
184
648
  return {
185
649
  onAssistantContentUpdated: (chunk: string) => {
186
650
  this.connection.sessionUpdate({
@@ -206,8 +670,72 @@ export class WaveAcpAgent implements AcpAgent {
206
670
  },
207
671
  });
208
672
  },
209
- onToolBlockUpdated: (params) => {
210
- const { id, name, stage, success, error, result } = params;
673
+ onToolBlockUpdated: (params: AgentToolBlockUpdateParams) => {
674
+ const {
675
+ id,
676
+ name,
677
+ stage,
678
+ success,
679
+ error,
680
+ result,
681
+ parameters,
682
+ compactParams,
683
+ shortResult,
684
+ startLineNumber,
685
+ } = params;
686
+
687
+ let state = toolStates.get(id);
688
+ if (!state) {
689
+ state = {};
690
+ toolStates.set(id, state);
691
+ }
692
+ if (name) state.name = name;
693
+ if (compactParams) state.compactParams = compactParams;
694
+ if (shortResult) state.shortResult = shortResult;
695
+ if (startLineNumber !== undefined)
696
+ state.startLineNumber = startLineNumber;
697
+
698
+ const effectiveName = state.name || name;
699
+ const effectiveCompactParams = state.compactParams || compactParams;
700
+ const effectiveShortResult = state.shortResult || shortResult;
701
+ const effectiveStartLineNumber =
702
+ state.startLineNumber !== undefined
703
+ ? state.startLineNumber
704
+ : startLineNumber;
705
+
706
+ const displayTitle =
707
+ effectiveName && effectiveCompactParams
708
+ ? `${effectiveName}: ${effectiveCompactParams}`
709
+ : effectiveName || "Tool Call";
710
+
711
+ let parsedParameters: Record<string, unknown> | undefined = undefined;
712
+ if (parameters) {
713
+ try {
714
+ parsedParameters = JSON.parse(parameters);
715
+ } catch {
716
+ // Ignore parse errors during streaming
717
+ }
718
+ }
719
+
720
+ const content =
721
+ effectiveName && (parsedParameters || effectiveShortResult)
722
+ ? this.getToolContent(
723
+ effectiveName,
724
+ parsedParameters,
725
+ effectiveShortResult,
726
+ )
727
+ : undefined;
728
+ const locations =
729
+ effectiveName && parsedParameters
730
+ ? this.getToolLocations(
731
+ effectiveName,
732
+ parsedParameters,
733
+ effectiveStartLineNumber,
734
+ )
735
+ : undefined;
736
+ const kind = effectiveName
737
+ ? this.getToolKind(effectiveName)
738
+ : undefined;
211
739
 
212
740
  if (stage === "start") {
213
741
  this.connection.sessionUpdate({
@@ -215,8 +743,12 @@ export class WaveAcpAgent implements AcpAgent {
215
743
  update: {
216
744
  sessionUpdate: "tool_call",
217
745
  toolCallId: id,
218
- title: name || "Tool Call",
746
+ title: displayTitle,
219
747
  status: "pending",
748
+ content,
749
+ locations,
750
+ kind,
751
+ rawInput: parsedParameters,
220
752
  },
221
753
  });
222
754
  return;
@@ -242,10 +774,18 @@ export class WaveAcpAgent implements AcpAgent {
242
774
  sessionUpdate: "tool_call_update",
243
775
  toolCallId: id,
244
776
  status,
245
- title: name || "Tool Call",
777
+ title: displayTitle,
246
778
  rawOutput: result || error,
779
+ content,
780
+ locations,
781
+ kind,
782
+ rawInput: parsedParameters,
247
783
  },
248
784
  });
785
+
786
+ if (stage === "end") {
787
+ toolStates.delete(id);
788
+ }
249
789
  },
250
790
  onTasksChange: (tasks) => {
251
791
  this.connection.sessionUpdate({
@@ -265,6 +805,25 @@ export class WaveAcpAgent implements AcpAgent {
265
805
  },
266
806
  });
267
807
  },
808
+ onPermissionModeChange: (mode) => {
809
+ this.connection.sessionUpdate({
810
+ sessionId: sessionId as AcpSessionId,
811
+ update: {
812
+ sessionUpdate: "current_mode_update",
813
+ currentModeId: mode,
814
+ },
815
+ });
816
+ const agent = getAgent();
817
+ if (agent) {
818
+ this.connection.sessionUpdate({
819
+ sessionId: sessionId as AcpSessionId,
820
+ update: {
821
+ sessionUpdate: "config_option_update",
822
+ configOptions: this.getSessionConfigOptions(agent),
823
+ },
824
+ });
825
+ }
826
+ },
268
827
  };
269
828
  }
270
829
  }