pi-nested-skills 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,7 @@ A Pi extension that discovers skills recursively and provides convenient aliases
11
11
  - Keeps Pi's native skill loader responsible for frontmatter validation and skill-body expansion; this package does not duplicate `read` compatibility or skill expansion.
12
12
  - Extends the native slash-command completion list and preserves built-in Pi command suggestions.
13
13
  - Provides `/skills` to list discovered aliases.
14
- - Provides `/config:nested-skills` to edit the skill roots through the normal TUI input; use `reset` for the default `skills` root.
14
+ - Provides `/config:nested-skills` to edit the skill roots in a TUI settings panel; use `reset` for the default `skills` root.
15
15
  - Contributes discovered skill files through Pi's `resources_discover` event.
16
16
  - Uses `pi-extensions-i18n` for all user-visible messages.
17
17
 
@@ -44,7 +44,7 @@ Start from [`config.example.json`](./config.example.json) and write it to:
44
44
 
45
45
  A relative root is resolved relative to the Pi agent directory. Each direct child of a root is treated as a skill package, and every visible descendant containing `SKILL.md` is discovered. A root containing `SKILL.md` itself is also accepted as one package.
46
46
 
47
- Use `/config:nested-skills` to change the roots in the normal TUI input. Enter comma-separated paths, or use `reset` for the default root. Run `/reload` after saving.
47
+ Use `/config:nested-skills` to change the roots in a TUI settings panel. Changes are saved and applied immediately, so the skill index is rebuilt without `/reload`. Press Enter on the roots row to edit the comma-separated list, or run `reset` for the default root.
48
48
 
49
49
  ## Aliases and native expansion
50
50
 
package/README.zh-CN.md CHANGED
@@ -11,7 +11,7 @@ Pi 的嵌套技能扩展:递归发现多级 `SKILL.md`,并为嵌套技能提
11
11
  - 保留 Pi 原生技能加载器负责 frontmatter 校验和正文展开;不重复实现 `read` 兼容层或技能展开。
12
12
  - 将嵌套技能补全合并到 Pi 原生斜杠命令补全中,不影响内置命令。
13
13
  - 提供 `/skills` 列出发现到的全部别名。
14
- - 提供 `/config:nested-skills` 通过常规 TUI 输入配置技能根目录;输入 `reset` 恢复默认的 `skills` 根目录。
14
+ - 提供 `/config:nested-skills` 通过 TUI 配置面板配置技能根目录;执行 `reset` 恢复默认的 `skills` 根目录。
15
15
  - 通过 Pi 的 `resources_discover` 事件注册发现到的技能文件。
16
16
  - 所有用户可见文案都通过 `pi-extensions-i18n` 提供中英文版本。
17
17
 
@@ -44,7 +44,7 @@ pi install npm:pi-nested-skills
44
44
 
45
45
  相对路径以 Pi agent 目录为基准。根目录的每个直接子目录视为一个技能包,扩展会递归发现其中所有可见目录下的 `SKILL.md`。如果根目录本身包含 `SKILL.md`,也会把它作为一个技能包处理。
46
46
 
47
- 使用 `/config:nested-skills` 在 TUI 输入框中修改配置,多个目录用逗号分隔;输入 `reset` 恢复默认目录。保存后执行 `/reload`。
47
+ 使用 `/config:nested-skills` 在 TUI 配置面板中修改技能根目录,改动即时保存并生效,会自动重建技能索引,不需要 `/reload`。在根目录那行回车编辑逗号分隔的列表,或执行 `reset` 恢复默认目录。
48
48
 
49
49
  ## 别名与原生展开
50
50
 
package/SKILL.md CHANGED
@@ -21,7 +21,7 @@ extensions/pi-nested-skills/config.json
21
21
 
22
22
  优先复制包内 [`config.example.json`](./config.example.json),再修改 `skillRoots`。路径可以是绝对路径、`~/` 路径,或相对于 Pi agent 目录的路径。
23
23
 
24
- 也可以使用 `/config:nested-skills` 修改配置;命令会在 TUI 输入框中接收逗号分隔的目录,输入 `reset` 恢复默认值。保存后执行 `/reload`。
24
+ 也可以使用 `/config:nested-skills` 打开 TUI 配置面板修改配置;在技能根目录那行回车编辑逗号分隔的目录,执行 `reset` 恢复默认值。改动即时保存并生效,会自动重建技能索引,不需要 `/reload`。
25
25
 
26
26
  别名形式为 `/技能包:子目录.子目录`,例如 `/development:code-reviewer` 或 `/design:icons.favicon`。也支持 `/skill:技能包.子目录`;扩展会把它转换为 Pi 原生 `/skill:<name>`,由 Pi 负责技能正文展开。
27
27
 
@@ -31,12 +31,8 @@
31
31
  "zh-CN": "配置嵌套技能根目录",
32
32
  "en-US": "Configure nested skill roots"
33
33
  },
34
- "configCommandInput": {
35
- "zh-CN": "输入技能根目录(多个目录用逗号分隔)",
36
- "en-US": "Enter skill roots (separate multiple roots with commas)"
37
- },
38
34
  "configCommandUsage": {
39
- "zh-CN": "用法:/config:nested-skills(打开配置输入)或 /config:nested-skills reset(恢复默认值)",
35
+ "zh-CN": "用法:/config:nested-skills(打开配置面板)或 /config:nested-skills reset(恢复默认值)",
40
36
  "en-US": "Usage: /config:nested-skills (open settings) or /config:nested-skills reset (restore defaults)"
41
37
  },
42
38
  "configCommandInteractiveOnly": {
@@ -44,11 +40,35 @@
44
40
  "en-US": "Nested skill configuration requires the TUI; run this command in an interactive Pi session."
45
41
  },
46
42
  "configCommandSaved": {
47
- "zh-CN": "嵌套技能配置已保存:{path}。请执行 /reload 使配置生效。",
48
- "en-US": "Nested skill configuration saved to {path}. Run /reload to apply it."
43
+ "zh-CN": "嵌套技能配置已保存:{path}。",
44
+ "en-US": "Nested skill configuration saved to {path}."
49
45
  },
50
46
  "configCommandInvalid": {
51
47
  "zh-CN": "嵌套技能配置无效:{error}",
52
48
  "en-US": "Invalid nested skill configuration: {error}"
49
+ },
50
+ "configSaveFailed": {
51
+ "zh-CN": "嵌套技能配置保存失败:{error}",
52
+ "en-US": "Failed to save nested skill configuration: {error}"
53
+ },
54
+ "configMenuTitle": {
55
+ "zh-CN": "嵌套技能配置",
56
+ "en-US": "Nested skill settings"
57
+ },
58
+ "configLabelSkillRoots": {
59
+ "zh-CN": "技能根目录",
60
+ "en-US": "Skill roots"
61
+ },
62
+ "configDescSkillRoots": {
63
+ "zh-CN": "逗号分隔的目录列表,递归扫描其中的 SKILL.md。",
64
+ "en-US": "Comma-separated directories scanned recursively for SKILL.md files."
65
+ },
66
+ "configInputPrompt": {
67
+ "zh-CN": "输入",
68
+ "en-US": "Input"
69
+ },
70
+ "configValueUnset": {
71
+ "zh-CN": "未设置",
72
+ "en-US": "unset"
53
73
  }
54
74
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-nested-skills",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Pi extension for recursive nested-skill discovery and aliases",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -62,7 +62,7 @@
62
62
  "@earendil-works/pi-tui": ">=0.80.0"
63
63
  },
64
64
  "dependencies": {
65
- "pi-extensions-i18n": "^0.7.0"
65
+ "pi-extensions-i18n": "^0.9.0"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@earendil-works/pi-coding-agent": "0.85.1",
@@ -0,0 +1,180 @@
1
+ /**
2
+ * pi-nested-skills 的 TUI 配置面板。
3
+ *
4
+ * 原来用一次 `ctx.ui.input` 让用户手输根目录,看不到当前值也看不出哪项能改。
5
+ * 这里改用 Pi 自带 `SettingsList`:左边字段名、右边当前值、选中项下方给一行说明;
6
+ * 技能根目录是字符串数组,面板上以逗号分隔显示为一行,回车打开预填输入框编辑整串,
7
+ * 提交后按逗号切分回数组。改一项立刻写盘并同步运行期配置,Esc 关闭即可生效。
8
+ */
9
+
10
+ import {
11
+ getSettingsListTheme,
12
+ type ExtensionCommandContext,
13
+ } from "@earendil-works/pi-coding-agent";
14
+ import {
15
+ Container,
16
+ Input,
17
+ SettingsList,
18
+ Text,
19
+ type Component,
20
+ type SettingItem,
21
+ } from "@earendil-works/pi-tui";
22
+ import { parseConfig, type NestedSkillsConfig } from "./config.ts";
23
+ import { i18n } from "./i18n.ts";
24
+
25
+ /** 面板字段名,同时用作 `SettingsList` 的行 id。 */
26
+ export const PANEL_FIELD = {
27
+ skillRoots: "skillRoots",
28
+ } as const;
29
+
30
+ /** 面板字段名的联合类型。 */
31
+ export type PanelFieldId = (typeof PANEL_FIELD)[keyof typeof PANEL_FIELD];
32
+
33
+ /** 面板覆盖的配置字段名,测试用它核对没有字段漏在面板外。 */
34
+ export const PANEL_FIELD_IDS: readonly string[] = [PANEL_FIELD.skillRoots];
35
+
36
+ /** 根目录数组在面板上的一行文本:逗号加空格分隔,与命令行输入保持一致。 */
37
+ const ROOT_DISPLAY_SEPARATOR = ", ";
38
+
39
+ /** 根目录数组在输入框里按逗号切分。 */
40
+ const ROOT_INPUT_SEPARATOR = ",";
41
+
42
+ /** 面板项:目前只有字符串数组一种取值方式。 */
43
+ interface PanelFieldSpec {
44
+ /** 字段名。 */
45
+ id: PanelFieldId;
46
+ /** 字段标签的本地化 key。 */
47
+ labelKey: string;
48
+ /** 说明行的本地化 key。 */
49
+ descriptionKey: string;
50
+ }
51
+
52
+ /** 面板顺序。 */
53
+ const PANEL_FIELDS: readonly PanelFieldSpec[] = [
54
+ { id: PANEL_FIELD.skillRoots, labelKey: "configLabelSkillRoots", descriptionKey: "configDescSkillRoots" },
55
+ ];
56
+
57
+ /** 根目录数组转面板上的一行文本。 */
58
+ export function rootsToText(roots: readonly string[]): string {
59
+ return roots.join(ROOT_DISPLAY_SEPARATOR);
60
+ }
61
+
62
+ /** 面板上的一行文本转根目录数组:按逗号切分、去空白、丢掉空项。 */
63
+ export function textToRoots(text: string): string[] {
64
+ return text
65
+ .split(ROOT_INPUT_SEPARATOR)
66
+ .map((root) => root.trim())
67
+ .filter(Boolean);
68
+ }
69
+
70
+ /** 当前配置里某个字段的取值。 */
71
+ function readField(config: NestedSkillsConfig, id: PanelFieldId): string {
72
+ switch (id) {
73
+ case PANEL_FIELD.skillRoots:
74
+ return rootsToText(config.skillRoots);
75
+ }
76
+ }
77
+
78
+ /** 自由文本二级菜单:输入框预填当前值,Enter 提交、Esc 取消。 */
79
+ function createTextSubmenu(
80
+ value: string,
81
+ placeholder: string,
82
+ done: (value?: string) => void,
83
+ ): Component {
84
+ const input = new Input({ prompt: `${i18n.t("configInputPrompt")} `, placeholder });
85
+ input.setValue(value);
86
+ input.focused = true;
87
+ input.onSubmit = (submitted) => done(submitted);
88
+ input.onEscape = () => done(undefined);
89
+ return {
90
+ /** 一行可编辑输入。 */
91
+ render: (width) => input.render(width),
92
+ /** 输入框没有共享缓存。 */
93
+ invalidate: () => input.invalidate(),
94
+ /** 所有按键都归输入框。 */
95
+ handleInput: (data) => input.handleInput(data),
96
+ };
97
+ }
98
+
99
+ /** 把当前配置转成 Pi 设置列表的条目。 */
100
+ export function buildSettingItems(config: NestedSkillsConfig): SettingItem[] {
101
+ return PANEL_FIELDS.map((field) => {
102
+ const label = i18n.t(field.labelKey);
103
+ const description = i18n.t(field.descriptionKey);
104
+ const stored = readField(config, field.id);
105
+ return {
106
+ id: field.id,
107
+ label,
108
+ description,
109
+ currentValue: stored || i18n.t("configValueUnset"),
110
+ // SettingsList 的 submenu 用 done 回传输入;这里编辑整串再切分回数组。
111
+ submenu: (_currentLabel, done) => createTextSubmenu(stored, description, done),
112
+ };
113
+ });
114
+ }
115
+
116
+ /**
117
+ * 把面板返回的文本写回配置;无法识别时返回 undefined,由调用方保持不变。
118
+ *
119
+ * 结果统一过一遍 parseConfig,写盘的值仍满足现有校验规则(只能是字符串数组),
120
+ * 面板与校验不会各走各的。空输入得到空数组,面板显示为「未设置」。
121
+ */
122
+ export function applyPanelChange(
123
+ config: NestedSkillsConfig,
124
+ id: string,
125
+ value: string,
126
+ ): NestedSkillsConfig | undefined {
127
+ const field = PANEL_FIELDS.find((candidate) => candidate.id === id);
128
+ if (!field) return undefined;
129
+ try {
130
+ return parseConfig({ skillRoots: textToRoots(value) });
131
+ } catch {
132
+ // 输入格式非法时保持原值,面板不写入坏配置。
133
+ return undefined;
134
+ }
135
+ }
136
+
137
+ /** 面板与宿主之间的接口;配置读写与生效动作都由入口注入。 */
138
+ export interface ConfigPanelHandlers {
139
+ /** 读取当前配置。 */
140
+ getConfig(): NestedSkillsConfig;
141
+ /** 某一项被改动后调用,由入口负责保存、同步运行期配置并重绘。 */
142
+ onChange(config: NestedSkillsConfig): void;
143
+ }
144
+
145
+ /** 打开配置面板;用户改动即时生效,Esc 关闭。 */
146
+ export async function openConfigPanel(
147
+ ctx: ExtensionCommandContext,
148
+ handlers: ConfigPanelHandlers,
149
+ ): Promise<void> {
150
+ await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
151
+ const container = new Container();
152
+ container.addChild(new Text(theme.fg("accent", theme.bold(i18n.t("configMenuTitle"))), 1, 1));
153
+
154
+ const settingsList = new SettingsList(
155
+ buildSettingItems(handlers.getConfig()),
156
+ PANEL_FIELDS.length,
157
+ getSettingsListTheme(),
158
+ (id, newValue) => {
159
+ const next = applyPanelChange(handlers.getConfig(), id, newValue);
160
+ if (next) handlers.onChange(next);
161
+ },
162
+ () => done(undefined),
163
+ );
164
+ container.addChild(settingsList);
165
+
166
+ // ctx.ui.custom 要求返回 Component:整体布局交给 Container,键盘转给设置列表,
167
+ // 列表改了数据后必须 requestRender,否则画面停在上一帧。
168
+ return {
169
+ /** 面板整体按 Container 布局渲染。 */
170
+ render: (width) => container.render(width),
171
+ /** 无本地缓存,交给 Container 清理。 */
172
+ invalidate: () => container.invalidate(),
173
+ /** 键盘输入交给设置列表,并触发一次重绘。 */
174
+ handleInput: (data) => {
175
+ settingsList.handleInput(data);
176
+ tui.requestRender();
177
+ },
178
+ };
179
+ });
180
+ }
package/src/i18n.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { createTranslator, loadCatalog } from "pi-extensions-i18n";
2
+
3
+ export const i18n = createTranslator(loadCatalog(new URL("../locales/index.json", import.meta.url)));
package/src/index.ts CHANGED
@@ -6,24 +6,22 @@ import type {
6
6
  import { fuzzyFilter } from "@earendil-works/pi-tui";
7
7
  import type { AutocompleteItem, AutocompleteProvider } from "@earendil-works/pi-tui";
8
8
  import {
9
- createTranslator,
10
- loadCatalog,
9
+ NOTICE_TAG_COLOR,
11
10
  installNoticeRenderer,
12
11
  notifyWithSource,
13
12
  type NoticeColor,
14
13
  type NoticeLevel,
15
14
  type NoticeSource,
16
15
  } from "pi-extensions-i18n";
17
- import { loadConfig, parseConfig, saveConfig, type LoadedNestedSkillsConfig } from "./config.ts";
16
+ import { loadConfig, parseConfig, saveConfig, type LoadedNestedSkillsConfig, type NestedSkillsConfig } from "./config.ts";
17
+ import { openConfigPanel } from "./config-panel.ts";
18
+ import { i18n } from "./i18n.ts";
18
19
  import { scanSkillRoots, type NestedSkill, type SkillScanResult } from "./skills.ts";
19
20
 
20
- const messages = loadCatalog(new URL("../locales/index.json", import.meta.url));
21
- const i18n = createTranslator(messages);
22
-
23
21
  /** 本扩展的提示标签;短且唯一,便于在会话里定位来源。 */
24
22
  const NOTICE_TAG = "skills";
25
- /** 提示标签颜色;与其它扩展错开,避免看起来像同一条消息。 */
26
- const NOTICE_COLOR: NoticeColor = "success";
23
+ /** 提示标签颜色:所有扩展统一用弱化色,来源靠 tag 文本区分,不靠颜色。 */
24
+ const NOTICE_COLOR: NoticeColor = NOTICE_TAG_COLOR;
27
25
  /** 本扩展的提示来源。 */
28
26
  const NOTICE_SOURCE: NoticeSource = { tag: NOTICE_TAG, color: NOTICE_COLOR };
29
27
 
@@ -32,8 +30,6 @@ const COMMAND_ALIASES = [COMMAND_NAME] as const;
32
30
  const CONFIG_COMMAND_ALIASES = ["config:nested-skills", "nested-skills-config", "pi-nested-skills-config"] as const;
33
31
  const CONFIG_RESET_COMMAND = "reset";
34
32
  const DEFAULT_CONFIG_ROOT = "skills";
35
- const ROOT_INPUT_SEPARATOR = ",";
36
- const ROOT_DISPLAY_SEPARATOR = ", ";
37
33
  const NOTICE_WARNING: NoticeLevel = "warning";
38
34
  const NOTICE_INFO: NoticeLevel = "info";
39
35
  const NOTICE_ERROR: NoticeLevel = "error";
@@ -222,8 +218,45 @@ function formatSkillsMessage(index: SkillIndex): string {
222
218
  return lines.join("\n");
223
219
  }
224
220
 
225
- /** 注册配置命令,通过 TUI 输入逗号分隔的技能根目录。 */
226
- function registerConfigCommand(pi: ExtensionAPI): void {
221
+ /**
222
+ * 运行期状态:配置和扫描索引都放在这里。
223
+ *
224
+ * 配置面板改完根目录后直接就地重建,让 resources_discover、输入转换、补全和 /skills
225
+ * 都看到新索引;否则用户改完配置看起来没反应,只能 /reload。
226
+ */
227
+ interface RuntimeState {
228
+ loaded: LoadedNestedSkillsConfig;
229
+ index: SkillIndex;
230
+ }
231
+
232
+ /** 按根目录重新扫描并重建索引,同时刷新运行期配置。 */
233
+ function rebuildIndex(state: RuntimeState, config: NestedSkillsConfig): void {
234
+ state.loaded = { config, source: "file", warnings: [], explicit: true };
235
+ state.index = buildSkillIndex(scanSkillRoots(config.skillRoots));
236
+ }
237
+
238
+ /** 保存一项配置并令其立即生效;写盘失败也要把新配置用在本次会话里。 */
239
+ function applyConfig(state: RuntimeState, config: NestedSkillsConfig, ctx: ExtensionCommandContext): void {
240
+ rebuildIndex(state, config);
241
+ try {
242
+ saveConfig(config);
243
+ } catch (error) {
244
+ notify(ctx, i18n.t("configSaveFailed", {
245
+ error: error instanceof Error ? error.message : String(error),
246
+ }), NOTICE_ERROR);
247
+ }
248
+ }
249
+
250
+ /** 打开 TUI 配置面板;每改一项立即写盘并重建技能索引。 */
251
+ async function openSkillsConfigPanel(state: RuntimeState, ctx: ExtensionCommandContext): Promise<void> {
252
+ await openConfigPanel(ctx, {
253
+ getConfig: () => state.loaded.config,
254
+ onChange: (config) => applyConfig(state, config, ctx),
255
+ });
256
+ }
257
+
258
+ /** 注册配置命令:无参数打开面板,`reset` 恢复默认值。 */
259
+ function registerConfigCommand(pi: ExtensionAPI, state: RuntimeState): void {
227
260
  const command = {
228
261
  description: i18n.t("configCommandDescription"),
229
262
  getArgumentCompletions: () => [{ value: CONFIG_RESET_COMMAND, label: CONFIG_RESET_COMMAND }],
@@ -235,7 +268,9 @@ function registerConfigCommand(pi: ExtensionAPI): void {
235
268
  }
236
269
  if (argument === CONFIG_RESET_COMMAND) {
237
270
  try {
238
- const path = saveConfig(parseConfig({ skillRoots: [DEFAULT_CONFIG_ROOT] }));
271
+ const config = parseConfig({ skillRoots: [DEFAULT_CONFIG_ROOT] });
272
+ const path = saveConfig(config);
273
+ rebuildIndex(state, loadConfig().config);
239
274
  notify(ctx, i18n.t("configCommandSaved", { path }), NOTICE_INFO);
240
275
  } catch (error) {
241
276
  notify(ctx, i18n.t("configCommandInvalid", {
@@ -248,33 +283,18 @@ function registerConfigCommand(pi: ExtensionAPI): void {
248
283
  notify(ctx, i18n.t("configCommandInteractiveOnly"), NOTICE_WARNING);
249
284
  return;
250
285
  }
251
-
252
- const current = loadConfig().config;
253
- const input = await ctx.ui.input(
254
- i18n.t("configCommandInput"),
255
- current.skillRoots.join(ROOT_DISPLAY_SEPARATOR),
256
- );
257
- if (input === undefined) return;
258
- try {
259
- const roots = input.split(ROOT_INPUT_SEPARATOR).map((root) => root.trim()).filter(Boolean);
260
- const path = saveConfig(parseConfig({ skillRoots: roots }));
261
- notify(ctx, i18n.t("configCommandSaved", { path }), NOTICE_INFO);
262
- } catch (error) {
263
- notify(ctx, i18n.t("configCommandInvalid", {
264
- error: error instanceof Error ? error.message : String(error),
265
- }), NOTICE_ERROR);
266
- }
286
+ await openSkillsConfigPanel(state, ctx);
267
287
  },
268
288
  };
269
289
  for (const name of CONFIG_COMMAND_ALIASES) pi.registerCommand(name, command);
270
290
  }
271
291
 
272
- function registerSkillsCommand(pi: ExtensionAPI, index: SkillIndex): void {
292
+ function registerSkillsCommand(pi: ExtensionAPI, state: RuntimeState): void {
273
293
  const command = {
274
294
  description: i18n.t("commandDescription"),
275
- getArgumentCompletions: (): AutocompleteItem[] => commandItems(index),
295
+ getArgumentCompletions: (): AutocompleteItem[] => commandItems(state.index),
276
296
  handler: async (_args: string, _ctx: SkillCommandContext) => {
277
- await pi.sendUserMessage(formatSkillsMessage(index), { deliverAs: "followUp" });
297
+ await pi.sendUserMessage(formatSkillsMessage(state.index), { deliverAs: "followUp" });
278
298
  },
279
299
  };
280
300
 
@@ -307,18 +327,20 @@ export default function nestedSkillsExtension(pi: ExtensionAPI): void {
307
327
  // 提示画成会话区里的带底色消息块;渲染器在本包这个模块实例里注册一次。
308
328
  installNoticeRenderer(pi);
309
329
  const loaded = loadConfig();
310
- const scan = scanSkillRoots(loaded.config.skillRoots);
311
- const index = buildSkillIndex(scan);
330
+ const state: RuntimeState = {
331
+ loaded,
332
+ index: buildSkillIndex(scanSkillRoots(loaded.config.skillRoots)),
333
+ };
312
334
 
313
335
  pi.on("resources_discover", () => ({
314
336
  // 每个 SKILL.md 单独交给原生 loader,绕过“父目录含 SKILL.md 后停止递归”的规则,
315
337
  // 同时保留 Pi 原生的 frontmatter 校验、正文展开和资源来源信息。
316
- skillPaths: index.skillPaths,
338
+ skillPaths: state.index.skillPaths,
317
339
  }));
318
340
 
319
341
  pi.on("input", (event) => {
320
342
  if (!INPUT_SOURCES.has(event.source)) return { action: "continue" as const };
321
- const transformed = transformSkillInput(event.text, index);
343
+ const transformed = transformSkillInput(event.text, state.index);
322
344
  if (!transformed) return { action: "continue" as const };
323
345
  return {
324
346
  action: "transform" as const,
@@ -328,13 +350,14 @@ export default function nestedSkillsExtension(pi: ExtensionAPI): void {
328
350
  });
329
351
 
330
352
  pi.on("session_start", (_event, ctx) => {
331
- notifyDiagnostics(ctx, loaded, index);
332
- if (index.aliases.size === 0) return;
333
- ctx.ui.addAutocompleteProvider((current) => createAutocompleteProvider(current, index));
353
+ notifyDiagnostics(ctx, state.loaded, state.index);
354
+ if (state.index.aliases.size === 0) return;
355
+ // 每次会话开始都按当前索引重新注册,配置面板改了根目录后补全才能跟上。
356
+ ctx.ui.addAutocompleteProvider((current) => createAutocompleteProvider(current, state.index));
334
357
  });
335
358
 
336
- registerConfigCommand(pi);
337
- registerSkillsCommand(pi, index);
359
+ registerConfigCommand(pi, state);
360
+ registerSkillsCommand(pi, state);
338
361
  }
339
362
 
340
363
  export { buildSkillIndex, createAutocompleteProvider, formatSkillsMessage };