@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kmiyh/pi-skills-menu",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Pi extension that moves skills into a dedicated /skills menu with browsing, preview, editing, and AI-assisted creation.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -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 CreateStep = { id: CreateTextStepId; title: string; hint: string; optional: boolean; kind: "text" };
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
- this.descriptionEditor.setText(this.createValues.description);
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 = this._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
- this.createValues.description = this.descriptionEditor.getText();
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: "project",
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 create • ctrl+j newline • alt+← back • esc cancel";
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 === "name") {
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
  }