@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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/assembler.ts +127 -0
  4. package/config.ts +201 -0
  5. package/i18n.ts +693 -0
  6. package/index.ts +99 -0
  7. package/menu/items.ts +264 -0
  8. package/menu/panels.ts +91 -0
  9. package/menu/settings-list.ts +84 -0
  10. package/menu/theme.ts +31 -0
  11. package/menu/toolkit-menu.ts +139 -0
  12. package/module.ts +156 -0
  13. package/modules/eyes/chain.ts +173 -0
  14. package/modules/eyes/config.ts +319 -0
  15. package/modules/eyes/index.ts +161 -0
  16. package/modules/eyes/menu.ts +828 -0
  17. package/modules/eyes/pi-model-backend.ts +245 -0
  18. package/modules/eyes/resilience.ts +161 -0
  19. package/modules/eyes/vision-bridge.ts +232 -0
  20. package/modules/eyes/vision-cache.ts +86 -0
  21. package/modules/eyes/vision-json.ts +19 -0
  22. package/modules/eyes/vision-preprocess.ts +192 -0
  23. package/modules/eyes/vision-probe.ts +21 -0
  24. package/modules/eyes/vision-prompt.ts +90 -0
  25. package/modules/eyes/vision-tool.ts +59 -0
  26. package/modules/eyes/vision-types.ts +33 -0
  27. package/modules/index.ts +10 -0
  28. package/modules/subagents/agents/researcher.md +51 -0
  29. package/modules/subagents/agents/scout.md +40 -0
  30. package/modules/subagents/agents/worker.md +79 -0
  31. package/modules/subagents/config.json.example +6 -0
  32. package/modules/subagents/config.ts +144 -0
  33. package/modules/subagents/index.ts +91 -0
  34. package/modules/subagents/menu.ts +388 -0
  35. package/modules/subagents/src/activity.ts +511 -0
  36. package/modules/subagents/src/agents.ts +126 -0
  37. package/modules/subagents/src/command.ts +37 -0
  38. package/modules/subagents/src/dependencies.ts +246 -0
  39. package/modules/subagents/src/diagnostics.ts +13 -0
  40. package/modules/subagents/src/display.ts +94 -0
  41. package/modules/subagents/src/headless.ts +342 -0
  42. package/modules/subagents/src/herdr.ts +203 -0
  43. package/modules/subagents/src/index.ts +2798 -0
  44. package/modules/subagents/src/inspect-tool.ts +839 -0
  45. package/modules/subagents/src/launch-config.ts +196 -0
  46. package/modules/subagents/src/layout-budget.ts +26 -0
  47. package/modules/subagents/src/list-tool.ts +292 -0
  48. package/modules/subagents/src/message-tool.ts +808 -0
  49. package/modules/subagents/src/names.ts +17 -0
  50. package/modules/subagents/src/pane-layout.ts +49 -0
  51. package/modules/subagents/src/params.ts +145 -0
  52. package/modules/subagents/src/renderers.ts +181 -0
  53. package/modules/subagents/src/result.ts +43 -0
  54. package/modules/subagents/src/retention.ts +114 -0
  55. package/modules/subagents/src/route-error.ts +212 -0
  56. package/modules/subagents/src/routing.ts +235 -0
  57. package/modules/subagents/src/runtime-registry.ts +159 -0
  58. package/modules/subagents/src/session.ts +801 -0
  59. package/modules/subagents/src/status.ts +513 -0
  60. package/modules/subagents/src/stop-tool.ts +235 -0
  61. package/modules/subagents/src/subagent-done.ts +590 -0
  62. package/modules/subagents/src/subagent-tool.ts +1155 -0
  63. package/modules/subagents/src/surface.ts +355 -0
  64. package/modules/subagents/src/team-dispatch-tool.ts +219 -0
  65. package/modules/subagents/src/team.ts +232 -0
  66. package/modules/subagents/src/tmux.ts +210 -0
  67. package/modules/subagents/src/tools/safe-bash.ts +72 -0
  68. package/modules/subagents/src/types.ts +131 -0
  69. package/modules/tui/adapter/provider-usage.ts +143 -0
  70. package/modules/tui/config.ts +50 -0
  71. package/modules/tui/index.ts +120 -0
  72. package/modules/tui/kernel/pkg/shared/grok-subscription.ts +169 -0
  73. package/modules/tui/kernel/pkg/shared/official-subscription.ts +237 -0
  74. package/modules/tui/kernel/pkg/shared/provider-catalog.ts +367 -0
  75. package/modules/tui/kernel/pkg/shared/provider-contracts.ts +150 -0
  76. package/modules/tui/kernel/pkg/shared/provider-display.ts +55 -0
  77. package/modules/tui/kernel/pkg/shared/provider-parsers.ts +171 -0
  78. package/modules/tui/kernel/pkg/shared/volcengine.ts +191 -0
  79. package/modules/tui/kernel/pkg/shared/zhipu.ts +149 -0
  80. package/modules/tui/kernel/pkg/usage-core/index.ts +335 -0
  81. package/modules/tui/kernel/pkg/usage-core/provider-routes.ts +187 -0
  82. package/modules/tui/kernel/pkg/usage-node/index.ts +635 -0
  83. package/modules/tui/kernel/pkg/usage-node/provider-usage.ts +388 -0
  84. package/modules/tui/kernel/usage-core.ts +2 -0
  85. package/modules/tui/kernel/usage-node.ts +2 -0
  86. package/modules/tui/menu.ts +418 -0
  87. package/modules/tui/plugin/editor.ts +396 -0
  88. package/modules/tui/plugin/footer.ts +161 -0
  89. package/modules/tui/plugin/index.ts +30 -0
  90. package/modules/tui/plugin/lifecycle.ts +637 -0
  91. package/modules/tui/plugin/package-order.ts +169 -0
  92. package/modules/tui/plugin/screen-transition.ts +203 -0
  93. package/modules/tui/plugin/settings-config.ts +297 -0
  94. package/modules/tui/plugin/status-sources.ts +49 -0
  95. package/modules/tui/plugin/transition-gate.ts +261 -0
  96. package/modules/tui/renderer/custom-header.ts +157 -0
  97. package/modules/tui/renderer/editor.ts +70 -0
  98. package/modules/tui/renderer/header.ts +72 -0
  99. package/modules/tui/renderer/icons.ts +149 -0
  100. package/modules/tui/renderer/pi-installer-logo.ts +194 -0
  101. package/modules/tui/status/auto-compaction.ts +67 -0
  102. package/modules/tui/status/project-status.ts +655 -0
  103. package/modules/tui/status/provider-status.ts +120 -0
  104. package/modules/tui/status/runtime-status.ts +307 -0
  105. package/modules/tui/status/session-status.ts +325 -0
  106. package/modules/tui/status/status-config.ts +95 -0
  107. package/modules/tui/status/status-segments.ts +263 -0
  108. package/modules/tui/status/turn-telemetry.ts +466 -0
  109. package/modules/tui/themes/LICENSE.pi-themes-bundle +21 -0
  110. package/modules/tui/themes/UPSTREAM.md +7 -0
  111. package/modules/tui/themes/catppuccin-latte.json +80 -0
  112. package/modules/tui/themes/catppuccin-mocha.json +79 -0
  113. package/modules/tui/themes/crimson-noir.json +85 -0
  114. package/modules/tui/themes/dracula.json +79 -0
  115. package/modules/tui/themes/everforest-dark.json +85 -0
  116. package/modules/tui/themes/gruvbox-dark.json +85 -0
  117. package/modules/tui/themes/gruvbox-light.json +85 -0
  118. package/modules/tui/themes/matrix.json +85 -0
  119. package/modules/tui/themes/nord.json +85 -0
  120. package/modules/tui/themes/one-dark.json +85 -0
  121. package/modules/tui/themes/rose-pine-dawn.json +85 -0
  122. package/modules/tui/themes/rose-pine.json +85 -0
  123. package/modules/tui/themes/solarized-dark.json +85 -0
  124. package/modules/tui/themes/solarized-light.json +85 -0
  125. package/modules/tui/themes/tokyo-night-storm.json +79 -0
  126. package/modules/tui/themes/tokyo-night.json +79 -0
  127. package/package.json +28 -0
  128. package/services.ts +38 -0
  129. package/toolkit.ts +147 -0
@@ -0,0 +1,637 @@
1
+ import {
2
+ SettingsManager,
3
+ VERSION,
4
+ getAgentDir,
5
+ type ExtensionAPI,
6
+ type ExtensionContext,
7
+ type ReadonlyFooterDataProvider,
8
+ } from "@earendil-works/pi-coding-agent";
9
+ import type {
10
+ EditorTheme,
11
+ TUI,
12
+ TuiMainScreenRenderState,
13
+ } from "@earendil-works/pi-tui";
14
+ import type { KeybindingsManager } from "@earendil-works/pi-coding-agent";
15
+ import { PiProviderUsageController } from "../adapter/provider-usage.ts";
16
+ import { PiTuiHeader } from "../renderer/header.ts";
17
+ import { resolveGlyphs } from "../renderer/icons.ts";
18
+ import {
19
+ ProjectStatusController,
20
+ createGitStatusQuery,
21
+ } from "../status/project-status.ts";
22
+ import {
23
+ RuntimeStatusController,
24
+ createRuntimeStatusDetector,
25
+ } from "../status/runtime-status.ts";
26
+ import { TurnTimerController } from "../status/status-segments.ts";
27
+ import { resolveStatusSettings } from "../status/status-config.ts";
28
+ import { collectSessionStatus } from "../status/session-status.ts";
29
+ import {
30
+ TurnTelemetryController,
31
+ readLatestTurnDuration,
32
+ registerTurnTelemetryRenderers,
33
+ } from "../status/turn-telemetry.ts";
34
+ import {
35
+ AutoCompactionStatusController,
36
+ watchAgentSettings,
37
+ } from "../status/auto-compaction.ts";
38
+ import {
39
+ loadStandaloneTuiConfig,
40
+ type LoadedPiTuiConfig,
41
+ } from "./settings-config.ts";
42
+ import { getTerminalTransitionGate, type TerminalTransitionGate } from "./transition-gate.ts";
43
+ import { ensureFirstPackage } from "./package-order.ts";
44
+ import {
45
+ flashVisibleScreen,
46
+ isInteractiveLaunch,
47
+ restoreVisibleMainScreen,
48
+ TRANSITION_SETTLE_MS,
49
+ type VisibleScreenOutput,
50
+ } from "./screen-transition.ts";
51
+ import { PiUiEditor, formatModel } from "./editor.ts";
52
+ import { ProjectStatusFooter } from "./footer.ts";
53
+ import type {
54
+ EditorLayoutSource,
55
+ FooterLayoutSource,
56
+ ProjectEnvironmentSource,
57
+ ProviderUsageSource,
58
+ SessionStatusSource,
59
+ StatusAppearance,
60
+ } from "./status-sources.ts";
61
+ import type { Translator } from "../../../i18n.ts";
62
+
63
+ export interface PiTuiPluginDependencies {
64
+ agentDir?: string;
65
+ env?: Readonly<Record<string, string | undefined>>;
66
+ loadConfig?: () => LoadedPiTuiConfig;
67
+ readAutoCompactionEnabled?: (ctx: ExtensionContext, agentDir: string) => boolean;
68
+ watchAutoCompactionSettings?: (agentDir: string, onChange: () => void) => () => void;
69
+ transitionGate?: TerminalTransitionGate | null;
70
+ /** 通知文案用的译者;缺省时不发本地化通知 */
71
+ t?: Translator;
72
+ }
73
+
74
+ /** registerPiTuiLifecycle 交回给模块的把手:菜单保存外观后靠它重装常驻 UI */
75
+ export interface PiTuiLifecycleHandle {
76
+ /** 重读配置;常驻 UI 已安装时原子重装,返回是否重装 */
77
+ applyConfig(): boolean;
78
+ /** 常驻 UI 是否已安装 */
79
+ isActive(): boolean;
80
+ /** 供应商余额/套餐运行态(常驻 UI 未安装时为 undefined) */
81
+ getProviderUsage(): PiProviderUsageController | undefined;
82
+ }
83
+
84
+ /**
85
+ * 插件装配与生命周期:安装/卸载 UI、创建并接线各状态控制器、订阅会话事件。
86
+ * 视图组件(editor/footer/设置向导)只通过数据源分组参数接收数据与回调,
87
+ * 不反向依赖本文件;jiti 重装与模块级状态也留在本文件内。
88
+ */
89
+ export function registerPiTuiLifecycle(
90
+ pi: ExtensionAPI,
91
+ output: VisibleScreenOutput = process.stdout,
92
+ dependencies: PiTuiPluginDependencies = {},
93
+ ): PiTuiLifecycleHandle {
94
+ const env = dependencies.env ?? process.env;
95
+ const agentDir = dependencies.agentDir ?? getAgentDir();
96
+ const readConfig = dependencies.loadConfig ?? (() => loadStandaloneTuiConfig(agentDir));
97
+ const t = dependencies.t;
98
+ const watchAutoCompactionSettings = dependencies.watchAutoCompactionSettings ?? watchAgentSettings;
99
+ const readAutoCompactionEnabled = dependencies.readAutoCompactionEnabled ?? ((ctx: ExtensionContext) => {
100
+ try {
101
+ return SettingsManager.create(ctx.cwd, agentDir, {
102
+ projectTrusted: ctx.isProjectTrusted?.() ?? false,
103
+ }).getCompactionEnabled();
104
+ } catch {
105
+ return false;
106
+ }
107
+ });
108
+
109
+ let loadedConfig = readConfig();
110
+ let currentConfig = loadedConfig.config;
111
+ let configWarningsShown = false;
112
+ let active = false;
113
+ /** 当前安装是否含底部状态栏;无 Footer 时转场闸门与状态查询不等 footerData 连接 */
114
+ let footerEnabled = true;
115
+ let lastContext: ExtensionContext | undefined;
116
+ let cleanupEditor: (() => void) | undefined;
117
+ let cleanupFooter: (() => void) | undefined;
118
+ let cleanupHeader: (() => void) | undefined;
119
+ let projectStatus: ProjectStatusController | undefined;
120
+ let runtimeStatus: RuntimeStatusController | undefined;
121
+ let providerUsage: PiProviderUsageController | undefined;
122
+ let turnTimer: TurnTimerController | undefined;
123
+ let autoCompactionStatus: AutoCompactionStatusController | undefined;
124
+ let cleanupSpinner: (() => void) | undefined;
125
+ const turnTelemetry = new TurnTelemetryController({
126
+ isEnabled: () => currentConfig.data.telemetry,
127
+ getTimer: () => turnTimer,
128
+ appendEntry: (customType, data) => pi.appendEntry(customType, data),
129
+ });
130
+ let installedTui: TUI | undefined;
131
+ // 启动守卫跨 UI 工厂共享,避免宿主重建 Footer 后查询状态失步。
132
+ let projectConnection: { footerData: ReadonlyFooterDataProvider } | undefined;
133
+ let statusQueriesStarted = false;
134
+ let deferredStatusImmediate: ReturnType<typeof setImmediate> | undefined;
135
+ let modelSwitchRepaintImmediate: ReturnType<typeof setImmediate> | undefined;
136
+ const cancelModelSwitchRepaint = (): void => {
137
+ if (!modelSwitchRepaintImmediate) return;
138
+ clearImmediate(modelSwitchRepaintImmediate);
139
+ modelSwitchRepaintImmediate = undefined;
140
+ };
141
+ const scheduleModelSwitchRepaint = (): void => {
142
+ if (modelSwitchRepaintImmediate) return;
143
+ modelSwitchRepaintImmediate = setImmediate(() => {
144
+ modelSwitchRepaintImmediate = undefined;
145
+ const tui = installedTui;
146
+ // 模型事件可能早于宿主关闭选择器;等当前宿主调用栈结束后,
147
+ // 只对仍然活跃且未处于 reload 过渡的主屏做可见区安全重绘。
148
+ if (!active || !tui || transitionGate?.isHolding()) return;
149
+ restoreVisibleMainScreen(tui);
150
+ });
151
+ modelSwitchRepaintImmediate.unref?.();
152
+ };
153
+ // 编辑框 factory 链:原子重装时“恢复目标”必须仍是本插件首次安装前的 factory,
154
+ // 不能错记成自己上一次的 ownFactory(否则真卸载时会重建已废弃的旧编辑框)。
155
+ type EditorFactory = NonNullable<ReturnType<ExtensionContext["ui"]["getEditorComponent"]>>;
156
+ let originalEditorFactory: EditorFactory | undefined;
157
+ let lastOwnEditorFactory: EditorFactory | undefined;
158
+ // 当前已安装的编辑框实例:闭包外持有,原子重装时 dispose 旧实例(自动补全
159
+ // Overlay 挂在 TUI 上,不 dispose 会残留)。
160
+ let installedEditorRef: PiUiEditor | undefined;
161
+ const transitionGate: TerminalTransitionGate | undefined = dependencies.transitionGate === null
162
+ ? undefined
163
+ : dependencies.transitionGate ?? (
164
+ output === process.stdout && isInteractiveLaunch() ? getTerminalTransitionGate() : undefined
165
+ );
166
+ const requestStatusRender = () => {
167
+ if (transitionGate?.isHolding()) return;
168
+ installedTui?.requestRender();
169
+ };
170
+ let transitionRevealEnabled = true;
171
+ let transitionRevealTimer: ReturnType<typeof setTimeout> | undefined;
172
+ // Footer 首帧回报:模块级,供揭示门槛读取(installUi 闭包外)。
173
+ const layoutState = { footerHeight: 0 };
174
+ const connectProjectStatus = (): void => {
175
+ if (!projectConnection) return;
176
+ projectStatus?.connect(projectConnection.footerData, requestStatusRender);
177
+ runtimeStatus?.connect(requestStatusRender);
178
+ };
179
+ const disconnectProjectStatus = (): void => {
180
+ if (!projectConnection) return;
181
+ projectConnection = undefined;
182
+ statusQueriesStarted = false;
183
+ projectStatus?.disconnect();
184
+ runtimeStatus?.disconnect();
185
+ };
186
+ const disposeProjectStatus = (): void => {
187
+ transitionRevealEnabled = false;
188
+ if (transitionRevealTimer) clearTimeout(transitionRevealTimer);
189
+ transitionRevealTimer = undefined;
190
+ if (deferredStatusImmediate) clearImmediate(deferredStatusImmediate);
191
+ deferredStatusImmediate = undefined;
192
+ statusQueriesStarted = false;
193
+ disconnectProjectStatus();
194
+ projectStatus?.dispose();
195
+ runtimeStatus?.dispose();
196
+ projectStatus = undefined;
197
+ runtimeStatus = undefined;
198
+ };
199
+ const startStatusQueries = (): void => {
200
+ // 安装尾部的延迟补排可能早于揭示,不能让它绕过首帧屏障。
201
+ if (statusQueriesStarted || !active || transitionGate?.isHolding()) return;
202
+ // 无 Footer 时没有 footerData 可接,但供应商刷新等状态查询仍要启动。
203
+ if (footerEnabled && !projectConnection) return;
204
+ statusQueriesStarted = true;
205
+ connectProjectStatus();
206
+ void providerUsage?.start();
207
+ };
208
+ const scheduleStatusQueries = (): void => {
209
+ if (statusQueriesStarted || deferredStatusImmediate) return;
210
+ deferredStatusImmediate = setImmediate(() => {
211
+ deferredStatusImmediate = undefined;
212
+ startStatusQueries();
213
+ });
214
+ deferredStatusImmediate.unref?.();
215
+ };
216
+ const scheduleTransitionReveal = (tui: TUI): void => {
217
+ if (!transitionRevealEnabled || !transitionGate?.isHolding()) return;
218
+ // 揭示门槛:Editor 稳定帧 + Footer 已出过首帧。只等 Editor 会让揭示帧
219
+ // 缺 Footer(dock 尚未调用 Footer 渲染),揭示后 Footer 补帧造成二次跳变。
220
+ // 未安装 Footer(appearance.footer=false)时没有首帧可等,只等 Editor。
221
+ if (footerEnabled && layoutState.footerHeight < 1) return;
222
+ if (transitionRevealTimer) clearTimeout(transitionRevealTimer);
223
+ transitionRevealTimer = setTimeout(() => {
224
+ transitionRevealTimer = undefined;
225
+ if (!transitionRevealEnabled || !transitionGate.isHolding()) return;
226
+ if (footerEnabled && layoutState.footerHeight < 1) return;
227
+ transitionGate.reveal(tui);
228
+ startStatusQueries();
229
+ }, TRANSITION_SETTLE_MS);
230
+ transitionRevealTimer.unref?.();
231
+ };
232
+ registerTurnTelemetryRenderers(pi, () => resolveGlyphs("auto", env));
233
+
234
+ const disposeInstalledControllers = (): void => {
235
+ cancelModelSwitchRepaint();
236
+ installedEditorRef?.dispose();
237
+ installedEditorRef = undefined;
238
+ turnTimer?.dispose();
239
+ autoCompactionStatus?.dispose();
240
+ providerUsage?.dispose();
241
+ disposeProjectStatus();
242
+ turnTimer = undefined;
243
+ autoCompactionStatus = undefined;
244
+ providerUsage = undefined;
245
+ cleanupFooter = undefined;
246
+ cleanupHeader = undefined;
247
+ cleanupEditor = undefined;
248
+ cleanupSpinner = undefined;
249
+ installedTui = undefined;
250
+ };
251
+
252
+ const installUi = (ctx: ExtensionContext): void => {
253
+ if (active) {
254
+ // 原子重装(设置保存后):只 dispose 控制器,不把组件恢复成 Pi 默认——
255
+ // “恢复默认→再装自定义”的往返会让 dock 高度抖动,把 transcript 行推进
256
+ // 滚动缓冲形成残影;组件由本次安装直接覆盖,高度最多变化一次。
257
+ active = false;
258
+ disposeInstalledControllers();
259
+ }
260
+ transitionRevealEnabled = true;
261
+ if (transitionRevealTimer) clearTimeout(transitionRevealTimer);
262
+ transitionRevealTimer = undefined;
263
+ footerEnabled = currentConfig.appearance.footer;
264
+ const currentFactory = ctx.ui.getEditorComponent();
265
+ // 恢复目标:当前生效的是自己上次的 factory 时沿用最初的 factory。
266
+ const previousFactory = currentFactory !== undefined && currentFactory === lastOwnEditorFactory
267
+ ? originalEditorFactory
268
+ : currentFactory;
269
+ originalEditorFactory = previousFactory;
270
+ const statusSettings = resolveStatusSettings(env, {
271
+ preset: currentConfig.status.preset,
272
+ segments: currentConfig.status.segments,
273
+ });
274
+ const autoCompaction = currentConfig.appearance.editor && statusSettings.footerUsage.includes("context")
275
+ ? new AutoCompactionStatusController(
276
+ () => readAutoCompactionEnabled(ctx, agentDir),
277
+ (onChange) => watchAutoCompactionSettings(agentDir, onChange),
278
+ requestStatusRender,
279
+ )
280
+ : undefined;
281
+ autoCompactionStatus = autoCompaction;
282
+ const getGlyphs = () => resolveGlyphs("auto", env);
283
+ const timer = statusSettings.editorLeft.includes("duration")
284
+ ? new TurnTimerController(
285
+ requestStatusRender,
286
+ 1_000,
287
+ Date.now,
288
+ readLatestTurnDuration(ctx.sessionManager?.getEntries?.() ?? []),
289
+ )
290
+ : undefined;
291
+ const usage = statusSettings.editorLeft.some((segment) =>
292
+ segment === "provider" || segment === "balance" || segment === "subscription"
293
+ )
294
+ ? new PiProviderUsageController(ctx, requestStatusRender, {
295
+ refreshMs: currentConfig.data.providerRefreshMs,
296
+ accessConfig: currentConfig.data.providerAccess,
297
+ })
298
+ : undefined;
299
+ const runtime = statusSettings.footerPrimary.includes("runtime")
300
+ ? new RuntimeStatusController(ctx.cwd,
301
+ createRuntimeStatusDetector(
302
+ async (command, args, commandCwd, commandSignal) => {
303
+ const result = await pi.exec(command, [...args], {
304
+ cwd: commandCwd,
305
+ signal: commandSignal,
306
+ timeout: 2500,
307
+ });
308
+ return {
309
+ stdout: result.stdout,
310
+ stderr: result.stderr,
311
+ code: result.code,
312
+ killed: result.killed,
313
+ };
314
+ },
315
+ env,
316
+ ),
317
+ )
318
+ : undefined;
319
+ const controller = statusSettings.footerPrimary.includes("git")
320
+ ? new ProjectStatusController(ctx.cwd, createGitStatusQuery(pi.exec.bind(pi)))
321
+ : undefined;
322
+ projectStatus = controller;
323
+ runtimeStatus = runtime;
324
+ let activeTui: TUI | undefined;
325
+ let preClearState: TuiMainScreenRenderState | undefined;
326
+ let headerInstalled = false;
327
+ let footerInstalled = false;
328
+ let installedEditor: PiUiEditor | undefined;
329
+ // 数据源分组接线:视图构造器只收分组接口;控制器刷新回调(供应商刷新、
330
+ // 转场闸门等模块级状态)留在装配层,不外泄到视图构造接口。
331
+ const appearanceSource: StatusAppearance = { getGlyphs, settings: statusSettings };
332
+ const providerSource: ProviderUsageSource = usage ? { getState: () => usage.getState() } : {};
333
+ const sessionSource: SessionStatusSource = {
334
+ getTimer: () => timer?.getSnapshot() ?? { state: "idle", elapsedMs: 0 },
335
+ getSessionStatus: () => collectSessionStatus(ctx.sessionManager),
336
+ getContextUsage: () => ctx.getContextUsage(),
337
+ getContextWindow: () => ctx.model?.contextWindow,
338
+ getAutoCompactionEnabled: () => autoCompaction?.getSnapshot() ?? false,
339
+ };
340
+ const projectSource: ProjectEnvironmentSource = {
341
+ getProjectStatus: controller ? () => controller.getSnapshot() : undefined,
342
+ getRuntimeStatus: runtime ? () => runtime.getSnapshot() : undefined,
343
+ cwd: ctx.cwd,
344
+ };
345
+ const editorLayoutSource: EditorLayoutSource = { getFooterHeight: () => layoutState.footerHeight };
346
+ const footerLayoutSource: FooterLayoutSource = {
347
+ reportHeight: (height) => { layoutState.footerHeight = height; },
348
+ };
349
+ const ownFactory = (tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => {
350
+ activeTui = tui;
351
+ installedTui = tui;
352
+ // 实例级拦截:挂到宿主真实 terminal(prototype patch 因 jiti 双副本无效)。
353
+ transitionGate?.hookTerminal(tui.terminal);
354
+ const editor = new PiUiEditor(
355
+ tui,
356
+ theme,
357
+ keybindings,
358
+ ctx,
359
+ appearanceSource,
360
+ providerSource,
361
+ sessionSource,
362
+ editorLayoutSource,
363
+ {
364
+ onReloadSubmit: () => {
365
+ transitionRevealEnabled = false;
366
+ transitionGate?.hold(tui, { clearVisibleScreen: true });
367
+ },
368
+ onFrameRendered: () => {
369
+ // 闸门存在但已不在 holding(曾被提前释放或已揭示)时,
370
+ // 旧逻辑两个分支都不执行,状态查询永不启动;这里兜底直启。
371
+ if (transitionGate?.isHolding() && transitionRevealEnabled) scheduleTransitionReveal(tui);
372
+ else scheduleStatusQueries();
373
+ },
374
+ },
375
+ );
376
+ installedEditor?.dispose();
377
+ installedEditor = editor;
378
+ installedEditorRef = editor;
379
+ if (!transitionGate) preClearState = flashVisibleScreen(tui, output);
380
+ return editor;
381
+ };
382
+ lastOwnEditorFactory = ownFactory;
383
+
384
+ try {
385
+ if (currentConfig.appearance.editor) ctx.ui.setEditorComponent(ownFactory);
386
+ // Pi 正式 TUI 提供 setHeader;精简宿主桩或旧宿主没有该能力时跳过 Header。
387
+ if (currentConfig.appearance.header && typeof ctx.ui.setHeader === "function") {
388
+ ctx.ui.setHeader((tui) => new PiTuiHeader(
389
+ () => ({
390
+ version: VERSION,
391
+ model: formatModel(ctx),
392
+ thinking: ctx.thinkingLevel ?? "off",
393
+ cwd: ctx.cwd,
394
+ }),
395
+ () => ctx.ui.theme,
396
+ getGlyphs,
397
+ tui.requestRender.bind(tui),
398
+ ));
399
+ headerInstalled = true;
400
+ }
401
+ if (currentConfig.appearance.footer) {
402
+ footerInstalled = true;
403
+ ctx.ui.setFooter((tui, theme, footerData) => {
404
+ activeTui ??= tui;
405
+ installedTui = tui;
406
+ disconnectProjectStatus();
407
+ const connection = { footerData };
408
+ projectConnection = connection;
409
+ const installedFooter = new ProjectStatusFooter(
410
+ tui,
411
+ theme,
412
+ footerData,
413
+ appearanceSource,
414
+ projectSource,
415
+ sessionSource,
416
+ footerLayoutSource,
417
+ {
418
+ beforeDispose: () => {
419
+ transitionGate?.hold(tui);
420
+ // 宿主可能先创建替代 Footer 再卸载旧实例,旧回调不能断开新连接。
421
+ if (projectConnection !== connection) return;
422
+ disconnectProjectStatus();
423
+ },
424
+ },
425
+ );
426
+ // 宿主重建 Footer 不会重跑安装;换绑数据源仍需经过同一个揭示屏障。
427
+ if (active) scheduleStatusQueries();
428
+ // 闸门 holding 期间 dock 不跑、Footer 首帧不产生,揭示门槛会永久等待。
429
+ // 工厂内主动渲染一次回报高度,解锁揭示;dock 接管后的渲染不受影响。
430
+ if (transitionGate?.isHolding()) {
431
+ installedFooter.render(Math.max(1, tui.terminal.columns));
432
+ }
433
+ return installedFooter;
434
+ });
435
+ }
436
+ if (currentConfig.advanced.spinner === "hidden") {
437
+ ctx.ui.setWorkingVisible?.(false);
438
+ } else {
439
+ ctx.ui.setWorkingVisible?.(true);
440
+ ctx.ui.setWorkingIndicator?.(
441
+ currentConfig.advanced.spinner === "static" ? { frames: ["●"] } : undefined,
442
+ );
443
+ }
444
+ if (!transitionGate && activeTui && preClearState) {
445
+ restoreVisibleMainScreen(activeTui, preClearState, true);
446
+ }
447
+ } catch (error) {
448
+ disposeProjectStatus();
449
+ installedEditor?.dispose();
450
+ autoCompaction?.dispose();
451
+ if (autoCompactionStatus === autoCompaction) autoCompactionStatus = undefined;
452
+ timer?.dispose();
453
+ usage?.dispose();
454
+ ctx.ui.setWorkingIndicator?.();
455
+ ctx.ui.setWorkingVisible?.(true);
456
+ try {
457
+ if (footerInstalled) ctx.ui.setFooter(undefined);
458
+ } finally {
459
+ try {
460
+ if (headerInstalled) ctx.ui.setHeader(undefined);
461
+ } finally {
462
+ if (currentConfig.appearance.editor && ctx.ui.getEditorComponent() === ownFactory) {
463
+ ctx.ui.setEditorComponent(previousFactory);
464
+ }
465
+ }
466
+ }
467
+ if (activeTui && preClearState) {
468
+ restoreVisibleMainScreen(activeTui, preClearState);
469
+ }
470
+ if (transitionGate?.isHolding()) {
471
+ if (activeTui) transitionGate.reveal(activeTui);
472
+ else transitionGate.release(true);
473
+ }
474
+ throw error;
475
+ }
476
+
477
+ providerUsage = usage;
478
+ turnTimer = timer;
479
+ cleanupEditor = currentConfig.appearance.editor ? () => {
480
+ installedEditor?.dispose();
481
+ installedEditor = undefined;
482
+ if (ctx.ui.getEditorComponent() === ownFactory) {
483
+ ctx.ui.setEditorComponent(previousFactory);
484
+ }
485
+ } : undefined;
486
+ cleanupFooter = footerInstalled ? () => ctx.ui.setFooter(undefined) : undefined;
487
+ cleanupHeader = headerInstalled ? () => ctx.ui.setHeader(undefined) : undefined;
488
+ cleanupSpinner = () => {
489
+ ctx.ui.setWorkingIndicator?.();
490
+ ctx.ui.setWorkingVisible?.(true);
491
+ };
492
+ active = true;
493
+ // 帧回调可能早于安装尾部,补排一次以免查询错过 active 切换。
494
+ scheduleStatusQueries();
495
+ // 没有自定义 Editor 时没有首帧回调可用:先释放闸门再同步启动状态查询。
496
+ if (!currentConfig.appearance.editor) {
497
+ if (transitionGate?.isHolding()) {
498
+ if (activeTui) transitionGate.reveal(activeTui);
499
+ else transitionGate.release(false);
500
+ }
501
+ startStatusQueries();
502
+ }
503
+ };
504
+
505
+ const uninstallUi = (): void => {
506
+ if (
507
+ !active && !projectStatus && !runtimeStatus && !providerUsage && !turnTimer && !autoCompactionStatus &&
508
+ !cleanupEditor && !cleanupFooter && !cleanupHeader && !cleanupSpinner
509
+ ) return;
510
+ active = false;
511
+ cancelModelSwitchRepaint();
512
+ const usage = providerUsage;
513
+ const timer = turnTimer;
514
+ const autoCompaction = autoCompactionStatus;
515
+ const restoreFooter = cleanupFooter;
516
+ const restoreHeader = cleanupHeader;
517
+ const restoreEditor = cleanupEditor;
518
+ const restoreSpinner = cleanupSpinner;
519
+ disposeProjectStatus();
520
+ providerUsage = undefined;
521
+ turnTimer = undefined;
522
+ autoCompactionStatus = undefined;
523
+ cleanupFooter = undefined;
524
+ cleanupHeader = undefined;
525
+ cleanupEditor = undefined;
526
+ cleanupSpinner = undefined;
527
+ installedTui = undefined;
528
+
529
+ timer?.dispose();
530
+ autoCompaction?.dispose();
531
+ usage?.dispose();
532
+ try {
533
+ restoreFooter?.();
534
+ } finally {
535
+ try {
536
+ restoreHeader?.();
537
+ } finally {
538
+ try {
539
+ restoreEditor?.();
540
+ } finally {
541
+ restoreSpinner?.();
542
+ }
543
+ }
544
+ }
545
+ };
546
+
547
+ /** 菜单保存外观后重读配置并原子重装常驻 UI;未安装时只更新内存副本 */
548
+ const applyConfig = (): boolean => {
549
+ loadedConfig = readConfig();
550
+ currentConfig = loadedConfig.config;
551
+ if (active && lastContext) {
552
+ installUi(lastContext);
553
+ return true;
554
+ }
555
+ return false;
556
+ };
557
+
558
+ /** 供应商余额/套餐运行态句柄(服务注册表用;未安装时为 undefined) */
559
+ const getProviderUsage = (): PiProviderUsageController | undefined => providerUsage;
560
+
561
+ let pendingOrderNotice: ReturnType<typeof setTimeout> | undefined;
562
+ pi.on("session_start", (event, ctx) => {
563
+ lastContext = ctx;
564
+ // 包顺序自调:调整只做一次(下次启动已在前),失败静默。notify 延迟
565
+ // 到揭示完成后(约 1s)再发,避免闸门 holding 期间被最终帧覆盖。
566
+ if (event.reason !== "reload") {
567
+ if (ensureFirstPackage(agentDir, env).adjusted && !pendingOrderNotice) {
568
+ pendingOrderNotice = setTimeout(() => {
569
+ pendingOrderNotice = undefined;
570
+ ctx.ui.notify?.(
571
+ t?.("module.tui.packageOrder.notice")
572
+ ?? "pi-toolkit was moved to the front of the startup package list; restart Pi to apply",
573
+ "info",
574
+ );
575
+ }, 2_500);
576
+ pendingOrderNotice.unref?.();
577
+ }
578
+ }
579
+ if (ctx.mode !== "tui") {
580
+ transitionGate?.release(true);
581
+ return;
582
+ }
583
+ // 首次启动必须等宿主完成项目 Trust 等启动前交互后再清屏;过早 hold()
584
+ // 会把宿主授权界面擦掉。session_start 时宿主 UI 已完全启动,清屏并
585
+ // 拦帧到插件界面原子揭示(与 OpenTUI 的过渡页策略对齐)。
586
+ if (!active) {
587
+ transitionGate?.hold(installedTui, { clearVisibleScreen: true });
588
+ }
589
+ if (active) {
590
+ transitionRevealEnabled = true;
591
+ installedTui?.requestRender(true);
592
+ return;
593
+ }
594
+ if (!configWarningsShown && loadedConfig.warnings.length > 0) {
595
+ configWarningsShown = true;
596
+ ctx.ui.notify(loadedConfig.warnings.join("\n"), "warning");
597
+ }
598
+ installUi(ctx);
599
+ });
600
+
601
+ pi.on("tool_execution_end", () => {
602
+ projectStatus?.requestRefresh();
603
+ runtimeStatus?.requestRefresh();
604
+ });
605
+ pi.on("model_select", (event) => {
606
+ void providerUsage?.refresh(event.model);
607
+ requestStatusRender();
608
+ scheduleModelSwitchRepaint();
609
+ });
610
+
611
+ pi.on("turn_start", (event) => turnTelemetry.handle(event));
612
+ pi.on("message_start", (event) => turnTelemetry.handle(event));
613
+ pi.on("message_update", (event) => turnTelemetry.handle(event));
614
+ pi.on("message_end", (event) => {
615
+ turnTelemetry.handle(event);
616
+ requestStatusRender();
617
+ });
618
+ pi.on("turn_end", (event) => turnTelemetry.handle(event));
619
+ pi.on("session_info_changed", requestStatusRender);
620
+ pi.on("session_compact", requestStatusRender);
621
+ pi.on("session_tree", requestStatusRender);
622
+
623
+ pi.on("agent_start", (event) => turnTelemetry.handle(event));
624
+ pi.on("agent_end", (event) => turnTelemetry.handle(event));
625
+ pi.on("agent_settled", (event, ctx) => {
626
+ void providerUsage?.refresh(ctx.model);
627
+ turnTelemetry.settle(event, ctx.mode);
628
+ });
629
+
630
+ pi.on("session_shutdown", (event) => {
631
+ turnTelemetry.reset();
632
+ uninstallUi();
633
+ if (event.reason === "quit") transitionGate?.release(false);
634
+ });
635
+
636
+ return { applyConfig, isActive: () => active, getProviderUsage };
637
+ }