wave-code 0.9.7 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/acp/agent.d.ts +14 -0
- package/dist/acp/agent.d.ts.map +1 -0
- package/dist/acp/agent.js +209 -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/DiffDisplay.d.ts.map +1 -1
- package/dist/components/DiffDisplay.js +33 -89
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/package.json +4 -2
- package/src/acp/agent.ts +270 -0
- package/src/acp/index.ts +28 -0
- package/src/acp-cli.ts +5 -0
- package/src/components/DiffDisplay.tsx +62 -134
- package/src/index.ts +11 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Agent as AcpAgent, AgentSideConnection, InitializeResponse, NewSessionRequest, NewSessionResponse, LoadSessionRequest, LoadSessionResponse, PromptRequest, PromptResponse, CancelNotification, AuthenticateResponse } from "@agentclientprotocol/sdk";
|
|
2
|
+
export declare class WaveAcpAgent implements AcpAgent {
|
|
3
|
+
private agents;
|
|
4
|
+
private connection;
|
|
5
|
+
constructor(connection: AgentSideConnection);
|
|
6
|
+
initialize(): Promise<InitializeResponse>;
|
|
7
|
+
authenticate(): Promise<AuthenticateResponse | void>;
|
|
8
|
+
newSession(params: NewSessionRequest): Promise<NewSessionResponse>;
|
|
9
|
+
loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse>;
|
|
10
|
+
prompt(params: PromptRequest): Promise<PromptResponse>;
|
|
11
|
+
cancel(params: CancelNotification): Promise<void>;
|
|
12
|
+
private createCallbacks;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=agent.d.ts.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { Agent as WaveAgent } from "wave-agent-sdk";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
export class WaveAcpAgent {
|
|
4
|
+
constructor(connection) {
|
|
5
|
+
this.agents = new Map();
|
|
6
|
+
this.connection = connection;
|
|
7
|
+
}
|
|
8
|
+
async initialize() {
|
|
9
|
+
logger.info("Initializing WaveAcpAgent");
|
|
10
|
+
return {
|
|
11
|
+
protocolVersion: 1,
|
|
12
|
+
agentInfo: {
|
|
13
|
+
name: "wave-agent",
|
|
14
|
+
version: "0.1.0",
|
|
15
|
+
},
|
|
16
|
+
agentCapabilities: {
|
|
17
|
+
loadSession: true,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async authenticate() {
|
|
22
|
+
// No authentication required for now
|
|
23
|
+
}
|
|
24
|
+
async newSession(params) {
|
|
25
|
+
const { cwd } = params;
|
|
26
|
+
logger.info(`Creating new session in ${cwd}`);
|
|
27
|
+
const callbacks = {};
|
|
28
|
+
const agent = await WaveAgent.create({
|
|
29
|
+
workdir: cwd,
|
|
30
|
+
callbacks: {
|
|
31
|
+
onAssistantContentUpdated: (chunk) => callbacks.onAssistantContentUpdated?.(chunk, ""),
|
|
32
|
+
onAssistantReasoningUpdated: (chunk) => callbacks.onAssistantReasoningUpdated?.(chunk, ""),
|
|
33
|
+
onToolBlockUpdated: (params) => {
|
|
34
|
+
const cb = callbacks.onToolBlockUpdated;
|
|
35
|
+
cb?.(params);
|
|
36
|
+
},
|
|
37
|
+
onTasksChange: (tasks) => {
|
|
38
|
+
const cb = callbacks.onTasksChange;
|
|
39
|
+
cb?.(tasks);
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const sessionId = agent.sessionId;
|
|
44
|
+
logger.info(`New session created: ${sessionId}`);
|
|
45
|
+
this.agents.set(sessionId, agent);
|
|
46
|
+
// Update the callbacks object with the correct sessionId
|
|
47
|
+
Object.assign(callbacks, this.createCallbacks(sessionId));
|
|
48
|
+
return {
|
|
49
|
+
sessionId: sessionId,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
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);
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
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));
|
|
75
|
+
return {};
|
|
76
|
+
}
|
|
77
|
+
async prompt(params) {
|
|
78
|
+
const { sessionId, prompt } = params;
|
|
79
|
+
logger.info(`Received prompt for session ${sessionId}`);
|
|
80
|
+
const agent = this.agents.get(sessionId);
|
|
81
|
+
if (!agent) {
|
|
82
|
+
logger.error(`Session ${sessionId} not found`);
|
|
83
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
84
|
+
}
|
|
85
|
+
// Map ACP prompt to Wave Agent sendMessage
|
|
86
|
+
const textContent = prompt
|
|
87
|
+
.filter((block) => block.type === "text")
|
|
88
|
+
.map((block) => block.text)
|
|
89
|
+
.join("\n");
|
|
90
|
+
const images = prompt
|
|
91
|
+
.filter((block) => block.type === "image")
|
|
92
|
+
.map((block) => {
|
|
93
|
+
const img = block;
|
|
94
|
+
return {
|
|
95
|
+
path: `data:${img.mimeType};base64,${img.data}`,
|
|
96
|
+
mimeType: img.mimeType,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
try {
|
|
100
|
+
logger.info(`Sending message to agent: ${textContent.substring(0, 50)}...`);
|
|
101
|
+
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
|
+
logger.info(`Message sent successfully for session ${sessionId}`);
|
|
105
|
+
return {
|
|
106
|
+
stopReason: "end_turn",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (error instanceof Error && error.message.includes("abort")) {
|
|
111
|
+
logger.info(`Message aborted for session ${sessionId}`);
|
|
112
|
+
return {
|
|
113
|
+
stopReason: "cancelled",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
logger.error(`Error sending message for session ${sessionId}:`, error);
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async cancel(params) {
|
|
121
|
+
const { sessionId } = params;
|
|
122
|
+
logger.info(`Cancelling message for session ${sessionId}`);
|
|
123
|
+
const agent = this.agents.get(sessionId);
|
|
124
|
+
if (agent) {
|
|
125
|
+
agent.abortMessage();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
createCallbacks(sessionId) {
|
|
129
|
+
return {
|
|
130
|
+
onAssistantContentUpdated: (chunk) => {
|
|
131
|
+
this.connection.sessionUpdate({
|
|
132
|
+
sessionId: sessionId,
|
|
133
|
+
update: {
|
|
134
|
+
sessionUpdate: "agent_message_chunk",
|
|
135
|
+
content: {
|
|
136
|
+
type: "text",
|
|
137
|
+
text: chunk,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
onAssistantReasoningUpdated: (chunk) => {
|
|
143
|
+
this.connection.sessionUpdate({
|
|
144
|
+
sessionId: sessionId,
|
|
145
|
+
update: {
|
|
146
|
+
sessionUpdate: "agent_thought_chunk",
|
|
147
|
+
content: {
|
|
148
|
+
type: "text",
|
|
149
|
+
text: chunk,
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
onToolBlockUpdated: (params) => {
|
|
155
|
+
const { id, name, stage, success, error, result } = params;
|
|
156
|
+
if (stage === "start") {
|
|
157
|
+
this.connection.sessionUpdate({
|
|
158
|
+
sessionId: sessionId,
|
|
159
|
+
update: {
|
|
160
|
+
sessionUpdate: "tool_call",
|
|
161
|
+
toolCallId: id,
|
|
162
|
+
title: name || "Tool Call",
|
|
163
|
+
status: "pending",
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (stage === "streaming") {
|
|
169
|
+
// We don't support streaming tool arguments in ACP yet
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const status = stage === "end"
|
|
173
|
+
? success
|
|
174
|
+
? "completed"
|
|
175
|
+
: "failed"
|
|
176
|
+
: stage === "running"
|
|
177
|
+
? "in_progress"
|
|
178
|
+
: "pending";
|
|
179
|
+
this.connection.sessionUpdate({
|
|
180
|
+
sessionId: sessionId,
|
|
181
|
+
update: {
|
|
182
|
+
sessionUpdate: "tool_call_update",
|
|
183
|
+
toolCallId: id,
|
|
184
|
+
status,
|
|
185
|
+
title: name || "Tool Call",
|
|
186
|
+
rawOutput: result || error,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
},
|
|
190
|
+
onTasksChange: (tasks) => {
|
|
191
|
+
this.connection.sessionUpdate({
|
|
192
|
+
sessionId: sessionId,
|
|
193
|
+
update: {
|
|
194
|
+
sessionUpdate: "plan",
|
|
195
|
+
entries: tasks.map((task) => ({
|
|
196
|
+
content: task.subject,
|
|
197
|
+
status: task.status === "completed"
|
|
198
|
+
? "completed"
|
|
199
|
+
: task.status === "in_progress"
|
|
200
|
+
? "in_progress"
|
|
201
|
+
: "pending",
|
|
202
|
+
priority: "medium",
|
|
203
|
+
})),
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DiffDisplay.d.ts","sourceRoot":"","sources":["../../src/components/DiffDisplay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkB,MAAM,OAAO,CAAC;AAMvC,UAAU,gBAAgB;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,
|
|
1
|
+
{"version":3,"file":"DiffDisplay.d.ts","sourceRoot":"","sources":["../../src/components/DiffDisplay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkB,MAAM,OAAO,CAAC;AAMvC,UAAU,gBAAgB;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,eAAO,MAAM,WAAW,EAAE,KAAK,CAAC,EAAE,CAAC,gBAAgB,CA0VlD,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import
|
|
2
|
+
import { useMemo } from "react";
|
|
3
3
|
import { Box, Text } from "ink";
|
|
4
4
|
import { WRITE_TOOL_NAME, EDIT_TOOL_NAME } from "wave-agent-sdk";
|
|
5
5
|
import { transformToolBlockToChanges } from "../utils/toolParameterTransforms.js";
|
|
@@ -85,26 +85,46 @@ export const DiffDisplay = ({ toolName, parameters, startLineNumber, }) => {
|
|
|
85
85
|
let newLineNum = change.startLineNumber || 1;
|
|
86
86
|
// Process line diffs
|
|
87
87
|
const diffElements = [];
|
|
88
|
-
lineDiffs.
|
|
88
|
+
for (let i = 0; i < lineDiffs.length; i++) {
|
|
89
|
+
const part = lineDiffs[i];
|
|
89
90
|
const lines = part.value.split("\n");
|
|
90
91
|
// diffLines might return a trailing empty string if the content ends with a newline
|
|
91
92
|
if (lines[lines.length - 1] === "") {
|
|
92
93
|
lines.pop();
|
|
93
94
|
}
|
|
94
|
-
if (part.
|
|
95
|
+
if (part.removed) {
|
|
96
|
+
// Look ahead for an added block
|
|
97
|
+
if (i + 1 < lineDiffs.length && lineDiffs[i + 1].added) {
|
|
98
|
+
const nextPart = lineDiffs[i + 1];
|
|
99
|
+
const addedLines = nextPart.value.split("\n");
|
|
100
|
+
if (addedLines[addedLines.length - 1] === "") {
|
|
101
|
+
addedLines.pop();
|
|
102
|
+
}
|
|
103
|
+
if (lines.length === addedLines.length) {
|
|
104
|
+
// Word-level diffing
|
|
105
|
+
lines.forEach((line, lineIndex) => {
|
|
106
|
+
const { removedParts, addedParts } = renderWordLevelDiff(line, addedLines[lineIndex], `word-${changeIndex}-${i}-${lineIndex}`);
|
|
107
|
+
diffElements.push(renderLine(oldLineNum++, null, "-", removedParts, "red", `remove-${changeIndex}-${i}-${lineIndex}`));
|
|
108
|
+
diffElements.push(renderLine(null, newLineNum++, "+", addedParts, "green", `add-${changeIndex}-${i}-${lineIndex}`));
|
|
109
|
+
});
|
|
110
|
+
i++; // Skip the added block
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Fallback to standard removed rendering
|
|
95
115
|
lines.forEach((line, lineIndex) => {
|
|
96
|
-
diffElements.push(renderLine(null,
|
|
116
|
+
diffElements.push(renderLine(oldLineNum++, null, "-", line, "red", `remove-${changeIndex}-${i}-${lineIndex}`));
|
|
97
117
|
});
|
|
98
118
|
}
|
|
99
|
-
else if (part.
|
|
119
|
+
else if (part.added) {
|
|
100
120
|
lines.forEach((line, lineIndex) => {
|
|
101
|
-
diffElements.push(renderLine(
|
|
121
|
+
diffElements.push(renderLine(null, newLineNum++, "+", line, "green", `add-${changeIndex}-${i}-${lineIndex}`));
|
|
102
122
|
});
|
|
103
123
|
}
|
|
104
124
|
else {
|
|
105
125
|
// Context lines - show unchanged content
|
|
106
|
-
const isFirstBlock =
|
|
107
|
-
const isLastBlock =
|
|
126
|
+
const isFirstBlock = i === 0;
|
|
127
|
+
const isLastBlock = i === lineDiffs.length - 1;
|
|
108
128
|
let linesToDisplay = lines;
|
|
109
129
|
let showEllipsisTop = false;
|
|
110
130
|
let showEllipsisBottom = false;
|
|
@@ -133,7 +153,7 @@ export const DiffDisplay = ({ toolName, parameters, startLineNumber, }) => {
|
|
|
133
153
|
}
|
|
134
154
|
}
|
|
135
155
|
if (showEllipsisTop) {
|
|
136
|
-
diffElements.push(_jsx(Box, { children: _jsxs(Text, { color: "gray", children: [" ".repeat(maxDigits * 2 + 2), "..."] }) }, `ellipsis-top-${changeIndex}-${
|
|
156
|
+
diffElements.push(_jsx(Box, { children: _jsxs(Text, { color: "gray", children: [" ".repeat(maxDigits * 2 + 2), "..."] }) }, `ellipsis-top-${changeIndex}-${i}`));
|
|
137
157
|
}
|
|
138
158
|
linesToDisplay.forEach((line, lineIndex) => {
|
|
139
159
|
// If it's a middle block and we are at the split point
|
|
@@ -144,9 +164,9 @@ export const DiffDisplay = ({ toolName, parameters, startLineNumber, }) => {
|
|
|
144
164
|
const skipCount = lines.length - 6;
|
|
145
165
|
oldLineNum += skipCount;
|
|
146
166
|
newLineNum += skipCount;
|
|
147
|
-
diffElements.push(_jsx(Box, { children: _jsxs(Text, { color: "gray", children: [" ".repeat(maxDigits * 2 + 2), "..."] }) }, `ellipsis-mid-${changeIndex}-${
|
|
167
|
+
diffElements.push(_jsx(Box, { children: _jsxs(Text, { color: "gray", children: [" ".repeat(maxDigits * 2 + 2), "..."] }) }, `ellipsis-mid-${changeIndex}-${i}`));
|
|
148
168
|
}
|
|
149
|
-
diffElements.push(renderLine(oldLineNum++, newLineNum++, " ", line, "white", `context-${changeIndex}-${
|
|
169
|
+
diffElements.push(renderLine(oldLineNum++, newLineNum++, " ", line, "white", `context-${changeIndex}-${i}-${lineIndex}`));
|
|
150
170
|
});
|
|
151
171
|
if (showEllipsisBottom) {
|
|
152
172
|
const skipCount = lines.length - linesToDisplay.length;
|
|
@@ -154,34 +174,11 @@ export const DiffDisplay = ({ toolName, parameters, startLineNumber, }) => {
|
|
|
154
174
|
// But we need to account for the lines we skipped at the end of this block
|
|
155
175
|
oldLineNum += skipCount;
|
|
156
176
|
newLineNum += skipCount;
|
|
157
|
-
diffElements.push(_jsx(Box, { children: _jsxs(Text, { color: "gray", children: [" ".repeat(maxDigits * 2 + 2), "..."] }) }, `ellipsis-bottom-${changeIndex}-${
|
|
177
|
+
diffElements.push(_jsx(Box, { children: _jsxs(Text, { color: "gray", children: [" ".repeat(maxDigits * 2 + 2), "..."] }) }, `ellipsis-bottom-${changeIndex}-${i}`));
|
|
158
178
|
}
|
|
159
179
|
}
|
|
160
|
-
});
|
|
161
|
-
// If it's a single line change (one removed, one added), use word-level diff
|
|
162
|
-
if (diffElements.length === 2 &&
|
|
163
|
-
React.isValidElement(diffElements[0]) &&
|
|
164
|
-
React.isValidElement(diffElements[1]) &&
|
|
165
|
-
typeof diffElements[0].key === "string" &&
|
|
166
|
-
diffElements[0].key.includes("remove-") &&
|
|
167
|
-
typeof diffElements[1].key === "string" &&
|
|
168
|
-
diffElements[1].key.includes("add-")) {
|
|
169
|
-
const removedText = extractTextFromElement(diffElements[0]);
|
|
170
|
-
const addedText = extractTextFromElement(diffElements[1]);
|
|
171
|
-
const oldLineNumVal = extractOldLineNumFromElement(diffElements[0]);
|
|
172
|
-
const newLineNumVal = extractNewLineNumFromElement(diffElements[1]);
|
|
173
|
-
if (removedText && addedText) {
|
|
174
|
-
const { removedParts, addedParts } = renderWordLevelDiff(removedText, addedText, `word-${changeIndex}`);
|
|
175
|
-
allElements.push(renderLine(oldLineNumVal, null, "-", removedParts, "red", `word-diff-removed-${changeIndex}`));
|
|
176
|
-
allElements.push(renderLine(null, newLineNumVal, "+", addedParts, "green", `word-diff-added-${changeIndex}`));
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
allElements.push(...diffElements);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
allElements.push(...diffElements);
|
|
184
180
|
}
|
|
181
|
+
allElements.push(...diffElements);
|
|
185
182
|
}
|
|
186
183
|
catch (error) {
|
|
187
184
|
console.warn(`Error rendering diff for change ${changeIndex}:`, error);
|
|
@@ -202,56 +199,3 @@ export const DiffDisplay = ({ toolName, parameters, startLineNumber, }) => {
|
|
|
202
199
|
}
|
|
203
200
|
return (_jsx(Box, { flexDirection: "column", children: _jsx(Box, { paddingLeft: 2, borderLeft: true, borderColor: "cyan", flexDirection: "column", children: renderExpandedDiff() }) }));
|
|
204
201
|
};
|
|
205
|
-
// Helper function to extract text content from a React element
|
|
206
|
-
const extractTextFromElement = (element) => {
|
|
207
|
-
if (!React.isValidElement(element))
|
|
208
|
-
return null;
|
|
209
|
-
// Navigate through Box -> Text structure
|
|
210
|
-
// Our new structure is: Box -> Text (old), Text (new), Text (|), Text (prefix), Text (content)
|
|
211
|
-
const children = element.props.children;
|
|
212
|
-
if (Array.isArray(children) && children.length >= 5) {
|
|
213
|
-
const textElement = children[4]; // Fifth child should be the Text with content
|
|
214
|
-
if (React.isValidElement(textElement)) {
|
|
215
|
-
const textChildren = textElement.props
|
|
216
|
-
.children;
|
|
217
|
-
return Array.isArray(textChildren)
|
|
218
|
-
? textChildren.join("")
|
|
219
|
-
: String(textChildren || "");
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
return null;
|
|
223
|
-
};
|
|
224
|
-
const extractOldLineNumFromElement = (element) => {
|
|
225
|
-
if (!React.isValidElement(element))
|
|
226
|
-
return null;
|
|
227
|
-
const children = element.props.children;
|
|
228
|
-
if (Array.isArray(children) && children.length >= 1) {
|
|
229
|
-
const textElement = children[0];
|
|
230
|
-
if (React.isValidElement(textElement)) {
|
|
231
|
-
const textChildren = textElement.props
|
|
232
|
-
.children;
|
|
233
|
-
const val = (Array.isArray(textChildren)
|
|
234
|
-
? textChildren.join("")
|
|
235
|
-
: String(textChildren || "")).trim();
|
|
236
|
-
return val ? parseInt(val, 10) : null;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return null;
|
|
240
|
-
};
|
|
241
|
-
const extractNewLineNumFromElement = (element) => {
|
|
242
|
-
if (!React.isValidElement(element))
|
|
243
|
-
return null;
|
|
244
|
-
const children = element.props.children;
|
|
245
|
-
if (Array.isArray(children) && children.length >= 2) {
|
|
246
|
-
const textElement = children[1];
|
|
247
|
-
if (React.isValidElement(textElement)) {
|
|
248
|
-
const textChildren = textElement.props
|
|
249
|
-
.children;
|
|
250
|
-
const val = (Array.isArray(textChildren)
|
|
251
|
-
? textChildren.join("")
|
|
252
|
-
: String(textChildren || "")).trim();
|
|
253
|
-
return val ? parseInt(val, 10) : null;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return null;
|
|
257
|
-
};
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAeA,wBAAsB,IAAI,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAeA,wBAAsB,IAAI,kBAkTzB;AAGD,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAGpC,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAG3C,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,oBAAoB,GAC1B,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -65,6 +65,11 @@ export async function main() {
|
|
|
65
65
|
description: "Specify the AI model to use",
|
|
66
66
|
type: "string",
|
|
67
67
|
global: false,
|
|
68
|
+
})
|
|
69
|
+
.option("acp", {
|
|
70
|
+
description: "Run as an ACP bridge",
|
|
71
|
+
type: "boolean",
|
|
72
|
+
global: false,
|
|
68
73
|
})
|
|
69
74
|
.command("plugin", "Manage plugins and marketplaces", (yargs) => {
|
|
70
75
|
return yargs
|
|
@@ -170,6 +175,11 @@ export async function main() {
|
|
|
170
175
|
if (worktreeSession) {
|
|
171
176
|
process.chdir(workdir);
|
|
172
177
|
}
|
|
178
|
+
// Handle ACP mode
|
|
179
|
+
if (argv.acp) {
|
|
180
|
+
const { runAcp } = await import("./acp-cli.js");
|
|
181
|
+
return runAcp();
|
|
182
|
+
}
|
|
173
183
|
// Handle restore session command
|
|
174
184
|
if (argv.restore === "" ||
|
|
175
185
|
(process.argv.includes("-r") && argv.restore === undefined) ||
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-code",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "CLI-based code assistant powered by AI, built with React and Ink",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
"react": "^19.2.4",
|
|
40
40
|
"react-dom": "19.2.4",
|
|
41
41
|
"yargs": "^17.7.2",
|
|
42
|
-
"
|
|
42
|
+
"@agentclientprotocol/sdk": "0.15.0",
|
|
43
|
+
"zod": "^3.23.8",
|
|
44
|
+
"wave-agent-sdk": "0.10.0"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
45
47
|
"@types/react": "^19.1.8",
|
package/src/acp/agent.ts
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { Agent as WaveAgent, AgentOptions } from "wave-agent-sdk";
|
|
2
|
+
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,
|
|
18
|
+
} from "@agentclientprotocol/sdk";
|
|
19
|
+
|
|
20
|
+
export class WaveAcpAgent implements AcpAgent {
|
|
21
|
+
private agents: Map<string, WaveAgent> = new Map();
|
|
22
|
+
private connection: AgentSideConnection;
|
|
23
|
+
|
|
24
|
+
constructor(connection: AgentSideConnection) {
|
|
25
|
+
this.connection = connection;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async initialize(): Promise<InitializeResponse> {
|
|
29
|
+
logger.info("Initializing WaveAcpAgent");
|
|
30
|
+
return {
|
|
31
|
+
protocolVersion: 1,
|
|
32
|
+
agentInfo: {
|
|
33
|
+
name: "wave-agent",
|
|
34
|
+
version: "0.1.0",
|
|
35
|
+
},
|
|
36
|
+
agentCapabilities: {
|
|
37
|
+
loadSession: true,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async authenticate(): Promise<AuthenticateResponse | void> {
|
|
43
|
+
// No authentication required for now
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
|
|
47
|
+
const { cwd } = params;
|
|
48
|
+
logger.info(`Creating new session in ${cwd}`);
|
|
49
|
+
const callbacks: AgentOptions["callbacks"] = {};
|
|
50
|
+
const agent = await WaveAgent.create({
|
|
51
|
+
workdir: cwd,
|
|
52
|
+
callbacks: {
|
|
53
|
+
onAssistantContentUpdated: (chunk: string) =>
|
|
54
|
+
callbacks.onAssistantContentUpdated?.(chunk, ""),
|
|
55
|
+
onAssistantReasoningUpdated: (chunk: string) =>
|
|
56
|
+
callbacks.onAssistantReasoningUpdated?.(chunk, ""),
|
|
57
|
+
onToolBlockUpdated: (params: unknown) => {
|
|
58
|
+
const cb = callbacks.onToolBlockUpdated as
|
|
59
|
+
| ((params: unknown) => void)
|
|
60
|
+
| undefined;
|
|
61
|
+
cb?.(params);
|
|
62
|
+
},
|
|
63
|
+
onTasksChange: (tasks: unknown[]) => {
|
|
64
|
+
const cb = callbacks.onTasksChange as
|
|
65
|
+
| ((tasks: unknown[]) => void)
|
|
66
|
+
| undefined;
|
|
67
|
+
cb?.(tasks);
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const sessionId = agent.sessionId;
|
|
73
|
+
logger.info(`New session created: ${sessionId}`);
|
|
74
|
+
this.agents.set(sessionId, agent);
|
|
75
|
+
|
|
76
|
+
// Update the callbacks object with the correct sessionId
|
|
77
|
+
Object.assign(callbacks, this.createCallbacks(sessionId));
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
sessionId: sessionId as AcpSessionId,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
|
|
85
|
+
const { sessionId } = params;
|
|
86
|
+
logger.info(`Loading session: ${sessionId}`);
|
|
87
|
+
const callbacks: AgentOptions["callbacks"] = {};
|
|
88
|
+
const agent = await WaveAgent.create({
|
|
89
|
+
restoreSessionId: sessionId,
|
|
90
|
+
callbacks: {
|
|
91
|
+
onAssistantContentUpdated: (chunk: string) =>
|
|
92
|
+
callbacks.onAssistantContentUpdated?.(chunk, ""),
|
|
93
|
+
onAssistantReasoningUpdated: (chunk: string) =>
|
|
94
|
+
callbacks.onAssistantReasoningUpdated?.(chunk, ""),
|
|
95
|
+
onToolBlockUpdated: (params: unknown) => {
|
|
96
|
+
const cb = callbacks.onToolBlockUpdated as
|
|
97
|
+
| ((params: unknown) => void)
|
|
98
|
+
| undefined;
|
|
99
|
+
cb?.(params);
|
|
100
|
+
},
|
|
101
|
+
onTasksChange: (tasks: unknown[]) => {
|
|
102
|
+
const cb = callbacks.onTasksChange as
|
|
103
|
+
| ((tasks: unknown[]) => void)
|
|
104
|
+
| undefined;
|
|
105
|
+
cb?.(tasks);
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
this.agents.set(sessionId, agent);
|
|
111
|
+
logger.info(`Session loaded: ${sessionId}`);
|
|
112
|
+
|
|
113
|
+
// Update the callbacks object with the correct sessionId
|
|
114
|
+
Object.assign(callbacks, this.createCallbacks(sessionId));
|
|
115
|
+
|
|
116
|
+
return {};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async prompt(params: PromptRequest): Promise<PromptResponse> {
|
|
120
|
+
const { sessionId, prompt } = params;
|
|
121
|
+
logger.info(`Received prompt for session ${sessionId}`);
|
|
122
|
+
const agent = this.agents.get(sessionId);
|
|
123
|
+
if (!agent) {
|
|
124
|
+
logger.error(`Session ${sessionId} not found`);
|
|
125
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Map ACP prompt to Wave Agent sendMessage
|
|
129
|
+
const textContent = prompt
|
|
130
|
+
.filter((block) => block.type === "text")
|
|
131
|
+
.map((block) => (block as { text: string }).text)
|
|
132
|
+
.join("\n");
|
|
133
|
+
|
|
134
|
+
const images = prompt
|
|
135
|
+
.filter((block) => block.type === "image")
|
|
136
|
+
.map((block) => {
|
|
137
|
+
const img = block as { data: string; mimeType: string };
|
|
138
|
+
return {
|
|
139
|
+
path: `data:${img.mimeType};base64,${img.data}`,
|
|
140
|
+
mimeType: img.mimeType,
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
logger.info(
|
|
146
|
+
`Sending message to agent: ${textContent.substring(0, 50)}...`,
|
|
147
|
+
);
|
|
148
|
+
await agent.sendMessage(
|
|
149
|
+
textContent,
|
|
150
|
+
images.length > 0 ? images : undefined,
|
|
151
|
+
);
|
|
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
|
+
logger.info(`Message sent successfully for session ${sessionId}`);
|
|
159
|
+
return {
|
|
160
|
+
stopReason: "end_turn" as StopReason,
|
|
161
|
+
};
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (error instanceof Error && error.message.includes("abort")) {
|
|
164
|
+
logger.info(`Message aborted for session ${sessionId}`);
|
|
165
|
+
return {
|
|
166
|
+
stopReason: "cancelled" as StopReason,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
logger.error(`Error sending message for session ${sessionId}:`, error);
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async cancel(params: CancelNotification): Promise<void> {
|
|
175
|
+
const { sessionId } = params;
|
|
176
|
+
logger.info(`Cancelling message for session ${sessionId}`);
|
|
177
|
+
const agent = this.agents.get(sessionId);
|
|
178
|
+
if (agent) {
|
|
179
|
+
agent.abortMessage();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private createCallbacks(sessionId: string): AgentOptions["callbacks"] {
|
|
184
|
+
return {
|
|
185
|
+
onAssistantContentUpdated: (chunk: string) => {
|
|
186
|
+
this.connection.sessionUpdate({
|
|
187
|
+
sessionId: sessionId as AcpSessionId,
|
|
188
|
+
update: {
|
|
189
|
+
sessionUpdate: "agent_message_chunk",
|
|
190
|
+
content: {
|
|
191
|
+
type: "text",
|
|
192
|
+
text: chunk,
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
},
|
|
197
|
+
onAssistantReasoningUpdated: (chunk: string) => {
|
|
198
|
+
this.connection.sessionUpdate({
|
|
199
|
+
sessionId: sessionId as AcpSessionId,
|
|
200
|
+
update: {
|
|
201
|
+
sessionUpdate: "agent_thought_chunk",
|
|
202
|
+
content: {
|
|
203
|
+
type: "text",
|
|
204
|
+
text: chunk,
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
},
|
|
209
|
+
onToolBlockUpdated: (params) => {
|
|
210
|
+
const { id, name, stage, success, error, result } = params;
|
|
211
|
+
|
|
212
|
+
if (stage === "start") {
|
|
213
|
+
this.connection.sessionUpdate({
|
|
214
|
+
sessionId: sessionId as AcpSessionId,
|
|
215
|
+
update: {
|
|
216
|
+
sessionUpdate: "tool_call",
|
|
217
|
+
toolCallId: id,
|
|
218
|
+
title: name || "Tool Call",
|
|
219
|
+
status: "pending",
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (stage === "streaming") {
|
|
226
|
+
// We don't support streaming tool arguments in ACP yet
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const status: ToolCallStatus =
|
|
231
|
+
stage === "end"
|
|
232
|
+
? success
|
|
233
|
+
? "completed"
|
|
234
|
+
: "failed"
|
|
235
|
+
: stage === "running"
|
|
236
|
+
? "in_progress"
|
|
237
|
+
: "pending";
|
|
238
|
+
|
|
239
|
+
this.connection.sessionUpdate({
|
|
240
|
+
sessionId: sessionId as AcpSessionId,
|
|
241
|
+
update: {
|
|
242
|
+
sessionUpdate: "tool_call_update",
|
|
243
|
+
toolCallId: id,
|
|
244
|
+
status,
|
|
245
|
+
title: name || "Tool Call",
|
|
246
|
+
rawOutput: result || error,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
},
|
|
250
|
+
onTasksChange: (tasks) => {
|
|
251
|
+
this.connection.sessionUpdate({
|
|
252
|
+
sessionId: sessionId as AcpSessionId,
|
|
253
|
+
update: {
|
|
254
|
+
sessionUpdate: "plan",
|
|
255
|
+
entries: tasks.map((task) => ({
|
|
256
|
+
content: task.subject,
|
|
257
|
+
status:
|
|
258
|
+
task.status === "completed"
|
|
259
|
+
? "completed"
|
|
260
|
+
: task.status === "in_progress"
|
|
261
|
+
? "in_progress"
|
|
262
|
+
: "pending",
|
|
263
|
+
priority: "medium",
|
|
264
|
+
})),
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
package/src/acp/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
|
|
6
|
+
export async function startAcpCli() {
|
|
7
|
+
// Redirect console.log to logger to avoid interfering with JSON-RPC over stdio
|
|
8
|
+
console.log = (...args: unknown[]) => {
|
|
9
|
+
logger.info(...args);
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
logger.info("Starting ACP bridge...");
|
|
13
|
+
|
|
14
|
+
// Convert Node.js stdio to Web streams
|
|
15
|
+
const stdin = Readable.toWeb(process.stdin) as ReadableStream<Uint8Array>;
|
|
16
|
+
const stdout = Writable.toWeb(process.stdout) as WritableStream<Uint8Array>;
|
|
17
|
+
|
|
18
|
+
// Create ACP stream
|
|
19
|
+
const stream = ndJsonStream(stdout, stdin);
|
|
20
|
+
|
|
21
|
+
// Initialize AgentSideConnection
|
|
22
|
+
const connection = new AgentSideConnection((conn) => {
|
|
23
|
+
return new WaveAcpAgent(conn);
|
|
24
|
+
}, stream);
|
|
25
|
+
|
|
26
|
+
// Wait for connection to close
|
|
27
|
+
await connection.closed;
|
|
28
|
+
}
|
package/src/acp-cli.ts
ADDED
|
@@ -154,43 +154,87 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
|
|
|
154
154
|
|
|
155
155
|
// Process line diffs
|
|
156
156
|
const diffElements: React.ReactNode[] = [];
|
|
157
|
-
lineDiffs.
|
|
157
|
+
for (let i = 0; i < lineDiffs.length; i++) {
|
|
158
|
+
const part = lineDiffs[i];
|
|
158
159
|
const lines = part.value.split("\n");
|
|
159
160
|
// diffLines might return a trailing empty string if the content ends with a newline
|
|
160
161
|
if (lines[lines.length - 1] === "") {
|
|
161
162
|
lines.pop();
|
|
162
163
|
}
|
|
163
164
|
|
|
164
|
-
if (part.
|
|
165
|
+
if (part.removed) {
|
|
166
|
+
// Look ahead for an added block
|
|
167
|
+
if (i + 1 < lineDiffs.length && lineDiffs[i + 1].added) {
|
|
168
|
+
const nextPart = lineDiffs[i + 1];
|
|
169
|
+
const addedLines = nextPart.value.split("\n");
|
|
170
|
+
if (addedLines[addedLines.length - 1] === "") {
|
|
171
|
+
addedLines.pop();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (lines.length === addedLines.length) {
|
|
175
|
+
// Word-level diffing
|
|
176
|
+
lines.forEach((line, lineIndex) => {
|
|
177
|
+
const { removedParts, addedParts } = renderWordLevelDiff(
|
|
178
|
+
line,
|
|
179
|
+
addedLines[lineIndex],
|
|
180
|
+
`word-${changeIndex}-${i}-${lineIndex}`,
|
|
181
|
+
);
|
|
182
|
+
diffElements.push(
|
|
183
|
+
renderLine(
|
|
184
|
+
oldLineNum++,
|
|
185
|
+
null,
|
|
186
|
+
"-",
|
|
187
|
+
removedParts,
|
|
188
|
+
"red",
|
|
189
|
+
`remove-${changeIndex}-${i}-${lineIndex}`,
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
diffElements.push(
|
|
193
|
+
renderLine(
|
|
194
|
+
null,
|
|
195
|
+
newLineNum++,
|
|
196
|
+
"+",
|
|
197
|
+
addedParts,
|
|
198
|
+
"green",
|
|
199
|
+
`add-${changeIndex}-${i}-${lineIndex}`,
|
|
200
|
+
),
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
i++; // Skip the added block
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Fallback to standard removed rendering
|
|
165
209
|
lines.forEach((line, lineIndex) => {
|
|
166
210
|
diffElements.push(
|
|
167
211
|
renderLine(
|
|
212
|
+
oldLineNum++,
|
|
168
213
|
null,
|
|
169
|
-
|
|
170
|
-
"+",
|
|
214
|
+
"-",
|
|
171
215
|
line,
|
|
172
|
-
"
|
|
173
|
-
`
|
|
216
|
+
"red",
|
|
217
|
+
`remove-${changeIndex}-${i}-${lineIndex}`,
|
|
174
218
|
),
|
|
175
219
|
);
|
|
176
220
|
});
|
|
177
|
-
} else if (part.
|
|
221
|
+
} else if (part.added) {
|
|
178
222
|
lines.forEach((line, lineIndex) => {
|
|
179
223
|
diffElements.push(
|
|
180
224
|
renderLine(
|
|
181
|
-
oldLineNum++,
|
|
182
225
|
null,
|
|
183
|
-
|
|
226
|
+
newLineNum++,
|
|
227
|
+
"+",
|
|
184
228
|
line,
|
|
185
|
-
"
|
|
186
|
-
`
|
|
229
|
+
"green",
|
|
230
|
+
`add-${changeIndex}-${i}-${lineIndex}`,
|
|
187
231
|
),
|
|
188
232
|
);
|
|
189
233
|
});
|
|
190
234
|
} else {
|
|
191
235
|
// Context lines - show unchanged content
|
|
192
|
-
const isFirstBlock =
|
|
193
|
-
const isLastBlock =
|
|
236
|
+
const isFirstBlock = i === 0;
|
|
237
|
+
const isLastBlock = i === lineDiffs.length - 1;
|
|
194
238
|
|
|
195
239
|
let linesToDisplay = lines;
|
|
196
240
|
let showEllipsisTop = false;
|
|
@@ -221,7 +265,7 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
|
|
|
221
265
|
|
|
222
266
|
if (showEllipsisTop) {
|
|
223
267
|
diffElements.push(
|
|
224
|
-
<Box key={`ellipsis-top-${changeIndex}-${
|
|
268
|
+
<Box key={`ellipsis-top-${changeIndex}-${i}`}>
|
|
225
269
|
<Text color="gray">{" ".repeat(maxDigits * 2 + 2)}...</Text>
|
|
226
270
|
</Box>,
|
|
227
271
|
);
|
|
@@ -239,7 +283,7 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
|
|
|
239
283
|
oldLineNum += skipCount;
|
|
240
284
|
newLineNum += skipCount;
|
|
241
285
|
diffElements.push(
|
|
242
|
-
<Box key={`ellipsis-mid-${changeIndex}-${
|
|
286
|
+
<Box key={`ellipsis-mid-${changeIndex}-${i}`}>
|
|
243
287
|
<Text color="gray">
|
|
244
288
|
{" ".repeat(maxDigits * 2 + 2)}...
|
|
245
289
|
</Text>
|
|
@@ -254,7 +298,7 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
|
|
|
254
298
|
" ",
|
|
255
299
|
line,
|
|
256
300
|
"white",
|
|
257
|
-
`context-${changeIndex}-${
|
|
301
|
+
`context-${changeIndex}-${i}-${lineIndex}`,
|
|
258
302
|
),
|
|
259
303
|
);
|
|
260
304
|
});
|
|
@@ -266,62 +310,14 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
|
|
|
266
310
|
oldLineNum += skipCount;
|
|
267
311
|
newLineNum += skipCount;
|
|
268
312
|
diffElements.push(
|
|
269
|
-
<Box key={`ellipsis-bottom-${changeIndex}-${
|
|
313
|
+
<Box key={`ellipsis-bottom-${changeIndex}-${i}`}>
|
|
270
314
|
<Text color="gray">{" ".repeat(maxDigits * 2 + 2)}...</Text>
|
|
271
315
|
</Box>,
|
|
272
316
|
);
|
|
273
317
|
}
|
|
274
318
|
}
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// If it's a single line change (one removed, one added), use word-level diff
|
|
278
|
-
if (
|
|
279
|
-
diffElements.length === 2 &&
|
|
280
|
-
React.isValidElement(diffElements[0]) &&
|
|
281
|
-
React.isValidElement(diffElements[1]) &&
|
|
282
|
-
typeof diffElements[0].key === "string" &&
|
|
283
|
-
diffElements[0].key.includes("remove-") &&
|
|
284
|
-
typeof diffElements[1].key === "string" &&
|
|
285
|
-
diffElements[1].key.includes("add-")
|
|
286
|
-
) {
|
|
287
|
-
const removedText = extractTextFromElement(diffElements[0]);
|
|
288
|
-
const addedText = extractTextFromElement(diffElements[1]);
|
|
289
|
-
const oldLineNumVal = extractOldLineNumFromElement(diffElements[0]);
|
|
290
|
-
const newLineNumVal = extractNewLineNumFromElement(diffElements[1]);
|
|
291
|
-
|
|
292
|
-
if (removedText && addedText) {
|
|
293
|
-
const { removedParts, addedParts } = renderWordLevelDiff(
|
|
294
|
-
removedText,
|
|
295
|
-
addedText,
|
|
296
|
-
`word-${changeIndex}`,
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
allElements.push(
|
|
300
|
-
renderLine(
|
|
301
|
-
oldLineNumVal,
|
|
302
|
-
null,
|
|
303
|
-
"-",
|
|
304
|
-
removedParts,
|
|
305
|
-
"red",
|
|
306
|
-
`word-diff-removed-${changeIndex}`,
|
|
307
|
-
),
|
|
308
|
-
);
|
|
309
|
-
allElements.push(
|
|
310
|
-
renderLine(
|
|
311
|
-
null,
|
|
312
|
-
newLineNumVal,
|
|
313
|
-
"+",
|
|
314
|
-
addedParts,
|
|
315
|
-
"green",
|
|
316
|
-
`word-diff-added-${changeIndex}`,
|
|
317
|
-
),
|
|
318
|
-
);
|
|
319
|
-
} else {
|
|
320
|
-
allElements.push(...diffElements);
|
|
321
|
-
}
|
|
322
|
-
} else {
|
|
323
|
-
allElements.push(...diffElements);
|
|
324
319
|
}
|
|
320
|
+
allElements.push(...diffElements);
|
|
325
321
|
} catch (error) {
|
|
326
322
|
console.warn(
|
|
327
323
|
`Error rendering diff for change ${changeIndex}:`,
|
|
@@ -361,71 +357,3 @@ export const DiffDisplay: React.FC<DiffDisplayProps> = ({
|
|
|
361
357
|
</Box>
|
|
362
358
|
);
|
|
363
359
|
};
|
|
364
|
-
|
|
365
|
-
// Helper function to extract text content from a React element
|
|
366
|
-
const extractTextFromElement = (element: React.ReactNode): string | null => {
|
|
367
|
-
if (!React.isValidElement(element)) return null;
|
|
368
|
-
|
|
369
|
-
// Navigate through Box -> Text structure
|
|
370
|
-
// Our new structure is: Box -> Text (old), Text (new), Text (|), Text (prefix), Text (content)
|
|
371
|
-
const children = (
|
|
372
|
-
element.props as unknown as { children?: React.ReactNode[] }
|
|
373
|
-
).children;
|
|
374
|
-
if (Array.isArray(children) && children.length >= 5) {
|
|
375
|
-
const textElement = children[4]; // Fifth child should be the Text with content
|
|
376
|
-
if (React.isValidElement(textElement)) {
|
|
377
|
-
const textChildren = (textElement.props as Record<string, unknown>)
|
|
378
|
-
.children;
|
|
379
|
-
return Array.isArray(textChildren)
|
|
380
|
-
? textChildren.join("")
|
|
381
|
-
: String(textChildren || "");
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
return null;
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
const extractOldLineNumFromElement = (
|
|
388
|
-
element: React.ReactNode,
|
|
389
|
-
): number | null => {
|
|
390
|
-
if (!React.isValidElement(element)) return null;
|
|
391
|
-
const children = (
|
|
392
|
-
element.props as unknown as { children?: React.ReactNode[] }
|
|
393
|
-
).children;
|
|
394
|
-
if (Array.isArray(children) && children.length >= 1) {
|
|
395
|
-
const textElement = children[0];
|
|
396
|
-
if (React.isValidElement(textElement)) {
|
|
397
|
-
const textChildren = (textElement.props as Record<string, unknown>)
|
|
398
|
-
.children;
|
|
399
|
-
const val = (
|
|
400
|
-
Array.isArray(textChildren)
|
|
401
|
-
? textChildren.join("")
|
|
402
|
-
: String(textChildren || "")
|
|
403
|
-
).trim();
|
|
404
|
-
return val ? parseInt(val, 10) : null;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
return null;
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
const extractNewLineNumFromElement = (
|
|
411
|
-
element: React.ReactNode,
|
|
412
|
-
): number | null => {
|
|
413
|
-
if (!React.isValidElement(element)) return null;
|
|
414
|
-
const children = (
|
|
415
|
-
element.props as unknown as { children?: React.ReactNode[] }
|
|
416
|
-
).children;
|
|
417
|
-
if (Array.isArray(children) && children.length >= 2) {
|
|
418
|
-
const textElement = children[1];
|
|
419
|
-
if (React.isValidElement(textElement)) {
|
|
420
|
-
const textChildren = (textElement.props as Record<string, unknown>)
|
|
421
|
-
.children;
|
|
422
|
-
const val = (
|
|
423
|
-
Array.isArray(textChildren)
|
|
424
|
-
? textChildren.join("")
|
|
425
|
-
: String(textChildren || "")
|
|
426
|
-
).trim();
|
|
427
|
-
return val ? parseInt(val, 10) : null;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
return null;
|
|
431
|
-
};
|
package/src/index.ts
CHANGED
|
@@ -70,6 +70,11 @@ export async function main() {
|
|
|
70
70
|
type: "string",
|
|
71
71
|
global: false,
|
|
72
72
|
})
|
|
73
|
+
.option("acp", {
|
|
74
|
+
description: "Run as an ACP bridge",
|
|
75
|
+
type: "boolean",
|
|
76
|
+
global: false,
|
|
77
|
+
})
|
|
73
78
|
.command("plugin", "Manage plugins and marketplaces", (yargs) => {
|
|
74
79
|
return yargs
|
|
75
80
|
.help()
|
|
@@ -246,6 +251,12 @@ export async function main() {
|
|
|
246
251
|
process.chdir(workdir);
|
|
247
252
|
}
|
|
248
253
|
|
|
254
|
+
// Handle ACP mode
|
|
255
|
+
if (argv.acp) {
|
|
256
|
+
const { runAcp } = await import("./acp-cli.js");
|
|
257
|
+
return runAcp();
|
|
258
|
+
}
|
|
259
|
+
|
|
249
260
|
// Handle restore session command
|
|
250
261
|
if (
|
|
251
262
|
argv.restore === "" ||
|