wave-code 1.0.6 → 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/AgentsManager.d.ts +7 -0
- package/dist/components/AgentsManager.js +109 -0
- 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/ConfirmationSelector.js +17 -3
- package/dist/components/InputBox.js +7 -21
- package/dist/components/LoginCommand.js +31 -2
- package/dist/components/MarketplaceAddForm.js +16 -2
- package/dist/components/RewindCommand.js +11 -4
- package/dist/constants/commands.js +6 -0
- package/dist/contexts/useChat.d.ts +18 -2
- package/dist/contexts/useChat.js +114 -9
- 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/hooks/useInputManager.d.ts +2 -0
- package/dist/hooks/useInputManager.js +8 -0
- package/dist/index.js +88 -0
- package/dist/managers/inputHandlers.js +3 -0
- package/dist/managers/inputReducer.d.ts +4 -0
- package/dist/managers/inputReducer.js +8 -0
- package/dist/reducers/agentsManagerReducer.d.ts +26 -0
- package/dist/reducers/agentsManagerReducer.js +54 -0
- package/dist/stdio/agentBridge.d.ts +15 -0
- package/dist/stdio/agentBridge.js +101 -20
- package/dist/stdio/protocol.d.ts +1 -1
- package/dist/utils/usageSummary.d.ts +0 -4
- package/dist/utils/usageSummary.js +1 -34
- package/package.json +2 -2
- package/src/components/AgentsManager.tsx +290 -0
- package/src/components/ChatInterface.tsx +2 -0
- package/src/components/ConfirmationDetails.tsx +6 -0
- package/src/components/ConfirmationSelector.tsx +18 -3
- package/src/components/InputBox.tsx +54 -45
- package/src/components/LoginCommand.tsx +35 -2
- package/src/components/MarketplaceAddForm.tsx +17 -2
- package/src/components/RewindCommand.tsx +10 -4
- package/src/constants/commands.ts +6 -0
- package/src/contexts/useChat.tsx +146 -7
- package/src/daemon/commands.ts +444 -0
- package/src/daemon/jsonRpcClient.ts +158 -0
- package/src/daemon/socketClient.ts +34 -0
- package/src/hooks/useInputManager.ts +8 -0
- package/src/index.ts +130 -0
- package/src/managers/inputHandlers.ts +2 -0
- package/src/managers/inputReducer.ts +10 -0
- package/src/reducers/agentsManagerReducer.ts +91 -0
- package/src/stdio/agentBridge.ts +123 -19
- package/src/stdio/protocol.ts +4 -0
- package/src/utils/usageSummary.ts +2 -46
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",
|
|
@@ -367,6 +367,8 @@ export const handleCommandSelect = (
|
|
|
367
367
|
});
|
|
368
368
|
} else if (command === "mcp") {
|
|
369
369
|
dispatch({ type: "SET_SHOW_MCP_MANAGER", payload: true });
|
|
370
|
+
} else if (command === "agents") {
|
|
371
|
+
dispatch({ type: "SET_SHOW_AGENTS_MANAGER", payload: true });
|
|
370
372
|
} else if (command === "rewind") {
|
|
371
373
|
dispatch({ type: "SET_SHOW_REWIND_MANAGER", payload: true });
|
|
372
374
|
} else if (command === "help") {
|
|
@@ -130,6 +130,7 @@ export interface InputState {
|
|
|
130
130
|
imageIdCounter: number;
|
|
131
131
|
showBackgroundTaskManager: boolean;
|
|
132
132
|
showMcpManager: boolean;
|
|
133
|
+
showAgentsManager: boolean;
|
|
133
134
|
showRewindManager: boolean;
|
|
134
135
|
showHelp: boolean;
|
|
135
136
|
showStatusCommand: boolean;
|
|
@@ -167,6 +168,7 @@ export const initialState: InputState = {
|
|
|
167
168
|
imageIdCounter: 1,
|
|
168
169
|
showBackgroundTaskManager: false,
|
|
169
170
|
showMcpManager: false,
|
|
171
|
+
showAgentsManager: false,
|
|
170
172
|
showRewindManager: false,
|
|
171
173
|
showHelp: false,
|
|
172
174
|
showStatusCommand: false,
|
|
@@ -400,6 +402,7 @@ export type InputAction =
|
|
|
400
402
|
| { type: "CLEAR_IMAGES" }
|
|
401
403
|
| { type: "SET_SHOW_BACKGROUND_TASK_MANAGER"; payload: boolean }
|
|
402
404
|
| { type: "SET_SHOW_MCP_MANAGER"; payload: boolean }
|
|
405
|
+
| { type: "SET_SHOW_AGENTS_MANAGER"; payload: boolean }
|
|
403
406
|
| { type: "SET_SHOW_REWIND_MANAGER"; payload: boolean }
|
|
404
407
|
| { type: "SET_SHOW_HELP"; payload: boolean }
|
|
405
408
|
| { type: "SET_SHOW_STATUS_COMMAND"; payload: boolean }
|
|
@@ -589,6 +592,12 @@ export function inputReducer(
|
|
|
589
592
|
showMcpManager: action.payload,
|
|
590
593
|
selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
|
|
591
594
|
};
|
|
595
|
+
case "SET_SHOW_AGENTS_MANAGER":
|
|
596
|
+
return {
|
|
597
|
+
...state,
|
|
598
|
+
showAgentsManager: action.payload,
|
|
599
|
+
selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
|
|
600
|
+
};
|
|
592
601
|
case "SET_SHOW_REWIND_MANAGER":
|
|
593
602
|
return {
|
|
594
603
|
...state,
|
|
@@ -970,6 +979,7 @@ export function inputReducer(
|
|
|
970
979
|
!(
|
|
971
980
|
state.showBackgroundTaskManager ||
|
|
972
981
|
state.showMcpManager ||
|
|
982
|
+
state.showAgentsManager ||
|
|
973
983
|
state.showRewindManager ||
|
|
974
984
|
state.showHelp ||
|
|
975
985
|
state.showStatusCommand ||
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Key } from "ink";
|
|
2
|
+
|
|
3
|
+
export type PendingEffect = { type: "CANCEL" };
|
|
4
|
+
|
|
5
|
+
export interface AgentsManagerState {
|
|
6
|
+
selectedIndex: number;
|
|
7
|
+
viewMode: "list" | "detail";
|
|
8
|
+
pendingEffect: PendingEffect | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type AgentsManagerAction =
|
|
12
|
+
| { type: "MOVE_UP" }
|
|
13
|
+
| { type: "MOVE_DOWN"; itemCount: number }
|
|
14
|
+
| { type: "SET_VIEW_MODE"; viewMode: "list" | "detail" }
|
|
15
|
+
| {
|
|
16
|
+
type: "HANDLE_KEY";
|
|
17
|
+
input: string;
|
|
18
|
+
key: Key;
|
|
19
|
+
itemCount: number;
|
|
20
|
+
}
|
|
21
|
+
| { type: "CLEAR_PENDING_EFFECT" };
|
|
22
|
+
|
|
23
|
+
export function agentsManagerReducer(
|
|
24
|
+
state: AgentsManagerState,
|
|
25
|
+
action: AgentsManagerAction,
|
|
26
|
+
): AgentsManagerState {
|
|
27
|
+
switch (action.type) {
|
|
28
|
+
case "MOVE_UP":
|
|
29
|
+
return {
|
|
30
|
+
...state,
|
|
31
|
+
selectedIndex: Math.max(0, state.selectedIndex - 1),
|
|
32
|
+
};
|
|
33
|
+
case "MOVE_DOWN":
|
|
34
|
+
return {
|
|
35
|
+
...state,
|
|
36
|
+
selectedIndex: Math.min(
|
|
37
|
+
Math.max(0, action.itemCount - 1),
|
|
38
|
+
state.selectedIndex + 1,
|
|
39
|
+
),
|
|
40
|
+
};
|
|
41
|
+
case "SET_VIEW_MODE":
|
|
42
|
+
return { ...state, viewMode: action.viewMode };
|
|
43
|
+
case "HANDLE_KEY": {
|
|
44
|
+
const { key, itemCount } = action;
|
|
45
|
+
|
|
46
|
+
if (key.return) {
|
|
47
|
+
if (state.viewMode === "list") {
|
|
48
|
+
return { ...state, viewMode: "detail" };
|
|
49
|
+
}
|
|
50
|
+
// Aligned with Claude Code AgentDetail: Enter returns to the list.
|
|
51
|
+
return { ...state, viewMode: "list" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (key.escape) {
|
|
55
|
+
if (state.viewMode === "detail") {
|
|
56
|
+
return { ...state, viewMode: "list" };
|
|
57
|
+
}
|
|
58
|
+
return { ...state, pendingEffect: { type: "CANCEL" } };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Detail view does not respond to arrow keys (aligned with CC
|
|
62
|
+
// AgentDetail, which only Esc/Enter back to the list).
|
|
63
|
+
if (state.viewMode === "detail") {
|
|
64
|
+
return state;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (key.upArrow) {
|
|
68
|
+
return {
|
|
69
|
+
...state,
|
|
70
|
+
selectedIndex: Math.max(0, state.selectedIndex - 1),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (key.downArrow) {
|
|
75
|
+
return {
|
|
76
|
+
...state,
|
|
77
|
+
selectedIndex: Math.min(
|
|
78
|
+
Math.max(0, itemCount - 1),
|
|
79
|
+
state.selectedIndex + 1,
|
|
80
|
+
),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return state;
|
|
85
|
+
}
|
|
86
|
+
case "CLEAR_PENDING_EFFECT":
|
|
87
|
+
return { ...state, pendingEffect: null };
|
|
88
|
+
default:
|
|
89
|
+
return state;
|
|
90
|
+
}
|
|
91
|
+
}
|
package/src/stdio/agentBridge.ts
CHANGED
|
@@ -42,6 +42,8 @@ import {
|
|
|
42
42
|
PluginCore,
|
|
43
43
|
validateWorktreeRemovalPath,
|
|
44
44
|
type SlashCommand,
|
|
45
|
+
loadUserConfigEnv,
|
|
46
|
+
type SubagentConfiguration,
|
|
45
47
|
} from "wave-agent-sdk";
|
|
46
48
|
import {
|
|
47
49
|
type JsonRpcError,
|
|
@@ -140,6 +142,15 @@ export class AgentBridge {
|
|
|
140
142
|
|
|
141
143
|
constructor(options: AgentBridgeOptions) {
|
|
142
144
|
this.emit = options.emit;
|
|
145
|
+
// Mirror the user-level settings env WAVE_SERVER_URL into process.env
|
|
146
|
+
// before any agent initializes. getAuthStatus (webviewReady →
|
|
147
|
+
// pushInitialState) can run before the first agent, and AuthService falls
|
|
148
|
+
// back to the default URL otherwise — refreshing a custom-domain token
|
|
149
|
+
// against the wrong host 401s into a logged-out state.
|
|
150
|
+
const userEnv = loadUserConfigEnv();
|
|
151
|
+
if (userEnv.WAVE_SERVER_URL) {
|
|
152
|
+
process.env.WAVE_SERVER_URL = userEnv.WAVE_SERVER_URL;
|
|
153
|
+
}
|
|
143
154
|
}
|
|
144
155
|
|
|
145
156
|
// ── Public API ────────────────────────────────────────────────
|
|
@@ -164,8 +175,14 @@ export class AgentBridge {
|
|
|
164
175
|
return this.getSessionInfo(sessionId);
|
|
165
176
|
case "listPendingPermissions":
|
|
166
177
|
return this.listPendingPermissions();
|
|
178
|
+
case "listDaemonSessions":
|
|
179
|
+
return this.listDaemonSessions();
|
|
167
180
|
case "updateConfig":
|
|
168
181
|
return this.updateConfig(p as unknown as UpdateConfigParams, sessionId);
|
|
182
|
+
case "getConfiguredModels":
|
|
183
|
+
return this.getConfiguredModels(sessionId);
|
|
184
|
+
case "setModel":
|
|
185
|
+
return this.setModel(p.model as string, sessionId);
|
|
169
186
|
|
|
170
187
|
// ── Messages ──
|
|
171
188
|
case "sendMessage":
|
|
@@ -222,6 +239,8 @@ export class AgentBridge {
|
|
|
222
239
|
// ── Commands ──
|
|
223
240
|
case "getSlashCommands":
|
|
224
241
|
return this.getSlashCommands(sessionId);
|
|
242
|
+
case "getSubagentConfigurations":
|
|
243
|
+
return this.getSubagentConfigurations(sessionId);
|
|
225
244
|
|
|
226
245
|
// ── File / History (global — no session required) ──
|
|
227
246
|
case "searchFiles":
|
|
@@ -727,6 +746,27 @@ export class AgentBridge {
|
|
|
727
746
|
return { sessionId: agent.sessionId };
|
|
728
747
|
}
|
|
729
748
|
|
|
749
|
+
private getConfiguredModels(sessionId?: string): {
|
|
750
|
+
models: string[];
|
|
751
|
+
currentModel: string | undefined;
|
|
752
|
+
} {
|
|
753
|
+
const entry = this.requireSession(sessionId);
|
|
754
|
+
return {
|
|
755
|
+
models: entry.agent.getConfiguredModels(),
|
|
756
|
+
currentModel: entry.agent.getModelConfig().model,
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
private async setModel(model: string, sessionId?: string): Promise<null> {
|
|
761
|
+
const entry = this.requireSession(sessionId);
|
|
762
|
+
entry.agent.setModel(model);
|
|
763
|
+
// Keep storedConfig in sync: updateConfig recreates the agent from
|
|
764
|
+
// storedConfig, so without this a later config save would revert the
|
|
765
|
+
// model chosen here.
|
|
766
|
+
entry.storedConfig = { ...entry.storedConfig, model };
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
|
|
730
770
|
// ── Messages ──────────────────────────────────────────────────
|
|
731
771
|
|
|
732
772
|
private async sendMessage(
|
|
@@ -752,10 +792,45 @@ export class AgentBridge {
|
|
|
752
792
|
} catch {
|
|
753
793
|
// Best-effort; don't block message sending on history save failure
|
|
754
794
|
}
|
|
755
|
-
await entry.agent.sendMessage(
|
|
795
|
+
await entry.agent.sendMessage(
|
|
796
|
+
params.text,
|
|
797
|
+
this.persistDataUrlImages(params.images),
|
|
798
|
+
);
|
|
756
799
|
return null;
|
|
757
800
|
}
|
|
758
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
|
+
|
|
759
834
|
private async bang(command: string, sessionId?: string): Promise<null> {
|
|
760
835
|
const entry = this.requireSession(sessionId);
|
|
761
836
|
await entry.agent.bang(command);
|
|
@@ -811,7 +886,10 @@ export class AgentBridge {
|
|
|
811
886
|
}> {
|
|
812
887
|
const entry = this.requireSession(sessionId);
|
|
813
888
|
const { messages } = await entry.agent.getFullMessageThread();
|
|
814
|
-
|
|
889
|
+
// 压缩是 append-only:同 id 消息(压缩前历史 + 压缩后 append 的重复)会
|
|
890
|
+
// 在磁盘完整线程中出现多次。用户看到的折叠视图对应最后一次出现,
|
|
891
|
+
// 因此匹配最后一个而非第一个,避免回滚时连压缩摘要一起删掉。
|
|
892
|
+
const index = messages.map((m) => m.id).lastIndexOf(messageId);
|
|
815
893
|
if (index === -1) {
|
|
816
894
|
throw new RpcError(
|
|
817
895
|
PROTOCOL_INTERNAL_ERROR,
|
|
@@ -831,13 +909,19 @@ export class AgentBridge {
|
|
|
831
909
|
}> {
|
|
832
910
|
const entry = this.requireSession(sessionId);
|
|
833
911
|
const { messages } = await entry.agent.getFullMessageThread();
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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()) };
|
|
841
925
|
}
|
|
842
926
|
|
|
843
927
|
private deleteQueuedMessage(index: number, sessionId?: string): null {
|
|
@@ -855,7 +939,7 @@ export class AgentBridge {
|
|
|
855
939
|
const entry = this.requireSession(sessionId);
|
|
856
940
|
const ok = entry.agent.updateQueuedMessageById(id, {
|
|
857
941
|
content: text,
|
|
858
|
-
images,
|
|
942
|
+
images: this.persistDataUrlImages(images),
|
|
859
943
|
});
|
|
860
944
|
return { ok };
|
|
861
945
|
}
|
|
@@ -984,6 +1068,13 @@ export class AgentBridge {
|
|
|
984
1068
|
return { commands: entry.agent.getSlashCommands() };
|
|
985
1069
|
}
|
|
986
1070
|
|
|
1071
|
+
private getSubagentConfigurations(sessionId?: string): {
|
|
1072
|
+
configurations: SubagentConfiguration[];
|
|
1073
|
+
} {
|
|
1074
|
+
const entry = this.requireSession(sessionId);
|
|
1075
|
+
return { configurations: entry.agent.getSubagentConfigurations() };
|
|
1076
|
+
}
|
|
1077
|
+
|
|
987
1078
|
// ── File / History (global) ───────────────────────────────────
|
|
988
1079
|
|
|
989
1080
|
private async searchFiles(
|
|
@@ -1099,6 +1190,26 @@ export class AgentBridge {
|
|
|
1099
1190
|
};
|
|
1100
1191
|
}
|
|
1101
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
|
+
|
|
1102
1213
|
// ── Auth (global) ────────────────────────────────────────────
|
|
1103
1214
|
|
|
1104
1215
|
private async getAuthStatus(): Promise<{
|
|
@@ -1107,13 +1218,6 @@ export class AgentBridge {
|
|
|
1107
1218
|
serverUrl: string;
|
|
1108
1219
|
}> {
|
|
1109
1220
|
const authService = AuthService.getInstance();
|
|
1110
|
-
// A stale-but-refreshable token still means "logged in" — the daemon may
|
|
1111
|
-
// have started with an expired access token (hourly expiry) and only
|
|
1112
|
-
// refreshes lazily on the first API call. Without this proactive refresh a
|
|
1113
|
-
// fresh client querying right after daemon start gets a false
|
|
1114
|
-
// isAuthenticated and e.g. the desktop welcome page keeps showing the
|
|
1115
|
-
// login button for an authenticated host. Mirrors the refresh that
|
|
1116
|
-
// createAuthAwareFetch does before every real request.
|
|
1117
1221
|
await authService.checkAndRefreshTokenIfNeeded();
|
|
1118
1222
|
return {
|
|
1119
1223
|
isAuthenticated: authService.isSSOAuthenticated(),
|
|
@@ -1424,10 +1528,10 @@ export class AgentBridge {
|
|
|
1424
1528
|
ctx.registeredSessionId,
|
|
1425
1529
|
);
|
|
1426
1530
|
},
|
|
1427
|
-
onCompleteBangMessage: (command, exitCode, messageId) => {
|
|
1531
|
+
onCompleteBangMessage: (command, exitCode, messageId, output) => {
|
|
1428
1532
|
this.emit(
|
|
1429
1533
|
"bangMessageCompleted",
|
|
1430
|
-
{ command, exitCode, messageId },
|
|
1534
|
+
{ command, exitCode, messageId, output },
|
|
1431
1535
|
ctx.registeredSessionId,
|
|
1432
1536
|
);
|
|
1433
1537
|
},
|
package/src/stdio/protocol.ts
CHANGED
|
@@ -75,8 +75,12 @@ export type RequestMethod =
|
|
|
75
75
|
| "getPromptHistory"
|
|
76
76
|
| "searchPromptHistory"
|
|
77
77
|
| "updateConfig"
|
|
78
|
+
| "getConfiguredModels"
|
|
79
|
+
| "setModel"
|
|
78
80
|
// Permissions (daemon attach: re-surface pending approvals after reconnect)
|
|
79
81
|
| "listPendingPermissions"
|
|
82
|
+
// Daemon (global — list in-memory session registry, no session required)
|
|
83
|
+
| "listDaemonSessions"
|
|
80
84
|
// Auth
|
|
81
85
|
| "getAuthStatus"
|
|
82
86
|
| "login"
|
|
@@ -12,13 +12,9 @@ export interface TokenSummary {
|
|
|
12
12
|
agent_calls: number;
|
|
13
13
|
compactions: number;
|
|
14
14
|
};
|
|
15
|
-
//
|
|
15
|
+
// Normalized cache tokens
|
|
16
16
|
cache_read_input_tokens?: number;
|
|
17
17
|
cache_creation_input_tokens?: number;
|
|
18
|
-
cache_creation?: {
|
|
19
|
-
ephemeral_5m_input_tokens: number;
|
|
20
|
-
ephemeral_1h_input_tokens: number;
|
|
21
|
-
};
|
|
22
18
|
}
|
|
23
19
|
|
|
24
20
|
/**
|
|
@@ -65,22 +61,6 @@ export function calculateTokenSummary(
|
|
|
65
61
|
(summary.cache_creation_input_tokens || 0) +
|
|
66
62
|
usage.cache_creation_input_tokens;
|
|
67
63
|
}
|
|
68
|
-
if (
|
|
69
|
-
usage.cache_creation &&
|
|
70
|
-
(usage.cache_creation.ephemeral_5m_input_tokens > 0 ||
|
|
71
|
-
usage.cache_creation.ephemeral_1h_input_tokens > 0)
|
|
72
|
-
) {
|
|
73
|
-
if (!summary.cache_creation) {
|
|
74
|
-
summary.cache_creation = {
|
|
75
|
-
ephemeral_5m_input_tokens: 0,
|
|
76
|
-
ephemeral_1h_input_tokens: 0,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
summary.cache_creation.ephemeral_5m_input_tokens +=
|
|
80
|
-
usage.cache_creation.ephemeral_5m_input_tokens || 0;
|
|
81
|
-
summary.cache_creation.ephemeral_1h_input_tokens +=
|
|
82
|
-
usage.cache_creation.ephemeral_1h_input_tokens || 0;
|
|
83
|
-
}
|
|
84
64
|
|
|
85
65
|
// Track operation types
|
|
86
66
|
if (usage.operation_type === "agent") {
|
|
@@ -132,8 +112,6 @@ export function displayUsageSummary(
|
|
|
132
112
|
let totalCompactions = 0;
|
|
133
113
|
let totalCacheRead = 0;
|
|
134
114
|
let totalCacheCreation = 0;
|
|
135
|
-
let totalCache5m = 0;
|
|
136
|
-
let totalCache1h = 0;
|
|
137
115
|
let hasCacheData = false;
|
|
138
116
|
|
|
139
117
|
for (const [, summary] of Object.entries(summaries)) {
|
|
@@ -147,8 +125,7 @@ export function displayUsageSummary(
|
|
|
147
125
|
// Display cache information if available
|
|
148
126
|
if (
|
|
149
127
|
summary.cache_read_input_tokens ||
|
|
150
|
-
summary.cache_creation_input_tokens
|
|
151
|
-
summary.cache_creation
|
|
128
|
+
summary.cache_creation_input_tokens
|
|
152
129
|
) {
|
|
153
130
|
hasCacheData = true;
|
|
154
131
|
console.log(" Cache Usage:");
|
|
@@ -172,21 +149,6 @@ export function displayUsageSummary(
|
|
|
172
149
|
);
|
|
173
150
|
totalCacheCreation += summary.cache_creation_input_tokens;
|
|
174
151
|
}
|
|
175
|
-
|
|
176
|
-
if (summary.cache_creation) {
|
|
177
|
-
if (summary.cache_creation.ephemeral_5m_input_tokens > 0) {
|
|
178
|
-
console.log(
|
|
179
|
-
` 5m cache: ${summary.cache_creation.ephemeral_5m_input_tokens.toLocaleString()} tokens`,
|
|
180
|
-
);
|
|
181
|
-
totalCache5m += summary.cache_creation.ephemeral_5m_input_tokens;
|
|
182
|
-
}
|
|
183
|
-
if (summary.cache_creation.ephemeral_1h_input_tokens > 0) {
|
|
184
|
-
console.log(
|
|
185
|
-
` 1h cache: ${summary.cache_creation.ephemeral_1h_input_tokens.toLocaleString()} tokens`,
|
|
186
|
-
);
|
|
187
|
-
totalCache1h += summary.cache_creation.ephemeral_1h_input_tokens;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
152
|
}
|
|
191
153
|
|
|
192
154
|
console.log(
|
|
@@ -219,12 +181,6 @@ export function displayUsageSummary(
|
|
|
219
181
|
` Created cache: ${totalCacheCreation.toLocaleString()} tokens`,
|
|
220
182
|
);
|
|
221
183
|
}
|
|
222
|
-
if (totalCache5m > 0) {
|
|
223
|
-
console.log(` 5m cache: ${totalCache5m.toLocaleString()} tokens`);
|
|
224
|
-
}
|
|
225
|
-
if (totalCache1h > 0) {
|
|
226
|
-
console.log(` 1h cache: ${totalCache1h.toLocaleString()} tokens`);
|
|
227
|
-
}
|
|
228
184
|
}
|
|
229
185
|
|
|
230
186
|
console.log(
|