wave-code 0.19.7 → 0.19.9
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/cli.js +1 -1
- package/dist/components/ConfirmationSelector.js +4 -3
- package/dist/components/TaskList.js +28 -12
- package/dist/contexts/useChat.js +1 -1
- package/dist/index.js +3 -2
- package/dist/print-cli.js +12 -4
- package/dist/reducers/confirmationReducer.d.ts +1 -1
- package/dist/reducers/confirmationReducer.js +9 -10
- package/dist/stdio/agentBridge.d.ts +11 -0
- package/dist/stdio/agentBridge.js +178 -1
- package/dist/stdio/protocol.d.ts +2 -2
- package/dist/utils/worktree.d.ts +8 -2
- package/dist/utils/worktree.js +35 -26
- package/package.json +2 -2
- package/src/cli.tsx +1 -1
- package/src/components/ConfirmationSelector.tsx +16 -15
- package/src/components/TaskList.tsx +30 -12
- package/src/contexts/useChat.tsx +1 -1
- package/src/index.ts +8 -2
- package/src/print-cli.ts +14 -4
- package/src/reducers/confirmationReducer.ts +9 -9
- package/src/stdio/agentBridge.ts +282 -0
- package/src/stdio/protocol.ts +14 -2
- package/src/utils/worktree.ts +57 -33
package/dist/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ export async function startCli(options) {
|
|
|
23
23
|
// Cleanup worktree if requested
|
|
24
24
|
if (shouldRemoveWorktree && worktreeSession) {
|
|
25
25
|
process.chdir(worktreeSession.repoRoot);
|
|
26
|
-
removeWorktree(worktreeSession);
|
|
26
|
+
await removeWorktree(worktreeSession);
|
|
27
27
|
}
|
|
28
28
|
process.exit(0);
|
|
29
29
|
}
|
|
@@ -14,7 +14,7 @@ const getHeaderColor = (header) => {
|
|
|
14
14
|
};
|
|
15
15
|
export const ConfirmationSelector = ({ toolName, toolInput, suggestedPrefix, hidePersistentOption, isExpanded = false, onDecision, onCancel, }) => {
|
|
16
16
|
const [state, dispatch] = useReducer(confirmationReducer, {
|
|
17
|
-
selectedOption:
|
|
17
|
+
selectedOption: "allow",
|
|
18
18
|
alternativeText: "",
|
|
19
19
|
alternativeCursorPosition: 0,
|
|
20
20
|
hasUserInput: false,
|
|
@@ -104,8 +104,9 @@ export const ConfirmationSelector = ({ toolName, toolInput, suggestedPrefix, hid
|
|
|
104
104
|
? "[x] "
|
|
105
105
|
: "[ ] "
|
|
106
106
|
: "", option.label, option.description ? ` - ${option.description}` : "", isOther && isSelected && (_jsxs(Text, { children: [":", " ", questionState.otherText ? (_jsxs(_Fragment, { children: [questionState.otherText.slice(0, questionState.otherCursorPosition), _jsx(Text, { backgroundColor: "white", color: "black", children: questionState.otherText[questionState.otherCursorPosition] || " " }), questionState.otherText.slice(questionState.otherCursorPosition + 1)] })) : (_jsx(Text, { color: "gray", dimColor: true, children: "[Type your answer...]" }))] }))] }) }, index));
|
|
107
|
-
}) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Question ", questionState.currentQuestionIndex + 1, " of", " ", questions.length, " \u2022", currentQuestion.multiSelect ? " Space to toggle •" : "", " Use \u2191\u2193 or Tab to navigate \u2022 Enter to confirm"] }) })] })), toolName !== ASK_USER_QUESTION_TOOL_NAME && !isExpanded && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "Do you want to proceed?" }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
107
|
+
}) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Question ", questionState.currentQuestionIndex + 1, " of", " ", questions.length, " \u2022", currentQuestion.multiSelect ? " Space to toggle •" : "", " Use \u2191\u2193 or Tab to navigate \u2022 Enter to confirm"] }) })] })), toolName !== ASK_USER_QUESTION_TOOL_NAME && !isExpanded && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "Do you want to proceed?" }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Box, { children: _jsxs(Text, { color: state.selectedOption === "allow" ? "black" : "white", backgroundColor: state.selectedOption === "allow" ? "yellow" : undefined, bold: state.selectedOption === "allow", children: [state.selectedOption === "allow" ? "> " : " ", toolName === EXIT_PLAN_MODE_TOOL_NAME
|
|
108
108
|
? "Yes, manually approve edits"
|
|
109
|
-
: "Yes, proceed"] }) }, "allow-option"), !hidePersistentOption && (_jsx(Box, { children: _jsxs(Text, { color: state.selectedOption === "auto" ? "black" : "white", backgroundColor: state.selectedOption === "auto" ? "yellow" : undefined, bold: state.selectedOption === "auto", children: [state.selectedOption === "auto" ? "> " : " ", getAutoOptionText()] }) }, "auto-option")),
|
|
109
|
+
: "Yes, proceed"] }) }, "allow-option"), !hidePersistentOption && (_jsx(Box, { children: _jsxs(Text, { color: state.selectedOption === "auto" ? "black" : "white", backgroundColor: state.selectedOption === "auto" ? "yellow" : undefined, bold: state.selectedOption === "auto", children: [state.selectedOption === "auto" ? "> " : " ", getAutoOptionText()] }) }, "auto-option")), (toolName === BASH_TOOL_NAME ||
|
|
110
|
+
toolName === EXIT_PLAN_MODE_TOOL_NAME) && (_jsx(Box, { children: _jsxs(Text, { color: state.selectedOption === "bypass" ? "black" : "white", backgroundColor: state.selectedOption === "bypass" ? "yellow" : undefined, bold: state.selectedOption === "bypass", children: [state.selectedOption === "bypass" ? "> " : " ", "Yes, and bypass permissions"] }) }, "bypass-option")), _jsx(Box, { children: _jsxs(Text, { color: state.selectedOption === "alternative" ? "black" : "white", backgroundColor: state.selectedOption === "alternative" ? "yellow" : undefined, bold: state.selectedOption === "alternative", children: [state.selectedOption === "alternative" ? "> " : " ", toolName === ENTER_PLAN_MODE_TOOL_NAME && showPlaceholder ? (_jsx(Text, { color: "gray", dimColor: true, children: "No, start implementing now" })) : showPlaceholder ? (_jsx(Text, { color: "gray", dimColor: true, children: placeholderText })) : (_jsx(Text, { children: state.alternativeText ? (_jsxs(_Fragment, { children: [state.alternativeText.slice(0, state.alternativeCursorPosition), _jsx(Text, { backgroundColor: "white", color: "black", children: state.alternativeText[state.alternativeCursorPosition] || " " }), state.alternativeText.slice(state.alternativeCursorPosition + 1)] })) : ("Type here to tell Wave what to change") }))] }) }, "alternative-option")] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Use \u2191\u2193 or Tab to navigate \u2022 ESC to cancel" }) })] }))] }));
|
|
110
111
|
};
|
|
111
112
|
ConfirmationSelector.displayName = "ConfirmationSelector";
|
|
@@ -80,12 +80,9 @@ export const TaskList = () => {
|
|
|
80
80
|
const completionTimestampsRef = React.useRef(new Map());
|
|
81
81
|
const previousCompletedIdsRef = React.useRef(null);
|
|
82
82
|
const autoHideTimerRef = React.useRef(null);
|
|
83
|
-
const [autoHidden, setAutoHidden] = React.useState(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const active = tasks.filter((t) => t.status !== "deleted");
|
|
87
|
-
return active.length > 0 && active.every((t) => t.status === "completed");
|
|
88
|
-
});
|
|
83
|
+
const [autoHidden, setAutoHidden] = React.useState(false);
|
|
84
|
+
// 是否在当前展示期间观察到过未完成任务;用于区分“任务在眼前完成”与“加载时已全部完成”
|
|
85
|
+
const hadIncompleteRef = React.useRef(false);
|
|
89
86
|
const [, forceUpdate] = React.useState(0);
|
|
90
87
|
const now = Date.now();
|
|
91
88
|
const activeTasks = tasks.filter((t) => t.status !== "deleted");
|
|
@@ -125,23 +122,42 @@ export const TaskList = () => {
|
|
|
125
122
|
// Auto-hide logic: when all active tasks are completed
|
|
126
123
|
const allCompleted = activeTasks.length > 0 &&
|
|
127
124
|
activeTasks.every((t) => t.status === "completed");
|
|
125
|
+
// 观察到任务从“存在未完成”变为“全部完成”时保留 5 秒再隐藏;
|
|
126
|
+
// 加载或恢复到已全部完成的会话时立即隐藏,避免先闪现再消失
|
|
128
127
|
React.useEffect(() => {
|
|
129
|
-
if (
|
|
128
|
+
if (activeTasks.length === 0) {
|
|
129
|
+
hadIncompleteRef.current = false;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!allCompleted) {
|
|
133
|
+
hadIncompleteRef.current = true;
|
|
134
|
+
if (autoHidden) {
|
|
135
|
+
setAutoHidden(false);
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!hadIncompleteRef.current) {
|
|
140
|
+
if (!autoHidden) {
|
|
141
|
+
setAutoHidden(true);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (!autoHidden) {
|
|
130
146
|
autoHideTimerRef.current = setTimeout(() => {
|
|
131
147
|
setAutoHidden(true);
|
|
132
148
|
}, AUTO_HIDE_DELAY_MS);
|
|
133
149
|
}
|
|
134
|
-
if (!allCompleted && autoHidden) {
|
|
135
|
-
setAutoHidden(false);
|
|
136
|
-
}
|
|
137
150
|
return () => {
|
|
138
151
|
if (autoHideTimerRef.current) {
|
|
139
152
|
clearTimeout(autoHideTimerRef.current);
|
|
140
153
|
autoHideTimerRef.current = null;
|
|
141
154
|
}
|
|
142
155
|
};
|
|
143
|
-
}, [allCompleted, autoHidden]);
|
|
144
|
-
if (tasks.length === 0 ||
|
|
156
|
+
}, [allCompleted, autoHidden, activeTasks.length]);
|
|
157
|
+
if (tasks.length === 0 ||
|
|
158
|
+
!isTaskListVisible ||
|
|
159
|
+
autoHidden ||
|
|
160
|
+
(allCompleted && !hadIncompleteRef.current)) {
|
|
145
161
|
return null;
|
|
146
162
|
}
|
|
147
163
|
const displayLimit = getDisplayLimit(stdout?.rows);
|
package/dist/contexts/useChat.js
CHANGED
|
@@ -235,7 +235,7 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
235
235
|
agentRef.current = agent;
|
|
236
236
|
// Inject worktree session into this agent's own DI container so system
|
|
237
237
|
// prompts and permission checks reflect the CLI -w worktree. This state is
|
|
238
|
-
// per-session, not process-global (see specs/
|
|
238
|
+
// per-session, not process-global (see docs/specs/multi-agent/worktree.md FR-042).
|
|
239
239
|
if (worktreeSession) {
|
|
240
240
|
const session = {
|
|
241
241
|
originalCwd: originalCwd ?? worktreeSession.repoRoot,
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import yargs from "yargs";
|
|
2
2
|
import { hideBin } from "yargs/helpers";
|
|
3
3
|
import { startCli } from "./cli.js";
|
|
4
|
-
import { generateRandomName } from "wave-agent-sdk";
|
|
4
|
+
import { generateRandomName, loadMergedWaveConfig, } from "wave-agent-sdk";
|
|
5
5
|
import { createWorktree } from "./utils/worktree.js";
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { readFileSync } from "fs";
|
|
@@ -236,7 +236,8 @@ export async function main() {
|
|
|
236
236
|
if (!name || name === "") {
|
|
237
237
|
name = generateRandomName();
|
|
238
238
|
}
|
|
239
|
-
|
|
239
|
+
const baseRef = loadMergedWaveConfig(originalCwd)?.worktree?.baseRef;
|
|
240
|
+
worktreeSession = await createWorktree(name, originalCwd, { baseRef });
|
|
240
241
|
// Note: the full worktree session (originalCwd etc.) is injected into the
|
|
241
242
|
// agent's DI container after the agent is created in useChat.tsx. This keeps
|
|
242
243
|
// worktree state per-session instead of process-global.
|
package/dist/print-cli.js
CHANGED
|
@@ -104,8 +104,16 @@ export async function startPrintCli(options) {
|
|
|
104
104
|
await agent.sendMessage(message);
|
|
105
105
|
process.stdout.write("\n");
|
|
106
106
|
}
|
|
107
|
-
// Wait for running background tasks
|
|
108
|
-
|
|
107
|
+
// Wait for running background tasks/subagents to complete AND for the main
|
|
108
|
+
// agent to finish processing their completion notifications. The last
|
|
109
|
+
// background task flips status to "completed" before its notification is
|
|
110
|
+
// enqueued and before the main agent's follow-up turn runs, so
|
|
111
|
+
// hasRunningBackgroundWork alone becomes false while the main agent is
|
|
112
|
+
// still mid-turn — causing a TOCTOU race that aborts the final response.
|
|
113
|
+
// Including isLoading and queued notifications closes that gap.
|
|
114
|
+
while (agent.hasRunningBackgroundWork ||
|
|
115
|
+
agent.isLoading ||
|
|
116
|
+
agent.hasPendingMessages) {
|
|
109
117
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
110
118
|
}
|
|
111
119
|
// Display usage summary before exit
|
|
@@ -130,7 +138,7 @@ export async function startPrintCli(options) {
|
|
|
130
138
|
const hasChanges = hasUncommittedChanges(cwd);
|
|
131
139
|
const hasCommits = hasNewCommits(cwd, baseBranch);
|
|
132
140
|
if (!hasChanges && !hasCommits) {
|
|
133
|
-
removeWorktree(worktreeSession);
|
|
141
|
+
await removeWorktree(worktreeSession);
|
|
134
142
|
}
|
|
135
143
|
else {
|
|
136
144
|
process.stdout.write(`\n⚠️ Worktree '${worktreeSession.name}' has changes or commits. Keeping it at: ${worktreeSession.path}\n`);
|
|
@@ -162,7 +170,7 @@ export async function startPrintCli(options) {
|
|
|
162
170
|
const hasChanges = hasUncommittedChanges(cwd);
|
|
163
171
|
const hasCommits = hasNewCommits(cwd, baseBranch);
|
|
164
172
|
if (!hasChanges && !hasCommits) {
|
|
165
|
-
removeWorktree(worktreeSession);
|
|
173
|
+
await removeWorktree(worktreeSession);
|
|
166
174
|
}
|
|
167
175
|
else {
|
|
168
176
|
process.stdout.write(`\n⚠️ Worktree '${worktreeSession.name}' has changes or commits. Keeping it at: ${worktreeSession.path}\n`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Key } from "ink";
|
|
2
2
|
import type { PermissionDecision } from "wave-agent-sdk";
|
|
3
3
|
export interface ConfirmationState {
|
|
4
|
-
selectedOption: "
|
|
4
|
+
selectedOption: "auto" | "bypass" | "allow" | "alternative";
|
|
5
5
|
alternativeText: string;
|
|
6
6
|
alternativeCursorPosition: number;
|
|
7
7
|
hasUserInput: boolean;
|
|
@@ -47,14 +47,7 @@ export function confirmationReducer(state, action) {
|
|
|
47
47
|
const { input, key, toolName, toolInput, suggestedPrefix, hidePersistentOption, } = action;
|
|
48
48
|
if (key.return) {
|
|
49
49
|
let decision = null;
|
|
50
|
-
if (state.selectedOption === "
|
|
51
|
-
decision = {
|
|
52
|
-
behavior: "allow",
|
|
53
|
-
newPermissionMode: "acceptEdits",
|
|
54
|
-
clearContext: true,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
else if (state.selectedOption === "allow") {
|
|
50
|
+
if (state.selectedOption === "allow") {
|
|
58
51
|
if (toolName === EXIT_PLAN_MODE_TOOL_NAME) {
|
|
59
52
|
decision = { behavior: "allow", newPermissionMode: "default" };
|
|
60
53
|
}
|
|
@@ -91,6 +84,12 @@ export function confirmationReducer(state, action) {
|
|
|
91
84
|
decision = { behavior: "allow", newPermissionMode: "acceptEdits" };
|
|
92
85
|
}
|
|
93
86
|
}
|
|
87
|
+
else if (state.selectedOption === "bypass") {
|
|
88
|
+
decision = {
|
|
89
|
+
behavior: "allow",
|
|
90
|
+
newPermissionMode: "bypassPermissions",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
94
93
|
else if (state.alternativeText.trim()) {
|
|
95
94
|
decision = {
|
|
96
95
|
behavior: "deny",
|
|
@@ -123,11 +122,11 @@ export function confirmationReducer(state, action) {
|
|
|
123
122
|
}
|
|
124
123
|
}
|
|
125
124
|
const availableOptions = [];
|
|
126
|
-
if (toolName === EXIT_PLAN_MODE_TOOL_NAME)
|
|
127
|
-
availableOptions.push("clear");
|
|
128
125
|
availableOptions.push("allow");
|
|
129
126
|
if (!hidePersistentOption)
|
|
130
127
|
availableOptions.push("auto");
|
|
128
|
+
if (toolName === BASH_TOOL_NAME || toolName === EXIT_PLAN_MODE_TOOL_NAME)
|
|
129
|
+
availableOptions.push("bypass");
|
|
131
130
|
availableOptions.push("alternative");
|
|
132
131
|
if (key.upArrow) {
|
|
133
132
|
const currentIndex = availableOptions.indexOf(state.selectedOption);
|
|
@@ -33,6 +33,9 @@ export declare class AgentBridge {
|
|
|
33
33
|
private destroy;
|
|
34
34
|
private restoreSession;
|
|
35
35
|
private listSessions;
|
|
36
|
+
private listGitBranches;
|
|
37
|
+
private createWorktreeSession;
|
|
38
|
+
private removeWorktreeSession;
|
|
36
39
|
private getSessionInfo;
|
|
37
40
|
private updateConfig;
|
|
38
41
|
private sendMessage;
|
|
@@ -40,11 +43,17 @@ export declare class AgentBridge {
|
|
|
40
43
|
private abortMessage;
|
|
41
44
|
private clearMessages;
|
|
42
45
|
private rewindToMessage;
|
|
46
|
+
private listRewindCheckpoints;
|
|
43
47
|
private deleteQueuedMessage;
|
|
44
48
|
private updateQueuedMessage;
|
|
45
49
|
private deleteQueuedMessageById;
|
|
46
50
|
private getMessages;
|
|
47
51
|
private getFullMessageThread;
|
|
52
|
+
private compact;
|
|
53
|
+
private getBackgroundTaskOutput;
|
|
54
|
+
private stopBackgroundTask;
|
|
55
|
+
private getWorkflowRuns;
|
|
56
|
+
private stopWorkflowRun;
|
|
48
57
|
private setPermissionMode;
|
|
49
58
|
private getPermissionMode;
|
|
50
59
|
private getMcpServers;
|
|
@@ -64,6 +73,8 @@ export declare class AgentBridge {
|
|
|
64
73
|
private uninstallPlugin;
|
|
65
74
|
private enablePlugin;
|
|
66
75
|
private disablePlugin;
|
|
76
|
+
private getProjectSettings;
|
|
77
|
+
private setBuiltinPluginEnabled;
|
|
67
78
|
private updatePlugin;
|
|
68
79
|
private listMarketplaces;
|
|
69
80
|
private addMarketplace;
|
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
* - Implement the canUseTool permission flow over the stdio protocol
|
|
15
15
|
* - Handle config updates by destroying and recreating the Agent
|
|
16
16
|
*/
|
|
17
|
-
import { Agent, listSessions, searchFiles, PromptHistoryManager, AuthService, PluginCore, } from "wave-agent-sdk";
|
|
17
|
+
import { Agent, listSessions, searchFiles, generateRandomName, getDefaultRemoteBranch, getMessageContent, PromptHistoryManager, AuthService, PluginCore, } from "wave-agent-sdk";
|
|
18
18
|
import { 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 { createWorktree, removeWorktree } from "../utils/worktree.js";
|
|
19
21
|
import { logger } from "../utils/logger.js";
|
|
20
22
|
export class AgentBridge {
|
|
21
23
|
constructor(options) {
|
|
@@ -52,6 +54,8 @@ export class AgentBridge {
|
|
|
52
54
|
return this.clearMessages(sessionId);
|
|
53
55
|
case "rewindToMessage":
|
|
54
56
|
return this.rewindToMessage(p.messageId, sessionId);
|
|
57
|
+
case "listRewindCheckpoints":
|
|
58
|
+
return this.listRewindCheckpoints(sessionId);
|
|
55
59
|
case "deleteQueuedMessage":
|
|
56
60
|
return this.deleteQueuedMessage(p.index, sessionId);
|
|
57
61
|
case "updateQueuedMessage":
|
|
@@ -102,6 +106,10 @@ export class AgentBridge {
|
|
|
102
106
|
return this.enablePlugin(p.pluginId, p.scope, p.workdir, sessionId);
|
|
103
107
|
case "disablePlugin":
|
|
104
108
|
return this.disablePlugin(p.pluginId, p.scope, p.workdir, sessionId);
|
|
109
|
+
case "getProjectSettings":
|
|
110
|
+
return this.getProjectSettings(p.workdir, sessionId);
|
|
111
|
+
case "setBuiltinPluginEnabled":
|
|
112
|
+
return this.setBuiltinPluginEnabled(p.pluginId, p.enabled, p.scope, p.workdir, sessionId);
|
|
105
113
|
case "updatePlugin":
|
|
106
114
|
return this.updatePlugin(p.pluginId, p.workdir, sessionId);
|
|
107
115
|
case "listMarketplaces":
|
|
@@ -112,6 +120,24 @@ export class AgentBridge {
|
|
|
112
120
|
return this.removeMarketplace(p.name, p.scope, p.workdir, sessionId);
|
|
113
121
|
case "updateMarketplace":
|
|
114
122
|
return this.updateMarketplace(p.name, p.workdir, sessionId);
|
|
123
|
+
case "compact":
|
|
124
|
+
return this.compact(p.customInstructions, sessionId);
|
|
125
|
+
// ── Background tasks ──
|
|
126
|
+
case "getBackgroundTaskOutput":
|
|
127
|
+
return this.getBackgroundTaskOutput(p.taskId, sessionId);
|
|
128
|
+
case "stopBackgroundTask":
|
|
129
|
+
return this.stopBackgroundTask(p.taskId, sessionId);
|
|
130
|
+
case "getWorkflowRuns":
|
|
131
|
+
return this.getWorkflowRuns(sessionId);
|
|
132
|
+
case "stopWorkflowRun":
|
|
133
|
+
return this.stopWorkflowRun(p.runId, sessionId);
|
|
134
|
+
// ── Git / worktree (global — no session required) ──
|
|
135
|
+
case "listGitBranches":
|
|
136
|
+
return this.listGitBranches(p.workdir);
|
|
137
|
+
case "createWorktree":
|
|
138
|
+
return this.createWorktreeSession(p);
|
|
139
|
+
case "removeWorktree":
|
|
140
|
+
return this.removeWorktreeSession(p);
|
|
115
141
|
default:
|
|
116
142
|
throw new RpcError(PROTOCOL_METHOD_NOT_FOUND, `Method not found: ${method}`);
|
|
117
143
|
}
|
|
@@ -148,6 +174,8 @@ export class AgentBridge {
|
|
|
148
174
|
disallowedTools: params.disallowedTools,
|
|
149
175
|
plugins: params.pluginDirs?.map((path) => ({ type: "local", path })),
|
|
150
176
|
mcpServers: params.mcpServers,
|
|
177
|
+
worktreeName: params.worktreeName,
|
|
178
|
+
isNewWorktree: params.isNewWorktree,
|
|
151
179
|
canUseTool: (context) => this.canUseTool(context, ctx),
|
|
152
180
|
};
|
|
153
181
|
const agent = await Agent.create(options);
|
|
@@ -183,6 +211,76 @@ export class AgentBridge {
|
|
|
183
211
|
const sessions = await listSessions(workdir || this.getSessionWorkdir(sessionId) || process.cwd());
|
|
184
212
|
return { sessions };
|
|
185
213
|
}
|
|
214
|
+
// ── Git / worktree ────────────────────────────────────────────
|
|
215
|
+
listGitBranches(workdir) {
|
|
216
|
+
if (!workdir) {
|
|
217
|
+
throw new RpcError(PROTOCOL_INTERNAL_ERROR, "workdir is required");
|
|
218
|
+
}
|
|
219
|
+
const gitOpts = {
|
|
220
|
+
cwd: workdir,
|
|
221
|
+
encoding: "utf8",
|
|
222
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
223
|
+
};
|
|
224
|
+
let branchesRaw;
|
|
225
|
+
try {
|
|
226
|
+
branchesRaw = execFileSync("git", ["for-each-ref", "--format=%(refname:short)", "refs/heads"], gitOpts).trim();
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
throw new RpcError(PROTOCOL_INTERNAL_ERROR, `Not a git repository (or git unavailable): ${workdir}`);
|
|
230
|
+
}
|
|
231
|
+
const branches = branchesRaw
|
|
232
|
+
? branchesRaw
|
|
233
|
+
.split("\n")
|
|
234
|
+
.map((b) => b.trim())
|
|
235
|
+
.filter(Boolean)
|
|
236
|
+
: [];
|
|
237
|
+
let current = null;
|
|
238
|
+
try {
|
|
239
|
+
const head = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], gitOpts).trim();
|
|
240
|
+
// Detached HEAD prints "HEAD" — treat as no current branch.
|
|
241
|
+
current = head && head !== "HEAD" ? head : null;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
current = null;
|
|
245
|
+
}
|
|
246
|
+
return { branches, current };
|
|
247
|
+
}
|
|
248
|
+
async createWorktreeSession(params) {
|
|
249
|
+
if (!params.workdir) {
|
|
250
|
+
throw new RpcError(PROTOCOL_INTERNAL_ERROR, "workdir is required");
|
|
251
|
+
}
|
|
252
|
+
const name = params.name?.trim() || generateRandomName();
|
|
253
|
+
try {
|
|
254
|
+
const session = await createWorktree(name, params.workdir, {
|
|
255
|
+
baseBranch: params.baseBranch,
|
|
256
|
+
});
|
|
257
|
+
return {
|
|
258
|
+
name: session.name,
|
|
259
|
+
path: session.path,
|
|
260
|
+
branch: session.branch,
|
|
261
|
+
repoRoot: session.repoRoot,
|
|
262
|
+
baseBranch: params.baseBranch ?? getDefaultRemoteBranch(params.workdir),
|
|
263
|
+
isNew: session.isNew,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
catch (e) {
|
|
267
|
+
throw new RpcError(PROTOCOL_INTERNAL_ERROR, e.message);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async removeWorktreeSession(params) {
|
|
271
|
+
// removeWorktree is best-effort/idempotent: already-removed worktrees or
|
|
272
|
+
// branches only log, never throw.
|
|
273
|
+
await removeWorktree({
|
|
274
|
+
name: "",
|
|
275
|
+
path: params.path,
|
|
276
|
+
branch: params.branch,
|
|
277
|
+
repoRoot: params.repoRoot,
|
|
278
|
+
hasUncommittedChanges: false,
|
|
279
|
+
hasNewCommits: false,
|
|
280
|
+
isNew: false,
|
|
281
|
+
});
|
|
282
|
+
return { ok: true };
|
|
283
|
+
}
|
|
186
284
|
getSessionInfo(sessionId) {
|
|
187
285
|
const entry = this.requireSession(sessionId);
|
|
188
286
|
return {
|
|
@@ -222,6 +320,9 @@ export class AgentBridge {
|
|
|
222
320
|
path: p,
|
|
223
321
|
})),
|
|
224
322
|
mcpServers: entry.storedConfig.mcpServers,
|
|
323
|
+
// Keep worktree context (permission safety) across recreation, but never
|
|
324
|
+
// re-fire WorktreeCreate — the hook ran at initial creation.
|
|
325
|
+
worktreeName: entry.storedConfig.worktreeName,
|
|
225
326
|
canUseTool: (context) => this.canUseTool(context, ctx),
|
|
226
327
|
};
|
|
227
328
|
const agent = await Agent.create(options);
|
|
@@ -276,6 +377,17 @@ export class AgentBridge {
|
|
|
276
377
|
await entry.agent.truncateHistory(index);
|
|
277
378
|
return { inputContent: textBlock?.content || "" };
|
|
278
379
|
}
|
|
380
|
+
async listRewindCheckpoints(sessionId) {
|
|
381
|
+
const entry = this.requireSession(sessionId);
|
|
382
|
+
const { messages } = await entry.agent.getFullMessageThread();
|
|
383
|
+
const checkpoints = messages
|
|
384
|
+
.filter((m) => m.role === "user" && !m.isMeta && m.id)
|
|
385
|
+
.map((m) => ({
|
|
386
|
+
id: m.id,
|
|
387
|
+
content: getMessageContent(m).replace(/\s+/g, " ").trim(),
|
|
388
|
+
}));
|
|
389
|
+
return { checkpoints };
|
|
390
|
+
}
|
|
279
391
|
deleteQueuedMessage(index, sessionId) {
|
|
280
392
|
const entry = this.requireSession(sessionId);
|
|
281
393
|
entry.agent.removeQueuedMessage(index);
|
|
@@ -302,6 +414,37 @@ export class AgentBridge {
|
|
|
302
414
|
const entry = this.requireSession(sessionId);
|
|
303
415
|
return entry.agent.getFullMessageThread();
|
|
304
416
|
}
|
|
417
|
+
async compact(customInstructions, sessionId) {
|
|
418
|
+
const entry = this.requireSession(sessionId);
|
|
419
|
+
await entry.agent.compact(customInstructions);
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
// ── Background tasks ──
|
|
423
|
+
getBackgroundTaskOutput(taskId, sessionId) {
|
|
424
|
+
const entry = this.requireSession(sessionId);
|
|
425
|
+
return { output: entry.agent.getBackgroundTaskOutput(taskId) };
|
|
426
|
+
}
|
|
427
|
+
stopBackgroundTask(taskId, sessionId) {
|
|
428
|
+
const entry = this.requireSession(sessionId);
|
|
429
|
+
const success = entry.agent.stopBackgroundTask(taskId);
|
|
430
|
+
return { success };
|
|
431
|
+
}
|
|
432
|
+
async getWorkflowRuns(sessionId) {
|
|
433
|
+
const entry = this.requireSession(sessionId);
|
|
434
|
+
const runs = await entry.agent.getWorkflowRuns();
|
|
435
|
+
return {
|
|
436
|
+
runs: runs.map((r) => {
|
|
437
|
+
const { completionPromise, ...rest } = r;
|
|
438
|
+
void completionPromise;
|
|
439
|
+
return rest;
|
|
440
|
+
}),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
stopWorkflowRun(runId, sessionId) {
|
|
444
|
+
const entry = this.requireSession(sessionId);
|
|
445
|
+
entry.agent.stopWorkflowRun(runId);
|
|
446
|
+
return { success: true };
|
|
447
|
+
}
|
|
305
448
|
// ── Permissions ───────────────────────────────────────────────
|
|
306
449
|
async setPermissionMode(mode, sessionId) {
|
|
307
450
|
const entry = this.requireSession(sessionId);
|
|
@@ -425,6 +568,22 @@ export class AgentBridge {
|
|
|
425
568
|
async disablePlugin(pluginId, scope, workdir, sessionId) {
|
|
426
569
|
return this.getPluginCore(workdir, sessionId).disablePlugin(pluginId, scope);
|
|
427
570
|
}
|
|
571
|
+
async getProjectSettings(workdir, sessionId) {
|
|
572
|
+
return {
|
|
573
|
+
enabledPlugins: this.getPluginCore(workdir, sessionId).getMergedEnabledPlugins(),
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
async setBuiltinPluginEnabled(pluginId, enabled, scope, workdir, sessionId) {
|
|
577
|
+
const core = this.getPluginCore(workdir, sessionId);
|
|
578
|
+
const targetScope = scope ?? "project";
|
|
579
|
+
if (enabled) {
|
|
580
|
+
await core.enablePlugin(pluginId, targetScope);
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
await core.disablePlugin(pluginId, targetScope);
|
|
584
|
+
}
|
|
585
|
+
return { enabledPlugins: core.getMergedEnabledPlugins() };
|
|
586
|
+
}
|
|
428
587
|
async updatePlugin(pluginId, workdir, sessionId) {
|
|
429
588
|
return this.getPluginCore(workdir, sessionId).updatePlugin(pluginId);
|
|
430
589
|
}
|
|
@@ -485,6 +644,21 @@ export class AgentBridge {
|
|
|
485
644
|
onTasksChange: (tasks) => {
|
|
486
645
|
this.emit("tasksChange", { tasks }, ctx.registeredSessionId);
|
|
487
646
|
},
|
|
647
|
+
onBackgroundTasksChange: (tasks) => {
|
|
648
|
+
const summaries = tasks.map((t) => ({
|
|
649
|
+
id: t.id,
|
|
650
|
+
type: t.type,
|
|
651
|
+
status: t.status,
|
|
652
|
+
startTime: t.startTime,
|
|
653
|
+
endTime: t.endTime,
|
|
654
|
+
command: t.command,
|
|
655
|
+
description: t.description,
|
|
656
|
+
exitCode: t.exitCode,
|
|
657
|
+
runtime: t.runtime,
|
|
658
|
+
outputPath: t.outputPath,
|
|
659
|
+
}));
|
|
660
|
+
this.emit("backgroundTasksChange", { tasks: summaries }, ctx.registeredSessionId);
|
|
661
|
+
},
|
|
488
662
|
onSessionIdChange: (newSessionId) => {
|
|
489
663
|
const oldSessionId = ctx.registeredSessionId;
|
|
490
664
|
// Emit with the OLD sessionId so the client's router can deliver it
|
|
@@ -526,6 +700,9 @@ export class AgentBridge {
|
|
|
526
700
|
onCompactBlockAdded: (content) => {
|
|
527
701
|
this.emit("compactBlockAdded", { content }, ctx.registeredSessionId);
|
|
528
702
|
},
|
|
703
|
+
onCompactionStateChange: (isCompacting) => {
|
|
704
|
+
this.emit("compactionStateChange", { isCompacting }, ctx.registeredSessionId);
|
|
705
|
+
},
|
|
529
706
|
};
|
|
530
707
|
}
|
|
531
708
|
findLastUserMessage(agent) {
|
package/dist/stdio/protocol.d.ts
CHANGED
|
@@ -34,8 +34,8 @@ export declare const INVALID_REQUEST = -32600;
|
|
|
34
34
|
export declare const METHOD_NOT_FOUND = -32601;
|
|
35
35
|
export declare const INVALID_PARAMS = -32602;
|
|
36
36
|
export declare const INTERNAL_ERROR = -32603;
|
|
37
|
-
export type RequestMethod = "initialize" | "destroy" | "restoreSession" | "listSessions" | "getSessionInfo" | "sendMessage" | "bang" | "abortMessage" | "clearMessages" | "rewindToMessage" | "deleteQueuedMessage" | "updateQueuedMessage" | "deleteQueuedMessageById" | "getMessages" | "getFullMessageThread" | "setPermissionMode" | "getPermissionMode" | "getMcpServers" | "connectMcpServer" | "disconnectMcpServer" | "getSlashCommands" | "searchFiles" | "getPromptHistory" | "searchPromptHistory" | "updateConfig" | "getAuthStatus" | "login" | "logout" | "listPlugins" | "installPlugin" | "uninstallPlugin" | "enablePlugin" | "disablePlugin" | "updatePlugin" | "listMarketplaces" | "addMarketplace" | "removeMarketplace" | "updateMarketplace";
|
|
37
|
+
export type RequestMethod = "initialize" | "destroy" | "restoreSession" | "listSessions" | "getSessionInfo" | "sendMessage" | "bang" | "abortMessage" | "clearMessages" | "rewindToMessage" | "listRewindCheckpoints" | "deleteQueuedMessage" | "updateQueuedMessage" | "deleteQueuedMessageById" | "getMessages" | "getFullMessageThread" | "setPermissionMode" | "getPermissionMode" | "getMcpServers" | "connectMcpServer" | "disconnectMcpServer" | "getSlashCommands" | "searchFiles" | "getPromptHistory" | "searchPromptHistory" | "updateConfig" | "getAuthStatus" | "login" | "logout" | "listPlugins" | "installPlugin" | "uninstallPlugin" | "enablePlugin" | "disablePlugin" | "updatePlugin" | "listMarketplaces" | "addMarketplace" | "removeMarketplace" | "updateMarketplace" | "compact" | "getBackgroundTaskOutput" | "stopBackgroundTask" | "getWorkflowRuns" | "stopWorkflowRun" | "listGitBranches" | "createWorktree" | "removeWorktree";
|
|
38
38
|
export type ClientNotificationMethod = "permissionResponse";
|
|
39
|
-
export type ServerNotificationMethod = "messagesChange" | "userMessageAdded" | "assistantMessageAdded" | "assistantContentUpdated" | "assistantReasoningUpdated" | "toolBlockUpdated" | "errorBlockAdded" | "loadingChange" | "commandRunningChange" | "queuedMessagesChange" | "tasksChange" | "sessionIdChange" | "permissionModeChange" | "mcpServersChange" | "workdirChange" | "bangMessageAdded" | "bangMessageUpdated" | "bangMessageCompleted" | "notificationMessageAdded" | "permissionRequest" | "authUrl" | "compactBlockAdded";
|
|
39
|
+
export type ServerNotificationMethod = "messagesChange" | "userMessageAdded" | "assistantMessageAdded" | "assistantContentUpdated" | "assistantReasoningUpdated" | "toolBlockUpdated" | "errorBlockAdded" | "loadingChange" | "commandRunningChange" | "queuedMessagesChange" | "tasksChange" | "sessionIdChange" | "permissionModeChange" | "mcpServersChange" | "workdirChange" | "bangMessageAdded" | "bangMessageUpdated" | "bangMessageCompleted" | "notificationMessageAdded" | "permissionRequest" | "authUrl" | "compactBlockAdded" | "compactionStateChange" | "backgroundTasksChange";
|
|
40
40
|
export declare function isRequest(msg: unknown): msg is JsonRpcRequest;
|
|
41
41
|
export declare function isNotification(msg: unknown): msg is JsonRpcNotification;
|
package/dist/utils/worktree.d.ts
CHANGED
|
@@ -11,11 +11,17 @@ export interface WorktreeSession {
|
|
|
11
11
|
* Create a new git worktree
|
|
12
12
|
* @param name Worktree name
|
|
13
13
|
* @param cwd Current working directory
|
|
14
|
+
* @param options Optional creation options
|
|
15
|
+
* @param options.baseRef "fresh" (default, origin/<default-branch>) | "head" (local HEAD)
|
|
16
|
+
* @param options.baseBranch Explicit base branch (overrides baseRef)
|
|
14
17
|
* @returns Worktree session details
|
|
15
18
|
*/
|
|
16
|
-
export declare function createWorktree(name: string, cwd: string
|
|
19
|
+
export declare function createWorktree(name: string, cwd: string, options?: {
|
|
20
|
+
baseRef?: "fresh" | "head";
|
|
21
|
+
baseBranch?: string;
|
|
22
|
+
}): Promise<WorktreeSession>;
|
|
17
23
|
/**
|
|
18
24
|
* Remove a git worktree and its associated branch
|
|
19
25
|
* @param session Worktree session details
|
|
20
26
|
*/
|
|
21
|
-
export declare function removeWorktree(session: WorktreeSession): void
|
|
27
|
+
export declare function removeWorktree(session: WorktreeSession): Promise<void>;
|