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.
- package/dist/components/ConfirmationSelector.js +4 -3
- package/dist/contexts/useChat.js +1 -1
- package/dist/index.js +3 -2
- package/dist/print-cli.js +10 -2
- package/dist/reducers/confirmationReducer.d.ts +1 -1
- package/dist/reducers/confirmationReducer.js +9 -10
- package/dist/stdio/agentBridge.d.ts +5 -0
- package/dist/stdio/agentBridge.js +60 -0
- package/dist/stdio/protocol.d.ts +2 -2
- package/dist/utils/worktree.d.ts +5 -1
- package/dist/utils/worktree.js +8 -4
- package/package.json +2 -2
- package/src/components/ConfirmationSelector.tsx +16 -15
- package/src/contexts/useChat.tsx +1 -1
- package/src/index.ts +8 -2
- package/src/print-cli.ts +12 -2
- package/src/reducers/confirmationReducer.ts +9 -9
- package/src/stdio/agentBridge.ts +96 -0
- package/src/stdio/protocol.ts +9 -2
- package/src/utils/worktree.ts +12 -4
|
@@ -14,7 +14,7 @@ const getHeaderColor = (header) => {
|
|
|
14
14
|
};
|
|
15
15
|
export const ConfirmationSelector = ({ toolName, toolInput, suggestedPrefix, hidePersistentOption, isExpanded = false, onDecision, onCancel, }) => {
|
|
16
16
|
const [state, dispatch] = useReducer(confirmationReducer, {
|
|
17
|
-
selectedOption:
|
|
17
|
+
selectedOption: "allow",
|
|
18
18
|
alternativeText: "",
|
|
19
19
|
alternativeCursorPosition: 0,
|
|
20
20
|
hasUserInput: false,
|
|
@@ -104,8 +104,9 @@ export const ConfirmationSelector = ({ toolName, toolInput, suggestedPrefix, hid
|
|
|
104
104
|
? "[x] "
|
|
105
105
|
: "[ ] "
|
|
106
106
|
: "", option.label, option.description ? ` - ${option.description}` : "", isOther && isSelected && (_jsxs(Text, { children: [":", " ", questionState.otherText ? (_jsxs(_Fragment, { children: [questionState.otherText.slice(0, questionState.otherCursorPosition), _jsx(Text, { backgroundColor: "white", color: "black", children: questionState.otherText[questionState.otherCursorPosition] || " " }), questionState.otherText.slice(questionState.otherCursorPosition + 1)] })) : (_jsx(Text, { color: "gray", dimColor: true, children: "[Type your answer...]" }))] }))] }) }, index));
|
|
107
|
-
}) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Question ", questionState.currentQuestionIndex + 1, " of", " ", questions.length, " \u2022", currentQuestion.multiSelect ? " Space to toggle •" : "", " Use \u2191\u2193 or Tab to navigate \u2022 Enter to confirm"] }) })] })), toolName !== ASK_USER_QUESTION_TOOL_NAME && !isExpanded && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "Do you want to proceed?" }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [
|
|
107
|
+
}) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Question ", questionState.currentQuestionIndex + 1, " of", " ", questions.length, " \u2022", currentQuestion.multiSelect ? " Space to toggle •" : "", " Use \u2191\u2193 or Tab to navigate \u2022 Enter to confirm"] }) })] })), toolName !== ASK_USER_QUESTION_TOOL_NAME && !isExpanded && (_jsxs(_Fragment, { children: [_jsx(Box, { marginTop: 1, children: _jsx(Text, { children: "Do you want to proceed?" }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Box, { children: _jsxs(Text, { color: state.selectedOption === "allow" ? "black" : "white", backgroundColor: state.selectedOption === "allow" ? "yellow" : undefined, bold: state.selectedOption === "allow", children: [state.selectedOption === "allow" ? "> " : " ", toolName === EXIT_PLAN_MODE_TOOL_NAME
|
|
108
108
|
? "Yes, manually approve edits"
|
|
109
|
-
: "Yes, proceed"] }) }, "allow-option"), !hidePersistentOption && (_jsx(Box, { children: _jsxs(Text, { color: state.selectedOption === "auto" ? "black" : "white", backgroundColor: state.selectedOption === "auto" ? "yellow" : undefined, bold: state.selectedOption === "auto", children: [state.selectedOption === "auto" ? "> " : " ", getAutoOptionText()] }) }, "auto-option")),
|
|
109
|
+
: "Yes, proceed"] }) }, "allow-option"), !hidePersistentOption && (_jsx(Box, { children: _jsxs(Text, { color: state.selectedOption === "auto" ? "black" : "white", backgroundColor: state.selectedOption === "auto" ? "yellow" : undefined, bold: state.selectedOption === "auto", children: [state.selectedOption === "auto" ? "> " : " ", getAutoOptionText()] }) }, "auto-option")), (toolName === BASH_TOOL_NAME ||
|
|
110
|
+
toolName === EXIT_PLAN_MODE_TOOL_NAME) && (_jsx(Box, { children: _jsxs(Text, { color: state.selectedOption === "bypass" ? "black" : "white", backgroundColor: state.selectedOption === "bypass" ? "yellow" : undefined, bold: state.selectedOption === "bypass", children: [state.selectedOption === "bypass" ? "> " : " ", "Yes, and bypass permissions"] }) }, "bypass-option")), _jsx(Box, { children: _jsxs(Text, { color: state.selectedOption === "alternative" ? "black" : "white", backgroundColor: state.selectedOption === "alternative" ? "yellow" : undefined, bold: state.selectedOption === "alternative", children: [state.selectedOption === "alternative" ? "> " : " ", toolName === ENTER_PLAN_MODE_TOOL_NAME && showPlaceholder ? (_jsx(Text, { color: "gray", dimColor: true, children: "No, start implementing now" })) : showPlaceholder ? (_jsx(Text, { color: "gray", dimColor: true, children: placeholderText })) : (_jsx(Text, { children: state.alternativeText ? (_jsxs(_Fragment, { children: [state.alternativeText.slice(0, state.alternativeCursorPosition), _jsx(Text, { backgroundColor: "white", color: "black", children: state.alternativeText[state.alternativeCursorPosition] || " " }), state.alternativeText.slice(state.alternativeCursorPosition + 1)] })) : ("Type here to tell Wave what to change") }))] }) }, "alternative-option")] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Use \u2191\u2193 or Tab to navigate \u2022 ESC to cancel" }) })] }))] }));
|
|
110
111
|
};
|
|
111
112
|
ConfirmationSelector.displayName = "ConfirmationSelector";
|
package/dist/contexts/useChat.js
CHANGED
|
@@ -235,7 +235,7 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
235
235
|
agentRef.current = agent;
|
|
236
236
|
// Inject worktree session into this agent's own DI container so system
|
|
237
237
|
// prompts and permission checks reflect the CLI -w worktree. This state is
|
|
238
|
-
// per-session, not process-global (see specs/
|
|
238
|
+
// per-session, not process-global (see docs/specs/multi-agent/worktree.md FR-042).
|
|
239
239
|
if (worktreeSession) {
|
|
240
240
|
const session = {
|
|
241
241
|
originalCwd: originalCwd ?? worktreeSession.repoRoot,
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import yargs from "yargs";
|
|
2
2
|
import { hideBin } from "yargs/helpers";
|
|
3
3
|
import { startCli } from "./cli.js";
|
|
4
|
-
import { generateRandomName } from "wave-agent-sdk";
|
|
4
|
+
import { generateRandomName, loadMergedWaveConfig, } from "wave-agent-sdk";
|
|
5
5
|
import { createWorktree } from "./utils/worktree.js";
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { readFileSync } from "fs";
|
|
@@ -236,7 +236,8 @@ export async function main() {
|
|
|
236
236
|
if (!name || name === "") {
|
|
237
237
|
name = generateRandomName();
|
|
238
238
|
}
|
|
239
|
-
|
|
239
|
+
const baseRef = loadMergedWaveConfig(originalCwd)?.worktree?.baseRef;
|
|
240
|
+
worktreeSession = createWorktree(name, originalCwd, { baseRef });
|
|
240
241
|
// Note: the full worktree session (originalCwd etc.) is injected into the
|
|
241
242
|
// agent's DI container after the agent is created in useChat.tsx. This keeps
|
|
242
243
|
// worktree state per-session instead of process-global.
|
package/dist/print-cli.js
CHANGED
|
@@ -104,8 +104,16 @@ export async function startPrintCli(options) {
|
|
|
104
104
|
await agent.sendMessage(message);
|
|
105
105
|
process.stdout.write("\n");
|
|
106
106
|
}
|
|
107
|
-
// Wait for running background tasks
|
|
108
|
-
|
|
107
|
+
// Wait for running background tasks/subagents to complete AND for the main
|
|
108
|
+
// agent to finish processing their completion notifications. The last
|
|
109
|
+
// background task flips status to "completed" before its notification is
|
|
110
|
+
// enqueued and before the main agent's follow-up turn runs, so
|
|
111
|
+
// hasRunningBackgroundWork alone becomes false while the main agent is
|
|
112
|
+
// still mid-turn — causing a TOCTOU race that aborts the final response.
|
|
113
|
+
// Including isLoading and queued notifications closes that gap.
|
|
114
|
+
while (agent.hasRunningBackgroundWork ||
|
|
115
|
+
agent.isLoading ||
|
|
116
|
+
agent.hasPendingMessages) {
|
|
109
117
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
110
118
|
}
|
|
111
119
|
// Display usage summary before exit
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Key } from "ink";
|
|
2
2
|
import type { PermissionDecision } from "wave-agent-sdk";
|
|
3
3
|
export interface ConfirmationState {
|
|
4
|
-
selectedOption: "
|
|
4
|
+
selectedOption: "auto" | "bypass" | "allow" | "alternative";
|
|
5
5
|
alternativeText: string;
|
|
6
6
|
alternativeCursorPosition: number;
|
|
7
7
|
hasUserInput: boolean;
|
|
@@ -47,14 +47,7 @@ export function confirmationReducer(state, action) {
|
|
|
47
47
|
const { input, key, toolName, toolInput, suggestedPrefix, hidePersistentOption, } = action;
|
|
48
48
|
if (key.return) {
|
|
49
49
|
let decision = null;
|
|
50
|
-
if (state.selectedOption === "
|
|
51
|
-
decision = {
|
|
52
|
-
behavior: "allow",
|
|
53
|
-
newPermissionMode: "acceptEdits",
|
|
54
|
-
clearContext: true,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
else if (state.selectedOption === "allow") {
|
|
50
|
+
if (state.selectedOption === "allow") {
|
|
58
51
|
if (toolName === EXIT_PLAN_MODE_TOOL_NAME) {
|
|
59
52
|
decision = { behavior: "allow", newPermissionMode: "default" };
|
|
60
53
|
}
|
|
@@ -91,6 +84,12 @@ export function confirmationReducer(state, action) {
|
|
|
91
84
|
decision = { behavior: "allow", newPermissionMode: "acceptEdits" };
|
|
92
85
|
}
|
|
93
86
|
}
|
|
87
|
+
else if (state.selectedOption === "bypass") {
|
|
88
|
+
decision = {
|
|
89
|
+
behavior: "allow",
|
|
90
|
+
newPermissionMode: "bypassPermissions",
|
|
91
|
+
};
|
|
92
|
+
}
|
|
94
93
|
else if (state.alternativeText.trim()) {
|
|
95
94
|
decision = {
|
|
96
95
|
behavior: "deny",
|
|
@@ -123,11 +122,11 @@ export function confirmationReducer(state, action) {
|
|
|
123
122
|
}
|
|
124
123
|
}
|
|
125
124
|
const availableOptions = [];
|
|
126
|
-
if (toolName === EXIT_PLAN_MODE_TOOL_NAME)
|
|
127
|
-
availableOptions.push("clear");
|
|
128
125
|
availableOptions.push("allow");
|
|
129
126
|
if (!hidePersistentOption)
|
|
130
127
|
availableOptions.push("auto");
|
|
128
|
+
if (toolName === BASH_TOOL_NAME || toolName === EXIT_PLAN_MODE_TOOL_NAME)
|
|
129
|
+
availableOptions.push("bypass");
|
|
131
130
|
availableOptions.push("alternative");
|
|
132
131
|
if (key.upArrow) {
|
|
133
132
|
const currentIndex = availableOptions.indexOf(state.selectedOption);
|
|
@@ -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) {
|
package/dist/stdio/protocol.d.ts
CHANGED
|
@@ -34,8 +34,8 @@ export declare const INVALID_REQUEST = -32600;
|
|
|
34
34
|
export declare const METHOD_NOT_FOUND = -32601;
|
|
35
35
|
export declare const INVALID_PARAMS = -32602;
|
|
36
36
|
export declare const INTERNAL_ERROR = -32603;
|
|
37
|
-
export type RequestMethod = "initialize" | "destroy" | "restoreSession" | "listSessions" | "getSessionInfo" | "sendMessage" | "bang" | "abortMessage" | "clearMessages" | "rewindToMessage" | "deleteQueuedMessage" | "updateQueuedMessage" | "deleteQueuedMessageById" | "getMessages" | "getFullMessageThread" | "setPermissionMode" | "getPermissionMode" | "getMcpServers" | "connectMcpServer" | "disconnectMcpServer" | "getSlashCommands" | "searchFiles" | "getPromptHistory" | "searchPromptHistory" | "updateConfig" | "getAuthStatus" | "login" | "logout" | "listPlugins" | "installPlugin" | "uninstallPlugin" | "enablePlugin" | "disablePlugin" | "updatePlugin" | "listMarketplaces" | "addMarketplace" | "removeMarketplace" | "updateMarketplace";
|
|
37
|
+
export type RequestMethod = "initialize" | "destroy" | "restoreSession" | "listSessions" | "getSessionInfo" | "sendMessage" | "bang" | "abortMessage" | "clearMessages" | "rewindToMessage" | "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;
|
package/dist/utils/worktree.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/utils/worktree.js
CHANGED
|
@@ -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
|
|
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 (
|
|
74
|
-
stderr.includes("
|
|
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.
|
|
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.
|
|
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:
|
|
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={
|
package/src/contexts/useChat.tsx
CHANGED
|
@@ -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/
|
|
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 {
|
|
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
|
-
|
|
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
|
|
158
|
-
|
|
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: "
|
|
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 === "
|
|
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) {
|
package/src/stdio/agentBridge.ts
CHANGED
|
@@ -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
|
|
package/src/stdio/protocol.ts
CHANGED
|
@@ -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
|
|
package/src/utils/worktree.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
-
|
|
94
|
-
stderr.includes("
|
|
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()!;
|