@kmiyh/pi-skills-menu 1.0.5 → 1.1.0

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
@@ -59,10 +59,22 @@ Press:
59
59
 
60
60
  Press:
61
61
 
62
- - `Enter` — select a skill and insert it into the editor
62
+ - `Enter` — select an enabled skill and insert it into the editor
63
63
 
64
64
  After that, the skill is inserted into the editor so it will be used by Pi when the message is sent.
65
65
 
66
+ ### Enable or disable a skill
67
+
68
+ Press:
69
+
70
+ - `x` — toggle the selected skill between enabled and disabled
71
+
72
+ This also works for skills that come from installed libraries/packages.
73
+
74
+ Disabled skills stay visible in the list and are marked with:
75
+
76
+ - `[disabled]`
77
+
66
78
  ### Create a new skill
67
79
 
68
80
  The list also contains a dedicated entry for creating a new skill.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kmiyh/pi-skills-menu",
3
- "version": "1.0.5",
3
+ "version": "1.1.0",
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": [
@@ -1014,6 +1014,7 @@ export async function createSkillFromAnswers(
1014
1014
  origin: "top-level",
1015
1015
  source: "auto",
1016
1016
  baseDir: targetDir,
1017
+ enabled: true,
1017
1018
  };
1018
1019
  }
1019
1020
 
package/src/index.ts CHANGED
@@ -5,12 +5,14 @@ import { detectExtensionInstallScope } from "./extension-scope.js";
5
5
  import { expandSkillMarkers, hasSkillMarker, insertSkillMarker, removeIncompleteSkillMarkerLines } from "./markers.js";
6
6
  import { loadSkillRegistry } from "./skill-registry.js";
7
7
  import { ensureSkillCommandsHidden } from "./settings-toggle.js";
8
+ import { setSkillEnabled } from "./skill-enabled-toggle.js";
8
9
  import type { ExtensionInstallScope, SkillRegistry } from "./types.js";
9
10
  import { showSkillPreview } from "./ui/skill-preview.js";
10
11
  import { showSkillsSelector } from "./ui/skills-selector.js";
11
12
 
12
13
  const EMPTY_REGISTRY: SkillRegistry = {
13
14
  skills: [],
15
+ allSkills: [],
14
16
  byName: new Map(),
15
17
  };
16
18
 
@@ -131,12 +133,29 @@ export default function skillsMenuExtension(pi: ExtensionAPI) {
131
133
  continue;
132
134
  }
133
135
  await refreshRegistry(ctx.cwd);
134
- const createdSkillIndex = registry.skills.findIndex((skill) => skill.path === createdSkill.path);
136
+ const createdSkillIndex = registry.allSkills.findIndex((skill) => skill.path === createdSkill.path);
135
137
  selectorIndex = createdSkillIndex >= 0 ? createdSkillIndex + 1 : 0;
136
138
  selectorQuery = "";
137
139
  continue;
138
140
  }
139
141
 
142
+ if (selection.type === "toggle") {
143
+ try {
144
+ await setSkillEnabled(ctx.cwd, selection.skill, !selection.skill.enabled);
145
+ await refreshRegistry(ctx.cwd);
146
+ ctx.ui.notify(
147
+ selection.skill.enabled
148
+ ? `Disabled ${selection.skill.name}. Run /reload to fully apply the change.`
149
+ : `Enabled ${selection.skill.name}. Run /reload to fully apply the change.`,
150
+ "info",
151
+ );
152
+ } catch (error) {
153
+ console.error("skills-menu: failed to toggle skill", error);
154
+ ctx.ui.notify(error instanceof Error ? error.message : "Failed to update skill visibility", "error");
155
+ }
156
+ continue;
157
+ }
158
+
140
159
  if (selection.type === "preview") {
141
160
  await showSkillPreview(ctx, selection.skill);
142
161
  continue;
@@ -0,0 +1,69 @@
1
+ import { dirname, join, relative } from "node:path";
2
+ import { getAgentDir, SettingsManager, type PackageSource } from "@mariozechner/pi-coding-agent";
3
+ import type { SkillEntry } from "./types.js";
4
+
5
+ function updatePatterns(current: string[], pattern: string, enabled: boolean): string[] {
6
+ const updated = current.filter((entry) => {
7
+ const stripped = entry.startsWith("!") || entry.startsWith("+") || entry.startsWith("-") ? entry.slice(1) : entry;
8
+ return stripped !== pattern;
9
+ });
10
+ updated.push(`${enabled ? "+" : "-"}${pattern}`);
11
+ return updated;
12
+ }
13
+
14
+ function getTopLevelPattern(skill: SkillEntry, cwd: string): string {
15
+ const baseDir = skill.scope === "project" ? join(cwd, ".pi") : getAgentDir();
16
+ return relative(baseDir, skill.path);
17
+ }
18
+
19
+ function getPackagePattern(skill: SkillEntry): string {
20
+ const baseDir = skill.baseDir ?? dirname(skill.path);
21
+ return relative(baseDir, skill.path);
22
+ }
23
+
24
+ function hasPackageFilters(pkg: Exclude<PackageSource, string>): boolean {
25
+ return pkg.extensions !== undefined || pkg.skills !== undefined || pkg.prompts !== undefined || pkg.themes !== undefined;
26
+ }
27
+
28
+ export async function setSkillEnabled(cwd: string, skill: SkillEntry, enabled: boolean): Promise<void> {
29
+ if (skill.scope === "temporary") {
30
+ throw new Error("Temporary skills cannot be toggled.");
31
+ }
32
+
33
+ const settingsManager = SettingsManager.create(cwd, getAgentDir());
34
+
35
+ if (skill.origin === "top-level") {
36
+ const settings = skill.scope === "project" ? settingsManager.getProjectSettings() : settingsManager.getGlobalSettings();
37
+ const current = [...(settings.skills ?? [])];
38
+ const updated = updatePatterns(current, getTopLevelPattern(skill, cwd), enabled);
39
+
40
+ if (skill.scope === "project") {
41
+ settingsManager.setProjectSkillPaths(updated);
42
+ } else {
43
+ settingsManager.setSkillPaths(updated);
44
+ }
45
+ await settingsManager.flush();
46
+ return;
47
+ }
48
+
49
+ const settings = skill.scope === "project" ? settingsManager.getProjectSettings() : settingsManager.getGlobalSettings();
50
+ const packages = [...(settings.packages ?? [])];
51
+ const packageIndex = packages.findIndex((pkg) => (typeof pkg === "string" ? pkg : pkg.source) === skill.source);
52
+ if (packageIndex === -1) {
53
+ throw new Error("Could not find the package settings entry for this skill.");
54
+ }
55
+
56
+ const packageEntry = packages[packageIndex];
57
+ const packageConfig = typeof packageEntry === "string" ? { source: packageEntry } : { ...packageEntry };
58
+ const current = [...(packageConfig.skills ?? [])];
59
+ const updated = updatePatterns(current, getPackagePattern(skill), enabled);
60
+ packageConfig.skills = updated.length > 0 ? updated : undefined;
61
+ packages[packageIndex] = hasPackageFilters(packageConfig) ? packageConfig : packageConfig.source;
62
+
63
+ if (skill.scope === "project") {
64
+ settingsManager.setProjectPackages(packages);
65
+ } else {
66
+ settingsManager.setPackages(packages);
67
+ }
68
+ await settingsManager.flush();
69
+ }
@@ -3,6 +3,10 @@ import { parseSkillFile } from "./skill-parser.js";
3
3
  import type { SkillEntry, SkillRegistry } from "./types.js";
4
4
 
5
5
  function compareSkills(a: SkillEntry, b: SkillEntry): number {
6
+ if (a.enabled !== b.enabled) {
7
+ return a.enabled ? -1 : 1;
8
+ }
9
+
6
10
  const scopeRank = (scope: SkillEntry["scope"]) => {
7
11
  switch (scope) {
8
12
  case "project":
@@ -25,8 +29,6 @@ function compareSkills(a: SkillEntry, b: SkillEntry): number {
25
29
  }
26
30
 
27
31
  function toSkillEntry(resource: ResolvedResource): SkillEntry | null {
28
- if (!resource.enabled) return null;
29
-
30
32
  const parsed = parseSkillFile(resource.path);
31
33
  if (!parsed) return null;
32
34
 
@@ -40,9 +42,21 @@ function toSkillEntry(resource: ResolvedResource): SkillEntry | null {
40
42
  origin: resource.metadata.origin,
41
43
  source: resource.metadata.source,
42
44
  baseDir: resource.metadata.baseDir,
45
+ enabled: resource.enabled,
43
46
  };
44
47
  }
45
48
 
49
+ function dedupeByPath(skills: SkillEntry[]): SkillEntry[] {
50
+ const seen = new Set<string>();
51
+ const deduped: SkillEntry[] = [];
52
+ for (const skill of skills) {
53
+ if (seen.has(skill.path)) continue;
54
+ seen.add(skill.path);
55
+ deduped.push(skill);
56
+ }
57
+ return deduped;
58
+ }
59
+
46
60
  export async function loadSkillRegistry(cwd: string): Promise<SkillRegistry> {
47
61
  const settingsManager = SettingsManager.create(cwd, getAgentDir());
48
62
  const packageManager = new DefaultPackageManager({
@@ -52,10 +66,10 @@ export async function loadSkillRegistry(cwd: string): Promise<SkillRegistry> {
52
66
  });
53
67
  const resolved = await packageManager.resolve();
54
68
 
69
+ const allSkills = dedupeByPath(resolved.skills.map(toSkillEntry).filter((entry): entry is SkillEntry => entry !== null)).sort(compareSkills);
55
70
  const byName = new Map<string, SkillEntry>();
56
- for (const resource of resolved.skills) {
57
- const entry = toSkillEntry(resource);
58
- if (!entry) continue;
71
+ for (const entry of allSkills) {
72
+ if (!entry.enabled) continue;
59
73
  if (!byName.has(entry.name)) {
60
74
  byName.set(entry.name, entry);
61
75
  }
@@ -64,6 +78,7 @@ export async function loadSkillRegistry(cwd: string): Promise<SkillRegistry> {
64
78
  const skills = Array.from(byName.values()).sort(compareSkills);
65
79
  return {
66
80
  skills,
81
+ allSkills,
67
82
  byName: new Map(skills.map((skill) => [skill.name, skill])),
68
83
  };
69
84
  }
package/src/types.ts CHANGED
@@ -10,9 +10,11 @@ export interface SkillEntry {
10
10
  origin: "package" | "top-level";
11
11
  source: string;
12
12
  baseDir?: string;
13
+ enabled: boolean;
13
14
  }
14
15
 
15
16
  export interface SkillRegistry {
16
17
  skills: SkillEntry[];
18
+ allSkills: SkillEntry[];
17
19
  byName: Map<string, SkillEntry>;
18
20
  }
@@ -22,6 +22,7 @@ export type SkillsMenuSelection =
22
22
  | { type: "create"; answers: SkillCreationAnswers; selectedIndex: number; query: string }
23
23
  | { type: "preview"; skill: SkillEntry; selectedIndex: number; query: string }
24
24
  | { type: "delete"; skill: SkillEntry; selectedIndex: number; query: string }
25
+ | { type: "toggle"; skill: SkillEntry; selectedIndex: number; query: string }
25
26
  | null;
26
27
 
27
28
  type CreateTextStepId = "name" | "description";
@@ -179,8 +180,8 @@ class SkillsSelectorComponent extends Container implements Focusable {
179
180
  }
180
181
 
181
182
  private orderBrowseSkills(skills: SkillEntry[]): SkillEntry[] {
182
- const ownSkills = skills.filter((skill) => isDeletableSkill(skill));
183
- const otherSkills = skills.filter((skill) => !isDeletableSkill(skill));
183
+ const ownSkills = skills.filter((skill) => isDeletableSkill(skill)).sort((a, b) => Number(b.enabled) - Number(a.enabled));
184
+ const otherSkills = skills.filter((skill) => !isDeletableSkill(skill)).sort((a, b) => Number(b.enabled) - Number(a.enabled));
184
185
  return [...ownSkills, ...otherSkills];
185
186
  }
186
187
 
@@ -361,10 +362,20 @@ class SkillsSelectorComponent extends Container implements Focusable {
361
362
  this.filteredSkills = this.orderBrowseSkills(this.filterSkills(this.browseQuery));
362
363
  this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.getSelectableCount() - 1));
363
364
  const selectedSkill = this.getSelectedSkill();
364
- const footer = !this.browseQuery && selectedSkill && isDeletableSkill(selectedSkill)
365
- ? "type to search • enter insert • tab preview • backspace delete • esc close"
366
- : "type to search • enter insert • tab preview • esc close";
367
- this.footerText.setText(this.theme.fg("dim", footer));
365
+ const actions = ["type to search"];
366
+ if (!selectedSkill) {
367
+ actions.push("enter create", "esc close");
368
+ } else {
369
+ if (selectedSkill.enabled) {
370
+ actions.push("enter insert");
371
+ }
372
+ actions.push("tab preview", "x enable/disable");
373
+ if (!this.browseQuery && isDeletableSkill(selectedSkill)) {
374
+ actions.push("backspace delete");
375
+ }
376
+ actions.push("esc close");
377
+ }
378
+ this.footerText.setText(this.theme.fg("dim", actions.join(" • ")));
368
379
  this.renderBrowseList();
369
380
  }
370
381
 
@@ -406,13 +417,14 @@ class SkillsSelectorComponent extends Container implements Focusable {
406
417
  } else {
407
418
  const skill = entry.skill;
408
419
  const prefix = isSelected ? this.theme.fg("accent", "→ ") : " ";
409
- const name = isSelected ? this.theme.fg("accent", skill.name) : skill.name;
420
+ const name = isSelected ? this.theme.fg("accent", skill.name) : skill.enabled ? skill.name : this.theme.fg("muted", skill.name);
421
+ const status = skill.enabled ? "" : this.theme.fg("warning", " [disabled]");
410
422
  const scope = this.theme.fg("muted", ` [${getScopeLabel(skill)}]`);
411
423
  const packageLabel = getPackageLabel(skill);
412
424
  const source = packageLabel ? this.theme.fg("muted", ` - [${packageLabel}]`) : "";
413
425
  const descriptionPrefix = packageLabel ? " " : " - ";
414
426
  const description = this.theme.fg("dim", `${descriptionPrefix}${skill.description}`);
415
- this.listContainer.addChild(new SingleLineText(`${prefix}${name}${scope}${source}${description}`, descriptionEllipsis));
427
+ this.listContainer.addChild(new SingleLineText(`${prefix}${name}${status}${scope}${source}${description}`, descriptionEllipsis));
416
428
  }
417
429
  }
418
430
  if (isSelectable) {
@@ -503,7 +515,7 @@ class SkillsSelectorComponent extends Container implements Focusable {
503
515
  return;
504
516
  }
505
517
  const skill = this.getSelectedSkill();
506
- if (skill) {
518
+ if (skill?.enabled) {
507
519
  this.done({ type: "skill", skill, selectedIndex: this.selectedIndex, query: this.browseQuery });
508
520
  }
509
521
  return;
@@ -515,6 +527,13 @@ class SkillsSelectorComponent extends Container implements Focusable {
515
527
  }
516
528
  return;
517
529
  }
530
+ if (data === "x" || data === "X") {
531
+ const skill = this.getSelectedSkill();
532
+ if (skill) {
533
+ this.done({ type: "toggle", skill, selectedIndex: this.selectedIndex, query: this.browseQuery });
534
+ }
535
+ return;
536
+ }
518
537
  if (matchesKey(data, Key.backspace) && !this.input.getValue()) {
519
538
  const skill = this.getSelectedSkill();
520
539
  if (skill && isDeletableSkill(skill)) {
@@ -593,7 +612,7 @@ export async function showSkillsSelector(
593
612
  ): Promise<SkillsMenuSelection> {
594
613
  return await ctx.ui.custom<SkillsMenuSelection>((tui, _theme, _kb, done) => {
595
614
  const component = new SkillsSelectorComponent(
596
- registry.skills,
615
+ registry.allSkills,
597
616
  ctx.ui.theme,
598
617
  done,
599
618
  tui,