wave-code 1.1.5 → 1.3.0
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/bundle/wave.mjs +1075 -613
- package/package.json +4 -3
- package/src/cli.tsx +9 -0
- package/src/commands/plugin/install.ts +6 -7
- package/src/commands/plugin/uninstall.ts +11 -5
- package/src/components/ChatInterface.tsx +3 -0
- package/src/components/ConfirmationDetails.tsx +36 -15
- package/src/components/ConfirmationSelector.tsx +39 -0
- package/src/components/HooksManager.tsx +286 -0
- package/src/components/InputBox.tsx +125 -51
- package/src/components/Markdown.tsx +21 -8
- package/src/components/MarketplaceDetail.tsx +10 -27
- package/src/components/MarketplaceView.tsx +17 -3
- package/src/components/MessageBlockItem.tsx +0 -5
- package/src/components/MessageList.tsx +0 -1
- package/src/components/Notifications.tsx +7 -0
- package/src/components/PlanView.tsx +146 -0
- package/src/components/PluginManagerShell.tsx +3 -3
- package/src/components/PluginManagerTypes.ts +2 -1
- package/src/components/ResumeCommand.tsx +84 -0
- package/src/components/ScrolledContent.tsx +71 -0
- package/src/components/SessionSelector.tsx +184 -6
- package/src/components/SessionSelectorFlow.tsx +246 -0
- package/src/constants/commands.ts +30 -0
- package/src/contexts/useChat.tsx +252 -98
- package/src/daemon/commands.ts +1046 -67
- package/src/daemon-cli.ts +6 -0
- package/src/hooks/useInputManager.ts +30 -0
- package/src/hooks/useLineScroll.ts +58 -0
- package/src/hooks/usePluginManager.ts +104 -78
- package/src/index.ts +156 -16
- package/src/managers/inputHandlers.ts +16 -9
- package/src/managers/inputReducer.ts +39 -9
- package/src/reducers/confirmationReducer.ts +20 -1
- package/src/reducers/hooksManagerReducer.ts +92 -0
- package/src/reducers/selectorReducer.ts +3 -0
- package/src/session-selector-cli.tsx +9 -255
- package/src/stdio/agentBridge.ts +932 -133
- package/src/stdio/daemonServer.ts +26 -82
- package/src/stdio/protocol.ts +23 -4
- package/src/stdio-cli.ts +60 -0
- package/src/utils/rewindCheckpoints.ts +2 -2
- package/src/utils/sessionOwnership.ts +62 -0
- package/src/utils/worktree.ts +175 -50
- package/src/components/BangDisplay.tsx +0 -41
- package/src/components/PlanDisplay.tsx +0 -18
|
@@ -8,7 +8,9 @@ import { BackgroundTaskManager } from "./BackgroundTaskManager.js";
|
|
|
8
8
|
import { McpManager } from "./McpManager.js";
|
|
9
9
|
import { AgentsManager } from "./AgentsManager.js";
|
|
10
10
|
import { SkillsManager } from "./SkillsManager.js";
|
|
11
|
+
import { HooksManager } from "./HooksManager.js";
|
|
11
12
|
import { RewindCommand } from "./RewindCommand.js";
|
|
13
|
+
import { ResumeCommand } from "./ResumeCommand.js";
|
|
12
14
|
import { HelpView } from "./HelpView.js";
|
|
13
15
|
import { StatusCommand } from "./StatusCommand.js";
|
|
14
16
|
import { LoginCommand } from "./LoginCommand.js";
|
|
@@ -18,10 +20,15 @@ import { WorkflowManager } from "./WorkflowManager.js";
|
|
|
18
20
|
import { StatusLine } from "./StatusLine.js";
|
|
19
21
|
import { Notifications } from "./Notifications.js";
|
|
20
22
|
import { BtwDisplay } from "./BtwDisplay.js";
|
|
23
|
+
import { PlanView } from "./PlanView.js";
|
|
21
24
|
import { useInputManager } from "../hooks/useInputManager.js";
|
|
22
25
|
import { useChat } from "../contexts/useChat.js";
|
|
23
26
|
|
|
24
|
-
import type {
|
|
27
|
+
import type {
|
|
28
|
+
McpServerStatus,
|
|
29
|
+
SlashCommand,
|
|
30
|
+
HookEventConfig,
|
|
31
|
+
} from "wave-agent-sdk";
|
|
25
32
|
|
|
26
33
|
export const INPUT_PLACEHOLDER_TEXT =
|
|
27
34
|
"Type your message (use /help for more info)...";
|
|
@@ -49,6 +56,10 @@ export interface InputBoxProps {
|
|
|
49
56
|
// Slash Command related properties
|
|
50
57
|
slashCommands?: SlashCommand[];
|
|
51
58
|
hasSlashCommand?: (commandId: string) => boolean;
|
|
59
|
+
// Hooks related properties (for /hooks overlay)
|
|
60
|
+
hooks?: Partial<
|
|
61
|
+
Record<"user" | "project" | "plugin", Record<string, HookEventConfig[]>>
|
|
62
|
+
>;
|
|
52
63
|
// Token usage
|
|
53
64
|
latestTotalTokens?: number;
|
|
54
65
|
maxInputTokens?: number;
|
|
@@ -67,6 +78,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
67
78
|
disconnectMcpServer = async () => false,
|
|
68
79
|
slashCommands = [],
|
|
69
80
|
hasSlashCommand = () => false,
|
|
81
|
+
hooks = {},
|
|
70
82
|
latestTotalTokens = 0,
|
|
71
83
|
maxInputTokens = 200000,
|
|
72
84
|
showLoginHint = false,
|
|
@@ -75,6 +87,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
75
87
|
permissionMode: chatPermissionMode,
|
|
76
88
|
setPermissionMode: setChatPermissionMode,
|
|
77
89
|
handleRewindSelect,
|
|
90
|
+
resumeSession,
|
|
78
91
|
backgroundCurrentTask,
|
|
79
92
|
messages,
|
|
80
93
|
getFullMessageThread,
|
|
@@ -84,15 +97,21 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
84
97
|
clearMessages,
|
|
85
98
|
compact,
|
|
86
99
|
addDir,
|
|
100
|
+
renameSession,
|
|
87
101
|
currentModel,
|
|
88
102
|
configuredModels,
|
|
89
103
|
setModel,
|
|
90
|
-
|
|
104
|
+
reloadPlugins,
|
|
105
|
+
notifyPluginChange,
|
|
106
|
+
pluginHint,
|
|
91
107
|
recallQueuedMessage,
|
|
92
108
|
queuedMessages,
|
|
93
109
|
setIsBtwActive,
|
|
94
110
|
agentDefinitions,
|
|
95
111
|
skills,
|
|
112
|
+
planView,
|
|
113
|
+
setPlanView,
|
|
114
|
+
handlePlanCommand,
|
|
96
115
|
} = useChat();
|
|
97
116
|
|
|
98
117
|
// Ref to hold setInputText so queue callbacks can access it before useInputManager returns
|
|
@@ -139,6 +158,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
139
158
|
showMcpManager,
|
|
140
159
|
showAgentsManager,
|
|
141
160
|
showRewindManager,
|
|
161
|
+
showResumeSelector,
|
|
142
162
|
showHelp,
|
|
143
163
|
showStatusCommand,
|
|
144
164
|
showLoginCommand,
|
|
@@ -146,10 +166,12 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
146
166
|
showModelSelector,
|
|
147
167
|
showWorkflowManager,
|
|
148
168
|
showSkillsManager,
|
|
169
|
+
showHooksManager,
|
|
149
170
|
setShowBackgroundTaskManager,
|
|
150
171
|
setShowMcpManager,
|
|
151
172
|
setShowAgentsManager,
|
|
152
173
|
setShowRewindManager,
|
|
174
|
+
setShowResumeSelector,
|
|
153
175
|
setShowHelp,
|
|
154
176
|
setShowStatusCommand,
|
|
155
177
|
setShowLoginCommand,
|
|
@@ -157,6 +179,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
157
179
|
setShowModelSelector,
|
|
158
180
|
setShowWorkflowManager,
|
|
159
181
|
setShowSkillsManager,
|
|
182
|
+
setShowHooksManager,
|
|
160
183
|
// Permission mode
|
|
161
184
|
permissionMode,
|
|
162
185
|
setPermissionMode,
|
|
@@ -175,11 +198,14 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
175
198
|
onAskBtw: askBtw,
|
|
176
199
|
onClearMessages: clearMessages,
|
|
177
200
|
onCompact: compact,
|
|
201
|
+
onReloadPlugins: reloadPlugins,
|
|
178
202
|
onAddDir: addDir,
|
|
203
|
+
onRenameSession: renameSession,
|
|
179
204
|
onHasSlashCommand: hasSlashCommand,
|
|
180
205
|
onAbortMessage: abortMessage,
|
|
181
206
|
onBackgroundCurrentTask: backgroundCurrentTask,
|
|
182
207
|
onPermissionModeChange: setChatPermissionMode,
|
|
208
|
+
onPlanCommand: handlePlanCommand,
|
|
183
209
|
sessionId,
|
|
184
210
|
workdir: workingDirectory,
|
|
185
211
|
getFullMessageThread,
|
|
@@ -211,7 +237,9 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
211
237
|
// If they are active, we should skip InputBox's global input handling to avoid
|
|
212
238
|
// duplicate dispatches or state update conflicts.
|
|
213
239
|
if (
|
|
240
|
+
planView ||
|
|
214
241
|
showRewindManager ||
|
|
242
|
+
showResumeSelector ||
|
|
215
243
|
showHelp ||
|
|
216
244
|
showStatusCommand ||
|
|
217
245
|
showLoginCommand ||
|
|
@@ -221,7 +249,8 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
221
249
|
showMcpManager ||
|
|
222
250
|
showAgentsManager ||
|
|
223
251
|
showWorkflowManager ||
|
|
224
|
-
showSkillsManager
|
|
252
|
+
showSkillsManager ||
|
|
253
|
+
showHooksManager
|
|
225
254
|
) {
|
|
226
255
|
return;
|
|
227
256
|
}
|
|
@@ -264,6 +293,21 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
264
293
|
await handleRewindSelect(index);
|
|
265
294
|
};
|
|
266
295
|
|
|
296
|
+
const handleResumeSelectWithClose = async (
|
|
297
|
+
sessionId: string,
|
|
298
|
+
resumeWorkdir?: string,
|
|
299
|
+
) => {
|
|
300
|
+
setShowResumeSelector(false);
|
|
301
|
+
// The switch replaces the whole conversation; a draft typed for the old
|
|
302
|
+
// session would otherwise silently become the first message of the new one.
|
|
303
|
+
setInputText("");
|
|
304
|
+
await resumeSession(sessionId, resumeWorkdir);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const handleResumeCancel = () => {
|
|
308
|
+
setShowResumeSelector(false);
|
|
309
|
+
};
|
|
310
|
+
|
|
267
311
|
return (
|
|
268
312
|
<Box flexDirection="column">
|
|
269
313
|
<BtwDisplay btwState={btwState} />
|
|
@@ -324,6 +368,13 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
324
368
|
/>
|
|
325
369
|
)}
|
|
326
370
|
|
|
371
|
+
{showHooksManager && (
|
|
372
|
+
<HooksManager
|
|
373
|
+
onCancel={() => setShowHooksManager(false)}
|
|
374
|
+
hooks={hooks}
|
|
375
|
+
/>
|
|
376
|
+
)}
|
|
377
|
+
|
|
327
378
|
{showWorkflowManager && (
|
|
328
379
|
<WorkflowManager onCancel={() => setShowWorkflowManager(false)} />
|
|
329
380
|
)}
|
|
@@ -337,6 +388,15 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
337
388
|
/>
|
|
338
389
|
)}
|
|
339
390
|
|
|
391
|
+
{showResumeSelector && (
|
|
392
|
+
<ResumeCommand
|
|
393
|
+
workdir={workingDirectory}
|
|
394
|
+
isBusy={!isIdle}
|
|
395
|
+
onSelect={handleResumeSelectWithClose}
|
|
396
|
+
onCancel={handleResumeCancel}
|
|
397
|
+
/>
|
|
398
|
+
)}
|
|
399
|
+
|
|
340
400
|
{showHelp && (
|
|
341
401
|
<HelpView
|
|
342
402
|
onCancel={() => setShowHelp(false)}
|
|
@@ -355,7 +415,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
355
415
|
{showPluginManager && (
|
|
356
416
|
<PluginManagerShell
|
|
357
417
|
onCancel={() => setShowPluginManager(false)}
|
|
358
|
-
|
|
418
|
+
onPluginChanged={notifyPluginChange}
|
|
359
419
|
/>
|
|
360
420
|
)}
|
|
361
421
|
|
|
@@ -368,56 +428,70 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
368
428
|
/>
|
|
369
429
|
)}
|
|
370
430
|
|
|
431
|
+
{planView && (
|
|
432
|
+
<PlanView
|
|
433
|
+
path={planView.path}
|
|
434
|
+
content={planView.content}
|
|
435
|
+
message={planView.message}
|
|
436
|
+
onCancel={() => setPlanView(null)}
|
|
437
|
+
/>
|
|
438
|
+
)}
|
|
439
|
+
|
|
371
440
|
{btwState.question || btwState.answer
|
|
372
441
|
? null
|
|
373
|
-
:
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
<Box
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
{
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
442
|
+
: planView
|
|
443
|
+
? null
|
|
444
|
+
: showBackgroundTaskManager ||
|
|
445
|
+
showMcpManager ||
|
|
446
|
+
showAgentsManager ||
|
|
447
|
+
showSkillsManager ||
|
|
448
|
+
showHooksManager ||
|
|
449
|
+
showRewindManager ||
|
|
450
|
+
showResumeSelector ||
|
|
451
|
+
showHelp ||
|
|
452
|
+
showStatusCommand ||
|
|
453
|
+
showLoginCommand ||
|
|
454
|
+
showPluginManager ||
|
|
455
|
+
showModelSelector ||
|
|
456
|
+
showWorkflowManager || (
|
|
457
|
+
<Box flexDirection="column">
|
|
458
|
+
{escClearPending && (
|
|
459
|
+
<Text color="gray">Press Esc again to clear input</Text>
|
|
460
|
+
)}
|
|
461
|
+
<Box
|
|
462
|
+
borderStyle="single"
|
|
463
|
+
borderColor="gray"
|
|
464
|
+
borderLeft={false}
|
|
465
|
+
borderRight={false}
|
|
466
|
+
>
|
|
467
|
+
<Text color={isPlaceholder ? "gray" : "white"}>
|
|
468
|
+
{shouldShowCursor ? (
|
|
469
|
+
<>
|
|
470
|
+
{beforeCursor}
|
|
471
|
+
<Text backgroundColor="white" color="black">
|
|
472
|
+
{atCursor}
|
|
473
|
+
</Text>
|
|
474
|
+
{afterCursor}
|
|
475
|
+
</>
|
|
476
|
+
) : (
|
|
477
|
+
displayText
|
|
478
|
+
)}
|
|
479
|
+
</Text>
|
|
480
|
+
</Box>
|
|
481
|
+
<Box justifyContent="space-between">
|
|
482
|
+
<StatusLine
|
|
483
|
+
permissionMode={permissionMode}
|
|
484
|
+
isShellCommand={isShellCommand}
|
|
485
|
+
/>
|
|
486
|
+
<Notifications
|
|
487
|
+
latestTotalTokens={latestTotalTokens}
|
|
488
|
+
maxInputTokens={maxInputTokens}
|
|
489
|
+
showLoginHint={showLoginHint}
|
|
490
|
+
pluginHint={pluginHint}
|
|
491
|
+
/>
|
|
492
|
+
</Box>
|
|
418
493
|
</Box>
|
|
419
|
-
|
|
420
|
-
)}
|
|
494
|
+
)}
|
|
421
495
|
</Box>
|
|
422
496
|
);
|
|
423
497
|
};
|
|
@@ -137,21 +137,34 @@ class AnsiRenderer extends Renderer<string> {
|
|
|
137
137
|
|
|
138
138
|
const createRenderer = (columns: number) => new AnsiRenderer(columns);
|
|
139
139
|
|
|
140
|
+
/** Renders Markdown to ANSI-styled text using the same renderer as <Markdown>.
|
|
141
|
+
* Callers that must linearize content into rows (e.g. the confirmation
|
|
142
|
+
* details area, which slices rows for PgUp/PgDn scrolling) render first and
|
|
143
|
+
* then split the result into lines. */
|
|
144
|
+
export const renderMarkdownToAnsi = (
|
|
145
|
+
content: string,
|
|
146
|
+
columns: number,
|
|
147
|
+
): string => {
|
|
148
|
+
const ansi = marked.parse(content, {
|
|
149
|
+
renderer: createRenderer(columns),
|
|
150
|
+
gfm: true,
|
|
151
|
+
breaks: true,
|
|
152
|
+
}) as string;
|
|
153
|
+
return ansi.trim();
|
|
154
|
+
};
|
|
155
|
+
|
|
140
156
|
// Markdown component using custom ANSI renderer
|
|
141
157
|
export const Markdown = React.memo(({ children }: MarkdownProps) => {
|
|
142
158
|
const { stdout } = useStdout();
|
|
143
159
|
const columns = stdout?.columns ?? 80;
|
|
144
|
-
const ansiContent = useMemo(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
breaks: true,
|
|
149
|
-
}) as string;
|
|
150
|
-
}, [children, columns]);
|
|
160
|
+
const ansiContent = useMemo(
|
|
161
|
+
() => renderMarkdownToAnsi(children, columns),
|
|
162
|
+
[children, columns],
|
|
163
|
+
);
|
|
151
164
|
|
|
152
165
|
return (
|
|
153
166
|
<Box flexDirection="column">
|
|
154
|
-
<Text>{ansiContent
|
|
167
|
+
<Text>{ansiContent}</Text>
|
|
155
168
|
</Box>
|
|
156
169
|
);
|
|
157
170
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useReducer, useEffect
|
|
1
|
+
import React, { useReducer, useEffect } from "react";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
3
|
import { usePluginManagerContext } from "../contexts/PluginManagerContext.js";
|
|
4
4
|
import {
|
|
@@ -6,6 +6,13 @@ import {
|
|
|
6
6
|
type SelectorState,
|
|
7
7
|
} from "../reducers/selectorReducer.js";
|
|
8
8
|
|
|
9
|
+
const ACTIONS = [
|
|
10
|
+
// 先刷新该市场检出,再把该市场内所有可更新插件一次升掉(范围恒为当前市场,
|
|
11
|
+
// spec 插件市场场景 13)
|
|
12
|
+
{ id: "update", label: "Update plugins (batch)" },
|
|
13
|
+
{ id: "remove", label: "Remove marketplace" },
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
9
16
|
export const MarketplaceDetail: React.FC = () => {
|
|
10
17
|
const { state, marketplaces, actions } = usePluginManagerContext();
|
|
11
18
|
const [selectorState, dispatch] = useReducer(
|
|
@@ -25,23 +32,10 @@ export const MarketplaceDetail: React.FC = () => {
|
|
|
25
32
|
|
|
26
33
|
const marketplace = marketplaces.find((m) => m.name === state.selectedId);
|
|
27
34
|
|
|
28
|
-
const ACTIONS = useMemo(
|
|
29
|
-
() =>
|
|
30
|
-
[
|
|
31
|
-
{
|
|
32
|
-
id: "toggle-auto-update",
|
|
33
|
-
label: `${marketplace?.autoUpdate ? "Disable" : "Enable"} auto-update`,
|
|
34
|
-
},
|
|
35
|
-
{ id: "update", label: "Update marketplace" },
|
|
36
|
-
{ id: "remove", label: "Remove marketplace" },
|
|
37
|
-
] as const,
|
|
38
|
-
[marketplace?.autoUpdate],
|
|
39
|
-
);
|
|
40
|
-
|
|
41
35
|
// Sync ACTIONS into reducer state
|
|
42
36
|
useEffect(() => {
|
|
43
37
|
dispatch({ type: "SET_ITEMS", items: [...ACTIONS] });
|
|
44
|
-
}, [
|
|
38
|
+
}, []);
|
|
45
39
|
|
|
46
40
|
useInput((_input, key) => {
|
|
47
41
|
if (state.isLoading && !key.escape) return;
|
|
@@ -58,9 +52,7 @@ export const MarketplaceDetail: React.FC = () => {
|
|
|
58
52
|
|
|
59
53
|
if (pendingDecision === "select" && marketplace && !state.isLoading) {
|
|
60
54
|
const action = actionsList[selectedActionIndex]?.id;
|
|
61
|
-
if (action === "
|
|
62
|
-
actions.toggleAutoUpdate(marketplace.name, !marketplace.autoUpdate);
|
|
63
|
-
} else if (action === "update") {
|
|
55
|
+
if (action === "update") {
|
|
64
56
|
actions.updateMarketplace(marketplace.name);
|
|
65
57
|
} else if (action === "remove") {
|
|
66
58
|
actions.removeMarketplace(marketplace.name);
|
|
@@ -104,15 +96,6 @@ export const MarketplaceDetail: React.FC = () => {
|
|
|
104
96
|
<Text>Source: {JSON.stringify(marketplace.source)}</Text>
|
|
105
97
|
</Box>
|
|
106
98
|
|
|
107
|
-
<Box marginBottom={1}>
|
|
108
|
-
<Text>
|
|
109
|
-
Auto-update:{" "}
|
|
110
|
-
<Text color={marketplace.autoUpdate ? "green" : "red"}>
|
|
111
|
-
{marketplace.autoUpdate ? "Enabled" : "Disabled"}
|
|
112
|
-
</Text>
|
|
113
|
-
</Text>
|
|
114
|
-
</Box>
|
|
115
|
-
|
|
116
99
|
{marketplace.lastUpdated && (
|
|
117
100
|
<Box marginBottom={1}>
|
|
118
101
|
<Text>
|
|
@@ -10,7 +10,12 @@ import {
|
|
|
10
10
|
import { MarketplaceList } from "./MarketplaceList.js";
|
|
11
11
|
|
|
12
12
|
export const MarketplaceView: React.FC = () => {
|
|
13
|
-
const {
|
|
13
|
+
const {
|
|
14
|
+
marketplaces,
|
|
15
|
+
checkingForUpdates,
|
|
16
|
+
state: managerState,
|
|
17
|
+
actions,
|
|
18
|
+
} = usePluginManagerContext();
|
|
14
19
|
const [state, dispatch] = useReducer(selectorReducer<KnownMarketplace>, {
|
|
15
20
|
selectedIndex: 0,
|
|
16
21
|
pendingDecision: null,
|
|
@@ -19,10 +24,16 @@ export const MarketplaceView: React.FC = () => {
|
|
|
19
24
|
|
|
20
25
|
const { selectedIndex, pendingDecision, items } = state;
|
|
21
26
|
|
|
22
|
-
// Sync marketplaces into reducer state
|
|
27
|
+
// Sync marketplaces into reducer state;焦点落到当前选中的市场(新增市场后由
|
|
28
|
+
// addMarketplace 设成新市场名,spec「管理市场」场景 5),没有匹配(首帧、市场被
|
|
29
|
+
// 移除)时保持 SET_ITEMS 归零后的列表首项。
|
|
23
30
|
useEffect(() => {
|
|
24
31
|
dispatch({ type: "SET_ITEMS", items: marketplaces });
|
|
25
|
-
|
|
32
|
+
const index = marketplaces.findIndex(
|
|
33
|
+
(m) => m.name === managerState.selectedId,
|
|
34
|
+
);
|
|
35
|
+
if (index > 0) dispatch({ type: "SET_INDEX", index });
|
|
36
|
+
}, [marketplaces, managerState.selectedId]);
|
|
26
37
|
|
|
27
38
|
useInput((input, key) => {
|
|
28
39
|
if (input === "a") {
|
|
@@ -52,6 +63,9 @@ export const MarketplaceView: React.FC = () => {
|
|
|
52
63
|
<Box flexDirection="column">
|
|
53
64
|
<Box marginBottom={1}>
|
|
54
65
|
<Text color="green">Press 'a' to add a new marketplace</Text>
|
|
66
|
+
{/* 打开插件管理器触发的后台清单刷新(只拉检出、不升级插件):不阻塞
|
|
67
|
+
列表浏览,完成后重读并呈现最新清单(spec ecosystem/plugin 场景 12) */}
|
|
68
|
+
{checkingForUpdates && <Text dimColor> Checking for updates...</Text>}
|
|
55
69
|
</Box>
|
|
56
70
|
<MarketplaceList
|
|
57
71
|
marketplaces={marketplaces}
|
|
@@ -2,7 +2,6 @@ import React from "react";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import type { Message, MessageBlock } from "wave-agent-sdk";
|
|
4
4
|
import { MessageSource } from "wave-agent-sdk";
|
|
5
|
-
import { BangDisplay } from "./BangDisplay.js";
|
|
6
5
|
import { ToolDisplay } from "./ToolDisplay.js";
|
|
7
6
|
import { CompactDisplay } from "./CompactDisplay.js";
|
|
8
7
|
import { ReasoningDisplay } from "./ReasoningDisplay.js";
|
|
@@ -54,10 +53,6 @@ export const MessageBlockItem = ({
|
|
|
54
53
|
</Box>
|
|
55
54
|
)}
|
|
56
55
|
|
|
57
|
-
{block.type === "bang" && (
|
|
58
|
-
<BangDisplay block={block} isExpanded={isExpanded} />
|
|
59
|
-
)}
|
|
60
|
-
|
|
61
56
|
{block.type === "tool" && (
|
|
62
57
|
<ToolDisplay block={block} isExpanded={isExpanded} />
|
|
63
58
|
)}
|
|
@@ -47,7 +47,6 @@ export const MessageList = React.memo(
|
|
|
47
47
|
(b.stage === "running" ||
|
|
48
48
|
b.stage === "streaming" ||
|
|
49
49
|
b.stage === "start")) ||
|
|
50
|
-
(b.type === "bang" && b.stage === "running") ||
|
|
51
50
|
(b.type === "reasoning" && b.stage === "streaming") ||
|
|
52
51
|
(b.type === "text" && b.stage === "streaming");
|
|
53
52
|
|
|
@@ -5,12 +5,18 @@ export interface NotificationsProps {
|
|
|
5
5
|
latestTotalTokens?: number;
|
|
6
6
|
maxInputTokens?: number;
|
|
7
7
|
showLoginHint?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* 一次性中性提示(插件变更待应用 / 重载完成)。由宿主显式设置并在数秒后清除,
|
|
10
|
+
* 不提供持久可见标识(docs/specs/ecosystem/plugin.md「插件变更提示」)。
|
|
11
|
+
*/
|
|
12
|
+
pluginHint?: string | null;
|
|
8
13
|
}
|
|
9
14
|
|
|
10
15
|
export const Notifications: React.FC<NotificationsProps> = ({
|
|
11
16
|
latestTotalTokens = 0,
|
|
12
17
|
maxInputTokens = 200000,
|
|
13
18
|
showLoginHint = false,
|
|
19
|
+
pluginHint = null,
|
|
14
20
|
}) => {
|
|
15
21
|
const percentage =
|
|
16
22
|
latestTotalTokens > 0
|
|
@@ -22,6 +28,7 @@ export const Notifications: React.FC<NotificationsProps> = ({
|
|
|
22
28
|
|
|
23
29
|
return (
|
|
24
30
|
<Box gap={1}>
|
|
31
|
+
{pluginHint && <Text color="gray">{pluginHint}</Text>}
|
|
25
32
|
{showLoginHint && <Text color="gray">Type /login to authenticate</Text>}
|
|
26
33
|
{percentage > 0 && (
|
|
27
34
|
<Text color={contextColor}>{percentage}% context</Text>
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { Box, Text, useInput, useWindowSize } from "ink";
|
|
3
|
+
import { useLineScroll } from "../hooks/useLineScroll.js";
|
|
4
|
+
import { ScrolledContent } from "./ScrolledContent.js";
|
|
5
|
+
|
|
6
|
+
// Row budget for the scroll indicators (↑ more / ↓ more, up to one each).
|
|
7
|
+
const SCROLL_INDICATOR_BUDGET = 2;
|
|
8
|
+
// Row budget for the fixed scroll-key hint shown when content is scrollable.
|
|
9
|
+
const SCROLL_HINT_BUDGET = 1;
|
|
10
|
+
// Overlay height as a fraction of the terminal height (spec: 50-60%).
|
|
11
|
+
const PLAN_OVERLAY_HEIGHT_RATIO = 0.55;
|
|
12
|
+
|
|
13
|
+
export interface PlanViewProps {
|
|
14
|
+
path?: string;
|
|
15
|
+
content?: string;
|
|
16
|
+
message?: string;
|
|
17
|
+
/** Available height (rows) for the whole overlay, computed by default from
|
|
18
|
+
* the terminal size. Tests pass it explicitly for determinism. */
|
|
19
|
+
maxHeight?: number;
|
|
20
|
+
onCancel: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Overlay shown by the /plan command:
|
|
25
|
+
* - With `message` only: a plain status line (e.g. "Enabled plan mode").
|
|
26
|
+
* - With `path`/`content`: the current plan file contents as plain text lines
|
|
27
|
+
* (no Markdown), fixed at ~55% of the terminal height with PgUp/PgDn and
|
|
28
|
+
* Ctrl+u/Ctrl+d scrolling and ↑/↓ "N more" indicators.
|
|
29
|
+
* Esc dismisses the overlay.
|
|
30
|
+
*/
|
|
31
|
+
export const PlanView: React.FC<PlanViewProps> = ({
|
|
32
|
+
path,
|
|
33
|
+
content,
|
|
34
|
+
message,
|
|
35
|
+
maxHeight,
|
|
36
|
+
onCancel,
|
|
37
|
+
}) => {
|
|
38
|
+
const { rows } = useWindowSize();
|
|
39
|
+
const overlayMaxHeight =
|
|
40
|
+
maxHeight ?? Math.max(Math.round(rows * PLAN_OVERLAY_HEIGHT_RATIO), 10);
|
|
41
|
+
|
|
42
|
+
const isMessageOnly = message !== undefined && content === undefined;
|
|
43
|
+
|
|
44
|
+
useInput((_input, key) => {
|
|
45
|
+
if (key.escape) {
|
|
46
|
+
onCancel();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Hook order must be stable across renders, so compute the scroll state
|
|
51
|
+
// before the message-only early return.
|
|
52
|
+
const headerRows = 2 + (path ? 1 : 0); // "Current Plan" + path + footer hint
|
|
53
|
+
const visibleCount = Math.max(
|
|
54
|
+
1,
|
|
55
|
+
overlayMaxHeight -
|
|
56
|
+
headerRows -
|
|
57
|
+
SCROLL_INDICATOR_BUDGET -
|
|
58
|
+
SCROLL_HINT_BUDGET,
|
|
59
|
+
);
|
|
60
|
+
const planLines =
|
|
61
|
+
!isMessageOnly && content !== undefined && content !== ""
|
|
62
|
+
? content.split("\n")
|
|
63
|
+
: [];
|
|
64
|
+
// Rows really occupied by the plan (wrapped rows included), measured after
|
|
65
|
+
// layout by <ScrolledContent>.
|
|
66
|
+
const [contentRows, setContentRows] = useState(0);
|
|
67
|
+
const { scrollOffset, hasMoreAbove, hasMoreBelow } = useLineScroll({
|
|
68
|
+
totalLines: contentRows,
|
|
69
|
+
visibleCount,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (isMessageOnly) {
|
|
73
|
+
return (
|
|
74
|
+
<Box
|
|
75
|
+
flexDirection="column"
|
|
76
|
+
paddingX={1}
|
|
77
|
+
borderStyle="single"
|
|
78
|
+
borderColor="cyan"
|
|
79
|
+
borderLeft={false}
|
|
80
|
+
borderRight={false}
|
|
81
|
+
>
|
|
82
|
+
<Text color="cyan">{message}</Text>
|
|
83
|
+
<Text dimColor>Press Escape to continue</Text>
|
|
84
|
+
</Box>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Box
|
|
90
|
+
flexDirection="column"
|
|
91
|
+
paddingX={1}
|
|
92
|
+
borderStyle="single"
|
|
93
|
+
borderColor="cyan"
|
|
94
|
+
borderLeft={false}
|
|
95
|
+
borderRight={false}
|
|
96
|
+
>
|
|
97
|
+
<Text color="cyan" bold>
|
|
98
|
+
Current Plan
|
|
99
|
+
</Text>
|
|
100
|
+
{path ? (
|
|
101
|
+
<Text dimColor wrap="truncate-end">
|
|
102
|
+
{path}
|
|
103
|
+
</Text>
|
|
104
|
+
) : null}
|
|
105
|
+
{hasMoreAbove && (
|
|
106
|
+
<Box>
|
|
107
|
+
<Text color="gray" dimColor>
|
|
108
|
+
↑ {scrollOffset} more
|
|
109
|
+
</Text>
|
|
110
|
+
</Box>
|
|
111
|
+
)}
|
|
112
|
+
<ScrolledContent
|
|
113
|
+
visibleRows={visibleCount}
|
|
114
|
+
scrollOffset={scrollOffset}
|
|
115
|
+
onMeasuredRows={setContentRows}
|
|
116
|
+
>
|
|
117
|
+
{planLines.length > 0 ? (
|
|
118
|
+
planLines.map((line, index) => (
|
|
119
|
+
<Box key={index}>
|
|
120
|
+
<Text>{line || " "}</Text>
|
|
121
|
+
</Box>
|
|
122
|
+
))
|
|
123
|
+
) : (
|
|
124
|
+
<Text>No plan written yet.</Text>
|
|
125
|
+
)}
|
|
126
|
+
</ScrolledContent>
|
|
127
|
+
{hasMoreBelow && (
|
|
128
|
+
<Box>
|
|
129
|
+
<Text color="gray" dimColor>
|
|
130
|
+
↓ {contentRows - scrollOffset - visibleCount} more
|
|
131
|
+
</Text>
|
|
132
|
+
</Box>
|
|
133
|
+
)}
|
|
134
|
+
{(hasMoreAbove || hasMoreBelow) && (
|
|
135
|
+
<Box>
|
|
136
|
+
<Text color="gray" dimColor>
|
|
137
|
+
PgUp/PgDn page • Ctrl+u/d half page
|
|
138
|
+
</Text>
|
|
139
|
+
</Box>
|
|
140
|
+
)}
|
|
141
|
+
<Text dimColor>Press Escape to continue</Text>
|
|
142
|
+
</Box>
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
PlanView.displayName = "PlanView";
|
|
@@ -13,9 +13,9 @@ import { PluginManagerContext } from "../contexts/PluginManagerContext.js";
|
|
|
13
13
|
export const PluginManagerShell: React.FC<{
|
|
14
14
|
children?: React.ReactNode;
|
|
15
15
|
onCancel?: () => void;
|
|
16
|
-
|
|
17
|
-
}> = ({ children, onCancel,
|
|
18
|
-
const pluginManager = usePluginManager({
|
|
16
|
+
onPluginChanged?: () => void;
|
|
17
|
+
}> = ({ children, onCancel, onPluginChanged }) => {
|
|
18
|
+
const pluginManager = usePluginManager({ onPluginChanged });
|
|
19
19
|
const { state, actions, discoverablePlugins } = pluginManager;
|
|
20
20
|
|
|
21
21
|
const setView = (view: ViewType) => {
|