pi-skillful 0.3.13 → 0.3.14
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 +3 -2
- package/package.json +1 -1
- package/src/extensions/skill-visibility.ts +113 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.14 - 2026-07-17
|
|
4
|
+
|
|
3
5
|
## 0.3.13 - 2026-07-17
|
|
4
6
|
|
|
5
7
|
## 0.3.12 - 2026-07-12
|
|
@@ -20,7 +22,7 @@ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com
|
|
|
20
22
|
- Let an explicit project `toggleModifier: "alt"` override a non-default global modifier; unsupported explicit values now follow the documented `"alt"` fallback.
|
|
21
23
|
- Consume only configured modifier-slot combinations in the prompt editor instead of registering all 54 possible shortcuts.
|
|
22
24
|
- Propagate focus and custom-editor hooks through the session-toggle editor wrapper for IME and extension compatibility.
|
|
23
|
-
-
|
|
25
|
+
- Restore red/dim coloring for hidden and visible skills in Pi's startup `[Skills]` list while preserving project-trust-aware settings.
|
|
24
26
|
- Remove the stale package-local `package-lock.json` in favor of the monorepo root lockfile.
|
|
25
27
|
- Preserve configured hidden skills at session start. Skillful no longer treats a temporarily incomplete loaded-skill list as authoritative and removes global or project visibility settings.
|
|
26
28
|
|
package/README.md
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
# pi-skillful
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Make [Pi](https://pi.dev) skills easier to invoke, curate, and control without editing their source files.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
`pi-skillful` provides three focused upgrades:
|
|
10
10
|
|
|
11
11
|
- **Inline skill invocation**: invoke one or more skills anywhere in a prompt with `/skill:name`.
|
|
12
12
|
- **Skill prompt visibility**: choose which skills are hidden from the model's automatic skill-discovery prompt while keeping them explicitly invokable.
|
|
@@ -33,6 +33,7 @@ Hide skills from the `<available_skills>` section of the system prompt without e
|
|
|
33
33
|
|
|
34
34
|
Hidden skills:
|
|
35
35
|
|
|
36
|
+
- appear in the startup `[Skills]` list in the theme's error color, while visible skills appear dimmed;
|
|
36
37
|
- are not advertised to the model for automatic skill selection;
|
|
37
38
|
- remain loaded by Pi;
|
|
38
39
|
- remain available for explicit invocation with `/skill:name`, including inline invocation.
|
package/package.json
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
DynamicBorder,
|
|
3
|
+
getSettingsListTheme,
|
|
4
|
+
InteractiveMode,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type Skill,
|
|
7
|
+
type Theme,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
9
|
import { type Component, Key, matchesKey, type SettingItem, SettingsList, truncateToWidth, type TUI } from "@earendil-works/pi-tui";
|
|
3
10
|
import {
|
|
11
|
+
normalizeSkillName,
|
|
4
12
|
normalizeSkillNames,
|
|
5
13
|
readEffectiveHiddenSkills,
|
|
6
14
|
readScopedSkillfulSettings,
|
|
@@ -15,6 +23,34 @@ import { replaceSkillsSection } from "../skill-prompt.js";
|
|
|
15
23
|
import { isTopLevelSkill, listLoadedSkills, type LoadedSkillInfo } from "../skills.js";
|
|
16
24
|
import { hasActiveSessionSkillToggles, refreshSessionSkillToggles } from "./session-skill-toggles.js";
|
|
17
25
|
const SCOPES: SkillfulScope[] = ["global", "project"];
|
|
26
|
+
const STORE_KEY = Symbol.for("pi-skillful.skillVisibilityStore");
|
|
27
|
+
const STARTUP_PATCH_KEY = Symbol.for("pi-skillful.startupPatchV3");
|
|
28
|
+
|
|
29
|
+
interface SkillVisibilityStore {
|
|
30
|
+
hiddenSkillsByCwd: Map<string, Set<string>>;
|
|
31
|
+
theme: Theme | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ExpandableTextLike {
|
|
35
|
+
getCollapsedText: () => string;
|
|
36
|
+
setText: (text: string) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface BoxLike {
|
|
40
|
+
children: unknown[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface InteractiveModeLike {
|
|
44
|
+
loadedResourcesContainer?: BoxLike;
|
|
45
|
+
showLoadedResources?: (options?: unknown) => void;
|
|
46
|
+
session?: { resourceLoader?: { getSkills: () => { skills: Skill[]; diagnostics: unknown[] } } };
|
|
47
|
+
sessionManager?: { getCwd?: () => string };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const store = (((globalThis as Record<PropertyKey, unknown>)[STORE_KEY] as SkillVisibilityStore | undefined) ??= {
|
|
51
|
+
hiddenSkillsByCwd: new Map<string, Set<string>>(),
|
|
52
|
+
theme: null,
|
|
53
|
+
}) as SkillVisibilityStore;
|
|
18
54
|
|
|
19
55
|
type SkillListItem = LoadedSkillInfo;
|
|
20
56
|
type HiddenSkillsByScope = Record<SkillfulScope, Set<string>>;
|
|
@@ -44,14 +80,17 @@ interface SettingsListSelectionView {
|
|
|
44
80
|
}
|
|
45
81
|
|
|
46
82
|
export default function skillVisibility(pi: ExtensionAPI) {
|
|
83
|
+
installStartupSkillListPatch();
|
|
84
|
+
|
|
47
85
|
pi.on("session_start", async (_event, ctx) => {
|
|
48
|
-
|
|
86
|
+
store.theme = ctx.ui.theme;
|
|
87
|
+
await refreshHiddenSkillCache(ctx.cwd, ctx.isProjectTrusted());
|
|
49
88
|
});
|
|
50
89
|
|
|
51
90
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
52
91
|
if (hasActiveSessionSkillToggles()) return;
|
|
53
92
|
|
|
54
|
-
const hidden = await
|
|
93
|
+
const hidden = await refreshHiddenSkillCache(ctx.cwd, ctx.isProjectTrusted());
|
|
55
94
|
if (hidden.size === 0 || !event.systemPromptOptions.skills?.length) return;
|
|
56
95
|
|
|
57
96
|
const filteredSkills: Skill[] = event.systemPromptOptions.skills.map((skill) =>
|
|
@@ -110,6 +149,76 @@ export default function skillVisibility(pi: ExtensionAPI) {
|
|
|
110
149
|
});
|
|
111
150
|
}
|
|
112
151
|
|
|
152
|
+
async function refreshHiddenSkillCache(cwd: string, projectTrusted: boolean): Promise<Set<string>> {
|
|
153
|
+
const hidden = await readEffectiveHiddenSkills(cwd, projectTrusted);
|
|
154
|
+
store.hiddenSkillsByCwd.set(cwd, hidden);
|
|
155
|
+
return hidden;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Pi has no public hook for styling the built-in startup resource list, so patch the
|
|
159
|
+
// prototype from Pi's own module and keep the workaround isolated here.
|
|
160
|
+
export function installStartupSkillListPatch(
|
|
161
|
+
realPrototype: InteractiveModeLike = (InteractiveMode as unknown as { prototype: InteractiveModeLike }).prototype,
|
|
162
|
+
): void {
|
|
163
|
+
if (!realPrototype) return;
|
|
164
|
+
|
|
165
|
+
const patchState = realPrototype as Record<PropertyKey, unknown>;
|
|
166
|
+
if (patchState[STARTUP_PATCH_KEY]) return;
|
|
167
|
+
|
|
168
|
+
const original = realPrototype.showLoadedResources;
|
|
169
|
+
if (typeof original !== "function") return;
|
|
170
|
+
|
|
171
|
+
realPrototype.showLoadedResources = function (this: InteractiveModeLike, options?: unknown): void {
|
|
172
|
+
const loader = this.session?.resourceLoader;
|
|
173
|
+
const originalGetSkills = loader?.getSkills;
|
|
174
|
+
if (!loader || typeof originalGetSkills !== "function") {
|
|
175
|
+
original.call(this, options);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const cwd = this.sessionManager?.getCwd?.();
|
|
180
|
+
let rawSkillNames: string[] = [];
|
|
181
|
+
loader.getSkills = () => {
|
|
182
|
+
const result = originalGetSkills.call(loader);
|
|
183
|
+
rawSkillNames = result.skills.map((skill) => normalizeSkillName(skill.name));
|
|
184
|
+
return result;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const childrenBefore = this.loadedResourcesContainer?.children.length ?? 0;
|
|
188
|
+
try {
|
|
189
|
+
original.call(this, options);
|
|
190
|
+
} finally {
|
|
191
|
+
loader.getSkills = originalGetSkills;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (rawSkillNames.length === 0 || !cwd || !this.loadedResourcesContainer) return;
|
|
195
|
+
|
|
196
|
+
const hidden = store.hiddenSkillsByCwd.get(cwd) ?? new Set<string>();
|
|
197
|
+
const children = this.loadedResourcesContainer.children;
|
|
198
|
+
for (let index = childrenBefore; index < children.length; index++) {
|
|
199
|
+
const child = children[index] as ExpandableTextLike | undefined;
|
|
200
|
+
if (!child || typeof child.getCollapsedText !== "function") continue;
|
|
201
|
+
if (!child.getCollapsedText().includes("[Skills]")) continue;
|
|
202
|
+
|
|
203
|
+
const colorized = buildColorizedSkillList(rawSkillNames, hidden, store.theme);
|
|
204
|
+
child.getCollapsedText = () => colorized;
|
|
205
|
+
child.setText(colorized);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
patchState[STARTUP_PATCH_KEY] = true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function buildColorizedSkillList(names: string[], hidden: Set<string>, theme: Theme | null): string {
|
|
214
|
+
const sorted = [...names].sort((a, b) => a.localeCompare(b));
|
|
215
|
+
if (!theme) return `[Skills]\n ${sorted.join(", ")}`;
|
|
216
|
+
|
|
217
|
+
const header = theme.fg("mdHeading", "[Skills]");
|
|
218
|
+
const parts = sorted.map((name) => (hidden.has(name) ? theme.fg("error", name) : theme.fg("dim", name)));
|
|
219
|
+
return `${header}\n ${parts.join(", ")}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
113
222
|
function getSkillItems(pi: ExtensionAPI): SkillListItem[] {
|
|
114
223
|
return listLoadedSkills(pi.getCommands()).filter(isTopLevelSkill);
|
|
115
224
|
}
|
|
@@ -331,7 +440,7 @@ class SkillfulVisibilityMenu implements Component {
|
|
|
331
440
|
.then(async () => {
|
|
332
441
|
if (scope === "project") await this.writeProjectOverrideOrInheritance();
|
|
333
442
|
else await writeHiddenSkills(scope, this.cwd, hiddenSnapshot);
|
|
334
|
-
await
|
|
443
|
+
await refreshHiddenSkillCache(this.cwd, this.projectTrusted);
|
|
335
444
|
})
|
|
336
445
|
.catch((error) => {
|
|
337
446
|
this.notify(`Failed to save ${scope} skill visibility: ${error instanceof Error ? error.message : String(error)}`, "error");
|