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.
- package/dist/components/AgentsManager.d.ts +7 -0
- package/dist/components/AgentsManager.js +109 -0
- package/dist/components/ConfirmationSelector.js +17 -3
- package/dist/components/InputBox.js +7 -21
- package/dist/components/LoginCommand.js +31 -2
- package/dist/components/MarketplaceAddForm.js +16 -2
- package/dist/constants/commands.js +6 -0
- package/dist/contexts/useChat.d.ts +2 -1
- package/dist/contexts/useChat.js +15 -2
- package/dist/hooks/useInputManager.d.ts +2 -0
- package/dist/hooks/useInputManager.js +8 -0
- package/dist/managers/inputHandlers.js +3 -0
- package/dist/managers/inputReducer.d.ts +4 -0
- package/dist/managers/inputReducer.js +8 -0
- package/dist/reducers/agentsManagerReducer.d.ts +26 -0
- package/dist/reducers/agentsManagerReducer.js +54 -0
- package/dist/stdio/agentBridge.d.ts +3 -0
- package/dist/stdio/agentBridge.js +38 -10
- package/dist/stdio/protocol.d.ts +1 -1
- package/dist/utils/usageSummary.d.ts +0 -4
- package/dist/utils/usageSummary.js +1 -34
- package/package.json +2 -2
- package/src/components/AgentsManager.tsx +290 -0
- package/src/components/ConfirmationSelector.tsx +18 -3
- package/src/components/InputBox.tsx +54 -45
- package/src/components/LoginCommand.tsx +35 -2
- package/src/components/MarketplaceAddForm.tsx +17 -2
- package/src/constants/commands.ts +6 -0
- package/src/contexts/useChat.tsx +23 -2
- package/src/hooks/useInputManager.ts +8 -0
- package/src/managers/inputHandlers.ts +2 -0
- package/src/managers/inputReducer.ts +10 -0
- package/src/reducers/agentsManagerReducer.ts +91 -0
- package/src/stdio/agentBridge.ts +47 -9
- package/src/stdio/protocol.ts +2 -0
- 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
|
-
|
|
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
|
-
|
|
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;
|
package/dist/contexts/useChat.js
CHANGED
|
@@ -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
|
-
? {
|
|
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
|