pi-skillful 0.3.12 → 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 +10 -0
- package/CONTRIBUTING.md +2 -0
- package/README.md +9 -7
- package/package.json +2 -2
- package/src/config.ts +35 -13
- package/src/extensions/session-skill-toggles.ts +115 -70
- package/src/extensions/skill-visibility.ts +49 -35
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.14 - 2026-07-17
|
|
4
|
+
|
|
5
|
+
## 0.3.13 - 2026-07-17
|
|
6
|
+
|
|
3
7
|
## 0.3.12 - 2026-07-12
|
|
4
8
|
|
|
5
9
|
## 0.3.11 - 2026-07-01
|
|
@@ -14,6 +18,12 @@ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com
|
|
|
14
18
|
|
|
15
19
|
### Fixed
|
|
16
20
|
|
|
21
|
+
- Ignore project `skillful` settings and block project-scope writes when Pi does not trust the current project.
|
|
22
|
+
- Let an explicit project `toggleModifier: "alt"` override a non-default global modifier; unsupported explicit values now follow the documented `"alt"` fallback.
|
|
23
|
+
- Consume only configured modifier-slot combinations in the prompt editor instead of registering all 54 possible shortcuts.
|
|
24
|
+
- Propagate focus and custom-editor hooks through the session-toggle editor wrapper for IME and extension compatibility.
|
|
25
|
+
- Restore red/dim coloring for hidden and visible skills in Pi's startup `[Skills]` list while preserving project-trust-aware settings.
|
|
26
|
+
- Remove the stale package-local `package-lock.json` in favor of the monorepo root lockfile.
|
|
17
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.
|
|
18
28
|
|
|
19
29
|
## [0.3.10] - 2026-07-01
|
package/CONTRIBUTING.md
CHANGED
|
@@ -7,6 +7,7 @@ Thanks for your interest in contributing to `pi-skillful`.
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install
|
|
9
9
|
npm run check
|
|
10
|
+
npm test
|
|
10
11
|
```
|
|
11
12
|
|
|
12
13
|
The package is source-distributed: Pi loads the TypeScript extension files directly. There is no build step for runtime use.
|
|
@@ -35,6 +36,7 @@ pi -e /path/to/pi-mono/packages/pi-skillful
|
|
|
35
36
|
Before opening a pull request:
|
|
36
37
|
|
|
37
38
|
- Run `npm run check`.
|
|
39
|
+
- Run `npm test`.
|
|
38
40
|
- Run `npm run pack:dry-run` and confirm the package contents are intentional.
|
|
39
41
|
- Update `README.md` if user-facing behavior changes.
|
|
40
42
|
- Update `CHANGELOG.md` for notable changes.
|
package/README.md
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
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
|
-
- **Skill prompt visibility**: choose which skills are hidden from the model's automatic skill-discovery prompt
|
|
12
|
+
- **Skill prompt visibility**: choose which skills are hidden from the model's automatic skill-discovery prompt while keeping them explicitly invokable.
|
|
13
13
|
- **Session skill toggles**: assign up to nine skills to number slots and toggle model visibility while writing a prompt.
|
|
14
14
|
|
|
15
15
|
> [!WARNING]
|
|
@@ -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.
|
|
@@ -64,9 +65,9 @@ Open the menu with:
|
|
|
64
65
|
|
|
65
66
|
The menu lists configurable 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.
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
Project settings are read and the Project tab is available only when Pi trusts the current project. In an untrusted project, `pi-skillful` ignores `.pi/settings.json` and exposes only global settings.
|
|
68
69
|
|
|
69
|
-
When the project settings file contains only `skillful` settings and the project
|
|
70
|
+
When the project settings file contains only `skillful` settings and the project override is removed, `.pi/settings.json` is deleted instead of leaving an empty settings file behind.
|
|
70
71
|
|
|
71
72
|
### Session skill toggles
|
|
72
73
|
|
|
@@ -86,9 +87,9 @@ Assign skills to up to nine prompt-editor slots with JSON settings:
|
|
|
86
87
|
}
|
|
87
88
|
```
|
|
88
89
|
|
|
89
|
-
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.
|
|
90
|
+
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. Only the configured modifier and assigned slot numbers are consumed while the prompt editor has focus; no modifier-number keys are reserved when no slots are configured.
|
|
90
91
|
|
|
91
|
-
`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.
|
|
92
|
+
`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. An explicitly configured project value takes precedence over the global value, including explicit `"alt"`. Unsupported explicit values fall back to `"alt"` in that scope.
|
|
92
93
|
|
|
93
94
|
On app startup, non-hidden skills are active and hidden skills are inactive. Within a running Pi process, `/new` preserves the current toggle state for the new session. Resuming, forking, cloning, reloading, or restarting Pi resets toggle state from settings. Inline `/skill:name` invocation remains explicit and works even when that skill is inactive. Skills bundled in Pi packages are never modified by these toggles.
|
|
94
95
|
|
|
@@ -146,6 +147,7 @@ Common commands:
|
|
|
146
147
|
```bash
|
|
147
148
|
npm install
|
|
148
149
|
npm run check
|
|
150
|
+
npm test
|
|
149
151
|
npm run pack:dry-run
|
|
150
152
|
```
|
|
151
153
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-skillful",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
4
4
|
"description": "Pi package with skill invocation and visibility improvements.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@earendil-works/pi-coding-agent": "^0.80.0",
|
|
49
49
|
"@earendil-works/pi-tui": "^0.80.0",
|
|
50
|
-
"@types/node": "^26.0
|
|
50
|
+
"@types/node": "^26.1.0",
|
|
51
51
|
"typescript": "^6.0.3"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
package/src/config.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface SkillfulSettings extends SkillToggleConfig {
|
|
|
24
24
|
hiddenSkillsDefined: boolean;
|
|
25
25
|
visibleSkills: string[];
|
|
26
26
|
toggleSlotsDefined: boolean;
|
|
27
|
+
toggleModifierDefined: boolean;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export interface EffectiveSkillfulSettings extends SkillfulSettings {
|
|
@@ -113,21 +114,27 @@ export async function readSkillfulSettings(path: string): Promise<SkillfulSettin
|
|
|
113
114
|
return settingsFromRecord(settings[SKILLFUL_SETTINGS_KEY]);
|
|
114
115
|
}
|
|
115
116
|
|
|
116
|
-
export async function readScopedSkillfulSettings(
|
|
117
|
+
export async function readScopedSkillfulSettings(
|
|
118
|
+
cwd: string,
|
|
119
|
+
projectTrusted: boolean,
|
|
120
|
+
): Promise<Record<SkillfulScope, SkillfulSettings>> {
|
|
117
121
|
const [global, project] = await Promise.all([
|
|
118
122
|
readSkillfulSettings(globalSettingsPath()),
|
|
119
|
-
readSkillfulSettings(projectSettingsPath(cwd)),
|
|
123
|
+
projectTrusted ? readSkillfulSettings(projectSettingsPath(cwd)) : Promise.resolve(settingsFromRecord(undefined)),
|
|
120
124
|
]);
|
|
121
125
|
return { global, project };
|
|
122
126
|
}
|
|
123
127
|
|
|
124
|
-
export async function readEffectiveHiddenSkills(cwd: string): Promise<Set<string>> {
|
|
125
|
-
const scoped = await readScopedSkillfulSettings(cwd);
|
|
128
|
+
export async function readEffectiveHiddenSkills(cwd: string, projectTrusted: boolean): Promise<Set<string>> {
|
|
129
|
+
const scoped = await readScopedSkillfulSettings(cwd, projectTrusted);
|
|
126
130
|
return effectiveHiddenSkillSet(scoped);
|
|
127
131
|
}
|
|
128
132
|
|
|
129
|
-
export async function readEffectiveSkillfulSettings(
|
|
130
|
-
|
|
133
|
+
export async function readEffectiveSkillfulSettings(
|
|
134
|
+
cwd: string,
|
|
135
|
+
projectTrusted: boolean,
|
|
136
|
+
): Promise<EffectiveSkillfulSettings> {
|
|
137
|
+
const scoped = await readScopedSkillfulSettings(cwd, projectTrusted);
|
|
131
138
|
const hiddenSkills = normalizeSkillNames(effectiveHiddenSkillSet(scoped));
|
|
132
139
|
const toggleSlots = scoped.project.toggleSlotsDefined ? scoped.project.toggleSlots : scoped.global.toggleSlots;
|
|
133
140
|
return {
|
|
@@ -137,8 +144,8 @@ export async function readEffectiveSkillfulSettings(cwd: string): Promise<Effect
|
|
|
137
144
|
hiddenSkillSet: new Set(hiddenSkills),
|
|
138
145
|
toggleSlots,
|
|
139
146
|
toggleSlotsDefined: scoped.global.toggleSlotsDefined || scoped.project.toggleSlotsDefined,
|
|
140
|
-
toggleModifier:
|
|
141
|
-
|
|
147
|
+
toggleModifier: scoped.project.toggleModifierDefined ? scoped.project.toggleModifier : scoped.global.toggleModifier,
|
|
148
|
+
toggleModifierDefined: scoped.global.toggleModifierDefined || scoped.project.toggleModifierDefined,
|
|
142
149
|
};
|
|
143
150
|
}
|
|
144
151
|
|
|
@@ -146,8 +153,9 @@ export async function writeHiddenSkills(
|
|
|
146
153
|
scope: SkillfulScope,
|
|
147
154
|
cwd: string,
|
|
148
155
|
hiddenSkills: Iterable<string>,
|
|
156
|
+
projectTrusted = false,
|
|
149
157
|
): Promise<SkillfulSettings> {
|
|
150
|
-
return updateSkillfulSettings(scope, cwd, (current) => {
|
|
158
|
+
return updateSkillfulSettings(scope, cwd, projectTrusted, (current) => {
|
|
151
159
|
current.hiddenSkills = normalizeSkillNames(hiddenSkills);
|
|
152
160
|
});
|
|
153
161
|
}
|
|
@@ -157,8 +165,9 @@ export async function writeSkillVisibility(
|
|
|
157
165
|
cwd: string,
|
|
158
166
|
hiddenSkills: Iterable<string>,
|
|
159
167
|
visibleSkills: Iterable<string> = [],
|
|
168
|
+
projectTrusted = false,
|
|
160
169
|
): Promise<SkillfulSettings> {
|
|
161
|
-
return updateSkillfulSettings(scope, cwd, (current) => {
|
|
170
|
+
return updateSkillfulSettings(scope, cwd, projectTrusted, (current) => {
|
|
162
171
|
const visible = normalizeSkillNames(visibleSkills);
|
|
163
172
|
current.hiddenSkills = normalizeSkillNames(hiddenSkills);
|
|
164
173
|
if (visible.length === 0) delete current.visibleSkills;
|
|
@@ -170,8 +179,9 @@ export async function writeToggleSlots(
|
|
|
170
179
|
scope: SkillfulScope,
|
|
171
180
|
cwd: string,
|
|
172
181
|
toggleSlots: Partial<Record<SkillToggleSlot, string>>,
|
|
182
|
+
projectTrusted = false,
|
|
173
183
|
): Promise<SkillfulSettings> {
|
|
174
|
-
return updateSkillfulSettings(scope, cwd, (current) => {
|
|
184
|
+
return updateSkillfulSettings(scope, cwd, projectTrusted, (current) => {
|
|
175
185
|
current.toggleSlots = normalizeToggleSlots(toggleSlots);
|
|
176
186
|
});
|
|
177
187
|
}
|
|
@@ -180,8 +190,9 @@ export async function writeProjectSkillfulOverride(
|
|
|
180
190
|
cwd: string,
|
|
181
191
|
hiddenSkills: Iterable<string> | undefined,
|
|
182
192
|
toggleSlots: Partial<Record<SkillToggleSlot, string>> | undefined,
|
|
193
|
+
projectTrusted: boolean,
|
|
183
194
|
): Promise<SkillfulSettings> {
|
|
184
|
-
return updateSkillfulSettings("project", cwd, (current) => {
|
|
195
|
+
return updateSkillfulSettings("project", cwd, projectTrusted, (current) => {
|
|
185
196
|
delete current.visibleSkills;
|
|
186
197
|
if (hiddenSkills === undefined) delete current.hiddenSkills;
|
|
187
198
|
else current.hiddenSkills = normalizeSkillNames(hiddenSkills);
|
|
@@ -193,8 +204,13 @@ export async function writeProjectSkillfulOverride(
|
|
|
193
204
|
async function updateSkillfulSettings(
|
|
194
205
|
scope: SkillfulScope,
|
|
195
206
|
cwd: string,
|
|
207
|
+
projectTrusted: boolean,
|
|
196
208
|
updater: (current: Record<string, unknown>) => void,
|
|
197
209
|
): Promise<SkillfulSettings> {
|
|
210
|
+
if (scope === "project" && !projectTrusted) {
|
|
211
|
+
throw new Error("Project skillful settings require a trusted project.");
|
|
212
|
+
}
|
|
213
|
+
|
|
198
214
|
const path = settingsPath(scope, cwd);
|
|
199
215
|
const document = await readSettingsDocument(path);
|
|
200
216
|
|
|
@@ -224,10 +240,15 @@ export async function updateHiddenSkills(
|
|
|
224
240
|
scope: SkillfulScope,
|
|
225
241
|
cwd: string,
|
|
226
242
|
updater: (current: string[]) => string[],
|
|
243
|
+
projectTrusted = false,
|
|
227
244
|
): Promise<SkillfulSettings> {
|
|
245
|
+
if (scope === "project" && !projectTrusted) {
|
|
246
|
+
throw new Error("Project skillful settings require a trusted project.");
|
|
247
|
+
}
|
|
248
|
+
|
|
228
249
|
const path = settingsPath(scope, cwd);
|
|
229
250
|
const current = await readSkillfulSettings(path);
|
|
230
|
-
return writeHiddenSkills(scope, cwd, updater(current.hiddenSkills));
|
|
251
|
+
return writeHiddenSkills(scope, cwd, updater(current.hiddenSkills), projectTrusted);
|
|
231
252
|
}
|
|
232
253
|
|
|
233
254
|
function settingsFromRecord(value: unknown): SkillfulSettings {
|
|
@@ -240,6 +261,7 @@ function settingsFromRecord(value: unknown): SkillfulSettings {
|
|
|
240
261
|
toggleSlots: normalizeToggleSlots(skillful.toggleSlots),
|
|
241
262
|
toggleSlotsDefined: Object.hasOwn(skillful, "toggleSlots"),
|
|
242
263
|
toggleModifier: normalizeToggleModifier(skillful.toggleModifier),
|
|
264
|
+
toggleModifierDefined: Object.hasOwn(skillful, "toggleModifier"),
|
|
243
265
|
};
|
|
244
266
|
}
|
|
245
267
|
|
|
@@ -1,19 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import {
|
|
2
|
+
CustomEditor,
|
|
3
|
+
type AppKeybinding,
|
|
4
|
+
type ExtensionAPI,
|
|
5
|
+
type KeybindingsManager,
|
|
6
|
+
type Skill,
|
|
7
|
+
type Theme,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import type { AutocompleteProvider, EditorComponent, EditorTheme, Focusable, KeyId, TUI } from "@earendil-works/pi-tui";
|
|
10
|
+
import { isFocusable, isKeyRelease, isKeyRepeat, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
11
|
import {
|
|
5
12
|
DEFAULT_TOGGLE_MODIFIER,
|
|
6
13
|
normalizeSkillName,
|
|
7
14
|
readEffectiveSkillfulSettings,
|
|
8
15
|
SKILL_TOGGLE_SLOTS,
|
|
9
|
-
SUPPORTED_TOGGLE_MODIFIERS,
|
|
10
16
|
type SkillToggleModifier,
|
|
11
17
|
type SkillToggleSlot,
|
|
12
18
|
} from "../config.js";
|
|
13
19
|
import { replaceSkillsSection } from "../skill-prompt.js";
|
|
14
20
|
import { isTopLevelSkill, listLoadedSkills } from "../skills.js";
|
|
15
21
|
|
|
16
|
-
const WIDGET_KEY = "pi-skillful-session-toggles";
|
|
17
22
|
const STORE_KEY = Symbol.for("pi-skillful.sessionSkillTogglesStore");
|
|
18
23
|
const BORDER_PREFIX = "─── ";
|
|
19
24
|
const BORDER_SUFFIX = " ";
|
|
@@ -33,10 +38,10 @@ interface SessionToggleState {
|
|
|
33
38
|
slots: ToggleSlotState[];
|
|
34
39
|
activeBySkill: Map<string, boolean>;
|
|
35
40
|
installedEditor: boolean;
|
|
36
|
-
installedWidget: boolean;
|
|
37
41
|
previousEditorFactory: SkillfulEditorFactory | undefined;
|
|
38
42
|
activeTui: TUI | undefined;
|
|
39
43
|
theme: Theme | undefined;
|
|
44
|
+
notify: (message: string, type?: "info" | "warning" | "error") => void;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
interface SessionToggleStore {
|
|
@@ -50,20 +55,8 @@ const store = (((globalThis as Record<PropertyKey, unknown>)[STORE_KEY] as Sessi
|
|
|
50
55
|
let state: SessionToggleState = createEmptyState();
|
|
51
56
|
|
|
52
57
|
export default function sessionSkillToggles(pi: ExtensionAPI) {
|
|
53
|
-
for (const modifier of SUPPORTED_TOGGLE_MODIFIERS) {
|
|
54
|
-
for (const slot of SKILL_TOGGLE_SLOTS) {
|
|
55
|
-
pi.registerShortcut(`${modifier}+${slot}`, {
|
|
56
|
-
description: `Toggle pi-skillful slot ${slot}`,
|
|
57
|
-
handler: (ctx) => {
|
|
58
|
-
if (state.modifier !== modifier) return;
|
|
59
|
-
toggleSlot(slot, ctx.ui.notify.bind(ctx.ui));
|
|
60
|
-
},
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
58
|
pi.on("session_start", async (event, ctx) => {
|
|
66
|
-
const settings = await readEffectiveSkillfulSettings(ctx.cwd);
|
|
59
|
+
const settings = await readEffectiveSkillfulSettings(ctx.cwd, ctx.isProjectTrusted());
|
|
67
60
|
const slots = configuredToggleSlots(pi, settings.toggleSlots);
|
|
68
61
|
|
|
69
62
|
const preservedActiveBySkill =
|
|
@@ -84,13 +77,13 @@ export default function sessionSkillToggles(pi: ExtensionAPI) {
|
|
|
84
77
|
]),
|
|
85
78
|
),
|
|
86
79
|
installedEditor: false,
|
|
87
|
-
installedWidget: false,
|
|
88
80
|
previousEditorFactory: undefined,
|
|
89
81
|
activeTui: undefined,
|
|
90
82
|
theme: ctx.ui.theme,
|
|
83
|
+
notify: ctx.ui.notify.bind(ctx.ui),
|
|
91
84
|
};
|
|
92
85
|
|
|
93
|
-
if (ctx.
|
|
86
|
+
if (ctx.mode === "tui" && slots.length > 0) installEditor(ctx.ui);
|
|
94
87
|
refreshUi();
|
|
95
88
|
});
|
|
96
89
|
|
|
@@ -108,7 +101,6 @@ export default function sessionSkillToggles(pi: ExtensionAPI) {
|
|
|
108
101
|
|
|
109
102
|
pi.on("session_shutdown", (event, ctx) => {
|
|
110
103
|
if (state.installedEditor) ctx.ui.setEditorComponent(state.previousEditorFactory);
|
|
111
|
-
if (state.installedWidget) ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
112
104
|
store.preservedNewSessionActiveBySkill =
|
|
113
105
|
event.reason === "new" ? { cwd: state.cwd, activeBySkill: new Map(state.activeBySkill) } : undefined;
|
|
114
106
|
state = createEmptyState();
|
|
@@ -123,10 +115,10 @@ function createEmptyState(): SessionToggleState {
|
|
|
123
115
|
slots: [],
|
|
124
116
|
activeBySkill: new Map<string, boolean>(),
|
|
125
117
|
installedEditor: false,
|
|
126
|
-
installedWidget: false,
|
|
127
118
|
previousEditorFactory: undefined,
|
|
128
119
|
activeTui: undefined,
|
|
129
120
|
theme: undefined,
|
|
121
|
+
notify: () => undefined,
|
|
130
122
|
};
|
|
131
123
|
}
|
|
132
124
|
|
|
@@ -134,8 +126,13 @@ export function hasActiveSessionSkillToggles(): boolean {
|
|
|
134
126
|
return state.slots.length > 0;
|
|
135
127
|
}
|
|
136
128
|
|
|
137
|
-
export async function refreshSessionSkillToggles(
|
|
138
|
-
|
|
129
|
+
export async function refreshSessionSkillToggles(
|
|
130
|
+
pi: ExtensionAPI,
|
|
131
|
+
cwd: string,
|
|
132
|
+
projectTrusted: boolean,
|
|
133
|
+
ui: SkillfulUi,
|
|
134
|
+
): Promise<void> {
|
|
135
|
+
const settings = await readEffectiveSkillfulSettings(cwd, projectTrusted);
|
|
139
136
|
const slots = configuredToggleSlots(pi, settings.toggleSlots);
|
|
140
137
|
|
|
141
138
|
const previousActiveBySkill = state.activeBySkill;
|
|
@@ -146,13 +143,11 @@ export async function refreshSessionSkillToggles(pi: ExtensionAPI, cwd: string,
|
|
|
146
143
|
slots.map(({ skillName }) => [skillName, previousActiveBySkill.get(skillName) ?? !settings.hiddenSkillSet.has(skillName)]),
|
|
147
144
|
);
|
|
148
145
|
|
|
149
|
-
if (slots.length > 0 && !state.installedEditor
|
|
146
|
+
if (slots.length > 0 && !state.installedEditor) {
|
|
150
147
|
installEditor(ui);
|
|
151
|
-
} else if (slots.length === 0) {
|
|
152
|
-
|
|
153
|
-
if (state.installedWidget) ui.setWidget(WIDGET_KEY, undefined);
|
|
148
|
+
} else if (slots.length === 0 && state.installedEditor) {
|
|
149
|
+
ui.setEditorComponent(state.previousEditorFactory);
|
|
154
150
|
state.installedEditor = false;
|
|
155
|
-
state.installedWidget = false;
|
|
156
151
|
state.previousEditorFactory = undefined;
|
|
157
152
|
}
|
|
158
153
|
|
|
@@ -179,17 +174,16 @@ function isSkillActive(skillName: string): boolean {
|
|
|
179
174
|
return state.activeBySkill.get(skillName) ?? !state.hiddenSkills.has(skillName);
|
|
180
175
|
}
|
|
181
176
|
|
|
182
|
-
function
|
|
183
|
-
const entry = state.slots.find((
|
|
184
|
-
if (!entry)
|
|
185
|
-
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
177
|
+
function handleToggleShortcut(data: string): boolean {
|
|
178
|
+
const entry = state.slots.find(({ slot }) => matchesKey(data, `${state.modifier}+${slot}` as KeyId));
|
|
179
|
+
if (!entry) return false;
|
|
180
|
+
if (isKeyRelease(data) || isKeyRepeat(data)) return true;
|
|
188
181
|
|
|
189
182
|
const next = !isSkillActive(entry.skillName);
|
|
190
183
|
state.activeBySkill.set(entry.skillName, next);
|
|
191
|
-
notify(`${entry.skillName} ${next ? "active" : "inactive"} for this session.`, "info");
|
|
184
|
+
state.notify(`${entry.skillName} ${next ? "active" : "inactive"} for this session.`, "info");
|
|
192
185
|
refreshUi();
|
|
186
|
+
return true;
|
|
193
187
|
}
|
|
194
188
|
|
|
195
189
|
type SkillfulEditorFactory = (tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => EditorComponent;
|
|
@@ -197,31 +191,17 @@ type SkillfulEditorFactory = (tui: TUI, theme: EditorTheme, keybindings: Keybind
|
|
|
197
191
|
type SkillfulUi = {
|
|
198
192
|
getEditorComponent: () => SkillfulEditorFactory | undefined;
|
|
199
193
|
setEditorComponent: (factory: SkillfulEditorFactory | undefined) => void;
|
|
200
|
-
setWidget: (
|
|
201
|
-
key: string,
|
|
202
|
-
content: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined,
|
|
203
|
-
options?: { placement?: "aboveEditor" | "belowEditor" },
|
|
204
|
-
) => void;
|
|
205
194
|
};
|
|
206
195
|
|
|
207
196
|
function installEditor(ui: SkillfulUi): void {
|
|
208
197
|
state.previousEditorFactory = ui.getEditorComponent();
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
});
|
|
217
|
-
state.installedEditor = true;
|
|
218
|
-
} catch {
|
|
219
|
-
ui.setWidget(WIDGET_KEY, (tui) => {
|
|
220
|
-
state.activeTui = tui;
|
|
221
|
-
return new SkillToggleWidget();
|
|
222
|
-
});
|
|
223
|
-
state.installedWidget = true;
|
|
224
|
-
}
|
|
198
|
+
const previous = state.previousEditorFactory;
|
|
199
|
+
ui.setEditorComponent((tui, theme, keybindings) => {
|
|
200
|
+
state.activeTui = tui;
|
|
201
|
+
if (previous) return new SkillToggleEditorWrapper(previous(tui, theme, keybindings));
|
|
202
|
+
return new SkillToggleEditor(tui, theme, keybindings);
|
|
203
|
+
});
|
|
204
|
+
state.installedEditor = true;
|
|
225
205
|
}
|
|
226
206
|
|
|
227
207
|
function refreshUi(): void {
|
|
@@ -229,6 +209,11 @@ function refreshUi(): void {
|
|
|
229
209
|
}
|
|
230
210
|
|
|
231
211
|
class SkillToggleEditor extends CustomEditor {
|
|
212
|
+
handleInput(data: string): void {
|
|
213
|
+
if (handleToggleShortcut(data)) return;
|
|
214
|
+
super.handleInput(data);
|
|
215
|
+
}
|
|
216
|
+
|
|
232
217
|
render(width: number): string[] {
|
|
233
218
|
const lines = super.render(width);
|
|
234
219
|
if (lines.length === 0) return lines;
|
|
@@ -237,11 +222,78 @@ class SkillToggleEditor extends CustomEditor {
|
|
|
237
222
|
}
|
|
238
223
|
}
|
|
239
224
|
|
|
240
|
-
|
|
241
|
-
|
|
225
|
+
interface CustomEditorHooks {
|
|
226
|
+
actionHandlers: Map<AppKeybinding, () => void>;
|
|
227
|
+
onEscape?: () => void;
|
|
228
|
+
onCtrlD?: () => void;
|
|
229
|
+
onPasteImage?: () => void;
|
|
230
|
+
onExtensionShortcut?: (data: string) => boolean;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function hasCustomEditorHooks(editor: EditorComponent): editor is EditorComponent & CustomEditorHooks {
|
|
234
|
+
return "actionHandlers" in editor && editor.actionHandlers instanceof Map;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
class SkillToggleEditorWrapper implements EditorComponent, Focusable {
|
|
238
|
+
private _focused = false;
|
|
239
|
+
|
|
240
|
+
constructor(private readonly inner: EditorComponent) {}
|
|
241
|
+
|
|
242
|
+
get borderColor(): ((str: string) => string) | undefined {
|
|
243
|
+
return this.inner.borderColor?.bind(this.inner);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
set borderColor(color: ((str: string) => string) | undefined) {
|
|
247
|
+
this.inner.borderColor = color;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
get focused(): boolean {
|
|
251
|
+
return this._focused;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
set focused(value: boolean) {
|
|
255
|
+
this._focused = value;
|
|
256
|
+
if (isFocusable(this.inner)) this.inner.focused = value;
|
|
257
|
+
}
|
|
242
258
|
|
|
243
|
-
|
|
244
|
-
this.
|
|
259
|
+
get wantsKeyRelease(): boolean | undefined {
|
|
260
|
+
return this.inner.wantsKeyRelease;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
get actionHandlers(): Map<AppKeybinding, () => void> | undefined {
|
|
264
|
+
return hasCustomEditorHooks(this.inner) ? this.inner.actionHandlers : undefined;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
get onEscape(): (() => void) | undefined {
|
|
268
|
+
return hasCustomEditorHooks(this.inner) ? this.inner.onEscape : undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
set onEscape(handler: (() => void) | undefined) {
|
|
272
|
+
if (hasCustomEditorHooks(this.inner)) this.inner.onEscape = handler;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
get onCtrlD(): (() => void) | undefined {
|
|
276
|
+
return hasCustomEditorHooks(this.inner) ? this.inner.onCtrlD : undefined;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
set onCtrlD(handler: (() => void) | undefined) {
|
|
280
|
+
if (hasCustomEditorHooks(this.inner)) this.inner.onCtrlD = handler;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
get onPasteImage(): (() => void) | undefined {
|
|
284
|
+
return hasCustomEditorHooks(this.inner) ? this.inner.onPasteImage : undefined;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
set onPasteImage(handler: (() => void) | undefined) {
|
|
288
|
+
if (hasCustomEditorHooks(this.inner)) this.inner.onPasteImage = handler;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
get onExtensionShortcut(): ((data: string) => boolean) | undefined {
|
|
292
|
+
return hasCustomEditorHooks(this.inner) ? this.inner.onExtensionShortcut : undefined;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
set onExtensionShortcut(handler: ((data: string) => boolean) | undefined) {
|
|
296
|
+
if (hasCustomEditorHooks(this.inner)) this.inner.onExtensionShortcut = handler;
|
|
245
297
|
}
|
|
246
298
|
|
|
247
299
|
get onSubmit(): ((text: string) => void) | undefined {
|
|
@@ -280,6 +332,7 @@ class SkillToggleEditorWrapper implements EditorComponent {
|
|
|
280
332
|
}
|
|
281
333
|
|
|
282
334
|
handleInput(data: string): void {
|
|
335
|
+
if (handleToggleShortcut(data)) return;
|
|
283
336
|
this.inner.handleInput(data);
|
|
284
337
|
}
|
|
285
338
|
|
|
@@ -312,14 +365,6 @@ class SkillToggleEditorWrapper implements EditorComponent {
|
|
|
312
365
|
}
|
|
313
366
|
}
|
|
314
367
|
|
|
315
|
-
class SkillToggleWidget implements Component {
|
|
316
|
-
render(width: number): string[] {
|
|
317
|
-
return [renderToggleBorder(width, (text) => text)];
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
invalidate(): void {}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
368
|
function renderToggleBorder(width: number, borderColor: (text: string) => string): string {
|
|
324
369
|
if (width <= 0) return "";
|
|
325
370
|
if (state.slots.length === 0) return borderColor("─".repeat(width));
|
|
@@ -24,11 +24,10 @@ import { isTopLevelSkill, listLoadedSkills, type LoadedSkillInfo } from "../skil
|
|
|
24
24
|
import { hasActiveSessionSkillToggles, refreshSessionSkillToggles } from "./session-skill-toggles.js";
|
|
25
25
|
const SCOPES: SkillfulScope[] = ["global", "project"];
|
|
26
26
|
const STORE_KEY = Symbol.for("pi-skillful.skillVisibilityStore");
|
|
27
|
-
const STARTUP_PATCH_KEY = Symbol.for("pi-skillful.
|
|
27
|
+
const STARTUP_PATCH_KEY = Symbol.for("pi-skillful.startupPatchV3");
|
|
28
28
|
|
|
29
29
|
interface SkillVisibilityStore {
|
|
30
30
|
hiddenSkillsByCwd: Map<string, Set<string>>;
|
|
31
|
-
lastHiddenSkills: Set<string>;
|
|
32
31
|
theme: Theme | null;
|
|
33
32
|
}
|
|
34
33
|
|
|
@@ -50,7 +49,6 @@ interface InteractiveModeLike {
|
|
|
50
49
|
|
|
51
50
|
const store = (((globalThis as Record<PropertyKey, unknown>)[STORE_KEY] as SkillVisibilityStore | undefined) ??= {
|
|
52
51
|
hiddenSkillsByCwd: new Map<string, Set<string>>(),
|
|
53
|
-
lastHiddenSkills: new Set<string>(),
|
|
54
52
|
theme: null,
|
|
55
53
|
}) as SkillVisibilityStore;
|
|
56
54
|
|
|
@@ -61,6 +59,7 @@ type DefinedByScope = Record<SkillfulScope, boolean>;
|
|
|
61
59
|
|
|
62
60
|
interface SkillfulVisibilityMenuOptions {
|
|
63
61
|
cwd: string;
|
|
62
|
+
projectTrusted: boolean;
|
|
64
63
|
skills: SkillListItem[];
|
|
65
64
|
hiddenByScope: HiddenSkillsByScope;
|
|
66
65
|
hiddenSkillsDefinedByScope: DefinedByScope;
|
|
@@ -85,13 +84,13 @@ export default function skillVisibility(pi: ExtensionAPI) {
|
|
|
85
84
|
|
|
86
85
|
pi.on("session_start", async (_event, ctx) => {
|
|
87
86
|
store.theme = ctx.ui.theme;
|
|
88
|
-
await refreshHiddenSkillCache(ctx.cwd);
|
|
87
|
+
await refreshHiddenSkillCache(ctx.cwd, ctx.isProjectTrusted());
|
|
89
88
|
});
|
|
90
89
|
|
|
91
90
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
92
91
|
if (hasActiveSessionSkillToggles()) return;
|
|
93
92
|
|
|
94
|
-
const hidden = await refreshHiddenSkillCache(ctx.cwd);
|
|
93
|
+
const hidden = await refreshHiddenSkillCache(ctx.cwd, ctx.isProjectTrusted());
|
|
95
94
|
if (hidden.size === 0 || !event.systemPromptOptions.skills?.length) return;
|
|
96
95
|
|
|
97
96
|
const filteredSkills: Skill[] = event.systemPromptOptions.skills.map((skill) =>
|
|
@@ -105,7 +104,7 @@ export default function skillVisibility(pi: ExtensionAPI) {
|
|
|
105
104
|
pi.registerCommand("skillful", {
|
|
106
105
|
description: "Open the pi-skillful skill visibility menu.",
|
|
107
106
|
handler: async (_args, ctx) => {
|
|
108
|
-
if (
|
|
107
|
+
if (ctx.mode !== "tui") {
|
|
109
108
|
ctx.ui.notify("/skillful requires interactive UI", "warning");
|
|
110
109
|
return;
|
|
111
110
|
}
|
|
@@ -116,10 +115,12 @@ export default function skillVisibility(pi: ExtensionAPI) {
|
|
|
116
115
|
return;
|
|
117
116
|
}
|
|
118
117
|
|
|
119
|
-
const
|
|
118
|
+
const projectTrusted = ctx.isProjectTrusted();
|
|
119
|
+
const scoped = await readScopedSkillfulSettings(ctx.cwd, projectTrusted);
|
|
120
120
|
await ctx.ui.custom<void>((tui, theme, _keybindings, done) =>
|
|
121
121
|
new SkillfulVisibilityMenu({
|
|
122
122
|
cwd: ctx.cwd,
|
|
123
|
+
projectTrusted,
|
|
123
124
|
skills,
|
|
124
125
|
hiddenByScope: {
|
|
125
126
|
global: new Set(scoped.global.hiddenSkills),
|
|
@@ -140,7 +141,7 @@ export default function skillVisibility(pi: ExtensionAPI) {
|
|
|
140
141
|
theme,
|
|
141
142
|
tui,
|
|
142
143
|
notify: (message, type) => ctx.ui.notify(message, type),
|
|
143
|
-
onToggleSlotsChanged: () => refreshSessionSkillToggles(pi, ctx.cwd, ctx.ui),
|
|
144
|
+
onToggleSlotsChanged: () => refreshSessionSkillToggles(pi, ctx.cwd, projectTrusted, ctx.ui),
|
|
144
145
|
done,
|
|
145
146
|
}),
|
|
146
147
|
);
|
|
@@ -148,16 +149,17 @@ export default function skillVisibility(pi: ExtensionAPI) {
|
|
|
148
149
|
});
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
async function refreshHiddenSkillCache(cwd: string): Promise<Set<string>> {
|
|
152
|
-
const hidden = await readEffectiveHiddenSkills(cwd);
|
|
152
|
+
async function refreshHiddenSkillCache(cwd: string, projectTrusted: boolean): Promise<Set<string>> {
|
|
153
|
+
const hidden = await readEffectiveHiddenSkills(cwd, projectTrusted);
|
|
153
154
|
store.hiddenSkillsByCwd.set(cwd, hidden);
|
|
154
|
-
store.lastHiddenSkills = hidden;
|
|
155
155
|
return hidden;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
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 {
|
|
161
163
|
if (!realPrototype) return;
|
|
162
164
|
|
|
163
165
|
const patchState = realPrototype as Record<PropertyKey, unknown>;
|
|
@@ -175,16 +177,14 @@ function installStartupSkillListPatch(): void {
|
|
|
175
177
|
}
|
|
176
178
|
|
|
177
179
|
const cwd = this.sessionManager?.getCwd?.();
|
|
178
|
-
|
|
179
180
|
let rawSkillNames: string[] = [];
|
|
180
181
|
loader.getSkills = () => {
|
|
181
182
|
const result = originalGetSkills.call(loader);
|
|
182
|
-
rawSkillNames = result.skills.map((
|
|
183
|
+
rawSkillNames = result.skills.map((skill) => normalizeSkillName(skill.name));
|
|
183
184
|
return result;
|
|
184
185
|
};
|
|
185
186
|
|
|
186
187
|
const childrenBefore = this.loadedResourcesContainer?.children.length ?? 0;
|
|
187
|
-
|
|
188
188
|
try {
|
|
189
189
|
original.call(this, options);
|
|
190
190
|
} finally {
|
|
@@ -193,14 +193,14 @@ function installStartupSkillListPatch(): void {
|
|
|
193
193
|
|
|
194
194
|
if (rawSkillNames.length === 0 || !cwd || !this.loadedResourcesContainer) return;
|
|
195
195
|
|
|
196
|
+
const hidden = store.hiddenSkillsByCwd.get(cwd) ?? new Set<string>();
|
|
196
197
|
const children = this.loadedResourcesContainer.children;
|
|
197
|
-
for (let
|
|
198
|
-
const child = children[
|
|
198
|
+
for (let index = childrenBefore; index < children.length; index++) {
|
|
199
|
+
const child = children[index] as ExpandableTextLike | undefined;
|
|
199
200
|
if (!child || typeof child.getCollapsedText !== "function") continue;
|
|
200
|
-
|
|
201
|
-
if (!collapsed.includes("[Skills]")) continue;
|
|
201
|
+
if (!child.getCollapsedText().includes("[Skills]")) continue;
|
|
202
202
|
|
|
203
|
-
const colorized = buildColorizedSkillList(rawSkillNames,
|
|
203
|
+
const colorized = buildColorizedSkillList(rawSkillNames, hidden, store.theme);
|
|
204
204
|
child.getCollapsedText = () => colorized;
|
|
205
205
|
child.setText(colorized);
|
|
206
206
|
break;
|
|
@@ -210,13 +210,12 @@ function installStartupSkillListPatch(): void {
|
|
|
210
210
|
patchState[STARTUP_PATCH_KEY] = true;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
function buildColorizedSkillList(names: string[], hidden: Set<string>, theme: Theme | null): string {
|
|
213
|
+
export function buildColorizedSkillList(names: string[], hidden: Set<string>, theme: Theme | null): string {
|
|
214
214
|
const sorted = [...names].sort((a, b) => a.localeCompare(b));
|
|
215
|
-
if (!theme) {
|
|
216
|
-
|
|
217
|
-
}
|
|
215
|
+
if (!theme) return `[Skills]\n ${sorted.join(", ")}`;
|
|
216
|
+
|
|
218
217
|
const header = theme.fg("mdHeading", "[Skills]");
|
|
219
|
-
const parts = sorted.map((
|
|
218
|
+
const parts = sorted.map((name) => (hidden.has(name) ? theme.fg("error", name) : theme.fg("dim", name)));
|
|
220
219
|
return `${header}\n ${parts.join(", ")}`;
|
|
221
220
|
}
|
|
222
221
|
|
|
@@ -226,6 +225,8 @@ function getSkillItems(pi: ExtensionAPI): SkillListItem[] {
|
|
|
226
225
|
|
|
227
226
|
class SkillfulVisibilityMenu implements Component {
|
|
228
227
|
private readonly cwd: string;
|
|
228
|
+
private readonly projectTrusted: boolean;
|
|
229
|
+
private readonly scopes: SkillfulScope[];
|
|
229
230
|
private readonly skills: SkillListItem[];
|
|
230
231
|
private readonly hiddenByScope: HiddenSkillsByScope;
|
|
231
232
|
private readonly hiddenSkillsDefinedByScope: DefinedByScope;
|
|
@@ -238,12 +239,15 @@ class SkillfulVisibilityMenu implements Component {
|
|
|
238
239
|
private readonly done: () => void;
|
|
239
240
|
private readonly topBorder: DynamicBorder;
|
|
240
241
|
private readonly bottomBorder: DynamicBorder;
|
|
241
|
-
private scope: SkillfulScope
|
|
242
|
+
private scope: SkillfulScope;
|
|
242
243
|
private settingsList: SettingsList;
|
|
243
244
|
private saveQueue: Promise<void> = Promise.resolve();
|
|
244
245
|
|
|
245
246
|
constructor(options: SkillfulVisibilityMenuOptions) {
|
|
246
247
|
this.cwd = options.cwd;
|
|
248
|
+
this.projectTrusted = options.projectTrusted;
|
|
249
|
+
this.scopes = options.projectTrusted ? SCOPES : ["global"];
|
|
250
|
+
this.scope = options.projectTrusted ? "project" : "global";
|
|
247
251
|
this.skills = options.skills;
|
|
248
252
|
this.hiddenByScope = options.hiddenByScope;
|
|
249
253
|
this.hiddenSkillsDefinedByScope = options.hiddenSkillsDefinedByScope;
|
|
@@ -267,7 +271,7 @@ class SkillfulVisibilityMenu implements Component {
|
|
|
267
271
|
"",
|
|
268
272
|
...this.settingsList.render(width),
|
|
269
273
|
"",
|
|
270
|
-
truncateToWidth(this.theme.fg("dim",
|
|
274
|
+
truncateToWidth(this.theme.fg("dim", this.renderHelp()), width),
|
|
271
275
|
...this.bottomBorder.render(width),
|
|
272
276
|
];
|
|
273
277
|
}
|
|
@@ -297,7 +301,7 @@ class SkillfulVisibilityMenu implements Component {
|
|
|
297
301
|
}
|
|
298
302
|
|
|
299
303
|
private renderTabs(): string {
|
|
300
|
-
return
|
|
304
|
+
return this.scopes
|
|
301
305
|
.map((scope) => {
|
|
302
306
|
const label = scope === "global" ? "Global" : "Project";
|
|
303
307
|
return scope === this.scope
|
|
@@ -307,9 +311,14 @@ class SkillfulVisibilityMenu implements Component {
|
|
|
307
311
|
.join(" ");
|
|
308
312
|
}
|
|
309
313
|
|
|
314
|
+
private renderHelp(): string {
|
|
315
|
+
const scopeHelp = this.projectTrusted ? "Tab/←/→ switch scope · " : "";
|
|
316
|
+
return ` ${scopeHelp}1-9 assign/clear toggle · Enter/Space on/off · Esc close`;
|
|
317
|
+
}
|
|
318
|
+
|
|
310
319
|
private switchScope(direction: 1 | -1): void {
|
|
311
|
-
const current =
|
|
312
|
-
this.scope =
|
|
320
|
+
const current = this.scopes.indexOf(this.scope);
|
|
321
|
+
this.scope = this.scopes[(current + direction + this.scopes.length) % this.scopes.length];
|
|
313
322
|
this.settingsList = this.createSettingsList();
|
|
314
323
|
this.tui.requestRender();
|
|
315
324
|
}
|
|
@@ -431,7 +440,7 @@ class SkillfulVisibilityMenu implements Component {
|
|
|
431
440
|
.then(async () => {
|
|
432
441
|
if (scope === "project") await this.writeProjectOverrideOrInheritance();
|
|
433
442
|
else await writeHiddenSkills(scope, this.cwd, hiddenSnapshot);
|
|
434
|
-
await refreshHiddenSkillCache(this.cwd);
|
|
443
|
+
await refreshHiddenSkillCache(this.cwd, this.projectTrusted);
|
|
435
444
|
})
|
|
436
445
|
.catch((error) => {
|
|
437
446
|
this.notify(`Failed to save ${scope} skill visibility: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
@@ -465,13 +474,18 @@ class SkillfulVisibilityMenu implements Component {
|
|
|
465
474
|
if (this.projectMatchesGlobal()) {
|
|
466
475
|
this.hiddenSkillsDefinedByScope.project = false;
|
|
467
476
|
this.toggleSlotsDefinedByScope.project = false;
|
|
468
|
-
await writeProjectSkillfulOverride(this.cwd, undefined, undefined);
|
|
477
|
+
await writeProjectSkillfulOverride(this.cwd, undefined, undefined, this.projectTrusted);
|
|
469
478
|
return;
|
|
470
479
|
}
|
|
471
480
|
|
|
472
481
|
this.hiddenSkillsDefinedByScope.project = true;
|
|
473
482
|
this.toggleSlotsDefinedByScope.project = true;
|
|
474
|
-
await writeProjectSkillfulOverride(
|
|
483
|
+
await writeProjectSkillfulOverride(
|
|
484
|
+
this.cwd,
|
|
485
|
+
this.hiddenByScope.project,
|
|
486
|
+
this.toggleSlotsByScope.project,
|
|
487
|
+
this.projectTrusted,
|
|
488
|
+
);
|
|
475
489
|
}
|
|
476
490
|
|
|
477
491
|
private projectMatchesGlobal(): boolean {
|