@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 +13 -1
- package/package.json +1 -1
- package/src/create-skill.ts +1 -0
- package/src/index.ts +20 -1
- package/src/skill-enabled-toggle.ts +69 -0
- package/src/skill-registry.ts +20 -5
- package/src/types.ts +2 -0
- package/src/ui/skills-selector.ts +29 -10
package/README.md
CHANGED
|
@@ -59,10 +59,22 @@ Press:
|
|
|
59
59
|
|
|
60
60
|
Press:
|
|
61
61
|
|
|
62
|
-
- `Enter` — select
|
|
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
package/src/create-skill.ts
CHANGED
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.
|
|
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
|
+
}
|
package/src/skill-registry.ts
CHANGED
|
@@ -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
|
|
57
|
-
|
|
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
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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.
|
|
615
|
+
registry.allSkills,
|
|
597
616
|
ctx.ui.theme,
|
|
598
617
|
done,
|
|
599
618
|
tui,
|