wave-code 1.2.0 → 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 +1049 -584
- 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 +1 -0
- package/src/components/ConfirmationDetails.tsx +36 -15
- package/src/components/ConfirmationSelector.tsx +39 -0
- package/src/components/InputBox.tsx +39 -2
- package/src/components/Markdown.tsx +21 -8
- package/src/components/MarketplaceDetail.tsx +10 -27
- package/src/components/MarketplaceView.tsx +17 -3
- package/src/components/Notifications.tsx +7 -0
- package/src/components/PlanView.tsx +22 -17
- 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 +18 -0
- package/src/contexts/useChat.tsx +158 -28
- package/src/daemon/commands.ts +569 -46
- package/src/daemon-cli.ts +6 -0
- package/src/hooks/useInputManager.ts +16 -0
- package/src/hooks/usePluginManager.ts +104 -78
- package/src/index.ts +62 -8
- package/src/managers/inputHandlers.ts +14 -9
- package/src/managers/inputReducer.ts +29 -9
- package/src/reducers/confirmationReducer.ts +20 -1
- package/src/reducers/selectorReducer.ts +3 -0
- package/src/session-selector-cli.tsx +9 -255
- package/src/stdio/agentBridge.ts +397 -95
- package/src/stdio/protocol.ts +9 -0
- package/src/stdio-cli.ts +6 -0
- package/src/utils/sessionOwnership.ts +62 -0
- package/src/components/PlanDisplay.tsx +0 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-code",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "CLI-based code assistant powered by AI, built with React and Ink",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@vscode/ripgrep": "^1.18.0"
|
|
31
|
+
"@vscode/ripgrep": "^1.18.0",
|
|
32
|
+
"sharp": "^0.35.4"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
34
35
|
"@types/react": "^19.1.8",
|
|
@@ -56,7 +57,7 @@
|
|
|
56
57
|
"wrap-ansi": "^10.0.0",
|
|
57
58
|
"yargs": "^17.7.2",
|
|
58
59
|
"zod": "^3.23.8",
|
|
59
|
-
"wave-agent-sdk": "1.
|
|
60
|
+
"wave-agent-sdk": "1.3.0"
|
|
60
61
|
},
|
|
61
62
|
"engines": {
|
|
62
63
|
"node": ">=22"
|
package/src/cli.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { render } from "ink";
|
|
3
|
+
import { ensureRuntimeDeps } from "wave-agent-sdk";
|
|
3
4
|
import { App } from "./components/App.js";
|
|
4
5
|
import { cleanupLogs } from "./utils/logger.js";
|
|
5
6
|
import { removeWorktree } from "./utils/worktree.js";
|
|
@@ -11,6 +12,14 @@ export interface CliOptions extends BaseAppProps {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export async function startCli(options: CliOptions): Promise<void> {
|
|
15
|
+
// Install the on-demand runtime dependencies (image codec + ripgrep) before
|
|
16
|
+
// the UI comes up. Waiting is the point: pasting an image or running a search
|
|
17
|
+
// a second later must not race a download. Costs ~16ms once they are
|
|
18
|
+
// installed (marker hit), ~6MB/20MB worth of time on the first run of a
|
|
19
|
+
// host-bundled CLI. A failed download only degrades the affected tool — never
|
|
20
|
+
// throws.
|
|
21
|
+
await ensureRuntimeDeps();
|
|
22
|
+
|
|
14
23
|
const {
|
|
15
24
|
restoreSessionId,
|
|
16
25
|
continueLastSession,
|
|
@@ -8,17 +8,16 @@ export async function installPluginCommand(argv: {
|
|
|
8
8
|
const pluginCore = new PluginCore(workdir);
|
|
9
9
|
|
|
10
10
|
try {
|
|
11
|
-
|
|
11
|
+
// When no scope is given, default to user scope (matches Claude Code).
|
|
12
|
+
const scope = argv.scope ?? "user";
|
|
13
|
+
const installed = await pluginCore.installPlugin(argv.plugin, scope);
|
|
12
14
|
console.log(
|
|
13
15
|
`Successfully installed plugin: ${installed.name} v${installed.version} from ${installed.marketplace}`,
|
|
14
16
|
);
|
|
15
17
|
console.log(`Cache path: ${installed.cachePath}`);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const pluginId = `${installed.name}@${installed.marketplace}`;
|
|
20
|
-
await pluginCore.enablePlugin(pluginId, scope);
|
|
21
|
-
console.log(`Plugin ${pluginId} enabled in ${scope} scope`);
|
|
18
|
+
console.log(
|
|
19
|
+
`Plugin ${installed.name}@${installed.marketplace} enabled in ${scope} scope`,
|
|
20
|
+
);
|
|
22
21
|
|
|
23
22
|
process.exit(0);
|
|
24
23
|
} catch (error) {
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import { PluginCore } from "wave-agent-sdk";
|
|
1
|
+
import { PluginCore, Scope } from "wave-agent-sdk";
|
|
2
2
|
|
|
3
|
-
export async function uninstallPluginCommand(argv: {
|
|
3
|
+
export async function uninstallPluginCommand(argv: {
|
|
4
|
+
plugin: string;
|
|
5
|
+
scope?: Scope;
|
|
6
|
+
}) {
|
|
4
7
|
const workdir = process.cwd();
|
|
5
8
|
const pluginCore = new PluginCore(workdir);
|
|
6
9
|
|
|
7
10
|
try {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
// Only the given scope is uninstalled (spec plugin A-015). Without --scope
|
|
12
|
+
// the core picks the scope the plugin is enabled in for this directory.
|
|
13
|
+
const scope = await pluginCore.uninstallPlugin(argv.plugin, argv.scope);
|
|
14
|
+
console.log(
|
|
15
|
+
`Successfully uninstalled plugin: ${argv.plugin} (scope: ${scope})`,
|
|
16
|
+
);
|
|
11
17
|
process.exit(0);
|
|
12
18
|
} catch (error) {
|
|
13
19
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -132,6 +132,7 @@ export const ChatInterface: React.FC = () => {
|
|
|
132
132
|
toolInput={confirmingTool!.input}
|
|
133
133
|
suggestedPrefix={confirmingTool!.suggestedPrefix}
|
|
134
134
|
hidePersistentOption={confirmingTool!.hidePersistentOption}
|
|
135
|
+
outsideSafeZoneDirectory={confirmingTool!.outsideSafeZoneDirectory}
|
|
135
136
|
permissionMode={confirmingTool!.permissionMode}
|
|
136
137
|
isExpanded={isExpanded}
|
|
137
138
|
onDecision={handleConfirmationDecision}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useState } from "react";
|
|
2
|
-
import { Box, Text, useInput } from "ink";
|
|
2
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
3
3
|
import {
|
|
4
4
|
BASH_TOOL_NAME,
|
|
5
5
|
EDIT_TOOL_NAME,
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
ARTIFACT_TOOL_NAME,
|
|
11
11
|
} from "wave-agent-sdk";
|
|
12
12
|
import { buildDiffLinesFromParams } from "./DiffDisplay.js";
|
|
13
|
+
import { renderMarkdownToAnsi } from "./Markdown.js";
|
|
14
|
+
import { ScrolledContent } from "./ScrolledContent.js";
|
|
13
15
|
import { highlightToAnsi } from "../utils/highlightUtils.js";
|
|
14
16
|
|
|
15
17
|
// Helper function to generate descriptive action text
|
|
@@ -35,6 +37,9 @@ const getActionDescription = (
|
|
|
35
37
|
case ASK_USER_QUESTION_TOOL_NAME:
|
|
36
38
|
return "Answer questions to clarify intent";
|
|
37
39
|
case ARTIFACT_TOOL_NAME:
|
|
40
|
+
if (toolInput.action === "read") {
|
|
41
|
+
return `Read artifact: ${toolInput.url || "unknown artifact"}`;
|
|
42
|
+
}
|
|
38
43
|
return `Publish file: ${toolInput.file_path || "unknown file"}`;
|
|
39
44
|
default:
|
|
40
45
|
return "Execute operation";
|
|
@@ -68,6 +73,11 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
|
|
|
68
73
|
(toolInput?.startLineNumber as number | undefined) ??
|
|
69
74
|
(toolName === WRITE_TOOL_NAME ? 1 : undefined);
|
|
70
75
|
|
|
76
|
+
// Terminal width is needed to render the plan's Markdown (table column widths
|
|
77
|
+
// are computed from it), matching <Markdown>'s own default.
|
|
78
|
+
const { stdout } = useStdout();
|
|
79
|
+
const columns = stdout?.columns ?? 80;
|
|
80
|
+
|
|
71
81
|
const headerRows = 2 + (warning ? 1 : 0);
|
|
72
82
|
|
|
73
83
|
// Number of content rows rendered at once. The header stays fixed above the
|
|
@@ -117,12 +127,17 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
|
|
|
117
127
|
toolName === EXIT_PLAN_MODE_TOOL_NAME &&
|
|
118
128
|
!!planContent;
|
|
119
129
|
|
|
130
|
+
// The plan is Markdown (the agent writes it to a .md file), so it is rendered
|
|
131
|
+
// with the same renderer as assistant messages and then linearized into rows
|
|
132
|
+
// for the row-slicing scroll above — same treatment as the JSON/diff rows.
|
|
120
133
|
const planLines = showPlan
|
|
121
|
-
? planContent
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
134
|
+
? renderMarkdownToAnsi(planContent!, columns)
|
|
135
|
+
.split("\n")
|
|
136
|
+
.map((line, index) => (
|
|
137
|
+
<Box key={`plan-${index}`}>
|
|
138
|
+
<Text>{line || " "}</Text>
|
|
139
|
+
</Box>
|
|
140
|
+
))
|
|
126
141
|
: [];
|
|
127
142
|
|
|
128
143
|
const contentLines: React.ReactNode[] = [
|
|
@@ -130,9 +145,11 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
|
|
|
130
145
|
...jsonLines,
|
|
131
146
|
...planLines,
|
|
132
147
|
];
|
|
133
|
-
const totalLines = contentLines.length;
|
|
134
|
-
const maxScroll = Math.max(0, totalLines - visibleCount);
|
|
135
148
|
|
|
149
|
+
// Rows really occupied by the content (wrapped rows included), measured after
|
|
150
|
+
// layout by <ScrolledContent> — see that component for why it is measured
|
|
151
|
+
// instead of counted from the row array.
|
|
152
|
+
const [contentRows, setContentRows] = useState(0);
|
|
136
153
|
const [scrollOffset, setScrollOffset] = useState(0);
|
|
137
154
|
|
|
138
155
|
// Reset scrolling when a new confirmation appears.
|
|
@@ -140,6 +157,8 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
|
|
|
140
157
|
setScrollOffset(0);
|
|
141
158
|
}, [toolName, toolInput, planContent]);
|
|
142
159
|
|
|
160
|
+
const maxScroll = Math.max(0, contentRows - visibleCount);
|
|
161
|
+
|
|
143
162
|
const halfPage = Math.max(1, Math.ceil(visibleCount / 2));
|
|
144
163
|
useInput((input, key) => {
|
|
145
164
|
if (key.pageUp) {
|
|
@@ -159,12 +178,8 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
|
|
|
159
178
|
});
|
|
160
179
|
|
|
161
180
|
const clampedOffset = Math.min(scrollOffset, maxScroll);
|
|
162
|
-
const visibleLines = contentLines.slice(
|
|
163
|
-
clampedOffset,
|
|
164
|
-
clampedOffset + visibleCount,
|
|
165
|
-
);
|
|
166
181
|
const hasMoreAbove = clampedOffset > 0;
|
|
167
|
-
const hasMoreBelow = clampedOffset + visibleCount <
|
|
182
|
+
const hasMoreBelow = clampedOffset + visibleCount < contentRows;
|
|
168
183
|
|
|
169
184
|
return (
|
|
170
185
|
<Box
|
|
@@ -195,11 +210,17 @@ export const ConfirmationDetails: React.FC<ConfirmationDetailsProps> = ({
|
|
|
195
210
|
</Text>
|
|
196
211
|
</Box>
|
|
197
212
|
)}
|
|
198
|
-
|
|
213
|
+
<ScrolledContent
|
|
214
|
+
visibleRows={visibleCount}
|
|
215
|
+
scrollOffset={clampedOffset}
|
|
216
|
+
onMeasuredRows={setContentRows}
|
|
217
|
+
>
|
|
218
|
+
{contentLines}
|
|
219
|
+
</ScrolledContent>
|
|
199
220
|
{hasMoreBelow && (
|
|
200
221
|
<Box>
|
|
201
222
|
<Text color="gray" dimColor>
|
|
202
|
-
↓ {
|
|
223
|
+
↓ {contentRows - clampedOffset - visibleCount} more
|
|
203
224
|
</Text>
|
|
204
225
|
</Box>
|
|
205
226
|
)}
|
|
@@ -7,8 +7,10 @@ import type {
|
|
|
7
7
|
} from "wave-agent-sdk";
|
|
8
8
|
import {
|
|
9
9
|
BASH_TOOL_NAME,
|
|
10
|
+
EDIT_TOOL_NAME,
|
|
10
11
|
EXIT_PLAN_MODE_TOOL_NAME,
|
|
11
12
|
ENTER_PLAN_MODE_TOOL_NAME,
|
|
13
|
+
WRITE_TOOL_NAME,
|
|
12
14
|
ASK_USER_QUESTION_TOOL_NAME,
|
|
13
15
|
} from "wave-agent-sdk";
|
|
14
16
|
import { confirmationReducer } from "../reducers/confirmationReducer.js";
|
|
@@ -24,11 +26,19 @@ const getHeaderColor = (header: string) => {
|
|
|
24
26
|
return colors[Math.abs(hash) % colors.length];
|
|
25
27
|
};
|
|
26
28
|
|
|
29
|
+
/** Last path segment of a directory path (handles both `/` and `\` separators). */
|
|
30
|
+
const lastPathSegment = (dirPath: string): string => {
|
|
31
|
+
const segments = dirPath.split(/[\\/]/).filter(Boolean);
|
|
32
|
+
return segments[segments.length - 1] ?? dirPath;
|
|
33
|
+
};
|
|
34
|
+
|
|
27
35
|
export interface ConfirmationSelectorProps {
|
|
28
36
|
toolName: string;
|
|
29
37
|
toolInput?: Record<string, unknown>;
|
|
30
38
|
suggestedPrefix?: string;
|
|
31
39
|
hidePersistentOption?: boolean;
|
|
40
|
+
/** Directory holding an out-of-Safe-Zone Write/Edit target, when the host reports one. */
|
|
41
|
+
outsideSafeZoneDirectory?: string;
|
|
32
42
|
permissionMode?: PermissionMode;
|
|
33
43
|
isExpanded?: boolean;
|
|
34
44
|
onDecision: (decision: PermissionDecision) => void;
|
|
@@ -40,6 +50,7 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
40
50
|
toolInput,
|
|
41
51
|
suggestedPrefix,
|
|
42
52
|
hidePersistentOption,
|
|
53
|
+
outsideSafeZoneDirectory,
|
|
43
54
|
permissionMode,
|
|
44
55
|
isExpanded = false,
|
|
45
56
|
onDecision,
|
|
@@ -104,6 +115,15 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
104
115
|
return "Yes, and auto-accept edits";
|
|
105
116
|
};
|
|
106
117
|
|
|
118
|
+
// Only Write/Edit can be out of the Safe Zone, and the host only reports the
|
|
119
|
+
// directory for such requests.
|
|
120
|
+
const showAddDirectoryOption =
|
|
121
|
+
(toolName === EDIT_TOOL_NAME || toolName === WRITE_TOOL_NAME) &&
|
|
122
|
+
!!outsideSafeZoneDirectory;
|
|
123
|
+
const addDirectoryOptionText = `Yes, and allow all edits in ${lastPathSegment(
|
|
124
|
+
outsideSafeZoneDirectory ?? "",
|
|
125
|
+
)}/ this session`;
|
|
126
|
+
|
|
107
127
|
const pasteDetectorRef = useRef(createBracketedPasteDetector());
|
|
108
128
|
|
|
109
129
|
useInput((input, key) => {
|
|
@@ -140,6 +160,7 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
140
160
|
toolInput,
|
|
141
161
|
suggestedPrefix,
|
|
142
162
|
hidePersistentOption,
|
|
163
|
+
outsideSafeZoneDirectory,
|
|
143
164
|
permissionMode,
|
|
144
165
|
});
|
|
145
166
|
}
|
|
@@ -248,6 +269,24 @@ export const ConfirmationSelector: React.FC<ConfirmationSelectorProps> = ({
|
|
|
248
269
|
: "Yes, proceed"}
|
|
249
270
|
</Text>
|
|
250
271
|
</Box>
|
|
272
|
+
{showAddDirectoryOption && (
|
|
273
|
+
<Box key="add-directory-option">
|
|
274
|
+
<Text
|
|
275
|
+
color={
|
|
276
|
+
state.selectedOption === "addDirectory" ? "black" : "white"
|
|
277
|
+
}
|
|
278
|
+
backgroundColor={
|
|
279
|
+
state.selectedOption === "addDirectory"
|
|
280
|
+
? "yellow"
|
|
281
|
+
: undefined
|
|
282
|
+
}
|
|
283
|
+
bold={state.selectedOption === "addDirectory"}
|
|
284
|
+
>
|
|
285
|
+
{state.selectedOption === "addDirectory" ? "> " : " "}
|
|
286
|
+
{addDirectoryOptionText}
|
|
287
|
+
</Text>
|
|
288
|
+
</Box>
|
|
289
|
+
)}
|
|
251
290
|
{!hidePersistentOption && (
|
|
252
291
|
<Box key="auto-option">
|
|
253
292
|
<Text
|
|
@@ -10,6 +10,7 @@ import { AgentsManager } from "./AgentsManager.js";
|
|
|
10
10
|
import { SkillsManager } from "./SkillsManager.js";
|
|
11
11
|
import { HooksManager } from "./HooksManager.js";
|
|
12
12
|
import { RewindCommand } from "./RewindCommand.js";
|
|
13
|
+
import { ResumeCommand } from "./ResumeCommand.js";
|
|
13
14
|
import { HelpView } from "./HelpView.js";
|
|
14
15
|
import { StatusCommand } from "./StatusCommand.js";
|
|
15
16
|
import { LoginCommand } from "./LoginCommand.js";
|
|
@@ -86,6 +87,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
86
87
|
permissionMode: chatPermissionMode,
|
|
87
88
|
setPermissionMode: setChatPermissionMode,
|
|
88
89
|
handleRewindSelect,
|
|
90
|
+
resumeSession,
|
|
89
91
|
backgroundCurrentTask,
|
|
90
92
|
messages,
|
|
91
93
|
getFullMessageThread,
|
|
@@ -95,10 +97,13 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
95
97
|
clearMessages,
|
|
96
98
|
compact,
|
|
97
99
|
addDir,
|
|
100
|
+
renameSession,
|
|
98
101
|
currentModel,
|
|
99
102
|
configuredModels,
|
|
100
103
|
setModel,
|
|
101
|
-
|
|
104
|
+
reloadPlugins,
|
|
105
|
+
notifyPluginChange,
|
|
106
|
+
pluginHint,
|
|
102
107
|
recallQueuedMessage,
|
|
103
108
|
queuedMessages,
|
|
104
109
|
setIsBtwActive,
|
|
@@ -153,6 +158,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
153
158
|
showMcpManager,
|
|
154
159
|
showAgentsManager,
|
|
155
160
|
showRewindManager,
|
|
161
|
+
showResumeSelector,
|
|
156
162
|
showHelp,
|
|
157
163
|
showStatusCommand,
|
|
158
164
|
showLoginCommand,
|
|
@@ -165,6 +171,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
165
171
|
setShowMcpManager,
|
|
166
172
|
setShowAgentsManager,
|
|
167
173
|
setShowRewindManager,
|
|
174
|
+
setShowResumeSelector,
|
|
168
175
|
setShowHelp,
|
|
169
176
|
setShowStatusCommand,
|
|
170
177
|
setShowLoginCommand,
|
|
@@ -191,7 +198,9 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
191
198
|
onAskBtw: askBtw,
|
|
192
199
|
onClearMessages: clearMessages,
|
|
193
200
|
onCompact: compact,
|
|
201
|
+
onReloadPlugins: reloadPlugins,
|
|
194
202
|
onAddDir: addDir,
|
|
203
|
+
onRenameSession: renameSession,
|
|
195
204
|
onHasSlashCommand: hasSlashCommand,
|
|
196
205
|
onAbortMessage: abortMessage,
|
|
197
206
|
onBackgroundCurrentTask: backgroundCurrentTask,
|
|
@@ -230,6 +239,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
230
239
|
if (
|
|
231
240
|
planView ||
|
|
232
241
|
showRewindManager ||
|
|
242
|
+
showResumeSelector ||
|
|
233
243
|
showHelp ||
|
|
234
244
|
showStatusCommand ||
|
|
235
245
|
showLoginCommand ||
|
|
@@ -283,6 +293,21 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
283
293
|
await handleRewindSelect(index);
|
|
284
294
|
};
|
|
285
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
|
+
|
|
286
311
|
return (
|
|
287
312
|
<Box flexDirection="column">
|
|
288
313
|
<BtwDisplay btwState={btwState} />
|
|
@@ -363,6 +388,15 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
363
388
|
/>
|
|
364
389
|
)}
|
|
365
390
|
|
|
391
|
+
{showResumeSelector && (
|
|
392
|
+
<ResumeCommand
|
|
393
|
+
workdir={workingDirectory}
|
|
394
|
+
isBusy={!isIdle}
|
|
395
|
+
onSelect={handleResumeSelectWithClose}
|
|
396
|
+
onCancel={handleResumeCancel}
|
|
397
|
+
/>
|
|
398
|
+
)}
|
|
399
|
+
|
|
366
400
|
{showHelp && (
|
|
367
401
|
<HelpView
|
|
368
402
|
onCancel={() => setShowHelp(false)}
|
|
@@ -381,7 +415,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
381
415
|
{showPluginManager && (
|
|
382
416
|
<PluginManagerShell
|
|
383
417
|
onCancel={() => setShowPluginManager(false)}
|
|
384
|
-
|
|
418
|
+
onPluginChanged={notifyPluginChange}
|
|
385
419
|
/>
|
|
386
420
|
)}
|
|
387
421
|
|
|
@@ -411,7 +445,9 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
411
445
|
showMcpManager ||
|
|
412
446
|
showAgentsManager ||
|
|
413
447
|
showSkillsManager ||
|
|
448
|
+
showHooksManager ||
|
|
414
449
|
showRewindManager ||
|
|
450
|
+
showResumeSelector ||
|
|
415
451
|
showHelp ||
|
|
416
452
|
showStatusCommand ||
|
|
417
453
|
showLoginCommand ||
|
|
@@ -451,6 +487,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
451
487
|
latestTotalTokens={latestTotalTokens}
|
|
452
488
|
maxInputTokens={maxInputTokens}
|
|
453
489
|
showLoginHint={showLoginHint}
|
|
490
|
+
pluginHint={pluginHint}
|
|
454
491
|
/>
|
|
455
492
|
</Box>
|
|
456
493
|
</Box>
|
|
@@ -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}
|
|
@@ -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>
|