wave-code 1.0.3 → 1.0.4

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.
Files changed (37) hide show
  1. package/dist/cli.js +2 -2
  2. package/dist/components/App.js +5 -5
  3. package/dist/components/ChatInterface.js +1 -1
  4. package/dist/components/ConfirmationSelector.d.ts +2 -1
  5. package/dist/components/ConfirmationSelector.js +4 -3
  6. package/dist/components/InputBox.js +2 -1
  7. package/dist/constants/commands.js +6 -0
  8. package/dist/contexts/useChat.d.ts +3 -1
  9. package/dist/contexts/useChat.js +45 -3
  10. package/dist/hooks/useInputManager.js +5 -1
  11. package/dist/index.js +12 -0
  12. package/dist/managers/inputHandlers.js +3 -0
  13. package/dist/managers/inputReducer.d.ts +1 -0
  14. package/dist/print-cli.js +2 -1
  15. package/dist/reducers/confirmationReducer.d.ts +2 -1
  16. package/dist/reducers/confirmationReducer.js +3 -2
  17. package/dist/stdio/agentBridge.d.ts +8 -0
  18. package/dist/stdio/agentBridge.js +33 -1
  19. package/dist/stdio/protocol.d.ts +1 -1
  20. package/dist/types.d.ts +2 -0
  21. package/package.json +2 -2
  22. package/src/cli.tsx +2 -0
  23. package/src/components/App.tsx +5 -0
  24. package/src/components/ChatInterface.tsx +1 -0
  25. package/src/components/ConfirmationSelector.tsx +10 -3
  26. package/src/components/InputBox.tsx +2 -0
  27. package/src/constants/commands.ts +6 -0
  28. package/src/contexts/useChat.tsx +60 -0
  29. package/src/hooks/useInputManager.ts +4 -0
  30. package/src/index.ts +15 -0
  31. package/src/managers/inputHandlers.ts +2 -0
  32. package/src/managers/inputReducer.ts +1 -0
  33. package/src/print-cli.ts +2 -0
  34. package/src/reducers/confirmationReducer.ts +7 -2
  35. package/src/stdio/agentBridge.ts +42 -0
  36. package/src/stdio/protocol.ts +1 -0
  37. 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();
@@ -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 === 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
+ : "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;
@@ -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 === BASH_TOOL_NAME || toolName === EXIT_PLAN_MODE_TOOL_NAME)
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),
@@ -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",
3
+ "version": "1.0.4",
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.3"
46
+ "wave-agent-sdk": "1.0.4"
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}
@@ -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 { PermissionDecision, AskUserQuestionInput } from "wave-agent-sdk";
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 === BASH_TOOL_NAME ||
244
- toolName === EXIT_PLAN_MODE_TOOL_NAME) && (
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
  ];
@@ -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,
@@ -394,6 +394,8 @@ export const handleCommandSelect = (
394
394
  await callbacks.onClearMessages?.();
395
395
  } else if (command === "compact") {
396
396
  await callbacks.onCompact?.();
397
+ } else if (command === "add-dir") {
398
+ await callbacks.onAddDir?.();
397
399
  }
398
400
  }
399
401
  })();
@@ -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 (toolName === BASH_TOOL_NAME || toolName === EXIT_PLAN_MODE_TOOL_NAME)
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
 
@@ -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,
@@ -71,6 +71,7 @@ export type RequestMethod =
71
71
  | "disconnectMcpServer"
72
72
  | "getSlashCommands"
73
73
  | "searchFiles"
74
+ | "writeArtifactFile"
74
75
  | "getPromptHistory"
75
76
  | "searchPromptHistory"
76
77
  | "updateConfig"
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[];