wave-code 1.1.0 → 1.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wave-code",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
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.0"
59
+ "wave-agent-sdk": "1.1.2"
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",