@kmiyh/pi-skills-menu 1.0.2 → 1.0.4
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 +1 -1
- package/src/ui/skills-selector.ts +79 -10
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,8 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
96
107
|
name: "",
|
|
97
108
|
description: "",
|
|
98
109
|
};
|
|
110
|
+
private createLocation: SkillLocation = "global";
|
|
111
|
+
private submittedDescriptionValue: string | undefined;
|
|
99
112
|
private createError: string | undefined;
|
|
100
113
|
private browseQuery: string;
|
|
101
114
|
|
|
@@ -128,7 +141,8 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
128
141
|
noMatch: (text: string) => this.theme.fg("warning", text),
|
|
129
142
|
},
|
|
130
143
|
});
|
|
131
|
-
this.descriptionEditor.onSubmit = () => {
|
|
144
|
+
this.descriptionEditor.onSubmit = (text: string) => {
|
|
145
|
+
this.submittedDescriptionValue = text;
|
|
132
146
|
this.goToNextCreateStep();
|
|
133
147
|
};
|
|
134
148
|
this.filteredSkills = skills;
|
|
@@ -204,6 +218,13 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
204
218
|
return CREATE_STEPS[this.createStepIndex]!;
|
|
205
219
|
}
|
|
206
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
|
+
|
|
207
228
|
private setBrowseInputValue(value: string): void {
|
|
208
229
|
this.browseQuery = value;
|
|
209
230
|
this.input.setValue(value);
|
|
@@ -232,9 +253,15 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
232
253
|
this.descriptionEditor.focused = false;
|
|
233
254
|
return;
|
|
234
255
|
}
|
|
235
|
-
|
|
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
|
+
}
|
|
236
263
|
this.input.focused = false;
|
|
237
|
-
this.descriptionEditor.focused =
|
|
264
|
+
this.descriptionEditor.focused = false;
|
|
238
265
|
}
|
|
239
266
|
|
|
240
267
|
private persistCreateInput(): void {
|
|
@@ -243,7 +270,14 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
243
270
|
this.createValues.name = this.input.getValue();
|
|
244
271
|
return;
|
|
245
272
|
}
|
|
246
|
-
|
|
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();
|
|
280
|
+
}
|
|
247
281
|
}
|
|
248
282
|
|
|
249
283
|
private validateCreateStep(): boolean {
|
|
@@ -262,6 +296,11 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
262
296
|
return false;
|
|
263
297
|
}
|
|
264
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
|
+
}
|
|
265
304
|
this.createError = undefined;
|
|
266
305
|
return true;
|
|
267
306
|
}
|
|
@@ -276,7 +315,6 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
276
315
|
}
|
|
277
316
|
|
|
278
317
|
private submitCreate(): void {
|
|
279
|
-
this.persistCreateInput();
|
|
280
318
|
const name = normalizeSkillName(this.createValues.name);
|
|
281
319
|
if (!name) {
|
|
282
320
|
this.createStepIndex = 0;
|
|
@@ -299,7 +337,7 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
299
337
|
name,
|
|
300
338
|
description: this.createValues.description.trim(),
|
|
301
339
|
allowedTools: [],
|
|
302
|
-
location:
|
|
340
|
+
location: this.createLocation,
|
|
303
341
|
},
|
|
304
342
|
selectedIndex: this.selectedIndex,
|
|
305
343
|
query: this.browseQuery,
|
|
@@ -393,7 +431,10 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
393
431
|
|
|
394
432
|
private getCreateFooter(step: CreateStep): string {
|
|
395
433
|
if (step.id === "description") {
|
|
396
|
-
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";
|
|
397
438
|
}
|
|
398
439
|
return this.createStepIndex >= CREATE_STEPS.length - 1
|
|
399
440
|
? "enter create • alt+← back • esc cancel"
|
|
@@ -410,6 +451,18 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
410
451
|
this.listContainer.addChild(new Spacer(1));
|
|
411
452
|
this.listContainer.addChild(new Text(this.theme.fg("dim", step.hint), 1, 0));
|
|
412
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
|
+
}
|
|
413
466
|
} else if (step.hint) {
|
|
414
467
|
this.listContainer.addChild(new Text(this.theme.fg("dim", step.hint), 1, 0));
|
|
415
468
|
}
|
|
@@ -497,7 +550,7 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
497
550
|
this.goToNextCreateStep();
|
|
498
551
|
return;
|
|
499
552
|
}
|
|
500
|
-
if (matchesKey(data, Key.enter) && this.currentCreateStep.id
|
|
553
|
+
if (matchesKey(data, Key.enter) && this.currentCreateStep.id !== "description") {
|
|
501
554
|
this.goToNextCreateStep();
|
|
502
555
|
return;
|
|
503
556
|
}
|
|
@@ -510,7 +563,23 @@ class SkillsSelectorComponent extends Container implements Focusable {
|
|
|
510
563
|
this.refreshCreate();
|
|
511
564
|
return;
|
|
512
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
|
+
}
|
|
513
579
|
this.descriptionEditor.handleInput(data);
|
|
580
|
+
if (matchesKey(data, Key.enter)) {
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
514
583
|
this.createValues.description = this.descriptionEditor.getText();
|
|
515
584
|
this.refreshCreate();
|
|
516
585
|
}
|