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 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
- - Remove the private `InteractiveMode.prototype` startup skill-list patch; visibility now uses only documented Pi APIs.
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
- `pi-skillful` is a [Pi](https://pi.dev) package that improves skill workflows.
7
+ Make [Pi](https://pi.dev) skills easier to invoke, curate, and control without editing their source files.
8
8
 
9
- It currently provides three extensions:
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,6 @@
1
1
  {
2
2
  "name": "pi-skillful",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
4
4
  "description": "Pi package with skill invocation and visibility improvements.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,14 @@
1
- import { DynamicBorder, getSettingsListTheme, type ExtensionAPI, type Skill, type Theme } from "@earendil-works/pi-coding-agent";
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
- await readEffectiveHiddenSkills(ctx.cwd, ctx.isProjectTrusted());
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 readEffectiveHiddenSkills(ctx.cwd, ctx.isProjectTrusted());
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 readEffectiveHiddenSkills(this.cwd, this.projectTrusted);
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");