wave-code 0.9.7 → 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.
- package/dist/acp/agent.d.ts +28 -0
- package/dist/acp/agent.d.ts.map +1 -0
- package/dist/acp/agent.js +603 -0
- package/dist/acp/index.d.ts +2 -0
- package/dist/acp/index.d.ts.map +1 -0
- package/dist/acp/index.js +22 -0
- package/dist/acp-cli.d.ts +2 -0
- package/dist/acp-cli.d.ts.map +1 -0
- package/dist/acp-cli.js +4 -0
- package/dist/components/ChatInterface.js +1 -1
- package/dist/components/DiffDisplay.d.ts.map +1 -1
- package/dist/components/DiffDisplay.js +33 -89
- package/dist/contexts/useChat.d.ts.map +1 -1
- package/dist/contexts/useChat.js +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/package.json +4 -2
- package/src/acp/agent.ts +740 -0
- package/src/acp/index.ts +28 -0
- package/src/acp-cli.ts +5 -0
- package/src/components/ChatInterface.tsx +1 -1
- package/src/components/DiffDisplay.tsx +62 -134
- package/src/contexts/useChat.tsx +0 -3
- package/src/index.ts +11 -0
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
export declare class WaveAcpAgent implements AcpAgent {
|
|
3
|
+
private agents;
|
|
4
|
+
private connection;
|
|
5
|
+
constructor(connection: AgentSideConnection);
|
|
6
|
+
private getSessionModeState;
|
|
7
|
+
private getSessionConfigOptions;
|
|
8
|
+
private cleanupAllAgents;
|
|
9
|
+
initialize(): Promise<InitializeResponse>;
|
|
10
|
+
authenticate(): Promise<AuthenticateResponse | void>;
|
|
11
|
+
private createAgent;
|
|
12
|
+
newSession(params: NewSessionRequest): Promise<NewSessionResponse>;
|
|
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>;
|
|
19
|
+
prompt(params: PromptRequest): Promise<PromptResponse>;
|
|
20
|
+
cancel(params: CancelNotification): Promise<void>;
|
|
21
|
+
private handlePermissionRequest;
|
|
22
|
+
private getToolContentAsync;
|
|
23
|
+
private getToolContent;
|
|
24
|
+
private getToolLocations;
|
|
25
|
+
private getToolKind;
|
|
26
|
+
private createCallbacks;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=agent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,603 @@
|
|
|
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";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
import { AGENT_METHODS, } from "@agentclientprotocol/sdk";
|
|
6
|
+
export class WaveAcpAgent {
|
|
7
|
+
constructor(connection) {
|
|
8
|
+
this.agents = new Map();
|
|
9
|
+
this.connection = connection;
|
|
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
|
+
}
|
|
61
|
+
async initialize() {
|
|
62
|
+
logger.info("Initializing WaveAcpAgent");
|
|
63
|
+
// Setup cleanup on connection closure
|
|
64
|
+
this.connection.closed.then(() => this.cleanupAllAgents());
|
|
65
|
+
return {
|
|
66
|
+
protocolVersion: 1,
|
|
67
|
+
agentInfo: {
|
|
68
|
+
name: "wave-agent",
|
|
69
|
+
version: "0.1.0",
|
|
70
|
+
},
|
|
71
|
+
agentCapabilities: {
|
|
72
|
+
loadSession: true,
|
|
73
|
+
sessionCapabilities: {
|
|
74
|
+
list: {},
|
|
75
|
+
close: {},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async authenticate() {
|
|
81
|
+
// No authentication required for now
|
|
82
|
+
}
|
|
83
|
+
async createAgent(sessionId, cwd) {
|
|
84
|
+
const callbacks = {};
|
|
85
|
+
const agentRef = {};
|
|
86
|
+
const agent = await WaveAgent.create({
|
|
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
|
+
},
|
|
95
|
+
callbacks: {
|
|
96
|
+
onAssistantContentUpdated: (chunk) => callbacks.onAssistantContentUpdated?.(chunk, ""),
|
|
97
|
+
onAssistantReasoningUpdated: (chunk) => callbacks.onAssistantReasoningUpdated?.(chunk, ""),
|
|
98
|
+
onToolBlockUpdated: (params) => {
|
|
99
|
+
const cb = callbacks.onToolBlockUpdated;
|
|
100
|
+
cb?.(params);
|
|
101
|
+
},
|
|
102
|
+
onTasksChange: (tasks) => callbacks.onTasksChange?.(tasks),
|
|
103
|
+
onPermissionModeChange: (mode) => callbacks.onPermissionModeChange?.(mode),
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
agentRef.instance = agent;
|
|
107
|
+
const actualSessionId = agent.sessionId;
|
|
108
|
+
this.agents.set(actualSessionId, agent);
|
|
109
|
+
// Update the callbacks object with the correct 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
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
sessionId: agent.sessionId,
|
|
136
|
+
modes: this.getSessionModeState(agent),
|
|
137
|
+
configOptions: this.getSessionConfigOptions(agent),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
async loadSession(params) {
|
|
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
|
+
})),
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
});
|
|
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
|
+
}
|
|
193
|
+
return {};
|
|
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
|
+
}
|
|
220
|
+
async prompt(params) {
|
|
221
|
+
const { sessionId, prompt } = params;
|
|
222
|
+
logger.info(`Received prompt for session ${sessionId}`);
|
|
223
|
+
const agent = this.agents.get(sessionId);
|
|
224
|
+
if (!agent) {
|
|
225
|
+
logger.error(`Session ${sessionId} not found`);
|
|
226
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
227
|
+
}
|
|
228
|
+
// Map ACP prompt to Wave Agent sendMessage
|
|
229
|
+
const textContent = prompt
|
|
230
|
+
.filter((block) => block.type === "text")
|
|
231
|
+
.map((block) => block.text)
|
|
232
|
+
.join("\n");
|
|
233
|
+
const images = prompt
|
|
234
|
+
.filter((block) => block.type === "image")
|
|
235
|
+
.map((block) => {
|
|
236
|
+
const img = block;
|
|
237
|
+
return {
|
|
238
|
+
path: `data:${img.mimeType};base64,${img.data}`,
|
|
239
|
+
mimeType: img.mimeType,
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
try {
|
|
243
|
+
logger.info(`Sending message to agent: ${textContent.substring(0, 50)}...`);
|
|
244
|
+
await agent.sendMessage(textContent, images.length > 0 ? images : undefined);
|
|
245
|
+
logger.info(`Message sent successfully for session ${sessionId}`);
|
|
246
|
+
return {
|
|
247
|
+
stopReason: "end_turn",
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (error instanceof Error && error.message.includes("abort")) {
|
|
252
|
+
logger.info(`Message aborted for session ${sessionId}`);
|
|
253
|
+
return {
|
|
254
|
+
stopReason: "cancelled",
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
logger.error(`Error sending message for session ${sessionId}:`, error);
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async cancel(params) {
|
|
262
|
+
const { sessionId } = params;
|
|
263
|
+
logger.info(`Cancelling message for session ${sessionId}`);
|
|
264
|
+
const agent = this.agents.get(sessionId);
|
|
265
|
+
if (agent) {
|
|
266
|
+
agent.abortMessage();
|
|
267
|
+
}
|
|
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
|
+
}
|
|
478
|
+
createCallbacks(sessionId) {
|
|
479
|
+
const getAgent = () => this.agents.get(sessionId);
|
|
480
|
+
return {
|
|
481
|
+
onAssistantContentUpdated: (chunk) => {
|
|
482
|
+
this.connection.sessionUpdate({
|
|
483
|
+
sessionId: sessionId,
|
|
484
|
+
update: {
|
|
485
|
+
sessionUpdate: "agent_message_chunk",
|
|
486
|
+
content: {
|
|
487
|
+
type: "text",
|
|
488
|
+
text: chunk,
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
},
|
|
493
|
+
onAssistantReasoningUpdated: (chunk) => {
|
|
494
|
+
this.connection.sessionUpdate({
|
|
495
|
+
sessionId: sessionId,
|
|
496
|
+
update: {
|
|
497
|
+
sessionUpdate: "agent_thought_chunk",
|
|
498
|
+
content: {
|
|
499
|
+
type: "text",
|
|
500
|
+
text: chunk,
|
|
501
|
+
},
|
|
502
|
+
},
|
|
503
|
+
});
|
|
504
|
+
},
|
|
505
|
+
onToolBlockUpdated: (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;
|
|
523
|
+
if (stage === "start") {
|
|
524
|
+
this.connection.sessionUpdate({
|
|
525
|
+
sessionId: sessionId,
|
|
526
|
+
update: {
|
|
527
|
+
sessionUpdate: "tool_call",
|
|
528
|
+
toolCallId: id,
|
|
529
|
+
title: name || "Tool Call",
|
|
530
|
+
status: "pending",
|
|
531
|
+
content,
|
|
532
|
+
locations,
|
|
533
|
+
kind,
|
|
534
|
+
rawInput: parsedParameters,
|
|
535
|
+
},
|
|
536
|
+
});
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (stage === "streaming") {
|
|
540
|
+
// We don't support streaming tool arguments in ACP yet
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const status = stage === "end"
|
|
544
|
+
? success
|
|
545
|
+
? "completed"
|
|
546
|
+
: "failed"
|
|
547
|
+
: stage === "running"
|
|
548
|
+
? "in_progress"
|
|
549
|
+
: "pending";
|
|
550
|
+
this.connection.sessionUpdate({
|
|
551
|
+
sessionId: sessionId,
|
|
552
|
+
update: {
|
|
553
|
+
sessionUpdate: "tool_call_update",
|
|
554
|
+
toolCallId: id,
|
|
555
|
+
status,
|
|
556
|
+
title: name || "Tool Call",
|
|
557
|
+
rawOutput: result || error,
|
|
558
|
+
content,
|
|
559
|
+
locations,
|
|
560
|
+
kind,
|
|
561
|
+
rawInput: parsedParameters,
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
},
|
|
565
|
+
onTasksChange: (tasks) => {
|
|
566
|
+
this.connection.sessionUpdate({
|
|
567
|
+
sessionId: sessionId,
|
|
568
|
+
update: {
|
|
569
|
+
sessionUpdate: "plan",
|
|
570
|
+
entries: tasks.map((task) => ({
|
|
571
|
+
content: task.subject,
|
|
572
|
+
status: task.status === "completed"
|
|
573
|
+
? "completed"
|
|
574
|
+
: task.status === "in_progress"
|
|
575
|
+
? "in_progress"
|
|
576
|
+
: "pending",
|
|
577
|
+
priority: "medium",
|
|
578
|
+
})),
|
|
579
|
+
},
|
|
580
|
+
});
|
|
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
|
+
},
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/acp/index.ts"],"names":[],"mappings":"AAKA,wBAAsB,WAAW,kBAsBhC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Readable, Writable } from "node:stream";
|
|
2
|
+
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
3
|
+
import { WaveAcpAgent } from "./agent.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
export async function startAcpCli() {
|
|
6
|
+
// Redirect console.log to logger to avoid interfering with JSON-RPC over stdio
|
|
7
|
+
console.log = (...args) => {
|
|
8
|
+
logger.info(...args);
|
|
9
|
+
};
|
|
10
|
+
logger.info("Starting ACP bridge...");
|
|
11
|
+
// Convert Node.js stdio to Web streams
|
|
12
|
+
const stdin = Readable.toWeb(process.stdin);
|
|
13
|
+
const stdout = Writable.toWeb(process.stdout);
|
|
14
|
+
// Create ACP stream
|
|
15
|
+
const stream = ndJsonStream(stdout, stdin);
|
|
16
|
+
// Initialize AgentSideConnection
|
|
17
|
+
const connection = new AgentSideConnection((conn) => {
|
|
18
|
+
return new WaveAcpAgent(conn);
|
|
19
|
+
}, stream);
|
|
20
|
+
// Wait for connection to close
|
|
21
|
+
await connection.closed;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"acp-cli.d.ts","sourceRoot":"","sources":["../src/acp-cli.ts"],"names":[],"mappings":"AAEA,wBAAsB,MAAM,kBAE3B"}
|
package/dist/acp-cli.js
ADDED
|
@@ -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
|
}, [
|