pi-skillful 0.3.4 → 0.3.6
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 +13 -0
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/config.ts +81 -23
- package/src/extensions/session-skill-toggles.ts +51 -11
- package/src/extensions/skill-visibility.ts +215 -28
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,19 @@ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.6] - 2026-05-12
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Made project skill visibility and toggle slots inherit global settings until the project scope is explicitly changed.
|
|
14
|
+
- Added `/skillful` menu support for assigning session toggle slots in global or project scope.
|
|
15
|
+
|
|
16
|
+
## [0.3.5] - 2026-05-12
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Fixed preserving session skill toggle state across `/new` after Pi reloads extension instances.
|
|
21
|
+
|
|
9
22
|
## [0.3.4] - 2026-05-12
|
|
10
23
|
|
|
11
24
|
### Changed
|
package/README.md
CHANGED
|
@@ -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
|
|
package/package.json
CHANGED
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);
|
|
@@ -14,6 +14,7 @@ import { replaceSkillsSection } from "../skill-prompt.js";
|
|
|
14
14
|
import { listLoadedSkills } from "../skills.js";
|
|
15
15
|
|
|
16
16
|
const WIDGET_KEY = "pi-skillful-session-toggles";
|
|
17
|
+
const STORE_KEY = Symbol.for("pi-skillful.sessionSkillTogglesStore");
|
|
17
18
|
const BORDER_PREFIX = "─── ";
|
|
18
19
|
const BORDER_SUFFIX = " ";
|
|
19
20
|
const BORDER_PREFIX_WIDTH = visibleWidth(BORDER_PREFIX);
|
|
@@ -38,8 +39,15 @@ interface SessionToggleState {
|
|
|
38
39
|
theme: Theme | undefined;
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
interface SessionToggleStore {
|
|
43
|
+
preservedNewSessionActiveBySkill: { cwd: string; activeBySkill: Map<string, boolean> } | undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const store = (((globalThis as Record<PropertyKey, unknown>)[STORE_KEY] as SessionToggleStore | undefined) ??= {
|
|
47
|
+
preservedNewSessionActiveBySkill: undefined,
|
|
48
|
+
}) as SessionToggleStore;
|
|
49
|
+
|
|
41
50
|
let state: SessionToggleState = createEmptyState();
|
|
42
|
-
let preservedNewSessionActiveBySkill: { cwd: string; activeBySkill: Map<string, boolean> } | undefined;
|
|
43
51
|
|
|
44
52
|
export default function sessionSkillToggles(pi: ExtensionAPI) {
|
|
45
53
|
for (const modifier of SUPPORTED_TOGGLE_MODIFIERS) {
|
|
@@ -56,18 +64,13 @@ export default function sessionSkillToggles(pi: ExtensionAPI) {
|
|
|
56
64
|
|
|
57
65
|
pi.on("session_start", async (event, ctx) => {
|
|
58
66
|
const settings = await readEffectiveSkillfulSettings(ctx.cwd);
|
|
59
|
-
const
|
|
60
|
-
const slots = SKILL_TOGGLE_SLOTS.flatMap((slot): ToggleSlotState[] => {
|
|
61
|
-
const skillName = settings.toggleSlots[slot];
|
|
62
|
-
if (!skillName || !loadedSkillNames.has(skillName)) return [];
|
|
63
|
-
return [{ slot, skillName }];
|
|
64
|
-
});
|
|
67
|
+
const slots = configuredToggleSlots(pi, settings.toggleSlots);
|
|
65
68
|
|
|
66
69
|
const preservedActiveBySkill =
|
|
67
|
-
event.reason === "new" && preservedNewSessionActiveBySkill?.cwd === ctx.cwd
|
|
68
|
-
? preservedNewSessionActiveBySkill.activeBySkill
|
|
70
|
+
event.reason === "new" && store.preservedNewSessionActiveBySkill?.cwd === ctx.cwd
|
|
71
|
+
? store.preservedNewSessionActiveBySkill.activeBySkill
|
|
69
72
|
: undefined;
|
|
70
|
-
preservedNewSessionActiveBySkill = undefined;
|
|
73
|
+
store.preservedNewSessionActiveBySkill = undefined;
|
|
71
74
|
|
|
72
75
|
state = {
|
|
73
76
|
cwd: ctx.cwd,
|
|
@@ -107,7 +110,7 @@ export default function sessionSkillToggles(pi: ExtensionAPI) {
|
|
|
107
110
|
pi.on("session_shutdown", (event, ctx) => {
|
|
108
111
|
if (state.installedEditor) ctx.ui.setEditorComponent(state.previousEditorFactory);
|
|
109
112
|
if (state.installedWidget) ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
110
|
-
preservedNewSessionActiveBySkill =
|
|
113
|
+
store.preservedNewSessionActiveBySkill =
|
|
111
114
|
event.reason === "new" ? { cwd: state.cwd, activeBySkill: new Map(state.activeBySkill) } : undefined;
|
|
112
115
|
state = createEmptyState();
|
|
113
116
|
});
|
|
@@ -132,6 +135,43 @@ export function hasActiveSessionSkillToggles(): boolean {
|
|
|
132
135
|
return state.slots.length > 0;
|
|
133
136
|
}
|
|
134
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
|
+
|
|
135
175
|
function isSkillActive(skillName: string): boolean {
|
|
136
176
|
return state.activeBySkill.get(skillName) ?? !state.hiddenSkills.has(skillName);
|
|
137
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
|
}
|