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 +2 -2
- package/README.zh-CN.md +2 -2
- package/SKILL.md +1 -1
- package/locales/index.json +27 -7
- package/package.json +2 -2
- package/src/config-panel.ts +180 -0
- package/src/i18n.ts +3 -0
- package/src/index.ts +64 -41
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
|
|
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
|
|
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`
|
|
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
|
|
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`
|
|
24
|
+
也可以使用 `/config:nested-skills` 打开 TUI 配置面板修改配置;在技能根目录那行回车编辑逗号分隔的目录,执行 `reset` 恢复默认值。改动即时保存并生效,会自动重建技能索引,不需要 `/reload`。
|
|
25
25
|
|
|
26
26
|
别名形式为 `/技能包:子目录.子目录`,例如 `/development:code-reviewer` 或 `/design:icons.favicon`。也支持 `/skill:技能包.子目录`;扩展会把它转换为 Pi 原生 `/skill:<name>`,由 Pi 负责技能正文展开。
|
|
27
27
|
|
package/locales/index.json
CHANGED
|
@@ -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
|
|
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}
|
|
48
|
-
"en-US": "Nested skill configuration saved to {path}.
|
|
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.
|
|
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.
|
|
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
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
|
-
|
|
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 =
|
|
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
|
-
/**
|
|
226
|
-
|
|
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
|
|
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,
|
|
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
|
|
311
|
-
|
|
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
|
-
|
|
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,
|
|
359
|
+
registerConfigCommand(pi, state);
|
|
360
|
+
registerSkillsCommand(pi, state);
|
|
338
361
|
}
|
|
339
362
|
|
|
340
363
|
export { buildSkillIndex, createAutocompleteProvider, formatSkillsMessage };
|