pi-skillful 0.1.0 → 0.2.1
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/CHANGELOG.md +3 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/extensions/skill-visibility.ts +79 -32
package/CHANGELOG.md
CHANGED
|
@@ -6,9 +6,11 @@ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.0] - 2026-05-07
|
|
10
|
+
|
|
9
11
|
### Added
|
|
10
12
|
|
|
11
13
|
- Inline `/skill:name` expansion anywhere in a prompt.
|
|
12
14
|
- `/skillful` menu for global/project skill prompt visibility.
|
|
13
15
|
- `skillful.hiddenSkills` settings support.
|
|
14
|
-
- Startup
|
|
16
|
+
- Startup `[Skills]` list highlights hidden skills in the error color.
|
package/README.md
CHANGED
|
@@ -57,7 +57,7 @@ Open the menu with:
|
|
|
57
57
|
|
|
58
58
|
The menu lists all loaded skills alphabetically. Toggle a skill off to save it in the active scope's `hiddenSkills` list. Use the Global/Project tabs to choose which settings file to edit.
|
|
59
59
|
|
|
60
|
-
Pi's startup `[Skills]` list
|
|
60
|
+
Pi's startup `[Skills]` list also highlights hidden skills in the error color (red in the default dark theme).
|
|
61
61
|
|
|
62
62
|
When the project settings file contains only `skillful` settings and the project `hiddenSkills` list becomes empty, `.pi/settings.json` is deleted instead of leaving an empty settings file behind.
|
|
63
63
|
|
package/package.json
CHANGED
|
@@ -20,16 +20,34 @@ import {
|
|
|
20
20
|
const SKILLS_SECTION_PATTERN = /\n\nThe following skills provide specialized instructions for specific tasks\.[\s\S]*?<\/available_skills>/;
|
|
21
21
|
const SCOPES: SkillfulScope[] = ["global", "project"];
|
|
22
22
|
const STORE_KEY = Symbol.for("pi-skillful.skillVisibilityStore");
|
|
23
|
-
const
|
|
23
|
+
const STARTUP_PATCH_KEY = Symbol.for("pi-skillful.startupPatchV2");
|
|
24
24
|
|
|
25
25
|
interface SkillVisibilityStore {
|
|
26
26
|
hiddenSkillsByCwd: Map<string, Set<string>>;
|
|
27
27
|
lastHiddenSkills: Set<string>;
|
|
28
|
+
theme: Theme | null;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
interface ExpandableTextLike {
|
|
32
|
+
getCollapsedText: () => string;
|
|
33
|
+
setText: (text: string) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface BoxLike {
|
|
37
|
+
children: unknown[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface InteractiveModeLike {
|
|
41
|
+
chatContainer?: BoxLike;
|
|
42
|
+
showLoadedResources?: (options?: unknown) => void;
|
|
43
|
+
session?: { resourceLoader?: { getSkills: () => { skills: Skill[]; diagnostics: unknown[] } } };
|
|
44
|
+
sessionManager?: { getCwd?: () => string };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const store = (((globalThis as Record<PropertyKey, unknown>)[STORE_KEY] as SkillVisibilityStore | undefined) ??= {
|
|
31
48
|
hiddenSkillsByCwd: new Map<string, Set<string>>(),
|
|
32
49
|
lastHiddenSkills: new Set<string>(),
|
|
50
|
+
theme: null,
|
|
33
51
|
}) as SkillVisibilityStore;
|
|
34
52
|
|
|
35
53
|
interface SkillListItem {
|
|
@@ -41,7 +59,8 @@ export default function skillVisibility(pi: ExtensionAPI) {
|
|
|
41
59
|
installStartupSkillListPatch();
|
|
42
60
|
|
|
43
61
|
pi.on("session_start", async (_event, ctx) => {
|
|
44
|
-
|
|
62
|
+
store.theme = ctx.ui.theme;
|
|
63
|
+
await pruneStaleHiddenSkills(pi, ctx.cwd);
|
|
45
64
|
});
|
|
46
65
|
|
|
47
66
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
@@ -90,34 +109,45 @@ export default function skillVisibility(pi: ExtensionAPI) {
|
|
|
90
109
|
});
|
|
91
110
|
}
|
|
92
111
|
|
|
112
|
+
async function pruneStaleHiddenSkills(pi: ExtensionAPI, cwd: string): Promise<void> {
|
|
113
|
+
const installedNames = new Set(getSkillItems(pi).map((s) => s.name));
|
|
114
|
+
const scoped = await readScopedSkillfulSettings(cwd);
|
|
115
|
+
|
|
116
|
+
await Promise.all(
|
|
117
|
+
SCOPES.map(async (scope) => {
|
|
118
|
+
const current = scoped[scope].hiddenSkills;
|
|
119
|
+
const pruned = current.filter((name) => installedNames.has(name));
|
|
120
|
+
if (pruned.length < current.length) {
|
|
121
|
+
await writeHiddenSkills(scope, cwd, pruned);
|
|
122
|
+
scoped[scope] = { hiddenSkills: pruned };
|
|
123
|
+
}
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const hidden = new Set([...scoped.global.hiddenSkills, ...scoped.project.hiddenSkills]);
|
|
128
|
+
store.hiddenSkillsByCwd.set(cwd, hidden);
|
|
129
|
+
store.lastHiddenSkills = hidden;
|
|
130
|
+
}
|
|
131
|
+
|
|
93
132
|
async function refreshHiddenSkillCache(cwd: string): Promise<Set<string>> {
|
|
94
133
|
const hidden = await readEffectiveHiddenSkills(cwd);
|
|
95
|
-
|
|
96
|
-
|
|
134
|
+
store.hiddenSkillsByCwd.set(cwd, hidden);
|
|
135
|
+
store.lastHiddenSkills = hidden;
|
|
97
136
|
return hidden;
|
|
98
137
|
}
|
|
99
138
|
|
|
139
|
+
// Must patch the real prototype from pi's module — separate module resolutions have distinct class identities.
|
|
100
140
|
function installStartupSkillListPatch(): void {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
};
|
|
104
|
-
type InteractiveModeWithStartupResources = {
|
|
105
|
-
showLoadedResources?: (options?: unknown) => void;
|
|
106
|
-
session?: { resourceLoader?: StartupResourceLoader };
|
|
107
|
-
sessionManager?: { getCwd?: () => string };
|
|
108
|
-
};
|
|
141
|
+
const realPrototype = (InteractiveMode as unknown as { prototype: InteractiveModeLike }).prototype;
|
|
142
|
+
if (!realPrototype) return;
|
|
109
143
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
if (patchState[STARTUP_SKILL_LIST_PATCH_KEY]) return;
|
|
144
|
+
const patchState = realPrototype as Record<PropertyKey, unknown>;
|
|
145
|
+
if (patchState[STARTUP_PATCH_KEY]) return;
|
|
113
146
|
|
|
114
|
-
const original =
|
|
147
|
+
const original = realPrototype.showLoadedResources;
|
|
115
148
|
if (typeof original !== "function") return;
|
|
116
149
|
|
|
117
|
-
|
|
118
|
-
this: InteractiveModeWithStartupResources,
|
|
119
|
-
options?: unknown,
|
|
120
|
-
): void {
|
|
150
|
+
realPrototype.showLoadedResources = function (this: InteractiveModeLike, options?: unknown): void {
|
|
121
151
|
const loader = this.session?.resourceLoader;
|
|
122
152
|
const originalGetSkills = loader?.getSkills;
|
|
123
153
|
if (!loader || typeof originalGetSkills !== "function") {
|
|
@@ -126,31 +156,48 @@ function installStartupSkillListPatch(): void {
|
|
|
126
156
|
}
|
|
127
157
|
|
|
128
158
|
const cwd = this.sessionManager?.getCwd?.();
|
|
129
|
-
const hidden = (cwd ? skillVisibilityStore.hiddenSkillsByCwd.get(cwd) : undefined) ?? skillVisibilityStore.lastHiddenSkills;
|
|
130
159
|
|
|
160
|
+
let rawSkillNames: string[] = [];
|
|
131
161
|
loader.getSkills = () => {
|
|
132
162
|
const result = originalGetSkills.call(loader);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
skills: result.skills.map((skill) => ({
|
|
136
|
-
...skill,
|
|
137
|
-
name: formatStartupSkillName(skill.name, hidden.has(skill.name)),
|
|
138
|
-
})),
|
|
139
|
-
};
|
|
163
|
+
rawSkillNames = result.skills.map((s) => normalizeSkillName(s.name));
|
|
164
|
+
return result;
|
|
140
165
|
};
|
|
141
166
|
|
|
167
|
+
const childrenBefore = this.chatContainer?.children.length ?? 0;
|
|
168
|
+
|
|
142
169
|
try {
|
|
143
170
|
original.call(this, options);
|
|
144
171
|
} finally {
|
|
145
172
|
loader.getSkills = originalGetSkills;
|
|
146
173
|
}
|
|
174
|
+
|
|
175
|
+
if (rawSkillNames.length === 0 || !cwd || !this.chatContainer) return;
|
|
176
|
+
|
|
177
|
+
const children = this.chatContainer.children;
|
|
178
|
+
for (let i = childrenBefore; i < children.length; i++) {
|
|
179
|
+
const child = children[i] as ExpandableTextLike | undefined;
|
|
180
|
+
if (!child || typeof child.getCollapsedText !== "function") continue;
|
|
181
|
+
const collapsed = child.getCollapsedText();
|
|
182
|
+
if (!collapsed.includes("[Skills]")) continue;
|
|
183
|
+
|
|
184
|
+
child.getCollapsedText = () => buildColorizedSkillList(rawSkillNames, store.lastHiddenSkills, store.theme);
|
|
185
|
+
child.setText(child.getCollapsedText());
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
147
188
|
};
|
|
148
189
|
|
|
149
|
-
patchState[
|
|
190
|
+
patchState[STARTUP_PATCH_KEY] = true;
|
|
150
191
|
}
|
|
151
192
|
|
|
152
|
-
function
|
|
153
|
-
|
|
193
|
+
function buildColorizedSkillList(names: string[], hidden: Set<string>, theme: Theme | null): string {
|
|
194
|
+
const sorted = [...names].sort((a, b) => a.localeCompare(b));
|
|
195
|
+
if (!theme) {
|
|
196
|
+
return `[Skills]\n ${sorted.join(", ")}`;
|
|
197
|
+
}
|
|
198
|
+
const header = theme.fg("mdHeading", "[Skills]");
|
|
199
|
+
const parts = sorted.map((n) => (hidden.has(n) ? theme.fg("error", n) : theme.fg("dim", n)));
|
|
200
|
+
return `${header}\n ${parts.join(", ")}`;
|
|
154
201
|
}
|
|
155
202
|
|
|
156
203
|
function getSkillItems(pi: ExtensionAPI): SkillListItem[] {
|