wave-code 0.19.7 → 0.19.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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";
@@ -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 = 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
@@ -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);
@@ -45,6 +45,11 @@ export declare class AgentBridge {
45
45
  private deleteQueuedMessageById;
46
46
  private getMessages;
47
47
  private getFullMessageThread;
48
+ private compact;
49
+ private getBackgroundTaskOutput;
50
+ private stopBackgroundTask;
51
+ private getWorkflowRuns;
52
+ private stopWorkflowRun;
48
53
  private setPermissionMode;
49
54
  private getPermissionMode;
50
55
  private getMcpServers;
@@ -112,6 +112,17 @@ export class AgentBridge {
112
112
  return this.removeMarketplace(p.name, p.scope, p.workdir, sessionId);
113
113
  case "updateMarketplace":
114
114
  return this.updateMarketplace(p.name, p.workdir, sessionId);
115
+ case "compact":
116
+ return this.compact(p.customInstructions, sessionId);
117
+ // ── Background tasks ──
118
+ case "getBackgroundTaskOutput":
119
+ return this.getBackgroundTaskOutput(p.taskId, sessionId);
120
+ case "stopBackgroundTask":
121
+ return this.stopBackgroundTask(p.taskId, sessionId);
122
+ case "getWorkflowRuns":
123
+ return this.getWorkflowRuns(sessionId);
124
+ case "stopWorkflowRun":
125
+ return this.stopWorkflowRun(p.runId, sessionId);
115
126
  default:
116
127
  throw new RpcError(PROTOCOL_METHOD_NOT_FOUND, `Method not found: ${method}`);
117
128
  }
@@ -302,6 +313,37 @@ export class AgentBridge {
302
313
  const entry = this.requireSession(sessionId);
303
314
  return entry.agent.getFullMessageThread();
304
315
  }
316
+ async compact(customInstructions, sessionId) {
317
+ const entry = this.requireSession(sessionId);
318
+ await entry.agent.compact(customInstructions);
319
+ return null;
320
+ }
321
+ // ── Background tasks ──
322
+ getBackgroundTaskOutput(taskId, sessionId) {
323
+ const entry = this.requireSession(sessionId);
324
+ return { output: entry.agent.getBackgroundTaskOutput(taskId) };
325
+ }
326
+ stopBackgroundTask(taskId, sessionId) {
327
+ const entry = this.requireSession(sessionId);
328
+ const success = entry.agent.stopBackgroundTask(taskId);
329
+ return { success };
330
+ }
331
+ async getWorkflowRuns(sessionId) {
332
+ const entry = this.requireSession(sessionId);
333
+ const runs = await entry.agent.getWorkflowRuns();
334
+ return {
335
+ runs: runs.map((r) => {
336
+ const { completionPromise, ...rest } = r;
337
+ void completionPromise;
338
+ return rest;
339
+ }),
340
+ };
341
+ }
342
+ stopWorkflowRun(runId, sessionId) {
343
+ const entry = this.requireSession(sessionId);
344
+ entry.agent.stopWorkflowRun(runId);
345
+ return { success: true };
346
+ }
305
347
  // ── Permissions ───────────────────────────────────────────────
306
348
  async setPermissionMode(mode, sessionId) {
307
349
  const entry = this.requireSession(sessionId);
@@ -485,6 +527,21 @@ export class AgentBridge {
485
527
  onTasksChange: (tasks) => {
486
528
  this.emit("tasksChange", { tasks }, ctx.registeredSessionId);
487
529
  },
530
+ onBackgroundTasksChange: (tasks) => {
531
+ const summaries = tasks.map((t) => ({
532
+ id: t.id,
533
+ type: t.type,
534
+ status: t.status,
535
+ startTime: t.startTime,
536
+ endTime: t.endTime,
537
+ command: t.command,
538
+ description: t.description,
539
+ exitCode: t.exitCode,
540
+ runtime: t.runtime,
541
+ outputPath: t.outputPath,
542
+ }));
543
+ this.emit("backgroundTasksChange", { tasks: summaries }, ctx.registeredSessionId);
544
+ },
488
545
  onSessionIdChange: (newSessionId) => {
489
546
  const oldSessionId = ctx.registeredSessionId;
490
547
  // Emit with the OLD sessionId so the client's router can deliver it
@@ -526,6 +583,9 @@ export class AgentBridge {
526
583
  onCompactBlockAdded: (content) => {
527
584
  this.emit("compactBlockAdded", { content }, ctx.registeredSessionId);
528
585
  },
586
+ onCompactionStateChange: (isCompacting) => {
587
+ this.emit("compactionStateChange", { isCompacting }, ctx.registeredSessionId);
588
+ },
529
589
  };
530
590
  }
531
591
  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" | "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";
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,9 +11,13 @@ 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)
14
16
  * @returns Worktree session details
15
17
  */
16
- export declare function createWorktree(name: string, cwd: string): WorktreeSession;
18
+ export declare function createWorktree(name: string, cwd: string, options?: {
19
+ baseRef?: "fresh" | "head";
20
+ }): WorktreeSession;
17
21
  /**
18
22
  * Remove a git worktree and its associated branch
19
23
  * @param session Worktree session details
@@ -6,13 +6,16 @@ import { getDefaultRemoteBranch, getGitMainRepoRoot } from "wave-agent-sdk";
6
6
  * Create a new git worktree
7
7
  * @param name Worktree name
8
8
  * @param cwd Current working directory
9
+ * @param options Optional creation options
10
+ * @param options.baseRef "fresh" (default, origin/<default-branch>) | "head" (local HEAD)
9
11
  * @returns Worktree session details
10
12
  */
11
- export function createWorktree(name, cwd) {
13
+ export function createWorktree(name, cwd, options) {
12
14
  const repoRoot = getGitMainRepoRoot(cwd);
13
15
  const worktreePath = path.join(repoRoot, ".wave", "worktrees", name);
14
16
  const branchName = `worktree-${name}`;
15
- const baseBranch = getDefaultRemoteBranch(cwd);
17
+ const useHead = options?.baseRef === "head";
18
+ const baseBranch = useHead ? "HEAD" : getDefaultRemoteBranch(cwd);
16
19
  // Ensure parent directory exists
17
20
  const parentDir = path.dirname(worktreePath);
18
21
  if (!fs.existsSync(parentDir)) {
@@ -70,8 +73,9 @@ export function createWorktree(name, cwd) {
70
73
  throw new Error(`Failed to add existing worktree branch: ${innerError.message}`);
71
74
  }
72
75
  }
73
- if (stderr.includes("not a valid object name") ||
74
- stderr.includes("unknown revision")) {
76
+ if (!useHead &&
77
+ (stderr.includes("not a valid object name") ||
78
+ stderr.includes("unknown revision"))) {
75
79
  // Base branch not fetched yet — try fetching then retrying
76
80
  const branchNameOnly = baseBranch.split("/").pop();
77
81
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-code",
3
- "version": "0.19.7",
3
+ "version": "0.19.8",
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.7"
44
+ "wave-agent-sdk": "0.19.8"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/react": "^19.1.8",
@@ -39,7 +39,7 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
39
39
  onCancel,
40
40
  }) => {
41
41
  const [state, dispatch] = useReducer(confirmationReducer, {
42
- selectedOption: toolName === EXIT_PLAN_MODE_TOOL_NAME ? "clear" : "allow",
42
+ selectedOption: "allow",
43
43
  alternativeText: "",
44
44
  alternativeCursorPosition: 0,
45
45
  hasUserInput: false,
@@ -212,20 +212,6 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
212
212
  <Text>Do you want to proceed?</Text>
213
213
  </Box>
214
214
  <Box marginTop={1} flexDirection="column">
215
- {toolName === EXIT_PLAN_MODE_TOOL_NAME && (
216
- <Box key="clear-option">
217
- <Text
218
- color={state.selectedOption === "clear" ? "black" : "white"}
219
- backgroundColor={
220
- state.selectedOption === "clear" ? "yellow" : undefined
221
- }
222
- bold={state.selectedOption === "clear"}
223
- >
224
- {state.selectedOption === "clear" ? "> " : " "}
225
- Yes, clear context and auto-accept edits
226
- </Text>
227
- </Box>
228
- )}
229
215
  <Box key="allow-option">
230
216
  <Text
231
217
  color={state.selectedOption === "allow" ? "black" : "white"}
@@ -254,6 +240,21 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
254
240
  </Text>
255
241
  </Box>
256
242
  )}
243
+ {(toolName === BASH_TOOL_NAME ||
244
+ toolName === EXIT_PLAN_MODE_TOOL_NAME) && (
245
+ <Box key="bypass-option">
246
+ <Text
247
+ color={state.selectedOption === "bypass" ? "black" : "white"}
248
+ backgroundColor={
249
+ state.selectedOption === "bypass" ? "yellow" : undefined
250
+ }
251
+ bold={state.selectedOption === "bypass"}
252
+ >
253
+ {state.selectedOption === "bypass" ? "> " : " "}
254
+ Yes, and bypass permissions
255
+ </Text>
256
+ </Box>
257
+ )}
257
258
  <Box key="alternative-option">
258
259
  <Text
259
260
  color={
@@ -482,7 +482,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
482
482
 
483
483
  // Inject worktree session into this agent's own DI container so system
484
484
  // prompts and permission checks reflect the CLI -w worktree. This state is
485
- // per-session, not process-global (see specs/047-worktree.md FR-042).
485
+ // per-session, not process-global (see docs/specs/multi-agent/worktree.md FR-042).
486
486
  if (worktreeSession) {
487
487
  const session: WorktreeSession = {
488
488
  originalCwd: originalCwd ?? worktreeSession.repoRoot,
package/src/index.ts CHANGED
@@ -1,7 +1,12 @@
1
1
  import yargs from "yargs";
2
2
  import { hideBin } from "yargs/helpers";
3
3
  import { startCli } from "./cli.js";
4
- import { Scope, generateRandomName, type PermissionMode } from "wave-agent-sdk";
4
+ import {
5
+ Scope,
6
+ generateRandomName,
7
+ loadMergedWaveConfig,
8
+ type PermissionMode,
9
+ } from "wave-agent-sdk";
5
10
  import { createWorktree, type WorktreeSession } from "./utils/worktree.js";
6
11
  import path from "path";
7
12
  import { readFileSync } from "fs";
@@ -320,7 +325,8 @@ export async function main() {
320
325
  if (!name || name === "") {
321
326
  name = generateRandomName();
322
327
  }
323
- worktreeSession = createWorktree(name, originalCwd);
328
+ const baseRef = loadMergedWaveConfig(originalCwd)?.worktree?.baseRef;
329
+ worktreeSession = createWorktree(name, originalCwd, { baseRef });
324
330
 
325
331
  // Note: the full worktree session (originalCwd etc.) is injected into the
326
332
  // agent's DI container after the agent is created in useChat.tsx. This keeps
package/src/print-cli.ts CHANGED
@@ -154,8 +154,18 @@ export async function startPrintCli(options: PrintCliOptions): Promise<void> {
154
154
  process.stdout.write("\n");
155
155
  }
156
156
 
157
- // Wait for running background tasks and subagents to complete
158
- while (agent.hasRunningBackgroundWork) {
157
+ // Wait for running background tasks/subagents to complete AND for the main
158
+ // agent to finish processing their completion notifications. The last
159
+ // background task flips status to "completed" before its notification is
160
+ // enqueued and before the main agent's follow-up turn runs, so
161
+ // hasRunningBackgroundWork alone becomes false while the main agent is
162
+ // still mid-turn — causing a TOCTOU race that aborts the final response.
163
+ // Including isLoading and queued notifications closes that gap.
164
+ while (
165
+ agent.hasRunningBackgroundWork ||
166
+ agent.isLoading ||
167
+ agent.hasPendingMessages
168
+ ) {
159
169
  await new Promise((resolve) => setTimeout(resolve, 500));
160
170
  }
161
171
 
@@ -7,7 +7,7 @@ import {
7
7
  } from "wave-agent-sdk";
8
8
 
9
9
  export interface ConfirmationState {
10
- selectedOption: "clear" | "auto" | "allow" | "alternative";
10
+ selectedOption: "auto" | "bypass" | "allow" | "alternative";
11
11
  alternativeText: string;
12
12
  alternativeCursorPosition: number;
13
13
  hasUserInput: boolean;
@@ -99,13 +99,7 @@ export function confirmationReducer(
99
99
 
100
100
  if (key.return) {
101
101
  let decision: PermissionDecision | null = null;
102
- if (state.selectedOption === "clear") {
103
- decision = {
104
- behavior: "allow",
105
- newPermissionMode: "acceptEdits",
106
- clearContext: true,
107
- };
108
- } else if (state.selectedOption === "allow") {
102
+ if (state.selectedOption === "allow") {
109
103
  if (toolName === EXIT_PLAN_MODE_TOOL_NAME) {
110
104
  decision = { behavior: "allow", newPermissionMode: "default" };
111
105
  } else if (toolName === ENTER_PLAN_MODE_TOOL_NAME) {
@@ -134,6 +128,11 @@ export function confirmationReducer(
134
128
  } else {
135
129
  decision = { behavior: "allow", newPermissionMode: "acceptEdits" };
136
130
  }
131
+ } else if (state.selectedOption === "bypass") {
132
+ decision = {
133
+ behavior: "allow",
134
+ newPermissionMode: "bypassPermissions",
135
+ };
137
136
  } else if (state.alternativeText.trim()) {
138
137
  decision = {
139
138
  behavior: "deny",
@@ -174,9 +173,10 @@ export function confirmationReducer(
174
173
  }
175
174
 
176
175
  const availableOptions: ConfirmationState["selectedOption"][] = [];
177
- if (toolName === EXIT_PLAN_MODE_TOOL_NAME) availableOptions.push("clear");
178
176
  availableOptions.push("allow");
179
177
  if (!hidePersistentOption) availableOptions.push("auto");
178
+ if (toolName === BASH_TOOL_NAME || toolName === EXIT_PLAN_MODE_TOOL_NAME)
179
+ availableOptions.push("bypass");
180
180
  availableOptions.push("alternative");
181
181
 
182
182
  if (key.upArrow) {
@@ -19,6 +19,9 @@ import {
19
19
  Agent,
20
20
  type AgentCallbacks,
21
21
  type AgentOptions,
22
+ type BackgroundTask,
23
+ type BackgroundTaskSummary,
24
+ type SerializableWorkflowRun,
22
25
  type Message,
23
26
  type PermissionDecision,
24
27
  type PermissionMode,
@@ -278,6 +281,22 @@ export class AgentBridge {
278
281
  p.workdir as string | undefined,
279
282
  sessionId,
280
283
  );
284
+ case "compact":
285
+ return this.compact(
286
+ p.customInstructions as string | undefined,
287
+ sessionId,
288
+ );
289
+
290
+ // ── Background tasks ──
291
+ case "getBackgroundTaskOutput":
292
+ return this.getBackgroundTaskOutput(p.taskId as string, sessionId);
293
+ case "stopBackgroundTask":
294
+ return this.stopBackgroundTask(p.taskId as string, sessionId);
295
+
296
+ case "getWorkflowRuns":
297
+ return this.getWorkflowRuns(sessionId);
298
+ case "stopWorkflowRun":
299
+ return this.stopWorkflowRun(p.runId as string, sessionId);
281
300
 
282
301
  default:
283
302
  throw new RpcError(
@@ -555,6 +574,57 @@ export class AgentBridge {
555
574
  return entry.agent.getFullMessageThread();
556
575
  }
557
576
 
577
+ private async compact(
578
+ customInstructions: string | undefined,
579
+ sessionId?: string,
580
+ ): Promise<null> {
581
+ const entry = this.requireSession(sessionId);
582
+ await entry.agent.compact(customInstructions);
583
+ return null;
584
+ }
585
+
586
+ // ── Background tasks ──
587
+
588
+ private getBackgroundTaskOutput(
589
+ taskId: string,
590
+ sessionId?: string,
591
+ ): { output: ReturnType<Agent["getBackgroundTaskOutput"]> } {
592
+ const entry = this.requireSession(sessionId);
593
+ return { output: entry.agent.getBackgroundTaskOutput(taskId) };
594
+ }
595
+
596
+ private stopBackgroundTask(
597
+ taskId: string,
598
+ sessionId?: string,
599
+ ): { success: boolean } {
600
+ const entry = this.requireSession(sessionId);
601
+ const success = entry.agent.stopBackgroundTask(taskId);
602
+ return { success };
603
+ }
604
+
605
+ private async getWorkflowRuns(
606
+ sessionId?: string,
607
+ ): Promise<{ runs: SerializableWorkflowRun[] }> {
608
+ const entry = this.requireSession(sessionId);
609
+ const runs = await entry.agent.getWorkflowRuns();
610
+ return {
611
+ runs: runs.map((r) => {
612
+ const { completionPromise, ...rest } = r;
613
+ void completionPromise;
614
+ return rest;
615
+ }),
616
+ };
617
+ }
618
+
619
+ private stopWorkflowRun(
620
+ runId: string,
621
+ sessionId?: string,
622
+ ): { success: boolean } {
623
+ const entry = this.requireSession(sessionId);
624
+ entry.agent.stopWorkflowRun(runId);
625
+ return { success: true };
626
+ }
627
+
558
628
  // ── Permissions ───────────────────────────────────────────────
559
629
 
560
630
  private async setPermissionMode(
@@ -867,6 +937,25 @@ export class AgentBridge {
867
937
  onTasksChange: (tasks: Task[]) => {
868
938
  this.emit("tasksChange", { tasks }, ctx.registeredSessionId);
869
939
  },
940
+ onBackgroundTasksChange: (tasks: BackgroundTask[]) => {
941
+ const summaries: BackgroundTaskSummary[] = tasks.map((t) => ({
942
+ id: t.id,
943
+ type: t.type,
944
+ status: t.status,
945
+ startTime: t.startTime,
946
+ endTime: t.endTime,
947
+ command: t.command,
948
+ description: t.description,
949
+ exitCode: t.exitCode,
950
+ runtime: t.runtime,
951
+ outputPath: t.outputPath,
952
+ }));
953
+ this.emit(
954
+ "backgroundTasksChange",
955
+ { tasks: summaries },
956
+ ctx.registeredSessionId,
957
+ );
958
+ },
870
959
  onSessionIdChange: (newSessionId: string) => {
871
960
  const oldSessionId = ctx.registeredSessionId;
872
961
  // Emit with the OLD sessionId so the client's router can deliver it
@@ -922,6 +1011,13 @@ export class AgentBridge {
922
1011
  onCompactBlockAdded: (content: string) => {
923
1012
  this.emit("compactBlockAdded", { content }, ctx.registeredSessionId);
924
1013
  },
1014
+ onCompactionStateChange: (isCompacting: boolean) => {
1015
+ this.emit(
1016
+ "compactionStateChange",
1017
+ { isCompacting },
1018
+ ctx.registeredSessionId,
1019
+ );
1020
+ },
925
1021
  };
926
1022
  }
927
1023
 
@@ -86,7 +86,12 @@ export type RequestMethod =
86
86
  | "listMarketplaces"
87
87
  | "addMarketplace"
88
88
  | "removeMarketplace"
89
- | "updateMarketplace";
89
+ | "updateMarketplace"
90
+ | "compact"
91
+ | "getBackgroundTaskOutput"
92
+ | "stopBackgroundTask"
93
+ | "getWorkflowRuns"
94
+ | "stopWorkflowRun";
90
95
 
91
96
  // ── Client → Server notification methods ────────────────────────
92
97
 
@@ -116,7 +121,9 @@ export type ServerNotificationMethod =
116
121
  | "notificationMessageAdded"
117
122
  | "permissionRequest"
118
123
  | "authUrl"
119
- | "compactBlockAdded";
124
+ | "compactBlockAdded"
125
+ | "compactionStateChange"
126
+ | "backgroundTasksChange";
120
127
 
121
128
  // ── Helper: is this a request (has id)? ─────────────────────────
122
129
 
@@ -17,13 +17,20 @@ export interface WorktreeSession {
17
17
  * Create a new git worktree
18
18
  * @param name Worktree name
19
19
  * @param cwd Current working directory
20
+ * @param options Optional creation options
21
+ * @param options.baseRef "fresh" (default, origin/<default-branch>) | "head" (local HEAD)
20
22
  * @returns Worktree session details
21
23
  */
22
- export function createWorktree(name: string, cwd: string): WorktreeSession {
24
+ export function createWorktree(
25
+ name: string,
26
+ cwd: string,
27
+ options?: { baseRef?: "fresh" | "head" },
28
+ ): WorktreeSession {
23
29
  const repoRoot = getGitMainRepoRoot(cwd);
24
30
  const worktreePath = path.join(repoRoot, ".wave", "worktrees", name);
25
31
  const branchName = `worktree-${name}`;
26
- const baseBranch = getDefaultRemoteBranch(cwd);
32
+ const useHead = options?.baseRef === "head";
33
+ const baseBranch = useHead ? "HEAD" : getDefaultRemoteBranch(cwd);
27
34
 
28
35
  // Ensure parent directory exists
29
36
  const parentDir = path.dirname(worktreePath);
@@ -90,8 +97,9 @@ export function createWorktree(name: string, cwd: string): WorktreeSession {
90
97
  }
91
98
  }
92
99
  if (
93
- stderr.includes("not a valid object name") ||
94
- stderr.includes("unknown revision")
100
+ !useHead &&
101
+ (stderr.includes("not a valid object name") ||
102
+ stderr.includes("unknown revision"))
95
103
  ) {
96
104
  // Base branch not fetched yet — try fetching then retrying
97
105
  const branchNameOnly = baseBranch.split("/").pop()!;