@buyi1net/pi-toolkit 0.0.1
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/LICENSE +21 -0
- package/README.md +45 -0
- package/assembler.ts +127 -0
- package/config.ts +201 -0
- package/i18n.ts +693 -0
- package/index.ts +99 -0
- package/menu/items.ts +264 -0
- package/menu/panels.ts +91 -0
- package/menu/settings-list.ts +84 -0
- package/menu/theme.ts +31 -0
- package/menu/toolkit-menu.ts +139 -0
- package/module.ts +156 -0
- package/modules/eyes/chain.ts +173 -0
- package/modules/eyes/config.ts +319 -0
- package/modules/eyes/index.ts +161 -0
- package/modules/eyes/menu.ts +828 -0
- package/modules/eyes/pi-model-backend.ts +245 -0
- package/modules/eyes/resilience.ts +161 -0
- package/modules/eyes/vision-bridge.ts +232 -0
- package/modules/eyes/vision-cache.ts +86 -0
- package/modules/eyes/vision-json.ts +19 -0
- package/modules/eyes/vision-preprocess.ts +192 -0
- package/modules/eyes/vision-probe.ts +21 -0
- package/modules/eyes/vision-prompt.ts +90 -0
- package/modules/eyes/vision-tool.ts +59 -0
- package/modules/eyes/vision-types.ts +33 -0
- package/modules/index.ts +10 -0
- package/modules/subagents/agents/researcher.md +51 -0
- package/modules/subagents/agents/scout.md +40 -0
- package/modules/subagents/agents/worker.md +79 -0
- package/modules/subagents/config.json.example +6 -0
- package/modules/subagents/config.ts +144 -0
- package/modules/subagents/index.ts +91 -0
- package/modules/subagents/menu.ts +388 -0
- package/modules/subagents/src/activity.ts +511 -0
- package/modules/subagents/src/agents.ts +126 -0
- package/modules/subagents/src/command.ts +37 -0
- package/modules/subagents/src/dependencies.ts +246 -0
- package/modules/subagents/src/diagnostics.ts +13 -0
- package/modules/subagents/src/display.ts +94 -0
- package/modules/subagents/src/headless.ts +342 -0
- package/modules/subagents/src/herdr.ts +203 -0
- package/modules/subagents/src/index.ts +2798 -0
- package/modules/subagents/src/inspect-tool.ts +839 -0
- package/modules/subagents/src/launch-config.ts +196 -0
- package/modules/subagents/src/layout-budget.ts +26 -0
- package/modules/subagents/src/list-tool.ts +292 -0
- package/modules/subagents/src/message-tool.ts +808 -0
- package/modules/subagents/src/names.ts +17 -0
- package/modules/subagents/src/pane-layout.ts +49 -0
- package/modules/subagents/src/params.ts +145 -0
- package/modules/subagents/src/renderers.ts +181 -0
- package/modules/subagents/src/result.ts +43 -0
- package/modules/subagents/src/retention.ts +114 -0
- package/modules/subagents/src/route-error.ts +212 -0
- package/modules/subagents/src/routing.ts +235 -0
- package/modules/subagents/src/runtime-registry.ts +159 -0
- package/modules/subagents/src/session.ts +801 -0
- package/modules/subagents/src/status.ts +513 -0
- package/modules/subagents/src/stop-tool.ts +235 -0
- package/modules/subagents/src/subagent-done.ts +590 -0
- package/modules/subagents/src/subagent-tool.ts +1155 -0
- package/modules/subagents/src/surface.ts +355 -0
- package/modules/subagents/src/team-dispatch-tool.ts +219 -0
- package/modules/subagents/src/team.ts +232 -0
- package/modules/subagents/src/tmux.ts +210 -0
- package/modules/subagents/src/tools/safe-bash.ts +72 -0
- package/modules/subagents/src/types.ts +131 -0
- package/modules/tui/adapter/provider-usage.ts +143 -0
- package/modules/tui/config.ts +50 -0
- package/modules/tui/index.ts +120 -0
- package/modules/tui/kernel/pkg/shared/grok-subscription.ts +169 -0
- package/modules/tui/kernel/pkg/shared/official-subscription.ts +237 -0
- package/modules/tui/kernel/pkg/shared/provider-catalog.ts +367 -0
- package/modules/tui/kernel/pkg/shared/provider-contracts.ts +150 -0
- package/modules/tui/kernel/pkg/shared/provider-display.ts +55 -0
- package/modules/tui/kernel/pkg/shared/provider-parsers.ts +171 -0
- package/modules/tui/kernel/pkg/shared/volcengine.ts +191 -0
- package/modules/tui/kernel/pkg/shared/zhipu.ts +149 -0
- package/modules/tui/kernel/pkg/usage-core/index.ts +335 -0
- package/modules/tui/kernel/pkg/usage-core/provider-routes.ts +187 -0
- package/modules/tui/kernel/pkg/usage-node/index.ts +635 -0
- package/modules/tui/kernel/pkg/usage-node/provider-usage.ts +388 -0
- package/modules/tui/kernel/usage-core.ts +2 -0
- package/modules/tui/kernel/usage-node.ts +2 -0
- package/modules/tui/menu.ts +418 -0
- package/modules/tui/plugin/editor.ts +396 -0
- package/modules/tui/plugin/footer.ts +161 -0
- package/modules/tui/plugin/index.ts +30 -0
- package/modules/tui/plugin/lifecycle.ts +637 -0
- package/modules/tui/plugin/package-order.ts +169 -0
- package/modules/tui/plugin/screen-transition.ts +203 -0
- package/modules/tui/plugin/settings-config.ts +297 -0
- package/modules/tui/plugin/status-sources.ts +49 -0
- package/modules/tui/plugin/transition-gate.ts +261 -0
- package/modules/tui/renderer/custom-header.ts +157 -0
- package/modules/tui/renderer/editor.ts +70 -0
- package/modules/tui/renderer/header.ts +72 -0
- package/modules/tui/renderer/icons.ts +149 -0
- package/modules/tui/renderer/pi-installer-logo.ts +194 -0
- package/modules/tui/status/auto-compaction.ts +67 -0
- package/modules/tui/status/project-status.ts +655 -0
- package/modules/tui/status/provider-status.ts +120 -0
- package/modules/tui/status/runtime-status.ts +307 -0
- package/modules/tui/status/session-status.ts +325 -0
- package/modules/tui/status/status-config.ts +95 -0
- package/modules/tui/status/status-segments.ts +263 -0
- package/modules/tui/status/turn-telemetry.ts +466 -0
- package/modules/tui/themes/LICENSE.pi-themes-bundle +21 -0
- package/modules/tui/themes/UPSTREAM.md +7 -0
- package/modules/tui/themes/catppuccin-latte.json +80 -0
- package/modules/tui/themes/catppuccin-mocha.json +79 -0
- package/modules/tui/themes/crimson-noir.json +85 -0
- package/modules/tui/themes/dracula.json +79 -0
- package/modules/tui/themes/everforest-dark.json +85 -0
- package/modules/tui/themes/gruvbox-dark.json +85 -0
- package/modules/tui/themes/gruvbox-light.json +85 -0
- package/modules/tui/themes/matrix.json +85 -0
- package/modules/tui/themes/nord.json +85 -0
- package/modules/tui/themes/one-dark.json +85 -0
- package/modules/tui/themes/rose-pine-dawn.json +85 -0
- package/modules/tui/themes/rose-pine.json +85 -0
- package/modules/tui/themes/solarized-dark.json +85 -0
- package/modules/tui/themes/solarized-light.json +85 -0
- package/modules/tui/themes/tokyo-night-storm.json +79 -0
- package/modules/tui/themes/tokyo-night.json +79 -0
- package/package.json +28 -0
- package/services.ts +38 -0
- package/toolkit.ts +147 -0
package/index.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// pi-toolkit 入口:装配模块,注册 /pi-toolkit 控制菜单。
|
|
2
|
+
// 菜单用 pi 原生 SettingsList + getSettingsListTheme()(与原生 /settings 同组件同主题)。
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getAgentDir,
|
|
6
|
+
type ExtensionAPI,
|
|
7
|
+
type ExtensionCommandContext,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import { languageLabelKey } from "./i18n.ts";
|
|
10
|
+
import { groupItemId } from "./menu/items.ts";
|
|
11
|
+
import { createMenuTheme } from "./menu/theme.ts";
|
|
12
|
+
import { applyMenuChange, ToolkitMenu } from "./menu/toolkit-menu.ts";
|
|
13
|
+
import { ENABLED_FIELD } from "./module.ts";
|
|
14
|
+
import { BUILT_IN_MODULES } from "./modules/index.ts";
|
|
15
|
+
import { createToolkit, type Toolkit } from "./toolkit.ts";
|
|
16
|
+
|
|
17
|
+
function describeError(error: unknown): string {
|
|
18
|
+
return error instanceof Error ? error.message : String(error);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default async function piToolkit(pi: ExtensionAPI): Promise<void> {
|
|
22
|
+
const toolkit = await createToolkit({
|
|
23
|
+
pi,
|
|
24
|
+
agentDir: getAgentDir(),
|
|
25
|
+
modules: BUILT_IN_MODULES,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
registerControlCommand(pi, toolkit);
|
|
29
|
+
reportProblems(pi, toolkit);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function reportProblems(pi: ExtensionAPI, toolkit: Toolkit): void {
|
|
33
|
+
if (toolkit.problems.length === 0) return;
|
|
34
|
+
pi.on("session_start", (_event, ctx) => {
|
|
35
|
+
if (!ctx.hasUI) return;
|
|
36
|
+
const t = toolkit.getTranslator();
|
|
37
|
+
for (const problem of toolkit.problems) {
|
|
38
|
+
const label = t(problem.kind === "config" ? "problem.config" : "problem.module");
|
|
39
|
+
ctx.ui.notify(
|
|
40
|
+
t("problem.summary", { label, source: problem.source, detail: problem.detail }),
|
|
41
|
+
"warning",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function registerControlCommand(pi: ExtensionAPI, toolkit: Toolkit): void {
|
|
48
|
+
pi.registerCommand("pi-toolkit", {
|
|
49
|
+
// 命令说明在注册时定稿(命令表不支持随语言重建),取装载时的配置语言。
|
|
50
|
+
description: toolkit.getTranslator()("command.description"),
|
|
51
|
+
handler: async (_args: string, ctx: ExtensionCommandContext): Promise<void> => {
|
|
52
|
+
const t = () => toolkit.getTranslator();
|
|
53
|
+
if (!ctx.hasUI) {
|
|
54
|
+
ctx.ui.notify(t()("notify.nonInteractive"), "warning");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let menu: ToolkitMenu | undefined;
|
|
59
|
+
|
|
60
|
+
const handleChange = async (id: string, value: string): Promise<void> => {
|
|
61
|
+
try {
|
|
62
|
+
const result = await applyMenuChange(toolkit, id, value);
|
|
63
|
+
if (result.kind === "ignored") return;
|
|
64
|
+
|
|
65
|
+
if (result.kind === "language") {
|
|
66
|
+
const name = t()(languageLabelKey(result.language));
|
|
67
|
+
ctx.ui.notify(t()("notify.languageChanged", { language: name }), "info");
|
|
68
|
+
// 语言变了要重建菜单,否则列表里的文案还是旧语言
|
|
69
|
+
menu?.refresh(groupItemId("general"));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (result.field === ENABLED_FIELD) {
|
|
74
|
+
const definition = toolkit.getModuleDefinition(result.moduleId);
|
|
75
|
+
const name = definition ? t()(definition.labelKey) : result.moduleId;
|
|
76
|
+
ctx.ui.notify(t()("notify.moduleTogglePending", { module: name }), "warning");
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
ctx.ui.notify(t()("notify.saveFailed", { reason: describeError(error) }), "error");
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
await ctx.ui.custom((tui, theme, _keybindings, done) => {
|
|
84
|
+
menu = new ToolkitMenu({
|
|
85
|
+
toolkit,
|
|
86
|
+
theme: createMenuTheme(theme),
|
|
87
|
+
context: ctx,
|
|
88
|
+
agentDir: getAgentDir(),
|
|
89
|
+
requestRender: () => tui.requestRender(),
|
|
90
|
+
onChange: (id, value) => {
|
|
91
|
+
void handleChange(id, value);
|
|
92
|
+
},
|
|
93
|
+
onClose: () => done(undefined),
|
|
94
|
+
});
|
|
95
|
+
return menu;
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
package/menu/items.ts
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
// 菜单行构造:顶层两组 + 分组页内容(语言、占位行、模块行)。
|
|
2
|
+
// 设计约定:SettingsList 一行只接受一个字符串,所以取值选择器回传"显示文案",
|
|
3
|
+
// 这里用同一份 i18n 选项表把文案映射回规范化取值(见 fieldValueFromLabel / languageFromLabel),
|
|
4
|
+
// 因此语言切换后菜单重建即刷新全部显示,无需额外的显示态。
|
|
5
|
+
|
|
6
|
+
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
7
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type { ToolkitConfig } from "../config.ts";
|
|
9
|
+
import {
|
|
10
|
+
languageLabelKey,
|
|
11
|
+
LANGUAGE_SETTINGS,
|
|
12
|
+
type LanguageSetting,
|
|
13
|
+
type MessageKey,
|
|
14
|
+
type ResolvedLanguage,
|
|
15
|
+
type Translator,
|
|
16
|
+
} from "../i18n.ts";
|
|
17
|
+
import {
|
|
18
|
+
isBooleanField,
|
|
19
|
+
MODULE_GROUPS,
|
|
20
|
+
normalizeConfigValue,
|
|
21
|
+
type ConfigField,
|
|
22
|
+
type ModuleConfigScalar,
|
|
23
|
+
type ModuleConfigRecord,
|
|
24
|
+
type ModuleDefinition,
|
|
25
|
+
type ModuleGroup,
|
|
26
|
+
} from "../module.ts";
|
|
27
|
+
import type { ActiveModule } from "../assembler.ts";
|
|
28
|
+
import { moduleFieldId } from "../assembler.ts";
|
|
29
|
+
import type { ToolkitProblem } from "../toolkit.ts";
|
|
30
|
+
import type { ServiceRegistry } from "../services.ts";
|
|
31
|
+
import { ChoicePicker, SettingsPanel } from "./panels.ts";
|
|
32
|
+
import type { MenuTheme } from "./theme.ts";
|
|
33
|
+
|
|
34
|
+
/** 语言行的 id */
|
|
35
|
+
export const ID_LANGUAGE = "language";
|
|
36
|
+
|
|
37
|
+
const GROUP_ID_PREFIX = "group.";
|
|
38
|
+
|
|
39
|
+
export function groupItemId(group: ModuleGroup): string {
|
|
40
|
+
return `${GROUP_ID_PREFIX}${group}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MenuState {
|
|
44
|
+
readonly config: ToolkitConfig;
|
|
45
|
+
readonly modules: readonly ActiveModule[];
|
|
46
|
+
readonly problems: readonly ToolkitProblem[];
|
|
47
|
+
readonly services: ServiceRegistry;
|
|
48
|
+
readonly language: LanguageSetting;
|
|
49
|
+
readonly resolvedLanguage: ResolvedLanguage;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface MenuBuildContext {
|
|
53
|
+
readonly t: Translator;
|
|
54
|
+
readonly theme: MenuTheme;
|
|
55
|
+
/** 打开菜单时的扩展上下文;模块自绘子菜单需要与宿主交互时用它 */
|
|
56
|
+
readonly context: ExtensionContext;
|
|
57
|
+
/** pi 的配置目录:模块自写盘时用 */
|
|
58
|
+
readonly agentDir: string;
|
|
59
|
+
getState(): MenuState;
|
|
60
|
+
getModuleConfig(moduleId: string): ModuleConfigRecord;
|
|
61
|
+
/** 顶层统一改动入口(id 与显示文案) */
|
|
62
|
+
onChange(id: string, value: string): void;
|
|
63
|
+
/** 异步动作结束后请宿主重绘 */
|
|
64
|
+
requestRender(): void;
|
|
65
|
+
/** 重建整棵菜单:语言切换后用它刷新全部文案 */
|
|
66
|
+
refresh(selectId?: string): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** 一个取值的规范化代码与本地化文案 */
|
|
70
|
+
export interface FieldOption {
|
|
71
|
+
readonly code: string;
|
|
72
|
+
readonly label: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function enumValueLabel(t: Translator, field: ConfigField, value: string): string {
|
|
76
|
+
const key = field.valueLabelKeys?.[value];
|
|
77
|
+
return key ? t(key) : value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 字段的可选值:boolean 为 开/关,枚举取 schema 声明的 values */
|
|
81
|
+
export function fieldOptions(t: Translator, field: ConfigField): FieldOption[] {
|
|
82
|
+
if (isBooleanField(field)) {
|
|
83
|
+
return [
|
|
84
|
+
{ code: "true", label: t("common.on") },
|
|
85
|
+
{ code: "false", label: t("common.off") },
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
return (field.values ?? []).map((value) => ({ code: value, label: enumValueLabel(t, field, value) }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function fieldCode(current: ModuleConfigScalar | undefined): string | undefined {
|
|
92
|
+
return current === undefined ? undefined : String(current);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 当前值在菜单里显示的本地化文案 */
|
|
96
|
+
export function fieldDisplayValue(
|
|
97
|
+
t: Translator,
|
|
98
|
+
field: ConfigField,
|
|
99
|
+
current: ModuleConfigScalar | undefined,
|
|
100
|
+
): string {
|
|
101
|
+
const code = fieldCode(current);
|
|
102
|
+
const option = fieldOptions(t, field).find((candidate) => candidate.code === code);
|
|
103
|
+
return option?.label ?? code ?? "";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 选择器回传的显示文案 → 规范化取值;匹配不上返回 undefined */
|
|
107
|
+
export function fieldValueFromLabel(
|
|
108
|
+
t: Translator,
|
|
109
|
+
field: ConfigField,
|
|
110
|
+
label: string,
|
|
111
|
+
): ModuleConfigScalar | undefined {
|
|
112
|
+
const option = fieldOptions(t, field).find((candidate) => candidate.label === label);
|
|
113
|
+
return option ? normalizeConfigValue(field, option.code) : undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** 语言选择项:四个设置加各自本地化文案 */
|
|
117
|
+
export function languageOptions(t: Translator): { setting: LanguageSetting; label: string }[] {
|
|
118
|
+
return LANGUAGE_SETTINGS.map((setting) => ({ setting, label: t(languageLabelKey(setting)) }));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function languageFromLabel(t: Translator, label: string): LanguageSetting | undefined {
|
|
122
|
+
return languageOptions(t).find((option) => option.label === label)?.setting;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function buildTopLevelItems(build: MenuBuildContext): SettingItem[] {
|
|
126
|
+
const state = build.getState();
|
|
127
|
+
const items: SettingItem[] = [];
|
|
128
|
+
for (const problem of state.problems) {
|
|
129
|
+
items.push(problemItem(build, problem));
|
|
130
|
+
}
|
|
131
|
+
for (const group of MODULE_GROUPS) {
|
|
132
|
+
items.push(groupItem(build, group));
|
|
133
|
+
}
|
|
134
|
+
return items;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function buildGroupItems(build: MenuBuildContext, group: ModuleGroup): SettingItem[] {
|
|
138
|
+
const items: SettingItem[] = [];
|
|
139
|
+
if (group === "general") {
|
|
140
|
+
items.push(languageItem(build));
|
|
141
|
+
}
|
|
142
|
+
for (const active of build.getState().modules) {
|
|
143
|
+
if (active.definition.group !== group) continue;
|
|
144
|
+
items.push(...moduleItems(build, active.definition));
|
|
145
|
+
}
|
|
146
|
+
return items;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function groupLabelKeys(group: ModuleGroup): { label: MessageKey; description: MessageKey } {
|
|
150
|
+
return group === "general"
|
|
151
|
+
? { label: "group.general", description: "group.general.description" }
|
|
152
|
+
: { label: "group.subagents", description: "group.subagents.description" };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function groupItem(build: MenuBuildContext, group: ModuleGroup): SettingItem {
|
|
156
|
+
const keys = groupLabelKeys(group);
|
|
157
|
+
return {
|
|
158
|
+
id: groupItemId(group),
|
|
159
|
+
label: build.t(keys.label),
|
|
160
|
+
description: build.t(keys.description),
|
|
161
|
+
currentValue: build.t("group.itemCount", { count: buildGroupItems(build, group).length }),
|
|
162
|
+
submenu: (_currentValue, done) =>
|
|
163
|
+
new SettingsPanel({
|
|
164
|
+
title: build.t(keys.label),
|
|
165
|
+
items: buildGroupItems(build, group),
|
|
166
|
+
theme: build.theme,
|
|
167
|
+
t: build.t,
|
|
168
|
+
onChange: build.onChange,
|
|
169
|
+
onClose: () => done(),
|
|
170
|
+
}),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function problemItem(build: MenuBuildContext, problem: ToolkitProblem): SettingItem {
|
|
175
|
+
return {
|
|
176
|
+
id: `problem.${problem.kind}.${problem.source}`,
|
|
177
|
+
label: build.t(problem.kind === "config" ? "problem.config" : "problem.module"),
|
|
178
|
+
description: problem.detail,
|
|
179
|
+
currentValue: problem.source,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function languageItem(build: MenuBuildContext): SettingItem {
|
|
184
|
+
const state = build.getState();
|
|
185
|
+
const description =
|
|
186
|
+
state.language === "auto"
|
|
187
|
+
? build.t("language.autoResolved", {
|
|
188
|
+
language: build.t(languageLabelKey(state.resolvedLanguage)),
|
|
189
|
+
})
|
|
190
|
+
: build.t("language.description");
|
|
191
|
+
return {
|
|
192
|
+
id: ID_LANGUAGE,
|
|
193
|
+
label: build.t("language.label"),
|
|
194
|
+
description,
|
|
195
|
+
currentValue: build.t(languageLabelKey(state.language)),
|
|
196
|
+
submenu: (_currentValue, done) =>
|
|
197
|
+
new ChoicePicker({
|
|
198
|
+
title: build.t("language.label"),
|
|
199
|
+
options: languageOptions(build.t).map((option) => ({
|
|
200
|
+
value: option.setting,
|
|
201
|
+
label: option.label,
|
|
202
|
+
...(option.setting === state.language ? { description: build.t("common.current") } : {}),
|
|
203
|
+
})),
|
|
204
|
+
theme: build.theme,
|
|
205
|
+
t: build.t,
|
|
206
|
+
onSelect: (_value, label) => done(label),
|
|
207
|
+
onCancel: () => done(),
|
|
208
|
+
}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function moduleItems(build: MenuBuildContext, definition: ModuleDefinition): SettingItem[] {
|
|
213
|
+
const config = build.getModuleConfig(definition.id);
|
|
214
|
+
const items: SettingItem[] = [];
|
|
215
|
+
for (const [key, field] of Object.entries(definition.configSchema)) {
|
|
216
|
+
// schema 字段是标量;这里再规范化一次,把类型收成标量(取值非法时回落到默认值)
|
|
217
|
+
const current = normalizeConfigValue(field, config[key]) ?? field.default;
|
|
218
|
+
items.push(fieldItem(build, definition, key, field, current));
|
|
219
|
+
}
|
|
220
|
+
if (definition.menuItems) {
|
|
221
|
+
items.push(
|
|
222
|
+
...definition.menuItems({
|
|
223
|
+
t: build.t,
|
|
224
|
+
services: build.getState().services,
|
|
225
|
+
getConfig: () => build.getModuleConfig(definition.id),
|
|
226
|
+
context: build.context,
|
|
227
|
+
agentDir: build.agentDir,
|
|
228
|
+
theme: build.theme,
|
|
229
|
+
requestRender: () => build.requestRender(),
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return items;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function fieldItem(
|
|
237
|
+
build: MenuBuildContext,
|
|
238
|
+
definition: ModuleDefinition,
|
|
239
|
+
key: string,
|
|
240
|
+
field: ConfigField,
|
|
241
|
+
current: ModuleConfigScalar | undefined,
|
|
242
|
+
): SettingItem {
|
|
243
|
+
const label = build.t(field.labelKey);
|
|
244
|
+
const currentCode = fieldCode(current);
|
|
245
|
+
return {
|
|
246
|
+
id: moduleFieldId(definition.id, key),
|
|
247
|
+
label,
|
|
248
|
+
...(field.descriptionKey === undefined ? {} : { description: build.t(field.descriptionKey) }),
|
|
249
|
+
currentValue: fieldDisplayValue(build.t, field, current),
|
|
250
|
+
submenu: (_currentValue, done) =>
|
|
251
|
+
new ChoicePicker({
|
|
252
|
+
title: label,
|
|
253
|
+
options: fieldOptions(build.t, field).map((option) => ({
|
|
254
|
+
value: option.code,
|
|
255
|
+
label: option.label,
|
|
256
|
+
...(option.code === currentCode ? { description: build.t("common.current") } : {}),
|
|
257
|
+
})),
|
|
258
|
+
theme: build.theme,
|
|
259
|
+
t: build.t,
|
|
260
|
+
onSelect: (_value, displayLabel) => done(displayLabel),
|
|
261
|
+
onCancel: () => done(),
|
|
262
|
+
}),
|
|
263
|
+
};
|
|
264
|
+
}
|
package/menu/panels.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// 菜单里的两种面板组件,都是 Container 子类内嵌 pi 原生列表组件(原生 WarningSettingsSubmenu 同款写法):
|
|
2
|
+
// - SettingsPanel:设置面板,内嵌 SettingsList,用于分组页(常规 / 子代理)
|
|
3
|
+
// - ChoicePicker:取值选择器,内嵌 SelectList,用于语言与各配置字段的取值选择
|
|
4
|
+
// 两者只加一行标题:子菜单渲染在顶层 Container 的边框之内(与原生 /settings 的子菜单一致,不再套第二层边框)。
|
|
5
|
+
// Esc 都交给上层回传 done() 返回上级。
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Container,
|
|
9
|
+
type SelectItem,
|
|
10
|
+
SelectList,
|
|
11
|
+
type SettingItem,
|
|
12
|
+
Text,
|
|
13
|
+
} from "@earendil-works/pi-tui";
|
|
14
|
+
import type { Translator } from "../i18n.ts";
|
|
15
|
+
import { I18nSettingsList } from "./settings-list.ts";
|
|
16
|
+
import type { MenuTheme } from "./theme.ts";
|
|
17
|
+
|
|
18
|
+
const MAX_VISIBLE = 10;
|
|
19
|
+
|
|
20
|
+
export interface SettingsPanelOptions {
|
|
21
|
+
readonly title: string;
|
|
22
|
+
readonly items: readonly SettingItem[];
|
|
23
|
+
readonly theme: MenuTheme;
|
|
24
|
+
readonly t: Translator;
|
|
25
|
+
readonly onChange: (id: string, value: string) => void;
|
|
26
|
+
/** Esc:返回上级菜单 */
|
|
27
|
+
readonly onClose: () => void;
|
|
28
|
+
readonly enableSearch?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class SettingsPanel extends Container {
|
|
32
|
+
private readonly list: I18nSettingsList;
|
|
33
|
+
|
|
34
|
+
constructor(options: SettingsPanelOptions) {
|
|
35
|
+
super();
|
|
36
|
+
this.addChild(new Text(options.theme.title(options.title), 1, 0));
|
|
37
|
+
this.list = new I18nSettingsList({
|
|
38
|
+
items: options.items,
|
|
39
|
+
maxVisible: MAX_VISIBLE,
|
|
40
|
+
theme: options.theme.settings,
|
|
41
|
+
t: options.t,
|
|
42
|
+
onChange: options.onChange,
|
|
43
|
+
onCancel: options.onClose,
|
|
44
|
+
...(options.enableSearch === undefined ? {} : { enableSearch: options.enableSearch }),
|
|
45
|
+
});
|
|
46
|
+
this.addChild(this.list);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
handleInput(data: string): void {
|
|
50
|
+
this.list.handleInput(data);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ChoiceOption {
|
|
55
|
+
readonly value: string;
|
|
56
|
+
readonly label: string;
|
|
57
|
+
readonly description?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ChoicePickerOptions {
|
|
61
|
+
readonly title: string;
|
|
62
|
+
readonly options: readonly ChoiceOption[];
|
|
63
|
+
readonly theme: MenuTheme;
|
|
64
|
+
readonly t: Translator;
|
|
65
|
+
/** 回传规范化取值与显示文案;由调用方决定如何回填父项 */
|
|
66
|
+
readonly onSelect: (value: string, label: string) => void;
|
|
67
|
+
readonly onCancel: () => void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class ChoicePicker extends Container {
|
|
71
|
+
private readonly list: SelectList;
|
|
72
|
+
|
|
73
|
+
constructor(options: ChoicePickerOptions) {
|
|
74
|
+
super();
|
|
75
|
+
this.addChild(new Text(options.theme.title(options.title), 1, 0));
|
|
76
|
+
const items: SelectItem[] = options.options.map((option) => ({
|
|
77
|
+
value: option.value,
|
|
78
|
+
label: option.label,
|
|
79
|
+
...(option.description === undefined ? {} : { description: option.description }),
|
|
80
|
+
}));
|
|
81
|
+
this.list = new SelectList(items, Math.min(items.length, MAX_VISIBLE), options.theme.select);
|
|
82
|
+
this.list.onSelect = (item) => options.onSelect(item.value, item.label);
|
|
83
|
+
this.list.onCancel = () => options.onCancel();
|
|
84
|
+
this.addChild(this.list);
|
|
85
|
+
this.addChild(new Text(options.theme.hint(options.t("hint.chooseOptions")), 1, 0));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
handleInput(data: string): void {
|
|
89
|
+
this.list.handleInput(data);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// SettingsList 的薄包装:组件内部自带几行英文提示("Enter/Space to change · Esc to cancel"、
|
|
2
|
+
// "Type to search"、"No matching settings" 等),无法通过构造参数改写。这里在渲染出口按行
|
|
3
|
+
// 替换成 i18n 文案,保证三语切换后菜单里没有残留英文;无法识别时原样放行,不猜不改。
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type Component,
|
|
7
|
+
type SettingItem,
|
|
8
|
+
SettingsList,
|
|
9
|
+
type SettingsListTheme,
|
|
10
|
+
type TuiMouseEvent,
|
|
11
|
+
type TuiMouseEventResult,
|
|
12
|
+
truncateToWidth,
|
|
13
|
+
} from "@earendil-works/pi-tui";
|
|
14
|
+
import type { MessageKey, Translator } from "../i18n.ts";
|
|
15
|
+
|
|
16
|
+
/** 组件内置提示行 → i18n 键;顺序即匹配顺序,先匹配更长的搜索提示 */
|
|
17
|
+
const BUILT_IN_HINTS: readonly { readonly match: string; readonly key: MessageKey }[] = [
|
|
18
|
+
{ match: "Type to search", key: "hint.search" },
|
|
19
|
+
{ match: "Enter/Space to change", key: "hint.change" },
|
|
20
|
+
{ match: "No matching settings", key: "hint.noMatch" },
|
|
21
|
+
{ match: "No settings available", key: "hint.noSettings" },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export interface I18nSettingsListOptions {
|
|
25
|
+
readonly items: readonly SettingItem[];
|
|
26
|
+
readonly maxVisible: number;
|
|
27
|
+
readonly theme: SettingsListTheme;
|
|
28
|
+
readonly t: Translator;
|
|
29
|
+
readonly onChange: (id: string, value: string) => void;
|
|
30
|
+
readonly onCancel: () => void;
|
|
31
|
+
readonly enableSearch?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class I18nSettingsList implements Component {
|
|
35
|
+
private readonly list: SettingsList;
|
|
36
|
+
private readonly t: Translator;
|
|
37
|
+
private readonly theme: SettingsListTheme;
|
|
38
|
+
|
|
39
|
+
constructor(options: I18nSettingsListOptions) {
|
|
40
|
+
this.t = options.t;
|
|
41
|
+
this.theme = options.theme;
|
|
42
|
+
this.list = new SettingsList(
|
|
43
|
+
[...options.items],
|
|
44
|
+
options.maxVisible,
|
|
45
|
+
options.theme,
|
|
46
|
+
options.onChange,
|
|
47
|
+
options.onCancel,
|
|
48
|
+
options.enableSearch === undefined ? {} : { enableSearch: options.enableSearch },
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
updateValue(id: string, newValue: string): void {
|
|
53
|
+
this.list.updateValue(id, newValue);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
selectItem(id: string): void {
|
|
57
|
+
this.list.selectItem(id);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
invalidate(): void {
|
|
61
|
+
this.list.invalidate();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
handleInput(data: string): void {
|
|
65
|
+
this.list.handleInput(data);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
69
|
+
return this.list.handleMouse(event);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
render(width: number): string[] {
|
|
73
|
+
return this.list.render(width).map((line) => this.translateLine(line, width));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private translateLine(line: string, width: number): string {
|
|
77
|
+
for (const hint of BUILT_IN_HINTS) {
|
|
78
|
+
if (line.includes(hint.match)) {
|
|
79
|
+
return truncateToWidth(this.theme.hint(this.t(hint.key)), width);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return line;
|
|
83
|
+
}
|
|
84
|
+
}
|
package/menu/theme.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// 菜单主题:SettingsList / SelectList 用 pi 原生 get*Theme(),与原生 /settings 同色;
|
|
2
|
+
// 标题与边框用 ctx.ui.custom 回调里的主题实例显式上色(jiti 加载下不要依赖组件内部默认主题)。
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getSelectListTheme,
|
|
6
|
+
getSettingsListTheme,
|
|
7
|
+
type Theme,
|
|
8
|
+
type ThemeColor,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import type { SelectListTheme, SettingsListTheme } from "@earendil-works/pi-tui";
|
|
11
|
+
|
|
12
|
+
export interface MenuTheme {
|
|
13
|
+
readonly settings: SettingsListTheme;
|
|
14
|
+
readonly select: SelectListTheme;
|
|
15
|
+
readonly title: (text: string) => string;
|
|
16
|
+
readonly border: (text: string) => string;
|
|
17
|
+
readonly hint: (text: string) => string;
|
|
18
|
+
/** 模块自绘面板用:状态行按语义上色(success / error / warning / dim) */
|
|
19
|
+
readonly fg: (color: ThemeColor, text: string) => string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createMenuTheme(theme: Theme): MenuTheme {
|
|
23
|
+
return {
|
|
24
|
+
settings: getSettingsListTheme(),
|
|
25
|
+
select: getSelectListTheme(),
|
|
26
|
+
title: (text) => theme.bold(theme.fg("accent", text)),
|
|
27
|
+
border: (text) => theme.fg("border", text),
|
|
28
|
+
hint: (text) => theme.fg("dim", text),
|
|
29
|
+
fg: (color, text) => theme.fg(color, text),
|
|
30
|
+
};
|
|
31
|
+
}
|