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/dist/acp/agent.d.ts +15 -1
- package/dist/acp/agent.d.ts.map +1 -1
- package/dist/acp/agent.js +477 -40
- package/dist/components/ChatInterface.js +1 -1
- package/dist/components/DiffDisplay.d.ts.map +1 -1
- package/dist/components/DiffDisplay.js +23 -2
- package/dist/contexts/useChat.d.ts.map +1 -1
- package/dist/contexts/useChat.js +15 -4
- package/dist/utils/highlightUtils.d.ts.map +1 -1
- package/dist/utils/highlightUtils.js +8 -0
- package/dist/utils/toolParameterTransforms.d.ts +1 -5
- package/dist/utils/toolParameterTransforms.d.ts.map +1 -1
- package/dist/utils/toolParameterTransforms.js +0 -14
- package/dist/utils/worktree.js +2 -2
- package/package.json +3 -3
- package/src/acp/agent.ts +627 -68
- package/src/components/ChatInterface.tsx +1 -1
- package/src/components/DiffDisplay.tsx +27 -2
- package/src/contexts/useChat.tsx +20 -4
- package/src/utils/highlightUtils.ts +8 -0
- package/src/utils/toolParameterTransforms.ts +1 -23
- package/src/utils/worktree.ts +2 -2
package/src/acp/agent.ts
CHANGED
|
@@ -1,20 +1,45 @@
|
|
|
1
|
-
import {
|
|
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
|
|
4
|
-
Agent as AcpAgent,
|
|
5
|
-
AgentSideConnection,
|
|
6
|
-
InitializeResponse,
|
|
7
|
-
NewSessionRequest,
|
|
8
|
-
NewSessionResponse,
|
|
9
|
-
LoadSessionRequest,
|
|
10
|
-
LoadSessionResponse,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
this.agents.set(
|
|
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(
|
|
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
|
|
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
|
-
|
|
111
|
-
|
|
215
|
+
return {
|
|
216
|
+
modes: this.getSessionModeState(agent),
|
|
217
|
+
configOptions: this.getSessionConfigOptions(agent),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
112
220
|
|
|
113
|
-
|
|
114
|
-
|
|
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 {
|
|
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:
|
|
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:
|
|
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
|
}
|