@kmiyh/pi-skills-menu 1.0.3 → 1.0.5
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/README.md +5 -1
- package/package.json +1 -1
- package/src/ui/skills-selector.ts +72 -13
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ After that, the skill is inserted into the editor so it will be used by Pi when
|
|
|
67
67
|
|
|
68
68
|
The list also contains a dedicated entry for creating a new skill.
|
|
69
69
|
|
|
70
|
-
Skill creation is done
|
|
70
|
+
Skill creation is done in three steps:
|
|
71
71
|
|
|
72
72
|
1. **skill name**
|
|
73
73
|
|
|
@@ -77,6 +77,10 @@ Skill creation is done by entering:
|
|
|
77
77
|
|
|
78
78
|

|
|
79
79
|
|
|
80
|
+
3. **skill visibility**
|
|
81
|
+
- **Global** — save the skill in your user-level Pi skills directory
|
|
82
|
+
- **Project** — save the skill in the current project's `.pi/skills` directory
|
|
83
|
+
|
|
80
84
|
After that, the extension generates a `SKILL.md`.
|
|
81
85
|
|
|
82
86
|
Generation uses:
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
truncateToWidth,
|
|
14
14
|
type TUI,
|
|
15
15
|
} from "@mariozechner/pi-tui";
|
|
16
|
-
import { normalizeSkillName, type SkillCreationAnswers } from "../create-skill.js";
|
|
16
|
+
import { normalizeSkillName, type SkillCreationAnswers, type SkillLocation } from "../create-skill.js";
|
|
17
17
|
import { isDeletableSkill } from "../delete-skill.js";
|
|
18
18
|
import type { SkillEntry, SkillRegistry } from "../types.js";
|
|
19
19
|
|
|
@@ -25,11 +25,22 @@ export type SkillsMenuSelection =
|
|
|
25
25
|
| null;
|
|
26
26
|
|
|
27
27
|
type CreateTextStepId = "name" | "description";
|
|
28
|
-
type
|
|
28
|
+
type CreateChoiceStepId = "location";
|
|
29
|
+
type CreateStepId = CreateTextStepId | CreateChoiceStepId;
|
|
30
|
+
type CreateTextStep = { id: CreateTextStepId; title: string; hint: string; optional: boolean; kind: "text" };
|
|
31
|
+
type CreateChoiceOption = { value: SkillLocation; label: string; description: string };
|
|
32
|
+
type CreateChoiceStep = { id: CreateChoiceStepId; title: string; hint: string; optional: boolean; kind: "choice"; options: CreateChoiceOption[] };
|
|
33
|
+
type CreateStep = CreateTextStep | CreateChoiceStep;
|
|
34
|
+
|
|
35
|
+
const LOCATION_OPTIONS: CreateChoiceOption[] = [
|
|
36
|
+
{ value: "global", label: "Global", description: "Save in your user-level Pi skills directory." },
|
|
37
|
+
{ value: "project", label: "Project", description: "Save in this project's .pi/skills directory." },
|
|
38
|
+
];
|
|
29
39
|
|
|
30
40
|
const CREATE_STEPS: CreateStep[] = [
|
|
31
41
|
{ id: "name", title: "Name", hint: "Use lowercase letters, numbers, and hyphens, for example react-review.", optional: false, kind: "text" },
|
|
32
42
|
{ id: "description", title: "Description", hint: "Describe what the skill does and when it should be used in one clear sentence.", optional: false, kind: "text" },
|
|
43
|
+
{ id: "location", title: "Visibility", hint: "Choose whether the skill is available only in this project or in all your Pi sessions.", optional: false, kind: "choice", options: LOCATION_OPTIONS },
|
|
33
44
|
];
|
|
34
45
|
|
|
35
46
|
function getScopeLabel(skill: SkillEntry): string {
|
|
@@ -96,6 +107,7 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
96
107
|
name: "",
|
|
97
108
|
description: "",
|
|
98
109
|
};
|
|
110
|
+
private createLocation: SkillLocation = "global";
|
|
99
111
|
private submittedDescriptionValue: string | undefined;
|
|
100
112
|
private createError: string | undefined;
|
|
101
113
|
private browseQuery: string;
|
|
@@ -206,6 +218,13 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
206
218
|
return CREATE_STEPS[this.createStepIndex]!;
|
|
207
219
|
}
|
|
208
220
|
|
|
221
|
+
private moveLocationSelection(delta: number): void {
|
|
222
|
+
const currentIndex = LOCATION_OPTIONS.findIndex((option) => option.value === this.createLocation);
|
|
223
|
+
const safeIndex = currentIndex === -1 ? 0 : currentIndex;
|
|
224
|
+
const nextIndex = (safeIndex + delta + LOCATION_OPTIONS.length) % LOCATION_OPTIONS.length;
|
|
225
|
+
this.createLocation = LOCATION_OPTIONS[nextIndex]!.value;
|
|
226
|
+
}
|
|
227
|
+
|
|
209
228
|
private setBrowseInputValue(value: string): void {
|
|
210
229
|
this.browseQuery = value;
|
|
211
230
|
this.input.setValue(value);
|
|
@@ -234,10 +253,15 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
234
253
|
this.descriptionEditor.focused = false;
|
|
235
254
|
return;
|
|
236
255
|
}
|
|
237
|
-
|
|
238
|
-
|
|
256
|
+
if (step.id === "description") {
|
|
257
|
+
this.submittedDescriptionValue = undefined;
|
|
258
|
+
this.descriptionEditor.setText(this.createValues.description);
|
|
259
|
+
this.input.focused = false;
|
|
260
|
+
this.descriptionEditor.focused = this._focused;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
239
263
|
this.input.focused = false;
|
|
240
|
-
this.descriptionEditor.focused =
|
|
264
|
+
this.descriptionEditor.focused = false;
|
|
241
265
|
}
|
|
242
266
|
|
|
243
267
|
private persistCreateInput(): void {
|
|
@@ -246,12 +270,14 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
246
270
|
this.createValues.name = this.input.getValue();
|
|
247
271
|
return;
|
|
248
272
|
}
|
|
249
|
-
if (
|
|
250
|
-
this.
|
|
251
|
-
|
|
252
|
-
|
|
273
|
+
if (step.id === "description") {
|
|
274
|
+
if (this.submittedDescriptionValue !== undefined) {
|
|
275
|
+
this.createValues.description = this.submittedDescriptionValue;
|
|
276
|
+
this.submittedDescriptionValue = undefined;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
this.createValues.description = this.descriptionEditor.getText();
|
|
253
280
|
}
|
|
254
|
-
this.createValues.description = this.descriptionEditor.getText();
|
|
255
281
|
}
|
|
256
282
|
|
|
257
283
|
private validateCreateStep(): boolean {
|
|
@@ -270,6 +296,11 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
270
296
|
return false;
|
|
271
297
|
}
|
|
272
298
|
}
|
|
299
|
+
if (step.kind === "choice" && !LOCATION_OPTIONS.some((option) => option.value === this.createLocation)) {
|
|
300
|
+
this.createError = `${step.title} is required.`;
|
|
301
|
+
this.refresh();
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
273
304
|
this.createError = undefined;
|
|
274
305
|
return true;
|
|
275
306
|
}
|
|
@@ -306,7 +337,7 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
306
337
|
name,
|
|
307
338
|
description: this.createValues.description.trim(),
|
|
308
339
|
allowedTools: [],
|
|
309
|
-
location:
|
|
340
|
+
location: this.createLocation,
|
|
310
341
|
},
|
|
311
342
|
selectedIndex: this.selectedIndex,
|
|
312
343
|
query: this.browseQuery,
|
|
@@ -400,7 +431,10 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
400
431
|
|
|
401
432
|
private getCreateFooter(step: CreateStep): string {
|
|
402
433
|
if (step.id === "description") {
|
|
403
|
-
return "enter
|
|
434
|
+
return "enter next • ctrl+j newline • alt+← back • alt+→ next • esc cancel";
|
|
435
|
+
}
|
|
436
|
+
if (step.id === "location") {
|
|
437
|
+
return "↑↓ choose • enter create • alt+← back • esc cancel";
|
|
404
438
|
}
|
|
405
439
|
return this.createStepIndex >= CREATE_STEPS.length - 1
|
|
406
440
|
? "enter create • alt+← back • esc cancel"
|
|
@@ -417,6 +451,18 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
417
451
|
this.listContainer.addChild(new Spacer(1));
|
|
418
452
|
this.listContainer.addChild(new Text(this.theme.fg("dim", step.hint), 1, 0));
|
|
419
453
|
}
|
|
454
|
+
} else if (step.id === "location") {
|
|
455
|
+
for (const option of step.options) {
|
|
456
|
+
const isSelected = option.value === this.createLocation;
|
|
457
|
+
const prefix = isSelected ? this.theme.fg("accent", "→ ") : " ";
|
|
458
|
+
const label = isSelected ? this.theme.fg("accent", option.label) : option.label;
|
|
459
|
+
const description = this.theme.fg("dim", ` — ${option.description}`);
|
|
460
|
+
this.listContainer.addChild(new SingleLineText(`${prefix}${label}${description}`));
|
|
461
|
+
}
|
|
462
|
+
if (step.hint) {
|
|
463
|
+
this.listContainer.addChild(new Spacer(1));
|
|
464
|
+
this.listContainer.addChild(new Text(this.theme.fg("dim", step.hint), 1, 0));
|
|
465
|
+
}
|
|
420
466
|
} else if (step.hint) {
|
|
421
467
|
this.listContainer.addChild(new Text(this.theme.fg("dim", step.hint), 1, 0));
|
|
422
468
|
}
|
|
@@ -504,7 +550,7 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
504
550
|
this.goToNextCreateStep();
|
|
505
551
|
return;
|
|
506
552
|
}
|
|
507
|
-
if (matchesKey(data, Key.enter) && this.currentCreateStep.id
|
|
553
|
+
if (matchesKey(data, Key.enter) && this.currentCreateStep.id !== "description") {
|
|
508
554
|
this.goToNextCreateStep();
|
|
509
555
|
return;
|
|
510
556
|
}
|
|
@@ -517,6 +563,19 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
517
563
|
this.refreshCreate();
|
|
518
564
|
return;
|
|
519
565
|
}
|
|
566
|
+
if (step.id === "location") {
|
|
567
|
+
if (matchesKey(data, Key.up)) {
|
|
568
|
+
this.moveLocationSelection(-1);
|
|
569
|
+
this.refreshCreate();
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (matchesKey(data, Key.down)) {
|
|
573
|
+
this.moveLocationSelection(1);
|
|
574
|
+
this.refreshCreate();
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
520
579
|
this.descriptionEditor.handleInput(data);
|
|
521
580
|
if (matchesKey(data, Key.enter)) {
|
|
522
581
|
return;
|