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 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: toolName === EXIT_PLAN_MODE_TOOL_NAME ? "clear" : "allow",
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: [toolName === EXIT_PLAN_MODE_TOOL_NAME && (_jsx(Box, { children: _jsxs(Text, { color: state.selectedOption === "clear" ? "black" : "white", backgroundColor: state.selectedOption === "clear" ? "yellow" : undefined, bold: state.selectedOption === "clear", children: [state.selectedOption === "clear" ? "> " : " ", "Yes, clear context and auto-accept edits"] }) }, "clear-option")), _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
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")), _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" }) })] }))] }));
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
- // If all tasks are already completed on mount (e.g. session restore),
85
- // start hidden immediately instead of flashing for 5 seconds.
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 (allCompleted && !autoHidden) {
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 || !isTaskListVisible || autoHidden) {
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);
@@ -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/047-worktree.md FR-042).
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
- worktreeSession = createWorktree(name, originalCwd);
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 and subagents to complete
108
- while (agent.hasRunningBackgroundWork) {
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: "clear" | "auto" | "allow" | "alternative";
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 === "clear") {
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) {
@@ -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;
@@ -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): WorktreeSession;
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>;