@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 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 by entering:
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
  ![skill-create-description.jpg](src/images/skill-create-description.jpg)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kmiyh/pi-skills-menu",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
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,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
- this.submittedDescriptionValue = undefined;
238
- 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
+ }
239
263
  this.input.focused = false;
240
- this.descriptionEditor.focused = this._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 (this.submittedDescriptionValue !== undefined) {
250
- this.createValues.description = this.submittedDescriptionValue;
251
- this.submittedDescriptionValue = undefined;
252
- return;
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: "project",
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 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";
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 === "name") {
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;