wave-code 1.0.7 → 1.0.8
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/components/ChatInterface.js +1 -1
- package/dist/components/ConfirmationDetails.d.ts +1 -0
- package/dist/components/ConfirmationDetails.js +5 -3
- package/dist/components/RewindCommand.js +11 -4
- package/dist/contexts/useChat.d.ts +17 -2
- package/dist/contexts/useChat.js +99 -7
- package/dist/daemon/commands.d.ts +49 -0
- package/dist/daemon/commands.js +341 -0
- package/dist/daemon/jsonRpcClient.d.ts +38 -0
- package/dist/daemon/jsonRpcClient.js +129 -0
- package/dist/daemon/socketClient.d.ts +13 -0
- package/dist/daemon/socketClient.js +26 -0
- package/dist/index.js +88 -0
- package/dist/stdio/agentBridge.d.ts +12 -0
- package/dist/stdio/agentBridge.js +63 -10
- package/dist/stdio/protocol.d.ts +1 -1
- package/package.json +2 -2
- package/src/components/ChatInterface.tsx +2 -0
- package/src/components/ConfirmationDetails.tsx +6 -0
- package/src/components/RewindCommand.tsx +10 -4
- package/src/contexts/useChat.tsx +123 -5
- package/src/daemon/commands.ts +444 -0
- package/src/daemon/jsonRpcClient.ts +158 -0
- package/src/daemon/socketClient.ts +34 -0
- package/src/index.ts +130 -0
- package/src/stdio/agentBridge.ts +76 -10
- package/src/stdio/protocol.ts +2 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JsonRpcClient — minimal JSON-RPC transport over a line-delimited duplex
|
|
3
|
+
* stream (one JSON object per line).
|
|
4
|
+
*
|
|
5
|
+
* Used by `wave daemon` subcommands to talk to the wave daemon's unix socket.
|
|
6
|
+
* Mirrors packages/desktop/src/main/stdio/jsonRpcClient.ts (packages/code
|
|
7
|
+
* cannot import from packages/desktop). Subclasses own the transport and hook
|
|
8
|
+
* in:
|
|
9
|
+
* - `writeLine(message)` writes one JSON line to the peer.
|
|
10
|
+
* - `attachReadable(readable)` wires the inbound half (socket).
|
|
11
|
+
* - `handleClosed(reason)` marks the transport dead and rejects every pending
|
|
12
|
+
* request. Idempotent — safe to call from both dispose() and an exit/close
|
|
13
|
+
* event on the underlying transport.
|
|
14
|
+
*/
|
|
15
|
+
import type { Readable } from "stream";
|
|
16
|
+
export type NotificationHandler = (params: unknown, sessionId?: string) => void;
|
|
17
|
+
export declare abstract class JsonRpcClient {
|
|
18
|
+
private nextId;
|
|
19
|
+
private pending;
|
|
20
|
+
private handlers;
|
|
21
|
+
private closedHandlers;
|
|
22
|
+
private closed;
|
|
23
|
+
protected abstract writeLine(message: string): void;
|
|
24
|
+
/** Wire an inbound Readable (socket) to the line parser. */
|
|
25
|
+
protected attachReadable(readable: Readable): void;
|
|
26
|
+
/** Mark the transport closed: reject every pending request. Idempotent. */
|
|
27
|
+
protected handleClosed(reason: string): void;
|
|
28
|
+
protected get isClosed(): boolean;
|
|
29
|
+
/** Close the transport and reject pending requests (subclass tears down its stream). */
|
|
30
|
+
abstract dispose(): void;
|
|
31
|
+
/** Observe transport teardown (dispose or unexpected close), fired once. */
|
|
32
|
+
onClosed(handler: () => void): void;
|
|
33
|
+
request(method: string, params?: unknown, sessionId?: string): Promise<unknown>;
|
|
34
|
+
notify(method: string, params?: unknown, sessionId?: string): void;
|
|
35
|
+
onNotification(method: string, handler: NotificationHandler): void;
|
|
36
|
+
offNotification(method: string, handler: NotificationHandler): void;
|
|
37
|
+
private handleLine;
|
|
38
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JsonRpcClient — minimal JSON-RPC transport over a line-delimited duplex
|
|
3
|
+
* stream (one JSON object per line).
|
|
4
|
+
*
|
|
5
|
+
* Used by `wave daemon` subcommands to talk to the wave daemon's unix socket.
|
|
6
|
+
* Mirrors packages/desktop/src/main/stdio/jsonRpcClient.ts (packages/code
|
|
7
|
+
* cannot import from packages/desktop). Subclasses own the transport and hook
|
|
8
|
+
* in:
|
|
9
|
+
* - `writeLine(message)` writes one JSON line to the peer.
|
|
10
|
+
* - `attachReadable(readable)` wires the inbound half (socket).
|
|
11
|
+
* - `handleClosed(reason)` marks the transport dead and rejects every pending
|
|
12
|
+
* request. Idempotent — safe to call from both dispose() and an exit/close
|
|
13
|
+
* event on the underlying transport.
|
|
14
|
+
*/
|
|
15
|
+
import { createInterface } from "readline";
|
|
16
|
+
export class JsonRpcClient {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.nextId = 1;
|
|
19
|
+
this.pending = new Map();
|
|
20
|
+
this.handlers = new Map();
|
|
21
|
+
this.closedHandlers = [];
|
|
22
|
+
this.closed = false;
|
|
23
|
+
}
|
|
24
|
+
/** Wire an inbound Readable (socket) to the line parser. */
|
|
25
|
+
attachReadable(readable) {
|
|
26
|
+
const rl = createInterface({ input: readable });
|
|
27
|
+
rl.on("line", (line) => this.handleLine(line));
|
|
28
|
+
// readline re-emits input errors on the Interface; the transport subclass
|
|
29
|
+
// already handles errors on the underlying stream, swallow them here so
|
|
30
|
+
// they never surface as an uncaught 'error' on the Interface.
|
|
31
|
+
rl.on("error", () => { });
|
|
32
|
+
}
|
|
33
|
+
/** Mark the transport closed: reject every pending request. Idempotent. */
|
|
34
|
+
handleClosed(reason) {
|
|
35
|
+
if (this.closed)
|
|
36
|
+
return;
|
|
37
|
+
this.closed = true;
|
|
38
|
+
const error = new Error(reason);
|
|
39
|
+
for (const p of this.pending.values())
|
|
40
|
+
p.reject(error);
|
|
41
|
+
this.pending.clear();
|
|
42
|
+
for (const handler of this.closedHandlers)
|
|
43
|
+
handler();
|
|
44
|
+
this.closedHandlers = [];
|
|
45
|
+
}
|
|
46
|
+
get isClosed() {
|
|
47
|
+
return this.closed;
|
|
48
|
+
}
|
|
49
|
+
/** Observe transport teardown (dispose or unexpected close), fired once. */
|
|
50
|
+
onClosed(handler) {
|
|
51
|
+
this.closedHandlers.push(handler);
|
|
52
|
+
}
|
|
53
|
+
async request(method, params, sessionId) {
|
|
54
|
+
if (this.closed) {
|
|
55
|
+
throw new Error("连接已断开。wave 进程已退出,请重启编辑器或检查 CLI 安装。");
|
|
56
|
+
}
|
|
57
|
+
const id = this.nextId++;
|
|
58
|
+
const envelope = { id, method, params };
|
|
59
|
+
if (sessionId)
|
|
60
|
+
envelope.sessionId = sessionId;
|
|
61
|
+
const message = JSON.stringify(envelope) + "\n";
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
this.pending.set(id, { resolve, reject });
|
|
64
|
+
this.writeLine(message);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
notify(method, params, sessionId) {
|
|
68
|
+
if (this.closed)
|
|
69
|
+
return;
|
|
70
|
+
const envelope = { method, params };
|
|
71
|
+
if (sessionId)
|
|
72
|
+
envelope.sessionId = sessionId;
|
|
73
|
+
this.writeLine(JSON.stringify(envelope) + "\n");
|
|
74
|
+
}
|
|
75
|
+
onNotification(method, handler) {
|
|
76
|
+
let set = this.handlers.get(method);
|
|
77
|
+
if (!set) {
|
|
78
|
+
set = new Set();
|
|
79
|
+
this.handlers.set(method, set);
|
|
80
|
+
}
|
|
81
|
+
set.add(handler);
|
|
82
|
+
}
|
|
83
|
+
offNotification(method, handler) {
|
|
84
|
+
this.handlers.get(method)?.delete(handler);
|
|
85
|
+
}
|
|
86
|
+
// ── Internal ──────────────────────────────────────────────────
|
|
87
|
+
handleLine(line) {
|
|
88
|
+
let msg;
|
|
89
|
+
try {
|
|
90
|
+
msg = JSON.parse(line);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
console.error("[wave-jsonrpc] Failed to parse:", line);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (typeof msg !== "object" || msg === null)
|
|
97
|
+
return;
|
|
98
|
+
const obj = msg;
|
|
99
|
+
// Response (has id + result/error)
|
|
100
|
+
if ("id" in obj && ("result" in obj || "error" in obj)) {
|
|
101
|
+
const id = Number(obj.id);
|
|
102
|
+
const pending = this.pending.get(id);
|
|
103
|
+
if (pending) {
|
|
104
|
+
this.pending.delete(id);
|
|
105
|
+
if (obj.error) {
|
|
106
|
+
const err = obj.error;
|
|
107
|
+
pending.reject(new Error(err.message));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
pending.resolve(obj.result);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
// Notification (has method, no id)
|
|
116
|
+
if ("method" in obj && !("id" in obj)) {
|
|
117
|
+
const method = obj.method;
|
|
118
|
+
const params = obj.params;
|
|
119
|
+
const sessionId = typeof obj.sessionId === "string" ? obj.sessionId : undefined;
|
|
120
|
+
const set = this.handlers.get(method);
|
|
121
|
+
if (set) {
|
|
122
|
+
for (const handler of set) {
|
|
123
|
+
handler(params, sessionId);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SocketClient — JSON-RPC transport over a unix socket connection to the wave
|
|
3
|
+
* daemon. Mirrors packages/desktop/src/main/stdio/socketClient.ts (packages/code
|
|
4
|
+
* cannot import from packages/desktop).
|
|
5
|
+
*/
|
|
6
|
+
import type { Socket } from "net";
|
|
7
|
+
import { JsonRpcClient } from "./jsonRpcClient.js";
|
|
8
|
+
export declare class SocketClient extends JsonRpcClient {
|
|
9
|
+
private socket;
|
|
10
|
+
constructor(socket: Socket);
|
|
11
|
+
protected writeLine(message: string): void;
|
|
12
|
+
dispose(): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SocketClient — JSON-RPC transport over a unix socket connection to the wave
|
|
3
|
+
* daemon. Mirrors packages/desktop/src/main/stdio/socketClient.ts (packages/code
|
|
4
|
+
* cannot import from packages/desktop).
|
|
5
|
+
*/
|
|
6
|
+
import { JsonRpcClient } from "./jsonRpcClient.js";
|
|
7
|
+
export class SocketClient extends JsonRpcClient {
|
|
8
|
+
constructor(socket) {
|
|
9
|
+
super();
|
|
10
|
+
this.socket = socket;
|
|
11
|
+
this.attachReadable(socket);
|
|
12
|
+
socket.on("close", () => {
|
|
13
|
+
this.handleClosed("远端连接已断开。");
|
|
14
|
+
});
|
|
15
|
+
socket.on("error", (err) => {
|
|
16
|
+
console.error("[wave-daemon] Socket error:", err.message);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
writeLine(message) {
|
|
20
|
+
this.socket.write(message);
|
|
21
|
+
}
|
|
22
|
+
dispose() {
|
|
23
|
+
this.handleClosed("远端连接已断开。");
|
|
24
|
+
this.socket.destroy();
|
|
25
|
+
}
|
|
26
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -178,6 +178,94 @@ export async function main() {
|
|
|
178
178
|
const { updatePluginCommand } = await import("./commands/plugin/update.js");
|
|
179
179
|
await updatePluginCommand(argv);
|
|
180
180
|
});
|
|
181
|
+
})
|
|
182
|
+
.command("daemon", "Manage the wave daemon (client subcommands — to START a daemon use `wave --daemon <socket>` instead)", (yargs) => {
|
|
183
|
+
return yargs
|
|
184
|
+
.help()
|
|
185
|
+
.command("list", "List sessions hosted by the daemon (in-memory registry)", {}, async () => {
|
|
186
|
+
const { daemonListCommand, DEFAULT_DAEMON_SOCKET } = await import("./daemon/commands.js");
|
|
187
|
+
await daemonListCommand(DEFAULT_DAEMON_SOCKET);
|
|
188
|
+
})
|
|
189
|
+
.command("status <sessionId>", "Show a session's progress and recent messages", (yargs) => {
|
|
190
|
+
return yargs
|
|
191
|
+
.positional("sessionId", {
|
|
192
|
+
describe: "Session ID hosted by the daemon",
|
|
193
|
+
type: "string",
|
|
194
|
+
})
|
|
195
|
+
.option("lines", {
|
|
196
|
+
describe: "Number of recent messages to show",
|
|
197
|
+
default: 20,
|
|
198
|
+
type: "number",
|
|
199
|
+
});
|
|
200
|
+
}, async (argv) => {
|
|
201
|
+
const { daemonStatusCommand, DEFAULT_DAEMON_SOCKET } = await import("./daemon/commands.js");
|
|
202
|
+
await daemonStatusCommand(DEFAULT_DAEMON_SOCKET, argv.sessionId, argv.lines);
|
|
203
|
+
})
|
|
204
|
+
.command("send <sessionId> <message>", "Inject a message into a session and wait for the reply", (yargs) => {
|
|
205
|
+
return yargs
|
|
206
|
+
.positional("sessionId", {
|
|
207
|
+
describe: "Session ID hosted by the daemon",
|
|
208
|
+
type: "string",
|
|
209
|
+
})
|
|
210
|
+
.positional("message", {
|
|
211
|
+
describe: "Message to send",
|
|
212
|
+
type: "string",
|
|
213
|
+
})
|
|
214
|
+
.option("timeout", {
|
|
215
|
+
describe: "Seconds to wait for the reply (0 = no limit)",
|
|
216
|
+
default: 600,
|
|
217
|
+
type: "number",
|
|
218
|
+
});
|
|
219
|
+
}, async (argv) => {
|
|
220
|
+
const { daemonSendCommand, DEFAULT_DAEMON_SOCKET } = await import("./daemon/commands.js");
|
|
221
|
+
await daemonSendCommand(DEFAULT_DAEMON_SOCKET, argv.sessionId, argv.message, { timeout: argv.timeout });
|
|
222
|
+
})
|
|
223
|
+
.command("respond <sessionId> <requestId>", "Respond to a pending permission request", (yargs) => {
|
|
224
|
+
return yargs
|
|
225
|
+
.positional("sessionId", {
|
|
226
|
+
describe: "Session ID hosting the pending request",
|
|
227
|
+
type: "string",
|
|
228
|
+
})
|
|
229
|
+
.positional("requestId", {
|
|
230
|
+
describe: "Pending permission request ID",
|
|
231
|
+
type: "string",
|
|
232
|
+
})
|
|
233
|
+
.option("allow", {
|
|
234
|
+
describe: "Allow the operation",
|
|
235
|
+
type: "boolean",
|
|
236
|
+
})
|
|
237
|
+
.option("deny", {
|
|
238
|
+
describe: "Deny the operation",
|
|
239
|
+
type: "boolean",
|
|
240
|
+
})
|
|
241
|
+
.option("reason", {
|
|
242
|
+
describe: "Reason for the decision (deny)",
|
|
243
|
+
type: "string",
|
|
244
|
+
})
|
|
245
|
+
.option("answer", {
|
|
246
|
+
describe: "Answers JSON for AskUserQuestion requests",
|
|
247
|
+
type: "string",
|
|
248
|
+
})
|
|
249
|
+
.option("rule", {
|
|
250
|
+
describe: "Persist an allowed rule (e.g. Bash(ls))",
|
|
251
|
+
type: "string",
|
|
252
|
+
})
|
|
253
|
+
.option("mode", {
|
|
254
|
+
describe: "Switch the session's permission mode",
|
|
255
|
+
type: "string",
|
|
256
|
+
});
|
|
257
|
+
}, async (argv) => {
|
|
258
|
+
const { daemonRespondCommand, DEFAULT_DAEMON_SOCKET } = await import("./daemon/commands.js");
|
|
259
|
+
await daemonRespondCommand(DEFAULT_DAEMON_SOCKET, argv.sessionId, argv.requestId, {
|
|
260
|
+
allow: argv.allow,
|
|
261
|
+
deny: argv.deny,
|
|
262
|
+
reason: argv.reason,
|
|
263
|
+
answer: argv.answer,
|
|
264
|
+
rule: argv.rule,
|
|
265
|
+
mode: argv.mode,
|
|
266
|
+
});
|
|
267
|
+
})
|
|
268
|
+
.demandCommand(1, "Please specify a daemon subcommand");
|
|
181
269
|
})
|
|
182
270
|
.command("update", "Update WAVE Code to the latest version", {}, async () => {
|
|
183
271
|
const { updateCommand } = await import("./commands/update.js");
|
|
@@ -59,6 +59,15 @@ export declare class AgentBridge {
|
|
|
59
59
|
private getConfiguredModels;
|
|
60
60
|
private setModel;
|
|
61
61
|
private sendMessage;
|
|
62
|
+
/**
|
|
63
|
+
* Webview hosts (desktop/vscode/jetbrains) send pasted images as inline
|
|
64
|
+
* data URLs — there is no local file behind them. Persist each to a temp
|
|
65
|
+
* file so the model gets a real path it can reference with tools (aligned
|
|
66
|
+
* with Claude Code's `[Image source: <path>]` metadata). Real paths pass
|
|
67
|
+
* through untouched; unparseable data URLs pass through as-is and are
|
|
68
|
+
* skipped by the SDK rather than blocking the message.
|
|
69
|
+
*/
|
|
70
|
+
private persistDataUrlImages;
|
|
62
71
|
private bang;
|
|
63
72
|
private askBtw;
|
|
64
73
|
private abortMessage;
|
|
@@ -99,6 +108,9 @@ export declare class AgentBridge {
|
|
|
99
108
|
* client disconnected (daemon mode). Responding to any listed requestId
|
|
100
109
|
* resolves the in-process promise. */
|
|
101
110
|
private listPendingPermissions;
|
|
111
|
+
/** Daemon list: expose the in-memory session registry (live sessions only,
|
|
112
|
+
* not disk-scanning). Registration order is preserved. */
|
|
113
|
+
private listDaemonSessions;
|
|
102
114
|
private getAuthStatus;
|
|
103
115
|
private login;
|
|
104
116
|
private logout;
|
|
@@ -59,6 +59,8 @@ export class AgentBridge {
|
|
|
59
59
|
return this.getSessionInfo(sessionId);
|
|
60
60
|
case "listPendingPermissions":
|
|
61
61
|
return this.listPendingPermissions();
|
|
62
|
+
case "listDaemonSessions":
|
|
63
|
+
return this.listDaemonSessions();
|
|
62
64
|
case "updateConfig":
|
|
63
65
|
return this.updateConfig(p, sessionId);
|
|
64
66
|
case "getConfiguredModels":
|
|
@@ -474,9 +476,39 @@ export class AgentBridge {
|
|
|
474
476
|
catch {
|
|
475
477
|
// Best-effort; don't block message sending on history save failure
|
|
476
478
|
}
|
|
477
|
-
await entry.agent.sendMessage(params.text, params.images);
|
|
479
|
+
await entry.agent.sendMessage(params.text, this.persistDataUrlImages(params.images));
|
|
478
480
|
return null;
|
|
479
481
|
}
|
|
482
|
+
/**
|
|
483
|
+
* Webview hosts (desktop/vscode/jetbrains) send pasted images as inline
|
|
484
|
+
* data URLs — there is no local file behind them. Persist each to a temp
|
|
485
|
+
* file so the model gets a real path it can reference with tools (aligned
|
|
486
|
+
* with Claude Code's `[Image source: <path>]` metadata). Real paths pass
|
|
487
|
+
* through untouched; unparseable data URLs pass through as-is and are
|
|
488
|
+
* skipped by the SDK rather than blocking the message.
|
|
489
|
+
*/
|
|
490
|
+
persistDataUrlImages(images) {
|
|
491
|
+
if (!images || images.length === 0)
|
|
492
|
+
return images;
|
|
493
|
+
return images.map((img) => {
|
|
494
|
+
if (!img.path.startsWith("data:"))
|
|
495
|
+
return img;
|
|
496
|
+
const match = /^data:([^;,]+);base64,(.*)$/s.exec(img.path);
|
|
497
|
+
if (!match)
|
|
498
|
+
return img;
|
|
499
|
+
try {
|
|
500
|
+
const mimeType = match[1];
|
|
501
|
+
const ext = mimeType.split("/")[1]?.replace("jpeg", "jpg") || "png";
|
|
502
|
+
const filePath = join(tmpdir(), `wave-image-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`);
|
|
503
|
+
writeFileSync(filePath, Buffer.from(match[2], "base64"));
|
|
504
|
+
return { path: filePath, mimeType };
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
logger.warn("Failed to persist pasted image to temp file:", error);
|
|
508
|
+
return img;
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
}
|
|
480
512
|
async bang(command, sessionId) {
|
|
481
513
|
const entry = this.requireSession(sessionId);
|
|
482
514
|
await entry.agent.bang(command);
|
|
@@ -511,7 +543,10 @@ export class AgentBridge {
|
|
|
511
543
|
async rewindToMessage(messageId, sessionId) {
|
|
512
544
|
const entry = this.requireSession(sessionId);
|
|
513
545
|
const { messages } = await entry.agent.getFullMessageThread();
|
|
514
|
-
|
|
546
|
+
// 压缩是 append-only:同 id 消息(压缩前历史 + 压缩后 append 的重复)会
|
|
547
|
+
// 在磁盘完整线程中出现多次。用户看到的折叠视图对应最后一次出现,
|
|
548
|
+
// 因此匹配最后一个而非第一个,避免回滚时连压缩摘要一起删掉。
|
|
549
|
+
const index = messages.map((m) => m.id).lastIndexOf(messageId);
|
|
515
550
|
if (index === -1) {
|
|
516
551
|
throw new RpcError(PROTOCOL_INTERNAL_ERROR, `Message not found: ${messageId}`);
|
|
517
552
|
}
|
|
@@ -523,13 +558,19 @@ export class AgentBridge {
|
|
|
523
558
|
async listRewindCheckpoints(sessionId) {
|
|
524
559
|
const entry = this.requireSession(sessionId);
|
|
525
560
|
const { messages } = await entry.agent.getFullMessageThread();
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
561
|
+
// 压缩 append-only 后同 id 消息在磁盘完整线程中重复出现(压缩前历史 +
|
|
562
|
+
// 压缩后 append 的重复)。按 id 去重并保留最后一次出现(与折叠后的
|
|
563
|
+
// UI/内存视图一致),避免弹窗把同一条用户消息显示两遍。
|
|
564
|
+
const checkpointMap = new Map();
|
|
565
|
+
for (const m of messages) {
|
|
566
|
+
if (isUserCheckpointMessage(m) && m.id) {
|
|
567
|
+
checkpointMap.set(m.id, {
|
|
568
|
+
id: m.id,
|
|
569
|
+
content: getMessageContent(m).replace(/\s+/g, " ").trim(),
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return { checkpoints: Array.from(checkpointMap.values()) };
|
|
533
574
|
}
|
|
534
575
|
deleteQueuedMessage(index, sessionId) {
|
|
535
576
|
const entry = this.requireSession(sessionId);
|
|
@@ -540,7 +581,7 @@ export class AgentBridge {
|
|
|
540
581
|
const entry = this.requireSession(sessionId);
|
|
541
582
|
const ok = entry.agent.updateQueuedMessageById(id, {
|
|
542
583
|
content: text,
|
|
543
|
-
images,
|
|
584
|
+
images: this.persistDataUrlImages(images),
|
|
544
585
|
});
|
|
545
586
|
return { ok };
|
|
546
587
|
}
|
|
@@ -698,6 +739,18 @@ export class AgentBridge {
|
|
|
698
739
|
})),
|
|
699
740
|
};
|
|
700
741
|
}
|
|
742
|
+
/** Daemon list: expose the in-memory session registry (live sessions only,
|
|
743
|
+
* not disk-scanning). Registration order is preserved. */
|
|
744
|
+
listDaemonSessions() {
|
|
745
|
+
return {
|
|
746
|
+
sessions: [...this.sessions.entries()].map(([sessionId, entry]) => ({
|
|
747
|
+
sessionId,
|
|
748
|
+
workingDirectory: entry.agent.workingDirectory,
|
|
749
|
+
isLoading: entry.agent.isLoading,
|
|
750
|
+
messageCount: entry.agent.messages.length,
|
|
751
|
+
})),
|
|
752
|
+
};
|
|
753
|
+
}
|
|
701
754
|
// ── Auth (global) ────────────────────────────────────────────
|
|
702
755
|
async getAuthStatus() {
|
|
703
756
|
const authService = AuthService.getInstance();
|
package/dist/stdio/protocol.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export declare const INVALID_REQUEST = -32600;
|
|
|
34
34
|
export declare const METHOD_NOT_FOUND = -32601;
|
|
35
35
|
export declare const INVALID_PARAMS = -32602;
|
|
36
36
|
export declare const INTERNAL_ERROR = -32603;
|
|
37
|
-
export type RequestMethod = "initialize" | "destroy" | "restoreSession" | "listSessions" | "getSessionInfo" | "sendMessage" | "bang" | "askBtw" | "abortMessage" | "clearMessages" | "rewindToMessage" | "listRewindCheckpoints" | "deleteQueuedMessage" | "updateQueuedMessage" | "deleteQueuedMessageById" | "getMessages" | "getFullMessageThread" | "setPermissionMode" | "getPermissionMode" | "getMcpServers" | "connectMcpServer" | "disconnectMcpServer" | "getSlashCommands" | "searchFiles" | "writeArtifactFile" | "getPromptHistory" | "searchPromptHistory" | "updateConfig" | "getConfiguredModels" | "setModel" | "listPendingPermissions" | "getAuthStatus" | "login" | "logout" | "listPlugins" | "installPlugin" | "uninstallPlugin" | "enablePlugin" | "disablePlugin" | "updatePlugin" | "listMarketplaces" | "addMarketplace" | "removeMarketplace" | "updateMarketplace" | "compact" | "getBackgroundTaskOutput" | "stopBackgroundTask" | "getWorkflowRuns" | "stopWorkflowRun" | "listGitBranches" | "createWorktree" | "removeWorktree";
|
|
37
|
+
export type RequestMethod = "initialize" | "destroy" | "restoreSession" | "listSessions" | "getSessionInfo" | "sendMessage" | "bang" | "askBtw" | "abortMessage" | "clearMessages" | "rewindToMessage" | "listRewindCheckpoints" | "deleteQueuedMessage" | "updateQueuedMessage" | "deleteQueuedMessageById" | "getMessages" | "getFullMessageThread" | "setPermissionMode" | "getPermissionMode" | "getMcpServers" | "connectMcpServer" | "disconnectMcpServer" | "getSlashCommands" | "searchFiles" | "writeArtifactFile" | "getPromptHistory" | "searchPromptHistory" | "updateConfig" | "getConfiguredModels" | "setModel" | "listPendingPermissions" | "listDaemonSessions" | "getAuthStatus" | "login" | "logout" | "listPlugins" | "installPlugin" | "uninstallPlugin" | "enablePlugin" | "disablePlugin" | "updatePlugin" | "listMarketplaces" | "addMarketplace" | "removeMarketplace" | "updateMarketplace" | "compact" | "getBackgroundTaskOutput" | "stopBackgroundTask" | "getWorkflowRuns" | "stopWorkflowRun" | "listGitBranches" | "createWorktree" | "removeWorktree";
|
|
38
38
|
export type ClientNotificationMethod = "permissionResponse";
|
|
39
39
|
export type ServerNotificationMethod = "userMessageAdded" | "assistantMessageAdded" | "assistantContentUpdated" | "assistantReasoningUpdated" | "toolBlockUpdated" | "errorBlockAdded" | "loadingChange" | "commandRunningChange" | "queuedMessagesChange" | "tasksChange" | "sessionIdChange" | "permissionModeChange" | "mcpServersChange" | "workdirChange" | "bangMessageAdded" | "bangMessageUpdated" | "bangMessageCompleted" | "notificationMessageAdded" | "permissionRequest" | "authUrl" | "compactBlockAdded" | "compactionStateChange" | "backgroundTasksChange" | "btwContent";
|
|
40
40
|
export declare function isRequest(msg: unknown): msg is JsonRpcRequest;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "CLI-based code assistant powered by AI, built with React and Ink",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"wrap-ansi": "^10.0.0",
|
|
44
44
|
"yargs": "^17.7.2",
|
|
45
45
|
"zod": "^3.23.8",
|
|
46
|
-
"wave-agent-sdk": "1.0.
|
|
46
|
+
"wave-agent-sdk": "1.0.8"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/react": "^19.1.8",
|
|
@@ -142,6 +142,7 @@ export const ChatInterface: React.FC = () => {
|
|
|
142
142
|
toolName={confirmingTool!.name}
|
|
143
143
|
toolInput={confirmingTool!.input}
|
|
144
144
|
planContent={confirmingTool!.planContent}
|
|
145
|
+
warning={confirmingTool!.warning}
|
|
145
146
|
isExpanded={isExpanded}
|
|
146
147
|
/>
|
|
147
148
|
)}
|
|
@@ -151,6 +152,7 @@ export const ChatInterface: React.FC = () => {
|
|
|
151
152
|
toolName={confirmingTool!.name}
|
|
152
153
|
toolInput={confirmingTool!.input}
|
|
153
154
|
planContent={confirmingTool!.planContent}
|
|
155
|
+
warning={confirmingTool!.warning}
|
|
154
156
|
isExpanded={isExpanded}
|
|
155
157
|
/>
|
|
156
158
|
)}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
EXIT_PLAN_MODE_TOOL_NAME,
|
|
8
8
|
ENTER_PLAN_MODE_TOOL_NAME,
|
|
9
9
|
ASK_USER_QUESTION_TOOL_NAME,
|
|
10
|
+
ARTIFACT_TOOL_NAME,
|
|
10
11
|
} from "wave-agent-sdk";
|
|
11
12
|
import { DiffDisplay } from "./DiffDisplay.js";
|
|
12
13
|
import { PlanDisplay } from "./PlanDisplay.js";
|
|
@@ -34,6 +35,8 @@ const getActionDescription = (
|
|
|
34
35
|
return "Enter plan mode for complex task planning";
|
|
35
36
|
case ASK_USER_QUESTION_TOOL_NAME:
|
|
36
37
|
return "Answer questions to clarify intent";
|
|
38
|
+
case ARTIFACT_TOOL_NAME:
|
|
39
|
+
return `Publish file: ${toolInput.file_path || "unknown file"}`;
|
|
37
40
|
default:
|
|
38
41
|
return "Execute operation";
|
|
39
42
|
}
|
|
@@ -43,6 +46,7 @@ export interface ConfirmationDetailsProps {
|
|
|
43
46
|
toolName: string;
|
|
44
47
|
toolInput?: Record<string, unknown>;
|
|
45
48
|
planContent?: string;
|
|
49
|
+
warning?: string;
|
|
46
50
|
isExpanded?: boolean;
|
|
47
51
|
}
|
|
48
52
|
|
|
@@ -50,6 +54,7 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
|
|
|
50
54
|
toolName,
|
|
51
55
|
toolInput,
|
|
52
56
|
planContent,
|
|
57
|
+
warning,
|
|
53
58
|
isExpanded = false,
|
|
54
59
|
}) => {
|
|
55
60
|
const startLineNumber =
|
|
@@ -69,6 +74,7 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
|
|
|
69
74
|
Tool: {toolName}
|
|
70
75
|
</Text>
|
|
71
76
|
<Text color="yellow">{getActionDescription(toolName, toolInput)}</Text>
|
|
77
|
+
{warning && <Text color="red">⚠ {warning}</Text>}
|
|
72
78
|
|
|
73
79
|
<DiffDisplay
|
|
74
80
|
toolName={toolName}
|
|
@@ -34,10 +34,16 @@ export const RewindCommand: React.FC<RewindCommandProps> = ({
|
|
|
34
34
|
}, [getFullMessageThread]);
|
|
35
35
|
|
|
36
36
|
// Filter user messages as checkpoints, excluding meta messages and
|
|
37
|
-
// system-generated user-role messages (task notifications, hook injections)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
// system-generated user-role messages (task notifications, hook injections).
|
|
38
|
+
// Compaction is append-only: the same message id appears twice on the full
|
|
39
|
+
// thread (pre-compact history + post-compact append), so dedupe by id and
|
|
40
|
+
// keep the last occurrence (matching the folded view the user sees).
|
|
41
|
+
const checkpointMap = new Map<string, { msg: Message; index: number }>();
|
|
42
|
+
messages.forEach((msg, index) => {
|
|
43
|
+
if (!isUserCheckpointMessage(msg)) return;
|
|
44
|
+
checkpointMap.set(msg.id ?? `index:${index}`, { msg, index });
|
|
45
|
+
});
|
|
46
|
+
const checkpoints = Array.from(checkpointMap.values());
|
|
41
47
|
|
|
42
48
|
const MAX_VISIBLE_ITEMS = 3;
|
|
43
49
|
|