pi-skillful 0.3.5 → 0.3.7
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 +14 -0
- package/CONTRIBUTING.md +2 -2
- package/README.md +11 -11
- package/package.json +5 -4
- package/src/config.ts +81 -23
- package/src/extensions/session-skill-toggles.ts +38 -6
- package/src/extensions/skill-visibility.ts +215 -28
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,20 @@ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.7] - 2026-05-20
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Moved package source to the `jvm/pi-mono` monorepo.
|
|
14
|
+
- Updated npm metadata to point at the monorepo package directory.
|
|
15
|
+
|
|
16
|
+
## [0.3.6] - 2026-05-12
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Made project skill visibility and toggle slots inherit global settings until the project scope is explicitly changed.
|
|
21
|
+
- Added `/skillful` menu support for assigning session toggle slots in global or project scope.
|
|
22
|
+
|
|
9
23
|
## [0.3.5] - 2026-05-12
|
|
10
24
|
|
|
11
25
|
### Fixed
|
package/CONTRIBUTING.md
CHANGED
|
@@ -18,7 +18,7 @@ Install this checkout into a temporary Pi project:
|
|
|
18
18
|
```bash
|
|
19
19
|
mkdir -p <test-project>
|
|
20
20
|
cd <test-project>
|
|
21
|
-
pi install -l /path/to/pi-skillful
|
|
21
|
+
pi install -l /path/to/pi-mono/packages/pi-skillful
|
|
22
22
|
pi
|
|
23
23
|
```
|
|
24
24
|
|
|
@@ -27,7 +27,7 @@ Then run `/skillful` inside Pi.
|
|
|
27
27
|
For a one-off run without changing settings:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
pi -e /path/to/pi-
|
|
30
|
+
pi -e /path/to/pi-mono/packages/pi-skillful
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
## Pull request checklist
|
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<p>
|
|
2
|
-
<img src="
|
|
2
|
+
<img src="./banner.png" alt="pi-skillful" width="1100">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
# pi-skillful
|
|
6
6
|
|
|
7
|
-
`pi-skillful` is a [Pi](https://
|
|
7
|
+
`pi-skillful` is a [Pi](https://pi.dev) package that improves skill workflows.
|
|
8
8
|
|
|
9
9
|
It currently provides three extensions:
|
|
10
10
|
|
|
@@ -52,7 +52,7 @@ Supported scopes:
|
|
|
52
52
|
- Global: `~/.pi/agent/settings.json`
|
|
53
53
|
- Project: `.pi/settings.json`
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
Project visibility and toggle slots inherit global settings until changed in the Project tab. When either is changed, Pi Skillful writes a full project override containing both `hiddenSkills` and `toggleSlots`. If the project state is changed back to match global, those project override keys are removed so the project inherits global again.
|
|
56
56
|
|
|
57
57
|
Open the menu with:
|
|
58
58
|
|
|
@@ -60,7 +60,7 @@ Open the menu with:
|
|
|
60
60
|
/skillful
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
The menu lists all loaded skills alphabetically. Toggle a skill off
|
|
63
|
+
The menu lists all loaded skills alphabetically. Toggle a skill off or on in the active scope. Use the Global/Project tabs to choose which settings file to edit. In the Project tab, inherited on/off values are shown normally; project overrides are highlighted. Press `1` through `9` on a selected skill to assign or clear that scope's session toggle slot. Visibility and toggle slots are independent.
|
|
64
64
|
|
|
65
65
|
Pi's startup `[Skills]` list also highlights hidden skills in the error color (red in the default dark theme).
|
|
66
66
|
|
|
@@ -84,7 +84,7 @@ Assign skills to up to nine prompt-editor slots with JSON settings:
|
|
|
84
84
|
}
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
Configured slots appear on the prompt editor's top border as `N skill-name`. Long names are truncated per slot when needed so all configured slot numbers remain visible. Active slots use the theme accent color; inactive slots use the muted color. Press `alt+1` through `alt+9` by default to toggle a slot for the current session only.
|
|
87
|
+
Configured slots appear on the prompt editor's top border as `N skill-name`. Project `toggleSlots`, when defined as part of a project override, replace global `toggleSlots`; otherwise global slots are used and shown in the Project tab. Long names are truncated per slot when needed so all configured slot numbers remain visible. Active slots use the theme accent color; inactive slots use the muted color. Press `alt+1` through `alt+9` by default to toggle a slot for the current session only.
|
|
88
88
|
|
|
89
89
|
`toggleModifier` defaults to `"alt"`. Supported values are `"alt"`, `"ctrl"`, `"ctrl+shift"`, `"alt+shift"`, `"ctrl+alt"`, and `"ctrl+alt+shift"`. Change it if your terminal reserves `alt+number` shortcuts.
|
|
90
90
|
|
|
@@ -92,28 +92,28 @@ On app startup, non-hidden skills are active and hidden skills are inactive. Wit
|
|
|
92
92
|
|
|
93
93
|
## Installation
|
|
94
94
|
|
|
95
|
-
Install from
|
|
95
|
+
Install from npm:
|
|
96
96
|
|
|
97
97
|
```bash
|
|
98
|
-
pi install
|
|
98
|
+
pi install npm:pi-skillful
|
|
99
99
|
```
|
|
100
100
|
|
|
101
101
|
Install project-locally with Pi's `-l` flag:
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
|
-
pi install -l
|
|
104
|
+
pi install -l npm:pi-skillful
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
During local development from this
|
|
107
|
+
During local development from this monorepo:
|
|
108
108
|
|
|
109
109
|
```bash
|
|
110
|
-
pi install /path/to/pi-skillful
|
|
110
|
+
pi install /path/to/pi-mono/packages/pi-skillful
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
For a one-off test run without installing:
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
|
-
pi -e /path/to/pi-
|
|
116
|
+
pi -e /path/to/pi-mono/packages/pi-skillful
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
## Usage
|
package/package.json
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-skillful",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "Pi package with skill invocation and visibility improvements.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "jvm",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/jvm/pi-
|
|
10
|
+
"url": "git+https://github.com/jvm/pi-mono.git",
|
|
11
|
+
"directory": "packages/pi-skillful"
|
|
11
12
|
},
|
|
12
13
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/jvm/pi-
|
|
14
|
+
"url": "https://github.com/jvm/pi-mono/issues"
|
|
14
15
|
},
|
|
15
|
-
"homepage": "https://github.com/jvm/pi-skillful#readme",
|
|
16
|
+
"homepage": "https://github.com/jvm/pi-mono/tree/main/packages/pi-skillful#readme",
|
|
16
17
|
"keywords": [
|
|
17
18
|
"pi-package",
|
|
18
19
|
"pi-extension",
|
package/src/config.ts
CHANGED
|
@@ -21,6 +21,9 @@ export interface SkillToggleConfig {
|
|
|
21
21
|
|
|
22
22
|
export interface SkillfulSettings extends SkillToggleConfig {
|
|
23
23
|
hiddenSkills: string[];
|
|
24
|
+
hiddenSkillsDefined: boolean;
|
|
25
|
+
visibleSkills: string[];
|
|
26
|
+
toggleSlotsDefined: boolean;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export interface EffectiveSkillfulSettings extends SkillfulSettings {
|
|
@@ -107,17 +110,7 @@ async function readSettingsDocument(path: string): Promise<PiSettingsDocument> {
|
|
|
107
110
|
|
|
108
111
|
export async function readSkillfulSettings(path: string): Promise<SkillfulSettings> {
|
|
109
112
|
const settings = await readSettingsDocument(path);
|
|
110
|
-
|
|
111
|
-
const skillfulObject = skillful && typeof skillful === "object" && !Array.isArray(skillful) ? skillful : {};
|
|
112
|
-
const hiddenSkills = Array.isArray(skillfulObject.hiddenSkills)
|
|
113
|
-
? skillfulObject.hiddenSkills.filter((name): name is string => typeof name === "string")
|
|
114
|
-
: [];
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
hiddenSkills: normalizeSkillNames(hiddenSkills),
|
|
118
|
-
toggleSlots: normalizeToggleSlots(skillfulObject.toggleSlots),
|
|
119
|
-
toggleModifier: normalizeToggleModifier(skillfulObject.toggleModifier),
|
|
120
|
-
};
|
|
113
|
+
return settingsFromRecord(settings[SKILLFUL_SETTINGS_KEY]);
|
|
121
114
|
}
|
|
122
115
|
|
|
123
116
|
export async function readScopedSkillfulSettings(cwd: string): Promise<Record<SkillfulScope, SkillfulSettings>> {
|
|
@@ -130,17 +123,20 @@ export async function readScopedSkillfulSettings(cwd: string): Promise<Record<Sk
|
|
|
130
123
|
|
|
131
124
|
export async function readEffectiveHiddenSkills(cwd: string): Promise<Set<string>> {
|
|
132
125
|
const scoped = await readScopedSkillfulSettings(cwd);
|
|
133
|
-
return
|
|
126
|
+
return effectiveHiddenSkillSet(scoped);
|
|
134
127
|
}
|
|
135
128
|
|
|
136
129
|
export async function readEffectiveSkillfulSettings(cwd: string): Promise<EffectiveSkillfulSettings> {
|
|
137
130
|
const scoped = await readScopedSkillfulSettings(cwd);
|
|
138
|
-
const hiddenSkills = normalizeSkillNames(
|
|
139
|
-
const toggleSlots =
|
|
131
|
+
const hiddenSkills = normalizeSkillNames(effectiveHiddenSkillSet(scoped));
|
|
132
|
+
const toggleSlots = scoped.project.toggleSlotsDefined ? scoped.project.toggleSlots : scoped.global.toggleSlots;
|
|
140
133
|
return {
|
|
141
134
|
hiddenSkills,
|
|
135
|
+
hiddenSkillsDefined: scoped.global.hiddenSkillsDefined || scoped.project.hiddenSkillsDefined,
|
|
136
|
+
visibleSkills: [],
|
|
142
137
|
hiddenSkillSet: new Set(hiddenSkills),
|
|
143
138
|
toggleSlots,
|
|
139
|
+
toggleSlotsDefined: scoped.global.toggleSlotsDefined || scoped.project.toggleSlotsDefined,
|
|
144
140
|
toggleModifier:
|
|
145
141
|
scoped.project.toggleModifier !== DEFAULT_TOGGLE_MODIFIER ? scoped.project.toggleModifier : scoped.global.toggleModifier,
|
|
146
142
|
};
|
|
@@ -150,28 +146,69 @@ export async function writeHiddenSkills(
|
|
|
150
146
|
scope: SkillfulScope,
|
|
151
147
|
cwd: string,
|
|
152
148
|
hiddenSkills: Iterable<string>,
|
|
149
|
+
): Promise<SkillfulSettings> {
|
|
150
|
+
return updateSkillfulSettings(scope, cwd, (current) => {
|
|
151
|
+
current.hiddenSkills = normalizeSkillNames(hiddenSkills);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function writeSkillVisibility(
|
|
156
|
+
scope: SkillfulScope,
|
|
157
|
+
cwd: string,
|
|
158
|
+
hiddenSkills: Iterable<string>,
|
|
159
|
+
visibleSkills: Iterable<string> = [],
|
|
160
|
+
): Promise<SkillfulSettings> {
|
|
161
|
+
return updateSkillfulSettings(scope, cwd, (current) => {
|
|
162
|
+
const visible = normalizeSkillNames(visibleSkills);
|
|
163
|
+
current.hiddenSkills = normalizeSkillNames(hiddenSkills);
|
|
164
|
+
if (visible.length === 0) delete current.visibleSkills;
|
|
165
|
+
else current.visibleSkills = visible;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function writeToggleSlots(
|
|
170
|
+
scope: SkillfulScope,
|
|
171
|
+
cwd: string,
|
|
172
|
+
toggleSlots: Partial<Record<SkillToggleSlot, string>>,
|
|
173
|
+
): Promise<SkillfulSettings> {
|
|
174
|
+
return updateSkillfulSettings(scope, cwd, (current) => {
|
|
175
|
+
current.toggleSlots = normalizeToggleSlots(toggleSlots);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function writeProjectSkillfulOverride(
|
|
180
|
+
cwd: string,
|
|
181
|
+
hiddenSkills: Iterable<string> | undefined,
|
|
182
|
+
toggleSlots: Partial<Record<SkillToggleSlot, string>> | undefined,
|
|
183
|
+
): Promise<SkillfulSettings> {
|
|
184
|
+
return updateSkillfulSettings("project", cwd, (current) => {
|
|
185
|
+
delete current.visibleSkills;
|
|
186
|
+
if (hiddenSkills === undefined) delete current.hiddenSkills;
|
|
187
|
+
else current.hiddenSkills = normalizeSkillNames(hiddenSkills);
|
|
188
|
+
if (toggleSlots === undefined) delete current.toggleSlots;
|
|
189
|
+
else current.toggleSlots = normalizeToggleSlots(toggleSlots);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function updateSkillfulSettings(
|
|
194
|
+
scope: SkillfulScope,
|
|
195
|
+
cwd: string,
|
|
196
|
+
updater: (current: Record<string, unknown>) => void,
|
|
153
197
|
): Promise<SkillfulSettings> {
|
|
154
198
|
const path = settingsPath(scope, cwd);
|
|
155
199
|
const document = await readSettingsDocument(path);
|
|
156
|
-
const updated = normalizeSkillNames(hiddenSkills);
|
|
157
200
|
|
|
158
201
|
const existingSkillful =
|
|
159
202
|
document[SKILLFUL_SETTINGS_KEY] && typeof document[SKILLFUL_SETTINGS_KEY] === "object" && !Array.isArray(document[SKILLFUL_SETTINGS_KEY])
|
|
160
203
|
? (document[SKILLFUL_SETTINGS_KEY] as Record<string, unknown>)
|
|
161
204
|
: {};
|
|
162
205
|
const nextSkillful = { ...existingSkillful };
|
|
163
|
-
|
|
164
|
-
if (updated.length === 0) delete nextSkillful.hiddenSkills;
|
|
165
|
-
else nextSkillful.hiddenSkills = updated;
|
|
206
|
+
updater(nextSkillful);
|
|
166
207
|
|
|
167
208
|
if (Object.keys(nextSkillful).length === 0) delete document[SKILLFUL_SETTINGS_KEY];
|
|
168
209
|
else document[SKILLFUL_SETTINGS_KEY] = nextSkillful as Partial<SkillfulSettings>;
|
|
169
210
|
|
|
170
|
-
const result
|
|
171
|
-
hiddenSkills: updated,
|
|
172
|
-
toggleSlots: normalizeToggleSlots(nextSkillful.toggleSlots),
|
|
173
|
-
toggleModifier: normalizeToggleModifier(nextSkillful.toggleModifier),
|
|
174
|
-
};
|
|
211
|
+
const result = settingsFromRecord(nextSkillful);
|
|
175
212
|
|
|
176
213
|
if (scope === "project" && Object.keys(document).length === 0) {
|
|
177
214
|
await unlinkIfExists(path);
|
|
@@ -193,6 +230,27 @@ export async function updateHiddenSkills(
|
|
|
193
230
|
return writeHiddenSkills(scope, cwd, updater(current.hiddenSkills));
|
|
194
231
|
}
|
|
195
232
|
|
|
233
|
+
function settingsFromRecord(value: unknown): SkillfulSettings {
|
|
234
|
+
const skillful = value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
hiddenSkills: normalizeSkillNames(stringArray(skillful.hiddenSkills)),
|
|
238
|
+
hiddenSkillsDefined: Object.hasOwn(skillful, "hiddenSkills"),
|
|
239
|
+
visibleSkills: normalizeSkillNames(stringArray(skillful.visibleSkills)),
|
|
240
|
+
toggleSlots: normalizeToggleSlots(skillful.toggleSlots),
|
|
241
|
+
toggleSlotsDefined: Object.hasOwn(skillful, "toggleSlots"),
|
|
242
|
+
toggleModifier: normalizeToggleModifier(skillful.toggleModifier),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function stringArray(value: unknown): string[] {
|
|
247
|
+
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function effectiveHiddenSkillSet(scoped: Record<SkillfulScope, SkillfulSettings>): Set<string> {
|
|
251
|
+
return new Set(scoped.project.hiddenSkillsDefined ? scoped.project.hiddenSkills : scoped.global.hiddenSkills);
|
|
252
|
+
}
|
|
253
|
+
|
|
196
254
|
async function unlinkIfExists(path: string): Promise<void> {
|
|
197
255
|
try {
|
|
198
256
|
await unlink(path);
|
|
@@ -64,12 +64,7 @@ export default function sessionSkillToggles(pi: ExtensionAPI) {
|
|
|
64
64
|
|
|
65
65
|
pi.on("session_start", async (event, ctx) => {
|
|
66
66
|
const settings = await readEffectiveSkillfulSettings(ctx.cwd);
|
|
67
|
-
const
|
|
68
|
-
const slots = SKILL_TOGGLE_SLOTS.flatMap((slot): ToggleSlotState[] => {
|
|
69
|
-
const skillName = settings.toggleSlots[slot];
|
|
70
|
-
if (!skillName || !loadedSkillNames.has(skillName)) return [];
|
|
71
|
-
return [{ slot, skillName }];
|
|
72
|
-
});
|
|
67
|
+
const slots = configuredToggleSlots(pi, settings.toggleSlots);
|
|
73
68
|
|
|
74
69
|
const preservedActiveBySkill =
|
|
75
70
|
event.reason === "new" && store.preservedNewSessionActiveBySkill?.cwd === ctx.cwd
|
|
@@ -140,6 +135,43 @@ export function hasActiveSessionSkillToggles(): boolean {
|
|
|
140
135
|
return state.slots.length > 0;
|
|
141
136
|
}
|
|
142
137
|
|
|
138
|
+
export async function refreshSessionSkillToggles(pi: ExtensionAPI, cwd: string, ui: SkillfulUi): Promise<void> {
|
|
139
|
+
const settings = await readEffectiveSkillfulSettings(cwd);
|
|
140
|
+
const slots = configuredToggleSlots(pi, settings.toggleSlots);
|
|
141
|
+
|
|
142
|
+
const previousActiveBySkill = state.activeBySkill;
|
|
143
|
+
state.modifier = settings.toggleModifier;
|
|
144
|
+
state.hiddenSkills = settings.hiddenSkillSet;
|
|
145
|
+
state.slots = slots;
|
|
146
|
+
state.activeBySkill = new Map(
|
|
147
|
+
slots.map(({ skillName }) => [skillName, previousActiveBySkill.get(skillName) ?? !settings.hiddenSkillSet.has(skillName)]),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (slots.length > 0 && !state.installedEditor && !state.installedWidget) {
|
|
151
|
+
installEditor(ui);
|
|
152
|
+
} else if (slots.length === 0) {
|
|
153
|
+
if (state.installedEditor) ui.setEditorComponent(state.previousEditorFactory);
|
|
154
|
+
if (state.installedWidget) ui.setWidget(WIDGET_KEY, undefined);
|
|
155
|
+
state.installedEditor = false;
|
|
156
|
+
state.installedWidget = false;
|
|
157
|
+
state.previousEditorFactory = undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
refreshUi();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function configuredToggleSlots(
|
|
164
|
+
pi: ExtensionAPI,
|
|
165
|
+
toggleSlots: Partial<Record<SkillToggleSlot, string>>,
|
|
166
|
+
): ToggleSlotState[] {
|
|
167
|
+
const loadedSkillNames = new Set(listLoadedSkills(pi.getCommands()).map((skill) => skill.name));
|
|
168
|
+
return SKILL_TOGGLE_SLOTS.flatMap((slot): ToggleSlotState[] => {
|
|
169
|
+
const skillName = toggleSlots[slot];
|
|
170
|
+
if (!skillName || !loadedSkillNames.has(skillName)) return [];
|
|
171
|
+
return [{ slot, skillName }];
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
143
175
|
function isSkillActive(skillName: string): boolean {
|
|
144
176
|
return state.activeBySkill.get(skillName) ?? !state.hiddenSkills.has(skillName);
|
|
145
177
|
}
|
|
@@ -12,12 +12,16 @@ import {
|
|
|
12
12
|
normalizeSkillNames,
|
|
13
13
|
readEffectiveHiddenSkills,
|
|
14
14
|
readScopedSkillfulSettings,
|
|
15
|
+
SKILL_TOGGLE_SLOTS,
|
|
15
16
|
type SkillfulScope,
|
|
17
|
+
type SkillToggleSlot,
|
|
16
18
|
writeHiddenSkills,
|
|
19
|
+
writeProjectSkillfulOverride,
|
|
20
|
+
writeToggleSlots,
|
|
17
21
|
} from "../config.js";
|
|
18
22
|
import { replaceSkillsSection } from "../skill-prompt.js";
|
|
19
23
|
import { listLoadedSkills, type LoadedSkillInfo } from "../skills.js";
|
|
20
|
-
import { hasActiveSessionSkillToggles } from "./session-skill-toggles.js";
|
|
24
|
+
import { hasActiveSessionSkillToggles, refreshSessionSkillToggles } from "./session-skill-toggles.js";
|
|
21
25
|
const SCOPES: SkillfulScope[] = ["global", "project"];
|
|
22
26
|
const STORE_KEY = Symbol.for("pi-skillful.skillVisibilityStore");
|
|
23
27
|
const STARTUP_PATCH_KEY = Symbol.for("pi-skillful.startupPatchV2");
|
|
@@ -51,6 +55,30 @@ const store = (((globalThis as Record<PropertyKey, unknown>)[STORE_KEY] as Skill
|
|
|
51
55
|
}) as SkillVisibilityStore;
|
|
52
56
|
|
|
53
57
|
type SkillListItem = LoadedSkillInfo;
|
|
58
|
+
type HiddenSkillsByScope = Record<SkillfulScope, Set<string>>;
|
|
59
|
+
type ToggleSlotsByScope = Record<SkillfulScope, Partial<Record<SkillToggleSlot, string>>>;
|
|
60
|
+
type DefinedByScope = Record<SkillfulScope, boolean>;
|
|
61
|
+
|
|
62
|
+
interface SkillfulVisibilityMenuOptions {
|
|
63
|
+
cwd: string;
|
|
64
|
+
skills: SkillListItem[];
|
|
65
|
+
hiddenByScope: HiddenSkillsByScope;
|
|
66
|
+
hiddenSkillsDefinedByScope: DefinedByScope;
|
|
67
|
+
toggleSlotsByScope: ToggleSlotsByScope;
|
|
68
|
+
toggleSlotsDefinedByScope: DefinedByScope;
|
|
69
|
+
theme: Theme;
|
|
70
|
+
tui: TUI;
|
|
71
|
+
notify: (message: string, type?: "info" | "warning" | "error") => void;
|
|
72
|
+
onToggleSlotsChanged: () => Promise<void>;
|
|
73
|
+
done: () => void;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface SettingsListSelectionView {
|
|
77
|
+
filteredItems?: SettingItem[];
|
|
78
|
+
items?: SettingItem[];
|
|
79
|
+
selectedIndex?: number;
|
|
80
|
+
submenuComponent?: unknown;
|
|
81
|
+
}
|
|
54
82
|
|
|
55
83
|
export default function skillVisibility(pi: ExtensionAPI) {
|
|
56
84
|
installStartupSkillListPatch();
|
|
@@ -97,9 +125,22 @@ export default function skillVisibility(pi: ExtensionAPI) {
|
|
|
97
125
|
global: new Set(scoped.global.hiddenSkills),
|
|
98
126
|
project: new Set(scoped.project.hiddenSkills),
|
|
99
127
|
},
|
|
128
|
+
hiddenSkillsDefinedByScope: {
|
|
129
|
+
global: scoped.global.hiddenSkillsDefined,
|
|
130
|
+
project: scoped.project.hiddenSkillsDefined,
|
|
131
|
+
},
|
|
132
|
+
toggleSlotsByScope: {
|
|
133
|
+
global: { ...scoped.global.toggleSlots },
|
|
134
|
+
project: { ...scoped.project.toggleSlots },
|
|
135
|
+
},
|
|
136
|
+
toggleSlotsDefinedByScope: {
|
|
137
|
+
global: scoped.global.toggleSlotsDefined,
|
|
138
|
+
project: scoped.project.toggleSlotsDefined,
|
|
139
|
+
},
|
|
100
140
|
theme,
|
|
101
141
|
tui,
|
|
102
142
|
notify: (message, type) => ctx.ui.notify(message, type),
|
|
143
|
+
onToggleSlotsChanged: () => refreshSessionSkillToggles(pi, ctx.cwd, ctx.ui),
|
|
103
144
|
done,
|
|
104
145
|
}),
|
|
105
146
|
);
|
|
@@ -113,15 +154,15 @@ async function pruneStaleHiddenSkills(pi: ExtensionAPI, cwd: string): Promise<vo
|
|
|
113
154
|
|
|
114
155
|
await Promise.all(
|
|
115
156
|
SCOPES.map(async (scope) => {
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
119
|
-
scoped[scope] = await writeHiddenSkills(scope, cwd,
|
|
157
|
+
const currentHidden = scoped[scope].hiddenSkills;
|
|
158
|
+
const prunedHidden = currentHidden.filter((name) => installedNames.has(name));
|
|
159
|
+
if (prunedHidden.length < currentHidden.length) {
|
|
160
|
+
scoped[scope] = await writeHiddenSkills(scope, cwd, prunedHidden);
|
|
120
161
|
}
|
|
121
162
|
}),
|
|
122
163
|
);
|
|
123
164
|
|
|
124
|
-
const hidden = new Set(
|
|
165
|
+
const hidden = new Set(scoped.project.hiddenSkillsDefined ? scoped.project.hiddenSkills : scoped.global.hiddenSkills);
|
|
125
166
|
store.hiddenSkillsByCwd.set(cwd, hidden);
|
|
126
167
|
store.lastHiddenSkills = hidden;
|
|
127
168
|
}
|
|
@@ -204,10 +245,14 @@ function getSkillItems(pi: ExtensionAPI): SkillListItem[] {
|
|
|
204
245
|
class SkillfulVisibilityMenu implements Component {
|
|
205
246
|
private readonly cwd: string;
|
|
206
247
|
private readonly skills: SkillListItem[];
|
|
207
|
-
private readonly hiddenByScope:
|
|
248
|
+
private readonly hiddenByScope: HiddenSkillsByScope;
|
|
249
|
+
private readonly hiddenSkillsDefinedByScope: DefinedByScope;
|
|
250
|
+
private readonly toggleSlotsByScope: ToggleSlotsByScope;
|
|
251
|
+
private readonly toggleSlotsDefinedByScope: DefinedByScope;
|
|
208
252
|
private readonly theme: Theme;
|
|
209
253
|
private readonly tui: TUI;
|
|
210
254
|
private readonly notify: (message: string, type?: "info" | "warning" | "error") => void;
|
|
255
|
+
private readonly onToggleSlotsChanged: () => Promise<void>;
|
|
211
256
|
private readonly done: () => void;
|
|
212
257
|
private readonly topBorder: DynamicBorder;
|
|
213
258
|
private readonly bottomBorder: DynamicBorder;
|
|
@@ -215,21 +260,17 @@ class SkillfulVisibilityMenu implements Component {
|
|
|
215
260
|
private settingsList: SettingsList;
|
|
216
261
|
private saveQueue: Promise<void> = Promise.resolve();
|
|
217
262
|
|
|
218
|
-
constructor(options: {
|
|
219
|
-
cwd: string;
|
|
220
|
-
skills: SkillListItem[];
|
|
221
|
-
hiddenByScope: Record<SkillfulScope, Set<string>>;
|
|
222
|
-
theme: Theme;
|
|
223
|
-
tui: TUI;
|
|
224
|
-
notify: (message: string, type?: "info" | "warning" | "error") => void;
|
|
225
|
-
done: () => void;
|
|
226
|
-
}) {
|
|
263
|
+
constructor(options: SkillfulVisibilityMenuOptions) {
|
|
227
264
|
this.cwd = options.cwd;
|
|
228
265
|
this.skills = options.skills;
|
|
229
266
|
this.hiddenByScope = options.hiddenByScope;
|
|
267
|
+
this.hiddenSkillsDefinedByScope = options.hiddenSkillsDefinedByScope;
|
|
268
|
+
this.toggleSlotsByScope = options.toggleSlotsByScope;
|
|
269
|
+
this.toggleSlotsDefinedByScope = options.toggleSlotsDefinedByScope;
|
|
230
270
|
this.theme = options.theme;
|
|
231
271
|
this.tui = options.tui;
|
|
232
272
|
this.notify = options.notify;
|
|
273
|
+
this.onToggleSlotsChanged = options.onToggleSlotsChanged;
|
|
233
274
|
this.done = options.done;
|
|
234
275
|
this.topBorder = new DynamicBorder((text: string) => this.theme.fg("accent", text));
|
|
235
276
|
this.bottomBorder = new DynamicBorder((text: string) => this.theme.fg("accent", text));
|
|
@@ -244,7 +285,7 @@ class SkillfulVisibilityMenu implements Component {
|
|
|
244
285
|
"",
|
|
245
286
|
...this.settingsList.render(width),
|
|
246
287
|
"",
|
|
247
|
-
truncateToWidth(this.theme.fg("dim", " Tab/←/→ switch
|
|
288
|
+
truncateToWidth(this.theme.fg("dim", " Tab/←/→ switch scope · 1-9 assign/clear toggle · Enter/Space on/off · Esc close"), width),
|
|
248
289
|
...this.bottomBorder.render(width),
|
|
249
290
|
];
|
|
250
291
|
}
|
|
@@ -258,6 +299,10 @@ class SkillfulVisibilityMenu implements Component {
|
|
|
258
299
|
this.switchScope(-1);
|
|
259
300
|
return;
|
|
260
301
|
}
|
|
302
|
+
if (/^[1-9]$/.test(data)) {
|
|
303
|
+
this.toggleSelectedSkillSlot(data as SkillToggleSlot);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
261
306
|
|
|
262
307
|
this.settingsList.handleInput(data);
|
|
263
308
|
this.tui.requestRender();
|
|
@@ -289,13 +334,13 @@ class SkillfulVisibilityMenu implements Component {
|
|
|
289
334
|
|
|
290
335
|
private createSettingsList(): SettingsList {
|
|
291
336
|
const items: SettingItem[] = this.skills.map((skill) => {
|
|
292
|
-
const hidden = this.
|
|
337
|
+
const hidden = this.isHidden(this.scope, skill.name);
|
|
293
338
|
return {
|
|
294
339
|
id: skill.name,
|
|
295
340
|
label: skill.name,
|
|
296
|
-
description: skill.description || "No skill description provided."
|
|
297
|
-
currentValue: hidden
|
|
298
|
-
values: [
|
|
341
|
+
description: `${skill.description || "No skill description provided."}\nPress 1-9 to assign or clear this skill's toggle slot in the ${this.scope} scope.`,
|
|
342
|
+
currentValue: this.skillValue(skill.name, hidden),
|
|
343
|
+
values: [this.skillValue(skill.name, false), this.skillValue(skill.name, true)],
|
|
299
344
|
};
|
|
300
345
|
});
|
|
301
346
|
|
|
@@ -303,27 +348,169 @@ class SkillfulVisibilityMenu implements Component {
|
|
|
303
348
|
items,
|
|
304
349
|
12,
|
|
305
350
|
getSettingsListTheme(),
|
|
306
|
-
(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
else hidden.delete(skillName);
|
|
351
|
+
(id, newValue) => {
|
|
352
|
+
this.setHidden(this.scope, id, newValue.includes("off"));
|
|
353
|
+
this.settingsList.updateValue(id, this.skillValue(id, this.isHidden(this.scope, id)));
|
|
310
354
|
this.persistScope(this.scope);
|
|
311
355
|
},
|
|
312
|
-
this.
|
|
356
|
+
() => this.close(),
|
|
313
357
|
{ enableSearch: true },
|
|
314
358
|
);
|
|
315
359
|
}
|
|
316
360
|
|
|
361
|
+
private isHidden(scope: SkillfulScope, skillName: string): boolean {
|
|
362
|
+
if (scope === "project" && !this.hiddenSkillsDefinedByScope.project) return this.hiddenByScope.global.has(skillName);
|
|
363
|
+
return this.hiddenByScope[scope].has(skillName);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private setHidden(scope: SkillfulScope, skillName: string, hidden: boolean): void {
|
|
367
|
+
const hiddenSkills = scope === "project" ? this.ensureFullProjectOverride().hiddenSkills : this.ensureWritableHiddenSkills(scope);
|
|
368
|
+
if (hidden) hiddenSkills.add(skillName);
|
|
369
|
+
else hiddenSkills.delete(skillName);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private ensureWritableHiddenSkills(scope: SkillfulScope): Set<string> {
|
|
373
|
+
if (scope === "global") this.hiddenSkillsDefinedByScope.global = true;
|
|
374
|
+
return this.hiddenByScope[scope];
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private ensureFullProjectOverride(): { hiddenSkills: Set<string>; toggleSlots: Partial<Record<SkillToggleSlot, string>> } {
|
|
378
|
+
if (!this.hiddenSkillsDefinedByScope.project) {
|
|
379
|
+
this.hiddenByScope.project = new Set(this.hiddenByScope.global);
|
|
380
|
+
this.hiddenSkillsDefinedByScope.project = true;
|
|
381
|
+
}
|
|
382
|
+
if (!this.toggleSlotsDefinedByScope.project) {
|
|
383
|
+
this.toggleSlotsByScope.project = { ...this.toggleSlotsByScope.global };
|
|
384
|
+
this.toggleSlotsDefinedByScope.project = true;
|
|
385
|
+
}
|
|
386
|
+
return { hiddenSkills: this.hiddenByScope.project, toggleSlots: this.toggleSlotsByScope.project };
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private skillValue(skillName: string, hidden: boolean): string {
|
|
390
|
+
const slot = SKILL_TOGGLE_SLOTS.find((candidate) => this.currentToggleSlots()[candidate] === skillName) ?? "";
|
|
391
|
+
const status = (hidden ? "off" : "on").padEnd(3, " ");
|
|
392
|
+
const statusText = this.isProjectOverride(skillName) ? this.theme.fg("accent", status) : status;
|
|
393
|
+
return `${statusText} ${slot}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private currentToggleSlots(): Partial<Record<SkillToggleSlot, string>> {
|
|
397
|
+
if (this.scope === "project" && !this.toggleSlotsDefinedByScope.project) return this.toggleSlotsByScope.global;
|
|
398
|
+
return this.toggleSlotsByScope[this.scope];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private ensureWritableToggleSlots(scope: SkillfulScope): Partial<Record<SkillToggleSlot, string>> {
|
|
402
|
+
if (scope === "project") return this.ensureFullProjectOverride().toggleSlots;
|
|
403
|
+
this.toggleSlotsDefinedByScope.global = true;
|
|
404
|
+
return this.toggleSlotsByScope.global;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private isProjectOverride(skillName: string): boolean {
|
|
408
|
+
if (this.scope !== "project") return false;
|
|
409
|
+
return this.isHidden("project", skillName) !== this.isHidden("global", skillName);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private toggleSelectedSkillSlot(slot: SkillToggleSlot): void {
|
|
413
|
+
const skillName = this.selectedSkillName();
|
|
414
|
+
if (!skillName) return;
|
|
415
|
+
|
|
416
|
+
const toggleSlots = this.ensureWritableToggleSlots(this.scope);
|
|
417
|
+
const affected = new Set<string>([skillName]);
|
|
418
|
+
const previousSkill = toggleSlots[slot];
|
|
419
|
+
if (previousSkill) affected.add(previousSkill);
|
|
420
|
+
|
|
421
|
+
if (previousSkill === skillName) {
|
|
422
|
+
delete toggleSlots[slot];
|
|
423
|
+
} else {
|
|
424
|
+
for (const candidate of SKILL_TOGGLE_SLOTS) {
|
|
425
|
+
if (toggleSlots[candidate] === skillName) delete toggleSlots[candidate];
|
|
426
|
+
}
|
|
427
|
+
toggleSlots[slot] = skillName;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
for (const affectedSkill of affected) {
|
|
431
|
+
this.settingsList.updateValue(affectedSkill, this.skillValue(affectedSkill, this.isHidden(this.scope, affectedSkill)));
|
|
432
|
+
}
|
|
433
|
+
this.persistToggleSlots(this.scope);
|
|
434
|
+
this.tui.requestRender();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private selectedSkillName(): string | undefined {
|
|
438
|
+
const list = this.settingsList as unknown as SettingsListSelectionView;
|
|
439
|
+
if (list.submenuComponent) return undefined;
|
|
440
|
+
const items = list.filteredItems ?? list.items ?? [];
|
|
441
|
+
const selectedIndex = list.selectedIndex ?? 0;
|
|
442
|
+
return items[selectedIndex]?.id;
|
|
443
|
+
}
|
|
444
|
+
|
|
317
445
|
private persistScope(scope: SkillfulScope): void {
|
|
318
|
-
const
|
|
446
|
+
const hiddenSnapshot = normalizeSkillNames(this.hiddenByScope[scope]);
|
|
319
447
|
this.saveQueue = this.saveQueue
|
|
320
448
|
.catch(() => undefined)
|
|
321
449
|
.then(async () => {
|
|
322
|
-
|
|
450
|
+
if (scope === "project") await this.writeProjectOverrideOrInheritance();
|
|
451
|
+
else await writeHiddenSkills(scope, this.cwd, hiddenSnapshot);
|
|
323
452
|
await refreshHiddenSkillCache(this.cwd);
|
|
324
453
|
})
|
|
325
454
|
.catch((error) => {
|
|
326
455
|
this.notify(`Failed to save ${scope} skill visibility: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
327
456
|
});
|
|
328
457
|
}
|
|
458
|
+
|
|
459
|
+
private persistToggleSlots(scope: SkillfulScope): void {
|
|
460
|
+
const snapshot = { ...this.toggleSlotsByScope[scope] };
|
|
461
|
+
this.saveQueue = this.saveQueue
|
|
462
|
+
.catch(() => undefined)
|
|
463
|
+
.then(async () => {
|
|
464
|
+
if (scope === "project") await this.writeProjectOverrideOrInheritance();
|
|
465
|
+
else await writeToggleSlots(scope, this.cwd, snapshot);
|
|
466
|
+
})
|
|
467
|
+
.catch((error) => {
|
|
468
|
+
this.notify(`Failed to save ${scope} skill toggles: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private close(): void {
|
|
473
|
+
this.saveQueue = this.saveQueue
|
|
474
|
+
.catch(() => undefined)
|
|
475
|
+
.then(() => this.onToggleSlotsChanged())
|
|
476
|
+
.catch((error) => {
|
|
477
|
+
this.notify(`Failed to refresh session skill toggles: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
478
|
+
})
|
|
479
|
+
.finally(() => this.done());
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
private async writeProjectOverrideOrInheritance(): Promise<void> {
|
|
483
|
+
if (this.projectMatchesGlobal()) {
|
|
484
|
+
this.hiddenSkillsDefinedByScope.project = false;
|
|
485
|
+
this.toggleSlotsDefinedByScope.project = false;
|
|
486
|
+
await writeProjectSkillfulOverride(this.cwd, undefined, undefined);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
this.hiddenSkillsDefinedByScope.project = true;
|
|
491
|
+
this.toggleSlotsDefinedByScope.project = true;
|
|
492
|
+
await writeProjectSkillfulOverride(this.cwd, this.hiddenByScope.project, this.toggleSlotsByScope.project);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private projectMatchesGlobal(): boolean {
|
|
496
|
+
return setsEqual(this.hiddenByScope.project, this.hiddenByScope.global) && toggleSlotsEqual(this.toggleSlotsByScope.project, this.toggleSlotsByScope.global);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function setsEqual(a: Set<string>, b: Set<string>): boolean {
|
|
501
|
+
if (a.size !== b.size) return false;
|
|
502
|
+
for (const value of a) {
|
|
503
|
+
if (!b.has(value)) return false;
|
|
504
|
+
}
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function toggleSlotsEqual(
|
|
509
|
+
a: Partial<Record<SkillToggleSlot, string>>,
|
|
510
|
+
b: Partial<Record<SkillToggleSlot, string>>,
|
|
511
|
+
): boolean {
|
|
512
|
+
for (const slot of SKILL_TOGGLE_SLOTS) {
|
|
513
|
+
if (a[slot] !== b[slot]) return false;
|
|
514
|
+
}
|
|
515
|
+
return true;
|
|
329
516
|
}
|