wave-code 1.0.9 → 1.1.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/bin/wave-code.js +8 -2
- package/dist/bundle/wave.mjs +3207 -0
- package/package.json +24 -25
- package/src/commands/update.ts +2 -7
- package/src/components/App.tsx +3 -7
- package/src/components/BtwDisplay.tsx +10 -1
- package/src/components/ChatInterface.tsx +21 -52
- package/src/components/ConfirmationDetails.tsx +144 -37
- package/src/components/DiffDisplay.tsx +335 -301
- package/src/components/InputBox.tsx +3 -1
- package/src/components/LoadingIndicator.tsx +16 -2
- package/src/components/MessageBlockItem.tsx +2 -4
- package/src/components/MessageList.tsx +75 -14
- package/src/components/ReasoningDisplay.tsx +2 -4
- package/src/components/SessionSelector.tsx +82 -11
- package/src/components/StatusCommand.tsx +2 -9
- package/src/components/ToolDisplay.tsx +2 -4
- package/src/contexts/useChat.tsx +172 -350
- package/src/daemon/commands.ts +57 -27
- package/src/daemon/jsonRpcClient.ts +1 -1
- package/src/daemon/socketClient.ts +2 -2
- package/src/hooks/useInputManager.ts +1 -1
- package/src/index.ts +57 -10
- package/src/managers/inputReducer.ts +186 -104
- package/src/print-cli.ts +21 -17
- package/src/reducers/selectorReducer.ts +10 -0
- package/src/session-selector-cli.tsx +269 -28
- package/src/stdio/agentBridge.ts +22 -21
- package/src/stdio/daemonServer.ts +3 -1
- package/src/stdio/jsonRpcConnection.ts +19 -1
- package/src/stdio/stdioServer.ts +17 -0
- package/src/utils/clipboard.ts +21 -0
- package/src/utils/highlightUtils.ts +46 -1
- package/src/utils/readPackageJson.ts +29 -0
- package/src/utils/streamingText.ts +11 -0
- package/src/utils/worktree.ts +191 -43
- package/dist/cli.d.ts +0 -6
- package/dist/cli.js +0 -53
- package/dist/commands/plugin/disable.d.ts +0 -5
- package/dist/commands/plugin/disable.js +0 -15
- package/dist/commands/plugin/enable.d.ts +0 -5
- package/dist/commands/plugin/enable.js +0 -15
- package/dist/commands/plugin/install.d.ts +0 -5
- package/dist/commands/plugin/install.js +0 -21
- package/dist/commands/plugin/list.d.ts +0 -1
- package/dist/commands/plugin/list.js +0 -31
- package/dist/commands/plugin/marketplace.d.ts +0 -10
- package/dist/commands/plugin/marketplace.js +0 -90
- package/dist/commands/plugin/uninstall.d.ts +0 -3
- package/dist/commands/plugin/uninstall.js +0 -16
- package/dist/commands/plugin/update.d.ts +0 -3
- package/dist/commands/plugin/update.js +0 -15
- package/dist/commands/update.d.ts +0 -1
- package/dist/commands/update.js +0 -97
- package/dist/components/AgentsManager.d.ts +0 -7
- package/dist/components/AgentsManager.js +0 -109
- package/dist/components/App.d.ts +0 -9
- package/dist/components/App.js +0 -76
- package/dist/components/BackgroundTaskManager.d.ts +0 -5
- package/dist/components/BackgroundTaskManager.js +0 -99
- package/dist/components/BangDisplay.d.ts +0 -8
- package/dist/components/BangDisplay.js +0 -17
- package/dist/components/BtwDisplay.d.ts +0 -7
- package/dist/components/BtwDisplay.js +0 -19
- package/dist/components/ChatInterface.d.ts +0 -2
- package/dist/components/ChatInterface.js +0 -61
- package/dist/components/CommandSelector.d.ts +0 -10
- package/dist/components/CommandSelector.js +0 -66
- package/dist/components/CompactDisplay.d.ts +0 -8
- package/dist/components/CompactDisplay.js +0 -13
- package/dist/components/ConfirmationDetails.d.ts +0 -9
- package/dist/components/ConfirmationDetails.js +0 -45
- package/dist/components/ConfirmationSelector.d.ts +0 -13
- package/dist/components/ConfirmationSelector.js +0 -127
- package/dist/components/DiffDisplay.d.ts +0 -8
- package/dist/components/DiffDisplay.js +0 -201
- package/dist/components/DiscoverView.d.ts +0 -2
- package/dist/components/DiscoverView.js +0 -37
- package/dist/components/FileSelector.d.ts +0 -11
- package/dist/components/FileSelector.js +0 -50
- package/dist/components/HelpView.d.ts +0 -7
- package/dist/components/HelpView.js +0 -74
- package/dist/components/HistorySearch.d.ts +0 -8
- package/dist/components/HistorySearch.js +0 -66
- package/dist/components/InputBox.d.ts +0 -24
- package/dist/components/InputBox.js +0 -149
- package/dist/components/InstalledView.d.ts +0 -2
- package/dist/components/InstalledView.js +0 -45
- package/dist/components/LoadingIndicator.d.ts +0 -10
- package/dist/components/LoadingIndicator.js +0 -6
- package/dist/components/LoginCommand.d.ts +0 -5
- package/dist/components/LoginCommand.js +0 -153
- package/dist/components/Markdown.d.ts +0 -5
- package/dist/components/Markdown.js +0 -127
- package/dist/components/MarketplaceAddForm.d.ts +0 -2
- package/dist/components/MarketplaceAddForm.js +0 -55
- package/dist/components/MarketplaceDetail.d.ts +0 -2
- package/dist/components/MarketplaceDetail.js +0 -74
- package/dist/components/MarketplaceList.d.ts +0 -8
- package/dist/components/MarketplaceList.js +0 -17
- package/dist/components/MarketplaceView.d.ts +0 -2
- package/dist/components/MarketplaceView.js +0 -41
- package/dist/components/McpManager.d.ts +0 -9
- package/dist/components/McpManager.js +0 -88
- package/dist/components/MessageBlockItem.d.ts +0 -8
- package/dist/components/MessageBlockItem.js +0 -15
- package/dist/components/MessageList.d.ts +0 -10
- package/dist/components/MessageList.js +0 -80
- package/dist/components/ModelSelector.d.ts +0 -8
- package/dist/components/ModelSelector.js +0 -45
- package/dist/components/Notifications.d.ts +0 -7
- package/dist/components/Notifications.js +0 -9
- package/dist/components/PlanDisplay.d.ts +0 -7
- package/dist/components/PlanDisplay.js +0 -6
- package/dist/components/PluginDetail.d.ts +0 -2
- package/dist/components/PluginDetail.js +0 -87
- package/dist/components/PluginList.d.ts +0 -13
- package/dist/components/PluginList.js +0 -15
- package/dist/components/PluginManagerShell.d.ts +0 -6
- package/dist/components/PluginManagerShell.js +0 -92
- package/dist/components/PluginManagerTypes.d.ts +0 -35
- package/dist/components/PluginManagerTypes.js +0 -1
- package/dist/components/QueuedMessageList.d.ts +0 -2
- package/dist/components/QueuedMessageList.js +0 -18
- package/dist/components/ReasoningDisplay.d.ts +0 -8
- package/dist/components/ReasoningDisplay.js +0 -13
- package/dist/components/RewindCommand.d.ts +0 -12
- package/dist/components/RewindCommand.js +0 -78
- package/dist/components/SessionSelector.d.ts +0 -12
- package/dist/components/SessionSelector.js +0 -47
- package/dist/components/StatusCommand.d.ts +0 -5
- package/dist/components/StatusCommand.js +0 -28
- package/dist/components/StatusLine.d.ts +0 -6
- package/dist/components/StatusLine.js +0 -9
- package/dist/components/TaskList.d.ts +0 -5
- package/dist/components/TaskList.js +0 -197
- package/dist/components/TaskNotificationMessage.d.ts +0 -5
- package/dist/components/TaskNotificationMessage.js +0 -12
- package/dist/components/ToolDisplay.d.ts +0 -9
- package/dist/components/ToolDisplay.js +0 -55
- package/dist/components/WorkflowManager.d.ts +0 -5
- package/dist/components/WorkflowManager.js +0 -102
- package/dist/components/WorktreeExitPrompt.d.ts +0 -12
- package/dist/components/WorktreeExitPrompt.js +0 -35
- package/dist/constants/commands.d.ts +0 -2
- package/dist/constants/commands.js +0 -92
- package/dist/contexts/PluginManagerContext.d.ts +0 -3
- package/dist/contexts/PluginManagerContext.js +0 -9
- package/dist/contexts/useAppConfig.d.ts +0 -10
- package/dist/contexts/useAppConfig.js +0 -13
- package/dist/contexts/useChat.d.ts +0 -101
- package/dist/contexts/useChat.js +0 -992
- package/dist/contracts/status.d.ts +0 -7
- package/dist/contracts/status.js +0 -1
- package/dist/daemon/commands.d.ts +0 -49
- package/dist/daemon/commands.js +0 -341
- package/dist/daemon/jsonRpcClient.d.ts +0 -38
- package/dist/daemon/jsonRpcClient.js +0 -129
- package/dist/daemon/socketClient.d.ts +0 -13
- package/dist/daemon/socketClient.js +0 -26
- package/dist/daemon-cli.d.ts +0 -10
- package/dist/daemon-cli.js +0 -15
- package/dist/hooks/useInputManager.d.ts +0 -75
- package/dist/hooks/useInputManager.js +0 -583
- package/dist/hooks/usePluginManager.d.ts +0 -4
- package/dist/hooks/usePluginManager.js +0 -265
- package/dist/hooks/useTasks.d.ts +0 -1
- package/dist/hooks/useTasks.js +0 -5
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -447
- package/dist/managers/inputHandlers.d.ts +0 -30
- package/dist/managers/inputHandlers.js +0 -625
- package/dist/managers/inputReducer.d.ts +0 -271
- package/dist/managers/inputReducer.js +0 -1121
- package/dist/plugin-manager-cli.d.ts +0 -5
- package/dist/plugin-manager-cli.js +0 -12
- package/dist/print-cli.d.ts +0 -10
- package/dist/print-cli.js +0 -209
- package/dist/reducers/agentsManagerReducer.d.ts +0 -26
- package/dist/reducers/agentsManagerReducer.js +0 -54
- package/dist/reducers/backgroundTaskManagerReducer.d.ts +0 -62
- package/dist/reducers/backgroundTaskManagerReducer.js +0 -106
- package/dist/reducers/confirmationReducer.d.ts +0 -37
- package/dist/reducers/confirmationReducer.js +0 -193
- package/dist/reducers/helpSelectorReducer.d.ts +0 -25
- package/dist/reducers/helpSelectorReducer.js +0 -52
- package/dist/reducers/marketplaceAddFormReducer.d.ts +0 -24
- package/dist/reducers/marketplaceAddFormReducer.js +0 -58
- package/dist/reducers/mcpManagerReducer.d.ts +0 -37
- package/dist/reducers/mcpManagerReducer.js +0 -74
- package/dist/reducers/pluginDetailReducer.d.ts +0 -32
- package/dist/reducers/pluginDetailReducer.js +0 -70
- package/dist/reducers/questionReducer.d.ts +0 -82
- package/dist/reducers/questionReducer.js +0 -375
- package/dist/reducers/rewindSelectorReducer.d.ts +0 -21
- package/dist/reducers/rewindSelectorReducer.js +0 -37
- package/dist/reducers/selectorReducer.d.ts +0 -23
- package/dist/reducers/selectorReducer.js +0 -42
- package/dist/reducers/workflowManagerReducer.d.ts +0 -40
- package/dist/reducers/workflowManagerReducer.js +0 -100
- package/dist/session-selector-cli.d.ts +0 -3
- package/dist/session-selector-cli.js +0 -25
- package/dist/stdio/agentBridge.d.ts +0 -139
- package/dist/stdio/agentBridge.js +0 -999
- package/dist/stdio/daemonServer.d.ts +0 -67
- package/dist/stdio/daemonServer.js +0 -191
- package/dist/stdio/index.d.ts +0 -5
- package/dist/stdio/index.js +0 -5
- package/dist/stdio/jsonRpcConnection.d.ts +0 -30
- package/dist/stdio/jsonRpcConnection.js +0 -127
- package/dist/stdio/protocol.d.ts +0 -41
- package/dist/stdio/protocol.js +0 -26
- package/dist/stdio/stdioServer.d.ts +0 -28
- package/dist/stdio/stdioServer.js +0 -36
- package/dist/stdio-cli.d.ts +0 -8
- package/dist/stdio-cli.js +0 -15
- package/dist/types.d.ts +0 -20
- package/dist/types.js +0 -1
- package/dist/utils/bracketedPaste.d.ts +0 -39
- package/dist/utils/bracketedPaste.js +0 -122
- package/dist/utils/clipboard.d.ts +0 -21
- package/dist/utils/clipboard.js +0 -347
- package/dist/utils/constants.d.ts +0 -16
- package/dist/utils/constants.js +0 -18
- package/dist/utils/highlightUtils.d.ts +0 -1
- package/dist/utils/highlightUtils.js +0 -93
- package/dist/utils/inputUtils.d.ts +0 -17
- package/dist/utils/inputUtils.js +0 -67
- package/dist/utils/logger.d.ts +0 -71
- package/dist/utils/logger.js +0 -259
- package/dist/utils/markdownTable.d.ts +0 -34
- package/dist/utils/markdownTable.js +0 -302
- package/dist/utils/rewindCheckpoints.d.ts +0 -8
- package/dist/utils/rewindCheckpoints.js +0 -17
- package/dist/utils/throttle.d.ts +0 -14
- package/dist/utils/throttle.js +0 -55
- package/dist/utils/toolParameterTransforms.d.ts +0 -18
- package/dist/utils/toolParameterTransforms.js +0 -73
- package/dist/utils/usageSummary.d.ts +0 -28
- package/dist/utils/usageSummary.js +0 -121
- package/dist/utils/version.d.ts +0 -1
- package/dist/utils/version.js +0 -4
- package/dist/utils/worktree.d.ts +0 -35
- package/dist/utils/worktree.js +0 -219
|
@@ -1,999 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AgentBridge — wraps the SDK Agent and translates between the JSON-RPC-like
|
|
3
|
-
* stdio protocol and Agent method calls / callbacks.
|
|
4
|
-
*
|
|
5
|
-
* Multi-tenant: maintains a Map<sessionId, {agent, storedConfig}> so a single
|
|
6
|
-
* `wave --stdio` process can host multiple sessions. Session-scoped requests
|
|
7
|
-
* carry `sessionId` on the JSON-RPC envelope for routing; global requests
|
|
8
|
-
* (listSessions/searchFiles/auth/plugins) don't require it but may use it
|
|
9
|
-
* for workdir fallback.
|
|
10
|
-
*
|
|
11
|
-
* Responsibilities:
|
|
12
|
-
* - Route incoming requests to the appropriate Agent method (by sessionId)
|
|
13
|
-
* - Translate AgentCallbacks into outgoing notifications (with sessionId)
|
|
14
|
-
* - Implement the canUseTool permission flow over the stdio protocol
|
|
15
|
-
* - Handle config updates by destroying and recreating the Agent
|
|
16
|
-
*/
|
|
17
|
-
import { Agent, listSessions, searchFiles, generateRandomName, getDefaultRemoteBranch, getMessageContent, PromptHistoryManager, AuthService, PluginCore, validateWorktreeRemovalPath, loadUserConfigEnv, } from "wave-agent-sdk";
|
|
18
|
-
import { INVALID_PARAMS as PROTOCOL_INVALID_PARAMS, INTERNAL_ERROR as PROTOCOL_INTERNAL_ERROR, METHOD_NOT_FOUND as PROTOCOL_METHOD_NOT_FOUND, } from "./protocol.js";
|
|
19
|
-
import { execFileSync } from "node:child_process";
|
|
20
|
-
import { mkdirSync, existsSync, writeFileSync } from "node:fs";
|
|
21
|
-
import { tmpdir } from "node:os";
|
|
22
|
-
import { basename, extname, join } from "node:path";
|
|
23
|
-
import { createWorktree, removeWorktree } from "../utils/worktree.js";
|
|
24
|
-
import { logger } from "../utils/logger.js";
|
|
25
|
-
import { isUserCheckpointMessage } from "../utils/rewindCheckpoints.js";
|
|
26
|
-
export class AgentBridge {
|
|
27
|
-
constructor(options) {
|
|
28
|
-
this.sessions = new Map();
|
|
29
|
-
/** Pending approval requests, keyed by requestId. Stored with the resolve +
|
|
30
|
-
* context so a re-attached client can list and respond to them (daemon mode:
|
|
31
|
-
* approvals outlive any single connection). */
|
|
32
|
-
this.pendingPermissions = new Map();
|
|
33
|
-
this.permissionCounter = 0;
|
|
34
|
-
this.emit = options.emit;
|
|
35
|
-
// Mirror the user-level settings env WAVE_SERVER_URL into process.env
|
|
36
|
-
// before any agent initializes. getAuthStatus (webviewReady →
|
|
37
|
-
// pushInitialState) can run before the first agent, and AuthService falls
|
|
38
|
-
// back to the default URL otherwise — refreshing a custom-domain token
|
|
39
|
-
// against the wrong host 401s into a logged-out state.
|
|
40
|
-
const userEnv = loadUserConfigEnv();
|
|
41
|
-
if (userEnv.WAVE_SERVER_URL) {
|
|
42
|
-
process.env.WAVE_SERVER_URL = userEnv.WAVE_SERVER_URL;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
// ── Public API ────────────────────────────────────────────────
|
|
46
|
-
async handleRequest(method, params, sessionId) {
|
|
47
|
-
const p = (params ?? {});
|
|
48
|
-
switch (method) {
|
|
49
|
-
// ── Lifecycle ──
|
|
50
|
-
case "initialize":
|
|
51
|
-
return this.initialize(p);
|
|
52
|
-
case "destroy":
|
|
53
|
-
return this.destroy(sessionId);
|
|
54
|
-
case "restoreSession":
|
|
55
|
-
return this.restoreSession(p.sessionId, sessionId);
|
|
56
|
-
case "listSessions":
|
|
57
|
-
return this.listSessions(p.workdir, sessionId);
|
|
58
|
-
case "getSessionInfo":
|
|
59
|
-
return this.getSessionInfo(sessionId);
|
|
60
|
-
case "listPendingPermissions":
|
|
61
|
-
return this.listPendingPermissions();
|
|
62
|
-
case "listDaemonSessions":
|
|
63
|
-
return this.listDaemonSessions();
|
|
64
|
-
case "updateConfig":
|
|
65
|
-
return this.updateConfig(p, sessionId);
|
|
66
|
-
case "getConfiguredModels":
|
|
67
|
-
return this.getConfiguredModels(sessionId);
|
|
68
|
-
case "setModel":
|
|
69
|
-
return this.setModel(p.model, sessionId);
|
|
70
|
-
// ── Messages ──
|
|
71
|
-
case "sendMessage":
|
|
72
|
-
return this.sendMessage(p, sessionId);
|
|
73
|
-
case "bang":
|
|
74
|
-
return this.bang(p.command, sessionId);
|
|
75
|
-
case "askBtw":
|
|
76
|
-
return this.askBtw(p.question, sessionId);
|
|
77
|
-
case "abortMessage":
|
|
78
|
-
return this.abortMessage(sessionId);
|
|
79
|
-
case "clearMessages":
|
|
80
|
-
return this.clearMessages(sessionId);
|
|
81
|
-
case "rewindToMessage":
|
|
82
|
-
return this.rewindToMessage(p.messageId, sessionId);
|
|
83
|
-
case "listRewindCheckpoints":
|
|
84
|
-
return this.listRewindCheckpoints(sessionId);
|
|
85
|
-
case "deleteQueuedMessage":
|
|
86
|
-
return this.deleteQueuedMessage(p.index, sessionId);
|
|
87
|
-
case "updateQueuedMessage":
|
|
88
|
-
return this.updateQueuedMessage(p.id, p.text, p.images, sessionId);
|
|
89
|
-
case "deleteQueuedMessageById":
|
|
90
|
-
return this.deleteQueuedMessageById(p.id, sessionId);
|
|
91
|
-
case "getMessages":
|
|
92
|
-
return this.getMessages(sessionId);
|
|
93
|
-
case "getFullMessageThread":
|
|
94
|
-
return this.getFullMessageThread(sessionId);
|
|
95
|
-
// ── Permissions ──
|
|
96
|
-
case "setPermissionMode":
|
|
97
|
-
return this.setPermissionMode(p.mode, sessionId);
|
|
98
|
-
case "getPermissionMode":
|
|
99
|
-
return this.getPermissionMode(sessionId);
|
|
100
|
-
// ── MCP ──
|
|
101
|
-
case "getMcpServers":
|
|
102
|
-
return this.getMcpServers(sessionId);
|
|
103
|
-
case "connectMcpServer":
|
|
104
|
-
return this.connectMcpServer(p.serverName, sessionId);
|
|
105
|
-
case "disconnectMcpServer":
|
|
106
|
-
return this.disconnectMcpServer(p.serverName, sessionId);
|
|
107
|
-
// ── Commands ──
|
|
108
|
-
case "getSlashCommands":
|
|
109
|
-
return this.getSlashCommands(sessionId);
|
|
110
|
-
case "getSubagentConfigurations":
|
|
111
|
-
return this.getSubagentConfigurations(sessionId);
|
|
112
|
-
// ── File / History (global — no session required) ──
|
|
113
|
-
case "searchFiles":
|
|
114
|
-
return this.searchFiles(p, sessionId);
|
|
115
|
-
case "writeArtifactFile":
|
|
116
|
-
return this.writeArtifactFile(p);
|
|
117
|
-
case "getPromptHistory":
|
|
118
|
-
return this.getPromptHistory(p.workdir, sessionId);
|
|
119
|
-
case "searchPromptHistory":
|
|
120
|
-
return this.searchPromptHistory(p.query, p.workdir, sessionId);
|
|
121
|
-
// ── Auth (global — no session required) ──
|
|
122
|
-
case "getAuthStatus":
|
|
123
|
-
return this.getAuthStatus();
|
|
124
|
-
case "login":
|
|
125
|
-
return this.login(p.serverUrl);
|
|
126
|
-
case "logout":
|
|
127
|
-
return this.logout();
|
|
128
|
-
// ── Plugins (global — no session required) ──
|
|
129
|
-
case "listPlugins":
|
|
130
|
-
return this.listPlugins(p.workdir, sessionId);
|
|
131
|
-
case "installPlugin":
|
|
132
|
-
return this.installPlugin(p.pluginId, p.scope, p.workdir, sessionId);
|
|
133
|
-
case "uninstallPlugin":
|
|
134
|
-
return this.uninstallPlugin(p.pluginId, p.workdir, sessionId);
|
|
135
|
-
case "enablePlugin":
|
|
136
|
-
return this.enablePlugin(p.pluginId, p.scope, p.workdir, sessionId);
|
|
137
|
-
case "disablePlugin":
|
|
138
|
-
return this.disablePlugin(p.pluginId, p.scope, p.workdir, sessionId);
|
|
139
|
-
case "getProjectSettings":
|
|
140
|
-
return this.getProjectSettings(p.workdir, sessionId);
|
|
141
|
-
case "setBuiltinPluginEnabled":
|
|
142
|
-
return this.setBuiltinPluginEnabled(p.pluginId, p.enabled, p.scope, p.workdir, sessionId);
|
|
143
|
-
case "updatePlugin":
|
|
144
|
-
return this.updatePlugin(p.pluginId, p.workdir, sessionId);
|
|
145
|
-
case "listMarketplaces":
|
|
146
|
-
return this.listMarketplaces(p.workdir, sessionId);
|
|
147
|
-
case "addMarketplace":
|
|
148
|
-
return this.addMarketplace(p.input, p.scope, p.workdir, sessionId);
|
|
149
|
-
case "removeMarketplace":
|
|
150
|
-
return this.removeMarketplace(p.name, p.scope, p.workdir, sessionId);
|
|
151
|
-
case "updateMarketplace":
|
|
152
|
-
return this.updateMarketplace(p.name, p.workdir, sessionId);
|
|
153
|
-
case "compact":
|
|
154
|
-
return this.compact(p.customInstructions, sessionId);
|
|
155
|
-
// ── Background tasks ──
|
|
156
|
-
case "getBackgroundTaskOutput":
|
|
157
|
-
return this.getBackgroundTaskOutput(p.taskId, sessionId);
|
|
158
|
-
case "stopBackgroundTask":
|
|
159
|
-
return this.stopBackgroundTask(p.taskId, sessionId);
|
|
160
|
-
case "backgroundCurrentTask":
|
|
161
|
-
return this.backgroundCurrentTask(sessionId);
|
|
162
|
-
case "getWorkflowRuns":
|
|
163
|
-
return this.getWorkflowRuns(sessionId);
|
|
164
|
-
case "stopWorkflowRun":
|
|
165
|
-
return this.stopWorkflowRun(p.runId, sessionId);
|
|
166
|
-
// ── Git / worktree (global — no session required) ──
|
|
167
|
-
case "listGitBranches":
|
|
168
|
-
return this.listGitBranches(p.workdir);
|
|
169
|
-
case "createWorktree":
|
|
170
|
-
return this.createWorktreeSession(p);
|
|
171
|
-
case "removeWorktree":
|
|
172
|
-
return this.removeWorktreeSession(p);
|
|
173
|
-
default:
|
|
174
|
-
throw new RpcError(PROTOCOL_METHOD_NOT_FOUND, `Method not found: ${method}`);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
handleNotification(method, params) {
|
|
178
|
-
if (method === "permissionResponse") {
|
|
179
|
-
const p = params;
|
|
180
|
-
// requestId is process-level unique; lookup doesn't need sessionId
|
|
181
|
-
const entry = this.pendingPermissions.get(p.requestId);
|
|
182
|
-
if (entry) {
|
|
183
|
-
this.pendingPermissions.delete(p.requestId);
|
|
184
|
-
entry.resolve(p.decision);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
// ── Lifecycle ─────────────────────────────────────────────────
|
|
189
|
-
async initialize(params) {
|
|
190
|
-
// Re-attach (daemon mode): if the target session is already live in this
|
|
191
|
-
// process, reuse it instead of creating a second agent writing to the same
|
|
192
|
-
// transcript. The live agent keeps running across client detach/attach.
|
|
193
|
-
if (params.restoreSessionId) {
|
|
194
|
-
const live = this.sessions.get(params.restoreSessionId);
|
|
195
|
-
if (live) {
|
|
196
|
-
return {
|
|
197
|
-
sessionId: live.agent.sessionId,
|
|
198
|
-
workingDirectory: live.agent.workingDirectory,
|
|
199
|
-
permissionMode: live.agent.getPermissionMode(),
|
|
200
|
-
latestTotalTokens: live.agent.latestTotalTokens,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
const ctx = {};
|
|
205
|
-
const callbacks = this.createCallbacks(ctx);
|
|
206
|
-
const options = {
|
|
207
|
-
callbacks,
|
|
208
|
-
logger,
|
|
209
|
-
workdir: params.workdir,
|
|
210
|
-
restoreSessionId: params.restoreSessionId,
|
|
211
|
-
apiKey: params.apiKey,
|
|
212
|
-
baseURL: params.baseURL,
|
|
213
|
-
defaultHeaders: params.defaultHeaders,
|
|
214
|
-
model: params.model,
|
|
215
|
-
fastModel: params.fastModel,
|
|
216
|
-
language: params.language,
|
|
217
|
-
permissionMode: params.permissionMode,
|
|
218
|
-
tools: params.tools,
|
|
219
|
-
allowedTools: params.allowedTools,
|
|
220
|
-
disallowedTools: params.disallowedTools,
|
|
221
|
-
plugins: params.pluginDirs?.map((path) => ({ type: "local", path })),
|
|
222
|
-
mcpServers: params.mcpServers,
|
|
223
|
-
worktreeName: params.worktreeName,
|
|
224
|
-
isNewWorktree: params.isNewWorktree,
|
|
225
|
-
canUseTool: (context) => this.canUseTool(context, ctx),
|
|
226
|
-
};
|
|
227
|
-
const agent = await Agent.create(options);
|
|
228
|
-
ctx.agent = agent;
|
|
229
|
-
ctx.registeredSessionId = agent.sessionId;
|
|
230
|
-
this.sessions.set(agent.sessionId, {
|
|
231
|
-
agent,
|
|
232
|
-
storedConfig: { ...params },
|
|
233
|
-
});
|
|
234
|
-
return {
|
|
235
|
-
sessionId: agent.sessionId,
|
|
236
|
-
workingDirectory: agent.workingDirectory,
|
|
237
|
-
permissionMode: agent.getPermissionMode(),
|
|
238
|
-
latestTotalTokens: agent.latestTotalTokens,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
async destroy(sessionId) {
|
|
242
|
-
if (sessionId) {
|
|
243
|
-
const entry = this.sessions.get(sessionId);
|
|
244
|
-
if (entry) {
|
|
245
|
-
await entry.agent.destroy();
|
|
246
|
-
this.sessions.delete(sessionId);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return null;
|
|
250
|
-
}
|
|
251
|
-
/**
|
|
252
|
-
* True when every hosted session has settled: not generating, nothing queued,
|
|
253
|
-
* and no background work (background bash / subagents / workflows) — the same
|
|
254
|
-
* condition `wave -p` waits on before exiting (print-cli.ts). Pending
|
|
255
|
-
* permission approvals keep the owning agent's isLoading true, so they are
|
|
256
|
-
* covered without an explicit check.
|
|
257
|
-
*/
|
|
258
|
-
isIdle() {
|
|
259
|
-
for (const entry of this.sessions.values()) {
|
|
260
|
-
const agent = entry.agent;
|
|
261
|
-
if (agent.isLoading ||
|
|
262
|
-
agent.hasPendingMessages ||
|
|
263
|
-
agent.hasRunningBackgroundWork) {
|
|
264
|
-
return false;
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return true;
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* Destroy every hosted session agent. Each Agent.destroy() saves its
|
|
271
|
-
* transcript, drains in-flight auto-memory extraction, and cleans up
|
|
272
|
-
* background tasks/subagents. Best-effort: one failing destroy must not
|
|
273
|
-
* block the rest of the shutdown.
|
|
274
|
-
*/
|
|
275
|
-
async destroyAll() {
|
|
276
|
-
const entries = [...this.sessions.values()];
|
|
277
|
-
this.sessions.clear();
|
|
278
|
-
await Promise.all(entries.map((entry) => entry.agent.destroy().catch(() => { })));
|
|
279
|
-
}
|
|
280
|
-
async restoreSession(restoreId, sessionId) {
|
|
281
|
-
const entry = this.requireSession(sessionId);
|
|
282
|
-
// Re-attach to a live session: the SDK restore would no-op (target is
|
|
283
|
-
// already current). Emit the current messages so the freshly attached
|
|
284
|
-
// client — whose router registered only after initialize returned and so
|
|
285
|
-
// missed any earlier notifications — gets a snapshot without replay.
|
|
286
|
-
if (entry.agent.sessionId === restoreId) {
|
|
287
|
-
this.emit("messagesChange", { messages: entry.agent.messages }, entry.agent.sessionId);
|
|
288
|
-
// The re-attached client also missed the loading state that settled
|
|
289
|
-
// before its router registered — replay it or the client's
|
|
290
|
-
// isStreaming/running indicator stays false while the live session
|
|
291
|
-
// keeps generating.
|
|
292
|
-
this.emit("loadingChange", {
|
|
293
|
-
loading: entry.agent.isLoading,
|
|
294
|
-
latestTotalTokens: entry.agent.latestTotalTokens,
|
|
295
|
-
}, entry.agent.sessionId);
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
await entry.agent.restoreSession(restoreId);
|
|
299
|
-
return null;
|
|
300
|
-
}
|
|
301
|
-
async listSessions(workdir, sessionId) {
|
|
302
|
-
const sessions = await listSessions(workdir || this.getSessionWorkdir(sessionId) || process.cwd());
|
|
303
|
-
return { sessions };
|
|
304
|
-
}
|
|
305
|
-
// ── Git / worktree ────────────────────────────────────────────
|
|
306
|
-
listGitBranches(workdir) {
|
|
307
|
-
if (!workdir) {
|
|
308
|
-
throw new RpcError(PROTOCOL_INTERNAL_ERROR, "workdir is required");
|
|
309
|
-
}
|
|
310
|
-
const gitOpts = {
|
|
311
|
-
cwd: workdir,
|
|
312
|
-
encoding: "utf8",
|
|
313
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
314
|
-
};
|
|
315
|
-
let branchesRaw;
|
|
316
|
-
try {
|
|
317
|
-
branchesRaw = execFileSync("git", ["for-each-ref", "--format=%(refname:short)", "refs/heads"], gitOpts).trim();
|
|
318
|
-
}
|
|
319
|
-
catch {
|
|
320
|
-
throw new RpcError(PROTOCOL_INTERNAL_ERROR, `Not a git repository (or git unavailable): ${workdir}`);
|
|
321
|
-
}
|
|
322
|
-
const branches = branchesRaw
|
|
323
|
-
? branchesRaw
|
|
324
|
-
.split("\n")
|
|
325
|
-
.map((b) => b.trim())
|
|
326
|
-
.filter(Boolean)
|
|
327
|
-
: [];
|
|
328
|
-
let current = null;
|
|
329
|
-
try {
|
|
330
|
-
const head = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], gitOpts).trim();
|
|
331
|
-
// Detached HEAD prints "HEAD" — treat as no current branch.
|
|
332
|
-
current = head && head !== "HEAD" ? head : null;
|
|
333
|
-
}
|
|
334
|
-
catch {
|
|
335
|
-
current = null;
|
|
336
|
-
}
|
|
337
|
-
return { branches, current };
|
|
338
|
-
}
|
|
339
|
-
async createWorktreeSession(params) {
|
|
340
|
-
if (!params.workdir) {
|
|
341
|
-
throw new RpcError(PROTOCOL_INTERNAL_ERROR, "workdir is required");
|
|
342
|
-
}
|
|
343
|
-
const name = params.name?.trim() || generateRandomName();
|
|
344
|
-
try {
|
|
345
|
-
const session = await createWorktree(name, params.workdir, {
|
|
346
|
-
baseBranch: params.baseBranch,
|
|
347
|
-
});
|
|
348
|
-
return {
|
|
349
|
-
name: session.name,
|
|
350
|
-
path: session.path,
|
|
351
|
-
branch: session.branch,
|
|
352
|
-
repoRoot: session.repoRoot,
|
|
353
|
-
baseBranch: params.baseBranch ?? getDefaultRemoteBranch(params.workdir),
|
|
354
|
-
isNew: session.isNew,
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
catch (e) {
|
|
358
|
-
throw new RpcError(PROTOCOL_INTERNAL_ERROR, e.message);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
async removeWorktreeSession(params) {
|
|
362
|
-
// Align with Claude Code v2.1.216+: refuse to remove a worktree whose path
|
|
363
|
-
// is a symlink or resolves outside the repo root. Already-removed (missing)
|
|
364
|
-
// paths pass validation so removal stays idempotent.
|
|
365
|
-
try {
|
|
366
|
-
validateWorktreeRemovalPath(params.path, params.repoRoot);
|
|
367
|
-
}
|
|
368
|
-
catch (e) {
|
|
369
|
-
throw new RpcError(PROTOCOL_INTERNAL_ERROR, e.message);
|
|
370
|
-
}
|
|
371
|
-
// Trigger the WorktreeRemove hook (before git removal, non-blocking) using
|
|
372
|
-
// the session that runs in this worktree, if it is still registered.
|
|
373
|
-
for (const entry of this.sessions.values()) {
|
|
374
|
-
if (entry.agent.workingDirectory === params.path) {
|
|
375
|
-
try {
|
|
376
|
-
await entry.agent.triggerWorktreeRemoveHook(params.path);
|
|
377
|
-
}
|
|
378
|
-
catch (e) {
|
|
379
|
-
logger.warn("WorktreeRemove hooks execution failed:", e);
|
|
380
|
-
}
|
|
381
|
-
break;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
// removeWorktree is best-effort/idempotent: already-removed worktrees or
|
|
385
|
-
// branches only log, never throw.
|
|
386
|
-
await removeWorktree({
|
|
387
|
-
name: "",
|
|
388
|
-
path: params.path,
|
|
389
|
-
branch: params.branch,
|
|
390
|
-
repoRoot: params.repoRoot,
|
|
391
|
-
hasUncommittedChanges: false,
|
|
392
|
-
hasNewCommits: false,
|
|
393
|
-
isNew: false,
|
|
394
|
-
});
|
|
395
|
-
return { ok: true };
|
|
396
|
-
}
|
|
397
|
-
getSessionInfo(sessionId) {
|
|
398
|
-
const entry = this.requireSession(sessionId);
|
|
399
|
-
return {
|
|
400
|
-
sessionId: entry.agent.sessionId,
|
|
401
|
-
workingDirectory: entry.agent.workingDirectory,
|
|
402
|
-
latestTotalTokens: entry.agent.latestTotalTokens,
|
|
403
|
-
permissionMode: entry.agent.getPermissionMode(),
|
|
404
|
-
availableTools: entry.agent.getAvailableToolNames(),
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
async updateConfig(params, sessionId) {
|
|
408
|
-
const entry = this.requireSession(sessionId);
|
|
409
|
-
const currentSessionId = entry.agent.sessionId;
|
|
410
|
-
// Merge new config into stored config
|
|
411
|
-
entry.storedConfig = { ...entry.storedConfig, ...params };
|
|
412
|
-
// Destroy and recreate within the same session slot
|
|
413
|
-
await entry.agent.destroy();
|
|
414
|
-
this.sessions.delete(currentSessionId);
|
|
415
|
-
const ctx = {};
|
|
416
|
-
const callbacks = this.createCallbacks(ctx);
|
|
417
|
-
const options = {
|
|
418
|
-
callbacks,
|
|
419
|
-
workdir: entry.storedConfig.workdir,
|
|
420
|
-
restoreSessionId: currentSessionId,
|
|
421
|
-
apiKey: entry.storedConfig.apiKey,
|
|
422
|
-
baseURL: entry.storedConfig.baseURL,
|
|
423
|
-
defaultHeaders: entry.storedConfig.defaultHeaders,
|
|
424
|
-
model: entry.storedConfig.model,
|
|
425
|
-
fastModel: entry.storedConfig.fastModel,
|
|
426
|
-
language: entry.storedConfig.language,
|
|
427
|
-
permissionMode: entry.storedConfig.permissionMode,
|
|
428
|
-
tools: entry.storedConfig.tools,
|
|
429
|
-
allowedTools: entry.storedConfig.allowedTools,
|
|
430
|
-
disallowedTools: entry.storedConfig.disallowedTools,
|
|
431
|
-
plugins: entry.storedConfig.pluginDirs?.map((p) => ({
|
|
432
|
-
type: "local",
|
|
433
|
-
path: p,
|
|
434
|
-
})),
|
|
435
|
-
mcpServers: entry.storedConfig.mcpServers,
|
|
436
|
-
// Keep worktree context (permission safety) across recreation, but never
|
|
437
|
-
// re-fire WorktreeCreate — the hook ran at initial creation.
|
|
438
|
-
worktreeName: entry.storedConfig.worktreeName,
|
|
439
|
-
canUseTool: (context) => this.canUseTool(context, ctx),
|
|
440
|
-
};
|
|
441
|
-
const agent = await Agent.create(options);
|
|
442
|
-
ctx.agent = agent;
|
|
443
|
-
ctx.registeredSessionId = agent.sessionId;
|
|
444
|
-
this.sessions.set(agent.sessionId, {
|
|
445
|
-
agent,
|
|
446
|
-
storedConfig: { ...entry.storedConfig },
|
|
447
|
-
});
|
|
448
|
-
return { sessionId: agent.sessionId };
|
|
449
|
-
}
|
|
450
|
-
getConfiguredModels(sessionId) {
|
|
451
|
-
const entry = this.requireSession(sessionId);
|
|
452
|
-
return {
|
|
453
|
-
models: entry.agent.getConfiguredModels(),
|
|
454
|
-
currentModel: entry.agent.getModelConfig().model,
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
async setModel(model, sessionId) {
|
|
458
|
-
const entry = this.requireSession(sessionId);
|
|
459
|
-
entry.agent.setModel(model);
|
|
460
|
-
// Keep storedConfig in sync: updateConfig recreates the agent from
|
|
461
|
-
// storedConfig, so without this a later config save would revert the
|
|
462
|
-
// model chosen here.
|
|
463
|
-
entry.storedConfig = { ...entry.storedConfig, model };
|
|
464
|
-
return null;
|
|
465
|
-
}
|
|
466
|
-
// ── Messages ──────────────────────────────────────────────────
|
|
467
|
-
async sendMessage(params, sessionId) {
|
|
468
|
-
const entry = this.requireSession(sessionId);
|
|
469
|
-
if (params.force) {
|
|
470
|
-
entry.agent.abortMessage();
|
|
471
|
-
}
|
|
472
|
-
// Save prompt to history (mirrors VSCE chatSession.ts:236-242)
|
|
473
|
-
try {
|
|
474
|
-
await PromptHistoryManager.addEntry(params.text, entry.agent.sessionId, {}, entry.agent.workingDirectory);
|
|
475
|
-
}
|
|
476
|
-
catch {
|
|
477
|
-
// Best-effort; don't block message sending on history save failure
|
|
478
|
-
}
|
|
479
|
-
await entry.agent.sendMessage(params.text, this.persistDataUrlImages(params.images));
|
|
480
|
-
return null;
|
|
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
|
-
}
|
|
512
|
-
async bang(command, sessionId) {
|
|
513
|
-
const entry = this.requireSession(sessionId);
|
|
514
|
-
await entry.agent.bang(command);
|
|
515
|
-
return null;
|
|
516
|
-
}
|
|
517
|
-
async askBtw(question, sessionId) {
|
|
518
|
-
const entry = this.requireSession(sessionId);
|
|
519
|
-
return entry.agent.askBtw(question, undefined,
|
|
520
|
-
// Stream partial content to the client so webview hosts can render the
|
|
521
|
-
// answer incrementally (thinking and content travel on separate
|
|
522
|
-
// channels so the panel can drop thinking text once content starts).
|
|
523
|
-
(content) => {
|
|
524
|
-
if (sessionId) {
|
|
525
|
-
this.emit("btwContent", { question, content, type: "content" }, sessionId);
|
|
526
|
-
}
|
|
527
|
-
}, (content) => {
|
|
528
|
-
if (sessionId) {
|
|
529
|
-
this.emit("btwContent", { question, content, type: "thinking" }, sessionId);
|
|
530
|
-
}
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
async abortMessage(sessionId) {
|
|
534
|
-
const entry = this.requireSession(sessionId);
|
|
535
|
-
entry.agent.abortMessage();
|
|
536
|
-
return null;
|
|
537
|
-
}
|
|
538
|
-
async clearMessages(sessionId) {
|
|
539
|
-
const entry = this.requireSession(sessionId);
|
|
540
|
-
entry.agent.clearMessages();
|
|
541
|
-
return null;
|
|
542
|
-
}
|
|
543
|
-
async rewindToMessage(messageId, sessionId) {
|
|
544
|
-
const entry = this.requireSession(sessionId);
|
|
545
|
-
const { messages } = await entry.agent.getFullMessageThread();
|
|
546
|
-
// 压缩是 append-only:同 id 消息(压缩前历史 + 压缩后 append 的重复)会
|
|
547
|
-
// 在磁盘完整线程中出现多次。用户看到的折叠视图对应最后一次出现,
|
|
548
|
-
// 因此匹配最后一个而非第一个,避免回滚时连压缩摘要一起删掉。
|
|
549
|
-
const index = messages.map((m) => m.id).lastIndexOf(messageId);
|
|
550
|
-
if (index === -1) {
|
|
551
|
-
throw new RpcError(PROTOCOL_INTERNAL_ERROR, `Message not found: ${messageId}`);
|
|
552
|
-
}
|
|
553
|
-
const message = messages[index];
|
|
554
|
-
const textBlock = message.blocks.find((b) => b.type === "text");
|
|
555
|
-
await entry.agent.truncateHistory(index);
|
|
556
|
-
return { inputContent: textBlock?.content || "" };
|
|
557
|
-
}
|
|
558
|
-
async listRewindCheckpoints(sessionId) {
|
|
559
|
-
const entry = this.requireSession(sessionId);
|
|
560
|
-
const { messages } = await entry.agent.getFullMessageThread();
|
|
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()) };
|
|
574
|
-
}
|
|
575
|
-
deleteQueuedMessage(index, sessionId) {
|
|
576
|
-
const entry = this.requireSession(sessionId);
|
|
577
|
-
entry.agent.removeQueuedMessage(index);
|
|
578
|
-
return null;
|
|
579
|
-
}
|
|
580
|
-
updateQueuedMessage(id, text, images, sessionId) {
|
|
581
|
-
const entry = this.requireSession(sessionId);
|
|
582
|
-
const ok = entry.agent.updateQueuedMessageById(id, {
|
|
583
|
-
content: text,
|
|
584
|
-
images: this.persistDataUrlImages(images),
|
|
585
|
-
});
|
|
586
|
-
return { ok };
|
|
587
|
-
}
|
|
588
|
-
deleteQueuedMessageById(id, sessionId) {
|
|
589
|
-
const entry = this.requireSession(sessionId);
|
|
590
|
-
entry.agent.removeQueuedMessageById(id);
|
|
591
|
-
return null;
|
|
592
|
-
}
|
|
593
|
-
getMessages(sessionId) {
|
|
594
|
-
const entry = this.requireSession(sessionId);
|
|
595
|
-
return { messages: entry.agent.messages };
|
|
596
|
-
}
|
|
597
|
-
async getFullMessageThread(sessionId) {
|
|
598
|
-
const entry = this.requireSession(sessionId);
|
|
599
|
-
return entry.agent.getFullMessageThread();
|
|
600
|
-
}
|
|
601
|
-
async compact(customInstructions, sessionId) {
|
|
602
|
-
const entry = this.requireSession(sessionId);
|
|
603
|
-
await entry.agent.compact(customInstructions);
|
|
604
|
-
return null;
|
|
605
|
-
}
|
|
606
|
-
// ── Background tasks ──
|
|
607
|
-
getBackgroundTaskOutput(taskId, sessionId) {
|
|
608
|
-
const entry = this.requireSession(sessionId);
|
|
609
|
-
return { output: entry.agent.getBackgroundTaskOutput(taskId) };
|
|
610
|
-
}
|
|
611
|
-
stopBackgroundTask(taskId, sessionId) {
|
|
612
|
-
const entry = this.requireSession(sessionId);
|
|
613
|
-
const success = entry.agent.stopBackgroundTask(taskId);
|
|
614
|
-
return { success };
|
|
615
|
-
}
|
|
616
|
-
async backgroundCurrentTask(sessionId) {
|
|
617
|
-
const entry = this.requireSession(sessionId);
|
|
618
|
-
await entry.agent.backgroundCurrentTask();
|
|
619
|
-
return null;
|
|
620
|
-
}
|
|
621
|
-
async getWorkflowRuns(sessionId) {
|
|
622
|
-
const entry = this.requireSession(sessionId);
|
|
623
|
-
const runs = await entry.agent.getWorkflowRuns();
|
|
624
|
-
return {
|
|
625
|
-
runs: runs.map((r) => {
|
|
626
|
-
const { completionPromise, ...rest } = r;
|
|
627
|
-
void completionPromise;
|
|
628
|
-
return rest;
|
|
629
|
-
}),
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
stopWorkflowRun(runId, sessionId) {
|
|
633
|
-
const entry = this.requireSession(sessionId);
|
|
634
|
-
entry.agent.stopWorkflowRun(runId);
|
|
635
|
-
return { success: true };
|
|
636
|
-
}
|
|
637
|
-
// ── Permissions ───────────────────────────────────────────────
|
|
638
|
-
async setPermissionMode(mode, sessionId) {
|
|
639
|
-
const entry = this.requireSession(sessionId);
|
|
640
|
-
await entry.agent.setPermissionMode(mode);
|
|
641
|
-
return null;
|
|
642
|
-
}
|
|
643
|
-
getPermissionMode(sessionId) {
|
|
644
|
-
const entry = this.requireSession(sessionId);
|
|
645
|
-
return { mode: entry.agent.getPermissionMode() };
|
|
646
|
-
}
|
|
647
|
-
// ── MCP ───────────────────────────────────────────────────────
|
|
648
|
-
getMcpServers(sessionId) {
|
|
649
|
-
const entry = this.requireSession(sessionId);
|
|
650
|
-
return { servers: entry.agent.getMcpServers() };
|
|
651
|
-
}
|
|
652
|
-
async connectMcpServer(serverName, sessionId) {
|
|
653
|
-
const entry = this.requireSession(sessionId);
|
|
654
|
-
const success = await entry.agent.connectMcpServer(serverName);
|
|
655
|
-
return { success };
|
|
656
|
-
}
|
|
657
|
-
async disconnectMcpServer(serverName, sessionId) {
|
|
658
|
-
const entry = this.requireSession(sessionId);
|
|
659
|
-
const success = await entry.agent.disconnectMcpServer(serverName);
|
|
660
|
-
return { success };
|
|
661
|
-
}
|
|
662
|
-
// ── Commands ──────────────────────────────────────────────────
|
|
663
|
-
getSlashCommands(sessionId) {
|
|
664
|
-
const entry = this.requireSession(sessionId);
|
|
665
|
-
return { commands: entry.agent.getSlashCommands() };
|
|
666
|
-
}
|
|
667
|
-
getSubagentConfigurations(sessionId) {
|
|
668
|
-
const entry = this.requireSession(sessionId);
|
|
669
|
-
return { configurations: entry.agent.getSubagentConfigurations() };
|
|
670
|
-
}
|
|
671
|
-
// ── File / History (global) ───────────────────────────────────
|
|
672
|
-
async searchFiles(params, sessionId) {
|
|
673
|
-
const files = await searchFiles(params.query, {
|
|
674
|
-
maxResults: params.maxResults,
|
|
675
|
-
workingDirectory: params.workdir || this.getSessionWorkdir(sessionId) || process.cwd(),
|
|
676
|
-
});
|
|
677
|
-
return { files };
|
|
678
|
-
}
|
|
679
|
-
/**
|
|
680
|
-
* Writes an uploaded file (from the desktop/webview "+上传文件" flow) into the
|
|
681
|
-
* artifacts dir on THIS machine — i.e. where the agent runs. Remote sessions
|
|
682
|
-
* call this over the ssh tunnel so the returned path is reachable by the
|
|
683
|
-
* remote agent's tools; local sessions never hit this (desktop writes locally
|
|
684
|
-
* itself).
|
|
685
|
-
*/
|
|
686
|
-
async writeArtifactFile(params) {
|
|
687
|
-
// Artifacts are keyed by basename only — reject anything that could escape
|
|
688
|
-
// the artifacts dir via path traversal.
|
|
689
|
-
const safeName = basename(params.name || "");
|
|
690
|
-
if (!safeName || safeName === "." || safeName === "..") {
|
|
691
|
-
throw new RpcError(PROTOCOL_INVALID_PARAMS, `Invalid artifact file name: ${params.name}`);
|
|
692
|
-
}
|
|
693
|
-
const artifactsDir = join(tmpdir(), "wave-artifacts");
|
|
694
|
-
mkdirSync(artifactsDir, { recursive: true });
|
|
695
|
-
let finalPath = join(artifactsDir, safeName);
|
|
696
|
-
let counter = 1;
|
|
697
|
-
while (existsSync(finalPath)) {
|
|
698
|
-
const ext = extname(safeName);
|
|
699
|
-
const baseName = basename(safeName, ext);
|
|
700
|
-
finalPath = join(artifactsDir, `${baseName}_${counter}${ext}`);
|
|
701
|
-
counter++;
|
|
702
|
-
}
|
|
703
|
-
writeFileSync(finalPath, Buffer.from(params.contentBase64 || "", "base64"));
|
|
704
|
-
return { path: finalPath };
|
|
705
|
-
}
|
|
706
|
-
async getPromptHistory(workdir, sessionId) {
|
|
707
|
-
const history = await PromptHistoryManager.getHistory({
|
|
708
|
-
workdir: workdir || this.getSessionWorkdir(sessionId),
|
|
709
|
-
});
|
|
710
|
-
return { history };
|
|
711
|
-
}
|
|
712
|
-
async searchPromptHistory(query, workdir, sessionId) {
|
|
713
|
-
const history = await PromptHistoryManager.searchHistory(query, {
|
|
714
|
-
workdir: workdir || this.getSessionWorkdir(sessionId),
|
|
715
|
-
});
|
|
716
|
-
return { history };
|
|
717
|
-
}
|
|
718
|
-
// ── canUseTool flow ───────────────────────────────────────────
|
|
719
|
-
canUseTool(context, ctx) {
|
|
720
|
-
const requestId = `perm_${++this.permissionCounter}`;
|
|
721
|
-
return new Promise((resolve) => {
|
|
722
|
-
this.pendingPermissions.set(requestId, {
|
|
723
|
-
resolve,
|
|
724
|
-
sessionId: ctx.registeredSessionId,
|
|
725
|
-
context,
|
|
726
|
-
});
|
|
727
|
-
this.emit("permissionRequest", { requestId, context }, ctx.registeredSessionId);
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
/** Attach snapshot: re-surface approvals that are still pending after a
|
|
731
|
-
* client disconnected (daemon mode). Responding to any listed requestId
|
|
732
|
-
* resolves the in-process promise. */
|
|
733
|
-
listPendingPermissions() {
|
|
734
|
-
return {
|
|
735
|
-
requests: [...this.pendingPermissions.entries()].map(([requestId, entry]) => ({
|
|
736
|
-
requestId,
|
|
737
|
-
sessionId: entry.sessionId,
|
|
738
|
-
context: entry.context,
|
|
739
|
-
})),
|
|
740
|
-
};
|
|
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
|
-
}
|
|
754
|
-
// ── Auth (global) ────────────────────────────────────────────
|
|
755
|
-
async getAuthStatus() {
|
|
756
|
-
const authService = AuthService.getInstance();
|
|
757
|
-
await authService.checkAndRefreshTokenIfNeeded();
|
|
758
|
-
return {
|
|
759
|
-
isAuthenticated: authService.isSSOAuthenticated(),
|
|
760
|
-
user: authService.getAuthUser(),
|
|
761
|
-
serverUrl: authService.getServerUrl(),
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
async login(serverUrl) {
|
|
765
|
-
const authService = AuthService.getInstance();
|
|
766
|
-
await authService.login({
|
|
767
|
-
onAuthUrl: (url) => {
|
|
768
|
-
this.emit("authUrl", { url });
|
|
769
|
-
},
|
|
770
|
-
serverUrl,
|
|
771
|
-
});
|
|
772
|
-
return { user: authService.getAuthUser() };
|
|
773
|
-
}
|
|
774
|
-
async logout() {
|
|
775
|
-
const authService = AuthService.getInstance();
|
|
776
|
-
await authService.clearAuth();
|
|
777
|
-
return null;
|
|
778
|
-
}
|
|
779
|
-
// ── Plugins (global) ─────────────────────────────────────────
|
|
780
|
-
getPluginCore(workdir, sessionId) {
|
|
781
|
-
const resolvedWorkdir = workdir || this.getSessionWorkdir(sessionId) || process.cwd();
|
|
782
|
-
if (!this.pluginCore || this.pluginCoreWorkdir !== resolvedWorkdir) {
|
|
783
|
-
this.pluginCore = new PluginCore(resolvedWorkdir);
|
|
784
|
-
this.pluginCoreWorkdir = resolvedWorkdir;
|
|
785
|
-
}
|
|
786
|
-
return this.pluginCore;
|
|
787
|
-
}
|
|
788
|
-
async listPlugins(workdir, sessionId) {
|
|
789
|
-
const core = this.getPluginCore(workdir, sessionId);
|
|
790
|
-
const { plugins, mergedEnabled } = await core.listPlugins();
|
|
791
|
-
return {
|
|
792
|
-
plugins: plugins.map((p) => {
|
|
793
|
-
const pluginId = `${p.name}@${p.marketplace}`;
|
|
794
|
-
return {
|
|
795
|
-
id: pluginId,
|
|
796
|
-
name: p.name,
|
|
797
|
-
description: p.description,
|
|
798
|
-
marketplace: p.marketplace,
|
|
799
|
-
installed: p.installed,
|
|
800
|
-
version: p.version,
|
|
801
|
-
enabled: mergedEnabled[pluginId] !== false,
|
|
802
|
-
scope: p.scope,
|
|
803
|
-
};
|
|
804
|
-
}),
|
|
805
|
-
};
|
|
806
|
-
}
|
|
807
|
-
async installPlugin(pluginId, scope, workdir, sessionId) {
|
|
808
|
-
return this.getPluginCore(workdir, sessionId).installPlugin(pluginId, scope);
|
|
809
|
-
}
|
|
810
|
-
async uninstallPlugin(pluginId, workdir, sessionId) {
|
|
811
|
-
await this.getPluginCore(workdir, sessionId).uninstallPlugin(pluginId);
|
|
812
|
-
return null;
|
|
813
|
-
}
|
|
814
|
-
async enablePlugin(pluginId, scope, workdir, sessionId) {
|
|
815
|
-
return this.getPluginCore(workdir, sessionId).enablePlugin(pluginId, scope);
|
|
816
|
-
}
|
|
817
|
-
async disablePlugin(pluginId, scope, workdir, sessionId) {
|
|
818
|
-
return this.getPluginCore(workdir, sessionId).disablePlugin(pluginId, scope);
|
|
819
|
-
}
|
|
820
|
-
async getProjectSettings(workdir, sessionId) {
|
|
821
|
-
return {
|
|
822
|
-
enabledPlugins: this.getPluginCore(workdir, sessionId).getMergedEnabledPlugins(),
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
async setBuiltinPluginEnabled(pluginId, enabled, scope, workdir, sessionId) {
|
|
826
|
-
const core = this.getPluginCore(workdir, sessionId);
|
|
827
|
-
const targetScope = scope ?? "project";
|
|
828
|
-
if (enabled) {
|
|
829
|
-
await core.enablePlugin(pluginId, targetScope);
|
|
830
|
-
}
|
|
831
|
-
else {
|
|
832
|
-
await core.disablePlugin(pluginId, targetScope);
|
|
833
|
-
}
|
|
834
|
-
return { enabledPlugins: core.getMergedEnabledPlugins() };
|
|
835
|
-
}
|
|
836
|
-
async updatePlugin(pluginId, workdir, sessionId) {
|
|
837
|
-
return this.getPluginCore(workdir, sessionId).updatePlugin(pluginId);
|
|
838
|
-
}
|
|
839
|
-
async listMarketplaces(workdir, sessionId) {
|
|
840
|
-
return this.getPluginCore(workdir, sessionId).listMarketplaces();
|
|
841
|
-
}
|
|
842
|
-
async addMarketplace(input, scope, workdir, sessionId) {
|
|
843
|
-
return this.getPluginCore(workdir, sessionId).addMarketplace(input, scope);
|
|
844
|
-
}
|
|
845
|
-
async removeMarketplace(name, scope, workdir, sessionId) {
|
|
846
|
-
await this.getPluginCore(workdir, sessionId).removeMarketplace(name, scope);
|
|
847
|
-
return null;
|
|
848
|
-
}
|
|
849
|
-
async updateMarketplace(name, workdir, sessionId) {
|
|
850
|
-
await this.getPluginCore(workdir, sessionId).updateMarketplace(name);
|
|
851
|
-
return null;
|
|
852
|
-
}
|
|
853
|
-
// ── Callbacks → Notifications ─────────────────────────────────
|
|
854
|
-
createCallbacks(ctx) {
|
|
855
|
-
return {
|
|
856
|
-
onUserMessageAdded: () => {
|
|
857
|
-
const msg = this.findLastUserMessage(ctx.agent);
|
|
858
|
-
if (msg)
|
|
859
|
-
this.emit("userMessageAdded", { message: msg }, ctx.registeredSessionId);
|
|
860
|
-
},
|
|
861
|
-
onAssistantMessageAdded: (messageId) => {
|
|
862
|
-
const msg = ctx.agent?.messages.find((m) => m.id === messageId);
|
|
863
|
-
if (msg)
|
|
864
|
-
this.emit("assistantMessageAdded", { message: msg }, ctx.registeredSessionId);
|
|
865
|
-
},
|
|
866
|
-
onAssistantContentUpdated: (params) => {
|
|
867
|
-
// Wire carries only the delta; consumers accumulate (spec: 流式通知纯增量负载).
|
|
868
|
-
this.emit("assistantContentUpdated", {
|
|
869
|
-
messageId: params.messageId,
|
|
870
|
-
chunk: params.chunk,
|
|
871
|
-
stage: params.stage,
|
|
872
|
-
}, ctx.registeredSessionId);
|
|
873
|
-
},
|
|
874
|
-
onAssistantReasoningUpdated: (params) => {
|
|
875
|
-
this.emit("assistantReasoningUpdated", {
|
|
876
|
-
messageId: params.messageId,
|
|
877
|
-
chunk: params.chunk,
|
|
878
|
-
stage: params.stage,
|
|
879
|
-
}, ctx.registeredSessionId);
|
|
880
|
-
},
|
|
881
|
-
onToolBlockUpdated: (params) => {
|
|
882
|
-
// Streaming stages carry only the parametersChunk delta; start/running
|
|
883
|
-
// (one-time snapshots) and end (authoritative full value) keep
|
|
884
|
-
// `parameters` (spec: 流式通知纯增量负载).
|
|
885
|
-
const { parameters, ...rest } = params;
|
|
886
|
-
void parameters;
|
|
887
|
-
const wireParams = params.stage === "streaming" ? rest : params;
|
|
888
|
-
this.emit("toolBlockUpdated", wireParams, ctx.registeredSessionId);
|
|
889
|
-
},
|
|
890
|
-
onErrorBlockAdded: (error) => {
|
|
891
|
-
this.emit("errorBlockAdded", { error }, ctx.registeredSessionId);
|
|
892
|
-
},
|
|
893
|
-
onLoadingChange: (loading) => {
|
|
894
|
-
this.emit("loadingChange", {
|
|
895
|
-
loading,
|
|
896
|
-
latestTotalTokens: ctx.agent?.latestTotalTokens,
|
|
897
|
-
}, ctx.registeredSessionId);
|
|
898
|
-
},
|
|
899
|
-
onCommandRunningChange: (running) => {
|
|
900
|
-
this.emit("commandRunningChange", { running }, ctx.registeredSessionId);
|
|
901
|
-
},
|
|
902
|
-
onQueuedMessagesChange: (messages) => {
|
|
903
|
-
this.emit("queuedMessagesChange", { messages }, ctx.registeredSessionId);
|
|
904
|
-
},
|
|
905
|
-
onTasksChange: (tasks) => {
|
|
906
|
-
this.emit("tasksChange", { tasks }, ctx.registeredSessionId);
|
|
907
|
-
},
|
|
908
|
-
onBackgroundTasksChange: (tasks) => {
|
|
909
|
-
const summaries = tasks.map((t) => ({
|
|
910
|
-
id: t.id,
|
|
911
|
-
type: t.type,
|
|
912
|
-
status: t.status,
|
|
913
|
-
startTime: t.startTime,
|
|
914
|
-
endTime: t.endTime,
|
|
915
|
-
command: t.command,
|
|
916
|
-
description: t.description,
|
|
917
|
-
exitCode: t.exitCode,
|
|
918
|
-
runtime: t.runtime,
|
|
919
|
-
outputPath: t.outputPath,
|
|
920
|
-
}));
|
|
921
|
-
this.emit("backgroundTasksChange", { tasks: summaries }, ctx.registeredSessionId);
|
|
922
|
-
},
|
|
923
|
-
onSessionIdChange: (newSessionId) => {
|
|
924
|
-
const oldSessionId = ctx.registeredSessionId;
|
|
925
|
-
// Emit with the OLD sessionId so the client's router can deliver it
|
|
926
|
-
this.emit("sessionIdChange", { sessionId: newSessionId }, oldSessionId);
|
|
927
|
-
// Update the sessions Map key atomically (single-threaded, no await)
|
|
928
|
-
if (oldSessionId && oldSessionId !== newSessionId) {
|
|
929
|
-
const entry = this.sessions.get(oldSessionId);
|
|
930
|
-
if (entry) {
|
|
931
|
-
this.sessions.delete(oldSessionId);
|
|
932
|
-
this.sessions.set(newSessionId, entry);
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
ctx.registeredSessionId = newSessionId;
|
|
936
|
-
},
|
|
937
|
-
onPermissionModeChange: (mode) => {
|
|
938
|
-
this.emit("permissionModeChange", { mode }, ctx.registeredSessionId);
|
|
939
|
-
},
|
|
940
|
-
onWorkdirChange: (newCwd) => {
|
|
941
|
-
this.emit("workdirChange", { workdir: newCwd }, ctx.registeredSessionId);
|
|
942
|
-
},
|
|
943
|
-
onMcpServersChange: (servers) => {
|
|
944
|
-
this.emit("mcpServersChange", { servers }, ctx.registeredSessionId);
|
|
945
|
-
},
|
|
946
|
-
onAddBangMessage: (command, messageId) => {
|
|
947
|
-
this.emit("bangMessageAdded", { command, messageId }, ctx.registeredSessionId);
|
|
948
|
-
},
|
|
949
|
-
onUpdateBangMessage: (command, output, messageId) => {
|
|
950
|
-
this.emit("bangMessageUpdated", { command, output, messageId }, ctx.registeredSessionId);
|
|
951
|
-
},
|
|
952
|
-
onCompleteBangMessage: (command, exitCode, messageId, output) => {
|
|
953
|
-
this.emit("bangMessageCompleted", { command, exitCode, messageId, output }, ctx.registeredSessionId);
|
|
954
|
-
},
|
|
955
|
-
onNotificationMessageAdded: (params) => {
|
|
956
|
-
const msg = ctx.agent?.messages.find((m) => m.role === "user" &&
|
|
957
|
-
m.blocks.some((b) => b.type === "task_notification" &&
|
|
958
|
-
b.taskId === params.taskId));
|
|
959
|
-
this.emit("notificationMessageAdded", { ...params, message: msg }, ctx.registeredSessionId);
|
|
960
|
-
},
|
|
961
|
-
onCompactBlockAdded: (content) => {
|
|
962
|
-
this.emit("compactBlockAdded", { content }, ctx.registeredSessionId);
|
|
963
|
-
},
|
|
964
|
-
onCompactionStateChange: (isCompacting) => {
|
|
965
|
-
this.emit("compactionStateChange", { isCompacting }, ctx.registeredSessionId);
|
|
966
|
-
},
|
|
967
|
-
};
|
|
968
|
-
}
|
|
969
|
-
findLastUserMessage(agent) {
|
|
970
|
-
const userMessages = agent?.messages.filter((m) => m.role === "user") ?? [];
|
|
971
|
-
return userMessages[userMessages.length - 1];
|
|
972
|
-
}
|
|
973
|
-
// ── Utils ─────────────────────────────────────────────────────
|
|
974
|
-
requireSession(sessionId) {
|
|
975
|
-
if (!sessionId) {
|
|
976
|
-
throw new RpcError(PROTOCOL_INTERNAL_ERROR, "sessionId is required for this request");
|
|
977
|
-
}
|
|
978
|
-
const entry = this.sessions.get(sessionId);
|
|
979
|
-
if (!entry) {
|
|
980
|
-
throw new RpcError(PROTOCOL_INTERNAL_ERROR, `Session not found: ${sessionId}`);
|
|
981
|
-
}
|
|
982
|
-
return entry;
|
|
983
|
-
}
|
|
984
|
-
getSessionWorkdir(sessionId) {
|
|
985
|
-
if (!sessionId)
|
|
986
|
-
return undefined;
|
|
987
|
-
return this.sessions.get(sessionId)?.agent.workingDirectory;
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
// ── Error class for protocol errors ─────────────────────────────
|
|
991
|
-
export class RpcError extends Error {
|
|
992
|
-
constructor(code, message) {
|
|
993
|
-
super(message);
|
|
994
|
-
this.code = code;
|
|
995
|
-
}
|
|
996
|
-
toJsonRpcError() {
|
|
997
|
-
return { code: this.code, message: this.message };
|
|
998
|
-
}
|
|
999
|
-
}
|