wave-code 0.19.8 → 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
  }
@@ -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);
package/dist/index.js CHANGED
@@ -237,7 +237,7 @@ export async function main() {
237
237
  name = generateRandomName();
238
238
  }
239
239
  const baseRef = loadMergedWaveConfig(originalCwd)?.worktree?.baseRef;
240
- worktreeSession = createWorktree(name, originalCwd, { baseRef });
240
+ worktreeSession = await createWorktree(name, originalCwd, { baseRef });
241
241
  // Note: the full worktree session (originalCwd etc.) is injected into the
242
242
  // agent's DI container after the agent is created in useChat.tsx. This keeps
243
243
  // worktree state per-session instead of process-global.
package/dist/print-cli.js CHANGED
@@ -138,7 +138,7 @@ export async function startPrintCli(options) {
138
138
  const hasChanges = hasUncommittedChanges(cwd);
139
139
  const hasCommits = hasNewCommits(cwd, baseBranch);
140
140
  if (!hasChanges && !hasCommits) {
141
- removeWorktree(worktreeSession);
141
+ await removeWorktree(worktreeSession);
142
142
  }
143
143
  else {
144
144
  process.stdout.write(`\n⚠️ Worktree '${worktreeSession.name}' has changes or commits. Keeping it at: ${worktreeSession.path}\n`);
@@ -170,7 +170,7 @@ export async function startPrintCli(options) {
170
170
  const hasChanges = hasUncommittedChanges(cwd);
171
171
  const hasCommits = hasNewCommits(cwd, baseBranch);
172
172
  if (!hasChanges && !hasCommits) {
173
- removeWorktree(worktreeSession);
173
+ await removeWorktree(worktreeSession);
174
174
  }
175
175
  else {
176
176
  process.stdout.write(`\n⚠️ Worktree '${worktreeSession.name}' has changes or commits. Keeping it at: ${worktreeSession.path}\n`);
@@ -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,6 +43,7 @@ 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;
@@ -69,6 +73,8 @@ export declare class AgentBridge {
69
73
  private uninstallPlugin;
70
74
  private enablePlugin;
71
75
  private disablePlugin;
76
+ private getProjectSettings;
77
+ private setBuiltinPluginEnabled;
72
78
  private updatePlugin;
73
79
  private listMarketplaces;
74
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":
@@ -123,6 +131,13 @@ export class AgentBridge {
123
131
  return this.getWorkflowRuns(sessionId);
124
132
  case "stopWorkflowRun":
125
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);
126
141
  default:
127
142
  throw new RpcError(PROTOCOL_METHOD_NOT_FOUND, `Method not found: ${method}`);
128
143
  }
@@ -159,6 +174,8 @@ export class AgentBridge {
159
174
  disallowedTools: params.disallowedTools,
160
175
  plugins: params.pluginDirs?.map((path) => ({ type: "local", path })),
161
176
  mcpServers: params.mcpServers,
177
+ worktreeName: params.worktreeName,
178
+ isNewWorktree: params.isNewWorktree,
162
179
  canUseTool: (context) => this.canUseTool(context, ctx),
163
180
  };
164
181
  const agent = await Agent.create(options);
@@ -194,6 +211,76 @@ export class AgentBridge {
194
211
  const sessions = await listSessions(workdir || this.getSessionWorkdir(sessionId) || process.cwd());
195
212
  return { sessions };
196
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
+ }
197
284
  getSessionInfo(sessionId) {
198
285
  const entry = this.requireSession(sessionId);
199
286
  return {
@@ -233,6 +320,9 @@ export class AgentBridge {
233
320
  path: p,
234
321
  })),
235
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,
236
326
  canUseTool: (context) => this.canUseTool(context, ctx),
237
327
  };
238
328
  const agent = await Agent.create(options);
@@ -287,6 +377,17 @@ export class AgentBridge {
287
377
  await entry.agent.truncateHistory(index);
288
378
  return { inputContent: textBlock?.content || "" };
289
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
+ }
290
391
  deleteQueuedMessage(index, sessionId) {
291
392
  const entry = this.requireSession(sessionId);
292
393
  entry.agent.removeQueuedMessage(index);
@@ -467,6 +568,22 @@ export class AgentBridge {
467
568
  async disablePlugin(pluginId, scope, workdir, sessionId) {
468
569
  return this.getPluginCore(workdir, sessionId).disablePlugin(pluginId, scope);
469
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
+ }
470
587
  async updatePlugin(pluginId, workdir, sessionId) {
471
588
  return this.getPluginCore(workdir, sessionId).updatePlugin(pluginId);
472
589
  }
@@ -34,7 +34,7 @@ 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" | "compact" | "getBackgroundTaskOutput" | "stopBackgroundTask" | "getWorkflowRuns" | "stopWorkflowRun";
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
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;
@@ -13,13 +13,15 @@ export interface WorktreeSession {
13
13
  * @param cwd Current working directory
14
14
  * @param options Optional creation options
15
15
  * @param options.baseRef "fresh" (default, origin/<default-branch>) | "head" (local HEAD)
16
+ * @param options.baseBranch Explicit base branch (overrides baseRef)
16
17
  * @returns Worktree session details
17
18
  */
18
19
  export declare function createWorktree(name: string, cwd: string, options?: {
19
20
  baseRef?: "fresh" | "head";
20
- }): WorktreeSession;
21
+ baseBranch?: string;
22
+ }): Promise<WorktreeSession>;
21
23
  /**
22
24
  * Remove a git worktree and its associated branch
23
25
  * @param session Worktree session details
24
26
  */
25
- export declare function removeWorktree(session: WorktreeSession): void;
27
+ export declare function removeWorktree(session: WorktreeSession): Promise<void>;
@@ -1,21 +1,27 @@
1
- import { execFileSync } from "node:child_process";
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
2
3
  import * as path from "node:path";
3
4
  import * as fs from "node:fs";
4
5
  import { getDefaultRemoteBranch, getGitMainRepoRoot } from "wave-agent-sdk";
6
+ // Never use execFileSync here: the shared `wave --stdio` process handles all
7
+ // desktop sessions, so a synchronous git call (especially a multi-second
8
+ // recursive worktree delete or a network fetch) freezes every session.
9
+ const execFileAsync = promisify(execFile);
5
10
  /**
6
11
  * Create a new git worktree
7
12
  * @param name Worktree name
8
13
  * @param cwd Current working directory
9
14
  * @param options Optional creation options
10
15
  * @param options.baseRef "fresh" (default, origin/<default-branch>) | "head" (local HEAD)
16
+ * @param options.baseBranch Explicit base branch (overrides baseRef)
11
17
  * @returns Worktree session details
12
18
  */
13
- export function createWorktree(name, cwd, options) {
19
+ export async function createWorktree(name, cwd, options) {
14
20
  const repoRoot = getGitMainRepoRoot(cwd);
15
21
  const worktreePath = path.join(repoRoot, ".wave", "worktrees", name);
16
22
  const branchName = `worktree-${name}`;
17
23
  const useHead = options?.baseRef === "head";
18
- const baseBranch = useHead ? "HEAD" : getDefaultRemoteBranch(cwd);
24
+ const resolvedBaseBranch = options?.baseBranch ?? (useHead ? "HEAD" : getDefaultRemoteBranch(cwd));
19
25
  // Ensure parent directory exists
20
26
  const parentDir = path.dirname(worktreePath);
21
27
  if (!fs.existsSync(parentDir)) {
@@ -36,9 +42,8 @@ export function createWorktree(name, cwd, options) {
36
42
  }
37
43
  try {
38
44
  // Create worktree and branch
39
- execFileSync("git", ["worktree", "add", "-b", branchName, worktreePath, baseBranch], {
45
+ await execFileAsync("git", ["worktree", "add", "-b", branchName, worktreePath, resolvedBaseBranch], {
40
46
  cwd: repoRoot,
41
- stdio: ["ignore", "pipe", "pipe"],
42
47
  });
43
48
  return {
44
49
  name,
@@ -55,9 +60,8 @@ export function createWorktree(name, cwd, options) {
55
60
  if (stderr.includes("already exists")) {
56
61
  // If branch already exists, try to add worktree without -b
57
62
  try {
58
- execFileSync("git", ["worktree", "add", worktreePath, branchName], {
63
+ await execFileAsync("git", ["worktree", "add", worktreePath, branchName], {
59
64
  cwd: repoRoot,
60
- stdio: ["ignore", "pipe", "pipe"],
61
65
  });
62
66
  return {
63
67
  name,
@@ -77,15 +81,20 @@ export function createWorktree(name, cwd, options) {
77
81
  (stderr.includes("not a valid object name") ||
78
82
  stderr.includes("unknown revision"))) {
79
83
  // Base branch not fetched yet — try fetching then retrying
80
- const branchNameOnly = baseBranch.split("/").pop();
84
+ const branchNameOnly = resolvedBaseBranch.split("/").pop();
81
85
  try {
82
- execFileSync("git", ["fetch", "origin", branchNameOnly], {
86
+ await execFileAsync("git", ["fetch", "origin", branchNameOnly], {
83
87
  cwd: repoRoot,
84
- stdio: ["ignore", "pipe", "pipe"],
85
88
  });
86
- execFileSync("git", ["worktree", "add", "-b", branchName, worktreePath, baseBranch], {
89
+ await execFileAsync("git", [
90
+ "worktree",
91
+ "add",
92
+ "-b",
93
+ branchName,
94
+ worktreePath,
95
+ resolvedBaseBranch,
96
+ ], {
87
97
  cwd: repoRoot,
88
- stdio: ["ignore", "pipe", "pipe"],
89
98
  });
90
99
  return {
91
100
  name,
@@ -100,9 +109,8 @@ export function createWorktree(name, cwd, options) {
100
109
  catch {
101
110
  // Fetch or retry failed — fall back to HEAD
102
111
  try {
103
- execFileSync("git", ["worktree", "add", "-b", branchName, worktreePath, "HEAD"], {
112
+ await execFileAsync("git", ["worktree", "add", "-b", branchName, worktreePath, "HEAD"], {
104
113
  cwd: repoRoot,
105
- stdio: ["ignore", "pipe", "pipe"],
106
114
  });
107
115
  return {
108
116
  name,
@@ -126,31 +134,29 @@ export function createWorktree(name, cwd, options) {
126
134
  * Remove a git worktree and its associated branch
127
135
  * @param session Worktree session details
128
136
  */
129
- export function removeWorktree(session) {
137
+ export async function removeWorktree(session) {
130
138
  const repoRoot = session.repoRoot;
131
139
  try {
132
140
  // Get current branch in worktree before removing it
133
141
  let currentBranch;
134
142
  try {
135
- currentBranch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
143
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
136
144
  cwd: session.path,
137
145
  encoding: "utf8",
138
- stdio: ["ignore", "pipe", "ignore"],
139
- }).trim();
146
+ });
147
+ currentBranch = stdout.trim();
140
148
  }
141
149
  catch {
142
150
  // Ignore errors getting current branch
143
151
  }
144
152
  // Remove worktree
145
- execFileSync("git", ["worktree", "remove", "--force", session.path], {
153
+ await execFileAsync("git", ["worktree", "remove", "--force", session.path], {
146
154
  cwd: repoRoot,
147
- stdio: ["ignore", "pipe", "pipe"],
148
155
  });
149
156
  // Delete original branch
150
157
  try {
151
- execFileSync("git", ["branch", "-D", session.branch], {
158
+ await execFileAsync("git", ["branch", "-D", session.branch], {
152
159
  cwd: repoRoot,
153
- stdio: ["ignore", "pipe", "pipe"],
154
160
  });
155
161
  }
156
162
  catch {
@@ -166,9 +172,8 @@ export function removeWorktree(session) {
166
172
  currentBranch !== "main" &&
167
173
  currentBranch !== "master") {
168
174
  try {
169
- execFileSync("git", ["branch", "-D", currentBranch], {
175
+ await execFileAsync("git", ["branch", "-D", currentBranch], {
170
176
  cwd: repoRoot,
171
- stdio: ["ignore", "pipe", "pipe"],
172
177
  });
173
178
  }
174
179
  catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-code",
3
- "version": "0.19.8",
3
+ "version": "0.19.9",
4
4
  "description": "CLI-based code assistant powered by AI, built with React and Ink",
5
5
  "repository": {
6
6
  "type": "git",
@@ -41,7 +41,7 @@
41
41
  "semver": "^7.7.4",
42
42
  "yargs": "^17.7.2",
43
43
  "zod": "^3.23.8",
44
- "wave-agent-sdk": "0.19.8"
44
+ "wave-agent-sdk": "0.19.9"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/react": "^19.1.8",
package/src/cli.tsx CHANGED
@@ -70,7 +70,7 @@ export async function startCli(options: CliOptions): Promise<void> {
70
70
  // Cleanup worktree if requested
71
71
  if (shouldRemoveWorktree && worktreeSession) {
72
72
  process.chdir(worktreeSession.repoRoot);
73
- removeWorktree(worktreeSession);
73
+ await removeWorktree(worktreeSession);
74
74
  }
75
75
 
76
76
  process.exit(0);
@@ -99,12 +99,9 @@ export const TaskList: React.FC = () => {
99
99
  const autoHideTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(
100
100
  null,
101
101
  );
102
- const [autoHidden, setAutoHidden] = React.useState(() => {
103
- // If all tasks are already completed on mount (e.g. session restore),
104
- // start hidden immediately instead of flashing for 5 seconds.
105
- const active = tasks.filter((t) => t.status !== "deleted");
106
- return active.length > 0 && active.every((t) => t.status === "completed");
107
- });
102
+ const [autoHidden, setAutoHidden] = React.useState(false);
103
+ // 是否在当前展示期间观察到过未完成任务;用于区分“任务在眼前完成”与“加载时已全部完成”
104
+ const hadIncompleteRef = React.useRef(false);
108
105
  const [, forceUpdate] = React.useState(0);
109
106
 
110
107
  const now = Date.now();
@@ -155,24 +152,45 @@ export const TaskList: React.FC = () => {
155
152
  activeTasks.length > 0 &&
156
153
  activeTasks.every((t) => t.status === "completed");
157
154
 
155
+ // 观察到任务从“存在未完成”变为“全部完成”时保留 5 秒再隐藏;
156
+ // 加载或恢复到已全部完成的会话时立即隐藏,避免先闪现再消失
158
157
  React.useEffect(() => {
159
- if (allCompleted && !autoHidden) {
158
+ if (activeTasks.length === 0) {
159
+ hadIncompleteRef.current = false;
160
+ return;
161
+ }
162
+ if (!allCompleted) {
163
+ hadIncompleteRef.current = true;
164
+ if (autoHidden) {
165
+ setAutoHidden(false);
166
+ }
167
+ return;
168
+ }
169
+ if (!hadIncompleteRef.current) {
170
+ if (!autoHidden) {
171
+ setAutoHidden(true);
172
+ }
173
+ return;
174
+ }
175
+ if (!autoHidden) {
160
176
  autoHideTimerRef.current = setTimeout(() => {
161
177
  setAutoHidden(true);
162
178
  }, AUTO_HIDE_DELAY_MS);
163
179
  }
164
- if (!allCompleted && autoHidden) {
165
- setAutoHidden(false);
166
- }
167
180
  return () => {
168
181
  if (autoHideTimerRef.current) {
169
182
  clearTimeout(autoHideTimerRef.current);
170
183
  autoHideTimerRef.current = null;
171
184
  }
172
185
  };
173
- }, [allCompleted, autoHidden]);
186
+ }, [allCompleted, autoHidden, activeTasks.length]);
174
187
 
175
- if (tasks.length === 0 || !isTaskListVisible || autoHidden) {
188
+ if (
189
+ tasks.length === 0 ||
190
+ !isTaskListVisible ||
191
+ autoHidden ||
192
+ (allCompleted && !hadIncompleteRef.current)
193
+ ) {
176
194
  return null;
177
195
  }
178
196
 
package/src/index.ts CHANGED
@@ -326,7 +326,7 @@ export async function main() {
326
326
  name = generateRandomName();
327
327
  }
328
328
  const baseRef = loadMergedWaveConfig(originalCwd)?.worktree?.baseRef;
329
- worktreeSession = createWorktree(name, originalCwd, { baseRef });
329
+ worktreeSession = await createWorktree(name, originalCwd, { baseRef });
330
330
 
331
331
  // Note: the full worktree session (originalCwd etc.) is injected into the
332
332
  // agent's DI container after the agent is created in useChat.tsx. This keeps
package/src/print-cli.ts CHANGED
@@ -194,7 +194,7 @@ export async function startPrintCli(options: PrintCliOptions): Promise<void> {
194
194
  const hasCommits = hasNewCommits(cwd, baseBranch);
195
195
 
196
196
  if (!hasChanges && !hasCommits) {
197
- removeWorktree(worktreeSession);
197
+ await removeWorktree(worktreeSession);
198
198
  } else {
199
199
  process.stdout.write(
200
200
  `\n⚠️ Worktree '${worktreeSession.name}' has changes or commits. Keeping it at: ${worktreeSession.path}\n`,
@@ -230,7 +230,7 @@ export async function startPrintCli(options: PrintCliOptions): Promise<void> {
230
230
  const hasCommits = hasNewCommits(cwd, baseBranch);
231
231
 
232
232
  if (!hasChanges && !hasCommits) {
233
- removeWorktree(worktreeSession);
233
+ await removeWorktree(worktreeSession);
234
234
  } else {
235
235
  process.stdout.write(
236
236
  `\n⚠️ Worktree '${worktreeSession.name}' has changes or commits. Keeping it at: ${worktreeSession.path}\n`,
@@ -34,6 +34,9 @@ import {
34
34
  type Scope,
35
35
  listSessions,
36
36
  searchFiles,
37
+ generateRandomName,
38
+ getDefaultRemoteBranch,
39
+ getMessageContent,
37
40
  PromptHistoryManager,
38
41
  AuthService,
39
42
  PluginCore,
@@ -44,6 +47,8 @@ import {
44
47
  INTERNAL_ERROR as PROTOCOL_INTERNAL_ERROR,
45
48
  METHOD_NOT_FOUND as PROTOCOL_METHOD_NOT_FOUND,
46
49
  } from "./protocol.js";
50
+ import { execFileSync } from "node:child_process";
51
+ import { createWorktree, removeWorktree } from "../utils/worktree.js";
47
52
  import { logger } from "../utils/logger.js";
48
53
 
49
54
  export type NotificationEmitter = (
@@ -72,6 +77,8 @@ interface InitializeParams {
72
77
  disallowedTools?: string[];
73
78
  pluginDirs?: string[];
74
79
  mcpServers?: Record<string, McpServerConfig>;
80
+ worktreeName?: string;
81
+ isNewWorktree?: boolean;
75
82
  }
76
83
 
77
84
  interface UpdateConfigParams {
@@ -163,6 +170,8 @@ export class AgentBridge {
163
170
  return this.clearMessages(sessionId);
164
171
  case "rewindToMessage":
165
172
  return this.rewindToMessage(p.messageId as string, sessionId);
173
+ case "listRewindCheckpoints":
174
+ return this.listRewindCheckpoints(sessionId);
166
175
  case "deleteQueuedMessage":
167
176
  return this.deleteQueuedMessage(p.index as number, sessionId);
168
177
  case "updateQueuedMessage":
@@ -250,6 +259,19 @@ export class AgentBridge {
250
259
  p.workdir as string | undefined,
251
260
  sessionId,
252
261
  );
262
+ case "getProjectSettings":
263
+ return this.getProjectSettings(
264
+ p.workdir as string | undefined,
265
+ sessionId,
266
+ );
267
+ case "setBuiltinPluginEnabled":
268
+ return this.setBuiltinPluginEnabled(
269
+ p.pluginId as string,
270
+ p.enabled as boolean,
271
+ p.scope as Scope | undefined,
272
+ p.workdir as string | undefined,
273
+ sessionId,
274
+ );
253
275
  case "updatePlugin":
254
276
  return this.updatePlugin(
255
277
  p.pluginId as string,
@@ -298,6 +320,22 @@ export class AgentBridge {
298
320
  case "stopWorkflowRun":
299
321
  return this.stopWorkflowRun(p.runId as string, sessionId);
300
322
 
323
+ // ── Git / worktree (global — no session required) ──
324
+ case "listGitBranches":
325
+ return this.listGitBranches(p.workdir as string | undefined);
326
+ case "createWorktree":
327
+ return this.createWorktreeSession(
328
+ p as unknown as {
329
+ workdir: string;
330
+ baseBranch?: string;
331
+ name?: string;
332
+ },
333
+ );
334
+ case "removeWorktree":
335
+ return this.removeWorktreeSession(
336
+ p as unknown as { path: string; branch: string; repoRoot: string },
337
+ );
338
+
301
339
  default:
302
340
  throw new RpcError(
303
341
  PROTOCOL_METHOD_NOT_FOUND,
@@ -349,6 +387,8 @@ export class AgentBridge {
349
387
  disallowedTools: params.disallowedTools,
350
388
  plugins: params.pluginDirs?.map((path) => ({ type: "local", path })),
351
389
  mcpServers: params.mcpServers,
390
+ worktreeName: params.worktreeName,
391
+ isNewWorktree: params.isNewWorktree,
352
392
  canUseTool: (context: ToolPermissionContext) =>
353
393
  this.canUseTool(context, ctx),
354
394
  };
@@ -400,6 +440,106 @@ export class AgentBridge {
400
440
  return { sessions };
401
441
  }
402
442
 
443
+ // ── Git / worktree ────────────────────────────────────────────
444
+
445
+ private listGitBranches(workdir?: string): {
446
+ branches: string[];
447
+ current: string | null;
448
+ } {
449
+ if (!workdir) {
450
+ throw new RpcError(PROTOCOL_INTERNAL_ERROR, "workdir is required");
451
+ }
452
+ const gitOpts = {
453
+ cwd: workdir,
454
+ encoding: "utf8" as const,
455
+ stdio: ["ignore", "pipe", "pipe"] as ["ignore", "pipe", "pipe"],
456
+ };
457
+ let branchesRaw: string;
458
+ try {
459
+ branchesRaw = execFileSync(
460
+ "git",
461
+ ["for-each-ref", "--format=%(refname:short)", "refs/heads"],
462
+ gitOpts,
463
+ ).trim();
464
+ } catch {
465
+ throw new RpcError(
466
+ PROTOCOL_INTERNAL_ERROR,
467
+ `Not a git repository (or git unavailable): ${workdir}`,
468
+ );
469
+ }
470
+ const branches = branchesRaw
471
+ ? branchesRaw
472
+ .split("\n")
473
+ .map((b) => b.trim())
474
+ .filter(Boolean)
475
+ : [];
476
+ let current: string | null = null;
477
+ try {
478
+ const head = execFileSync(
479
+ "git",
480
+ ["rev-parse", "--abbrev-ref", "HEAD"],
481
+ gitOpts,
482
+ ).trim();
483
+ // Detached HEAD prints "HEAD" — treat as no current branch.
484
+ current = head && head !== "HEAD" ? head : null;
485
+ } catch {
486
+ current = null;
487
+ }
488
+ return { branches, current };
489
+ }
490
+
491
+ private async createWorktreeSession(params: {
492
+ workdir: string;
493
+ baseBranch?: string;
494
+ name?: string;
495
+ }): Promise<{
496
+ name: string;
497
+ path: string;
498
+ branch: string;
499
+ repoRoot: string;
500
+ baseBranch: string;
501
+ isNew: boolean;
502
+ }> {
503
+ if (!params.workdir) {
504
+ throw new RpcError(PROTOCOL_INTERNAL_ERROR, "workdir is required");
505
+ }
506
+ const name = params.name?.trim() || generateRandomName();
507
+ try {
508
+ const session = await createWorktree(name, params.workdir, {
509
+ baseBranch: params.baseBranch,
510
+ });
511
+ return {
512
+ name: session.name,
513
+ path: session.path,
514
+ branch: session.branch,
515
+ repoRoot: session.repoRoot,
516
+ baseBranch: params.baseBranch ?? getDefaultRemoteBranch(params.workdir),
517
+ isNew: session.isNew,
518
+ };
519
+ } catch (e) {
520
+ throw new RpcError(PROTOCOL_INTERNAL_ERROR, (e as Error).message);
521
+ }
522
+ }
523
+
524
+ private async removeWorktreeSession(params: {
525
+ path: string;
526
+ branch: string;
527
+ repoRoot: string;
528
+ }): Promise<{ ok: true }> {
529
+ // removeWorktree is best-effort/idempotent: already-removed worktrees or
530
+ // branches only log, never throw.
531
+ await removeWorktree({
532
+ name: "",
533
+ path: params.path,
534
+ branch: params.branch,
535
+ repoRoot: params.repoRoot,
536
+ hasUncommittedChanges: false,
537
+ hasNewCommits: false,
538
+ isNew: false,
539
+ });
540
+ return { ok: true };
541
+ }
542
+
403
543
  private getSessionInfo(sessionId?: string): {
404
544
  sessionId: string;
405
545
  workingDirectory: string;
@@ -450,6 +590,9 @@ export class AgentBridge {
450
590
  path: p,
451
591
  })),
452
592
  mcpServers: entry.storedConfig.mcpServers,
593
+ // Keep worktree context (permission safety) across recreation, but never
594
+ // re-fire WorktreeCreate — the hook ran at initial creation.
595
+ worktreeName: entry.storedConfig.worktreeName,
453
596
  canUseTool: (context: ToolPermissionContext) =>
454
597
  this.canUseTool(context, ctx),
455
598
  };
@@ -535,6 +678,20 @@ export class AgentBridge {
535
678
  return { inputContent: textBlock?.content || "" };
536
679
  }
537
680
 
681
+ private async listRewindCheckpoints(sessionId?: string): Promise<{
682
+ checkpoints: Array<{ id: string; content: string }>;
683
+ }> {
684
+ const entry = this.requireSession(sessionId);
685
+ const { messages } = await entry.agent.getFullMessageThread();
686
+ const checkpoints = messages
687
+ .filter((m) => m.role === "user" && !m.isMeta && m.id)
688
+ .map((m) => ({
689
+ id: m.id as string,
690
+ content: getMessageContent(m).replace(/\s+/g, " ").trim(),
691
+ }));
692
+ return { checkpoints };
693
+ }
694
+
538
695
  private deleteQueuedMessage(index: number, sessionId?: string): null {
539
696
  const entry = this.requireSession(sessionId);
540
697
  entry.agent.removeQueuedMessage(index);
@@ -837,6 +994,35 @@ export class AgentBridge {
837
994
  );
838
995
  }
839
996
 
997
+ private async getProjectSettings(
998
+ workdir?: string,
999
+ sessionId?: string,
1000
+ ): Promise<{ enabledPlugins: Record<string, boolean> }> {
1001
+ return {
1002
+ enabledPlugins: this.getPluginCore(
1003
+ workdir,
1004
+ sessionId,
1005
+ ).getMergedEnabledPlugins(),
1006
+ };
1007
+ }
1008
+
1009
+ private async setBuiltinPluginEnabled(
1010
+ pluginId: string,
1011
+ enabled: boolean,
1012
+ scope: Scope | undefined,
1013
+ workdir?: string,
1014
+ sessionId?: string,
1015
+ ): Promise<{ enabledPlugins: Record<string, boolean> }> {
1016
+ const core = this.getPluginCore(workdir, sessionId);
1017
+ const targetScope = scope ?? "project";
1018
+ if (enabled) {
1019
+ await core.enablePlugin(pluginId, targetScope);
1020
+ } else {
1021
+ await core.disablePlugin(pluginId, targetScope);
1022
+ }
1023
+ return { enabledPlugins: core.getMergedEnabledPlugins() };
1024
+ }
1025
+
840
1026
  private async updatePlugin(
841
1027
  pluginId: string,
842
1028
  workdir?: string,
@@ -57,6 +57,7 @@ export type RequestMethod =
57
57
  | "abortMessage"
58
58
  | "clearMessages"
59
59
  | "rewindToMessage"
60
+ | "listRewindCheckpoints"
60
61
  | "deleteQueuedMessage"
61
62
  | "updateQueuedMessage"
62
63
  | "deleteQueuedMessageById"
@@ -91,7 +92,11 @@ export type RequestMethod =
91
92
  | "getBackgroundTaskOutput"
92
93
  | "stopBackgroundTask"
93
94
  | "getWorkflowRuns"
94
- | "stopWorkflowRun";
95
+ | "stopWorkflowRun"
96
+ // Git / worktree (global — no session required)
97
+ | "listGitBranches"
98
+ | "createWorktree"
99
+ | "removeWorktree";
95
100
 
96
101
  // ── Client → Server notification methods ────────────────────────
97
102
 
@@ -1,8 +1,14 @@
1
- import { execFileSync } from "node:child_process";
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
2
3
  import * as path from "node:path";
3
4
  import * as fs from "node:fs";
4
5
  import { getDefaultRemoteBranch, getGitMainRepoRoot } from "wave-agent-sdk";
5
6
 
7
+ // Never use execFileSync here: the shared `wave --stdio` process handles all
8
+ // desktop sessions, so a synchronous git call (especially a multi-second
9
+ // recursive worktree delete or a network fetch) freezes every session.
10
+ const execFileAsync = promisify(execFile);
11
+
6
12
  export interface WorktreeSession {
7
13
  name: string;
8
14
  path: string;
@@ -19,18 +25,20 @@ export interface WorktreeSession {
19
25
  * @param cwd Current working directory
20
26
  * @param options Optional creation options
21
27
  * @param options.baseRef "fresh" (default, origin/<default-branch>) | "head" (local HEAD)
28
+ * @param options.baseBranch Explicit base branch (overrides baseRef)
22
29
  * @returns Worktree session details
23
30
  */
24
- export function createWorktree(
31
+ export async function createWorktree(
25
32
  name: string,
26
33
  cwd: string,
27
- options?: { baseRef?: "fresh" | "head" },
28
- ): WorktreeSession {
34
+ options?: { baseRef?: "fresh" | "head"; baseBranch?: string },
35
+ ): Promise<WorktreeSession> {
29
36
  const repoRoot = getGitMainRepoRoot(cwd);
30
37
  const worktreePath = path.join(repoRoot, ".wave", "worktrees", name);
31
38
  const branchName = `worktree-${name}`;
32
39
  const useHead = options?.baseRef === "head";
33
- const baseBranch = useHead ? "HEAD" : getDefaultRemoteBranch(cwd);
40
+ const resolvedBaseBranch =
41
+ options?.baseBranch ?? (useHead ? "HEAD" : getDefaultRemoteBranch(cwd));
34
42
 
35
43
  // Ensure parent directory exists
36
44
  const parentDir = path.dirname(worktreePath);
@@ -54,12 +62,11 @@ export function createWorktree(
54
62
 
55
63
  try {
56
64
  // Create worktree and branch
57
- execFileSync(
65
+ await execFileAsync(
58
66
  "git",
59
- ["worktree", "add", "-b", branchName, worktreePath, baseBranch],
67
+ ["worktree", "add", "-b", branchName, worktreePath, resolvedBaseBranch],
60
68
  {
61
69
  cwd: repoRoot,
62
- stdio: ["ignore", "pipe", "pipe"],
63
70
  },
64
71
  );
65
72
 
@@ -73,14 +80,18 @@ export function createWorktree(
73
80
  isNew: true,
74
81
  };
75
82
  } catch (error: unknown) {
76
- const stderr = (error as { stderr?: Buffer }).stderr?.toString() || "";
83
+ const stderr =
84
+ (error as { stderr?: Buffer | string }).stderr?.toString() || "";
77
85
  if (stderr.includes("already exists")) {
78
86
  // If branch already exists, try to add worktree without -b
79
87
  try {
80
- execFileSync("git", ["worktree", "add", worktreePath, branchName], {
81
- cwd: repoRoot,
82
- stdio: ["ignore", "pipe", "pipe"],
83
- });
88
+ await execFileAsync(
89
+ "git",
90
+ ["worktree", "add", worktreePath, branchName],
91
+ {
92
+ cwd: repoRoot,
93
+ },
94
+ );
84
95
  return {
85
96
  name,
86
97
  path: worktreePath,
@@ -102,18 +113,23 @@ export function createWorktree(
102
113
  stderr.includes("unknown revision"))
103
114
  ) {
104
115
  // Base branch not fetched yet — try fetching then retrying
105
- const branchNameOnly = baseBranch.split("/").pop()!;
116
+ const branchNameOnly = resolvedBaseBranch.split("/").pop()!;
106
117
  try {
107
- execFileSync("git", ["fetch", "origin", branchNameOnly], {
118
+ await execFileAsync("git", ["fetch", "origin", branchNameOnly], {
108
119
  cwd: repoRoot,
109
- stdio: ["ignore", "pipe", "pipe"],
110
120
  });
111
- execFileSync(
121
+ await execFileAsync(
112
122
  "git",
113
- ["worktree", "add", "-b", branchName, worktreePath, baseBranch],
123
+ [
124
+ "worktree",
125
+ "add",
126
+ "-b",
127
+ branchName,
128
+ worktreePath,
129
+ resolvedBaseBranch,
130
+ ],
114
131
  {
115
132
  cwd: repoRoot,
116
- stdio: ["ignore", "pipe", "pipe"],
117
133
  },
118
134
  );
119
135
  return {
@@ -128,12 +144,11 @@ export function createWorktree(
128
144
  } catch {
129
145
  // Fetch or retry failed — fall back to HEAD
130
146
  try {
131
- execFileSync(
147
+ await execFileAsync(
132
148
  "git",
133
149
  ["worktree", "add", "-b", branchName, worktreePath, "HEAD"],
134
150
  {
135
151
  cwd: repoRoot,
136
- stdio: ["ignore", "pipe", "pipe"],
137
152
  },
138
153
  );
139
154
  return {
@@ -162,37 +177,39 @@ export function createWorktree(
162
177
  * Remove a git worktree and its associated branch
163
178
  * @param session Worktree session details
164
179
  */
165
- export function removeWorktree(session: WorktreeSession): void {
180
+ export async function removeWorktree(session: WorktreeSession): Promise<void> {
166
181
  const repoRoot = session.repoRoot;
167
182
 
168
183
  try {
169
184
  // Get current branch in worktree before removing it
170
185
  let currentBranch: string | undefined;
171
186
  try {
172
- currentBranch = execFileSync(
187
+ const { stdout } = await execFileAsync(
173
188
  "git",
174
189
  ["rev-parse", "--abbrev-ref", "HEAD"],
175
190
  {
176
191
  cwd: session.path,
177
192
  encoding: "utf8",
178
- stdio: ["ignore", "pipe", "ignore"],
179
193
  },
180
- ).trim();
194
+ );
195
+ currentBranch = stdout.trim();
181
196
  } catch {
182
197
  // Ignore errors getting current branch
183
198
  }
184
199
 
185
200
  // Remove worktree
186
- execFileSync("git", ["worktree", "remove", "--force", session.path], {
187
- cwd: repoRoot,
188
- stdio: ["ignore", "pipe", "pipe"],
189
- });
201
+ await execFileAsync(
202
+ "git",
203
+ ["worktree", "remove", "--force", session.path],
204
+ {
205
+ cwd: repoRoot,
206
+ },
207
+ );
190
208
 
191
209
  // Delete original branch
192
210
  try {
193
- execFileSync("git", ["branch", "-D", session.branch], {
211
+ await execFileAsync("git", ["branch", "-D", session.branch], {
194
212
  cwd: repoRoot,
195
- stdio: ["ignore", "pipe", "pipe"],
196
213
  });
197
214
  } catch {
198
215
  // Ignore errors deleting original branch
@@ -213,9 +230,8 @@ export function removeWorktree(session: WorktreeSession): void {
213
230
  currentBranch !== "master"
214
231
  ) {
215
232
  try {
216
- execFileSync("git", ["branch", "-D", currentBranch], {
233
+ await execFileAsync("git", ["branch", "-D", currentBranch], {
217
234
  cwd: repoRoot,
218
- stdio: ["ignore", "pipe", "pipe"],
219
235
  });
220
236
  } catch {
221
237
  // Ignore errors deleting current branch