pi-skillful 0.1.0 → 0.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/CHANGELOG.md +3 -1
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/extensions/skill-visibility.ts +58 -31
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,6 +59,7 @@ export default function skillVisibility(pi: ExtensionAPI) {
|
|
|
41
59
|
installStartupSkillListPatch();
|
|
42
60
|
|
|
43
61
|
pi.on("session_start", async (_event, ctx) => {
|
|
62
|
+
store.theme = ctx.ui.theme;
|
|
44
63
|
await refreshHiddenSkillCache(ctx.cwd);
|
|
45
64
|
});
|
|
46
65
|
|
|
@@ -92,32 +111,23 @@ export default function skillVisibility(pi: ExtensionAPI) {
|
|
|
92
111
|
|
|
93
112
|
async function refreshHiddenSkillCache(cwd: string): Promise<Set<string>> {
|
|
94
113
|
const hidden = await readEffectiveHiddenSkills(cwd);
|
|
95
|
-
|
|
96
|
-
|
|
114
|
+
store.hiddenSkillsByCwd.set(cwd, hidden);
|
|
115
|
+
store.lastHiddenSkills = hidden;
|
|
97
116
|
return hidden;
|
|
98
117
|
}
|
|
99
118
|
|
|
119
|
+
// Must patch the real prototype from pi's module — separate module resolutions have distinct class identities.
|
|
100
120
|
function installStartupSkillListPatch(): void {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
};
|
|
104
|
-
type InteractiveModeWithStartupResources = {
|
|
105
|
-
showLoadedResources?: (options?: unknown) => void;
|
|
106
|
-
session?: { resourceLoader?: StartupResourceLoader };
|
|
107
|
-
sessionManager?: { getCwd?: () => string };
|
|
108
|
-
};
|
|
121
|
+
const realPrototype = (InteractiveMode as unknown as { prototype: InteractiveModeLike }).prototype;
|
|
122
|
+
if (!realPrototype) return;
|
|
109
123
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
if (patchState[STARTUP_SKILL_LIST_PATCH_KEY]) return;
|
|
124
|
+
const patchState = realPrototype as Record<PropertyKey, unknown>;
|
|
125
|
+
if (patchState[STARTUP_PATCH_KEY]) return;
|
|
113
126
|
|
|
114
|
-
const original =
|
|
127
|
+
const original = realPrototype.showLoadedResources;
|
|
115
128
|
if (typeof original !== "function") return;
|
|
116
129
|
|
|
117
|
-
|
|
118
|
-
this: InteractiveModeWithStartupResources,
|
|
119
|
-
options?: unknown,
|
|
120
|
-
): void {
|
|
130
|
+
realPrototype.showLoadedResources = function (this: InteractiveModeLike, options?: unknown): void {
|
|
121
131
|
const loader = this.session?.resourceLoader;
|
|
122
132
|
const originalGetSkills = loader?.getSkills;
|
|
123
133
|
if (!loader || typeof originalGetSkills !== "function") {
|
|
@@ -126,31 +136,48 @@ function installStartupSkillListPatch(): void {
|
|
|
126
136
|
}
|
|
127
137
|
|
|
128
138
|
const cwd = this.sessionManager?.getCwd?.();
|
|
129
|
-
const hidden = (cwd ? skillVisibilityStore.hiddenSkillsByCwd.get(cwd) : undefined) ?? skillVisibilityStore.lastHiddenSkills;
|
|
130
139
|
|
|
140
|
+
let rawSkillNames: string[] = [];
|
|
131
141
|
loader.getSkills = () => {
|
|
132
142
|
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
|
-
};
|
|
143
|
+
rawSkillNames = result.skills.map((s) => normalizeSkillName(s.name));
|
|
144
|
+
return result;
|
|
140
145
|
};
|
|
141
146
|
|
|
147
|
+
const childrenBefore = this.chatContainer?.children.length ?? 0;
|
|
148
|
+
|
|
142
149
|
try {
|
|
143
150
|
original.call(this, options);
|
|
144
151
|
} finally {
|
|
145
152
|
loader.getSkills = originalGetSkills;
|
|
146
153
|
}
|
|
154
|
+
|
|
155
|
+
if (rawSkillNames.length === 0 || !cwd || !this.chatContainer) return;
|
|
156
|
+
|
|
157
|
+
const children = this.chatContainer.children;
|
|
158
|
+
for (let i = childrenBefore; i < children.length; i++) {
|
|
159
|
+
const child = children[i] as ExpandableTextLike | undefined;
|
|
160
|
+
if (!child || typeof child.getCollapsedText !== "function") continue;
|
|
161
|
+
const collapsed = child.getCollapsedText();
|
|
162
|
+
if (!collapsed.includes("[Skills]")) continue;
|
|
163
|
+
|
|
164
|
+
child.getCollapsedText = () => buildColorizedSkillList(rawSkillNames, store.lastHiddenSkills, store.theme);
|
|
165
|
+
child.setText(child.getCollapsedText());
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
147
168
|
};
|
|
148
169
|
|
|
149
|
-
patchState[
|
|
170
|
+
patchState[STARTUP_PATCH_KEY] = true;
|
|
150
171
|
}
|
|
151
172
|
|
|
152
|
-
function
|
|
153
|
-
|
|
173
|
+
function buildColorizedSkillList(names: string[], hidden: Set<string>, theme: Theme | null): string {
|
|
174
|
+
const sorted = [...names].sort((a, b) => a.localeCompare(b));
|
|
175
|
+
if (!theme) {
|
|
176
|
+
return `[Skills]\n ${sorted.join(", ")}`;
|
|
177
|
+
}
|
|
178
|
+
const header = theme.fg("mdHeading", "[Skills]");
|
|
179
|
+
const parts = sorted.map((n) => (hidden.has(n) ? theme.fg("error", n) : theme.fg("dim", n)));
|
|
180
|
+
return `${header}\n ${parts.join(", ")}`;
|
|
154
181
|
}
|
|
155
182
|
|
|
156
183
|
function getSkillItems(pi: ExtensionAPI): SkillListItem[] {
|