@kmiyh/pi-skills-menu 1.1.0 → 1.2.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 +1 -1
- package/package.json +1 -1
- package/src/create-skill.ts +75 -21
- package/src/index.ts +31 -71
- package/src/skill-registry.ts +0 -4
- package/src/ui/skill-preview.ts +3 -3
- package/src/ui/skills-manager.ts +1357 -0
- package/src/ui/skills-selector.ts +4 -4
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
|
Press:
|
|
69
69
|
|
|
70
|
-
- `
|
|
70
|
+
- `Ctrl+X` — toggle the selected skill between enabled and disabled
|
|
71
71
|
|
|
72
72
|
This also works for skills that come from installed libraries/packages.
|
|
73
73
|
|
package/package.json
CHANGED
package/src/create-skill.ts
CHANGED
|
@@ -546,6 +546,7 @@ export type SkillCreationThinkingLevel = ThinkingLevel | "off";
|
|
|
546
546
|
|
|
547
547
|
export interface SkillGenerationOptions {
|
|
548
548
|
thinkingLevel?: SkillCreationThinkingLevel;
|
|
549
|
+
signal?: AbortSignal;
|
|
549
550
|
}
|
|
550
551
|
|
|
551
552
|
class SingleLineText implements Component {
|
|
@@ -863,11 +864,21 @@ function getGenerationStatusLabel(
|
|
|
863
864
|
: `Generating skill draft using ${modelLabel}...`;
|
|
864
865
|
}
|
|
865
866
|
|
|
867
|
+
function isAbortError(error: unknown): boolean {
|
|
868
|
+
if (!error || typeof error !== "object") return false;
|
|
869
|
+
const name = "name" in error ? String((error as { name?: unknown }).name) : "";
|
|
870
|
+
const message = "message" in error ? String((error as { message?: unknown }).message) : "";
|
|
871
|
+
return name === "AbortError" || message.toLowerCase().includes("aborted");
|
|
872
|
+
}
|
|
873
|
+
|
|
866
874
|
async function generateSkillDraft(
|
|
867
875
|
ctx: ExtensionContext,
|
|
868
876
|
answers: SkillCreationAnswers,
|
|
869
877
|
options?: SkillGenerationOptions,
|
|
870
878
|
): Promise<string> {
|
|
879
|
+
if (options?.signal?.aborted) {
|
|
880
|
+
throw new Error("Generation aborted");
|
|
881
|
+
}
|
|
871
882
|
if (!ctx.model) {
|
|
872
883
|
return buildFallbackSkill(answers);
|
|
873
884
|
}
|
|
@@ -910,9 +921,13 @@ async function generateSkillDraft(
|
|
|
910
921
|
const response = await completeSimple(
|
|
911
922
|
ctx.model,
|
|
912
923
|
{ systemPrompt: GENERATE_SKILL_SYSTEM_PROMPT, messages: [userMessage] },
|
|
913
|
-
{ apiKey: auth.apiKey, headers: auth.headers, ...(reasoning ? { reasoning } : {}) },
|
|
924
|
+
{ apiKey: auth.apiKey, headers: auth.headers, ...(reasoning ? { reasoning } : {}), ...(options?.signal ? { signal: options.signal } : {}) },
|
|
914
925
|
);
|
|
915
926
|
|
|
927
|
+
if (options?.signal?.aborted) {
|
|
928
|
+
throw new Error("Generation aborted");
|
|
929
|
+
}
|
|
930
|
+
|
|
916
931
|
const generated = response.content
|
|
917
932
|
.filter((c): c is { type: "text"; text: string } => c.type === "text")
|
|
918
933
|
.map((c) => c.text)
|
|
@@ -942,12 +957,45 @@ async function runDraftGeneration(
|
|
|
942
957
|
|
|
943
958
|
generateSkillDraft(ctx, answers, options)
|
|
944
959
|
.then(done)
|
|
945
|
-
.catch(() => done(buildFallbackSkill(answers)));
|
|
960
|
+
.catch((error) => done(isAbortError(error) ? null : buildFallbackSkill(answers)));
|
|
946
961
|
|
|
947
962
|
return loader;
|
|
948
963
|
});
|
|
949
964
|
}
|
|
950
965
|
|
|
966
|
+
async function saveCreatedSkill(
|
|
967
|
+
ctx: ExtensionContext,
|
|
968
|
+
answers: SkillCreationAnswers,
|
|
969
|
+
draft: string,
|
|
970
|
+
): Promise<SkillEntry | null> {
|
|
971
|
+
let parsedSkill: ParsedSkillDraft;
|
|
972
|
+
try {
|
|
973
|
+
parsedSkill = parseSkillDraft(draft, answers.name);
|
|
974
|
+
} catch (error) {
|
|
975
|
+
ctx.ui.notify(error instanceof Error ? error.message : "Invalid generated SKILL.md", "error");
|
|
976
|
+
return null;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const targetDir = getTargetDir(ctx, answers.location, answers.name);
|
|
980
|
+
const targetPath = join(targetDir, "SKILL.md");
|
|
981
|
+
await mkdir(targetDir, { recursive: true });
|
|
982
|
+
await writeFile(targetPath, parsedSkill.raw, "utf8");
|
|
983
|
+
|
|
984
|
+
ctx.ui.notify(`Created skill: ${targetPath}`, "info");
|
|
985
|
+
return {
|
|
986
|
+
name: parsedSkill.name,
|
|
987
|
+
description: parsedSkill.description,
|
|
988
|
+
path: targetPath,
|
|
989
|
+
content: parsedSkill.content,
|
|
990
|
+
frontmatter: parsedSkill.frontmatter,
|
|
991
|
+
scope: answers.location === "global" ? "user" : "project",
|
|
992
|
+
origin: "top-level",
|
|
993
|
+
source: "auto",
|
|
994
|
+
baseDir: targetDir,
|
|
995
|
+
enabled: true,
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
|
|
951
999
|
async function collectAnswers(ctx: ExtensionContext): Promise<SkillCreationAnswers | null> {
|
|
952
1000
|
const answers = await ctx.ui.custom<SkillCreationAnswers | null>((tui, _theme, _kb, done) => {
|
|
953
1001
|
const component = new SkillCreationWizard(ctx.ui.theme, done);
|
|
@@ -992,30 +1040,36 @@ export async function createSkillFromAnswers(
|
|
|
992
1040
|
return null;
|
|
993
1041
|
}
|
|
994
1042
|
|
|
995
|
-
|
|
1043
|
+
return await saveCreatedSkill(ctx, answers, draft);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
export async function createSkillFromAnswersWithoutUI(
|
|
1047
|
+
ctx: ExtensionContext,
|
|
1048
|
+
answers: SkillCreationAnswers,
|
|
1049
|
+
options?: SkillGenerationOptions,
|
|
1050
|
+
): Promise<SkillEntry | null> {
|
|
1051
|
+
const targetDir = getTargetDir(ctx, answers.location, answers.name);
|
|
1052
|
+
const targetPath = join(targetDir, "SKILL.md");
|
|
1053
|
+
if (existsSync(targetPath)) {
|
|
1054
|
+
ctx.ui.notify(`Skill already exists: ${targetPath}`, "error");
|
|
1055
|
+
return null;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
let draft: string;
|
|
996
1059
|
try {
|
|
997
|
-
|
|
1060
|
+
draft = await generateSkillDraft(ctx, answers, options);
|
|
998
1061
|
} catch (error) {
|
|
999
|
-
|
|
1000
|
-
|
|
1062
|
+
if (isAbortError(error) || options?.signal?.aborted) {
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
draft = buildFallbackSkill(answers);
|
|
1001
1066
|
}
|
|
1002
1067
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1068
|
+
if (options?.signal?.aborted) {
|
|
1069
|
+
return null;
|
|
1070
|
+
}
|
|
1005
1071
|
|
|
1006
|
-
ctx
|
|
1007
|
-
return {
|
|
1008
|
-
name: parsedSkill.name,
|
|
1009
|
-
description: parsedSkill.description,
|
|
1010
|
-
path: targetPath,
|
|
1011
|
-
content: parsedSkill.content,
|
|
1012
|
-
frontmatter: parsedSkill.frontmatter,
|
|
1013
|
-
scope: answers.location === "global" ? "user" : "project",
|
|
1014
|
-
origin: "top-level",
|
|
1015
|
-
source: "auto",
|
|
1016
|
-
baseDir: targetDir,
|
|
1017
|
-
enabled: true,
|
|
1018
|
-
};
|
|
1072
|
+
return await saveCreatedSkill(ctx, answers, draft);
|
|
1019
1073
|
}
|
|
1020
1074
|
|
|
1021
1075
|
export async function createNewSkill(
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext, InputEventResult } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import {
|
|
2
|
+
import { createSkillFromAnswersWithoutUI } from "./create-skill.js";
|
|
3
3
|
import { deleteSkill } from "./delete-skill.js";
|
|
4
4
|
import { detectExtensionInstallScope } from "./extension-scope.js";
|
|
5
5
|
import { expandSkillMarkers, hasSkillMarker, insertSkillMarker, removeIncompleteSkillMarkerLines } from "./markers.js";
|
|
@@ -7,8 +7,7 @@ import { loadSkillRegistry } from "./skill-registry.js";
|
|
|
7
7
|
import { ensureSkillCommandsHidden } from "./settings-toggle.js";
|
|
8
8
|
import { setSkillEnabled } from "./skill-enabled-toggle.js";
|
|
9
9
|
import type { ExtensionInstallScope, SkillRegistry } from "./types.js";
|
|
10
|
-
import {
|
|
11
|
-
import { showSkillsSelector } from "./ui/skills-selector.js";
|
|
10
|
+
import { showSkillsManager } from "./ui/skills-manager.js";
|
|
12
11
|
|
|
13
12
|
const EMPTY_REGISTRY: SkillRegistry = {
|
|
14
13
|
skills: [],
|
|
@@ -107,81 +106,42 @@ export default function skillsMenuExtension(pi: ExtensionAPI) {
|
|
|
107
106
|
return;
|
|
108
107
|
}
|
|
109
108
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
selectorIndex = selection.selectedIndex;
|
|
126
|
-
selectorQuery = selection.query;
|
|
127
|
-
|
|
128
|
-
if (selection.type === "create") {
|
|
129
|
-
const createdSkill = await createSkillFromAnswers(ctx, selection.answers, {
|
|
130
|
-
thinkingLevel: pi.getThinkingLevel(),
|
|
131
|
-
});
|
|
132
|
-
if (!createdSkill) {
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
await refreshRegistry(ctx.cwd);
|
|
136
|
-
const createdSkillIndex = registry.allSkills.findIndex((skill) => skill.path === createdSkill.path);
|
|
137
|
-
selectorIndex = createdSkillIndex >= 0 ? createdSkillIndex + 1 : 0;
|
|
138
|
-
selectorQuery = "";
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (selection.type === "toggle") {
|
|
109
|
+
try {
|
|
110
|
+
await refreshRegistry(ctx.cwd);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error("skills-menu: failed to refresh skills registry", error);
|
|
113
|
+
ctx.ui.notify("Failed to load skills list", "error");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const selection = await showSkillsManager(ctx, registry, {
|
|
118
|
+
onCreate: async (answers, signal) => await createSkillFromAnswersWithoutUI(ctx, answers, {
|
|
119
|
+
thinkingLevel: pi.getThinkingLevel(),
|
|
120
|
+
signal,
|
|
121
|
+
}),
|
|
122
|
+
onDelete: async (skill) => {
|
|
143
123
|
try {
|
|
144
|
-
await
|
|
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
|
-
);
|
|
124
|
+
return await deleteSkill(ctx, skill);
|
|
152
125
|
} catch (error) {
|
|
153
|
-
console.error("skills-menu: failed to
|
|
154
|
-
ctx.ui.notify(
|
|
155
|
-
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (selection.type === "preview") {
|
|
160
|
-
await showSkillPreview(ctx, selection.skill);
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (selection.type === "delete") {
|
|
165
|
-
const confirmed = await ctx.ui.confirm(
|
|
166
|
-
"Delete skill",
|
|
167
|
-
`Delete ${selection.skill.name}? This removes the skill from disk and cannot be undone.`,
|
|
168
|
-
);
|
|
169
|
-
if (!confirmed) {
|
|
170
|
-
continue;
|
|
126
|
+
console.error("skills-menu: failed to delete skill", error);
|
|
127
|
+
ctx.ui.notify("Failed to delete skill", "error");
|
|
128
|
+
return false;
|
|
171
129
|
}
|
|
130
|
+
},
|
|
131
|
+
onToggle: async (skill, enabled) => {
|
|
172
132
|
try {
|
|
173
|
-
await
|
|
174
|
-
selectorIndex = Math.max(0, selectorIndex - 1);
|
|
133
|
+
await setSkillEnabled(ctx.cwd, skill, enabled);
|
|
175
134
|
} catch (error) {
|
|
176
|
-
console.error("skills-menu: failed to
|
|
177
|
-
|
|
135
|
+
console.error("skills-menu: failed to toggle skill", error);
|
|
136
|
+
throw error instanceof Error ? error : new Error("Failed to update skill visibility");
|
|
178
137
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
138
|
+
},
|
|
139
|
+
onRefresh: async () => await refreshRegistry(ctx.cwd),
|
|
140
|
+
});
|
|
141
|
+
if (!selection) {
|
|
183
142
|
return;
|
|
184
143
|
}
|
|
144
|
+
insertSkillMarker(ctx, selection);
|
|
185
145
|
},
|
|
186
146
|
});
|
|
187
147
|
|
|
@@ -207,7 +167,7 @@ export default function skillsMenuExtension(pi: ExtensionAPI) {
|
|
|
207
167
|
return { action: "continue" };
|
|
208
168
|
}
|
|
209
169
|
|
|
210
|
-
if (!currentCwd || currentCwd !== ctx.cwd || registry.
|
|
170
|
+
if (!currentCwd || currentCwd !== ctx.cwd || registry.allSkills.length === 0) {
|
|
211
171
|
try {
|
|
212
172
|
await refreshRegistry(ctx.cwd);
|
|
213
173
|
} catch (error) {
|
package/src/skill-registry.ts
CHANGED
|
@@ -3,10 +3,6 @@ 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
|
-
|
|
10
6
|
const scopeRank = (scope: SkillEntry["scope"]) => {
|
|
11
7
|
switch (scope) {
|
|
12
8
|
case "project":
|
package/src/ui/skill-preview.ts
CHANGED
|
@@ -37,7 +37,7 @@ function getSkillLocation(skill: SkillEntry): string {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function getSkillLocationLabel(skill: SkillEntry): string {
|
|
40
|
-
return skill.origin === "package" ? "package" : "
|
|
40
|
+
return skill.origin === "package" ? "package • " : "";
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function formatScalar(value: unknown): string {
|
|
@@ -271,7 +271,7 @@ class ScrollableSkillPreview implements Component {
|
|
|
271
271
|
private buildContentLines(innerWidth: number): string[] {
|
|
272
272
|
const content = new Container();
|
|
273
273
|
content.addChild(new Text(this.theme.fg("accent", this.theme.bold(this.skill.name)), 0, 0));
|
|
274
|
-
content.addChild(new Text(this.theme.fg("muted", `${getSkillLocationLabel(this.skill)}
|
|
274
|
+
content.addChild(new Text(this.theme.fg("muted", `${getSkillLocationLabel(this.skill)}${getSkillLocation(this.skill)}`), 0, 0));
|
|
275
275
|
content.addChild(new Spacer(1));
|
|
276
276
|
content.addChild(new Text(this.theme.fg("muted", this.theme.bold("Metadata")), 0, 0));
|
|
277
277
|
content.addChild(new Text(this.theme.fg("dim", buildFrontmatterBlock(this.skill)), 0, 0));
|
|
@@ -292,7 +292,7 @@ class ScrollableSkillPreview implements Component {
|
|
|
292
292
|
: "";
|
|
293
293
|
const editInfo = this.editable ? " • e edit • r rename" : "";
|
|
294
294
|
return truncateToWidth(
|
|
295
|
-
this.theme.fg("dim", `↑/↓ scroll
|
|
295
|
+
this.theme.fg("dim", `↑/↓ scroll${editInfo} • esc back${scrollInfo}`),
|
|
296
296
|
innerWidth,
|
|
297
297
|
this.theme.fg("dim", "..."),
|
|
298
298
|
);
|