wave-code 1.0.6 → 1.0.7

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 (36) hide show
  1. package/dist/components/AgentsManager.d.ts +7 -0
  2. package/dist/components/AgentsManager.js +109 -0
  3. package/dist/components/ConfirmationSelector.js +17 -3
  4. package/dist/components/InputBox.js +7 -21
  5. package/dist/components/LoginCommand.js +31 -2
  6. package/dist/components/MarketplaceAddForm.js +16 -2
  7. package/dist/constants/commands.js +6 -0
  8. package/dist/contexts/useChat.d.ts +2 -1
  9. package/dist/contexts/useChat.js +15 -2
  10. package/dist/hooks/useInputManager.d.ts +2 -0
  11. package/dist/hooks/useInputManager.js +8 -0
  12. package/dist/managers/inputHandlers.js +3 -0
  13. package/dist/managers/inputReducer.d.ts +4 -0
  14. package/dist/managers/inputReducer.js +8 -0
  15. package/dist/reducers/agentsManagerReducer.d.ts +26 -0
  16. package/dist/reducers/agentsManagerReducer.js +54 -0
  17. package/dist/stdio/agentBridge.d.ts +3 -0
  18. package/dist/stdio/agentBridge.js +38 -10
  19. package/dist/stdio/protocol.d.ts +1 -1
  20. package/dist/utils/usageSummary.d.ts +0 -4
  21. package/dist/utils/usageSummary.js +1 -34
  22. package/package.json +2 -2
  23. package/src/components/AgentsManager.tsx +290 -0
  24. package/src/components/ConfirmationSelector.tsx +18 -3
  25. package/src/components/InputBox.tsx +54 -45
  26. package/src/components/LoginCommand.tsx +35 -2
  27. package/src/components/MarketplaceAddForm.tsx +17 -2
  28. package/src/constants/commands.ts +6 -0
  29. package/src/contexts/useChat.tsx +23 -2
  30. package/src/hooks/useInputManager.ts +8 -0
  31. package/src/managers/inputHandlers.ts +2 -0
  32. package/src/managers/inputReducer.ts +10 -0
  33. package/src/reducers/agentsManagerReducer.ts +91 -0
  34. package/src/stdio/agentBridge.ts +47 -9
  35. package/src/stdio/protocol.ts +2 -0
  36. package/src/utils/usageSummary.ts +2 -46
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ import type { SubagentConfiguration } from "wave-agent-sdk";
3
+ export interface AgentsManagerProps {
4
+ onCancel: () => void;
5
+ agentDefinitions: SubagentConfiguration[];
6
+ }
7
+ export declare const AgentsManager: React.FC<AgentsManagerProps>;
@@ -0,0 +1,109 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useReducer } from "react";
3
+ import { Box, Text, useInput, useStdout } from "ink";
4
+ import { Markdown } from "./Markdown.js";
5
+ import { agentsManagerReducer, } from "../reducers/agentsManagerReducer.js";
6
+ const SCOPE_LABELS = {
7
+ builtin: "Built-in agents",
8
+ user: "User agents",
9
+ project: "Project agents",
10
+ plugin: "Plugin agents",
11
+ };
12
+ const SCOPE_ORDER = [
13
+ "builtin",
14
+ "user",
15
+ "project",
16
+ "plugin",
17
+ ];
18
+ const initialState = {
19
+ selectedIndex: 0,
20
+ viewMode: "list",
21
+ pendingEffect: null,
22
+ };
23
+ export const AgentsManager = ({ onCancel, agentDefinitions, }) => {
24
+ const [state, dispatch] = useReducer(agentsManagerReducer, initialState);
25
+ const { stdout } = useStdout();
26
+ // Handle pending effects
27
+ useEffect(() => {
28
+ if (!state.pendingEffect)
29
+ return;
30
+ const effect = state.pendingEffect;
31
+ dispatch({ type: "CLEAR_PENDING_EFFECT" });
32
+ if (effect.type === "CANCEL") {
33
+ onCancel();
34
+ }
35
+ }, [state.pendingEffect, onCancel]);
36
+ // Flatten definitions (grouped by scope) into one navigable list. Headers
37
+ // and the empty-state line are non-selectable.
38
+ const entries = useMemo(() => {
39
+ const result = [];
40
+ let selectableCount = 0;
41
+ result.push({ kind: "header", label: "AGENTS", selectableIndex: -1 });
42
+ let definitionCount = 0;
43
+ for (const scope of SCOPE_ORDER) {
44
+ const defs = agentDefinitions
45
+ .filter((d) => d.scope === scope)
46
+ .sort((a, b) => a.name.localeCompare(b.name));
47
+ if (defs.length === 0)
48
+ continue;
49
+ result.push({
50
+ kind: "header",
51
+ label: SCOPE_LABELS[scope],
52
+ scope,
53
+ selectableIndex: -1,
54
+ });
55
+ for (const def of defs) {
56
+ result.push({
57
+ kind: "definition",
58
+ label: def.name,
59
+ model: def.model,
60
+ sub: def.description,
61
+ scope: def.scope,
62
+ selectableIndex: selectableCount++,
63
+ definition: def,
64
+ });
65
+ definitionCount++;
66
+ }
67
+ }
68
+ if (definitionCount === 0) {
69
+ result.push({
70
+ kind: "empty",
71
+ label: "No agents available",
72
+ selectableIndex: -1,
73
+ });
74
+ }
75
+ return result;
76
+ }, [agentDefinitions]);
77
+ const itemCount = entries.filter((e) => e.selectableIndex >= 0).length;
78
+ // Window slice: center the selected item within the visible area, clamping
79
+ // to the terminal's available rows (reusable pattern from
80
+ // BackgroundTaskManager).
81
+ const availableRows = stdout?.rows ?? 24;
82
+ const maxVisible = Math.max(3, Math.min(15, availableRows - 12));
83
+ const selectedFlatIndex = entries.findIndex((e) => e.selectableIndex === state.selectedIndex);
84
+ const startIndex = Math.max(0, Math.min(selectedFlatIndex - Math.floor(maxVisible / 2), Math.max(0, entries.length - maxVisible)));
85
+ const visibleEntries = entries.slice(startIndex, startIndex + maxVisible);
86
+ useInput((input, key) => {
87
+ dispatch({ type: "HANDLE_KEY", input, key, itemCount });
88
+ });
89
+ const selectedEntry = entries.find((e) => e.selectableIndex === state.selectedIndex);
90
+ // Detail view — body renders fully expanded with no height limit, no
91
+ // clipping and no scrolling (aligned with Claude Code's AgentDetail).
92
+ if (state.viewMode === "detail" && selectedEntry) {
93
+ const def = selectedEntry.definition;
94
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", borderBottom: false, borderLeft: false, borderRight: false, paddingTop: 1, gap: 1, children: [_jsx(Box, { children: _jsxs(Text, { color: "cyan", bold: true, children: ["Agent: ", selectedEntry.label] }) }), _jsxs(Box, { flexDirection: "column", gap: 1, children: [def?.description && (_jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "Description:" }), " ", def.description] }) })), _jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "Model:" }), " ", def?.model || "default (not explicitly configured)"] }) }), _jsx(Box, { children: _jsxs(Text, { children: [_jsx(Text, { color: "blue", children: "Scope:" }), " ", def ? SCOPE_LABELS[def.scope] : ""] }) }), def?.tools && def.tools.length > 0 && (_jsx(Box, { children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "blue", children: "Tools:" }), " ", def.tools.join(", ")] }) })), def?.filePath && (_jsx(Box, { children: _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "blue", children: "File:" }), " ", def.filePath] }) }))] }), def && (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { color: "blue", bold: true, children: "System Prompt:" }), _jsx(Box, { marginLeft: 2, marginRight: 2, children: _jsx(Markdown, { children: def.systemPrompt }) })] })), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Esc or Enter to go back" }) })] }));
95
+ }
96
+ if (itemCount === 0) {
97
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", borderBottom: false, borderLeft: false, borderRight: false, paddingTop: 1, children: [_jsx(Text, { color: "cyan", bold: true, children: "Agents" }), _jsx(Text, { children: "No agents available" }), _jsx(Text, { dimColor: true, children: "Press Escape to close" })] }));
98
+ }
99
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", borderBottom: false, borderLeft: false, borderRight: false, paddingTop: 1, gap: 1, children: [_jsx(Box, { children: _jsx(Text, { color: "cyan", bold: true, children: "Agents" }) }), _jsx(Text, { dimColor: true, children: "Select an agent to view details" }), _jsx(Box, { flexDirection: "column", children: visibleEntries.map((entry, index) => {
100
+ const isSelected = entry.selectableIndex === state.selectedIndex;
101
+ if (entry.kind === "header") {
102
+ return (_jsx(Text, { dimColor: true, bold: true, children: entry.label }, `${entry.kind}-${entry.label}-${index}`));
103
+ }
104
+ if (entry.kind === "empty") {
105
+ return (_jsx(Text, { dimColor: true, children: entry.label }, `empty-${index}`));
106
+ }
107
+ return (_jsxs(Text, { color: isSelected ? "black" : "white", backgroundColor: isSelected ? "cyan" : undefined, wrap: "truncate-end", children: [isSelected ? "▶ " : " ", entry.selectableIndex + 1, ". ", entry.label, entry.model ? (_jsxs(Text, { color: isSelected ? "black" : "gray", children: [" ", "\u00B7 ", entry.model] })) : null, entry.sub ? ` · ${entry.sub}` : ""] }, `${entry.kind}-${entry.selectableIndex}`));
108
+ }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "\u2191/\u2193 to select \u00B7 Enter to view details \u00B7 Esc to close" }) })] }));
109
+ };
@@ -1,9 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useReducer } from "react";
2
+ import { useEffect, useReducer, useRef } from "react";
3
3
  import { Box, Text, useInput } from "ink";
4
4
  import { BASH_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "wave-agent-sdk";
5
5
  import { confirmationReducer } from "../reducers/confirmationReducer.js";
6
6
  import { questionReducer } from "../reducers/questionReducer.js";
7
+ import { createBracketedPasteDetector } from "../utils/bracketedPaste.js";
7
8
  const getHeaderColor = (header) => {
8
9
  const colors = ["red", "green", "blue", "magenta", "cyan"];
9
10
  let hash = 0;
@@ -64,15 +65,28 @@ export const ConfirmationSelector = ({ toolName, toolInput, suggestedPrefix, hid
64
65
  }
65
66
  return "Yes, and auto-accept edits";
66
67
  };
68
+ const pasteDetectorRef = useRef(createBracketedPasteDetector());
67
69
  useInput((input, key) => {
68
70
  if (key.escape) {
69
71
  onCancel();
70
72
  return;
71
73
  }
74
+ const result = pasteDetectorRef.current.process(input);
75
+ if (result.kind === "consume") {
76
+ // Content of an in-flight bracketed paste: hold it, never submit.
77
+ return;
78
+ }
79
+ let cleanInput;
80
+ if (result.kind === "paste") {
81
+ cleanInput = (result.leadingInput ?? "") + result.text;
82
+ }
83
+ else {
84
+ cleanInput = result.input;
85
+ }
72
86
  if (toolName === ASK_USER_QUESTION_TOOL_NAME) {
73
87
  questionDispatch({
74
88
  type: "HANDLE_KEY",
75
- input,
89
+ input: cleanInput,
76
90
  key,
77
91
  questions,
78
92
  });
@@ -80,7 +94,7 @@ export const ConfirmationSelector = ({ toolName, toolInput, suggestedPrefix, hid
80
94
  else {
81
95
  dispatch({
82
96
  type: "HANDLE_KEY",
83
- input,
97
+ input: cleanInput,
84
98
  key,
85
99
  toolName,
86
100
  toolInput,
@@ -7,6 +7,7 @@ import { CommandSelector } from "./CommandSelector.js";
7
7
  import { HistorySearch } from "./HistorySearch.js";
8
8
  import { BackgroundTaskManager } from "./BackgroundTaskManager.js";
9
9
  import { McpManager } from "./McpManager.js";
10
+ import { AgentsManager } from "./AgentsManager.js";
10
11
  import { RewindCommand } from "./RewindCommand.js";
11
12
  import { HelpView } from "./HelpView.js";
12
13
  import { StatusCommand } from "./StatusCommand.js";
@@ -22,7 +23,7 @@ import { useChat } from "../contexts/useChat.js";
22
23
  export const INPUT_PLACEHOLDER_TEXT = "Type your message (use /help for more info)...";
23
24
  export const INPUT_PLACEHOLDER_TEXT_PREFIX = INPUT_PLACEHOLDER_TEXT.substring(0, 10);
24
25
  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, addDir, currentModel, configuredModels, setModel, recreateAgent, recallQueuedMessage, queuedMessages, setIsBtwActive, } = useChat();
26
+ const { permissionMode: chatPermissionMode, setPermissionMode: setChatPermissionMode, handleRewindSelect, backgroundCurrentTask, messages, getFullMessageThread, sessionId, workingDirectory, askBtw, clearMessages, compact, addDir, currentModel, configuredModels, setModel, recreateAgent, recallQueuedMessage, queuedMessages, setIsBtwActive, agentDefinitions, } = useChat();
26
27
  // Ref to hold setInputText so queue callbacks can access it before useInputManager returns
27
28
  const setInputTextRef = useRef(() => { });
28
29
  const hasQueuedMessages = (queuedMessages?.length ?? 0) > 0;
@@ -45,7 +46,7 @@ export const InputBox = ({ isLoading, isCommandRunning, isCompacting, sendMessag
45
46
  // History search
46
47
  showHistorySearch, historySearchQuery,
47
48
  // Task/MCP Manager
48
- showBackgroundTaskManager, showMcpManager, showRewindManager, showHelp, showStatusCommand, showLoginCommand, showPluginManager, showModelSelector, showWorkflowManager, setShowBackgroundTaskManager, setShowMcpManager, setShowRewindManager, setShowHelp, setShowStatusCommand, setShowLoginCommand, setShowPluginManager, setShowModelSelector, setShowWorkflowManager,
49
+ showBackgroundTaskManager, showMcpManager, showAgentsManager, showRewindManager, showHelp, showStatusCommand, showLoginCommand, showPluginManager, showModelSelector, showWorkflowManager, setShowBackgroundTaskManager, setShowMcpManager, setShowAgentsManager, setShowRewindManager, setShowHelp, setShowStatusCommand, setShowLoginCommand, setShowPluginManager, setShowModelSelector, setShowWorkflowManager,
49
50
  // Permission mode
50
51
  permissionMode, setPermissionMode,
51
52
  // BTW state
@@ -101,6 +102,7 @@ export const InputBox = ({ isLoading, isCommandRunning, isCompacting, sendMessag
101
102
  showModelSelector ||
102
103
  showBackgroundTaskManager ||
103
104
  showMcpManager ||
105
+ showAgentsManager ||
104
106
  showWorkflowManager) {
105
107
  return;
106
108
  }
@@ -132,32 +134,16 @@ export const InputBox = ({ isLoading, isCommandRunning, isCompacting, sendMessag
132
134
  }
133
135
  await handleRewindSelect(index);
134
136
  };
135
- if (showRewindManager) {
136
- return (_jsx(RewindCommand, { messages: messages, onSelect: handleRewindSelectWithClose, onCancel: handleRewindCancel, getFullMessageThread: getFullMessageThread }));
137
- }
138
- if (showHelp) {
139
- return (_jsx(HelpView, { onCancel: () => setShowHelp(false), commands: slashCommands }));
140
- }
141
- if (showStatusCommand) {
142
- return _jsx(StatusCommand, { onCancel: () => setShowStatusCommand(false) });
143
- }
144
- if (showLoginCommand) {
145
- return _jsx(LoginCommand, { onCancel: () => setShowLoginCommand(false) });
146
- }
147
- if (showPluginManager) {
148
- return (_jsx(PluginManagerShell, { onCancel: () => setShowPluginManager(false), onPluginInstalled: recreateAgent }));
149
- }
150
- if (showModelSelector) {
151
- return (_jsx(ModelSelector, { onCancel: () => setShowModelSelector(false), currentModel: currentModel, configuredModels: configuredModels, onSelectModel: setModel }));
152
- }
153
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(BtwDisplay, { btwState: btwState }), showFileSelector && (_jsx(FileSelector, { files: filteredFiles, searchQuery: searchQuery, isLoading: isFileSearching, onSelect: handleFileSelect, onCancel: handleCancelFileSelect })), showCommandSelector && (_jsx(CommandSelector, { searchQuery: commandSearchQuery, onSelect: handleCommandSelect, onInsert: handleCommandInsert, onCancel: handleCancelCommandSelect, commands: slashCommands })), showHistorySearch && (_jsx(HistorySearch, { searchQuery: historySearchQuery, onSelect: handleHistorySearchSelect, onCancel: handleCancelHistorySearch })), showBackgroundTaskManager && (_jsx(BackgroundTaskManager, { onCancel: () => setShowBackgroundTaskManager(false) })), showMcpManager && (_jsx(McpManager, { onCancel: () => setShowMcpManager(false), servers: mcpServers, onConnectServer: connectMcpServer, onDisconnectServer: disconnectMcpServer })), showWorkflowManager && (_jsx(WorkflowManager, { onCancel: () => setShowWorkflowManager(false) })), btwState.question || btwState.answer
137
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(BtwDisplay, { btwState: btwState }), showFileSelector && (_jsx(FileSelector, { files: filteredFiles, searchQuery: searchQuery, isLoading: isFileSearching, onSelect: handleFileSelect, onCancel: handleCancelFileSelect })), showCommandSelector && (_jsx(CommandSelector, { searchQuery: commandSearchQuery, onSelect: handleCommandSelect, onInsert: handleCommandInsert, onCancel: handleCancelCommandSelect, commands: slashCommands })), showHistorySearch && (_jsx(HistorySearch, { searchQuery: historySearchQuery, onSelect: handleHistorySearchSelect, onCancel: handleCancelHistorySearch })), showBackgroundTaskManager && (_jsx(BackgroundTaskManager, { onCancel: () => setShowBackgroundTaskManager(false) })), showMcpManager && (_jsx(McpManager, { onCancel: () => setShowMcpManager(false), servers: mcpServers, onConnectServer: connectMcpServer, onDisconnectServer: disconnectMcpServer })), showAgentsManager && (_jsx(AgentsManager, { onCancel: () => setShowAgentsManager(false), agentDefinitions: agentDefinitions })), showWorkflowManager && (_jsx(WorkflowManager, { onCancel: () => setShowWorkflowManager(false) })), showRewindManager && (_jsx(RewindCommand, { messages: messages, onSelect: handleRewindSelectWithClose, onCancel: handleRewindCancel, getFullMessageThread: getFullMessageThread })), showHelp && (_jsx(HelpView, { onCancel: () => setShowHelp(false), commands: slashCommands })), showStatusCommand && (_jsx(StatusCommand, { onCancel: () => setShowStatusCommand(false) })), showLoginCommand && (_jsx(LoginCommand, { onCancel: () => setShowLoginCommand(false) })), showPluginManager && (_jsx(PluginManagerShell, { onCancel: () => setShowPluginManager(false), onPluginInstalled: recreateAgent })), showModelSelector && (_jsx(ModelSelector, { onCancel: () => setShowModelSelector(false), currentModel: currentModel, configuredModels: configuredModels, onSelectModel: setModel })), btwState.question || btwState.answer
154
138
  ? null
155
139
  : showBackgroundTaskManager ||
156
140
  showMcpManager ||
141
+ showAgentsManager ||
157
142
  showRewindManager ||
158
143
  showHelp ||
159
144
  showStatusCommand ||
160
145
  showLoginCommand ||
161
146
  showPluginManager ||
147
+ showModelSelector ||
162
148
  showWorkflowManager || (_jsxs(Box, { flexDirection: "column", children: [escClearPending && _jsx(Text, { color: "gray", children: "\u518D\u6B21\u6309 Esc \u6E05\u7A7A\u8F93\u5165" }), _jsx(Box, { borderStyle: "single", borderColor: "gray", borderLeft: false, borderRight: false, children: _jsx(Text, { color: isPlaceholder ? "gray" : "white", children: shouldShowCursor ? (_jsxs(_Fragment, { children: [beforeCursor, _jsx(Text, { backgroundColor: "white", color: "black", children: atCursor }), afterCursor] })) : (displayText) }) }), _jsxs(Box, { justifyContent: "space-between", children: [_jsx(StatusLine, { permissionMode: permissionMode, isShellCommand: isShellCommand }), _jsx(Notifications, { latestTotalTokens: latestTotalTokens, maxInputTokens: maxInputTokens, showLoginHint: showLoginHint })] })] }))] }));
163
149
  };
@@ -4,6 +4,7 @@ import { Box, Text, useInput } from "ink";
4
4
  import { execFile } from "child_process";
5
5
  import { promisify } from "util";
6
6
  import { authService } from "wave-agent-sdk";
7
+ import { createBracketedPasteDetector } from "../utils/bracketedPaste.js";
7
8
  const execFileAsync = promisify(execFile);
8
9
  function openBrowser(url) {
9
10
  const platform = process.platform;
@@ -26,6 +27,11 @@ export const LoginCommand = ({ onCancel }) => {
26
27
  const [tokenInput, setTokenInput] = useState("");
27
28
  const isLoadingRef = useRef(isLoading);
28
29
  isLoadingRef.current = isLoading;
30
+ // Detects and strips bracketed paste markers (\x1b[200~ ... \x1b[201~)
31
+ // that terminals wrap pasted text in. Unlike the main InputBox pipeline,
32
+ // this overlay's token input goes through raw ink useInput, which only
33
+ // strips ONE leading ESC, leaving the markers (e.g. "[200~") in the input.
34
+ const pasteDetectorRef = useRef(createBracketedPasteDetector());
29
35
  // Resolve/reject refs for the token promise
30
36
  const tokenResolveRef = useRef(null);
31
37
  const tokenRejectRef = useRef(null);
@@ -64,9 +70,31 @@ export const LoginCommand = ({ onCancel }) => {
64
70
  setTokenInput((prev) => prev.slice(0, -1));
65
71
  return;
66
72
  }
67
- // Regular character input (single or pasted multi-char)
73
+ // Regular character input (single or pasted multi-char). Run through the
74
+ // bracketed paste detector: a pasted token arrives wrapped in
75
+ // \x1b[200~ ... \x1b[201~ markers (possibly split across chunks), which
76
+ // must be stripped instead of being appended to the token.
68
77
  if (input && !key.ctrl && !key.meta && !key.return && input.length > 0) {
69
- setTokenInput((prev) => prev + input);
78
+ const result = pasteDetectorRef.current.process(input);
79
+ if (result.kind === "consume") {
80
+ // In-flight bracketed paste content: hold it; the final chunk
81
+ // delivers the complete text.
82
+ return;
83
+ }
84
+ if (result.kind === "paste") {
85
+ // Tokens never contain carriage returns — drop \r (CRLF terminals
86
+ // send \r, not \n, in pasted text).
87
+ const leading = result.leadingInput?.replace(/\r/g, "");
88
+ if (leading) {
89
+ setTokenInput((prev) => prev + leading);
90
+ }
91
+ const text = result.text.replace(/\r/g, "");
92
+ if (text) {
93
+ setTokenInput((prev) => prev + text);
94
+ }
95
+ return;
96
+ }
97
+ setTokenInput((prev) => prev + result.input);
70
98
  }
71
99
  });
72
100
  const handleEnter = async () => {
@@ -84,6 +112,7 @@ export const LoginCommand = ({ onCancel }) => {
84
112
  setError("");
85
113
  setAuthUrl("");
86
114
  setTokenInput("");
115
+ pasteDetectorRef.current.reset();
87
116
  setMessage("Starting authentication...");
88
117
  // Promise that resolves when user presses Enter with token input
89
118
  const readToken = () => new Promise((resolve, reject) => {
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useReducer, useEffect } from "react";
2
+ import { useReducer, useEffect, useRef } from "react";
3
3
  import { Box, Text, useInput } from "ink";
4
4
  import { usePluginManagerContext } from "../contexts/PluginManagerContext.js";
5
5
  import { marketplaceAddFormReducer, SCOPES, } from "../reducers/marketplaceAddFormReducer.js";
6
+ import { createBracketedPasteDetector } from "../utils/bracketedPaste.js";
6
7
  export const MarketplaceAddForm = () => {
7
8
  const { state: ctxState, actions } = usePluginManagerContext();
8
9
  const [state, dispatch] = useReducer(marketplaceAddFormReducer, {
@@ -23,11 +24,24 @@ export const MarketplaceAddForm = () => {
23
24
  }
24
25
  dispatch({ type: "CLEAR_PENDING_ACTION" });
25
26
  }, [state.pendingAction, actions]);
27
+ const pasteDetectorRef = useRef(createBracketedPasteDetector());
26
28
  useInput((input, key) => {
29
+ const result = pasteDetectorRef.current.process(input);
30
+ if (result.kind === "consume") {
31
+ // Content of an in-flight bracketed paste: hold it, never submit.
32
+ return;
33
+ }
34
+ let cleanInput;
35
+ if (result.kind === "paste") {
36
+ cleanInput = (result.leadingInput ?? "") + result.text;
37
+ }
38
+ else {
39
+ cleanInput = result.input;
40
+ }
27
41
  dispatch({
28
42
  type: "HANDLE_KEY",
29
43
  key,
30
- input,
44
+ input: cleanInput,
31
45
  isLoading: ctxState.isLoading,
32
46
  });
33
47
  });
@@ -11,6 +11,12 @@ export const AVAILABLE_COMMANDS = [
11
11
  description: "View and manage MCP servers",
12
12
  handler: () => { }, // Handler here won't be used, actual processing is in the hook
13
13
  },
14
+ {
15
+ id: "agents",
16
+ name: "agents",
17
+ description: "List available agents and active subagents",
18
+ handler: () => { }, // Handler here won't be used, actual processing is in the hook
19
+ },
14
20
  {
15
21
  id: "rewind",
16
22
  name: "rewind",
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
- import type { Message, McpServerStatus, BackgroundTask, Task, SlashCommand, PermissionDecision, PermissionMode, QueuedMessage, WorkflowRun } from "wave-agent-sdk";
2
+ import type { Message, McpServerStatus, BackgroundTask, Task, SlashCommand, SubagentConfiguration, PermissionDecision, PermissionMode, QueuedMessage, WorkflowRun } from "wave-agent-sdk";
3
3
  import { BaseAppProps } from "../types.js";
4
4
  export interface ChatContextType {
5
5
  messages: Message[];
@@ -46,6 +46,7 @@ export interface ChatContextType {
46
46
  stopBackgroundTask: (taskId: string) => boolean;
47
47
  slashCommands: SlashCommand[];
48
48
  hasSlashCommand: (commandId: string) => boolean;
49
+ agentDefinitions: SubagentConfiguration[];
49
50
  permissionMode: PermissionMode;
50
51
  setPermissionMode: (mode: PermissionMode) => void;
51
52
  isConfirmationVisible: boolean;
@@ -204,6 +204,8 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
204
204
  const [tasks, setTasks] = useState([]);
205
205
  // Command state
206
206
  const [slashCommands, setSlashCommands] = useState([]);
207
+ // Agent definitions (for /agents overlay)
208
+ const [agentDefinitions, setAgentDefinitions] = useState([]);
207
209
  // Permission state
208
210
  const [permissionMode, setPermissionModeState] = useState(initialPermissionMode ||
209
211
  (bypassPermissions ? "bypassPermissions" : "default"));
@@ -355,14 +357,20 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
355
357
  }
356
358
  : m));
357
359
  },
358
- onCompleteBangMessage: (command, exitCode, messageId) => {
360
+ onCompleteBangMessage: (command, exitCode, messageId, output) => {
359
361
  if (isExpandedRef.current)
360
362
  return;
361
363
  setMessages((prev) => prev.map((m) => m.id === messageId
362
364
  ? {
363
365
  ...m,
364
366
  blocks: m.blocks.map((b, idx) => idx === m.blocks.length - 1 && b.type === "bang"
365
- ? { ...b, command, exitCode, stage: "end" }
367
+ ? {
368
+ ...b,
369
+ command,
370
+ exitCode,
371
+ stage: "end",
372
+ ...(output !== undefined ? { output } : {}),
373
+ }
366
374
  : b),
367
375
  }
368
376
  : m));
@@ -481,6 +489,9 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
481
489
  // Get initial commands
482
490
  const agentSlashCommands = agent.getSlashCommands?.() || [];
483
491
  setSlashCommands(agentSlashCommands);
492
+ // Get initial agent definitions
493
+ const initialAgentDefinitions = agent.getSubagentConfigurations?.() || [];
494
+ setAgentDefinitions(initialAgentDefinitions);
484
495
  }
485
496
  catch (error) {
486
497
  console.error("Failed to initialize AI manager:", error);
@@ -520,6 +531,7 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
520
531
  setMessages([]);
521
532
  setMcpServerStatuses([]);
522
533
  setSlashCommands([]);
534
+ setAgentDefinitions([]);
523
535
  setSessionId("");
524
536
  setIsLoading(false);
525
537
  setLatestTotalTokens(0);
@@ -841,6 +853,7 @@ export const ChatProvider = ({ children, bypassPermissions, permissionMode: init
841
853
  stopBackgroundTask,
842
854
  slashCommands,
843
855
  hasSlashCommand,
856
+ agentDefinitions,
844
857
  permissionMode,
845
858
  setPermissionMode,
846
859
  isConfirmationVisible,
@@ -16,6 +16,7 @@ export declare const useInputManager: (callbacks?: Partial<InputManagerCallbacks
16
16
  historySearchQuery: string;
17
17
  showBackgroundTaskManager: boolean;
18
18
  showMcpManager: boolean;
19
+ showAgentsManager: boolean;
19
20
  showRewindManager: boolean;
20
21
  showHelp: boolean;
21
22
  showStatusCommand: boolean;
@@ -49,6 +50,7 @@ export declare const useInputManager: (callbacks?: Partial<InputManagerCallbacks
49
50
  processSelectorInput: (char: string) => void;
50
51
  setShowBackgroundTaskManager: (show: boolean) => void;
51
52
  setShowMcpManager: (show: boolean) => void;
53
+ setShowAgentsManager: (show: boolean) => void;
52
54
  setShowRewindManager: (show: boolean) => void;
53
55
  setShowHelp: (show: boolean) => void;
54
56
  setShowStatusCommand: (show: boolean) => void;
@@ -174,6 +174,9 @@ export const useInputManager = (callbacks = {}) => {
174
174
  else if (command === "mcp") {
175
175
  dispatch({ type: "SET_SHOW_MCP_MANAGER", payload: true });
176
176
  }
177
+ else if (command === "agents") {
178
+ dispatch({ type: "SET_SHOW_AGENTS_MANAGER", payload: true });
179
+ }
177
180
  else if (command === "rewind") {
178
181
  dispatch({ type: "SET_SHOW_REWIND_MANAGER", payload: true });
179
182
  }
@@ -378,6 +381,9 @@ export const useInputManager = (callbacks = {}) => {
378
381
  const setShowMcpManager = useCallback((show) => {
379
382
  dispatch({ type: "SET_SHOW_MCP_MANAGER", payload: show });
380
383
  }, []);
384
+ const setShowAgentsManager = useCallback((show) => {
385
+ dispatch({ type: "SET_SHOW_AGENTS_MANAGER", payload: show });
386
+ }, []);
381
387
  const setShowRewindManager = useCallback((show) => {
382
388
  dispatch({ type: "SET_SHOW_REWIND_MANAGER", payload: show });
383
389
  }, []);
@@ -506,6 +512,7 @@ export const useInputManager = (callbacks = {}) => {
506
512
  historySearchQuery: state.historySearchQuery,
507
513
  showBackgroundTaskManager: state.showBackgroundTaskManager,
508
514
  showMcpManager: state.showMcpManager,
515
+ showAgentsManager: state.showAgentsManager,
509
516
  showRewindManager: state.showRewindManager,
510
517
  showHelp: state.showHelp,
511
518
  showStatusCommand: state.showStatusCommand,
@@ -545,6 +552,7 @@ export const useInputManager = (callbacks = {}) => {
545
552
  // Bash/MCP Manager
546
553
  setShowBackgroundTaskManager,
547
554
  setShowMcpManager,
555
+ setShowAgentsManager,
548
556
  setShowRewindManager,
549
557
  setShowHelp,
550
558
  setShowStatusCommand,
@@ -264,6 +264,9 @@ export const handleCommandSelect = (state, dispatch, callbacks, command) => {
264
264
  else if (command === "mcp") {
265
265
  dispatch({ type: "SET_SHOW_MCP_MANAGER", payload: true });
266
266
  }
267
+ else if (command === "agents") {
268
+ dispatch({ type: "SET_SHOW_AGENTS_MANAGER", payload: true });
269
+ }
267
270
  else if (command === "rewind") {
268
271
  dispatch({ type: "SET_SHOW_REWIND_MANAGER", payload: true });
269
272
  }
@@ -111,6 +111,7 @@ export interface InputState {
111
111
  imageIdCounter: number;
112
112
  showBackgroundTaskManager: boolean;
113
113
  showMcpManager: boolean;
114
+ showAgentsManager: boolean;
114
115
  showRewindManager: boolean;
115
116
  showHelp: boolean;
116
117
  showStatusCommand: boolean;
@@ -187,6 +188,9 @@ export type InputAction = {
187
188
  } | {
188
189
  type: "SET_SHOW_MCP_MANAGER";
189
190
  payload: boolean;
191
+ } | {
192
+ type: "SET_SHOW_AGENTS_MANAGER";
193
+ payload: boolean;
190
194
  } | {
191
195
  type: "SET_SHOW_REWIND_MANAGER";
192
196
  payload: boolean;
@@ -26,6 +26,7 @@ export const initialState = {
26
26
  imageIdCounter: 1,
27
27
  showBackgroundTaskManager: false,
28
28
  showMcpManager: false,
29
+ showAgentsManager: false,
29
30
  showRewindManager: false,
30
31
  showHelp: false,
31
32
  showStatusCommand: false,
@@ -347,6 +348,12 @@ export function inputReducer(state, action) {
347
348
  showMcpManager: action.payload,
348
349
  selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
349
350
  };
351
+ case "SET_SHOW_AGENTS_MANAGER":
352
+ return {
353
+ ...state,
354
+ showAgentsManager: action.payload,
355
+ selectorJustUsed: !action.payload ? true : state.selectorJustUsed,
356
+ };
350
357
  case "SET_SHOW_REWIND_MANAGER":
351
358
  return {
352
359
  ...state,
@@ -695,6 +702,7 @@ export function inputReducer(state, action) {
695
702
  }
696
703
  if (!(state.showBackgroundTaskManager ||
697
704
  state.showMcpManager ||
705
+ state.showAgentsManager ||
698
706
  state.showRewindManager ||
699
707
  state.showHelp ||
700
708
  state.showStatusCommand ||
@@ -0,0 +1,26 @@
1
+ import { Key } from "ink";
2
+ export type PendingEffect = {
3
+ type: "CANCEL";
4
+ };
5
+ export interface AgentsManagerState {
6
+ selectedIndex: number;
7
+ viewMode: "list" | "detail";
8
+ pendingEffect: PendingEffect | null;
9
+ }
10
+ export type AgentsManagerAction = {
11
+ type: "MOVE_UP";
12
+ } | {
13
+ type: "MOVE_DOWN";
14
+ itemCount: number;
15
+ } | {
16
+ type: "SET_VIEW_MODE";
17
+ viewMode: "list" | "detail";
18
+ } | {
19
+ type: "HANDLE_KEY";
20
+ input: string;
21
+ key: Key;
22
+ itemCount: number;
23
+ } | {
24
+ type: "CLEAR_PENDING_EFFECT";
25
+ };
26
+ export declare function agentsManagerReducer(state: AgentsManagerState, action: AgentsManagerAction): AgentsManagerState;
@@ -0,0 +1,54 @@
1
+ export function agentsManagerReducer(state, action) {
2
+ switch (action.type) {
3
+ case "MOVE_UP":
4
+ return {
5
+ ...state,
6
+ selectedIndex: Math.max(0, state.selectedIndex - 1),
7
+ };
8
+ case "MOVE_DOWN":
9
+ return {
10
+ ...state,
11
+ selectedIndex: Math.min(Math.max(0, action.itemCount - 1), state.selectedIndex + 1),
12
+ };
13
+ case "SET_VIEW_MODE":
14
+ return { ...state, viewMode: action.viewMode };
15
+ case "HANDLE_KEY": {
16
+ const { key, itemCount } = action;
17
+ if (key.return) {
18
+ if (state.viewMode === "list") {
19
+ return { ...state, viewMode: "detail" };
20
+ }
21
+ // Aligned with Claude Code AgentDetail: Enter returns to the list.
22
+ return { ...state, viewMode: "list" };
23
+ }
24
+ if (key.escape) {
25
+ if (state.viewMode === "detail") {
26
+ return { ...state, viewMode: "list" };
27
+ }
28
+ return { ...state, pendingEffect: { type: "CANCEL" } };
29
+ }
30
+ // Detail view does not respond to arrow keys (aligned with CC
31
+ // AgentDetail, which only Esc/Enter back to the list).
32
+ if (state.viewMode === "detail") {
33
+ return state;
34
+ }
35
+ if (key.upArrow) {
36
+ return {
37
+ ...state,
38
+ selectedIndex: Math.max(0, state.selectedIndex - 1),
39
+ };
40
+ }
41
+ if (key.downArrow) {
42
+ return {
43
+ ...state,
44
+ selectedIndex: Math.min(Math.max(0, itemCount - 1), state.selectedIndex + 1),
45
+ };
46
+ }
47
+ return state;
48
+ }
49
+ case "CLEAR_PENDING_EFFECT":
50
+ return { ...state, pendingEffect: null };
51
+ default:
52
+ return state;
53
+ }
54
+ }
@@ -56,6 +56,8 @@ export declare class AgentBridge {
56
56
  private removeWorktreeSession;
57
57
  private getSessionInfo;
58
58
  private updateConfig;
59
+ private getConfiguredModels;
60
+ private setModel;
59
61
  private sendMessage;
60
62
  private bang;
61
63
  private askBtw;
@@ -80,6 +82,7 @@ export declare class AgentBridge {
80
82
  private connectMcpServer;
81
83
  private disconnectMcpServer;
82
84
  private getSlashCommands;
85
+ private getSubagentConfigurations;
83
86
  private searchFiles;
84
87
  /**
85
88
  * Writes an uploaded file (from the desktop/webview "+上传文件" flow) into the