wave-code 1.1.0 → 1.1.1
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 +489 -465
- package/package.json +2 -2
- package/src/cli.tsx +6 -1
- package/src/components/InputBox.tsx +14 -1
- package/src/components/SkillsManager.tsx +312 -0
- package/src/constants/commands.ts +6 -0
- package/src/contexts/useChat.tsx +259 -136
- package/src/hooks/useInputManager.ts +6 -0
- package/src/managers/inputHandlers.ts +2 -0
- package/src/managers/inputReducer.ts +9 -0
- package/src/print-cli.ts +4 -0
- package/src/reducers/skillsManagerReducer.ts +91 -0
- package/src/stdio/agentBridge.ts +8 -0
- package/src/stdio/protocol.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wave-code",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "CLI-based code assistant powered by AI, built with React and Ink",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"wrap-ansi": "^10.0.0",
|
|
57
57
|
"yargs": "^17.7.2",
|
|
58
58
|
"zod": "^3.23.8",
|
|
59
|
-
"wave-agent-sdk": "1.1.
|
|
59
|
+
"wave-agent-sdk": "1.1.1"
|
|
60
60
|
},
|
|
61
61
|
"engines": {
|
|
62
62
|
"node": ">=22"
|
package/src/cli.tsx
CHANGED
|
@@ -88,10 +88,15 @@ export async function startCli(options: CliOptions): Promise<void> {
|
|
|
88
88
|
console.warn("Failed to cleanup old logs:", error);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
|
-
// Cleanup worktree if requested
|
|
91
|
+
// Cleanup worktree if requested. The Ink UI is already unmounted, and on
|
|
92
|
+
// Windows recursive deletion can take a long time (deep paths, Defender
|
|
93
|
+
// scanning), so show explicit progress feedback instead of leaving the
|
|
94
|
+
// terminal looking frozen.
|
|
92
95
|
if (shouldRemoveWorktree && worktreeSession) {
|
|
96
|
+
process.stdout.write("\nDeleting worktree ...\n");
|
|
93
97
|
process.chdir(worktreeSession.repoRoot);
|
|
94
98
|
await removeWorktree(worktreeSession);
|
|
99
|
+
process.stdout.write("Done.\n");
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
process.exit(0);
|
|
@@ -7,6 +7,7 @@ import { HistorySearch } from "./HistorySearch.js";
|
|
|
7
7
|
import { BackgroundTaskManager } from "./BackgroundTaskManager.js";
|
|
8
8
|
import { McpManager } from "./McpManager.js";
|
|
9
9
|
import { AgentsManager } from "./AgentsManager.js";
|
|
10
|
+
import { SkillsManager } from "./SkillsManager.js";
|
|
10
11
|
import { RewindCommand } from "./RewindCommand.js";
|
|
11
12
|
import { HelpView } from "./HelpView.js";
|
|
12
13
|
import { StatusCommand } from "./StatusCommand.js";
|
|
@@ -91,6 +92,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
91
92
|
queuedMessages,
|
|
92
93
|
setIsBtwActive,
|
|
93
94
|
agentDefinitions,
|
|
95
|
+
skills,
|
|
94
96
|
} = useChat();
|
|
95
97
|
|
|
96
98
|
// Ref to hold setInputText so queue callbacks can access it before useInputManager returns
|
|
@@ -143,6 +145,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
143
145
|
showPluginManager,
|
|
144
146
|
showModelSelector,
|
|
145
147
|
showWorkflowManager,
|
|
148
|
+
showSkillsManager,
|
|
146
149
|
setShowBackgroundTaskManager,
|
|
147
150
|
setShowMcpManager,
|
|
148
151
|
setShowAgentsManager,
|
|
@@ -153,6 +156,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
153
156
|
setShowPluginManager,
|
|
154
157
|
setShowModelSelector,
|
|
155
158
|
setShowWorkflowManager,
|
|
159
|
+
setShowSkillsManager,
|
|
156
160
|
// Permission mode
|
|
157
161
|
permissionMode,
|
|
158
162
|
setPermissionMode,
|
|
@@ -216,7 +220,8 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
216
220
|
showBackgroundTaskManager ||
|
|
217
221
|
showMcpManager ||
|
|
218
222
|
showAgentsManager ||
|
|
219
|
-
showWorkflowManager
|
|
223
|
+
showWorkflowManager ||
|
|
224
|
+
showSkillsManager
|
|
220
225
|
) {
|
|
221
226
|
return;
|
|
222
227
|
}
|
|
@@ -312,6 +317,13 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
312
317
|
/>
|
|
313
318
|
)}
|
|
314
319
|
|
|
320
|
+
{showSkillsManager && (
|
|
321
|
+
<SkillsManager
|
|
322
|
+
onCancel={() => setShowSkillsManager(false)}
|
|
323
|
+
skills={skills}
|
|
324
|
+
/>
|
|
325
|
+
)}
|
|
326
|
+
|
|
315
327
|
{showWorkflowManager && (
|
|
316
328
|
<WorkflowManager onCancel={() => setShowWorkflowManager(false)} />
|
|
317
329
|
)}
|
|
@@ -361,6 +373,7 @@ export const InputBox: React.FC<InputBoxProps> = ({
|
|
|
361
373
|
: showBackgroundTaskManager ||
|
|
362
374
|
showMcpManager ||
|
|
363
375
|
showAgentsManager ||
|
|
376
|
+
showSkillsManager ||
|
|
364
377
|
showRewindManager ||
|
|
365
378
|
showHelp ||
|
|
366
379
|
showStatusCommand ||
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useReducer } from "react";
|
|
2
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
3
|
+
import type { SkillMetadata } from "wave-agent-sdk";
|
|
4
|
+
import {
|
|
5
|
+
skillsManagerReducer,
|
|
6
|
+
type SkillsManagerState,
|
|
7
|
+
} from "../reducers/skillsManagerReducer.js";
|
|
8
|
+
|
|
9
|
+
export interface SkillsManagerProps {
|
|
10
|
+
onCancel: () => void;
|
|
11
|
+
skills: SkillMetadata[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface DisplayEntry {
|
|
15
|
+
kind: "header" | "skill" | "empty";
|
|
16
|
+
label: string;
|
|
17
|
+
sub?: string;
|
|
18
|
+
scope?: SkillScope;
|
|
19
|
+
selectableIndex: number; // -1 for non-selectable rows
|
|
20
|
+
skill?: SkillMetadata;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
type SkillScope = "builtin" | "user" | "project" | "plugin";
|
|
24
|
+
|
|
25
|
+
const SCOPE_LABELS: Record<SkillScope, string> = {
|
|
26
|
+
builtin: "Built-in skills",
|
|
27
|
+
user: "User skills",
|
|
28
|
+
project: "Project skills",
|
|
29
|
+
plugin: "Plugin skills",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const SCOPE_ORDER: SkillScope[] = ["builtin", "user", "project", "plugin"];
|
|
33
|
+
|
|
34
|
+
const initialState: SkillsManagerState = {
|
|
35
|
+
selectedIndex: 0,
|
|
36
|
+
viewMode: "list",
|
|
37
|
+
pendingEffect: null,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Group scope for a skill: plugin skills (pluginName set) get their own
|
|
41
|
+
* group, everything else groups by its discovery type ("personal" skills
|
|
42
|
+
* are shown under the user scope). */
|
|
43
|
+
function getSkillScope(skill: SkillMetadata): SkillScope {
|
|
44
|
+
if (skill.pluginName) {
|
|
45
|
+
return "plugin";
|
|
46
|
+
}
|
|
47
|
+
if (skill.type === "personal") {
|
|
48
|
+
return "user";
|
|
49
|
+
}
|
|
50
|
+
return skill.type;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const SkillsManager: React.FC<SkillsManagerProps> = ({
|
|
54
|
+
onCancel,
|
|
55
|
+
skills,
|
|
56
|
+
}) => {
|
|
57
|
+
const [state, dispatch] = useReducer(skillsManagerReducer, initialState);
|
|
58
|
+
const { stdout } = useStdout();
|
|
59
|
+
|
|
60
|
+
// Handle pending effects
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!state.pendingEffect) return;
|
|
63
|
+
const effect = state.pendingEffect;
|
|
64
|
+
dispatch({ type: "CLEAR_PENDING_EFFECT" });
|
|
65
|
+
if (effect.type === "CANCEL") {
|
|
66
|
+
onCancel();
|
|
67
|
+
}
|
|
68
|
+
}, [state.pendingEffect, onCancel]);
|
|
69
|
+
|
|
70
|
+
// Flatten skills (grouped by scope) into one navigable list. Headers and
|
|
71
|
+
// the empty-state line are non-selectable.
|
|
72
|
+
const entries = useMemo<DisplayEntry[]>(() => {
|
|
73
|
+
const result: DisplayEntry[] = [];
|
|
74
|
+
let selectableCount = 0;
|
|
75
|
+
|
|
76
|
+
result.push({ kind: "header", label: "SKILLS", selectableIndex: -1 });
|
|
77
|
+
let skillCount = 0;
|
|
78
|
+
for (const scope of SCOPE_ORDER) {
|
|
79
|
+
const scopedSkills = skills
|
|
80
|
+
.filter((s) => getSkillScope(s) === scope)
|
|
81
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
82
|
+
if (scopedSkills.length === 0) continue;
|
|
83
|
+
result.push({
|
|
84
|
+
kind: "header",
|
|
85
|
+
label: SCOPE_LABELS[scope],
|
|
86
|
+
scope,
|
|
87
|
+
selectableIndex: -1,
|
|
88
|
+
});
|
|
89
|
+
for (const skill of scopedSkills) {
|
|
90
|
+
result.push({
|
|
91
|
+
kind: "skill",
|
|
92
|
+
label: skill.name,
|
|
93
|
+
sub: skill.description,
|
|
94
|
+
scope,
|
|
95
|
+
selectableIndex: selectableCount++,
|
|
96
|
+
skill,
|
|
97
|
+
});
|
|
98
|
+
skillCount++;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (skillCount === 0) {
|
|
102
|
+
result.push({
|
|
103
|
+
kind: "empty",
|
|
104
|
+
label: "No skills available",
|
|
105
|
+
selectableIndex: -1,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return result;
|
|
110
|
+
}, [skills]);
|
|
111
|
+
|
|
112
|
+
const itemCount = entries.filter((e) => e.selectableIndex >= 0).length;
|
|
113
|
+
|
|
114
|
+
// Window slice: center the selected item within the visible area, clamping
|
|
115
|
+
// to the terminal's available rows (reusable pattern from
|
|
116
|
+
// AgentsManager/BackgroundTaskManager).
|
|
117
|
+
const availableRows = stdout?.rows ?? 24;
|
|
118
|
+
const maxVisible = Math.max(3, Math.min(15, availableRows - 12));
|
|
119
|
+
const selectedFlatIndex = entries.findIndex(
|
|
120
|
+
(e) => e.selectableIndex === state.selectedIndex,
|
|
121
|
+
);
|
|
122
|
+
const startIndex = Math.max(
|
|
123
|
+
0,
|
|
124
|
+
Math.min(
|
|
125
|
+
selectedFlatIndex - Math.floor(maxVisible / 2),
|
|
126
|
+
Math.max(0, entries.length - maxVisible),
|
|
127
|
+
),
|
|
128
|
+
);
|
|
129
|
+
const visibleEntries = entries.slice(startIndex, startIndex + maxVisible);
|
|
130
|
+
|
|
131
|
+
useInput((input, key) => {
|
|
132
|
+
dispatch({ type: "HANDLE_KEY", input, key, itemCount });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const selectedEntry = entries.find(
|
|
136
|
+
(e) => e.selectableIndex === state.selectedIndex,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Detail view — body renders fully expanded with no height limit, no
|
|
140
|
+
// clipping and no scrolling (aligned with AgentsManager's detail view).
|
|
141
|
+
if (state.viewMode === "detail" && selectedEntry) {
|
|
142
|
+
const skill = selectedEntry.skill;
|
|
143
|
+
return (
|
|
144
|
+
<Box
|
|
145
|
+
flexDirection="column"
|
|
146
|
+
borderStyle="single"
|
|
147
|
+
borderColor="cyan"
|
|
148
|
+
borderBottom={false}
|
|
149
|
+
borderLeft={false}
|
|
150
|
+
borderRight={false}
|
|
151
|
+
paddingTop={1}
|
|
152
|
+
gap={1}
|
|
153
|
+
>
|
|
154
|
+
<Box>
|
|
155
|
+
<Text color="cyan" bold>
|
|
156
|
+
Skill: {selectedEntry.label}
|
|
157
|
+
</Text>
|
|
158
|
+
</Box>
|
|
159
|
+
|
|
160
|
+
<Box flexDirection="column" gap={1}>
|
|
161
|
+
{skill?.description && (
|
|
162
|
+
<Box>
|
|
163
|
+
<Text>
|
|
164
|
+
<Text color="blue">Description:</Text> {skill.description}
|
|
165
|
+
</Text>
|
|
166
|
+
</Box>
|
|
167
|
+
)}
|
|
168
|
+
<Box>
|
|
169
|
+
<Text>
|
|
170
|
+
<Text color="blue">Scope:</Text>{" "}
|
|
171
|
+
{skill ? SCOPE_LABELS[getSkillScope(skill)] : ""}
|
|
172
|
+
{skill?.pluginName ? ` (${skill.pluginName})` : ""}
|
|
173
|
+
</Text>
|
|
174
|
+
</Box>
|
|
175
|
+
{skill?.skillPath && (
|
|
176
|
+
<Box>
|
|
177
|
+
<Text wrap="wrap">
|
|
178
|
+
<Text color="blue">Path:</Text> {skill.skillPath}
|
|
179
|
+
</Text>
|
|
180
|
+
</Box>
|
|
181
|
+
)}
|
|
182
|
+
{skill?.model && (
|
|
183
|
+
<Box>
|
|
184
|
+
<Text>
|
|
185
|
+
<Text color="blue">Model:</Text> {skill.model}
|
|
186
|
+
</Text>
|
|
187
|
+
</Box>
|
|
188
|
+
)}
|
|
189
|
+
{skill?.agent && (
|
|
190
|
+
<Box>
|
|
191
|
+
<Text>
|
|
192
|
+
<Text color="blue">Agent:</Text> {skill.agent}
|
|
193
|
+
</Text>
|
|
194
|
+
</Box>
|
|
195
|
+
)}
|
|
196
|
+
{skill?.allowedTools && skill.allowedTools.length > 0 && (
|
|
197
|
+
<Box>
|
|
198
|
+
<Text wrap="wrap">
|
|
199
|
+
<Text color="blue">Allowed tools:</Text>{" "}
|
|
200
|
+
{skill.allowedTools.join(", ")}
|
|
201
|
+
</Text>
|
|
202
|
+
</Box>
|
|
203
|
+
)}
|
|
204
|
+
{skill && (
|
|
205
|
+
<Box>
|
|
206
|
+
<Text wrap="wrap">
|
|
207
|
+
<Text color="blue">Invocation:</Text>{" "}
|
|
208
|
+
{[
|
|
209
|
+
skill.userInvocable === false ? "not user-invocable" : null,
|
|
210
|
+
skill.disableModelInvocation
|
|
211
|
+
? "model invocation disabled"
|
|
212
|
+
: null,
|
|
213
|
+
]
|
|
214
|
+
.filter(Boolean)
|
|
215
|
+
.join(", ") || "user-invocable, model-invocable"}
|
|
216
|
+
</Text>
|
|
217
|
+
</Box>
|
|
218
|
+
)}
|
|
219
|
+
</Box>
|
|
220
|
+
|
|
221
|
+
<Box marginTop={1}>
|
|
222
|
+
<Text dimColor>Esc or Enter to go back</Text>
|
|
223
|
+
</Box>
|
|
224
|
+
</Box>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (itemCount === 0) {
|
|
229
|
+
return (
|
|
230
|
+
<Box
|
|
231
|
+
flexDirection="column"
|
|
232
|
+
borderStyle="single"
|
|
233
|
+
borderColor="cyan"
|
|
234
|
+
borderBottom={false}
|
|
235
|
+
borderLeft={false}
|
|
236
|
+
borderRight={false}
|
|
237
|
+
paddingTop={1}
|
|
238
|
+
>
|
|
239
|
+
<Text color="cyan" bold>
|
|
240
|
+
Skills
|
|
241
|
+
</Text>
|
|
242
|
+
<Text>No skills available</Text>
|
|
243
|
+
<Text dimColor>Create skills in .wave/skills/ or ~/.wave/skills/</Text>
|
|
244
|
+
<Text dimColor>Press Escape to close</Text>
|
|
245
|
+
</Box>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<Box
|
|
251
|
+
flexDirection="column"
|
|
252
|
+
borderStyle="single"
|
|
253
|
+
borderColor="cyan"
|
|
254
|
+
borderBottom={false}
|
|
255
|
+
borderLeft={false}
|
|
256
|
+
borderRight={false}
|
|
257
|
+
paddingTop={1}
|
|
258
|
+
gap={1}
|
|
259
|
+
>
|
|
260
|
+
<Box>
|
|
261
|
+
<Text color="cyan" bold>
|
|
262
|
+
Skills
|
|
263
|
+
</Text>
|
|
264
|
+
</Box>
|
|
265
|
+
<Text dimColor>Select a skill to view details</Text>
|
|
266
|
+
|
|
267
|
+
<Box flexDirection="column">
|
|
268
|
+
{visibleEntries.map((entry, index) => {
|
|
269
|
+
const isSelected = entry.selectableIndex === state.selectedIndex;
|
|
270
|
+
if (entry.kind === "header") {
|
|
271
|
+
return (
|
|
272
|
+
<Text key={`${entry.kind}-${entry.label}-${index}`} dimColor bold>
|
|
273
|
+
{entry.label}
|
|
274
|
+
</Text>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
if (entry.kind === "empty") {
|
|
278
|
+
return (
|
|
279
|
+
<Text key={`empty-${index}`} dimColor>
|
|
280
|
+
{entry.label}
|
|
281
|
+
</Text>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return (
|
|
285
|
+
<Text
|
|
286
|
+
key={`${entry.kind}-${entry.selectableIndex}`}
|
|
287
|
+
color={isSelected ? "black" : "white"}
|
|
288
|
+
backgroundColor={isSelected ? "cyan" : undefined}
|
|
289
|
+
wrap="truncate-end"
|
|
290
|
+
>
|
|
291
|
+
{isSelected ? "▶ " : " "}
|
|
292
|
+
{entry.selectableIndex + 1}. {entry.label}
|
|
293
|
+
{entry.scope === "plugin" && entry.skill?.pluginName ? (
|
|
294
|
+
<Text color={isSelected ? "black" : "gray"}>
|
|
295
|
+
{" "}
|
|
296
|
+
· {entry.skill.pluginName}
|
|
297
|
+
</Text>
|
|
298
|
+
) : null}
|
|
299
|
+
{entry.sub ? ` · ${entry.sub}` : ""}
|
|
300
|
+
</Text>
|
|
301
|
+
);
|
|
302
|
+
})}
|
|
303
|
+
</Box>
|
|
304
|
+
|
|
305
|
+
<Box marginTop={1}>
|
|
306
|
+
<Text dimColor>
|
|
307
|
+
↑/↓ to select · Enter to view details · Esc to close
|
|
308
|
+
</Text>
|
|
309
|
+
</Box>
|
|
310
|
+
</Box>
|
|
311
|
+
);
|
|
312
|
+
};
|
|
@@ -19,6 +19,12 @@ export const AVAILABLE_COMMANDS: SlashCommand[] = [
|
|
|
19
19
|
description: "List available agents and active subagents",
|
|
20
20
|
handler: () => {}, // Handler here won't be used, actual processing is in the hook
|
|
21
21
|
},
|
|
22
|
+
{
|
|
23
|
+
id: "skills",
|
|
24
|
+
name: "skills",
|
|
25
|
+
description: "List available skills",
|
|
26
|
+
handler: () => {}, // Handler here won't be used, actual processing is in the hook
|
|
27
|
+
},
|
|
22
28
|
{
|
|
23
29
|
id: "rewind",
|
|
24
30
|
name: "rewind",
|