wave-code 1.0.3 → 1.0.5
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 +2 -2
- package/dist/components/App.js +5 -5
- package/dist/components/ChatInterface.js +1 -1
- package/dist/components/ConfirmationSelector.d.ts +2 -1
- package/dist/components/ConfirmationSelector.js +4 -3
- package/dist/components/InputBox.js +2 -1
- package/dist/constants/commands.js +6 -0
- package/dist/contexts/useChat.d.ts +3 -1
- package/dist/contexts/useChat.js +45 -3
- package/dist/hooks/useInputManager.js +5 -1
- package/dist/index.js +12 -0
- package/dist/managers/inputHandlers.js +3 -0
- package/dist/managers/inputReducer.d.ts +1 -0
- package/dist/print-cli.js +2 -1
- package/dist/reducers/confirmationReducer.d.ts +2 -1
- package/dist/reducers/confirmationReducer.js +3 -2
- package/dist/stdio/agentBridge.d.ts +8 -0
- package/dist/stdio/agentBridge.js +33 -1
- package/dist/stdio/protocol.d.ts +1 -1
- package/dist/types.d.ts +2 -0
- package/package.json +2 -2
- package/src/cli.tsx +2 -0
- package/src/components/App.tsx +5 -0
- package/src/components/ChatInterface.tsx +1 -0
- package/src/components/ConfirmationSelector.tsx +10 -3
- package/src/components/InputBox.tsx +2 -0
- package/src/constants/commands.ts +6 -0
- package/src/contexts/useChat.tsx +60 -0
- package/src/hooks/useInputManager.ts +4 -0
- package/src/index.ts +15 -0
- package/src/managers/inputHandlers.ts +2 -0
- package/src/managers/inputReducer.ts +1 -0
- package/src/print-cli.ts +2 -0
- package/src/reducers/confirmationReducer.ts +7 -2
- package/src/stdio/agentBridge.ts +42 -0
- package/src/stdio/protocol.ts +1 -0
- package/src/types.ts +2 -0
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,7 @@ import { App } from "./components/App.js";
|
|
|
4
4
|
import { cleanupLogs } from "./utils/logger.js";
|
|
5
5
|
import { removeWorktree } from "./utils/worktree.js";
|
|
6
6
|
export async function startCli(options) {
|
|
7
|
-
const { restoreSessionId, continueLastSession, bypassPermissions, permissionMode, pluginDirs, tools, allowedTools, disallowedTools, worktreeSession, workdir, originalCwd, version, model, mcpServers, } = options;
|
|
7
|
+
const { restoreSessionId, continueLastSession, bypassPermissions, permissionMode, pluginDirs, additionalDirectories, tools, allowedTools, disallowedTools, worktreeSession, workdir, originalCwd, version, model, mcpServers, } = options;
|
|
8
8
|
// Continue with ink-based UI for normal mode
|
|
9
9
|
let shouldRemoveWorktree = false;
|
|
10
10
|
// Enable bracketed paste (DECSET 2004) so terminals wrap pasted text in
|
|
@@ -22,7 +22,7 @@ export async function startCli(options) {
|
|
|
22
22
|
unmount();
|
|
23
23
|
};
|
|
24
24
|
// Render the application
|
|
25
|
-
const { unmount, waitUntilExit } = render(_jsx(App, { restoreSessionId: restoreSessionId, continueLastSession: continueLastSession, bypassPermissions: bypassPermissions, permissionMode: permissionMode, pluginDirs: pluginDirs, tools: tools, allowedTools: allowedTools, disallowedTools: disallowedTools, worktreeSession: worktreeSession, workdir: workdir, originalCwd: originalCwd, version: version, model: model, mcpServers: mcpServers, onExit: handleExit }), { exitOnCtrlC: false });
|
|
25
|
+
const { unmount, waitUntilExit } = render(_jsx(App, { restoreSessionId: restoreSessionId, continueLastSession: continueLastSession, bypassPermissions: bypassPermissions, permissionMode: permissionMode, pluginDirs: pluginDirs, additionalDirectories: additionalDirectories, tools: tools, allowedTools: allowedTools, disallowedTools: disallowedTools, worktreeSession: worktreeSession, workdir: workdir, originalCwd: originalCwd, version: version, model: model, mcpServers: mcpServers, onExit: handleExit }), { exitOnCtrlC: false });
|
|
26
26
|
// Wait for the app to finish unmounting
|
|
27
27
|
try {
|
|
28
28
|
await waitUntilExit();
|
package/dist/components/App.js
CHANGED
|
@@ -54,7 +54,7 @@ const ChatWithExitPrompt = ({ worktreeSession, onExit }) => {
|
|
|
54
54
|
}
|
|
55
55
|
return _jsx(ChatInterface, {});
|
|
56
56
|
};
|
|
57
|
-
const AppWithProviders = ({ bypassPermissions, permissionMode, pluginDirs, tools, allowedTools, disallowedTools, worktreeSession, workdir, originalCwd, version, model, mcpServers, onExit, }) => {
|
|
57
|
+
const AppWithProviders = ({ bypassPermissions, permissionMode, pluginDirs, additionalDirectories, tools, allowedTools, disallowedTools, worktreeSession, workdir, originalCwd, version, model, mcpServers, onExit, }) => {
|
|
58
58
|
// Handle Ctrl-C for non-worktree sessions (immediate exit)
|
|
59
59
|
// Ink runs terminal in raw mode, so Ctrl+C arrives as useInput event, not SIGINT
|
|
60
60
|
useInput((input, key) => {
|
|
@@ -67,10 +67,10 @@ const AppWithProviders = ({ bypassPermissions, permissionMode, pluginDirs, tools
|
|
|
67
67
|
}
|
|
68
68
|
});
|
|
69
69
|
if (worktreeSession) {
|
|
70
|
-
return (_jsx(ChatProvider, { bypassPermissions: bypassPermissions, permissionMode: permissionMode, pluginDirs: pluginDirs, tools: tools, allowedTools: allowedTools, disallowedTools: disallowedTools, workdir: workdir, worktreeSession: worktreeSession, originalCwd: originalCwd, version: version, model: model, mcpServers: mcpServers, children: _jsx(ChatWithExitPrompt, { worktreeSession: worktreeSession, onExit: onExit }) }));
|
|
70
|
+
return (_jsx(ChatProvider, { bypassPermissions: bypassPermissions, permissionMode: permissionMode, pluginDirs: pluginDirs, additionalDirectories: additionalDirectories, tools: tools, allowedTools: allowedTools, disallowedTools: disallowedTools, workdir: workdir, worktreeSession: worktreeSession, originalCwd: originalCwd, version: version, model: model, mcpServers: mcpServers, children: _jsx(ChatWithExitPrompt, { worktreeSession: worktreeSession, onExit: onExit }) }));
|
|
71
71
|
}
|
|
72
|
-
return (_jsx(ChatProvider, { bypassPermissions: bypassPermissions, permissionMode: permissionMode, pluginDirs: pluginDirs, tools: tools, allowedTools: allowedTools, disallowedTools: disallowedTools, workdir: workdir, worktreeSession: worktreeSession, originalCwd: originalCwd, version: version, model: model, mcpServers: mcpServers, children: _jsx(ChatInterface, {}) }));
|
|
72
|
+
return (_jsx(ChatProvider, { bypassPermissions: bypassPermissions, permissionMode: permissionMode, pluginDirs: pluginDirs, additionalDirectories: additionalDirectories, tools: tools, allowedTools: allowedTools, disallowedTools: disallowedTools, workdir: workdir, worktreeSession: worktreeSession, originalCwd: originalCwd, version: version, model: model, mcpServers: mcpServers, children: _jsx(ChatInterface, {}) }));
|
|
73
73
|
};
|
|
74
|
-
export const App = ({ restoreSessionId, continueLastSession, bypassPermissions, permissionMode, pluginDirs, tools, allowedTools, disallowedTools, worktreeSession, workdir, originalCwd, version, model, mcpServers, onExit, }) => {
|
|
75
|
-
return (_jsx(AppProvider, { restoreSessionId: restoreSessionId, continueLastSession: continueLastSession, children: _jsx(AppWithProviders, { bypassPermissions: bypassPermissions, permissionMode: permissionMode, pluginDirs: pluginDirs, tools: tools, allowedTools: allowedTools, disallowedTools: disallowedTools, worktreeSession: worktreeSession, workdir: workdir, originalCwd: originalCwd, version: version, model: model, mcpServers: mcpServers, onExit: onExit }) }));
|
|
74
|
+
export const App = ({ restoreSessionId, continueLastSession, bypassPermissions, permissionMode, pluginDirs, additionalDirectories, tools, allowedTools, disallowedTools, worktreeSession, workdir, originalCwd, version, model, mcpServers, onExit, }) => {
|
|
75
|
+
return (_jsx(AppProvider, { restoreSessionId: restoreSessionId, continueLastSession: continueLastSession, children: _jsx(AppWithProviders, { bypassPermissions: bypassPermissions, permissionMode: permissionMode, pluginDirs: pluginDirs, additionalDirectories: additionalDirectories, tools: tools, allowedTools: allowedTools, disallowedTools: disallowedTools, worktreeSession: worktreeSession, workdir: workdir, originalCwd: originalCwd, version: version, model: model, mcpServers: mcpServers, onExit: onExit }) }));
|
|
76
76
|
};
|
|
@@ -57,5 +57,5 @@ export const ChatInterface = () => {
|
|
|
57
57
|
]);
|
|
58
58
|
if (!sessionId)
|
|
59
59
|
return null;
|
|
60
|
-
return (_jsxs(Box, { ref: chatInterfaceRef, flexDirection: "column", children: [_jsx(MessageList, { messages: displayMessages, isExpanded: isExpanded, version: version, workdir: workdir, forceStatic: forceStatic }, remountKey), !isConfirmationVisible && !isExpanded && (_jsxs(_Fragment, { children: [(isLoading || isCommandRunning || isCompacting) && (_jsx(LoadingIndicator, { isLoading: isLoading, isCommandRunning: isCommandRunning, isCompacting: isCompacting, latestTotalTokens: latestTotalTokens })), _jsx(TaskList, {}), _jsx(QueuedMessageList, {}), _jsx(InputBox, { isLoading: isLoading, isCommandRunning: isCommandRunning, isCompacting: isCompacting, sendMessage: sendMessage, abortMessage: abortMessage, mcpServers: mcpServers, connectMcpServer: connectMcpServer, disconnectMcpServer: disconnectMcpServer, slashCommands: slashCommands, hasSlashCommand: hasSlashCommand, latestTotalTokens: latestTotalTokens, maxInputTokens: maxInputTokens, showLoginHint: showLoginHint })] })), isConfirmationVisible && (_jsxs(_Fragment, { children: [forceStatic ? (_jsx(Static, { items: [{ key: "confirmation-details" }], children: () => (_jsx(ConfirmationDetails, { toolName: confirmingTool.name, toolInput: confirmingTool.input, planContent: confirmingTool.planContent, isExpanded: isExpanded }, "confirmation-details")) })) : (_jsx(ConfirmationDetails, { toolName: confirmingTool.name, toolInput: confirmingTool.input, planContent: confirmingTool.planContent, isExpanded: isExpanded })), _jsx(ConfirmationSelector, { toolName: confirmingTool.name, toolInput: confirmingTool.input, suggestedPrefix: confirmingTool.suggestedPrefix, hidePersistentOption: confirmingTool.hidePersistentOption, isExpanded: isExpanded, onDecision: handleConfirmationDecision, onCancel: handleConfirmationCancel })] }))] }));
|
|
60
|
+
return (_jsxs(Box, { ref: chatInterfaceRef, flexDirection: "column", children: [_jsx(MessageList, { messages: displayMessages, isExpanded: isExpanded, version: version, workdir: workdir, forceStatic: forceStatic }, remountKey), !isConfirmationVisible && !isExpanded && (_jsxs(_Fragment, { children: [(isLoading || isCommandRunning || isCompacting) && (_jsx(LoadingIndicator, { isLoading: isLoading, isCommandRunning: isCommandRunning, isCompacting: isCompacting, latestTotalTokens: latestTotalTokens })), _jsx(TaskList, {}), _jsx(QueuedMessageList, {}), _jsx(InputBox, { isLoading: isLoading, isCommandRunning: isCommandRunning, isCompacting: isCompacting, sendMessage: sendMessage, abortMessage: abortMessage, mcpServers: mcpServers, connectMcpServer: connectMcpServer, disconnectMcpServer: disconnectMcpServer, slashCommands: slashCommands, hasSlashCommand: hasSlashCommand, latestTotalTokens: latestTotalTokens, maxInputTokens: maxInputTokens, showLoginHint: showLoginHint })] })), isConfirmationVisible && (_jsxs(_Fragment, { children: [forceStatic ? (_jsx(Static, { items: [{ key: "confirmation-details" }], children: () => (_jsx(ConfirmationDetails, { toolName: confirmingTool.name, toolInput: confirmingTool.input, planContent: confirmingTool.planContent, isExpanded: isExpanded }, "confirmation-details")) })) : (_jsx(ConfirmationDetails, { toolName: confirmingTool.name, toolInput: confirmingTool.input, planContent: confirmingTool.planContent, isExpanded: isExpanded })), _jsx(ConfirmationSelector, { toolName: confirmingTool.name, toolInput: confirmingTool.input, suggestedPrefix: confirmingTool.suggestedPrefix, hidePersistentOption: confirmingTool.hidePersistentOption, permissionMode: confirmingTool.permissionMode, isExpanded: isExpanded, onDecision: handleConfirmationDecision, onCancel: handleConfirmationCancel })] }))] }));
|
|
61
61
|
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import type { PermissionDecision } from "wave-agent-sdk";
|
|
2
|
+
import type { PermissionDecision, PermissionMode } from "wave-agent-sdk";
|
|
3
3
|
export interface ConfirmationSelectorProps {
|
|
4
4
|
toolName: string;
|
|
5
5
|
toolInput?: Record<string, unknown>;
|
|
6
6
|
suggestedPrefix?: string;
|
|
7
7
|
hidePersistentOption?: boolean;
|
|
8
|
+
permissionMode?: PermissionMode;
|
|
8
9
|
isExpanded?: boolean;
|
|
9
10
|
onDecision: (decision: PermissionDecision) => void;
|
|
10
11
|
onCancel: () => void;
|
|
@@ -12,7 +12,7 @@ const getHeaderColor = (header) => {
|
|
|
12
12
|
}
|
|
13
13
|
return colors[Math.abs(hash) % colors.length];
|
|
14
14
|
};
|
|
15
|
-
export const ConfirmationSelector = ({ toolName, toolInput, suggestedPrefix, hidePersistentOption, isExpanded = false, onDecision, onCancel, }) => {
|
|
15
|
+
export const ConfirmationSelector = ({ toolName, toolInput, suggestedPrefix, hidePersistentOption, permissionMode, isExpanded = false, onDecision, onCancel, }) => {
|
|
16
16
|
const [state, dispatch] = useReducer(confirmationReducer, {
|
|
17
17
|
selectedOption: "allow",
|
|
18
18
|
alternativeText: "",
|
|
@@ -86,6 +86,7 @@ export const ConfirmationSelector = ({ toolName, toolInput, suggestedPrefix, hid
|
|
|
86
86
|
toolInput,
|
|
87
87
|
suggestedPrefix,
|
|
88
88
|
hidePersistentOption,
|
|
89
|
+
permissionMode,
|
|
89
90
|
});
|
|
90
91
|
}
|
|
91
92
|
});
|
|
@@ -106,7 +107,7 @@ export const ConfirmationSelector = ({ toolName, toolInput, suggestedPrefix, hid
|
|
|
106
107
|
: "", 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
108
|
}) }), _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
109
|
? "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")), (toolName ===
|
|
110
|
-
toolName ===
|
|
110
|
+
: "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 === EXIT_PLAN_MODE_TOOL_NAME ||
|
|
111
|
+
(toolName === BASH_TOOL_NAME && permissionMode !== "plan")) && (_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" }) })] }))] }));
|
|
111
112
|
};
|
|
112
113
|
ConfirmationSelector.displayName = "ConfirmationSelector";
|
|
@@ -22,7 +22,7 @@ import { useChat } from "../contexts/useChat.js";
|
|
|
22
22
|
export const INPUT_PLACEHOLDER_TEXT = "Type your message (use /help for more info)...";
|
|
23
23
|
export const INPUT_PLACEHOLDER_TEXT_PREFIX = INPUT_PLACEHOLDER_TEXT.substring(0, 10);
|
|
24
24
|
export const InputBox = ({ isLoading, isCommandRunning, isCompacting, sendMessage = () => { }, abortMessage = () => { }, mcpServers = [], connectMcpServer = async () => false, disconnectMcpServer = async () => false, slashCommands = [], hasSlashCommand = () => false, latestTotalTokens = 0, maxInputTokens = 200000, showLoginHint = false, }) => {
|
|
25
|
-
const { permissionMode: chatPermissionMode, setPermissionMode: setChatPermissionMode, handleRewindSelect, backgroundCurrentTask, messages, getFullMessageThread, sessionId, workingDirectory, askBtw, clearMessages, compact, currentModel, configuredModels, setModel, recreateAgent, recallQueuedMessage, queuedMessages, setIsBtwActive, } = useChat();
|
|
25
|
+
const { permissionMode: chatPermissionMode, setPermissionMode: setChatPermissionMode, handleRewindSelect, backgroundCurrentTask, messages, getFullMessageThread, sessionId, workingDirectory, askBtw, clearMessages, compact, addDir, currentModel, configuredModels, setModel, recreateAgent, recallQueuedMessage, queuedMessages, setIsBtwActive, } = useChat();
|
|
26
26
|
// Ref to hold setInputText so queue callbacks can access it before useInputManager returns
|
|
27
27
|
const setInputTextRef = useRef(() => { });
|
|
28
28
|
const hasQueuedMessages = (queuedMessages?.length ?? 0) > 0;
|
|
@@ -62,6 +62,7 @@ export const InputBox = ({ isLoading, isCommandRunning, isCompacting, sendMessag
|
|
|
62
62
|
onAskBtw: askBtw,
|
|
63
63
|
onClearMessages: clearMessages,
|
|
64
64
|
onCompact: compact,
|
|
65
|
+
onAddDir: addDir,
|
|
65
66
|
onHasSlashCommand: hasSlashCommand,
|
|
66
67
|
onAbortMessage: abortMessage,
|
|
67
68
|
onBackgroundCurrentTask: backgroundCurrentTask,
|
|
@@ -77,4 +77,10 @@ export const AVAILABLE_COMMANDS = [
|
|
|
77
77
|
description: "Compact conversation history to reduce context usage",
|
|
78
78
|
handler: () => { },
|
|
79
79
|
},
|
|
80
|
+
{
|
|
81
|
+
id: "add-dir",
|
|
82
|
+
name: "add-dir",
|
|
83
|
+
description: "Add a directory to the session Safe Zone",
|
|
84
|
+
handler: () => { }, // Handler here won't be used, actual processing is in the hook
|
|
85
|
+
},
|
|
80
86
|
];
|
|
@@ -20,6 +20,7 @@ export interface ChatContextType {
|
|
|
20
20
|
askBtw: (question: string, abortSignal?: AbortSignal, onContent?: (content: string) => void) => Promise<string>;
|
|
21
21
|
clearMessages: () => Promise<void>;
|
|
22
22
|
compact: (instructions?: string) => Promise<void>;
|
|
23
|
+
addDir: (args?: string) => Promise<void>;
|
|
23
24
|
abortMessage: () => void;
|
|
24
25
|
recallQueuedMessage: () => QueuedMessage | null;
|
|
25
26
|
removeQueuedMessageById: (id: string) => boolean;
|
|
@@ -55,8 +56,9 @@ export interface ChatContextType {
|
|
|
55
56
|
suggestedPrefix?: string;
|
|
56
57
|
hidePersistentOption?: boolean;
|
|
57
58
|
planContent?: string;
|
|
59
|
+
permissionMode?: PermissionMode;
|
|
58
60
|
};
|
|
59
|
-
showConfirmation: (toolName: string, toolInput?: Record<string, unknown>, suggestedPrefix?: string, hidePersistentOption?: boolean, planContent?: string) => Promise<PermissionDecision>;
|
|
61
|
+
showConfirmation: (toolName: string, toolInput?: Record<string, unknown>, suggestedPrefix?: string, hidePersistentOption?: boolean, planContent?: string, permissionMode?: PermissionMode) => Promise<PermissionDecision>;
|
|
60
62
|
hideConfirmation: () => void;
|
|
61
63
|
handleConfirmationDecision: (decision: PermissionDecision) => void;
|
|
62
64
|
handleConfirmationCancel: () => void;
|
package/dist/contexts/useChat.js
CHANGED
|
@@ -15,7 +15,7 @@ export const useChat = () => {
|
|
|
15
15
|
}
|
|
16
16
|
return context;
|
|
17
17
|
};
|
|
18
|
-
export const ChatProvider = ({ children, bypassPermissions, permissionMode: initialPermissionMode, pluginDirs, tools, allowedTools, disallowedTools, workdir, worktreeSession, originalCwd, version, model, mcpServers, }) => {
|
|
18
|
+
export const ChatProvider = ({ children, bypassPermissions, permissionMode: initialPermissionMode, pluginDirs, additionalDirectories, tools, allowedTools, disallowedTools, workdir, worktreeSession, originalCwd, version, model, mcpServers, }) => {
|
|
19
19
|
const { restoreSessionId, continueLastSession } = useAppConfig();
|
|
20
20
|
const { stdout } = useStdout();
|
|
21
21
|
// Message Display State
|
|
@@ -169,7 +169,7 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
169
169
|
}
|
|
170
170
|
}, []);
|
|
171
171
|
// Permission confirmation methods with queue support
|
|
172
|
-
const showConfirmation = useCallback(async (toolName, toolInput, suggestedPrefix, hidePersistentOption, planContent) => {
|
|
172
|
+
const showConfirmation = useCallback(async (toolName, toolInput, suggestedPrefix, hidePersistentOption, planContent, permissionMode) => {
|
|
173
173
|
return new Promise((resolve, reject) => {
|
|
174
174
|
const queueItem = {
|
|
175
175
|
toolName,
|
|
@@ -177,6 +177,7 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
177
177
|
suggestedPrefix,
|
|
178
178
|
hidePersistentOption,
|
|
179
179
|
planContent,
|
|
180
|
+
permissionMode,
|
|
180
181
|
resolver: resolve,
|
|
181
182
|
reject,
|
|
182
183
|
};
|
|
@@ -354,7 +355,7 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
354
355
|
// Create the permission callback inside the try block to access showConfirmation
|
|
355
356
|
const permissionCallback = async (context) => {
|
|
356
357
|
try {
|
|
357
|
-
return await showConfirmation(context.toolName, context.toolInput, context.suggestedPrefix, context.hidePersistentOption, context.planContent);
|
|
358
|
+
return await showConfirmation(context.toolName, context.toolInput, context.suggestedPrefix, context.hidePersistentOption, context.planContent, context.permissionMode);
|
|
358
359
|
}
|
|
359
360
|
catch {
|
|
360
361
|
// If confirmation was cancelled or failed, deny the operation
|
|
@@ -374,6 +375,7 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
374
375
|
canUseTool: permissionCallback,
|
|
375
376
|
stream: true,
|
|
376
377
|
plugins: pluginDirs?.map((path) => ({ type: "local", path })),
|
|
378
|
+
additionalDirectories,
|
|
377
379
|
tools,
|
|
378
380
|
allowedTools,
|
|
379
381
|
disallowedTools,
|
|
@@ -426,6 +428,7 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
426
428
|
bypassPermissions,
|
|
427
429
|
showConfirmation,
|
|
428
430
|
pluginDirs,
|
|
431
|
+
additionalDirectories,
|
|
429
432
|
tools,
|
|
430
433
|
allowedTools,
|
|
431
434
|
disallowedTools,
|
|
@@ -545,6 +548,43 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
545
548
|
refreshMessages();
|
|
546
549
|
forceRemount();
|
|
547
550
|
}, [refreshMessages, forceRemount]);
|
|
551
|
+
// /add-dir: add a directory to the session Safe Zone (session-level, with
|
|
552
|
+
// optional --remember persistence). Bare command shows usage + current list.
|
|
553
|
+
const addDir = useCallback(async (args) => {
|
|
554
|
+
const agent = agentRef.current;
|
|
555
|
+
if (!agent)
|
|
556
|
+
return;
|
|
557
|
+
// Show the result as a UI-only assistant text message (same display path
|
|
558
|
+
// as error blocks; not persisted to session history).
|
|
559
|
+
const showResult = (content) => {
|
|
560
|
+
setMessages((prev) => [
|
|
561
|
+
...prev,
|
|
562
|
+
{
|
|
563
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
|
|
564
|
+
role: "assistant",
|
|
565
|
+
timestamp: new Date().toISOString(),
|
|
566
|
+
blocks: [{ type: "text", content, stage: "end" }],
|
|
567
|
+
},
|
|
568
|
+
]);
|
|
569
|
+
};
|
|
570
|
+
const rawArgs = (args ?? "").trim();
|
|
571
|
+
if (!rawArgs) {
|
|
572
|
+
const dirs = agent.getAdditionalDirectories();
|
|
573
|
+
const list = dirs.length > 0
|
|
574
|
+
? dirs.map((dir) => ` - ${dir}`).join("\n")
|
|
575
|
+
: " (none)";
|
|
576
|
+
showResult(`Usage: /add-dir <path> [--remember]\n\nAdditional working directories:\n${list}`);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const remember = rawArgs.includes("--remember");
|
|
580
|
+
const dir = rawArgs.replace("--remember", "").trim();
|
|
581
|
+
if (!dir) {
|
|
582
|
+
showResult("Usage: /add-dir <path> [--remember]");
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
await agent.addAdditionalDirectory(dir, { remember });
|
|
586
|
+
showResult(`Added ${dir} to the session Safe Zone${remember ? " (remembered)" : ""}.`);
|
|
587
|
+
}, []);
|
|
548
588
|
// Unified interrupt method, interrupt both AI messages and command execution
|
|
549
589
|
const abortMessage = useCallback(() => {
|
|
550
590
|
agentRef.current?.abortMessage();
|
|
@@ -603,6 +643,7 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
603
643
|
suggestedPrefix: next.suggestedPrefix,
|
|
604
644
|
hidePersistentOption: next.hidePersistentOption,
|
|
605
645
|
planContent: next.planContent,
|
|
646
|
+
permissionMode: next.permissionMode,
|
|
606
647
|
});
|
|
607
648
|
setIsConfirmationVisible(true);
|
|
608
649
|
setConfirmationQueue((prev) => prev.slice(1));
|
|
@@ -716,6 +757,7 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
|
|
|
716
757
|
askBtw,
|
|
717
758
|
clearMessages,
|
|
718
759
|
compact,
|
|
760
|
+
addDir,
|
|
719
761
|
abortMessage,
|
|
720
762
|
recallQueuedMessage,
|
|
721
763
|
removeQueuedMessageById,
|
|
@@ -14,7 +14,7 @@ export const useInputManager = (callbacks = {}) => {
|
|
|
14
14
|
// dismissed ref suppresses the late SET_BTW_STATE dispatch / error logging.
|
|
15
15
|
const btwAbortRef = useRef(null);
|
|
16
16
|
const btwDismissedRef = useRef(false);
|
|
17
|
-
const { onInputTextChange, onCursorPositionChange, onFileSelectorStateChange, onCommandSelectorStateChange, onHistorySearchStateChange, onBackgroundTaskManagerStateChange, onMcpManagerStateChange, onRewindManagerStateChange, onHelpStateChange, onStatusCommandStateChange, onPluginManagerStateChange, onModelSelectorStateChange, onWorkflowManagerStateChange, onImagesStateChange, onSendMessage, onHasSlashCommand, onAbortMessage, onBackgroundCurrentTask, onPermissionModeChange, onAskBtw, sessionId, workdir, getFullMessageThread, logger, hasQueuedMessages: hasQueuedMessagesProp, onRecallQueuedMessage, onClearMessages, onCompact, isIdle: isIdleProp, } = callbacks;
|
|
17
|
+
const { onInputTextChange, onCursorPositionChange, onFileSelectorStateChange, onCommandSelectorStateChange, onHistorySearchStateChange, onBackgroundTaskManagerStateChange, onMcpManagerStateChange, onRewindManagerStateChange, onHelpStateChange, onStatusCommandStateChange, onPluginManagerStateChange, onModelSelectorStateChange, onWorkflowManagerStateChange, onImagesStateChange, onSendMessage, onHasSlashCommand, onAbortMessage, onBackgroundCurrentTask, onPermissionModeChange, onAskBtw, sessionId, workdir, getFullMessageThread, logger, hasQueuedMessages: hasQueuedMessagesProp, onRecallQueuedMessage, onClearMessages, onCompact, onAddDir, isIdle: isIdleProp, } = callbacks;
|
|
18
18
|
// Handle debounced file search
|
|
19
19
|
useEffect(() => {
|
|
20
20
|
if (state.showFileSelector) {
|
|
@@ -216,6 +216,9 @@ export const useInputManager = (callbacks = {}) => {
|
|
|
216
216
|
else if (command === "compact") {
|
|
217
217
|
await onCompact?.(effect.args);
|
|
218
218
|
}
|
|
219
|
+
else if (command === "add-dir") {
|
|
220
|
+
await onAddDir?.(effect.args);
|
|
221
|
+
}
|
|
219
222
|
}
|
|
220
223
|
break;
|
|
221
224
|
case "RECALL_QUEUED_MESSAGE":
|
|
@@ -243,6 +246,7 @@ export const useInputManager = (callbacks = {}) => {
|
|
|
243
246
|
onRecallQueuedMessage,
|
|
244
247
|
onClearMessages,
|
|
245
248
|
onCompact,
|
|
249
|
+
onAddDir,
|
|
246
250
|
]);
|
|
247
251
|
useEffect(() => {
|
|
248
252
|
onFileSelectorStateChange?.(state.showFileSelector, state.filteredFiles, state.fileSearchQuery, state.atPosition);
|
package/dist/index.js
CHANGED
|
@@ -78,6 +78,12 @@ export async function main() {
|
|
|
78
78
|
type: "array",
|
|
79
79
|
string: true,
|
|
80
80
|
global: false,
|
|
81
|
+
})
|
|
82
|
+
.option("add-dir", {
|
|
83
|
+
description: "Add a directory to the session Safe Zone (repeatable)",
|
|
84
|
+
type: "array",
|
|
85
|
+
string: true,
|
|
86
|
+
global: false,
|
|
81
87
|
})
|
|
82
88
|
.option("tools", {
|
|
83
89
|
description: 'Specify a comma-separated list of tools to enable (e.g., \'Bash,Read,Write\'). Use "" to disable all, "default" for all.',
|
|
@@ -233,6 +239,9 @@ export async function main() {
|
|
|
233
239
|
}
|
|
234
240
|
// Resolve plugin directories to absolute paths before any worktree logic
|
|
235
241
|
const pluginDirs = argv.pluginDir?.map((dir) => path.resolve(originalCwd, dir));
|
|
242
|
+
// Resolve additional directories to absolute paths (relative to the
|
|
243
|
+
// original working directory, before any worktree change)
|
|
244
|
+
const additionalDirectories = argv.addDir?.map((dir) => path.resolve(originalCwd, dir));
|
|
236
245
|
let worktreeSession;
|
|
237
246
|
if (argv.worktree !== undefined ||
|
|
238
247
|
process.argv.includes("-w") ||
|
|
@@ -267,6 +276,7 @@ export async function main() {
|
|
|
267
276
|
bypassPermissions: argv.dangerouslySkipPermissions,
|
|
268
277
|
permissionMode: argv.permissionMode,
|
|
269
278
|
pluginDirs,
|
|
279
|
+
additionalDirectories,
|
|
270
280
|
tools,
|
|
271
281
|
allowedTools,
|
|
272
282
|
disallowedTools,
|
|
@@ -289,6 +299,7 @@ export async function main() {
|
|
|
289
299
|
bypassPermissions: argv.dangerouslySkipPermissions,
|
|
290
300
|
permissionMode: argv.permissionMode,
|
|
291
301
|
pluginDirs,
|
|
302
|
+
additionalDirectories,
|
|
292
303
|
tools,
|
|
293
304
|
allowedTools,
|
|
294
305
|
disallowedTools,
|
|
@@ -316,6 +327,7 @@ export async function main() {
|
|
|
316
327
|
bypassPermissions: argv.dangerouslySkipPermissions,
|
|
317
328
|
permissionMode: argv.permissionMode,
|
|
318
329
|
pluginDirs,
|
|
330
|
+
additionalDirectories,
|
|
319
331
|
tools,
|
|
320
332
|
allowedTools,
|
|
321
333
|
disallowedTools,
|
|
@@ -300,6 +300,9 @@ export const handleCommandSelect = (state, dispatch, callbacks, command) => {
|
|
|
300
300
|
else if (command === "compact") {
|
|
301
301
|
await callbacks.onCompact?.();
|
|
302
302
|
}
|
|
303
|
+
else if (command === "add-dir") {
|
|
304
|
+
await callbacks.onAddDir?.();
|
|
305
|
+
}
|
|
303
306
|
}
|
|
304
307
|
})();
|
|
305
308
|
dispatch({ type: "CANCEL_COMMAND_SELECTOR" });
|
|
@@ -81,6 +81,7 @@ export interface InputManagerCallbacks {
|
|
|
81
81
|
onAskBtw?: (question: string, abortSignal?: AbortSignal, onContent?: (content: string) => void) => Promise<string>;
|
|
82
82
|
onClearMessages?: () => Promise<void>;
|
|
83
83
|
onCompact?: (instructions?: string) => Promise<void>;
|
|
84
|
+
onAddDir?: (args?: string) => Promise<void>;
|
|
84
85
|
sessionId?: string;
|
|
85
86
|
workdir?: string;
|
|
86
87
|
getFullMessageThread?: () => Promise<{
|
package/dist/print-cli.js
CHANGED
|
@@ -14,7 +14,7 @@ function displayTimingInfo(startTime, showStats) {
|
|
|
14
14
|
}
|
|
15
15
|
export async function startPrintCli(options) {
|
|
16
16
|
const startTime = new Date();
|
|
17
|
-
const { restoreSessionId, continueLastSession, message, showStats = false, bypassPermissions, permissionMode, pluginDirs, tools, allowedTools, disallowedTools, worktreeSession, workdir, originalCwd, model, mcpServers, } = options;
|
|
17
|
+
const { restoreSessionId, continueLastSession, message, showStats = false, bypassPermissions, permissionMode, pluginDirs, additionalDirectories, tools, allowedTools, disallowedTools, worktreeSession, workdir, originalCwd, model, mcpServers, } = options;
|
|
18
18
|
if ((typeof message !== "string" || message.trim() === "") &&
|
|
19
19
|
!continueLastSession &&
|
|
20
20
|
!restoreSessionId) {
|
|
@@ -77,6 +77,7 @@ export async function startPrintCli(options) {
|
|
|
77
77
|
continueLastSession,
|
|
78
78
|
permissionMode: permissionMode || (bypassPermissions ? "bypassPermissions" : undefined),
|
|
79
79
|
plugins: pluginDirs?.map((path) => ({ type: "local", path })),
|
|
80
|
+
additionalDirectories,
|
|
80
81
|
tools,
|
|
81
82
|
allowedTools,
|
|
82
83
|
disallowedTools,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Key } from "ink";
|
|
2
|
-
import type { PermissionDecision } from "wave-agent-sdk";
|
|
2
|
+
import type { PermissionDecision, PermissionMode } from "wave-agent-sdk";
|
|
3
3
|
export interface ConfirmationState {
|
|
4
4
|
selectedOption: "auto" | "bypass" | "allow" | "alternative";
|
|
5
5
|
alternativeText: string;
|
|
@@ -32,5 +32,6 @@ export type ConfirmationAction = {
|
|
|
32
32
|
toolInput?: Record<string, unknown>;
|
|
33
33
|
suggestedPrefix?: string;
|
|
34
34
|
hidePersistentOption?: boolean;
|
|
35
|
+
permissionMode?: PermissionMode;
|
|
35
36
|
};
|
|
36
37
|
export declare function confirmationReducer(state: ConfirmationState, action: ConfirmationAction): ConfirmationState;
|
|
@@ -44,7 +44,7 @@ export function confirmationReducer(state, action) {
|
|
|
44
44
|
case "CLEAR_DECISION":
|
|
45
45
|
return { ...state, decision: null };
|
|
46
46
|
case "HANDLE_KEY": {
|
|
47
|
-
const { input, key, toolName, toolInput, suggestedPrefix, hidePersistentOption, } = action;
|
|
47
|
+
const { input, key, toolName, toolInput, suggestedPrefix, hidePersistentOption, permissionMode, } = action;
|
|
48
48
|
if (key.return) {
|
|
49
49
|
let decision = null;
|
|
50
50
|
if (state.selectedOption === "allow") {
|
|
@@ -125,7 +125,8 @@ export function confirmationReducer(state, action) {
|
|
|
125
125
|
availableOptions.push("allow");
|
|
126
126
|
if (!hidePersistentOption)
|
|
127
127
|
availableOptions.push("auto");
|
|
128
|
-
if (toolName ===
|
|
128
|
+
if (toolName === EXIT_PLAN_MODE_TOOL_NAME ||
|
|
129
|
+
(toolName === BASH_TOOL_NAME && permissionMode !== "plan"))
|
|
129
130
|
availableOptions.push("bypass");
|
|
130
131
|
availableOptions.push("alternative");
|
|
131
132
|
if (key.upArrow) {
|
|
@@ -80,6 +80,14 @@ export declare class AgentBridge {
|
|
|
80
80
|
private disconnectMcpServer;
|
|
81
81
|
private getSlashCommands;
|
|
82
82
|
private searchFiles;
|
|
83
|
+
/**
|
|
84
|
+
* Writes an uploaded file (from the desktop/webview "+上传文件" flow) into the
|
|
85
|
+
* artifacts dir on THIS machine — i.e. where the agent runs. Remote sessions
|
|
86
|
+
* call this over the ssh tunnel so the returned path is reachable by the
|
|
87
|
+
* remote agent's tools; local sessions never hit this (desktop writes locally
|
|
88
|
+
* itself).
|
|
89
|
+
*/
|
|
90
|
+
private writeArtifactFile;
|
|
83
91
|
private getPromptHistory;
|
|
84
92
|
private searchPromptHistory;
|
|
85
93
|
private canUseTool;
|
|
@@ -15,8 +15,11 @@
|
|
|
15
15
|
* - Handle config updates by destroying and recreating the Agent
|
|
16
16
|
*/
|
|
17
17
|
import { Agent, listSessions, searchFiles, generateRandomName, getDefaultRemoteBranch, getMessageContent, PromptHistoryManager, AuthService, PluginCore, validateWorktreeRemovalPath, } from "wave-agent-sdk";
|
|
18
|
-
import { INTERNAL_ERROR as PROTOCOL_INTERNAL_ERROR, METHOD_NOT_FOUND as PROTOCOL_METHOD_NOT_FOUND, } from "./protocol.js";
|
|
18
|
+
import { INVALID_PARAMS as PROTOCOL_INVALID_PARAMS, INTERNAL_ERROR as PROTOCOL_INTERNAL_ERROR, METHOD_NOT_FOUND as PROTOCOL_METHOD_NOT_FOUND, } from "./protocol.js";
|
|
19
19
|
import { execFileSync } from "node:child_process";
|
|
20
|
+
import { mkdirSync, existsSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { tmpdir } from "node:os";
|
|
22
|
+
import { basename, extname, join } from "node:path";
|
|
20
23
|
import { createWorktree, removeWorktree } from "../utils/worktree.js";
|
|
21
24
|
import { logger } from "../utils/logger.js";
|
|
22
25
|
import { isUserCheckpointMessage } from "../utils/rewindCheckpoints.js";
|
|
@@ -92,6 +95,8 @@ export class AgentBridge {
|
|
|
92
95
|
// ── File / History (global — no session required) ──
|
|
93
96
|
case "searchFiles":
|
|
94
97
|
return this.searchFiles(p, sessionId);
|
|
98
|
+
case "writeArtifactFile":
|
|
99
|
+
return this.writeArtifactFile(p);
|
|
95
100
|
case "getPromptHistory":
|
|
96
101
|
return this.getPromptHistory(p.workdir, sessionId);
|
|
97
102
|
case "searchPromptHistory":
|
|
@@ -588,6 +593,33 @@ export class AgentBridge {
|
|
|
588
593
|
});
|
|
589
594
|
return { files };
|
|
590
595
|
}
|
|
596
|
+
/**
|
|
597
|
+
* Writes an uploaded file (from the desktop/webview "+上传文件" flow) into the
|
|
598
|
+
* artifacts dir on THIS machine — i.e. where the agent runs. Remote sessions
|
|
599
|
+
* call this over the ssh tunnel so the returned path is reachable by the
|
|
600
|
+
* remote agent's tools; local sessions never hit this (desktop writes locally
|
|
601
|
+
* itself).
|
|
602
|
+
*/
|
|
603
|
+
async writeArtifactFile(params) {
|
|
604
|
+
// Artifacts are keyed by basename only — reject anything that could escape
|
|
605
|
+
// the artifacts dir via path traversal.
|
|
606
|
+
const safeName = basename(params.name || "");
|
|
607
|
+
if (!safeName || safeName === "." || safeName === "..") {
|
|
608
|
+
throw new RpcError(PROTOCOL_INVALID_PARAMS, `Invalid artifact file name: ${params.name}`);
|
|
609
|
+
}
|
|
610
|
+
const artifactsDir = join(tmpdir(), "wave-artifacts");
|
|
611
|
+
mkdirSync(artifactsDir, { recursive: true });
|
|
612
|
+
let finalPath = join(artifactsDir, safeName);
|
|
613
|
+
let counter = 1;
|
|
614
|
+
while (existsSync(finalPath)) {
|
|
615
|
+
const ext = extname(safeName);
|
|
616
|
+
const baseName = basename(safeName, ext);
|
|
617
|
+
finalPath = join(artifactsDir, `${baseName}_${counter}${ext}`);
|
|
618
|
+
counter++;
|
|
619
|
+
}
|
|
620
|
+
writeFileSync(finalPath, Buffer.from(params.contentBase64 || "", "base64"));
|
|
621
|
+
return { path: finalPath };
|
|
622
|
+
}
|
|
591
623
|
async getPromptHistory(workdir, sessionId) {
|
|
592
624
|
const history = await PromptHistoryManager.getHistory({
|
|
593
625
|
workdir: workdir || this.getSessionWorkdir(sessionId),
|
package/dist/stdio/protocol.d.ts
CHANGED
|
@@ -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" | "askBtw" | "abortMessage" | "clearMessages" | "rewindToMessage" | "listRewindCheckpoints" | "deleteQueuedMessage" | "updateQueuedMessage" | "deleteQueuedMessageById" | "getMessages" | "getFullMessageThread" | "setPermissionMode" | "getPermissionMode" | "getMcpServers" | "connectMcpServer" | "disconnectMcpServer" | "getSlashCommands" | "searchFiles" | "getPromptHistory" | "searchPromptHistory" | "updateConfig" | "listPendingPermissions" | "getAuthStatus" | "login" | "logout" | "listPlugins" | "installPlugin" | "uninstallPlugin" | "enablePlugin" | "disablePlugin" | "updatePlugin" | "listMarketplaces" | "addMarketplace" | "removeMarketplace" | "updateMarketplace" | "compact" | "getBackgroundTaskOutput" | "stopBackgroundTask" | "getWorkflowRuns" | "stopWorkflowRun" | "listGitBranches" | "createWorktree" | "removeWorktree";
|
|
37
|
+
export type RequestMethod = "initialize" | "destroy" | "restoreSession" | "listSessions" | "getSessionInfo" | "sendMessage" | "bang" | "askBtw" | "abortMessage" | "clearMessages" | "rewindToMessage" | "listRewindCheckpoints" | "deleteQueuedMessage" | "updateQueuedMessage" | "deleteQueuedMessageById" | "getMessages" | "getFullMessageThread" | "setPermissionMode" | "getPermissionMode" | "getMcpServers" | "connectMcpServer" | "disconnectMcpServer" | "getSlashCommands" | "searchFiles" | "writeArtifactFile" | "getPromptHistory" | "searchPromptHistory" | "updateConfig" | "listPendingPermissions" | "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 = "userMessageAdded" | "assistantMessageAdded" | "assistantContentUpdated" | "assistantReasoningUpdated" | "toolBlockUpdated" | "errorBlockAdded" | "loadingChange" | "commandRunningChange" | "queuedMessagesChange" | "tasksChange" | "sessionIdChange" | "permissionModeChange" | "mcpServersChange" | "workdirChange" | "bangMessageAdded" | "bangMessageUpdated" | "bangMessageCompleted" | "notificationMessageAdded" | "permissionRequest" | "authUrl" | "compactBlockAdded" | "compactionStateChange" | "backgroundTasksChange" | "btwContent";
|
|
40
40
|
export declare function isRequest(msg: unknown): msg is JsonRpcRequest;
|
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export interface BaseAppProps {
|
|
|
4
4
|
bypassPermissions?: boolean;
|
|
5
5
|
permissionMode?: PermissionMode;
|
|
6
6
|
pluginDirs?: string[];
|
|
7
|
+
/** Additional directories to add to the session Safe Zone (from --add-dir). */
|
|
8
|
+
additionalDirectories?: string[];
|
|
7
9
|
tools?: string[];
|
|
8
10
|
allowedTools?: string[];
|
|
9
11
|
disallowedTools?: string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "CLI-based code assistant powered by AI, built with React and Ink",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"wrap-ansi": "^10.0.0",
|
|
44
44
|
"yargs": "^17.7.2",
|
|
45
45
|
"zod": "^3.23.8",
|
|
46
|
-
"wave-agent-sdk": "1.0.
|
|
46
|
+
"wave-agent-sdk": "1.0.5"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/react": "^19.1.8",
|
package/src/cli.tsx
CHANGED
|
@@ -17,6 +17,7 @@ export async function startCli(options: CliOptions): Promise<void> {
|
|
|
17
17
|
bypassPermissions,
|
|
18
18
|
permissionMode,
|
|
19
19
|
pluginDirs,
|
|
20
|
+
additionalDirectories,
|
|
20
21
|
tools,
|
|
21
22
|
allowedTools,
|
|
22
23
|
disallowedTools,
|
|
@@ -55,6 +56,7 @@ export async function startCli(options: CliOptions): Promise<void> {
|
|
|
55
56
|
bypassPermissions={bypassPermissions}
|
|
56
57
|
permissionMode={permissionMode}
|
|
57
58
|
pluginDirs={pluginDirs}
|
|
59
|
+
additionalDirectories={additionalDirectories}
|
|
58
60
|
tools={tools}
|
|
59
61
|
allowedTools={allowedTools}
|
|
60
62
|
disallowedTools={disallowedTools}
|
package/src/components/App.tsx
CHANGED
|
@@ -96,6 +96,7 @@ const AppWithProviders: React.FC<AppWithProvidersProps> = ({
|
|
|
96
96
|
bypassPermissions,
|
|
97
97
|
permissionMode,
|
|
98
98
|
pluginDirs,
|
|
99
|
+
additionalDirectories,
|
|
99
100
|
tools,
|
|
100
101
|
allowedTools,
|
|
101
102
|
disallowedTools,
|
|
@@ -124,6 +125,7 @@ const AppWithProviders: React.FC<AppWithProvidersProps> = ({
|
|
|
124
125
|
bypassPermissions={bypassPermissions}
|
|
125
126
|
permissionMode={permissionMode}
|
|
126
127
|
pluginDirs={pluginDirs}
|
|
128
|
+
additionalDirectories={additionalDirectories}
|
|
127
129
|
tools={tools}
|
|
128
130
|
allowedTools={allowedTools}
|
|
129
131
|
disallowedTools={disallowedTools}
|
|
@@ -144,6 +146,7 @@ const AppWithProviders: React.FC<AppWithProvidersProps> = ({
|
|
|
144
146
|
bypassPermissions={bypassPermissions}
|
|
145
147
|
permissionMode={permissionMode}
|
|
146
148
|
pluginDirs={pluginDirs}
|
|
149
|
+
additionalDirectories={additionalDirectories}
|
|
147
150
|
tools={tools}
|
|
148
151
|
allowedTools={allowedTools}
|
|
149
152
|
disallowedTools={disallowedTools}
|
|
@@ -165,6 +168,7 @@ export const App: React.FC<AppProps> = ({
|
|
|
165
168
|
bypassPermissions,
|
|
166
169
|
permissionMode,
|
|
167
170
|
pluginDirs,
|
|
171
|
+
additionalDirectories,
|
|
168
172
|
tools,
|
|
169
173
|
allowedTools,
|
|
170
174
|
disallowedTools,
|
|
@@ -185,6 +189,7 @@ export const App: React.FC<AppProps> = ({
|
|
|
185
189
|
bypassPermissions={bypassPermissions}
|
|
186
190
|
permissionMode={permissionMode}
|
|
187
191
|
pluginDirs={pluginDirs}
|
|
192
|
+
additionalDirectories={additionalDirectories}
|
|
188
193
|
tools={tools}
|
|
189
194
|
allowedTools={allowedTools}
|
|
190
195
|
disallowedTools={disallowedTools}
|
|
@@ -159,6 +159,7 @@ export const ChatInterface: React.FC = () => {
|
|
|
159
159
|
toolInput={confirmingTool!.input}
|
|
160
160
|
suggestedPrefix={confirmingTool!.suggestedPrefix}
|
|
161
161
|
hidePersistentOption={confirmingTool!.hidePersistentOption}
|
|
162
|
+
permissionMode={confirmingTool!.permissionMode}
|
|
162
163
|
isExpanded={isExpanded}
|
|
163
164
|
onDecision={handleConfirmationDecision}
|
|
164
165
|
onCancel={handleConfirmationCancel}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import React, { useEffect, useReducer } from "react";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
PermissionDecision,
|
|
5
|
+
PermissionMode,
|
|
6
|
+
AskUserQuestionInput,
|
|
7
|
+
} from "wave-agent-sdk";
|
|
4
8
|
import {
|
|
5
9
|
BASH_TOOL_NAME,
|
|
6
10
|
EXIT_PLAN_MODE_TOOL_NAME,
|
|
@@ -24,6 +28,7 @@ export interface ConfirmationSelectorProps {
|
|
|
24
28
|
toolInput?: Record<string, unknown>;
|
|
25
29
|
suggestedPrefix?: string;
|
|
26
30
|
hidePersistentOption?: boolean;
|
|
31
|
+
permissionMode?: PermissionMode;
|
|
27
32
|
isExpanded?: boolean;
|
|
28
33
|
onDecision: (decision: PermissionDecision) => void;
|
|
29
34
|
onCancel: () => void;
|
|
@@ -34,6 +39,7 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
34
39
|
toolInput,
|
|
35
40
|
suggestedPrefix,
|
|
36
41
|
hidePersistentOption,
|
|
42
|
+
permissionMode,
|
|
37
43
|
isExpanded = false,
|
|
38
44
|
onDecision,
|
|
39
45
|
onCancel,
|
|
@@ -119,6 +125,7 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
119
125
|
toolInput,
|
|
120
126
|
suggestedPrefix,
|
|
121
127
|
hidePersistentOption,
|
|
128
|
+
permissionMode,
|
|
122
129
|
});
|
|
123
130
|
}
|
|
124
131
|
});
|
|
@@ -240,8 +247,8 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
240
247
|
</Text>
|
|
241
248
|
</Box>
|
|
242
249
|
)}
|
|
243
|
-
{(toolName ===
|
|
244
|
-
toolName ===
|
|
250
|
+
{(toolName === EXIT_PLAN_MODE_TOOL_NAME ||
|
|
251
|
+
(toolName === BASH_TOOL_NAME && permissionMode !== "plan")) && (
|
|
245
252
|
<Box key="bypass-option">
|
|
246
253
|
<Text
|
|
247
254
|
color={state.selectedOption === "bypass" ? "black" : "white"}
|
|
@@ -81,6 +81,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
81
81
|
askBtw,
|
|
82
82
|
clearMessages,
|
|
83
83
|
compact,
|
|
84
|
+
addDir,
|
|
84
85
|
currentModel,
|
|
85
86
|
configuredModels,
|
|
86
87
|
setModel,
|
|
@@ -166,6 +167,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
166
167
|
onAskBtw: askBtw,
|
|
167
168
|
onClearMessages: clearMessages,
|
|
168
169
|
onCompact: compact,
|
|
170
|
+
onAddDir: addDir,
|
|
169
171
|
onHasSlashCommand: hasSlashCommand,
|
|
170
172
|
onAbortMessage: abortMessage,
|
|
171
173
|
onBackgroundCurrentTask: backgroundCurrentTask,
|
|
@@ -80,4 +80,10 @@ export const AVAILABLE_COMMANDS: SlashCommand[] = [
|
|
|
80
80
|
description: "Compact conversation history to reduce context usage",
|
|
81
81
|
handler: () => {},
|
|
82
82
|
},
|
|
83
|
+
{
|
|
84
|
+
id: "add-dir",
|
|
85
|
+
name: "add-dir",
|
|
86
|
+
description: "Add a directory to the session Safe Zone",
|
|
87
|
+
handler: () => {}, // Handler here won't be used, actual processing is in the hook
|
|
88
|
+
},
|
|
83
89
|
];
|
package/src/contexts/useChat.tsx
CHANGED
|
@@ -64,6 +64,7 @@ export interface ChatContextType {
|
|
|
64
64
|
) => Promise<string>;
|
|
65
65
|
clearMessages: () => Promise<void>;
|
|
66
66
|
compact: (instructions?: string) => Promise<void>;
|
|
67
|
+
addDir: (args?: string) => Promise<void>;
|
|
67
68
|
abortMessage: () => void;
|
|
68
69
|
recallQueuedMessage: () => QueuedMessage | null;
|
|
69
70
|
removeQueuedMessageById: (id: string) => boolean;
|
|
@@ -107,6 +108,7 @@ export interface ChatContextType {
|
|
|
107
108
|
suggestedPrefix?: string;
|
|
108
109
|
hidePersistentOption?: boolean;
|
|
109
110
|
planContent?: string;
|
|
111
|
+
permissionMode?: PermissionMode;
|
|
110
112
|
};
|
|
111
113
|
showConfirmation: (
|
|
112
114
|
toolName: string,
|
|
@@ -114,6 +116,7 @@ export interface ChatContextType {
|
|
|
114
116
|
suggestedPrefix?: string,
|
|
115
117
|
hidePersistentOption?: boolean,
|
|
116
118
|
planContent?: string,
|
|
119
|
+
permissionMode?: PermissionMode,
|
|
117
120
|
) => Promise<PermissionDecision>;
|
|
118
121
|
hideConfirmation: () => void;
|
|
119
122
|
handleConfirmationDecision: (decision: PermissionDecision) => void;
|
|
@@ -160,6 +163,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
160
163
|
bypassPermissions,
|
|
161
164
|
permissionMode: initialPermissionMode,
|
|
162
165
|
pluginDirs,
|
|
166
|
+
additionalDirectories,
|
|
163
167
|
tools,
|
|
164
168
|
allowedTools,
|
|
165
169
|
disallowedTools,
|
|
@@ -361,6 +365,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
361
365
|
suggestedPrefix?: string;
|
|
362
366
|
hidePersistentOption?: boolean;
|
|
363
367
|
planContent?: string;
|
|
368
|
+
permissionMode?: PermissionMode;
|
|
364
369
|
}
|
|
365
370
|
| undefined
|
|
366
371
|
>();
|
|
@@ -371,6 +376,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
371
376
|
suggestedPrefix?: string;
|
|
372
377
|
hidePersistentOption?: boolean;
|
|
373
378
|
planContent?: string;
|
|
379
|
+
permissionMode?: PermissionMode;
|
|
374
380
|
resolver: (decision: PermissionDecision) => void;
|
|
375
381
|
reject: () => void;
|
|
376
382
|
}>
|
|
@@ -381,6 +387,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
381
387
|
suggestedPrefix?: string;
|
|
382
388
|
hidePersistentOption?: boolean;
|
|
383
389
|
planContent?: string;
|
|
390
|
+
permissionMode?: PermissionMode;
|
|
384
391
|
resolver: (decision: PermissionDecision) => void;
|
|
385
392
|
reject: () => void;
|
|
386
393
|
} | null>(null);
|
|
@@ -421,6 +428,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
421
428
|
suggestedPrefix?: string,
|
|
422
429
|
hidePersistentOption?: boolean,
|
|
423
430
|
planContent?: string,
|
|
431
|
+
permissionMode?: PermissionMode,
|
|
424
432
|
): Promise<PermissionDecision> => {
|
|
425
433
|
return new Promise<PermissionDecision>((resolve, reject) => {
|
|
426
434
|
const queueItem = {
|
|
@@ -429,6 +437,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
429
437
|
suggestedPrefix,
|
|
430
438
|
hidePersistentOption,
|
|
431
439
|
planContent,
|
|
440
|
+
permissionMode,
|
|
432
441
|
resolver: resolve,
|
|
433
442
|
reject,
|
|
434
443
|
};
|
|
@@ -632,6 +641,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
632
641
|
context.suggestedPrefix,
|
|
633
642
|
context.hidePersistentOption,
|
|
634
643
|
context.planContent,
|
|
644
|
+
context.permissionMode,
|
|
635
645
|
);
|
|
636
646
|
} catch {
|
|
637
647
|
// If confirmation was cancelled or failed, deny the operation
|
|
@@ -653,6 +663,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
653
663
|
canUseTool: permissionCallback,
|
|
654
664
|
stream: true,
|
|
655
665
|
plugins: pluginDirs?.map((path) => ({ type: "local", path })),
|
|
666
|
+
additionalDirectories,
|
|
656
667
|
tools,
|
|
657
668
|
allowedTools,
|
|
658
669
|
disallowedTools,
|
|
@@ -710,6 +721,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
710
721
|
bypassPermissions,
|
|
711
722
|
showConfirmation,
|
|
712
723
|
pluginDirs,
|
|
724
|
+
additionalDirectories,
|
|
713
725
|
tools,
|
|
714
726
|
allowedTools,
|
|
715
727
|
disallowedTools,
|
|
@@ -861,6 +873,52 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
861
873
|
[refreshMessages, forceRemount],
|
|
862
874
|
);
|
|
863
875
|
|
|
876
|
+
// /add-dir: add a directory to the session Safe Zone (session-level, with
|
|
877
|
+
// optional --remember persistence). Bare command shows usage + current list.
|
|
878
|
+
const addDir = useCallback(async (args?: string) => {
|
|
879
|
+
const agent = agentRef.current;
|
|
880
|
+
if (!agent) return;
|
|
881
|
+
|
|
882
|
+
// Show the result as a UI-only assistant text message (same display path
|
|
883
|
+
// as error blocks; not persisted to session history).
|
|
884
|
+
const showResult = (content: string) => {
|
|
885
|
+
setMessages((prev) => [
|
|
886
|
+
...prev,
|
|
887
|
+
{
|
|
888
|
+
id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
|
|
889
|
+
role: "assistant",
|
|
890
|
+
timestamp: new Date().toISOString(),
|
|
891
|
+
blocks: [{ type: "text", content, stage: "end" }],
|
|
892
|
+
},
|
|
893
|
+
]);
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
const rawArgs = (args ?? "").trim();
|
|
897
|
+
if (!rawArgs) {
|
|
898
|
+
const dirs = agent.getAdditionalDirectories();
|
|
899
|
+
const list =
|
|
900
|
+
dirs.length > 0
|
|
901
|
+
? dirs.map((dir) => ` - ${dir}`).join("\n")
|
|
902
|
+
: " (none)";
|
|
903
|
+
showResult(
|
|
904
|
+
`Usage: /add-dir <path> [--remember]\n\nAdditional working directories:\n${list}`,
|
|
905
|
+
);
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
const remember = rawArgs.includes("--remember");
|
|
910
|
+
const dir = rawArgs.replace("--remember", "").trim();
|
|
911
|
+
if (!dir) {
|
|
912
|
+
showResult("Usage: /add-dir <path> [--remember]");
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
await agent.addAdditionalDirectory(dir, { remember });
|
|
917
|
+
showResult(
|
|
918
|
+
`Added ${dir} to the session Safe Zone${remember ? " (remembered)" : ""}.`,
|
|
919
|
+
);
|
|
920
|
+
}, []);
|
|
921
|
+
|
|
864
922
|
// Unified interrupt method, interrupt both AI messages and command execution
|
|
865
923
|
const abortMessage = useCallback(() => {
|
|
866
924
|
agentRef.current?.abortMessage();
|
|
@@ -925,6 +983,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
925
983
|
suggestedPrefix: next.suggestedPrefix,
|
|
926
984
|
hidePersistentOption: next.hidePersistentOption,
|
|
927
985
|
planContent: next.planContent,
|
|
986
|
+
permissionMode: next.permissionMode,
|
|
928
987
|
});
|
|
929
988
|
setIsConfirmationVisible(true);
|
|
930
989
|
setConfirmationQueue((prev) => prev.slice(1));
|
|
@@ -1058,6 +1117,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
1058
1117
|
askBtw,
|
|
1059
1118
|
clearMessages,
|
|
1060
1119
|
compact,
|
|
1120
|
+
addDir,
|
|
1061
1121
|
abortMessage,
|
|
1062
1122
|
recallQueuedMessage,
|
|
1063
1123
|
removeQueuedMessageById,
|
|
@@ -61,6 +61,7 @@ export const useInputManager = (
|
|
|
61
61
|
onRecallQueuedMessage,
|
|
62
62
|
onClearMessages,
|
|
63
63
|
onCompact,
|
|
64
|
+
onAddDir,
|
|
64
65
|
isIdle: isIdleProp,
|
|
65
66
|
} = callbacks;
|
|
66
67
|
|
|
@@ -272,6 +273,8 @@ export const useInputManager = (
|
|
|
272
273
|
await onClearMessages?.();
|
|
273
274
|
} else if (command === "compact") {
|
|
274
275
|
await onCompact?.(effect.args);
|
|
276
|
+
} else if (command === "add-dir") {
|
|
277
|
+
await onAddDir?.(effect.args);
|
|
275
278
|
}
|
|
276
279
|
}
|
|
277
280
|
break;
|
|
@@ -300,6 +303,7 @@ export const useInputManager = (
|
|
|
300
303
|
onRecallQueuedMessage,
|
|
301
304
|
onClearMessages,
|
|
302
305
|
onCompact,
|
|
306
|
+
onAddDir,
|
|
303
307
|
]);
|
|
304
308
|
|
|
305
309
|
useEffect(() => {
|
package/src/index.ts
CHANGED
|
@@ -88,6 +88,12 @@ export async function main() {
|
|
|
88
88
|
string: true,
|
|
89
89
|
global: false,
|
|
90
90
|
})
|
|
91
|
+
.option("add-dir", {
|
|
92
|
+
description: "Add a directory to the session Safe Zone (repeatable)",
|
|
93
|
+
type: "array",
|
|
94
|
+
string: true,
|
|
95
|
+
global: false,
|
|
96
|
+
})
|
|
91
97
|
.option("tools", {
|
|
92
98
|
description:
|
|
93
99
|
'Specify a comma-separated list of tools to enable (e.g., \'Bash,Read,Write\'). Use "" to disable all, "default" for all.',
|
|
@@ -321,6 +327,12 @@ export async function main() {
|
|
|
321
327
|
path.resolve(originalCwd, dir),
|
|
322
328
|
);
|
|
323
329
|
|
|
330
|
+
// Resolve additional directories to absolute paths (relative to the
|
|
331
|
+
// original working directory, before any worktree change)
|
|
332
|
+
const additionalDirectories = (argv.addDir as string[] | undefined)?.map(
|
|
333
|
+
(dir) => path.resolve(originalCwd, dir),
|
|
334
|
+
);
|
|
335
|
+
|
|
324
336
|
let worktreeSession: WorktreeSession | undefined;
|
|
325
337
|
if (
|
|
326
338
|
argv.worktree !== undefined ||
|
|
@@ -365,6 +377,7 @@ export async function main() {
|
|
|
365
377
|
bypassPermissions: argv.dangerouslySkipPermissions as boolean,
|
|
366
378
|
permissionMode: argv.permissionMode as PermissionMode | undefined,
|
|
367
379
|
pluginDirs,
|
|
380
|
+
additionalDirectories,
|
|
368
381
|
tools,
|
|
369
382
|
allowedTools,
|
|
370
383
|
disallowedTools,
|
|
@@ -390,6 +403,7 @@ export async function main() {
|
|
|
390
403
|
| undefined,
|
|
391
404
|
permissionMode: argv.permissionMode as PermissionMode | undefined,
|
|
392
405
|
pluginDirs,
|
|
406
|
+
additionalDirectories,
|
|
393
407
|
tools,
|
|
394
408
|
allowedTools,
|
|
395
409
|
disallowedTools,
|
|
@@ -420,6 +434,7 @@ export async function main() {
|
|
|
420
434
|
bypassPermissions: argv.dangerouslySkipPermissions as boolean | undefined,
|
|
421
435
|
permissionMode: argv.permissionMode as PermissionMode | undefined,
|
|
422
436
|
pluginDirs,
|
|
437
|
+
additionalDirectories,
|
|
423
438
|
tools,
|
|
424
439
|
allowedTools,
|
|
425
440
|
disallowedTools,
|
|
@@ -99,6 +99,7 @@ export interface InputManagerCallbacks {
|
|
|
99
99
|
) => Promise<string>;
|
|
100
100
|
onClearMessages?: () => Promise<void>;
|
|
101
101
|
onCompact?: (instructions?: string) => Promise<void>;
|
|
102
|
+
onAddDir?: (args?: string) => Promise<void>;
|
|
102
103
|
sessionId?: string;
|
|
103
104
|
workdir?: string;
|
|
104
105
|
getFullMessageThread?: () => Promise<{
|
package/src/print-cli.ts
CHANGED
|
@@ -44,6 +44,7 @@ export async function startPrintCli(options: PrintCliOptions): Promise<void> {
|
|
|
44
44
|
bypassPermissions,
|
|
45
45
|
permissionMode,
|
|
46
46
|
pluginDirs,
|
|
47
|
+
additionalDirectories,
|
|
47
48
|
tools,
|
|
48
49
|
allowedTools,
|
|
49
50
|
disallowedTools,
|
|
@@ -125,6 +126,7 @@ export async function startPrintCli(options: PrintCliOptions): Promise<void> {
|
|
|
125
126
|
permissionMode:
|
|
126
127
|
permissionMode || (bypassPermissions ? "bypassPermissions" : undefined),
|
|
127
128
|
plugins: pluginDirs?.map((path) => ({ type: "local", path })),
|
|
129
|
+
additionalDirectories,
|
|
128
130
|
tools,
|
|
129
131
|
allowedTools,
|
|
130
132
|
disallowedTools,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Key } from "ink";
|
|
2
|
-
import type { PermissionDecision } from "wave-agent-sdk";
|
|
2
|
+
import type { PermissionDecision, PermissionMode } from "wave-agent-sdk";
|
|
3
3
|
import {
|
|
4
4
|
BASH_TOOL_NAME,
|
|
5
5
|
EXIT_PLAN_MODE_TOOL_NAME,
|
|
@@ -30,6 +30,7 @@ export type ConfirmationAction =
|
|
|
30
30
|
toolInput?: Record<string, unknown>;
|
|
31
31
|
suggestedPrefix?: string;
|
|
32
32
|
hidePersistentOption?: boolean;
|
|
33
|
+
permissionMode?: PermissionMode;
|
|
33
34
|
};
|
|
34
35
|
|
|
35
36
|
export function confirmationReducer(
|
|
@@ -95,6 +96,7 @@ export function confirmationReducer(
|
|
|
95
96
|
toolInput,
|
|
96
97
|
suggestedPrefix,
|
|
97
98
|
hidePersistentOption,
|
|
99
|
+
permissionMode,
|
|
98
100
|
} = action;
|
|
99
101
|
|
|
100
102
|
if (key.return) {
|
|
@@ -175,7 +177,10 @@ export function confirmationReducer(
|
|
|
175
177
|
const availableOptions: ConfirmationState["selectedOption"][] = [];
|
|
176
178
|
availableOptions.push("allow");
|
|
177
179
|
if (!hidePersistentOption) availableOptions.push("auto");
|
|
178
|
-
if (
|
|
180
|
+
if (
|
|
181
|
+
toolName === EXIT_PLAN_MODE_TOOL_NAME ||
|
|
182
|
+
(toolName === BASH_TOOL_NAME && permissionMode !== "plan")
|
|
183
|
+
)
|
|
179
184
|
availableOptions.push("bypass");
|
|
180
185
|
availableOptions.push("alternative");
|
|
181
186
|
|
package/src/stdio/agentBridge.ts
CHANGED
|
@@ -45,10 +45,14 @@ import {
|
|
|
45
45
|
} from "wave-agent-sdk";
|
|
46
46
|
import {
|
|
47
47
|
type JsonRpcError,
|
|
48
|
+
INVALID_PARAMS as PROTOCOL_INVALID_PARAMS,
|
|
48
49
|
INTERNAL_ERROR as PROTOCOL_INTERNAL_ERROR,
|
|
49
50
|
METHOD_NOT_FOUND as PROTOCOL_METHOD_NOT_FOUND,
|
|
50
51
|
} from "./protocol.js";
|
|
51
52
|
import { execFileSync } from "node:child_process";
|
|
53
|
+
import { mkdirSync, existsSync, writeFileSync } from "node:fs";
|
|
54
|
+
import { tmpdir } from "node:os";
|
|
55
|
+
import { basename, extname, join } from "node:path";
|
|
52
56
|
import { createWorktree, removeWorktree } from "../utils/worktree.js";
|
|
53
57
|
import { logger } from "../utils/logger.js";
|
|
54
58
|
import { isUserCheckpointMessage } from "../utils/rewindCheckpoints.js";
|
|
@@ -222,6 +226,10 @@ export class AgentBridge {
|
|
|
222
226
|
// ── File / History (global — no session required) ──
|
|
223
227
|
case "searchFiles":
|
|
224
228
|
return this.searchFiles(p as unknown as SearchFilesParams, sessionId);
|
|
229
|
+
case "writeArtifactFile":
|
|
230
|
+
return this.writeArtifactFile(
|
|
231
|
+
p as unknown as { name: string; contentBase64: string },
|
|
232
|
+
);
|
|
225
233
|
case "getPromptHistory":
|
|
226
234
|
return this.getPromptHistory(
|
|
227
235
|
p.workdir as string | undefined,
|
|
@@ -982,6 +990,40 @@ export class AgentBridge {
|
|
|
982
990
|
return { files };
|
|
983
991
|
}
|
|
984
992
|
|
|
993
|
+
/**
|
|
994
|
+
* Writes an uploaded file (from the desktop/webview "+上传文件" flow) into the
|
|
995
|
+
* artifacts dir on THIS machine — i.e. where the agent runs. Remote sessions
|
|
996
|
+
* call this over the ssh tunnel so the returned path is reachable by the
|
|
997
|
+
* remote agent's tools; local sessions never hit this (desktop writes locally
|
|
998
|
+
* itself).
|
|
999
|
+
*/
|
|
1000
|
+
private async writeArtifactFile(params: {
|
|
1001
|
+
name: string;
|
|
1002
|
+
contentBase64: string;
|
|
1003
|
+
}): Promise<{ path: string }> {
|
|
1004
|
+
// Artifacts are keyed by basename only — reject anything that could escape
|
|
1005
|
+
// the artifacts dir via path traversal.
|
|
1006
|
+
const safeName = basename(params.name || "");
|
|
1007
|
+
if (!safeName || safeName === "." || safeName === "..") {
|
|
1008
|
+
throw new RpcError(
|
|
1009
|
+
PROTOCOL_INVALID_PARAMS,
|
|
1010
|
+
`Invalid artifact file name: ${params.name}`,
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
const artifactsDir = join(tmpdir(), "wave-artifacts");
|
|
1014
|
+
mkdirSync(artifactsDir, { recursive: true });
|
|
1015
|
+
let finalPath = join(artifactsDir, safeName);
|
|
1016
|
+
let counter = 1;
|
|
1017
|
+
while (existsSync(finalPath)) {
|
|
1018
|
+
const ext = extname(safeName);
|
|
1019
|
+
const baseName = basename(safeName, ext);
|
|
1020
|
+
finalPath = join(artifactsDir, `${baseName}_${counter}${ext}`);
|
|
1021
|
+
counter++;
|
|
1022
|
+
}
|
|
1023
|
+
writeFileSync(finalPath, Buffer.from(params.contentBase64 || "", "base64"));
|
|
1024
|
+
return { path: finalPath };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
985
1027
|
private async getPromptHistory(
|
|
986
1028
|
workdir?: string,
|
|
987
1029
|
sessionId?: string,
|
package/src/stdio/protocol.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -5,6 +5,8 @@ export interface BaseAppProps {
|
|
|
5
5
|
bypassPermissions?: boolean;
|
|
6
6
|
permissionMode?: PermissionMode;
|
|
7
7
|
pluginDirs?: string[];
|
|
8
|
+
/** Additional directories to add to the session Safe Zone (from --add-dir). */
|
|
9
|
+
additionalDirectories?: string[];
|
|
8
10
|
tools?: string[];
|
|
9
11
|
allowedTools?: string[];
|
|
10
12
|
disallowedTools?: string[];
|