wave-code 1.0.5 → 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 +96 -21
- 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 +4 -0
- package/dist/stdio/agentBridge.js +45 -10
- package/dist/stdio/protocol.d.ts +1 -1
- package/dist/utils/rewindCheckpoints.d.ts +2 -2
- package/dist/utils/rewindCheckpoints.js +4 -2
- 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 +159 -72
- 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 +55 -9
- package/src/stdio/protocol.ts +2 -0
- package/src/utils/rewindCheckpoints.ts +3 -2
- package/src/utils/usageSummary.ts +2 -46
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useReducer } from "react";
|
|
2
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
3
|
+
import type { SubagentConfiguration } from "wave-agent-sdk";
|
|
4
|
+
import { Markdown } from "./Markdown.js";
|
|
5
|
+
import {
|
|
6
|
+
agentsManagerReducer,
|
|
7
|
+
type AgentsManagerState,
|
|
8
|
+
} from "../reducers/agentsManagerReducer.js";
|
|
9
|
+
|
|
10
|
+
export interface AgentsManagerProps {
|
|
11
|
+
onCancel: () => void;
|
|
12
|
+
agentDefinitions: SubagentConfiguration[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface DisplayEntry {
|
|
16
|
+
kind: "header" | "definition" | "empty";
|
|
17
|
+
label: string;
|
|
18
|
+
sub?: string;
|
|
19
|
+
model?: string;
|
|
20
|
+
scope?: SubagentConfiguration["scope"];
|
|
21
|
+
selectableIndex: number; // -1 for non-selectable rows
|
|
22
|
+
definition?: SubagentConfiguration;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const SCOPE_LABELS: Record<SubagentConfiguration["scope"], string> = {
|
|
26
|
+
builtin: "Built-in agents",
|
|
27
|
+
user: "User agents",
|
|
28
|
+
project: "Project agents",
|
|
29
|
+
plugin: "Plugin agents",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const SCOPE_ORDER: SubagentConfiguration["scope"][] = [
|
|
33
|
+
"builtin",
|
|
34
|
+
"user",
|
|
35
|
+
"project",
|
|
36
|
+
"plugin",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const initialState: AgentsManagerState = {
|
|
40
|
+
selectedIndex: 0,
|
|
41
|
+
viewMode: "list",
|
|
42
|
+
pendingEffect: null,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const AgentsManager: React.FC<AgentsManagerProps> = ({
|
|
46
|
+
onCancel,
|
|
47
|
+
agentDefinitions,
|
|
48
|
+
}) => {
|
|
49
|
+
const [state, dispatch] = useReducer(agentsManagerReducer, initialState);
|
|
50
|
+
const { stdout } = useStdout();
|
|
51
|
+
|
|
52
|
+
// Handle pending effects
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (!state.pendingEffect) return;
|
|
55
|
+
const effect = state.pendingEffect;
|
|
56
|
+
dispatch({ type: "CLEAR_PENDING_EFFECT" });
|
|
57
|
+
if (effect.type === "CANCEL") {
|
|
58
|
+
onCancel();
|
|
59
|
+
}
|
|
60
|
+
}, [state.pendingEffect, onCancel]);
|
|
61
|
+
|
|
62
|
+
// Flatten definitions (grouped by scope) into one navigable list. Headers
|
|
63
|
+
// and the empty-state line are non-selectable.
|
|
64
|
+
const entries = useMemo<DisplayEntry[]>(() => {
|
|
65
|
+
const result: DisplayEntry[] = [];
|
|
66
|
+
let selectableCount = 0;
|
|
67
|
+
|
|
68
|
+
result.push({ kind: "header", label: "AGENTS", selectableIndex: -1 });
|
|
69
|
+
let definitionCount = 0;
|
|
70
|
+
for (const scope of SCOPE_ORDER) {
|
|
71
|
+
const defs = agentDefinitions
|
|
72
|
+
.filter((d) => d.scope === scope)
|
|
73
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
74
|
+
if (defs.length === 0) continue;
|
|
75
|
+
result.push({
|
|
76
|
+
kind: "header",
|
|
77
|
+
label: SCOPE_LABELS[scope],
|
|
78
|
+
scope,
|
|
79
|
+
selectableIndex: -1,
|
|
80
|
+
});
|
|
81
|
+
for (const def of defs) {
|
|
82
|
+
result.push({
|
|
83
|
+
kind: "definition",
|
|
84
|
+
label: def.name,
|
|
85
|
+
model: def.model,
|
|
86
|
+
sub: def.description,
|
|
87
|
+
scope: def.scope,
|
|
88
|
+
selectableIndex: selectableCount++,
|
|
89
|
+
definition: def,
|
|
90
|
+
});
|
|
91
|
+
definitionCount++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (definitionCount === 0) {
|
|
95
|
+
result.push({
|
|
96
|
+
kind: "empty",
|
|
97
|
+
label: "No agents available",
|
|
98
|
+
selectableIndex: -1,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result;
|
|
103
|
+
}, [agentDefinitions]);
|
|
104
|
+
|
|
105
|
+
const itemCount = entries.filter((e) => e.selectableIndex >= 0).length;
|
|
106
|
+
|
|
107
|
+
// Window slice: center the selected item within the visible area, clamping
|
|
108
|
+
// to the terminal's available rows (reusable pattern from
|
|
109
|
+
// BackgroundTaskManager).
|
|
110
|
+
const availableRows = stdout?.rows ?? 24;
|
|
111
|
+
const maxVisible = Math.max(3, Math.min(15, availableRows - 12));
|
|
112
|
+
const selectedFlatIndex = entries.findIndex(
|
|
113
|
+
(e) => e.selectableIndex === state.selectedIndex,
|
|
114
|
+
);
|
|
115
|
+
const startIndex = Math.max(
|
|
116
|
+
0,
|
|
117
|
+
Math.min(
|
|
118
|
+
selectedFlatIndex - Math.floor(maxVisible / 2),
|
|
119
|
+
Math.max(0, entries.length - maxVisible),
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
const visibleEntries = entries.slice(startIndex, startIndex + maxVisible);
|
|
123
|
+
|
|
124
|
+
useInput((input, key) => {
|
|
125
|
+
dispatch({ type: "HANDLE_KEY", input, key, itemCount });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const selectedEntry = entries.find(
|
|
129
|
+
(e) => e.selectableIndex === state.selectedIndex,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// Detail view — body renders fully expanded with no height limit, no
|
|
133
|
+
// clipping and no scrolling (aligned with Claude Code's AgentDetail).
|
|
134
|
+
if (state.viewMode === "detail" && selectedEntry) {
|
|
135
|
+
const def = selectedEntry.definition;
|
|
136
|
+
return (
|
|
137
|
+
<Box
|
|
138
|
+
flexDirection="column"
|
|
139
|
+
borderStyle="single"
|
|
140
|
+
borderColor="cyan"
|
|
141
|
+
borderBottom={false}
|
|
142
|
+
borderLeft={false}
|
|
143
|
+
borderRight={false}
|
|
144
|
+
paddingTop={1}
|
|
145
|
+
gap={1}
|
|
146
|
+
>
|
|
147
|
+
<Box>
|
|
148
|
+
<Text color="cyan" bold>
|
|
149
|
+
Agent: {selectedEntry.label}
|
|
150
|
+
</Text>
|
|
151
|
+
</Box>
|
|
152
|
+
|
|
153
|
+
<Box flexDirection="column" gap={1}>
|
|
154
|
+
{def?.description && (
|
|
155
|
+
<Box>
|
|
156
|
+
<Text>
|
|
157
|
+
<Text color="blue">Description:</Text> {def.description}
|
|
158
|
+
</Text>
|
|
159
|
+
</Box>
|
|
160
|
+
)}
|
|
161
|
+
<Box>
|
|
162
|
+
<Text>
|
|
163
|
+
<Text color="blue">Model:</Text>{" "}
|
|
164
|
+
{def?.model || "default (not explicitly configured)"}
|
|
165
|
+
</Text>
|
|
166
|
+
</Box>
|
|
167
|
+
<Box>
|
|
168
|
+
<Text>
|
|
169
|
+
<Text color="blue">Scope:</Text>{" "}
|
|
170
|
+
{def ? SCOPE_LABELS[def.scope] : ""}
|
|
171
|
+
</Text>
|
|
172
|
+
</Box>
|
|
173
|
+
{def?.tools && def.tools.length > 0 && (
|
|
174
|
+
<Box>
|
|
175
|
+
<Text wrap="wrap">
|
|
176
|
+
<Text color="blue">Tools:</Text> {def.tools.join(", ")}
|
|
177
|
+
</Text>
|
|
178
|
+
</Box>
|
|
179
|
+
)}
|
|
180
|
+
{def?.filePath && (
|
|
181
|
+
<Box>
|
|
182
|
+
<Text wrap="wrap">
|
|
183
|
+
<Text color="blue">File:</Text> {def.filePath}
|
|
184
|
+
</Text>
|
|
185
|
+
</Box>
|
|
186
|
+
)}
|
|
187
|
+
</Box>
|
|
188
|
+
|
|
189
|
+
{def && (
|
|
190
|
+
<Box flexDirection="column" marginTop={1}>
|
|
191
|
+
<Text color="blue" bold>
|
|
192
|
+
System Prompt:
|
|
193
|
+
</Text>
|
|
194
|
+
<Box marginLeft={2} marginRight={2}>
|
|
195
|
+
<Markdown>{def.systemPrompt}</Markdown>
|
|
196
|
+
</Box>
|
|
197
|
+
</Box>
|
|
198
|
+
)}
|
|
199
|
+
|
|
200
|
+
<Box marginTop={1}>
|
|
201
|
+
<Text dimColor>Esc or Enter to go back</Text>
|
|
202
|
+
</Box>
|
|
203
|
+
</Box>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (itemCount === 0) {
|
|
208
|
+
return (
|
|
209
|
+
<Box
|
|
210
|
+
flexDirection="column"
|
|
211
|
+
borderStyle="single"
|
|
212
|
+
borderColor="cyan"
|
|
213
|
+
borderBottom={false}
|
|
214
|
+
borderLeft={false}
|
|
215
|
+
borderRight={false}
|
|
216
|
+
paddingTop={1}
|
|
217
|
+
>
|
|
218
|
+
<Text color="cyan" bold>
|
|
219
|
+
Agents
|
|
220
|
+
</Text>
|
|
221
|
+
<Text>No agents available</Text>
|
|
222
|
+
<Text dimColor>Press Escape to close</Text>
|
|
223
|
+
</Box>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<Box
|
|
229
|
+
flexDirection="column"
|
|
230
|
+
borderStyle="single"
|
|
231
|
+
borderColor="cyan"
|
|
232
|
+
borderBottom={false}
|
|
233
|
+
borderLeft={false}
|
|
234
|
+
borderRight={false}
|
|
235
|
+
paddingTop={1}
|
|
236
|
+
gap={1}
|
|
237
|
+
>
|
|
238
|
+
<Box>
|
|
239
|
+
<Text color="cyan" bold>
|
|
240
|
+
Agents
|
|
241
|
+
</Text>
|
|
242
|
+
</Box>
|
|
243
|
+
<Text dimColor>Select an agent to view details</Text>
|
|
244
|
+
|
|
245
|
+
<Box flexDirection="column">
|
|
246
|
+
{visibleEntries.map((entry, index) => {
|
|
247
|
+
const isSelected = entry.selectableIndex === state.selectedIndex;
|
|
248
|
+
if (entry.kind === "header") {
|
|
249
|
+
return (
|
|
250
|
+
<Text key={`${entry.kind}-${entry.label}-${index}`} dimColor bold>
|
|
251
|
+
{entry.label}
|
|
252
|
+
</Text>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
if (entry.kind === "empty") {
|
|
256
|
+
return (
|
|
257
|
+
<Text key={`empty-${index}`} dimColor>
|
|
258
|
+
{entry.label}
|
|
259
|
+
</Text>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
return (
|
|
263
|
+
<Text
|
|
264
|
+
key={`${entry.kind}-${entry.selectableIndex}`}
|
|
265
|
+
color={isSelected ? "black" : "white"}
|
|
266
|
+
backgroundColor={isSelected ? "cyan" : undefined}
|
|
267
|
+
wrap="truncate-end"
|
|
268
|
+
>
|
|
269
|
+
{isSelected ? "▶ " : " "}
|
|
270
|
+
{entry.selectableIndex + 1}. {entry.label}
|
|
271
|
+
{entry.model ? (
|
|
272
|
+
<Text color={isSelected ? "black" : "gray"}>
|
|
273
|
+
{" "}
|
|
274
|
+
· {entry.model}
|
|
275
|
+
</Text>
|
|
276
|
+
) : null}
|
|
277
|
+
{entry.sub ? ` · ${entry.sub}` : ""}
|
|
278
|
+
</Text>
|
|
279
|
+
);
|
|
280
|
+
})}
|
|
281
|
+
</Box>
|
|
282
|
+
|
|
283
|
+
<Box marginTop={1}>
|
|
284
|
+
<Text dimColor>
|
|
285
|
+
↑/↓ to select · Enter to view details · Esc to close
|
|
286
|
+
</Text>
|
|
287
|
+
</Box>
|
|
288
|
+
</Box>
|
|
289
|
+
);
|
|
290
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useReducer } from "react";
|
|
1
|
+
import React, { useEffect, useReducer, useRef } from "react";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
3
|
import type {
|
|
4
4
|
PermissionDecision,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "wave-agent-sdk";
|
|
14
14
|
import { confirmationReducer } from "../reducers/confirmationReducer.js";
|
|
15
15
|
import { questionReducer } from "../reducers/questionReducer.js";
|
|
16
|
+
import { createBracketedPasteDetector } from "../utils/bracketedPaste.js";
|
|
16
17
|
|
|
17
18
|
const getHeaderColor = (header: string) => {
|
|
18
19
|
const colors = ["red", "green", "blue", "magenta", "cyan"] as const;
|
|
@@ -103,23 +104,37 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
103
104
|
return "Yes, and auto-accept edits";
|
|
104
105
|
};
|
|
105
106
|
|
|
107
|
+
const pasteDetectorRef = useRef(createBracketedPasteDetector());
|
|
108
|
+
|
|
106
109
|
useInput((input, key) => {
|
|
107
110
|
if (key.escape) {
|
|
108
111
|
onCancel();
|
|
109
112
|
return;
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
const result = pasteDetectorRef.current.process(input);
|
|
116
|
+
if (result.kind === "consume") {
|
|
117
|
+
// Content of an in-flight bracketed paste: hold it, never submit.
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
let cleanInput: string;
|
|
121
|
+
if (result.kind === "paste") {
|
|
122
|
+
cleanInput = (result.leadingInput ?? "") + result.text;
|
|
123
|
+
} else {
|
|
124
|
+
cleanInput = result.input;
|
|
125
|
+
}
|
|
126
|
+
|
|
112
127
|
if (toolName === ASK_USER_QUESTION_TOOL_NAME) {
|
|
113
128
|
questionDispatch({
|
|
114
129
|
type: "HANDLE_KEY",
|
|
115
|
-
input,
|
|
130
|
+
input: cleanInput,
|
|
116
131
|
key,
|
|
117
132
|
questions,
|
|
118
133
|
});
|
|
119
134
|
} else {
|
|
120
135
|
dispatch({
|
|
121
136
|
type: "HANDLE_KEY",
|
|
122
|
-
input,
|
|
137
|
+
input: cleanInput,
|
|
123
138
|
key,
|
|
124
139
|
toolName,
|
|
125
140
|
toolInput,
|
|
@@ -6,6 +6,7 @@ import { CommandSelector } from "./CommandSelector.js";
|
|
|
6
6
|
import { HistorySearch } from "./HistorySearch.js";
|
|
7
7
|
import { BackgroundTaskManager } from "./BackgroundTaskManager.js";
|
|
8
8
|
import { McpManager } from "./McpManager.js";
|
|
9
|
+
import { AgentsManager } from "./AgentsManager.js";
|
|
9
10
|
import { RewindCommand } from "./RewindCommand.js";
|
|
10
11
|
import { HelpView } from "./HelpView.js";
|
|
11
12
|
import { StatusCommand } from "./StatusCommand.js";
|
|
@@ -89,6 +90,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
89
90
|
recallQueuedMessage,
|
|
90
91
|
queuedMessages,
|
|
91
92
|
setIsBtwActive,
|
|
93
|
+
agentDefinitions,
|
|
92
94
|
} = useChat();
|
|
93
95
|
|
|
94
96
|
// Ref to hold setInputText so queue callbacks can access it before useInputManager returns
|
|
@@ -133,6 +135,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
133
135
|
// Task/MCP Manager
|
|
134
136
|
showBackgroundTaskManager,
|
|
135
137
|
showMcpManager,
|
|
138
|
+
showAgentsManager,
|
|
136
139
|
showRewindManager,
|
|
137
140
|
showHelp,
|
|
138
141
|
showStatusCommand,
|
|
@@ -142,6 +145,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
142
145
|
showWorkflowManager,
|
|
143
146
|
setShowBackgroundTaskManager,
|
|
144
147
|
setShowMcpManager,
|
|
148
|
+
setShowAgentsManager,
|
|
145
149
|
setShowRewindManager,
|
|
146
150
|
setShowHelp,
|
|
147
151
|
setShowStatusCommand,
|
|
@@ -211,6 +215,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
211
215
|
showModelSelector ||
|
|
212
216
|
showBackgroundTaskManager ||
|
|
213
217
|
showMcpManager ||
|
|
218
|
+
showAgentsManager ||
|
|
214
219
|
showWorkflowManager
|
|
215
220
|
) {
|
|
216
221
|
return;
|
|
@@ -254,51 +259,6 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
254
259
|
await handleRewindSelect(index);
|
|
255
260
|
};
|
|
256
261
|
|
|
257
|
-
if (showRewindManager) {
|
|
258
|
-
return (
|
|
259
|
-
<RewindCommand
|
|
260
|
-
messages={messages}
|
|
261
|
-
onSelect={handleRewindSelectWithClose}
|
|
262
|
-
onCancel={handleRewindCancel}
|
|
263
|
-
getFullMessageThread={getFullMessageThread}
|
|
264
|
-
/>
|
|
265
|
-
);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (showHelp) {
|
|
269
|
-
return (
|
|
270
|
-
<HelpView onCancel={() => setShowHelp(false)} commands={slashCommands} />
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (showStatusCommand) {
|
|
275
|
-
return <StatusCommand onCancel={() => setShowStatusCommand(false)} />;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (showLoginCommand) {
|
|
279
|
-
return <LoginCommand onCancel={() => setShowLoginCommand(false)} />;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (showPluginManager) {
|
|
283
|
-
return (
|
|
284
|
-
<PluginManagerShell
|
|
285
|
-
onCancel={() => setShowPluginManager(false)}
|
|
286
|
-
onPluginInstalled={recreateAgent}
|
|
287
|
-
/>
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (showModelSelector) {
|
|
292
|
-
return (
|
|
293
|
-
<ModelSelector
|
|
294
|
-
onCancel={() => setShowModelSelector(false)}
|
|
295
|
-
currentModel={currentModel}
|
|
296
|
-
configuredModels={configuredModels}
|
|
297
|
-
onSelectModel={setModel}
|
|
298
|
-
/>
|
|
299
|
-
);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
262
|
return (
|
|
303
263
|
<Box flexDirection="column">
|
|
304
264
|
<BtwDisplay btwState={btwState} />
|
|
@@ -345,19 +305,68 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
345
305
|
/>
|
|
346
306
|
)}
|
|
347
307
|
|
|
308
|
+
{showAgentsManager && (
|
|
309
|
+
<AgentsManager
|
|
310
|
+
onCancel={() => setShowAgentsManager(false)}
|
|
311
|
+
agentDefinitions={agentDefinitions}
|
|
312
|
+
/>
|
|
313
|
+
)}
|
|
314
|
+
|
|
348
315
|
{showWorkflowManager && (
|
|
349
316
|
<WorkflowManager onCancel={() => setShowWorkflowManager(false)} />
|
|
350
317
|
)}
|
|
351
318
|
|
|
319
|
+
{showRewindManager && (
|
|
320
|
+
<RewindCommand
|
|
321
|
+
messages={messages}
|
|
322
|
+
onSelect={handleRewindSelectWithClose}
|
|
323
|
+
onCancel={handleRewindCancel}
|
|
324
|
+
getFullMessageThread={getFullMessageThread}
|
|
325
|
+
/>
|
|
326
|
+
)}
|
|
327
|
+
|
|
328
|
+
{showHelp && (
|
|
329
|
+
<HelpView
|
|
330
|
+
onCancel={() => setShowHelp(false)}
|
|
331
|
+
commands={slashCommands}
|
|
332
|
+
/>
|
|
333
|
+
)}
|
|
334
|
+
|
|
335
|
+
{showStatusCommand && (
|
|
336
|
+
<StatusCommand onCancel={() => setShowStatusCommand(false)} />
|
|
337
|
+
)}
|
|
338
|
+
|
|
339
|
+
{showLoginCommand && (
|
|
340
|
+
<LoginCommand onCancel={() => setShowLoginCommand(false)} />
|
|
341
|
+
)}
|
|
342
|
+
|
|
343
|
+
{showPluginManager && (
|
|
344
|
+
<PluginManagerShell
|
|
345
|
+
onCancel={() => setShowPluginManager(false)}
|
|
346
|
+
onPluginInstalled={recreateAgent}
|
|
347
|
+
/>
|
|
348
|
+
)}
|
|
349
|
+
|
|
350
|
+
{showModelSelector && (
|
|
351
|
+
<ModelSelector
|
|
352
|
+
onCancel={() => setShowModelSelector(false)}
|
|
353
|
+
currentModel={currentModel}
|
|
354
|
+
configuredModels={configuredModels}
|
|
355
|
+
onSelectModel={setModel}
|
|
356
|
+
/>
|
|
357
|
+
)}
|
|
358
|
+
|
|
352
359
|
{btwState.question || btwState.answer
|
|
353
360
|
? null
|
|
354
361
|
: showBackgroundTaskManager ||
|
|
355
362
|
showMcpManager ||
|
|
363
|
+
showAgentsManager ||
|
|
356
364
|
showRewindManager ||
|
|
357
365
|
showHelp ||
|
|
358
366
|
showStatusCommand ||
|
|
359
367
|
showLoginCommand ||
|
|
360
368
|
showPluginManager ||
|
|
369
|
+
showModelSelector ||
|
|
361
370
|
showWorkflowManager || (
|
|
362
371
|
<Box flexDirection="column">
|
|
363
372
|
{escClearPending && <Text color="gray">再次按 Esc 清空输入</Text>}
|
|
@@ -3,6 +3,7 @@ import { Box, Text, useInput } from "ink";
|
|
|
3
3
|
import { execFile } from "child_process";
|
|
4
4
|
import { promisify } from "util";
|
|
5
5
|
import { authService } from "wave-agent-sdk";
|
|
6
|
+
import { createBracketedPasteDetector } from "../utils/bracketedPaste.js";
|
|
6
7
|
|
|
7
8
|
const execFileAsync = promisify(execFile);
|
|
8
9
|
|
|
@@ -32,6 +33,12 @@ export const LoginCommand: React.FC<LoginCommandProps> = ({ onCancel }) => {
|
|
|
32
33
|
const isLoadingRef = useRef(isLoading);
|
|
33
34
|
isLoadingRef.current = isLoading;
|
|
34
35
|
|
|
36
|
+
// Detects and strips bracketed paste markers (\x1b[200~ ... \x1b[201~)
|
|
37
|
+
// that terminals wrap pasted text in. Unlike the main InputBox pipeline,
|
|
38
|
+
// this overlay's token input goes through raw ink useInput, which only
|
|
39
|
+
// strips ONE leading ESC, leaving the markers (e.g. "[200~") in the input.
|
|
40
|
+
const pasteDetectorRef = useRef(createBracketedPasteDetector());
|
|
41
|
+
|
|
35
42
|
// Resolve/reject refs for the token promise
|
|
36
43
|
const tokenResolveRef = useRef<((token: string) => void) | null>(null);
|
|
37
44
|
const tokenRejectRef = useRef<((err: Error) => void) | null>(null);
|
|
@@ -71,9 +78,34 @@ export const LoginCommand: React.FC<LoginCommandProps> = ({ onCancel }) => {
|
|
|
71
78
|
setTokenInput((prev) => prev.slice(0, -1));
|
|
72
79
|
return;
|
|
73
80
|
}
|
|
74
|
-
// Regular character input (single or pasted multi-char)
|
|
81
|
+
// Regular character input (single or pasted multi-char). Run through the
|
|
82
|
+
// bracketed paste detector: a pasted token arrives wrapped in
|
|
83
|
+
// \x1b[200~ ... \x1b[201~ markers (possibly split across chunks), which
|
|
84
|
+
// must be stripped instead of being appended to the token.
|
|
75
85
|
if (input && !key.ctrl && !key.meta && !key.return && input.length > 0) {
|
|
76
|
-
|
|
86
|
+
const result = pasteDetectorRef.current.process(input);
|
|
87
|
+
|
|
88
|
+
if (result.kind === "consume") {
|
|
89
|
+
// In-flight bracketed paste content: hold it; the final chunk
|
|
90
|
+
// delivers the complete text.
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (result.kind === "paste") {
|
|
95
|
+
// Tokens never contain carriage returns — drop \r (CRLF terminals
|
|
96
|
+
// send \r, not \n, in pasted text).
|
|
97
|
+
const leading = result.leadingInput?.replace(/\r/g, "");
|
|
98
|
+
if (leading) {
|
|
99
|
+
setTokenInput((prev) => prev + leading);
|
|
100
|
+
}
|
|
101
|
+
const text = result.text.replace(/\r/g, "");
|
|
102
|
+
if (text) {
|
|
103
|
+
setTokenInput((prev) => prev + text);
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
setTokenInput((prev) => prev + result.input);
|
|
77
109
|
}
|
|
78
110
|
});
|
|
79
111
|
|
|
@@ -93,6 +125,7 @@ export const LoginCommand: React.FC<LoginCommandProps> = ({ onCancel }) => {
|
|
|
93
125
|
setError("");
|
|
94
126
|
setAuthUrl("");
|
|
95
127
|
setTokenInput("");
|
|
128
|
+
pasteDetectorRef.current.reset();
|
|
96
129
|
setMessage("Starting authentication...");
|
|
97
130
|
|
|
98
131
|
// Promise that resolves when user presses Enter with token input
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useReducer, useEffect } from "react";
|
|
1
|
+
import React, { useReducer, useEffect, useRef } from "react";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
3
|
import { usePluginManagerContext } from "../contexts/PluginManagerContext.js";
|
|
4
4
|
import {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
SCOPES,
|
|
7
7
|
type MarketplaceAddFormState,
|
|
8
8
|
} from "../reducers/marketplaceAddFormReducer.js";
|
|
9
|
+
import { createBracketedPasteDetector } from "../utils/bracketedPaste.js";
|
|
9
10
|
|
|
10
11
|
export const MarketplaceAddForm: React.FC = () => {
|
|
11
12
|
const { state: ctxState, actions } = usePluginManagerContext();
|
|
@@ -32,11 +33,25 @@ export const MarketplaceAddForm: React.FC = () => {
|
|
|
32
33
|
dispatch({ type: "CLEAR_PENDING_ACTION" });
|
|
33
34
|
}, [state.pendingAction, actions]);
|
|
34
35
|
|
|
36
|
+
const pasteDetectorRef = useRef(createBracketedPasteDetector());
|
|
37
|
+
|
|
35
38
|
useInput((input, key) => {
|
|
39
|
+
const result = pasteDetectorRef.current.process(input);
|
|
40
|
+
if (result.kind === "consume") {
|
|
41
|
+
// Content of an in-flight bracketed paste: hold it, never submit.
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let cleanInput: string;
|
|
45
|
+
if (result.kind === "paste") {
|
|
46
|
+
cleanInput = (result.leadingInput ?? "") + result.text;
|
|
47
|
+
} else {
|
|
48
|
+
cleanInput = result.input;
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
dispatch({
|
|
37
52
|
type: "HANDLE_KEY",
|
|
38
53
|
key,
|
|
39
|
-
input,
|
|
54
|
+
input: cleanInput,
|
|
40
55
|
isLoading: ctxState.isLoading,
|
|
41
56
|
});
|
|
42
57
|
});
|
|
@@ -13,6 +13,12 @@ export const AVAILABLE_COMMANDS: SlashCommand[] = [
|
|
|
13
13
|
description: "View and manage MCP servers",
|
|
14
14
|
handler: () => {}, // Handler here won't be used, actual processing is in the hook
|
|
15
15
|
},
|
|
16
|
+
{
|
|
17
|
+
id: "agents",
|
|
18
|
+
name: "agents",
|
|
19
|
+
description: "List available agents and active subagents",
|
|
20
|
+
handler: () => {}, // Handler here won't be used, actual processing is in the hook
|
|
21
|
+
},
|
|
16
22
|
{
|
|
17
23
|
id: "rewind",
|
|
18
24
|
name: "rewind",
|