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 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 skill-list annotations for visible vs hidden skills.
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 is also annotated: visible skills are marked with a green dot, and hidden skills are marked with a dim red dot.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-skillful",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Pi package with skill invocation and visibility improvements.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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 STARTUP_SKILL_LIST_PATCH_KEY = Symbol.for("pi-skillful.startupSkillListPatchInstalled");
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
- const skillVisibilityStore = (((globalThis as Record<PropertyKey, unknown>)[STORE_KEY] as SkillVisibilityStore | undefined) ??= {
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
- skillVisibilityStore.hiddenSkillsByCwd.set(cwd, hidden);
96
- skillVisibilityStore.lastHiddenSkills = hidden;
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
- type StartupResourceLoader = {
102
- getSkills: () => { skills: Skill[]; diagnostics: unknown[] };
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 prototype = (InteractiveMode as unknown as { prototype: InteractiveModeWithStartupResources }).prototype;
111
- const patchState = prototype as InteractiveModeWithStartupResources & Record<PropertyKey, unknown>;
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 = prototype.showLoadedResources;
127
+ const original = realPrototype.showLoadedResources;
115
128
  if (typeof original !== "function") return;
116
129
 
117
- prototype.showLoadedResources = function showLoadedResourcesWithSkillfulVisibility(
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
- return {
134
- ...result,
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[STARTUP_SKILL_LIST_PATCH_KEY] = true;
170
+ patchState[STARTUP_PATCH_KEY] = true;
150
171
  }
151
172
 
152
- function formatStartupSkillName(name: string, hidden: boolean): string {
153
- return hidden ? `${name} \x1b[31;2m●\x1b[22;39m` : `${name} \x1b[32m●\x1b[39m`;
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[] {