pi-skillful 0.3.12 → 0.3.13
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 +8 -0
- package/CONTRIBUTING.md +2 -0
- package/README.md +6 -5
- 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 +32 -127
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.13 - 2026-07-17
|
|
4
|
+
|
|
3
5
|
## 0.3.12 - 2026-07-12
|
|
4
6
|
|
|
5
7
|
## 0.3.11 - 2026-07-01
|
|
@@ -14,6 +16,12 @@ This project follows the spirit of [Keep a Changelog](https://keepachangelog.com
|
|
|
14
16
|
|
|
15
17
|
### Fixed
|
|
16
18
|
|
|
19
|
+
- Ignore project `skillful` settings and block project-scope writes when Pi does not trust the current project.
|
|
20
|
+
- Let an explicit project `toggleModifier: "alt"` override a non-default global modifier; unsupported explicit values now follow the documented `"alt"` fallback.
|
|
21
|
+
- Consume only configured modifier-slot combinations in the prompt editor instead of registering all 54 possible shortcuts.
|
|
22
|
+
- Propagate focus and custom-editor hooks through the session-toggle editor wrapper for IME and extension compatibility.
|
|
23
|
+
- Remove the private `InteractiveMode.prototype` startup skill-list patch; visibility now uses only documented Pi APIs.
|
|
24
|
+
- Remove the stale package-local `package-lock.json` in favor of the monorepo root lockfile.
|
|
17
25
|
- 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
26
|
|
|
19
27
|
## [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
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
It currently provides three extensions:
|
|
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]
|
|
@@ -64,9 +64,9 @@ Open the menu with:
|
|
|
64
64
|
|
|
65
65
|
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
66
|
|
|
67
|
-
|
|
67
|
+
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
68
|
|
|
69
|
-
When the project settings file contains only `skillful` settings and the project
|
|
69
|
+
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
70
|
|
|
71
71
|
### Session skill toggles
|
|
72
72
|
|
|
@@ -86,9 +86,9 @@ Assign skills to up to nine prompt-editor slots with JSON settings:
|
|
|
86
86
|
}
|
|
87
87
|
```
|
|
88
88
|
|
|
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.
|
|
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. 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
90
|
|
|
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.
|
|
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. 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
92
|
|
|
93
93
|
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
94
|
|
|
@@ -146,6 +146,7 @@ Common commands:
|
|
|
146
146
|
```bash
|
|
147
147
|
npm install
|
|
148
148
|
npm run check
|
|
149
|
+
npm test
|
|
149
150
|
npm run pack:dry-run
|
|
150
151
|
```
|
|
151
152
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-skillful",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.13",
|
|
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));
|
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DynamicBorder,
|
|
3
|
-
getSettingsListTheme,
|
|
4
|
-
InteractiveMode,
|
|
5
|
-
type ExtensionAPI,
|
|
6
|
-
type Skill,
|
|
7
|
-
type Theme,
|
|
8
|
-
} from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { DynamicBorder, getSettingsListTheme, type ExtensionAPI, type Skill, type Theme } from "@earendil-works/pi-coding-agent";
|
|
9
2
|
import { type Component, Key, matchesKey, type SettingItem, SettingsList, truncateToWidth, type TUI } from "@earendil-works/pi-tui";
|
|
10
3
|
import {
|
|
11
|
-
normalizeSkillName,
|
|
12
4
|
normalizeSkillNames,
|
|
13
5
|
readEffectiveHiddenSkills,
|
|
14
6
|
readScopedSkillfulSettings,
|
|
@@ -23,36 +15,6 @@ import { replaceSkillsSection } from "../skill-prompt.js";
|
|
|
23
15
|
import { isTopLevelSkill, listLoadedSkills, type LoadedSkillInfo } from "../skills.js";
|
|
24
16
|
import { hasActiveSessionSkillToggles, refreshSessionSkillToggles } from "./session-skill-toggles.js";
|
|
25
17
|
const SCOPES: SkillfulScope[] = ["global", "project"];
|
|
26
|
-
const STORE_KEY = Symbol.for("pi-skillful.skillVisibilityStore");
|
|
27
|
-
const STARTUP_PATCH_KEY = Symbol.for("pi-skillful.startupPatchV2");
|
|
28
|
-
|
|
29
|
-
interface SkillVisibilityStore {
|
|
30
|
-
hiddenSkillsByCwd: Map<string, Set<string>>;
|
|
31
|
-
lastHiddenSkills: Set<string>;
|
|
32
|
-
theme: Theme | null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface ExpandableTextLike {
|
|
36
|
-
getCollapsedText: () => string;
|
|
37
|
-
setText: (text: string) => void;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface BoxLike {
|
|
41
|
-
children: unknown[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface InteractiveModeLike {
|
|
45
|
-
loadedResourcesContainer?: BoxLike;
|
|
46
|
-
showLoadedResources?: (options?: unknown) => void;
|
|
47
|
-
session?: { resourceLoader?: { getSkills: () => { skills: Skill[]; diagnostics: unknown[] } } };
|
|
48
|
-
sessionManager?: { getCwd?: () => string };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const store = (((globalThis as Record<PropertyKey, unknown>)[STORE_KEY] as SkillVisibilityStore | undefined) ??= {
|
|
52
|
-
hiddenSkillsByCwd: new Map<string, Set<string>>(),
|
|
53
|
-
lastHiddenSkills: new Set<string>(),
|
|
54
|
-
theme: null,
|
|
55
|
-
}) as SkillVisibilityStore;
|
|
56
18
|
|
|
57
19
|
type SkillListItem = LoadedSkillInfo;
|
|
58
20
|
type HiddenSkillsByScope = Record<SkillfulScope, Set<string>>;
|
|
@@ -61,6 +23,7 @@ type DefinedByScope = Record<SkillfulScope, boolean>;
|
|
|
61
23
|
|
|
62
24
|
interface SkillfulVisibilityMenuOptions {
|
|
63
25
|
cwd: string;
|
|
26
|
+
projectTrusted: boolean;
|
|
64
27
|
skills: SkillListItem[];
|
|
65
28
|
hiddenByScope: HiddenSkillsByScope;
|
|
66
29
|
hiddenSkillsDefinedByScope: DefinedByScope;
|
|
@@ -81,17 +44,14 @@ interface SettingsListSelectionView {
|
|
|
81
44
|
}
|
|
82
45
|
|
|
83
46
|
export default function skillVisibility(pi: ExtensionAPI) {
|
|
84
|
-
installStartupSkillListPatch();
|
|
85
|
-
|
|
86
47
|
pi.on("session_start", async (_event, ctx) => {
|
|
87
|
-
|
|
88
|
-
await refreshHiddenSkillCache(ctx.cwd);
|
|
48
|
+
await readEffectiveHiddenSkills(ctx.cwd, ctx.isProjectTrusted());
|
|
89
49
|
});
|
|
90
50
|
|
|
91
51
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
92
52
|
if (hasActiveSessionSkillToggles()) return;
|
|
93
53
|
|
|
94
|
-
const hidden = await
|
|
54
|
+
const hidden = await readEffectiveHiddenSkills(ctx.cwd, ctx.isProjectTrusted());
|
|
95
55
|
if (hidden.size === 0 || !event.systemPromptOptions.skills?.length) return;
|
|
96
56
|
|
|
97
57
|
const filteredSkills: Skill[] = event.systemPromptOptions.skills.map((skill) =>
|
|
@@ -105,7 +65,7 @@ export default function skillVisibility(pi: ExtensionAPI) {
|
|
|
105
65
|
pi.registerCommand("skillful", {
|
|
106
66
|
description: "Open the pi-skillful skill visibility menu.",
|
|
107
67
|
handler: async (_args, ctx) => {
|
|
108
|
-
if (
|
|
68
|
+
if (ctx.mode !== "tui") {
|
|
109
69
|
ctx.ui.notify("/skillful requires interactive UI", "warning");
|
|
110
70
|
return;
|
|
111
71
|
}
|
|
@@ -116,10 +76,12 @@ export default function skillVisibility(pi: ExtensionAPI) {
|
|
|
116
76
|
return;
|
|
117
77
|
}
|
|
118
78
|
|
|
119
|
-
const
|
|
79
|
+
const projectTrusted = ctx.isProjectTrusted();
|
|
80
|
+
const scoped = await readScopedSkillfulSettings(ctx.cwd, projectTrusted);
|
|
120
81
|
await ctx.ui.custom<void>((tui, theme, _keybindings, done) =>
|
|
121
82
|
new SkillfulVisibilityMenu({
|
|
122
83
|
cwd: ctx.cwd,
|
|
84
|
+
projectTrusted,
|
|
123
85
|
skills,
|
|
124
86
|
hiddenByScope: {
|
|
125
87
|
global: new Set(scoped.global.hiddenSkills),
|
|
@@ -140,7 +102,7 @@ export default function skillVisibility(pi: ExtensionAPI) {
|
|
|
140
102
|
theme,
|
|
141
103
|
tui,
|
|
142
104
|
notify: (message, type) => ctx.ui.notify(message, type),
|
|
143
|
-
onToggleSlotsChanged: () => refreshSessionSkillToggles(pi, ctx.cwd, ctx.ui),
|
|
105
|
+
onToggleSlotsChanged: () => refreshSessionSkillToggles(pi, ctx.cwd, projectTrusted, ctx.ui),
|
|
144
106
|
done,
|
|
145
107
|
}),
|
|
146
108
|
);
|
|
@@ -148,84 +110,14 @@ export default function skillVisibility(pi: ExtensionAPI) {
|
|
|
148
110
|
});
|
|
149
111
|
}
|
|
150
112
|
|
|
151
|
-
async function refreshHiddenSkillCache(cwd: string): Promise<Set<string>> {
|
|
152
|
-
const hidden = await readEffectiveHiddenSkills(cwd);
|
|
153
|
-
store.hiddenSkillsByCwd.set(cwd, hidden);
|
|
154
|
-
store.lastHiddenSkills = hidden;
|
|
155
|
-
return hidden;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// Must patch the real prototype from pi's module — separate module resolutions have distinct class identities.
|
|
159
|
-
function installStartupSkillListPatch(): void {
|
|
160
|
-
const realPrototype = (InteractiveMode as unknown as { prototype: InteractiveModeLike }).prototype;
|
|
161
|
-
if (!realPrototype) return;
|
|
162
|
-
|
|
163
|
-
const patchState = realPrototype as Record<PropertyKey, unknown>;
|
|
164
|
-
if (patchState[STARTUP_PATCH_KEY]) return;
|
|
165
|
-
|
|
166
|
-
const original = realPrototype.showLoadedResources;
|
|
167
|
-
if (typeof original !== "function") return;
|
|
168
|
-
|
|
169
|
-
realPrototype.showLoadedResources = function (this: InteractiveModeLike, options?: unknown): void {
|
|
170
|
-
const loader = this.session?.resourceLoader;
|
|
171
|
-
const originalGetSkills = loader?.getSkills;
|
|
172
|
-
if (!loader || typeof originalGetSkills !== "function") {
|
|
173
|
-
original.call(this, options);
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
const cwd = this.sessionManager?.getCwd?.();
|
|
178
|
-
|
|
179
|
-
let rawSkillNames: string[] = [];
|
|
180
|
-
loader.getSkills = () => {
|
|
181
|
-
const result = originalGetSkills.call(loader);
|
|
182
|
-
rawSkillNames = result.skills.map((s) => normalizeSkillName(s.name));
|
|
183
|
-
return result;
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const childrenBefore = this.loadedResourcesContainer?.children.length ?? 0;
|
|
187
|
-
|
|
188
|
-
try {
|
|
189
|
-
original.call(this, options);
|
|
190
|
-
} finally {
|
|
191
|
-
loader.getSkills = originalGetSkills;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
if (rawSkillNames.length === 0 || !cwd || !this.loadedResourcesContainer) return;
|
|
195
|
-
|
|
196
|
-
const children = this.loadedResourcesContainer.children;
|
|
197
|
-
for (let i = childrenBefore; i < children.length; i++) {
|
|
198
|
-
const child = children[i] as ExpandableTextLike | undefined;
|
|
199
|
-
if (!child || typeof child.getCollapsedText !== "function") continue;
|
|
200
|
-
const collapsed = child.getCollapsedText();
|
|
201
|
-
if (!collapsed.includes("[Skills]")) continue;
|
|
202
|
-
|
|
203
|
-
const colorized = buildColorizedSkillList(rawSkillNames, store.lastHiddenSkills, store.theme);
|
|
204
|
-
child.getCollapsedText = () => colorized;
|
|
205
|
-
child.setText(colorized);
|
|
206
|
-
break;
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
patchState[STARTUP_PATCH_KEY] = true;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function buildColorizedSkillList(names: string[], hidden: Set<string>, theme: Theme | null): string {
|
|
214
|
-
const sorted = [...names].sort((a, b) => a.localeCompare(b));
|
|
215
|
-
if (!theme) {
|
|
216
|
-
return `[Skills]\n ${sorted.join(", ")}`;
|
|
217
|
-
}
|
|
218
|
-
const header = theme.fg("mdHeading", "[Skills]");
|
|
219
|
-
const parts = sorted.map((n) => (hidden.has(n) ? theme.fg("error", n) : theme.fg("dim", n)));
|
|
220
|
-
return `${header}\n ${parts.join(", ")}`;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
113
|
function getSkillItems(pi: ExtensionAPI): SkillListItem[] {
|
|
224
114
|
return listLoadedSkills(pi.getCommands()).filter(isTopLevelSkill);
|
|
225
115
|
}
|
|
226
116
|
|
|
227
117
|
class SkillfulVisibilityMenu implements Component {
|
|
228
118
|
private readonly cwd: string;
|
|
119
|
+
private readonly projectTrusted: boolean;
|
|
120
|
+
private readonly scopes: SkillfulScope[];
|
|
229
121
|
private readonly skills: SkillListItem[];
|
|
230
122
|
private readonly hiddenByScope: HiddenSkillsByScope;
|
|
231
123
|
private readonly hiddenSkillsDefinedByScope: DefinedByScope;
|
|
@@ -238,12 +130,15 @@ class SkillfulVisibilityMenu implements Component {
|
|
|
238
130
|
private readonly done: () => void;
|
|
239
131
|
private readonly topBorder: DynamicBorder;
|
|
240
132
|
private readonly bottomBorder: DynamicBorder;
|
|
241
|
-
private scope: SkillfulScope
|
|
133
|
+
private scope: SkillfulScope;
|
|
242
134
|
private settingsList: SettingsList;
|
|
243
135
|
private saveQueue: Promise<void> = Promise.resolve();
|
|
244
136
|
|
|
245
137
|
constructor(options: SkillfulVisibilityMenuOptions) {
|
|
246
138
|
this.cwd = options.cwd;
|
|
139
|
+
this.projectTrusted = options.projectTrusted;
|
|
140
|
+
this.scopes = options.projectTrusted ? SCOPES : ["global"];
|
|
141
|
+
this.scope = options.projectTrusted ? "project" : "global";
|
|
247
142
|
this.skills = options.skills;
|
|
248
143
|
this.hiddenByScope = options.hiddenByScope;
|
|
249
144
|
this.hiddenSkillsDefinedByScope = options.hiddenSkillsDefinedByScope;
|
|
@@ -267,7 +162,7 @@ class SkillfulVisibilityMenu implements Component {
|
|
|
267
162
|
"",
|
|
268
163
|
...this.settingsList.render(width),
|
|
269
164
|
"",
|
|
270
|
-
truncateToWidth(this.theme.fg("dim",
|
|
165
|
+
truncateToWidth(this.theme.fg("dim", this.renderHelp()), width),
|
|
271
166
|
...this.bottomBorder.render(width),
|
|
272
167
|
];
|
|
273
168
|
}
|
|
@@ -297,7 +192,7 @@ class SkillfulVisibilityMenu implements Component {
|
|
|
297
192
|
}
|
|
298
193
|
|
|
299
194
|
private renderTabs(): string {
|
|
300
|
-
return
|
|
195
|
+
return this.scopes
|
|
301
196
|
.map((scope) => {
|
|
302
197
|
const label = scope === "global" ? "Global" : "Project";
|
|
303
198
|
return scope === this.scope
|
|
@@ -307,9 +202,14 @@ class SkillfulVisibilityMenu implements Component {
|
|
|
307
202
|
.join(" ");
|
|
308
203
|
}
|
|
309
204
|
|
|
205
|
+
private renderHelp(): string {
|
|
206
|
+
const scopeHelp = this.projectTrusted ? "Tab/←/→ switch scope · " : "";
|
|
207
|
+
return ` ${scopeHelp}1-9 assign/clear toggle · Enter/Space on/off · Esc close`;
|
|
208
|
+
}
|
|
209
|
+
|
|
310
210
|
private switchScope(direction: 1 | -1): void {
|
|
311
|
-
const current =
|
|
312
|
-
this.scope =
|
|
211
|
+
const current = this.scopes.indexOf(this.scope);
|
|
212
|
+
this.scope = this.scopes[(current + direction + this.scopes.length) % this.scopes.length];
|
|
313
213
|
this.settingsList = this.createSettingsList();
|
|
314
214
|
this.tui.requestRender();
|
|
315
215
|
}
|
|
@@ -431,7 +331,7 @@ class SkillfulVisibilityMenu implements Component {
|
|
|
431
331
|
.then(async () => {
|
|
432
332
|
if (scope === "project") await this.writeProjectOverrideOrInheritance();
|
|
433
333
|
else await writeHiddenSkills(scope, this.cwd, hiddenSnapshot);
|
|
434
|
-
await
|
|
334
|
+
await readEffectiveHiddenSkills(this.cwd, this.projectTrusted);
|
|
435
335
|
})
|
|
436
336
|
.catch((error) => {
|
|
437
337
|
this.notify(`Failed to save ${scope} skill visibility: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
@@ -465,13 +365,18 @@ class SkillfulVisibilityMenu implements Component {
|
|
|
465
365
|
if (this.projectMatchesGlobal()) {
|
|
466
366
|
this.hiddenSkillsDefinedByScope.project = false;
|
|
467
367
|
this.toggleSlotsDefinedByScope.project = false;
|
|
468
|
-
await writeProjectSkillfulOverride(this.cwd, undefined, undefined);
|
|
368
|
+
await writeProjectSkillfulOverride(this.cwd, undefined, undefined, this.projectTrusted);
|
|
469
369
|
return;
|
|
470
370
|
}
|
|
471
371
|
|
|
472
372
|
this.hiddenSkillsDefinedByScope.project = true;
|
|
473
373
|
this.toggleSlotsDefinedByScope.project = true;
|
|
474
|
-
await writeProjectSkillfulOverride(
|
|
374
|
+
await writeProjectSkillfulOverride(
|
|
375
|
+
this.cwd,
|
|
376
|
+
this.hiddenByScope.project,
|
|
377
|
+
this.toggleSlotsByScope.project,
|
|
378
|
+
this.projectTrusted,
|
|
379
|
+
);
|
|
475
380
|
}
|
|
476
381
|
|
|
477
382
|
private projectMatchesGlobal(): boolean {
|