wave-code 0.10.0 → 0.10.1

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,14 +1,28 @@
1
- import type { Agent as AcpAgent, AgentSideConnection, InitializeResponse, NewSessionRequest, NewSessionResponse, LoadSessionRequest, LoadSessionResponse, PromptRequest, PromptResponse, CancelNotification, AuthenticateResponse } from "@agentclientprotocol/sdk";
1
+ import { type Agent as AcpAgent, type AgentSideConnection, type InitializeResponse, type NewSessionRequest, type NewSessionResponse, type LoadSessionRequest, type LoadSessionResponse, type ListSessionsRequest, type ListSessionsResponse, type PromptRequest, type PromptResponse, type CancelNotification, type AuthenticateResponse, type SetSessionModeRequest, type SetSessionConfigOptionRequest, type SetSessionConfigOptionResponse } from "@agentclientprotocol/sdk";
2
2
  export declare class WaveAcpAgent implements AcpAgent {
3
3
  private agents;
4
4
  private connection;
5
5
  constructor(connection: AgentSideConnection);
6
+ private getSessionModeState;
7
+ private getSessionConfigOptions;
8
+ private cleanupAllAgents;
6
9
  initialize(): Promise<InitializeResponse>;
7
10
  authenticate(): Promise<AuthenticateResponse | void>;
11
+ private createAgent;
8
12
  newSession(params: NewSessionRequest): Promise<NewSessionResponse>;
9
13
  loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse>;
14
+ listSessions(params: ListSessionsRequest): Promise<ListSessionsResponse>;
15
+ unstable_closeSession(params: Record<string, unknown>): Promise<Record<string, unknown>>;
16
+ extMethod(method: string, params: Record<string, unknown>): Promise<Record<string, unknown>>;
17
+ setSessionMode(params: SetSessionModeRequest): Promise<void>;
18
+ setSessionConfigOption(params: SetSessionConfigOptionRequest): Promise<SetSessionConfigOptionResponse>;
10
19
  prompt(params: PromptRequest): Promise<PromptResponse>;
11
20
  cancel(params: CancelNotification): Promise<void>;
21
+ private handlePermissionRequest;
22
+ private getToolContentAsync;
23
+ private getToolContent;
24
+ private getToolLocations;
25
+ private getToolKind;
12
26
  private createCallbacks;
13
27
  }
14
28
  //# sourceMappingURL=agent.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/acp/agent.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,KAAK,IAAI,QAAQ,EACjB,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,aAAa,EACb,cAAc,EACd,kBAAkB,EAClB,oBAAoB,EAIrB,MAAM,0BAA0B,CAAC;AAElC,qBAAa,YAAa,YAAW,QAAQ;IAC3C,OAAO,CAAC,MAAM,CAAqC;IACnD,OAAO,CAAC,UAAU,CAAsB;gBAE5B,UAAU,EAAE,mBAAmB;IAIrC,UAAU,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAczC,YAAY,IAAI,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IAIpD,UAAU,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAsClE,WAAW,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAmCrE,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;IAuDtD,MAAM,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IASvD,OAAO,CAAC,eAAe;CAuFxB"}
1
+ {"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/acp/agent.ts"],"names":[],"mappings":"AAaA,OAAO,EACL,KAAK,KAAK,IAAI,QAAQ,EACtB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EAUzB,KAAK,qBAAqB,EAC1B,KAAK,6BAA6B,EAClC,KAAK,8BAA8B,EAEpC,MAAM,0BAA0B,CAAC;AAElC,qBAAa,YAAa,YAAW,QAAQ;IAC3C,OAAO,CAAC,MAAM,CAAqC;IACnD,OAAO,CAAC,UAAU,CAAsB;gBAE5B,UAAU,EAAE,mBAAmB;IAI3C,OAAO,CAAC,mBAAmB;IA4B3B,OAAO,CAAC,uBAAuB;YAkBjB,gBAAgB;IASxB,UAAU,IAAI,OAAO,CAAC,kBAAkB,CAAC;IAoBzC,YAAY,IAAI,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;YAI5C,WAAW;IA8CnB,UAAU,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,CAAC;IA8BlE,WAAW,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IA4BrE,YAAY,CAChB,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,oBAAoB,CAAC;IAmB1B,qBAAqB,CACzB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAc7B,SAAS,CACb,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAO7B,cAAc,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IAS5D,sBAAsB,CAC1B,MAAM,EAAE,6BAA6B,GACpC,OAAO,CAAC,8BAA8B,CAAC;IAgBpC,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,cAAc,CAAC;IAiDtD,MAAM,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;YASzC,uBAAuB;YAsGvB,mBAAmB;IA2EjC,OAAO,CAAC,cAAc;IA4BtB,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,WAAW;IAmBnB,OAAO,CAAC,eAAe;CAsIxB"}
package/dist/acp/agent.js CHANGED
@@ -1,12 +1,67 @@
1
- import { Agent as WaveAgent } from "wave-agent-sdk";
1
+ import { Agent as WaveAgent, listSessions as listWaveSessions, deleteSession as deleteWaveSession, } from "wave-agent-sdk";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
2
4
  import { logger } from "../utils/logger.js";
5
+ import { AGENT_METHODS, } from "@agentclientprotocol/sdk";
3
6
  export class WaveAcpAgent {
4
7
  constructor(connection) {
5
8
  this.agents = new Map();
6
9
  this.connection = connection;
7
10
  }
11
+ getSessionModeState(agent) {
12
+ return {
13
+ currentModeId: agent.getPermissionMode(),
14
+ availableModes: [
15
+ {
16
+ id: "default",
17
+ name: "Default",
18
+ description: "Ask for permission for restricted tools",
19
+ },
20
+ {
21
+ id: "acceptEdits",
22
+ name: "Accept Edits",
23
+ description: "Automatically accept file edits",
24
+ },
25
+ {
26
+ id: "plan",
27
+ name: "Plan",
28
+ description: "Plan mode for complex tasks",
29
+ },
30
+ {
31
+ id: "bypassPermissions",
32
+ name: "Bypass Permissions",
33
+ description: "Automatically accept all tool calls",
34
+ },
35
+ ],
36
+ };
37
+ }
38
+ getSessionConfigOptions(agent) {
39
+ return [
40
+ {
41
+ id: "permission_mode",
42
+ name: "Permission Mode",
43
+ type: "select",
44
+ category: "mode",
45
+ currentValue: agent.getPermissionMode(),
46
+ options: [
47
+ { value: "default", name: "Default" },
48
+ { value: "acceptEdits", name: "Accept Edits" },
49
+ { value: "plan", name: "Plan" },
50
+ { value: "bypassPermissions", name: "Bypass Permissions" },
51
+ ],
52
+ },
53
+ ];
54
+ }
55
+ async cleanupAllAgents() {
56
+ logger.info("Cleaning up all active agents due to connection closure");
57
+ const destroyPromises = Array.from(this.agents.values()).map((agent) => agent.destroy());
58
+ await Promise.all(destroyPromises);
59
+ this.agents.clear();
60
+ }
8
61
  async initialize() {
9
62
  logger.info("Initializing WaveAcpAgent");
63
+ // Setup cleanup on connection closure
64
+ this.connection.closed.then(() => this.cleanupAllAgents());
10
65
  return {
11
66
  protocolVersion: 1,
12
67
  agentInfo: {
@@ -15,18 +70,28 @@ export class WaveAcpAgent {
15
70
  },
16
71
  agentCapabilities: {
17
72
  loadSession: true,
73
+ sessionCapabilities: {
74
+ list: {},
75
+ close: {},
76
+ },
18
77
  },
19
78
  };
20
79
  }
21
80
  async authenticate() {
22
81
  // No authentication required for now
23
82
  }
24
- async newSession(params) {
25
- const { cwd } = params;
26
- logger.info(`Creating new session in ${cwd}`);
83
+ async createAgent(sessionId, cwd) {
27
84
  const callbacks = {};
85
+ const agentRef = {};
28
86
  const agent = await WaveAgent.create({
29
87
  workdir: cwd,
88
+ restoreSessionId: sessionId,
89
+ canUseTool: (context) => {
90
+ if (!agentRef.instance) {
91
+ throw new Error("Agent instance not yet initialized");
92
+ }
93
+ return this.handlePermissionRequest(agentRef.instance.sessionId, context);
94
+ },
30
95
  callbacks: {
31
96
  onAssistantContentUpdated: (chunk) => callbacks.onAssistantContentUpdated?.(chunk, ""),
32
97
  onAssistantReasoningUpdated: (chunk) => callbacks.onAssistantReasoningUpdated?.(chunk, ""),
@@ -34,46 +99,124 @@ export class WaveAcpAgent {
34
99
  const cb = callbacks.onToolBlockUpdated;
35
100
  cb?.(params);
36
101
  },
37
- onTasksChange: (tasks) => {
38
- const cb = callbacks.onTasksChange;
39
- cb?.(tasks);
40
- },
102
+ onTasksChange: (tasks) => callbacks.onTasksChange?.(tasks),
103
+ onPermissionModeChange: (mode) => callbacks.onPermissionModeChange?.(mode),
41
104
  },
42
105
  });
43
- const sessionId = agent.sessionId;
44
- logger.info(`New session created: ${sessionId}`);
45
- this.agents.set(sessionId, agent);
106
+ agentRef.instance = agent;
107
+ const actualSessionId = agent.sessionId;
108
+ this.agents.set(actualSessionId, agent);
46
109
  // Update the callbacks object with the correct sessionId
47
- Object.assign(callbacks, this.createCallbacks(sessionId));
110
+ Object.assign(callbacks, this.createCallbacks(actualSessionId));
111
+ return agent;
112
+ }
113
+ async newSession(params) {
114
+ const { cwd } = params;
115
+ logger.info(`Creating new session in ${cwd}`);
116
+ const agent = await this.createAgent(undefined, cwd);
117
+ logger.info(`New session created with ID: ${agent.sessionId}`);
118
+ // Send initial available commands after agent creation
119
+ setImmediate(() => {
120
+ this.connection.sessionUpdate({
121
+ sessionId: agent.sessionId,
122
+ update: {
123
+ sessionUpdate: "available_commands_update",
124
+ availableCommands: agent.getSlashCommands().map((cmd) => ({
125
+ name: cmd.name,
126
+ description: cmd.description,
127
+ input: {
128
+ hint: "Enter arguments...",
129
+ },
130
+ })),
131
+ },
132
+ });
133
+ });
48
134
  return {
49
- sessionId: sessionId,
135
+ sessionId: agent.sessionId,
136
+ modes: this.getSessionModeState(agent),
137
+ configOptions: this.getSessionConfigOptions(agent),
50
138
  };
51
139
  }
52
140
  async loadSession(params) {
53
- const { sessionId } = params;
54
- logger.info(`Loading session: ${sessionId}`);
55
- const callbacks = {};
56
- const agent = await WaveAgent.create({
57
- restoreSessionId: sessionId,
58
- callbacks: {
59
- onAssistantContentUpdated: (chunk) => callbacks.onAssistantContentUpdated?.(chunk, ""),
60
- onAssistantReasoningUpdated: (chunk) => callbacks.onAssistantReasoningUpdated?.(chunk, ""),
61
- onToolBlockUpdated: (params) => {
62
- const cb = callbacks.onToolBlockUpdated;
63
- cb?.(params);
64
- },
65
- onTasksChange: (tasks) => {
66
- const cb = callbacks.onTasksChange;
67
- cb?.(tasks);
141
+ const { sessionId, cwd } = params;
142
+ logger.info(`Loading session: ${sessionId} in ${cwd}`);
143
+ const agent = await this.createAgent(sessionId, cwd);
144
+ // Send initial available commands after agent creation
145
+ setImmediate(() => {
146
+ this.connection.sessionUpdate({
147
+ sessionId: agent.sessionId,
148
+ update: {
149
+ sessionUpdate: "available_commands_update",
150
+ availableCommands: agent.getSlashCommands().map((cmd) => ({
151
+ name: cmd.name,
152
+ description: cmd.description,
153
+ input: {
154
+ hint: "Enter arguments...",
155
+ },
156
+ })),
68
157
  },
69
- },
158
+ });
70
159
  });
71
- this.agents.set(sessionId, agent);
72
- logger.info(`Session loaded: ${sessionId}`);
73
- // Update the callbacks object with the correct sessionId
74
- Object.assign(callbacks, this.createCallbacks(sessionId));
160
+ return {
161
+ modes: this.getSessionModeState(agent),
162
+ configOptions: this.getSessionConfigOptions(agent),
163
+ };
164
+ }
165
+ async listSessions(params) {
166
+ const { cwd } = params;
167
+ logger.info(`listSessions called with params: ${JSON.stringify(params)}`);
168
+ if (!cwd) {
169
+ logger.warn("listSessions called without cwd, returning empty list");
170
+ return { sessions: [] };
171
+ }
172
+ logger.info(`Listing sessions for ${cwd}`);
173
+ const waveSessions = await listWaveSessions(cwd);
174
+ logger.info(`Found ${waveSessions.length} sessions for ${cwd}`);
175
+ const sessions = waveSessions.map((meta) => ({
176
+ sessionId: meta.id,
177
+ cwd: meta.workdir,
178
+ updatedAt: meta.lastActiveAt.toISOString(),
179
+ }));
180
+ return { sessions };
181
+ }
182
+ async unstable_closeSession(params) {
183
+ const sessionId = params.sessionId;
184
+ logger.info(`Stopping session ${sessionId}`);
185
+ const agent = this.agents.get(sessionId);
186
+ if (agent) {
187
+ const workdir = agent.workingDirectory;
188
+ await agent.destroy();
189
+ this.agents.delete(sessionId);
190
+ // Delete the session file so it doesn't show up in listSessions
191
+ await deleteWaveSession(sessionId, workdir);
192
+ }
75
193
  return {};
76
194
  }
195
+ async extMethod(method, params) {
196
+ if (method === AGENT_METHODS.session_close) {
197
+ return this.unstable_closeSession(params);
198
+ }
199
+ throw new Error(`Method ${method} not implemented`);
200
+ }
201
+ async setSessionMode(params) {
202
+ const { sessionId, modeId } = params;
203
+ const agent = this.agents.get(sessionId);
204
+ if (!agent)
205
+ throw new Error(`Session ${sessionId} not found`);
206
+ agent.setPermissionMode(modeId);
207
+ }
208
+ async setSessionConfigOption(params) {
209
+ const { sessionId, configId, value } = params;
210
+ const agent = this.agents.get(sessionId);
211
+ if (!agent)
212
+ throw new Error(`Session ${sessionId} not found`);
213
+ if (configId === "permission_mode") {
214
+ agent.setPermissionMode(value);
215
+ }
216
+ return {
217
+ configOptions: this.getSessionConfigOptions(agent),
218
+ };
219
+ }
77
220
  async prompt(params) {
78
221
  const { sessionId, prompt } = params;
79
222
  logger.info(`Received prompt for session ${sessionId}`);
@@ -99,8 +242,6 @@ export class WaveAcpAgent {
99
242
  try {
100
243
  logger.info(`Sending message to agent: ${textContent.substring(0, 50)}...`);
101
244
  await agent.sendMessage(textContent, images.length > 0 ? images : undefined);
102
- // Force save session so it can be loaded later
103
- await agent.messageManager.saveSession();
104
245
  logger.info(`Message sent successfully for session ${sessionId}`);
105
246
  return {
106
247
  stopReason: "end_turn",
@@ -125,7 +266,217 @@ export class WaveAcpAgent {
125
266
  agent.abortMessage();
126
267
  }
127
268
  }
269
+ async handlePermissionRequest(sessionId, context) {
270
+ logger.info(`Handling permission request for ${context.toolName} in session ${sessionId}`);
271
+ const agent = this.agents.get(sessionId);
272
+ const workdir = agent?.workingDirectory || process.cwd();
273
+ const toolCallId = context.toolCallId ||
274
+ "perm-" + Math.random().toString(36).substring(2, 9);
275
+ const options = [
276
+ {
277
+ optionId: "allow_once",
278
+ name: "Allow Once",
279
+ kind: "allow_once",
280
+ },
281
+ {
282
+ optionId: "allow_always",
283
+ name: "Allow Always",
284
+ kind: "allow_always",
285
+ },
286
+ {
287
+ optionId: "reject_once",
288
+ name: "Reject Once",
289
+ kind: "reject_once",
290
+ },
291
+ {
292
+ optionId: "reject_always",
293
+ name: "Reject Always",
294
+ kind: "reject_always",
295
+ },
296
+ ];
297
+ const content = context.toolName
298
+ ? await this.getToolContentAsync(context.toolName, context.toolInput, workdir)
299
+ : undefined;
300
+ const locations = context.toolName
301
+ ? this.getToolLocations(context.toolName, context.toolInput)
302
+ : undefined;
303
+ const kind = context.toolName
304
+ ? this.getToolKind(context.toolName)
305
+ : undefined;
306
+ try {
307
+ const response = await this.connection.requestPermission({
308
+ sessionId: sessionId,
309
+ toolCall: {
310
+ toolCallId,
311
+ title: `Permission for ${context.toolName}`,
312
+ status: "pending",
313
+ rawInput: context.toolInput,
314
+ content,
315
+ locations,
316
+ kind,
317
+ },
318
+ options,
319
+ });
320
+ if (response.outcome.outcome === "cancelled") {
321
+ return { behavior: "deny", message: "Cancelled by user" };
322
+ }
323
+ const selectedOptionId = response.outcome.optionId;
324
+ logger.info(`User selected permission option: ${selectedOptionId}`);
325
+ switch (selectedOptionId) {
326
+ case "allow_once":
327
+ return { behavior: "allow" };
328
+ case "allow_always":
329
+ return {
330
+ behavior: "allow",
331
+ newPermissionRule: `${context.toolName}(*)`,
332
+ };
333
+ case "reject_once":
334
+ return { behavior: "deny", message: "Rejected by user" };
335
+ case "reject_always":
336
+ return {
337
+ behavior: "deny",
338
+ message: "Rejected by user",
339
+ newPermissionRule: `!${context.toolName}(*)`,
340
+ };
341
+ default:
342
+ return { behavior: "deny", message: "Unknown option selected" };
343
+ }
344
+ }
345
+ catch (error) {
346
+ logger.error("Error requesting permission via ACP:", error);
347
+ return {
348
+ behavior: "deny",
349
+ message: `Error requesting permission: ${error instanceof Error ? error.message : String(error)}`,
350
+ };
351
+ }
352
+ }
353
+ async getToolContentAsync(name, parameters, workdir) {
354
+ if (!parameters)
355
+ return undefined;
356
+ if (name === "Write") {
357
+ let oldText = null;
358
+ try {
359
+ const filePath = parameters.file_path;
360
+ const fullPath = path.isAbsolute(filePath)
361
+ ? filePath
362
+ : path.join(workdir, filePath);
363
+ oldText = await fs.readFile(fullPath, "utf-8");
364
+ }
365
+ catch {
366
+ // File might not exist, which is fine for Write
367
+ }
368
+ return [
369
+ {
370
+ type: "diff",
371
+ path: parameters.file_path,
372
+ oldText,
373
+ newText: parameters.content,
374
+ },
375
+ ];
376
+ }
377
+ if (name === "Edit") {
378
+ let oldText = null;
379
+ let newText = null;
380
+ try {
381
+ const filePath = parameters.file_path;
382
+ const fullPath = path.isAbsolute(filePath)
383
+ ? filePath
384
+ : path.join(workdir, filePath);
385
+ oldText = await fs.readFile(fullPath, "utf-8");
386
+ if (oldText) {
387
+ if (parameters.replace_all) {
388
+ newText = oldText
389
+ .split(parameters.old_string)
390
+ .join(parameters.new_string);
391
+ }
392
+ else {
393
+ newText = oldText.replace(parameters.old_string, parameters.new_string);
394
+ }
395
+ }
396
+ }
397
+ catch {
398
+ logger.error("Failed to read file for Edit diff");
399
+ }
400
+ if (oldText && newText) {
401
+ return [
402
+ {
403
+ type: "diff",
404
+ path: parameters.file_path,
405
+ oldText,
406
+ newText,
407
+ },
408
+ ];
409
+ }
410
+ // Fallback to snippets if file reading fails
411
+ return [
412
+ {
413
+ type: "diff",
414
+ path: parameters.file_path,
415
+ oldText: parameters.old_string,
416
+ newText: parameters.new_string,
417
+ },
418
+ ];
419
+ }
420
+ return this.getToolContent(name, parameters);
421
+ }
422
+ getToolContent(name, parameters) {
423
+ if (!parameters)
424
+ return undefined;
425
+ if (name === "Write") {
426
+ return [
427
+ {
428
+ type: "diff",
429
+ path: parameters.file_path,
430
+ oldText: null,
431
+ newText: parameters.content,
432
+ },
433
+ ];
434
+ }
435
+ if (name === "Edit") {
436
+ return [
437
+ {
438
+ type: "diff",
439
+ path: parameters.file_path,
440
+ oldText: parameters.old_string,
441
+ newText: parameters.new_string,
442
+ },
443
+ ];
444
+ }
445
+ return undefined;
446
+ }
447
+ getToolLocations(name, parameters) {
448
+ if (!parameters)
449
+ return undefined;
450
+ if (name === "Write" || name === "Edit" || name === "Read") {
451
+ return [
452
+ {
453
+ path: parameters.file_path,
454
+ line: parameters.offset,
455
+ },
456
+ ];
457
+ }
458
+ return undefined;
459
+ }
460
+ getToolKind(name) {
461
+ switch (name) {
462
+ case "Read":
463
+ case "Glob":
464
+ case "Grep":
465
+ case "LSP":
466
+ return "read";
467
+ case "Write":
468
+ case "Edit":
469
+ return "edit";
470
+ case "Bash":
471
+ return "execute";
472
+ case "Agent":
473
+ return "other";
474
+ default:
475
+ return "other";
476
+ }
477
+ }
128
478
  createCallbacks(sessionId) {
479
+ const getAgent = () => this.agents.get(sessionId);
129
480
  return {
130
481
  onAssistantContentUpdated: (chunk) => {
131
482
  this.connection.sessionUpdate({
@@ -152,7 +503,23 @@ export class WaveAcpAgent {
152
503
  });
153
504
  },
154
505
  onToolBlockUpdated: (params) => {
155
- const { id, name, stage, success, error, result } = params;
506
+ const { id, name, stage, success, error, result, parameters } = params;
507
+ let parsedParameters = undefined;
508
+ if (parameters) {
509
+ try {
510
+ parsedParameters = JSON.parse(parameters);
511
+ }
512
+ catch {
513
+ // Ignore parse errors during streaming
514
+ }
515
+ }
516
+ const content = name && parsedParameters
517
+ ? this.getToolContent(name, parsedParameters)
518
+ : undefined;
519
+ const locations = name && parsedParameters
520
+ ? this.getToolLocations(name, parsedParameters)
521
+ : undefined;
522
+ const kind = name ? this.getToolKind(name) : undefined;
156
523
  if (stage === "start") {
157
524
  this.connection.sessionUpdate({
158
525
  sessionId: sessionId,
@@ -161,6 +528,10 @@ export class WaveAcpAgent {
161
528
  toolCallId: id,
162
529
  title: name || "Tool Call",
163
530
  status: "pending",
531
+ content,
532
+ locations,
533
+ kind,
534
+ rawInput: parsedParameters,
164
535
  },
165
536
  });
166
537
  return;
@@ -184,6 +555,10 @@ export class WaveAcpAgent {
184
555
  status,
185
556
  title: name || "Tool Call",
186
557
  rawOutput: result || error,
558
+ content,
559
+ locations,
560
+ kind,
561
+ rawInput: parsedParameters,
187
562
  },
188
563
  });
189
564
  },
@@ -204,6 +579,25 @@ export class WaveAcpAgent {
204
579
  },
205
580
  });
206
581
  },
582
+ onPermissionModeChange: (mode) => {
583
+ this.connection.sessionUpdate({
584
+ sessionId: sessionId,
585
+ update: {
586
+ sessionUpdate: "current_mode_update",
587
+ currentModeId: mode,
588
+ },
589
+ });
590
+ const agent = getAgent();
591
+ if (agent) {
592
+ this.connection.sessionUpdate({
593
+ sessionId: sessionId,
594
+ update: {
595
+ sessionUpdate: "config_option_update",
596
+ configOptions: this.getSessionConfigOptions(agent),
597
+ },
598
+ });
599
+ }
600
+ },
207
601
  };
208
602
  }
209
603
  }
@@ -39,7 +39,7 @@ export const ChatInterface = () => {
39
39
  }
40
40
  const terminalHeight = stdout?.rows || 24;
41
41
  const totalHeight = detailsHeight + selectorHeight + dynamicBlocksHeight;
42
- if (totalHeight > terminalHeight) {
42
+ if (totalHeight > terminalHeight - 3) {
43
43
  setIsConfirmationTooTall(true);
44
44
  }
45
45
  }, [
@@ -1 +1 @@
1
- {"version":3,"file":"useChat.d.ts","sourceRoot":"","sources":["../../src/contexts/useChat.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useChat.d.ts","sourceRoot":"","sources":["../../src/contexts/useChat.tsx"],"names":[],"mappings":"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"}
@@ -113,9 +113,6 @@ export const ChatProvider = ({ children, bypassPermissions, pluginDirs, tools, w
113
113
  onPermissionModeChange: (mode) => {
114
114
  setPermissionModeState(mode);
115
115
  },
116
- onSlashCommandsChange: (commands) => {
117
- setSlashCommands([...commands]);
118
- },
119
116
  };
120
117
  try {
121
118
  // Create the permission callback inside the try block to access showConfirmation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-code",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "description": "CLI-based code assistant powered by AI, built with React and Ink",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,9 +39,9 @@
39
39
  "react": "^19.2.4",
40
40
  "react-dom": "19.2.4",
41
41
  "yargs": "^17.7.2",
42
- "@agentclientprotocol/sdk": "0.15.0",
42
+ "@agentclientprotocol/sdk": "0.16.1",
43
43
  "zod": "^3.23.8",
44
- "wave-agent-sdk": "0.10.0"
44
+ "wave-agent-sdk": "0.10.1"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/react": "^19.1.8",
package/src/acp/agent.ts CHANGED
@@ -1,20 +1,43 @@
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
+ deleteSession as deleteWaveSession,
10
+ } from "wave-agent-sdk";
11
+ import * as fs from "node:fs/promises";
12
+ import * as path from "node:path";
2
13
  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,
14
+ import {
15
+ type Agent as AcpAgent,
16
+ type AgentSideConnection,
17
+ type InitializeResponse,
18
+ type NewSessionRequest,
19
+ type NewSessionResponse,
20
+ type LoadSessionRequest,
21
+ type LoadSessionResponse,
22
+ type ListSessionsRequest,
23
+ type ListSessionsResponse,
24
+ type PromptRequest,
25
+ type PromptResponse,
26
+ type CancelNotification,
27
+ type AuthenticateResponse,
28
+ type SessionId as AcpSessionId,
29
+ type ToolCallStatus,
30
+ type StopReason,
31
+ type PermissionOption,
32
+ type SessionInfo,
33
+ type ToolCallContent,
34
+ type ToolCallLocation,
35
+ type ToolKind,
36
+ type SessionConfigOption,
37
+ type SetSessionModeRequest,
38
+ type SetSessionConfigOptionRequest,
39
+ type SetSessionConfigOptionResponse,
40
+ AGENT_METHODS,
18
41
  } from "@agentclientprotocol/sdk";
19
42
 
20
43
  export class WaveAcpAgent implements AcpAgent {
@@ -25,8 +48,65 @@ export class WaveAcpAgent implements AcpAgent {
25
48
  this.connection = connection;
26
49
  }
27
50
 
51
+ private getSessionModeState(agent: WaveAgent) {
52
+ return {
53
+ currentModeId: agent.getPermissionMode(),
54
+ availableModes: [
55
+ {
56
+ id: "default",
57
+ name: "Default",
58
+ description: "Ask for permission for restricted tools",
59
+ },
60
+ {
61
+ id: "acceptEdits",
62
+ name: "Accept Edits",
63
+ description: "Automatically accept file edits",
64
+ },
65
+ {
66
+ id: "plan",
67
+ name: "Plan",
68
+ description: "Plan mode for complex tasks",
69
+ },
70
+ {
71
+ id: "bypassPermissions",
72
+ name: "Bypass Permissions",
73
+ description: "Automatically accept all tool calls",
74
+ },
75
+ ],
76
+ };
77
+ }
78
+
79
+ private getSessionConfigOptions(agent: WaveAgent): SessionConfigOption[] {
80
+ return [
81
+ {
82
+ id: "permission_mode",
83
+ name: "Permission Mode",
84
+ type: "select",
85
+ category: "mode",
86
+ currentValue: agent.getPermissionMode(),
87
+ options: [
88
+ { value: "default", name: "Default" },
89
+ { value: "acceptEdits", name: "Accept Edits" },
90
+ { value: "plan", name: "Plan" },
91
+ { value: "bypassPermissions", name: "Bypass Permissions" },
92
+ ],
93
+ },
94
+ ];
95
+ }
96
+
97
+ private async cleanupAllAgents() {
98
+ logger.info("Cleaning up all active agents due to connection closure");
99
+ const destroyPromises = Array.from(this.agents.values()).map((agent) =>
100
+ agent.destroy(),
101
+ );
102
+ await Promise.all(destroyPromises);
103
+ this.agents.clear();
104
+ }
105
+
28
106
  async initialize(): Promise<InitializeResponse> {
29
107
  logger.info("Initializing WaveAcpAgent");
108
+ // Setup cleanup on connection closure
109
+ this.connection.closed.then(() => this.cleanupAllAgents());
30
110
  return {
31
111
  protocolVersion: 1,
32
112
  agentInfo: {
@@ -35,6 +115,10 @@ export class WaveAcpAgent implements AcpAgent {
35
115
  },
36
116
  agentCapabilities: {
37
117
  loadSession: true,
118
+ sessionCapabilities: {
119
+ list: {},
120
+ close: {},
121
+ },
38
122
  },
39
123
  };
40
124
  }
@@ -43,12 +127,25 @@ export class WaveAcpAgent implements AcpAgent {
43
127
  // No authentication required for now
44
128
  }
45
129
 
46
- async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
47
- const { cwd } = params;
48
- logger.info(`Creating new session in ${cwd}`);
130
+ private async createAgent(
131
+ sessionId: string | undefined,
132
+ cwd: string,
133
+ ): Promise<WaveAgent> {
49
134
  const callbacks: AgentOptions["callbacks"] = {};
135
+ const agentRef: { instance?: WaveAgent } = {};
136
+
50
137
  const agent = await WaveAgent.create({
51
138
  workdir: cwd,
139
+ restoreSessionId: sessionId,
140
+ canUseTool: (context) => {
141
+ if (!agentRef.instance) {
142
+ throw new Error("Agent instance not yet initialized");
143
+ }
144
+ return this.handlePermissionRequest(
145
+ agentRef.instance.sessionId,
146
+ context,
147
+ );
148
+ },
52
149
  callbacks: {
53
150
  onAssistantContentUpdated: (chunk: string) =>
54
151
  callbacks.onAssistantContentUpdated?.(chunk, ""),
@@ -60,62 +157,154 @@ export class WaveAcpAgent implements AcpAgent {
60
157
  | undefined;
61
158
  cb?.(params);
62
159
  },
63
- onTasksChange: (tasks: unknown[]) => {
64
- const cb = callbacks.onTasksChange as
65
- | ((tasks: unknown[]) => void)
66
- | undefined;
67
- cb?.(tasks);
68
- },
160
+ onTasksChange: (tasks) => callbacks.onTasksChange?.(tasks as Task[]),
161
+ onPermissionModeChange: (mode) =>
162
+ callbacks.onPermissionModeChange?.(mode),
69
163
  },
70
164
  });
71
165
 
72
- const sessionId = agent.sessionId;
73
- logger.info(`New session created: ${sessionId}`);
74
- this.agents.set(sessionId, agent);
166
+ agentRef.instance = agent;
167
+ const actualSessionId = agent.sessionId;
168
+ this.agents.set(actualSessionId, agent);
75
169
 
76
170
  // Update the callbacks object with the correct sessionId
77
- Object.assign(callbacks, this.createCallbacks(sessionId));
171
+ Object.assign(callbacks, this.createCallbacks(actualSessionId));
172
+
173
+ return agent;
174
+ }
175
+
176
+ async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
177
+ const { cwd } = params;
178
+ logger.info(`Creating new session in ${cwd}`);
179
+ const agent = await this.createAgent(undefined, cwd);
180
+ logger.info(`New session created with ID: ${agent.sessionId}`);
181
+
182
+ // Send initial available commands after agent creation
183
+ setImmediate(() => {
184
+ this.connection.sessionUpdate({
185
+ sessionId: agent.sessionId as AcpSessionId,
186
+ update: {
187
+ sessionUpdate: "available_commands_update",
188
+ availableCommands: agent.getSlashCommands().map((cmd) => ({
189
+ name: cmd.name,
190
+ description: cmd.description,
191
+ input: {
192
+ hint: "Enter arguments...",
193
+ },
194
+ })),
195
+ },
196
+ });
197
+ });
78
198
 
79
199
  return {
80
- sessionId: sessionId as AcpSessionId,
200
+ sessionId: agent.sessionId as AcpSessionId,
201
+ modes: this.getSessionModeState(agent),
202
+ configOptions: this.getSessionConfigOptions(agent),
81
203
  };
82
204
  }
83
205
 
84
206
  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);
207
+ const { sessionId, cwd } = params;
208
+ logger.info(`Loading session: ${sessionId} in ${cwd}`);
209
+ const agent = await this.createAgent(sessionId, cwd);
210
+
211
+ // Send initial available commands after agent creation
212
+ setImmediate(() => {
213
+ this.connection.sessionUpdate({
214
+ sessionId: agent.sessionId as AcpSessionId,
215
+ update: {
216
+ sessionUpdate: "available_commands_update",
217
+ availableCommands: agent.getSlashCommands().map((cmd) => ({
218
+ name: cmd.name,
219
+ description: cmd.description,
220
+ input: {
221
+ hint: "Enter arguments...",
222
+ },
223
+ })),
106
224
  },
107
- },
225
+ });
108
226
  });
109
227
 
110
- this.agents.set(sessionId, agent);
111
- logger.info(`Session loaded: ${sessionId}`);
228
+ return {
229
+ modes: this.getSessionModeState(agent),
230
+ configOptions: this.getSessionConfigOptions(agent),
231
+ };
232
+ }
233
+
234
+ async listSessions(
235
+ params: ListSessionsRequest,
236
+ ): Promise<ListSessionsResponse> {
237
+ const { cwd } = params;
238
+ logger.info(`listSessions called with params: ${JSON.stringify(params)}`);
239
+ if (!cwd) {
240
+ logger.warn("listSessions called without cwd, returning empty list");
241
+ return { sessions: [] };
242
+ }
112
243
 
113
- // Update the callbacks object with the correct sessionId
114
- Object.assign(callbacks, this.createCallbacks(sessionId));
244
+ logger.info(`Listing sessions for ${cwd}`);
245
+ const waveSessions = await listWaveSessions(cwd);
246
+ logger.info(`Found ${waveSessions.length} sessions for ${cwd}`);
247
+ const sessions: SessionInfo[] = waveSessions.map((meta) => ({
248
+ sessionId: meta.id as AcpSessionId,
249
+ cwd: meta.workdir,
250
+ updatedAt: meta.lastActiveAt.toISOString(),
251
+ }));
252
+ return { sessions };
253
+ }
115
254
 
255
+ async unstable_closeSession(
256
+ params: Record<string, unknown>,
257
+ ): Promise<Record<string, unknown>> {
258
+ const sessionId = params.sessionId as string;
259
+ logger.info(`Stopping session ${sessionId}`);
260
+ const agent = this.agents.get(sessionId);
261
+ if (agent) {
262
+ const workdir = agent.workingDirectory;
263
+ await agent.destroy();
264
+ this.agents.delete(sessionId);
265
+ // Delete the session file so it doesn't show up in listSessions
266
+ await deleteWaveSession(sessionId, workdir);
267
+ }
116
268
  return {};
117
269
  }
118
270
 
271
+ async extMethod(
272
+ method: string,
273
+ params: Record<string, unknown>,
274
+ ): Promise<Record<string, unknown>> {
275
+ if (method === AGENT_METHODS.session_close) {
276
+ return this.unstable_closeSession(params);
277
+ }
278
+ throw new Error(`Method ${method} not implemented`);
279
+ }
280
+
281
+ async setSessionMode(params: SetSessionModeRequest): Promise<void> {
282
+ const { sessionId, modeId } = params;
283
+ const agent = this.agents.get(sessionId);
284
+ if (!agent) throw new Error(`Session ${sessionId} not found`);
285
+ agent.setPermissionMode(
286
+ modeId as "default" | "acceptEdits" | "plan" | "bypassPermissions",
287
+ );
288
+ }
289
+
290
+ async setSessionConfigOption(
291
+ params: SetSessionConfigOptionRequest,
292
+ ): Promise<SetSessionConfigOptionResponse> {
293
+ const { sessionId, configId, value } = params;
294
+ const agent = this.agents.get(sessionId);
295
+ if (!agent) throw new Error(`Session ${sessionId} not found`);
296
+
297
+ if (configId === "permission_mode") {
298
+ agent.setPermissionMode(
299
+ value as "default" | "acceptEdits" | "plan" | "bypassPermissions",
300
+ );
301
+ }
302
+
303
+ return {
304
+ configOptions: this.getSessionConfigOptions(agent),
305
+ };
306
+ }
307
+
119
308
  async prompt(params: PromptRequest): Promise<PromptResponse> {
120
309
  const { sessionId, prompt } = params;
121
310
  logger.info(`Received prompt for session ${sessionId}`);
@@ -149,12 +338,6 @@ export class WaveAcpAgent implements AcpAgent {
149
338
  textContent,
150
339
  images.length > 0 ? images : undefined,
151
340
  );
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
341
  logger.info(`Message sent successfully for session ${sessionId}`);
159
342
  return {
160
343
  stopReason: "end_turn" as StopReason,
@@ -180,7 +363,248 @@ export class WaveAcpAgent implements AcpAgent {
180
363
  }
181
364
  }
182
365
 
366
+ private async handlePermissionRequest(
367
+ sessionId: string,
368
+ context: ToolPermissionContext,
369
+ ): Promise<PermissionDecision> {
370
+ logger.info(
371
+ `Handling permission request for ${context.toolName} in session ${sessionId}`,
372
+ );
373
+
374
+ const agent = this.agents.get(sessionId);
375
+ const workdir = agent?.workingDirectory || process.cwd();
376
+
377
+ const toolCallId =
378
+ context.toolCallId ||
379
+ "perm-" + Math.random().toString(36).substring(2, 9);
380
+
381
+ const options: PermissionOption[] = [
382
+ {
383
+ optionId: "allow_once",
384
+ name: "Allow Once",
385
+ kind: "allow_once",
386
+ },
387
+ {
388
+ optionId: "allow_always",
389
+ name: "Allow Always",
390
+ kind: "allow_always",
391
+ },
392
+ {
393
+ optionId: "reject_once",
394
+ name: "Reject Once",
395
+ kind: "reject_once",
396
+ },
397
+ {
398
+ optionId: "reject_always",
399
+ name: "Reject Always",
400
+ kind: "reject_always",
401
+ },
402
+ ];
403
+
404
+ const content = context.toolName
405
+ ? await this.getToolContentAsync(
406
+ context.toolName,
407
+ context.toolInput,
408
+ workdir,
409
+ )
410
+ : undefined;
411
+ const locations = context.toolName
412
+ ? this.getToolLocations(context.toolName, context.toolInput)
413
+ : undefined;
414
+ const kind = context.toolName
415
+ ? this.getToolKind(context.toolName)
416
+ : undefined;
417
+
418
+ try {
419
+ const response = await this.connection.requestPermission({
420
+ sessionId: sessionId as AcpSessionId,
421
+ toolCall: {
422
+ toolCallId,
423
+ title: `Permission for ${context.toolName}`,
424
+ status: "pending",
425
+ rawInput: context.toolInput,
426
+ content,
427
+ locations,
428
+ kind,
429
+ },
430
+ options,
431
+ });
432
+
433
+ if (response.outcome.outcome === "cancelled") {
434
+ return { behavior: "deny", message: "Cancelled by user" };
435
+ }
436
+
437
+ const selectedOptionId = response.outcome.optionId;
438
+ logger.info(`User selected permission option: ${selectedOptionId}`);
439
+
440
+ switch (selectedOptionId) {
441
+ case "allow_once":
442
+ return { behavior: "allow" };
443
+ case "allow_always":
444
+ return {
445
+ behavior: "allow",
446
+ newPermissionRule: `${context.toolName}(*)`,
447
+ };
448
+ case "reject_once":
449
+ return { behavior: "deny", message: "Rejected by user" };
450
+ case "reject_always":
451
+ return {
452
+ behavior: "deny",
453
+ message: "Rejected by user",
454
+ newPermissionRule: `!${context.toolName}(*)`,
455
+ };
456
+ default:
457
+ return { behavior: "deny", message: "Unknown option selected" };
458
+ }
459
+ } catch (error) {
460
+ logger.error("Error requesting permission via ACP:", error);
461
+ return {
462
+ behavior: "deny",
463
+ message: `Error requesting permission: ${error instanceof Error ? error.message : String(error)}`,
464
+ };
465
+ }
466
+ }
467
+
468
+ private async getToolContentAsync(
469
+ name: string,
470
+ parameters: Record<string, unknown> | undefined,
471
+ workdir: string,
472
+ ): Promise<ToolCallContent[] | undefined> {
473
+ if (!parameters) return undefined;
474
+ if (name === "Write") {
475
+ let oldText: string | null = null;
476
+ try {
477
+ const filePath = parameters.file_path as string;
478
+ const fullPath = path.isAbsolute(filePath)
479
+ ? filePath
480
+ : path.join(workdir, filePath);
481
+ oldText = await fs.readFile(fullPath, "utf-8");
482
+ } catch {
483
+ // File might not exist, which is fine for Write
484
+ }
485
+ return [
486
+ {
487
+ type: "diff",
488
+ path: parameters.file_path as string,
489
+ oldText,
490
+ newText: parameters.content as string,
491
+ },
492
+ ];
493
+ }
494
+ if (name === "Edit") {
495
+ let oldText: string | null = null;
496
+ let newText: string | null = null;
497
+ try {
498
+ const filePath = parameters.file_path as string;
499
+ const fullPath = path.isAbsolute(filePath)
500
+ ? filePath
501
+ : path.join(workdir, filePath);
502
+ oldText = await fs.readFile(fullPath, "utf-8");
503
+ if (oldText) {
504
+ if (parameters.replace_all) {
505
+ newText = oldText
506
+ .split(parameters.old_string as string)
507
+ .join(parameters.new_string as string);
508
+ } else {
509
+ newText = oldText.replace(
510
+ parameters.old_string as string,
511
+ parameters.new_string as string,
512
+ );
513
+ }
514
+ }
515
+ } catch {
516
+ logger.error("Failed to read file for Edit diff");
517
+ }
518
+
519
+ if (oldText && newText) {
520
+ return [
521
+ {
522
+ type: "diff",
523
+ path: parameters.file_path as string,
524
+ oldText,
525
+ newText,
526
+ },
527
+ ];
528
+ }
529
+
530
+ // Fallback to snippets if file reading fails
531
+ return [
532
+ {
533
+ type: "diff",
534
+ path: parameters.file_path as string,
535
+ oldText: parameters.old_string as string,
536
+ newText: parameters.new_string as string,
537
+ },
538
+ ];
539
+ }
540
+ return this.getToolContent(name, parameters);
541
+ }
542
+
543
+ private getToolContent(
544
+ name: string,
545
+ parameters: Record<string, unknown> | undefined,
546
+ ): ToolCallContent[] | undefined {
547
+ if (!parameters) return undefined;
548
+ if (name === "Write") {
549
+ return [
550
+ {
551
+ type: "diff",
552
+ path: parameters.file_path as string,
553
+ oldText: null,
554
+ newText: parameters.content as string,
555
+ },
556
+ ];
557
+ }
558
+ if (name === "Edit") {
559
+ return [
560
+ {
561
+ type: "diff",
562
+ path: parameters.file_path as string,
563
+ oldText: parameters.old_string as string,
564
+ newText: parameters.new_string as string,
565
+ },
566
+ ];
567
+ }
568
+ return undefined;
569
+ }
570
+
571
+ private getToolLocations(
572
+ name: string,
573
+ parameters: Record<string, unknown> | undefined,
574
+ ): ToolCallLocation[] | undefined {
575
+ if (!parameters) return undefined;
576
+ if (name === "Write" || name === "Edit" || name === "Read") {
577
+ return [
578
+ {
579
+ path: parameters.file_path as string,
580
+ line: parameters.offset as number,
581
+ },
582
+ ];
583
+ }
584
+ return undefined;
585
+ }
586
+
587
+ private getToolKind(name: string): ToolKind {
588
+ switch (name) {
589
+ case "Read":
590
+ case "Glob":
591
+ case "Grep":
592
+ case "LSP":
593
+ return "read";
594
+ case "Write":
595
+ case "Edit":
596
+ return "edit";
597
+ case "Bash":
598
+ return "execute";
599
+ case "Agent":
600
+ return "other";
601
+ default:
602
+ return "other";
603
+ }
604
+ }
605
+
183
606
  private createCallbacks(sessionId: string): AgentOptions["callbacks"] {
607
+ const getAgent = () => this.agents.get(sessionId);
184
608
  return {
185
609
  onAssistantContentUpdated: (chunk: string) => {
186
610
  this.connection.sessionUpdate({
@@ -206,8 +630,27 @@ export class WaveAcpAgent implements AcpAgent {
206
630
  },
207
631
  });
208
632
  },
209
- onToolBlockUpdated: (params) => {
210
- const { id, name, stage, success, error, result } = params;
633
+ onToolBlockUpdated: (params: AgentToolBlockUpdateParams) => {
634
+ const { id, name, stage, success, error, result, parameters } = params;
635
+
636
+ let parsedParameters: Record<string, unknown> | undefined = undefined;
637
+ if (parameters) {
638
+ try {
639
+ parsedParameters = JSON.parse(parameters);
640
+ } catch {
641
+ // Ignore parse errors during streaming
642
+ }
643
+ }
644
+
645
+ const content =
646
+ name && parsedParameters
647
+ ? this.getToolContent(name, parsedParameters)
648
+ : undefined;
649
+ const locations =
650
+ name && parsedParameters
651
+ ? this.getToolLocations(name, parsedParameters)
652
+ : undefined;
653
+ const kind = name ? this.getToolKind(name) : undefined;
211
654
 
212
655
  if (stage === "start") {
213
656
  this.connection.sessionUpdate({
@@ -217,6 +660,10 @@ export class WaveAcpAgent implements AcpAgent {
217
660
  toolCallId: id,
218
661
  title: name || "Tool Call",
219
662
  status: "pending",
663
+ content,
664
+ locations,
665
+ kind,
666
+ rawInput: parsedParameters,
220
667
  },
221
668
  });
222
669
  return;
@@ -244,6 +691,10 @@ export class WaveAcpAgent implements AcpAgent {
244
691
  status,
245
692
  title: name || "Tool Call",
246
693
  rawOutput: result || error,
694
+ content,
695
+ locations,
696
+ kind,
697
+ rawInput: parsedParameters,
247
698
  },
248
699
  });
249
700
  },
@@ -265,6 +716,25 @@ export class WaveAcpAgent implements AcpAgent {
265
716
  },
266
717
  });
267
718
  },
719
+ onPermissionModeChange: (mode) => {
720
+ this.connection.sessionUpdate({
721
+ sessionId: sessionId as AcpSessionId,
722
+ update: {
723
+ sessionUpdate: "current_mode_update",
724
+ currentModeId: mode,
725
+ },
726
+ });
727
+ const agent = getAgent();
728
+ if (agent) {
729
+ this.connection.sessionUpdate({
730
+ sessionId: sessionId as AcpSessionId,
731
+ update: {
732
+ sessionUpdate: "config_option_update",
733
+ configOptions: this.getSessionConfigOptions(agent),
734
+ },
735
+ });
736
+ }
737
+ },
268
738
  };
269
739
  }
270
740
  }
@@ -72,7 +72,7 @@ export const ChatInterface: React.FC = () => {
72
72
 
73
73
  const terminalHeight = stdout?.rows || 24;
74
74
  const totalHeight = detailsHeight + selectorHeight + dynamicBlocksHeight;
75
- if (totalHeight > terminalHeight) {
75
+ if (totalHeight > terminalHeight - 3) {
76
76
  setIsConfirmationTooTall(true);
77
77
  }
78
78
  }, [
@@ -295,9 +295,6 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
295
295
  onPermissionModeChange: (mode) => {
296
296
  setPermissionModeState(mode);
297
297
  },
298
- onSlashCommandsChange: (commands) => {
299
- setSlashCommands([...commands]);
300
- },
301
298
  };
302
299
 
303
300
  try {