wave-code 1.0.6 → 1.0.8
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/ChatInterface.js +1 -1
- package/dist/components/ConfirmationDetails.d.ts +1 -0
- package/dist/components/ConfirmationDetails.js +5 -3
- 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/components/RewindCommand.js +11 -4
- package/dist/constants/commands.js +6 -0
- package/dist/contexts/useChat.d.ts +18 -2
- package/dist/contexts/useChat.js +114 -9
- package/dist/daemon/commands.d.ts +49 -0
- package/dist/daemon/commands.js +341 -0
- package/dist/daemon/jsonRpcClient.d.ts +38 -0
- package/dist/daemon/jsonRpcClient.js +129 -0
- package/dist/daemon/socketClient.d.ts +13 -0
- package/dist/daemon/socketClient.js +26 -0
- package/dist/hooks/useInputManager.d.ts +2 -0
- package/dist/hooks/useInputManager.js +8 -0
- package/dist/index.js +88 -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 +15 -0
- package/dist/stdio/agentBridge.js +101 -20
- 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/ChatInterface.tsx +2 -0
- package/src/components/ConfirmationDetails.tsx +6 -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/components/RewindCommand.tsx +10 -4
- package/src/constants/commands.ts +6 -0
- package/src/contexts/useChat.tsx +146 -7
- package/src/daemon/commands.ts +444 -0
- package/src/daemon/jsonRpcClient.ts +158 -0
- package/src/daemon/socketClient.ts +34 -0
- package/src/hooks/useInputManager.ts +8 -0
- package/src/index.ts +130 -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 +123 -19
- package/src/stdio/protocol.ts +4 -0
- package/src/utils/usageSummary.ts +2 -46
|
@@ -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
|
});
|
|
@@ -34,10 +34,16 @@ export const RewindCommand: React.FC<RewindCommandProps> = ({
|
|
|
34
34
|
}, [getFullMessageThread]);
|
|
35
35
|
|
|
36
36
|
// Filter user messages as checkpoints, excluding meta messages and
|
|
37
|
-
// system-generated user-role messages (task notifications, hook injections)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
// system-generated user-role messages (task notifications, hook injections).
|
|
38
|
+
// Compaction is append-only: the same message id appears twice on the full
|
|
39
|
+
// thread (pre-compact history + post-compact append), so dedupe by id and
|
|
40
|
+
// keep the last occurrence (matching the folded view the user sees).
|
|
41
|
+
const checkpointMap = new Map<string, { msg: Message; index: number }>();
|
|
42
|
+
messages.forEach((msg, index) => {
|
|
43
|
+
if (!isUserCheckpointMessage(msg)) return;
|
|
44
|
+
checkpointMap.set(msg.id ?? `index:${index}`, { msg, index });
|
|
45
|
+
});
|
|
46
|
+
const checkpoints = Array.from(checkpointMap.values());
|
|
41
47
|
|
|
42
48
|
const MAX_VISIBLE_ITEMS = 3;
|
|
43
49
|
|
|
@@ -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",
|
package/src/contexts/useChat.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
BackgroundTask,
|
|
16
16
|
Task,
|
|
17
17
|
SlashCommand,
|
|
18
|
+
SubagentConfiguration,
|
|
18
19
|
PermissionDecision,
|
|
19
20
|
PermissionMode,
|
|
20
21
|
QueuedMessage,
|
|
@@ -30,7 +31,6 @@ import {
|
|
|
30
31
|
extractLatestTotalTokens,
|
|
31
32
|
} from "wave-agent-sdk";
|
|
32
33
|
import { logger } from "../utils/logger.js";
|
|
33
|
-
import { throttle } from "../utils/throttle.js";
|
|
34
34
|
import { displayUsageSummary } from "../utils/usageSummary.js";
|
|
35
35
|
import { expandLongTextPlaceholders } from "../managers/inputHandlers.js";
|
|
36
36
|
|
|
@@ -96,6 +96,8 @@ export interface ChatContextType {
|
|
|
96
96
|
// Slash Command functionality
|
|
97
97
|
slashCommands: SlashCommand[];
|
|
98
98
|
hasSlashCommand: (commandId: string) => boolean;
|
|
99
|
+
// Agent definitions (for /agents overlay)
|
|
100
|
+
agentDefinitions: SubagentConfiguration[];
|
|
99
101
|
// Permission functionality
|
|
100
102
|
permissionMode: PermissionMode;
|
|
101
103
|
setPermissionMode: (mode: PermissionMode) => void;
|
|
@@ -109,6 +111,7 @@ export interface ChatContextType {
|
|
|
109
111
|
hidePersistentOption?: boolean;
|
|
110
112
|
planContent?: string;
|
|
111
113
|
permissionMode?: PermissionMode;
|
|
114
|
+
warning?: string;
|
|
112
115
|
};
|
|
113
116
|
showConfirmation: (
|
|
114
117
|
toolName: string,
|
|
@@ -117,6 +120,7 @@ export interface ChatContextType {
|
|
|
117
120
|
hidePersistentOption?: boolean,
|
|
118
121
|
planContent?: string,
|
|
119
122
|
permissionMode?: PermissionMode,
|
|
123
|
+
warning?: string,
|
|
120
124
|
) => Promise<PermissionDecision>;
|
|
121
125
|
hideConfirmation: () => void;
|
|
122
126
|
handleConfirmationDecision: (decision: PermissionDecision) => void;
|
|
@@ -236,6 +240,99 @@ function createStreamingWindowThrottle(
|
|
|
236
240
|
return throttled;
|
|
237
241
|
}
|
|
238
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Per-tool window-concat throttle for pure-delta tool parameter streaming:
|
|
245
|
+
* `parametersChunk` deltas are accumulated independently per tool block id
|
|
246
|
+
* within the cooldown window, so interleaved multi-tool streams lose no delta
|
|
247
|
+
* (a plain throttle's single last-args slot would drop every earlier tool's
|
|
248
|
+
* deltas, leaving the first tool without streaming parameters). `start` /
|
|
249
|
+
* `running` apply immediately (one-shot snapshots); `end` flushes pending
|
|
250
|
+
* streaming deltas first, then applies the authoritative parameters/result.
|
|
251
|
+
*/
|
|
252
|
+
export function createToolStreamingThrottle(
|
|
253
|
+
fn: (params: ToolBlockUpdateCallbackParams) => void,
|
|
254
|
+
wait: number,
|
|
255
|
+
): {
|
|
256
|
+
(params: ToolBlockUpdateCallbackParams): void;
|
|
257
|
+
cancel: () => void;
|
|
258
|
+
flush: () => void;
|
|
259
|
+
} {
|
|
260
|
+
let timer: NodeJS.Timeout | null = null;
|
|
261
|
+
let pending: { messageId: string; chunks: Map<string, string> } | null = null;
|
|
262
|
+
|
|
263
|
+
const fire = () => {
|
|
264
|
+
if (pending && pending.chunks.size > 0) {
|
|
265
|
+
const { messageId, chunks } = pending;
|
|
266
|
+
pending = null;
|
|
267
|
+
for (const [id, chunk] of chunks) {
|
|
268
|
+
fn({ messageId, id, parametersChunk: chunk, stage: "streaming" });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const throttled = (params: ToolBlockUpdateCallbackParams) => {
|
|
274
|
+
if (params.stage === "end") {
|
|
275
|
+
// Flush any deltas still pending inside the cooldown window first
|
|
276
|
+
if (timer) {
|
|
277
|
+
clearTimeout(timer);
|
|
278
|
+
timer = null;
|
|
279
|
+
}
|
|
280
|
+
fire();
|
|
281
|
+
fn(params);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (params.stage === "streaming") {
|
|
285
|
+
if (!pending) {
|
|
286
|
+
pending = { messageId: params.messageId, chunks: new Map() };
|
|
287
|
+
}
|
|
288
|
+
const prev = pending.chunks.get(params.id) || "";
|
|
289
|
+
pending.chunks.set(params.id, prev + (params.parametersChunk || ""));
|
|
290
|
+
if (!timer) {
|
|
291
|
+
timer = setTimeout(() => {
|
|
292
|
+
timer = null;
|
|
293
|
+
fire();
|
|
294
|
+
}, wait);
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// start / running — one-shot snapshots applied immediately. Drop this
|
|
299
|
+
// tool's buffered streaming deltas first: start/running carry the
|
|
300
|
+
// authoritative parameters, and a pending timer would otherwise fire late
|
|
301
|
+
// with a stale `streaming` event, regressing this tool block's stage back
|
|
302
|
+
// to streaming (yellow dot -> gray) mid-execution. Other tools' in-flight
|
|
303
|
+
// chunks are kept so interleaved multi-tool streaming still accumulates.
|
|
304
|
+
if (pending) {
|
|
305
|
+
pending.chunks.delete(params.id);
|
|
306
|
+
if (pending.chunks.size === 0) {
|
|
307
|
+
pending = null;
|
|
308
|
+
if (timer) {
|
|
309
|
+
clearTimeout(timer);
|
|
310
|
+
timer = null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
fn(params);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
throttled.cancel = () => {
|
|
318
|
+
if (timer) {
|
|
319
|
+
clearTimeout(timer);
|
|
320
|
+
timer = null;
|
|
321
|
+
}
|
|
322
|
+
pending = null;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
throttled.flush = () => {
|
|
326
|
+
if (timer) {
|
|
327
|
+
clearTimeout(timer);
|
|
328
|
+
timer = null;
|
|
329
|
+
}
|
|
330
|
+
fire();
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
return throttled;
|
|
334
|
+
}
|
|
335
|
+
|
|
239
336
|
export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
240
337
|
children,
|
|
241
338
|
bypassPermissions,
|
|
@@ -342,8 +439,13 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
342
439
|
|
|
343
440
|
const throttledToolBlockUpdate = useMemo(
|
|
344
441
|
() =>
|
|
345
|
-
|
|
346
|
-
const {
|
|
442
|
+
createToolStreamingThrottle((params) => {
|
|
443
|
+
const {
|
|
444
|
+
messageId,
|
|
445
|
+
id: toolBlockId,
|
|
446
|
+
parametersChunk,
|
|
447
|
+
...updates
|
|
448
|
+
} = params;
|
|
347
449
|
setMessages((prev) =>
|
|
348
450
|
prev.map((m) => {
|
|
349
451
|
if (m.id !== messageId) return m;
|
|
@@ -360,7 +462,8 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
360
462
|
id: toolBlockId,
|
|
361
463
|
name: updates.name || "",
|
|
362
464
|
stage: updates.stage || "start",
|
|
363
|
-
parameters:
|
|
465
|
+
parameters:
|
|
466
|
+
(updates.parameters || "") + (parametersChunk || ""),
|
|
364
467
|
result: updates.result || "",
|
|
365
468
|
...updates,
|
|
366
469
|
},
|
|
@@ -371,7 +474,18 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
371
474
|
...m,
|
|
372
475
|
blocks: m.blocks.map((b, idx) =>
|
|
373
476
|
idx === toolBlockIndex && b.type === "tool"
|
|
374
|
-
? {
|
|
477
|
+
? {
|
|
478
|
+
...b,
|
|
479
|
+
...updates,
|
|
480
|
+
// Streaming carries only the delta; append it to the
|
|
481
|
+
// accumulated parameters. start/running/end carry the
|
|
482
|
+
// authoritative value and replace wholesale.
|
|
483
|
+
parameters: parametersChunk
|
|
484
|
+
? (b.parameters || "") + parametersChunk
|
|
485
|
+
: updates.parameters !== undefined
|
|
486
|
+
? updates.parameters
|
|
487
|
+
: b.parameters,
|
|
488
|
+
}
|
|
375
489
|
: b,
|
|
376
490
|
),
|
|
377
491
|
};
|
|
@@ -419,6 +533,11 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
419
533
|
// Command state
|
|
420
534
|
const [slashCommands, setSlashCommands] = useState<SlashCommand[]>([]);
|
|
421
535
|
|
|
536
|
+
// Agent definitions (for /agents overlay)
|
|
537
|
+
const [agentDefinitions, setAgentDefinitions] = useState<
|
|
538
|
+
SubagentConfiguration[]
|
|
539
|
+
>([]);
|
|
540
|
+
|
|
422
541
|
// Permission state
|
|
423
542
|
const [permissionMode, setPermissionModeState] = useState<PermissionMode>(
|
|
424
543
|
initialPermissionMode ||
|
|
@@ -435,6 +554,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
435
554
|
hidePersistentOption?: boolean;
|
|
436
555
|
planContent?: string;
|
|
437
556
|
permissionMode?: PermissionMode;
|
|
557
|
+
warning?: string;
|
|
438
558
|
}
|
|
439
559
|
| undefined
|
|
440
560
|
>();
|
|
@@ -446,6 +566,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
446
566
|
hidePersistentOption?: boolean;
|
|
447
567
|
planContent?: string;
|
|
448
568
|
permissionMode?: PermissionMode;
|
|
569
|
+
warning?: string;
|
|
449
570
|
resolver: (decision: PermissionDecision) => void;
|
|
450
571
|
reject: () => void;
|
|
451
572
|
}>
|
|
@@ -457,6 +578,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
457
578
|
hidePersistentOption?: boolean;
|
|
458
579
|
planContent?: string;
|
|
459
580
|
permissionMode?: PermissionMode;
|
|
581
|
+
warning?: string;
|
|
460
582
|
resolver: (decision: PermissionDecision) => void;
|
|
461
583
|
reject: () => void;
|
|
462
584
|
} | null>(null);
|
|
@@ -498,6 +620,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
498
620
|
hidePersistentOption?: boolean,
|
|
499
621
|
planContent?: string,
|
|
500
622
|
permissionMode?: PermissionMode,
|
|
623
|
+
warning?: string,
|
|
501
624
|
): Promise<PermissionDecision> => {
|
|
502
625
|
return new Promise<PermissionDecision>((resolve, reject) => {
|
|
503
626
|
const queueItem = {
|
|
@@ -507,6 +630,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
507
630
|
hidePersistentOption,
|
|
508
631
|
planContent,
|
|
509
632
|
permissionMode,
|
|
633
|
+
warning,
|
|
510
634
|
resolver: resolve,
|
|
511
635
|
reject,
|
|
512
636
|
};
|
|
@@ -627,7 +751,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
627
751
|
),
|
|
628
752
|
);
|
|
629
753
|
},
|
|
630
|
-
onCompleteBangMessage: (command, exitCode, messageId) => {
|
|
754
|
+
onCompleteBangMessage: (command, exitCode, messageId, output) => {
|
|
631
755
|
if (isExpandedRef.current) return;
|
|
632
756
|
setMessages((prev) =>
|
|
633
757
|
prev.map((m) =>
|
|
@@ -636,7 +760,13 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
636
760
|
...m,
|
|
637
761
|
blocks: m.blocks.map((b, idx) =>
|
|
638
762
|
idx === m.blocks.length - 1 && b.type === "bang"
|
|
639
|
-
? {
|
|
763
|
+
? {
|
|
764
|
+
...b,
|
|
765
|
+
command,
|
|
766
|
+
exitCode,
|
|
767
|
+
stage: "end",
|
|
768
|
+
...(output !== undefined ? { output } : {}),
|
|
769
|
+
}
|
|
640
770
|
: b,
|
|
641
771
|
),
|
|
642
772
|
}
|
|
@@ -709,6 +839,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
709
839
|
context.hidePersistentOption,
|
|
710
840
|
context.planContent,
|
|
711
841
|
context.permissionMode,
|
|
842
|
+
context.warning,
|
|
712
843
|
);
|
|
713
844
|
} catch {
|
|
714
845
|
// If confirmation was cancelled or failed, deny the operation
|
|
@@ -778,6 +909,11 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
778
909
|
// Get initial commands
|
|
779
910
|
const agentSlashCommands = agent.getSlashCommands?.() || [];
|
|
780
911
|
setSlashCommands(agentSlashCommands);
|
|
912
|
+
|
|
913
|
+
// Get initial agent definitions
|
|
914
|
+
const initialAgentDefinitions =
|
|
915
|
+
agent.getSubagentConfigurations?.() || [];
|
|
916
|
+
setAgentDefinitions(initialAgentDefinitions);
|
|
781
917
|
} catch (error) {
|
|
782
918
|
console.error("Failed to initialize AI manager:", error);
|
|
783
919
|
}
|
|
@@ -818,6 +954,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
818
954
|
setMessages([]);
|
|
819
955
|
setMcpServerStatuses([]);
|
|
820
956
|
setSlashCommands([]);
|
|
957
|
+
setAgentDefinitions([]);
|
|
821
958
|
setSessionId("");
|
|
822
959
|
setIsLoading(false);
|
|
823
960
|
setLatestTotalTokens(0);
|
|
@@ -1050,6 +1187,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
1050
1187
|
hidePersistentOption: next.hidePersistentOption,
|
|
1051
1188
|
planContent: next.planContent,
|
|
1052
1189
|
permissionMode: next.permissionMode,
|
|
1190
|
+
warning: next.warning,
|
|
1053
1191
|
});
|
|
1054
1192
|
setIsConfirmationVisible(true);
|
|
1055
1193
|
setConfirmationQueue((prev) => prev.slice(1));
|
|
@@ -1205,6 +1343,7 @@ export const ChatProvider: React.FC<ChatProviderProps> = ({
|
|
|
1205
1343
|
stopBackgroundTask,
|
|
1206
1344
|
slashCommands,
|
|
1207
1345
|
hasSlashCommand,
|
|
1346
|
+
agentDefinitions,
|
|
1208
1347
|
permissionMode,
|
|
1209
1348
|
setPermissionMode,
|
|
1210
1349
|
isConfirmationVisible,
|