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,158 @@
|
|
|
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
|
+
|
|
16
|
+
import { createInterface } from "readline";
|
|
17
|
+
import type { Readable } from "stream";
|
|
18
|
+
|
|
19
|
+
export type NotificationHandler = (params: unknown, sessionId?: string) => void;
|
|
20
|
+
|
|
21
|
+
interface PendingRequest {
|
|
22
|
+
resolve: (value: unknown) => void;
|
|
23
|
+
reject: (error: Error) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export abstract class JsonRpcClient {
|
|
27
|
+
private nextId = 1;
|
|
28
|
+
private pending = new Map<number, PendingRequest>();
|
|
29
|
+
private handlers = new Map<string, Set<NotificationHandler>>();
|
|
30
|
+
private closedHandlers: Array<() => void> = [];
|
|
31
|
+
private closed = false;
|
|
32
|
+
|
|
33
|
+
// ── Transport hooks (subclass) ─────────────────────────────────
|
|
34
|
+
|
|
35
|
+
protected abstract writeLine(message: string): void;
|
|
36
|
+
|
|
37
|
+
/** Wire an inbound Readable (socket) to the line parser. */
|
|
38
|
+
protected attachReadable(readable: Readable): void {
|
|
39
|
+
const rl = createInterface({ input: readable });
|
|
40
|
+
rl.on("line", (line) => this.handleLine(line));
|
|
41
|
+
// readline re-emits input errors on the Interface; the transport subclass
|
|
42
|
+
// already handles errors on the underlying stream, swallow them here so
|
|
43
|
+
// they never surface as an uncaught 'error' on the Interface.
|
|
44
|
+
rl.on("error", () => {});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Mark the transport closed: reject every pending request. Idempotent. */
|
|
48
|
+
protected handleClosed(reason: string): void {
|
|
49
|
+
if (this.closed) return;
|
|
50
|
+
this.closed = true;
|
|
51
|
+
const error = new Error(reason);
|
|
52
|
+
for (const p of this.pending.values()) p.reject(error);
|
|
53
|
+
this.pending.clear();
|
|
54
|
+
for (const handler of this.closedHandlers) handler();
|
|
55
|
+
this.closedHandlers = [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
protected get isClosed(): boolean {
|
|
59
|
+
return this.closed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Public API ─────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/** Close the transport and reject pending requests (subclass tears down its stream). */
|
|
65
|
+
abstract dispose(): void;
|
|
66
|
+
|
|
67
|
+
/** Observe transport teardown (dispose or unexpected close), fired once. */
|
|
68
|
+
onClosed(handler: () => void): void {
|
|
69
|
+
this.closedHandlers.push(handler);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async request(
|
|
73
|
+
method: string,
|
|
74
|
+
params?: unknown,
|
|
75
|
+
sessionId?: string,
|
|
76
|
+
): Promise<unknown> {
|
|
77
|
+
if (this.closed) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
"连接已断开。wave 进程已退出,请重启编辑器或检查 CLI 安装。",
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
const id = this.nextId++;
|
|
83
|
+
const envelope: Record<string, unknown> = { id, method, params };
|
|
84
|
+
if (sessionId) envelope.sessionId = sessionId;
|
|
85
|
+
const message = JSON.stringify(envelope) + "\n";
|
|
86
|
+
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
this.pending.set(id, { resolve, reject });
|
|
89
|
+
this.writeLine(message);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
notify(method: string, params?: unknown, sessionId?: string): void {
|
|
94
|
+
if (this.closed) return;
|
|
95
|
+
const envelope: Record<string, unknown> = { method, params };
|
|
96
|
+
if (sessionId) envelope.sessionId = sessionId;
|
|
97
|
+
this.writeLine(JSON.stringify(envelope) + "\n");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
onNotification(method: string, handler: NotificationHandler): void {
|
|
101
|
+
let set = this.handlers.get(method);
|
|
102
|
+
if (!set) {
|
|
103
|
+
set = new Set();
|
|
104
|
+
this.handlers.set(method, set);
|
|
105
|
+
}
|
|
106
|
+
set.add(handler);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
offNotification(method: string, handler: NotificationHandler): void {
|
|
110
|
+
this.handlers.get(method)?.delete(handler);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Internal ──────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
private handleLine(line: string): void {
|
|
116
|
+
let msg: unknown;
|
|
117
|
+
try {
|
|
118
|
+
msg = JSON.parse(line);
|
|
119
|
+
} catch {
|
|
120
|
+
console.error("[wave-jsonrpc] Failed to parse:", line);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (typeof msg !== "object" || msg === null) return;
|
|
125
|
+
const obj = msg as Record<string, unknown>;
|
|
126
|
+
|
|
127
|
+
// Response (has id + result/error)
|
|
128
|
+
if ("id" in obj && ("result" in obj || "error" in obj)) {
|
|
129
|
+
const id = Number(obj.id);
|
|
130
|
+
const pending = this.pending.get(id);
|
|
131
|
+
if (pending) {
|
|
132
|
+
this.pending.delete(id);
|
|
133
|
+
if (obj.error) {
|
|
134
|
+
const err = obj.error as { code: number; message: string };
|
|
135
|
+
pending.reject(new Error(err.message));
|
|
136
|
+
} else {
|
|
137
|
+
pending.resolve(obj.result);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Notification (has method, no id)
|
|
144
|
+
if ("method" in obj && !("id" in obj)) {
|
|
145
|
+
const method = obj.method as string;
|
|
146
|
+
const params = obj.params;
|
|
147
|
+
const sessionId =
|
|
148
|
+
typeof obj.sessionId === "string" ? obj.sessionId : undefined;
|
|
149
|
+
const set = this.handlers.get(method);
|
|
150
|
+
if (set) {
|
|
151
|
+
for (const handler of set) {
|
|
152
|
+
handler(params, sessionId);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
|
|
7
|
+
import type { Socket } from "net";
|
|
8
|
+
import { JsonRpcClient } from "./jsonRpcClient.js";
|
|
9
|
+
|
|
10
|
+
export class SocketClient extends JsonRpcClient {
|
|
11
|
+
private socket: Socket;
|
|
12
|
+
|
|
13
|
+
constructor(socket: Socket) {
|
|
14
|
+
super();
|
|
15
|
+
this.socket = socket;
|
|
16
|
+
this.attachReadable(socket);
|
|
17
|
+
|
|
18
|
+
socket.on("close", () => {
|
|
19
|
+
this.handleClosed("远端连接已断开。");
|
|
20
|
+
});
|
|
21
|
+
socket.on("error", (err) => {
|
|
22
|
+
console.error("[wave-daemon] Socket error:", err.message);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
protected writeLine(message: string): void {
|
|
27
|
+
this.socket.write(message);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
dispose(): void {
|
|
31
|
+
this.handleClosed("远端连接已断开。");
|
|
32
|
+
this.socket.destroy();
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -252,6 +252,136 @@ export async function main() {
|
|
|
252
252
|
},
|
|
253
253
|
);
|
|
254
254
|
})
|
|
255
|
+
.command(
|
|
256
|
+
"daemon",
|
|
257
|
+
"Manage the wave daemon (client subcommands — to START a daemon use `wave --daemon <socket>` instead)",
|
|
258
|
+
(yargs) => {
|
|
259
|
+
return yargs
|
|
260
|
+
.help()
|
|
261
|
+
.command(
|
|
262
|
+
"list",
|
|
263
|
+
"List sessions hosted by the daemon (in-memory registry)",
|
|
264
|
+
{},
|
|
265
|
+
async () => {
|
|
266
|
+
const { daemonListCommand, DEFAULT_DAEMON_SOCKET } =
|
|
267
|
+
await import("./daemon/commands.js");
|
|
268
|
+
await daemonListCommand(DEFAULT_DAEMON_SOCKET);
|
|
269
|
+
},
|
|
270
|
+
)
|
|
271
|
+
.command(
|
|
272
|
+
"status <sessionId>",
|
|
273
|
+
"Show a session's progress and recent messages",
|
|
274
|
+
(yargs) => {
|
|
275
|
+
return yargs
|
|
276
|
+
.positional("sessionId", {
|
|
277
|
+
describe: "Session ID hosted by the daemon",
|
|
278
|
+
type: "string",
|
|
279
|
+
})
|
|
280
|
+
.option("lines", {
|
|
281
|
+
describe: "Number of recent messages to show",
|
|
282
|
+
default: 20,
|
|
283
|
+
type: "number",
|
|
284
|
+
});
|
|
285
|
+
},
|
|
286
|
+
async (argv) => {
|
|
287
|
+
const { daemonStatusCommand, DEFAULT_DAEMON_SOCKET } =
|
|
288
|
+
await import("./daemon/commands.js");
|
|
289
|
+
await daemonStatusCommand(
|
|
290
|
+
DEFAULT_DAEMON_SOCKET,
|
|
291
|
+
argv.sessionId as string,
|
|
292
|
+
argv.lines as number,
|
|
293
|
+
);
|
|
294
|
+
},
|
|
295
|
+
)
|
|
296
|
+
.command(
|
|
297
|
+
"send <sessionId> <message>",
|
|
298
|
+
"Inject a message into a session and wait for the reply",
|
|
299
|
+
(yargs) => {
|
|
300
|
+
return yargs
|
|
301
|
+
.positional("sessionId", {
|
|
302
|
+
describe: "Session ID hosted by the daemon",
|
|
303
|
+
type: "string",
|
|
304
|
+
})
|
|
305
|
+
.positional("message", {
|
|
306
|
+
describe: "Message to send",
|
|
307
|
+
type: "string",
|
|
308
|
+
})
|
|
309
|
+
.option("timeout", {
|
|
310
|
+
describe: "Seconds to wait for the reply (0 = no limit)",
|
|
311
|
+
default: 600,
|
|
312
|
+
type: "number",
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
async (argv) => {
|
|
316
|
+
const { daemonSendCommand, DEFAULT_DAEMON_SOCKET } =
|
|
317
|
+
await import("./daemon/commands.js");
|
|
318
|
+
await daemonSendCommand(
|
|
319
|
+
DEFAULT_DAEMON_SOCKET,
|
|
320
|
+
argv.sessionId as string,
|
|
321
|
+
argv.message as string,
|
|
322
|
+
{ timeout: argv.timeout as number },
|
|
323
|
+
);
|
|
324
|
+
},
|
|
325
|
+
)
|
|
326
|
+
.command(
|
|
327
|
+
"respond <sessionId> <requestId>",
|
|
328
|
+
"Respond to a pending permission request",
|
|
329
|
+
(yargs) => {
|
|
330
|
+
return yargs
|
|
331
|
+
.positional("sessionId", {
|
|
332
|
+
describe: "Session ID hosting the pending request",
|
|
333
|
+
type: "string",
|
|
334
|
+
})
|
|
335
|
+
.positional("requestId", {
|
|
336
|
+
describe: "Pending permission request ID",
|
|
337
|
+
type: "string",
|
|
338
|
+
})
|
|
339
|
+
.option("allow", {
|
|
340
|
+
describe: "Allow the operation",
|
|
341
|
+
type: "boolean",
|
|
342
|
+
})
|
|
343
|
+
.option("deny", {
|
|
344
|
+
describe: "Deny the operation",
|
|
345
|
+
type: "boolean",
|
|
346
|
+
})
|
|
347
|
+
.option("reason", {
|
|
348
|
+
describe: "Reason for the decision (deny)",
|
|
349
|
+
type: "string",
|
|
350
|
+
})
|
|
351
|
+
.option("answer", {
|
|
352
|
+
describe: "Answers JSON for AskUserQuestion requests",
|
|
353
|
+
type: "string",
|
|
354
|
+
})
|
|
355
|
+
.option("rule", {
|
|
356
|
+
describe: "Persist an allowed rule (e.g. Bash(ls))",
|
|
357
|
+
type: "string",
|
|
358
|
+
})
|
|
359
|
+
.option("mode", {
|
|
360
|
+
describe: "Switch the session's permission mode",
|
|
361
|
+
type: "string",
|
|
362
|
+
});
|
|
363
|
+
},
|
|
364
|
+
async (argv) => {
|
|
365
|
+
const { daemonRespondCommand, DEFAULT_DAEMON_SOCKET } =
|
|
366
|
+
await import("./daemon/commands.js");
|
|
367
|
+
await daemonRespondCommand(
|
|
368
|
+
DEFAULT_DAEMON_SOCKET,
|
|
369
|
+
argv.sessionId as string,
|
|
370
|
+
argv.requestId as string,
|
|
371
|
+
{
|
|
372
|
+
allow: argv.allow as boolean | undefined,
|
|
373
|
+
deny: argv.deny as boolean | undefined,
|
|
374
|
+
reason: argv.reason as string | undefined,
|
|
375
|
+
answer: argv.answer as string | undefined,
|
|
376
|
+
rule: argv.rule as string | undefined,
|
|
377
|
+
mode: argv.mode as string | undefined,
|
|
378
|
+
},
|
|
379
|
+
);
|
|
380
|
+
},
|
|
381
|
+
)
|
|
382
|
+
.demandCommand(1, "Please specify a daemon subcommand");
|
|
383
|
+
},
|
|
384
|
+
)
|
|
255
385
|
.command(
|
|
256
386
|
"update",
|
|
257
387
|
"Update WAVE Code to the latest version",
|
package/src/stdio/agentBridge.ts
CHANGED
|
@@ -175,6 +175,8 @@ export class AgentBridge {
|
|
|
175
175
|
return this.getSessionInfo(sessionId);
|
|
176
176
|
case "listPendingPermissions":
|
|
177
177
|
return this.listPendingPermissions();
|
|
178
|
+
case "listDaemonSessions":
|
|
179
|
+
return this.listDaemonSessions();
|
|
178
180
|
case "updateConfig":
|
|
179
181
|
return this.updateConfig(p as unknown as UpdateConfigParams, sessionId);
|
|
180
182
|
case "getConfiguredModels":
|
|
@@ -790,10 +792,45 @@ export class AgentBridge {
|
|
|
790
792
|
} catch {
|
|
791
793
|
// Best-effort; don't block message sending on history save failure
|
|
792
794
|
}
|
|
793
|
-
await entry.agent.sendMessage(
|
|
795
|
+
await entry.agent.sendMessage(
|
|
796
|
+
params.text,
|
|
797
|
+
this.persistDataUrlImages(params.images),
|
|
798
|
+
);
|
|
794
799
|
return null;
|
|
795
800
|
}
|
|
796
801
|
|
|
802
|
+
/**
|
|
803
|
+
* Webview hosts (desktop/vscode/jetbrains) send pasted images as inline
|
|
804
|
+
* data URLs — there is no local file behind them. Persist each to a temp
|
|
805
|
+
* file so the model gets a real path it can reference with tools (aligned
|
|
806
|
+
* with Claude Code's `[Image source: <path>]` metadata). Real paths pass
|
|
807
|
+
* through untouched; unparseable data URLs pass through as-is and are
|
|
808
|
+
* skipped by the SDK rather than blocking the message.
|
|
809
|
+
*/
|
|
810
|
+
private persistDataUrlImages(
|
|
811
|
+
images?: Array<{ path: string; mimeType: string }>,
|
|
812
|
+
): Array<{ path: string; mimeType: string }> | undefined {
|
|
813
|
+
if (!images || images.length === 0) return images;
|
|
814
|
+
return images.map((img) => {
|
|
815
|
+
if (!img.path.startsWith("data:")) return img;
|
|
816
|
+
const match = /^data:([^;,]+);base64,(.*)$/s.exec(img.path);
|
|
817
|
+
if (!match) return img;
|
|
818
|
+
try {
|
|
819
|
+
const mimeType = match[1];
|
|
820
|
+
const ext = mimeType.split("/")[1]?.replace("jpeg", "jpg") || "png";
|
|
821
|
+
const filePath = join(
|
|
822
|
+
tmpdir(),
|
|
823
|
+
`wave-image-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.${ext}`,
|
|
824
|
+
);
|
|
825
|
+
writeFileSync(filePath, Buffer.from(match[2], "base64"));
|
|
826
|
+
return { path: filePath, mimeType };
|
|
827
|
+
} catch (error) {
|
|
828
|
+
logger.warn("Failed to persist pasted image to temp file:", error);
|
|
829
|
+
return img;
|
|
830
|
+
}
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
797
834
|
private async bang(command: string, sessionId?: string): Promise<null> {
|
|
798
835
|
const entry = this.requireSession(sessionId);
|
|
799
836
|
await entry.agent.bang(command);
|
|
@@ -849,7 +886,10 @@ export class AgentBridge {
|
|
|
849
886
|
}> {
|
|
850
887
|
const entry = this.requireSession(sessionId);
|
|
851
888
|
const { messages } = await entry.agent.getFullMessageThread();
|
|
852
|
-
|
|
889
|
+
// 压缩是 append-only:同 id 消息(压缩前历史 + 压缩后 append 的重复)会
|
|
890
|
+
// 在磁盘完整线程中出现多次。用户看到的折叠视图对应最后一次出现,
|
|
891
|
+
// 因此匹配最后一个而非第一个,避免回滚时连压缩摘要一起删掉。
|
|
892
|
+
const index = messages.map((m) => m.id).lastIndexOf(messageId);
|
|
853
893
|
if (index === -1) {
|
|
854
894
|
throw new RpcError(
|
|
855
895
|
PROTOCOL_INTERNAL_ERROR,
|
|
@@ -869,13 +909,19 @@ export class AgentBridge {
|
|
|
869
909
|
}> {
|
|
870
910
|
const entry = this.requireSession(sessionId);
|
|
871
911
|
const { messages } = await entry.agent.getFullMessageThread();
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
912
|
+
// 压缩 append-only 后同 id 消息在磁盘完整线程中重复出现(压缩前历史 +
|
|
913
|
+
// 压缩后 append 的重复)。按 id 去重并保留最后一次出现(与折叠后的
|
|
914
|
+
// UI/内存视图一致),避免弹窗把同一条用户消息显示两遍。
|
|
915
|
+
const checkpointMap = new Map<string, { id: string; content: string }>();
|
|
916
|
+
for (const m of messages) {
|
|
917
|
+
if (isUserCheckpointMessage(m) && m.id) {
|
|
918
|
+
checkpointMap.set(m.id, {
|
|
919
|
+
id: m.id,
|
|
920
|
+
content: getMessageContent(m).replace(/\s+/g, " ").trim(),
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return { checkpoints: Array.from(checkpointMap.values()) };
|
|
879
925
|
}
|
|
880
926
|
|
|
881
927
|
private deleteQueuedMessage(index: number, sessionId?: string): null {
|
|
@@ -893,7 +939,7 @@ export class AgentBridge {
|
|
|
893
939
|
const entry = this.requireSession(sessionId);
|
|
894
940
|
const ok = entry.agent.updateQueuedMessageById(id, {
|
|
895
941
|
content: text,
|
|
896
|
-
images,
|
|
942
|
+
images: this.persistDataUrlImages(images),
|
|
897
943
|
});
|
|
898
944
|
return { ok };
|
|
899
945
|
}
|
|
@@ -1144,6 +1190,26 @@ export class AgentBridge {
|
|
|
1144
1190
|
};
|
|
1145
1191
|
}
|
|
1146
1192
|
|
|
1193
|
+
/** Daemon list: expose the in-memory session registry (live sessions only,
|
|
1194
|
+
* not disk-scanning). Registration order is preserved. */
|
|
1195
|
+
private listDaemonSessions(): {
|
|
1196
|
+
sessions: Array<{
|
|
1197
|
+
sessionId: string;
|
|
1198
|
+
workingDirectory: string;
|
|
1199
|
+
isLoading: boolean;
|
|
1200
|
+
messageCount: number;
|
|
1201
|
+
}>;
|
|
1202
|
+
} {
|
|
1203
|
+
return {
|
|
1204
|
+
sessions: [...this.sessions.entries()].map(([sessionId, entry]) => ({
|
|
1205
|
+
sessionId,
|
|
1206
|
+
workingDirectory: entry.agent.workingDirectory,
|
|
1207
|
+
isLoading: entry.agent.isLoading,
|
|
1208
|
+
messageCount: entry.agent.messages.length,
|
|
1209
|
+
})),
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1147
1213
|
// ── Auth (global) ────────────────────────────────────────────
|
|
1148
1214
|
|
|
1149
1215
|
private async getAuthStatus(): Promise<{
|
package/src/stdio/protocol.ts
CHANGED
|
@@ -79,6 +79,8 @@ export type RequestMethod =
|
|
|
79
79
|
| "setModel"
|
|
80
80
|
// Permissions (daemon attach: re-surface pending approvals after reconnect)
|
|
81
81
|
| "listPendingPermissions"
|
|
82
|
+
// Daemon (global — list in-memory session registry, no session required)
|
|
83
|
+
| "listDaemonSessions"
|
|
82
84
|
// Auth
|
|
83
85
|
| "getAuthStatus"
|
|
84
86
|
| "login"
|