pi-one-ui 0.7.0 → 0.7.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 (49) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/README.en.md +19 -6
  3. package/README.md +19 -6
  4. package/extensions/app/config/context.ts +42 -0
  5. package/extensions/app/config/editor.ts +80 -0
  6. package/extensions/app/config/footer.ts +291 -0
  7. package/extensions/app/config/panel.ts +1 -5
  8. package/extensions/app/config/renderer.ts +1 -6
  9. package/extensions/app/config/shell.ts +358 -1435
  10. package/extensions/app/config/store.ts +22 -1
  11. package/extensions/app/config/values.ts +33 -0
  12. package/extensions/app/config/working-line.ts +100 -0
  13. package/extensions/app/overlay/input-router.ts +56 -32
  14. package/extensions/app/overlay/overlay-manager.ts +183 -21
  15. package/extensions/app/runtime/event-coordinator.ts +90 -42
  16. package/extensions/{features → app/runtime}/flush-docked-bash.ts +12 -2
  17. package/extensions/app/runtime/render-scheduler.ts +35 -31
  18. package/extensions/app/runtime/tui-runtime.ts +315 -312
  19. package/extensions/app/settings/controller.ts +202 -0
  20. package/extensions/app/settings/panel.ts +81 -0
  21. package/extensions/app/settings/presets.ts +43 -0
  22. package/extensions/features/context-inspector/index.ts +83 -662
  23. package/extensions/layouts/context/index.ts +58 -161
  24. package/extensions/layouts/context/message/user-message.ts +1 -1
  25. package/extensions/layouts/context/renderer/index.ts +31 -13
  26. package/extensions/layouts/context/renderer/mouse/interaction.ts +29 -14
  27. package/extensions/layouts/editor/controller.ts +9 -18
  28. package/extensions/layouts/editor/factory.ts +3 -3
  29. package/extensions/layouts/footer/controller.ts +30 -19
  30. package/extensions/layouts/footer/footer.ts +2 -2
  31. package/extensions/layouts/overlay/context-inspector.ts +497 -0
  32. package/extensions/{app → layouts}/overlay/selector-border.ts +5 -5
  33. package/extensions/{app → layouts}/overlay/selector-controller.ts +1 -1
  34. package/extensions/layouts/overlay/settings-panel.ts +359 -0
  35. package/extensions/{app/commands → layouts/overlay}/settings-previews.ts +4 -4
  36. package/extensions/layouts/working-line/controller.ts +3 -29
  37. package/extensions/layouts/working-line/working-line.ts +17 -114
  38. package/extensions/shared/working-line-text.ts +108 -0
  39. package/package.json +1 -1
  40. package/extensions/app/host/pi-extension-port.ts +0 -25
  41. package/extensions/app/host/pi-ui-port.ts +0 -52
  42. package/extensions/app/host/tui-capabilities.ts +0 -27
  43. package/extensions/app/ownership/layout-registry.ts +0 -65
  44. package/extensions/app/ownership.ts +0 -32
  45. package/extensions/app/panel.ts +0 -770
  46. package/extensions/app/presets.ts +0 -35
  47. package/extensions/app/runtime/runtime-state.ts +0 -83
  48. /package/extensions/{layouts/working-line → app/config}/working-line-messages.ts +0 -0
  49. /package/extensions/app/{ownership → runtime}/prototype-patch-registry.ts +0 -0
@@ -3,187 +3,84 @@ import type {
3
3
  ExtensionContext,
4
4
  Theme,
5
5
  } from "@earendil-works/pi-coding-agent";
6
- import { config } from "../../app/config/renderer.ts";
6
+ import { type Config, config } from "../../app/config/renderer.ts";
7
7
  import {
8
8
  hasUnsupportedComponentStyle,
9
- loadConfig,
10
- mergeConfig,
11
- saveUserMessagesComponentPatch,
12
- type UserMessagesComponentConfig,
13
9
  type ZentuiConfig,
14
10
  } from "../../app/config/shell.ts";
15
- import { type ConfigRecord, configStore } from "../../app/config/store.ts";
16
- // shell
17
- import piAliases from "../../features/aliases.ts";
18
- import context from "../../features/context-inspector/index.ts";
19
- import effortCommand from "../../features/effort-command.ts";
20
- import { installFlushDockedBash } from "../../features/flush-docked-bash.ts";
21
- import sessionReference from "../../features/session-reference/index.ts";
22
- // feature
23
- import agentAutocomplete from "../../features/subagent-autocomplete.ts";
24
- import {
25
- installUserMessageStyle,
26
- removeUserMessageStyle,
27
- } from "./message/user-message.ts";
28
- // renderer
11
+ import { installUserMessageStyle } from "./message/user-message.ts";
29
12
  import registerContextRenderer, {
30
13
  getCompactThinkingConfig,
14
+ type RendererExtensionOptions,
15
+ type RendererRuntimeController,
31
16
  } from "./renderer/index.ts";
32
17
  import markdownEnhance from "./renderer/markdown-enhance.ts";
33
18
  import agentSummary from "./summary/index.ts";
34
19
  import { installCompactThinking } from "./thinking/compact-thinking.ts";
35
20
 
36
- export type ContextRuntimeController = {
37
- setMode: (mode: "on" | "compact" | "off", ctx: ExtensionContext) => void;
38
- updateConfig: (
39
- partial: Partial<typeof config>,
40
- ctx: ExtensionContext,
41
- ) => void;
42
- setUserMessagesComponent: (
43
- patch: Partial<UserMessagesComponentConfig>,
44
- ctx: ExtensionContext,
45
- ) => void;
21
+ export type ContextRuntimeController = RendererRuntimeController & {
22
+ applyConfig(ctx: ExtensionContext, previous: Config): void;
46
23
  };
47
-
48
24
  export type ContextExtensionOptions = {
49
- /**
50
- * Exposes live context refresh to the unified settings panel.
51
- */
52
- onRuntimeController?: (controller: ContextRuntimeController) => void;
25
+ readonly getConfig: () => ZentuiConfig;
26
+ readonly services?: RendererExtensionOptions["services"];
53
27
  };
54
28
 
55
- let contextConfig: ZentuiConfig = loadConfig();
56
- let activeTheme: Theme | undefined;
57
- let cleanupUserMessageStyle: () => void = () => {};
58
- let userMessageStyleInstalled = false;
59
- let unsubscribeConfig: () => void = () => {};
60
-
61
- /**
62
- * Reports whether a session context can safely install TUI-owned patches.
63
- */
64
- function isTuiContext(ctx: ExtensionContext): boolean {
65
- return ctx.mode === "tui" && ctx.hasUI;
66
- }
67
-
68
- /**
69
- * Installs the Context-owned User Message renderer once for the active session.
70
- */
71
- function installContextUserMessages(): void {
72
- if (userMessageStyleInstalled || !activeTheme) return;
73
- try {
74
- cleanupUserMessageStyle = installUserMessageStyle(
75
- () => activeTheme,
76
- () => contextConfig,
77
- );
78
- userMessageStyleInstalled = true;
79
- } catch {
80
- cleanupUserMessageStyle = () => {};
81
- userMessageStyleInstalled = false;
82
- }
83
- }
84
-
85
- /**
86
- * Removes the Context-owned User Message renderer and clears its patch state.
87
- */
88
- function uninstallContextUserMessages(): void {
89
- try {
90
- cleanupUserMessageStyle();
91
- } finally {
92
- cleanupUserMessageStyle = () => {};
93
- userMessageStyleInstalled = false;
94
- removeUserMessageStyle();
95
- }
96
- }
97
-
98
- /**
99
- * Reconciles User Message ownership with the latest shell configuration.
100
- */
101
- function reconcileContextUserMessages(): void {
102
- const enabled =
103
- contextConfig.components.userMessages.enabled &&
104
- !hasUnsupportedComponentStyle(contextConfig, "userMessages");
105
- if (enabled) installContextUserMessages();
106
- else uninstallContextUserMessages();
107
- }
29
+ /** Context 仅组织自身内容组件,Feature 的启用由 app 决定。 */
30
+ export default function registerContext(
31
+ pi: ExtensionAPI,
32
+ options: ContextExtensionOptions,
33
+ ): ContextRuntimeController {
34
+ let activeTheme: Theme | undefined;
35
+ let cleanupUserMessages: (() => void) | undefined;
36
+ const reconcile = (): void => {
37
+ const current = options.getConfig();
38
+ const enabled =
39
+ current.components.userMessages.enabled &&
40
+ !hasUnsupportedComponentStyle(current, "userMessages");
41
+ if (!enabled || !activeTheme) {
42
+ cleanupUserMessages?.();
43
+ cleanupUserMessages = undefined;
44
+ return;
45
+ }
46
+ if (!cleanupUserMessages) {
47
+ cleanupUserMessages = installUserMessageStyle(
48
+ () => activeTheme,
49
+ options.getConfig,
50
+ );
51
+ }
52
+ };
108
53
 
109
- /**
110
- * Starts the Context session binding and subscribes it to shared config updates.
111
- */
112
- function startContextSession(ctx: ExtensionContext): void {
113
- if (!isTuiContext(ctx)) return;
114
- activeTheme = ctx.ui.theme;
115
- contextConfig = loadConfig();
116
- unsubscribeConfig();
117
- unsubscribeConfig = configStore.subscribe((record: ConfigRecord) => {
118
- contextConfig = mergeConfig(record);
119
- reconcileContextUserMessages();
54
+ markdownEnhance(pi);
55
+ const thinking = installCompactThinking(pi, getCompactThinkingConfig());
56
+ const renderer = registerContextRenderer(pi, undefined, thinking, {
57
+ services: options.services,
120
58
  });
121
- reconcileContextUserMessages();
122
- }
123
-
124
- /**
125
- * Applies a User Message configuration patch owned by Context.
126
- *
127
- * @param patch User Message configuration changes.
128
- * @param _ctx Active Pi extension context.
129
- */
130
- function setContextUserMessages(
131
- patch: Partial<UserMessagesComponentConfig>,
132
- _ctx: ExtensionContext,
133
- ): void {
134
- contextConfig = saveUserMessagesComponentPatch(patch);
135
- if (patch.enabled !== undefined || patch.style !== undefined)
136
- reconcileContextUserMessages();
137
- }
138
-
139
- /**
140
- * Releases Context session bindings and restores the original User Message patch.
141
- */
142
- function stopContextSession(): void {
143
- unsubscribeConfig();
144
- unsubscribeConfig = () => {};
145
- uninstallContextUserMessages();
146
- activeTheme = undefined;
147
- }
148
-
149
- /**
150
- * Registers the Context layout, its renderer stack and its feature hooks.
151
- */
152
- export default function (
153
- pi: ExtensionAPI,
154
- options: ContextExtensionOptions = {},
155
- ): void {
156
- // shell chrome
157
- if (config.enableAliases) piAliases(pi);
158
- if (config.enableEffortCommand) {
159
- effortCommand(pi);
59
+ if (config.enableAgentSummary) {
60
+ agentSummary(pi);
160
61
  }
161
- installFlushDockedBash();
162
- // The thinking controller is queried directly by the context render stack.
163
- markdownEnhance(pi);
164
- registerContextRenderer(
165
- pi,
166
- undefined,
167
- installCompactThinking(pi, getCompactThinkingConfig()),
168
- {
169
- onRuntimeController: (controller) =>
170
- options.onRuntimeController?.({
171
- ...controller,
172
- setUserMessagesComponent: setContextUserMessages,
173
- }),
174
- },
175
- );
176
-
177
- // features
178
- if (config.enableContextCommand) context(pi);
179
- if (config.enableSessionReference) sessionReference(pi);
180
- if (config.enableSubagentAutocomplete) agentAutocomplete(pi);
181
- if (config.enableAgentSummary) agentSummary(pi);
182
-
183
62
  pi.on("session_start", (_event, ctx) => {
184
- startContextSession(ctx);
63
+ if (ctx.mode !== "tui" || !ctx.hasUI) {
64
+ return;
65
+ }
66
+ activeTheme = ctx.ui.theme;
67
+ reconcile();
185
68
  });
186
69
  pi.on("session_shutdown", () => {
187
- stopContextSession();
70
+ cleanupUserMessages?.();
71
+ cleanupUserMessages = undefined;
72
+ activeTheme = undefined;
188
73
  });
74
+ return {
75
+ ...renderer,
76
+ applyConfig(ctx, previous) {
77
+ reconcile();
78
+ thinking.updateConfig(getCompactThinkingConfig());
79
+ if (config.mode !== previous.mode) {
80
+ renderer.setMode(config.mode, ctx);
81
+ } else {
82
+ renderer.updateConfig(config, ctx);
83
+ }
84
+ },
85
+ };
189
86
  }
@@ -7,7 +7,7 @@ import type { ZentuiConfig } from "../../../app/config/shell.ts";
7
7
  import {
8
8
  installPrototypePatch,
9
9
  removePrototypePatch,
10
- } from "../../../app/ownership/prototype-patch-registry.ts";
10
+ } from "../../../app/runtime/prototype-patch-registry.ts";
11
11
  import {
12
12
  sanitizeRenderedUserMessageLines,
13
13
  sanitizeRenderedUserMessageText,
@@ -5,7 +5,6 @@ import {
5
5
  config,
6
6
  normalizeConfig,
7
7
  setConfig,
8
- updateConfig,
9
8
  } from "../../../app/config/renderer.ts";
10
9
  import { isLazyProxyTui } from "../../../tools/fullscreen-detect.ts";
11
10
  import {
@@ -30,6 +29,7 @@ import {
30
29
  resetToolHoverState,
31
30
  scheduleSessionRender,
32
31
  TOOL_MOUSE_DISABLE,
32
+ type ToolMouseServices,
33
33
  teardownToolMouseInteraction,
34
34
  } from "./mouse/interaction.ts";
35
35
  import { getToolMouseTui } from "./mouse/scroll.ts";
@@ -65,14 +65,19 @@ let compactModeHooks: CompactModeHooks | undefined;
65
65
  function refreshCurrentContext(
66
66
  ctx?: any,
67
67
  toolGrouping?: ToolGroupingHooks,
68
+ render?: ToolMouseServices["render"],
68
69
  ): void {
69
70
  const tui = getToolMouseTui();
70
71
  toolGrouping?.refresh(tui);
71
72
  refreshMessageDisplays(tui);
72
73
  refreshCompactModeComponents(tui);
73
74
  compactModeHooks?.refresh();
74
- tui?.requestRender?.(true);
75
- ctx?.ui?.requestRender?.(true);
75
+ if (render) {
76
+ render.request(true);
77
+ } else {
78
+ tui?.requestRender?.(true);
79
+ ctx?.ui?.requestRender?.(true);
80
+ }
76
81
  }
77
82
 
78
83
  function syncCompactMode(ctx: any): void {
@@ -84,8 +89,9 @@ function applyStyleMode(
84
89
  mode: CompactStyleMode,
85
90
  ctx: any,
86
91
  toolGrouping?: ToolGroupingHooks,
92
+ render?: ToolMouseServices["render"],
87
93
  ): void {
88
- updateConfig({ mode });
94
+ setConfig(normalizeConfig({ ...config, mode }));
89
95
  if (mode === "off") {
90
96
  // Native rendering:清 hover/click,关闭鼠标上报以恢复终端默认滚轮。
91
97
  resetToolHoverState();
@@ -103,10 +109,10 @@ function applyStyleMode(
103
109
  }
104
110
  // Reshape immediately and once more after the panel/custom UI unmounts and
105
111
  // the main context is mounted again.
106
- refreshCurrentContext(ctx, toolGrouping);
112
+ refreshCurrentContext(ctx, toolGrouping, render);
107
113
  scheduleSessionRender(() => {
108
114
  if (mode === "compact") syncCompactMode(ctx);
109
- refreshCurrentContext(ctx, toolGrouping);
115
+ refreshCurrentContext(ctx, toolGrouping, render);
110
116
  });
111
117
  ctx.ui.notify(`Claude Code style: ${mode}`, "info");
112
118
  }
@@ -129,6 +135,7 @@ export type RendererRuntimeController = {
129
135
  };
130
136
 
131
137
  export type RendererExtensionOptions = {
138
+ services?: ToolMouseServices;
132
139
  onRuntimeController?: (controller: RendererRuntimeController) => void;
133
140
  };
134
141
 
@@ -183,17 +190,27 @@ export default function (
183
190
  return installation;
184
191
  };
185
192
 
186
- options.onRuntimeController?.({
193
+ const controller: RendererRuntimeController = {
187
194
  setMode: (mode, ctx) =>
188
- applyStyleMode(mode, ctx, installation?.toolGrouping),
195
+ applyStyleMode(
196
+ mode,
197
+ ctx,
198
+ installation?.toolGrouping,
199
+ options.services?.render,
200
+ ),
189
201
  updateConfig: (partial, ctx) => {
190
- updateConfig(partial);
202
+ setConfig(normalizeConfig({ ...config, ...partial }));
191
203
  // 配置(如 expandedPreviewMaxLines / excludeRenderers)变化可能改变
192
204
  // 渲染结果:丢弃工具卡跨 toggle 缓存,下次 updateDisplay 重建。
193
205
  installation?.toggleRenderCache.clear();
194
- refreshCurrentContext(ctx, installation?.toolGrouping);
206
+ refreshCurrentContext(
207
+ ctx,
208
+ installation?.toolGrouping,
209
+ options.services?.render,
210
+ );
195
211
  },
196
- });
212
+ };
213
+ options.onRuntimeController?.(controller);
197
214
 
198
215
  pi.on("message_update", async (event) => {
199
216
  // compact-thinking 在 session_start 时于 compact 补丁之上再装一层;
@@ -221,7 +238,7 @@ export default function (
221
238
  // 鼠标交互独立于渲染层:fullscreen 渲染层让位(hooks undefined)但
222
239
  // 工具点击/回到底部适配仍需安装;保持在渲染层安装之后以维持原顺序。
223
240
  if (ctx?.mode === "tui" && ctx?.hasUI)
224
- installToolMouseInteraction(ctx, mouseOwner);
241
+ installToolMouseInteraction(ctx, mouseOwner, options.services);
225
242
  if (!hooks) return;
226
243
  hooks.toolGrouping.setTheme(ctx.ui.theme);
227
244
  hooks.toggleRenderCache.setTheme(ctx.ui.theme);
@@ -240,7 +257,7 @@ export default function (
240
257
  // Compaction rebuilds the context without session_start. Rebind after
241
258
  // other TUI extensions may have replaced the root input dispatcher.
242
259
  if (ctx?.mode === "tui" && ctx?.hasUI)
243
- installToolMouseInteraction(ctx, mouseOwner);
260
+ installToolMouseInteraction(ctx, mouseOwner, options.services);
244
261
  if (!hooks) return;
245
262
  hooks.toolGrouping.setTheme(ctx.ui.theme);
246
263
  hooks.toggleRenderCache.setTheme(ctx.ui.theme);
@@ -282,6 +299,7 @@ export default function (
282
299
  clearAllAnimations();
283
300
  installation = undefined;
284
301
  });
302
+ return controller;
285
303
  }
286
304
 
287
305
  // ---- 对外导出:入口/测试实际消费的符号 ----
@@ -1,15 +1,19 @@
1
1
  import { config } from "../../../../app/config/renderer.ts";
2
- import { inputRouter } from "../../../../app/overlay/input-router.ts";
3
- import { overlayManager } from "../../../../app/overlay/overlay-manager.ts";
4
2
  import {
5
- hasActiveTextPreview,
6
- showTextPreview,
7
- } from "../../../../features/context-inspector/index.ts";
3
+ type InputRouter,
4
+ inputRouter,
5
+ } from "../../../../app/overlay/input-router.ts";
6
+ import {
7
+ type OverlayManager,
8
+ overlayManager,
9
+ } from "../../../../app/overlay/overlay-manager.ts";
10
+ import type { RenderScheduler } from "../../../../app/runtime/render-scheduler.ts";
8
11
  import { isLazyProxyTui } from "../../../../tools/fullscreen-detect.ts";
9
12
  import {
10
13
  patchRegistry,
11
14
  TOOL_MOUSE_OWNER_KEY,
12
15
  } from "../../../../tools/patch-keys.ts";
16
+ import { showTextPreview } from "../../../overlay/context-inspector.ts";
13
17
  import { ThinkingPreviewBlock } from "../../thinking/compact-thinking.ts";
14
18
  import {
15
19
  isCompactAssistantComponent,
@@ -101,6 +105,7 @@ const DEFAULT_TOOL_MOUSE_OWNER = {};
101
105
  export const TOOL_MOUSE_DISABLE = "\x1b[?1006l\x1b[?1003l\x1b[?1000l";
102
106
 
103
107
  let toolMouseUi: any = null;
108
+ let toolMouseOverlays = overlayManager;
104
109
  let toolMouseInputUnsubscribe: (() => void) | null = null;
105
110
  let toolMouseRenderPatchTui: any = null;
106
111
  let toolMouseRenderPatchOriginal: ((...args: any[]) => any) | null = null;
@@ -144,7 +149,7 @@ function tryOpenToolIoShowMore(region: InteractionRegion): boolean {
144
149
  const title = section === "input" ? "Tool Input" : "Tool Output";
145
150
  const content =
146
151
  section === "input" ? ioView.getInputBody() : ioView.getOutputBody();
147
- void showTextPreview({ ui }, title, content || "(empty)");
152
+ void showTextPreview({ ui }, title, content || "(empty)", toolMouseOverlays);
148
153
  return true;
149
154
  }
150
155
 
@@ -498,7 +503,7 @@ function patchFullscreenViewportInput(tui: any): void {
498
503
  const packets = parseSgrMousePackets(data);
499
504
  // 官方 fullscreen 会消费全部鼠标;文本预览 overlay 活动时放行给 focused
500
505
  // custom component,使 [esc] 点击和滚轮可用。
501
- if (packets && tui.hasOverlay?.() && hasActiveTextPreview())
506
+ if (packets && tui.hasOverlay?.() && toolMouseOverlays.hasActive())
502
507
  return undefined;
503
508
  if (packets && !tui.hasOverlay?.()) {
504
509
  for (const packet of packets) {
@@ -942,9 +947,19 @@ export function resetToolHoverState(): void {
942
947
  * @param ctx Active TUI extension context.
943
948
  * @param owner Ownership token used to make teardown safe across reloads.
944
949
  */
950
+ export type ToolMouseServices = {
951
+ input: InputRouter;
952
+ overlays: OverlayManager;
953
+ render?: RenderScheduler;
954
+ };
955
+
945
956
  export function installToolMouseInteraction(
946
957
  ctx: any,
947
958
  owner: object = DEFAULT_TOOL_MOUSE_OWNER,
959
+ services: ToolMouseServices = {
960
+ input: inputRouter,
961
+ overlays: overlayManager,
962
+ },
948
963
  ): void {
949
964
  teardownToolMouseInteraction(toolMouseInstallationOwner ?? owner);
950
965
  if (ctx?.mode !== "tui" || !ctx?.hasUI) return;
@@ -958,9 +973,11 @@ export function installToolMouseInteraction(
958
973
  patchRegistry.install(TOOL_MOUSE_OWNER_KEY, owner);
959
974
  setHoveredToolCallId(null);
960
975
  toolMouseUi = ctx.ui;
976
+ toolMouseOverlays = services.overlays;
961
977
  // 0.84+ 的 tui 是惰性 Proxy:regular 保留原生 scrollback;fullscreen
962
978
  // 由官方 LayoutFrame 命中,并由扩展补齐 hover 所需的 all-motion 上报。
963
979
  ctx.ui.setWidget(TOOL_MOUSE_WIDGET_KEY, (tui: any, theme: any) => {
980
+ services.render?.register(owner, (force) => tui.requestRender(force));
964
981
  setToolMouseTui(tui);
965
982
  setToolTuiFullscreen(fullscreenLazyTui(tui));
966
983
  if (isLazyProxyTui(tui)) {
@@ -987,17 +1004,15 @@ export function installToolMouseInteraction(
987
1004
  setScrollButtonWidget(widget);
988
1005
  return widget;
989
1006
  });
990
- const removeMouseRoute = inputRouter.register(
1007
+ const removeInput = services.input.bind(
1008
+ ctx.ui,
991
1009
  (data) =>
992
- overlayManager.hasActive() ? undefined : handleToolMouseInput(data),
1010
+ services.overlays.hasActive() ? undefined : handleToolMouseInput(data),
993
1011
  100,
994
1012
  );
995
- const removeHostInput = ctx.ui.onTerminalInput((data: string) =>
996
- inputRouter.dispatch(data),
997
- );
998
1013
  toolMouseInputUnsubscribe = () => {
999
- removeHostInput();
1000
- removeMouseRoute();
1014
+ removeInput();
1015
+ services.render?.register(owner, undefined);
1001
1016
  };
1002
1017
  }
1003
1018
 
@@ -29,12 +29,9 @@ export type EditorChangeResult = { ok: true } | { ok: false; reason: string };
29
29
 
30
30
  export type EditorLayoutControllerContext = {
31
31
  readonly getConfig: () => PolishedTuiConfig;
32
- readonly saveComponent: (
33
- patch: Partial<PolishedTuiConfig["components"]["editor"]>,
34
- ) => PolishedTuiConfig;
35
32
  readonly getState: () => FooterState;
36
33
  readonly sessionLifecycle: SessionLifecycle;
37
- readonly render: Pick<RenderScheduler, "request">;
34
+ readonly render: Pick<RenderScheduler, "request" | "register">;
38
35
  readonly getThinkingLevel: () => ReturnType<ExtensionAPI["getThinkingLevel"]>;
39
36
  readonly getAgentDurationMs: () => number;
40
37
  readonly isAgentActive: () => boolean;
@@ -54,7 +51,6 @@ export class EditorLayoutController {
54
51
  private readonly context: EditorLayoutControllerContext;
55
52
  private readonly ownerToken = Symbol("pi-one-ui-editor-owner");
56
53
  private activeTuiContext: ExtensionContext | undefined;
57
- private requestEditorRender: (() => void) | undefined;
58
54
  private editorInstalled = false;
59
55
  private editorInstallMode: EditorInstallMode = "none";
60
56
  private installedEditorFactory: EditorFactory | undefined;
@@ -131,17 +127,16 @@ export class EditorLayoutController {
131
127
  }
132
128
 
133
129
  /**
134
- * Applies an Editor component configuration patch and reconciles the host.
130
+ * 应用 app 已提交的 Editor 配置并更新组件。
135
131
  *
136
- * @param patch Editor configuration changes to persist.
137
- * @param ctx Active Pi extension context.
138
- * @returns Whether the runtime applied the requested replacement.
132
+ * @param patch 本次修改的 Editor 字段。
133
+ * @param ctx 当前 Pi session 的上下文。
134
+ * @returns 组件应用结果。
139
135
  */
140
- setComponent(
136
+ applyConfig(
141
137
  patch: Partial<PolishedTuiConfig["components"]["editor"]>,
142
138
  ctx: ExtensionContext,
143
139
  ): { applied: boolean; reason?: string } {
144
- this.context.saveComponent(patch);
145
140
  let result: EditorChangeResult | undefined;
146
141
  if (patch.style === "off") {
147
142
  // off 透传原生渲染:不替换工厂,既有编辑器实例与 overlay preFocus
@@ -298,7 +293,6 @@ export class EditorLayoutController {
298
293
  */
299
294
  cleanup(ctx?: ExtensionContext): void {
300
295
  this.resetAgentTimer();
301
- this.context.sessionLifecycle.shutdown();
302
296
  if (ctx && this.isTuiContext(ctx)) {
303
297
  try {
304
298
  const currentFactory = ctx.ui.getEditorComponent();
@@ -413,7 +407,7 @@ export class EditorLayoutController {
413
407
  */
414
408
  private clearEditorOwnership(): void {
415
409
  this.setMinimalistDecorationActive(false);
416
- this.requestEditorRender = undefined;
410
+ this.context.render.register(this, undefined);
417
411
  this.wrappedEditorFactory = undefined;
418
412
  this.installedEditorFactory = undefined;
419
413
  this.editorInstallMode = "none";
@@ -481,7 +475,7 @@ export class EditorLayoutController {
481
475
  isAgentActive: this.context.isAgentActive,
482
476
  getProjectRoot: this.context.getProjectRoot,
483
477
  onRender: (requestRender) => {
484
- this.requestEditorRender = requestRender;
478
+ this.context.render.register(this, requestRender);
485
479
  },
486
480
  onDecorationActive: (active) =>
487
481
  this.setMinimalistDecorationActive(active),
@@ -532,11 +526,8 @@ export class EditorLayoutController {
532
526
  });
533
527
  }
534
528
 
535
- /**
536
- * Requests both the mounted editor redraw and the shared runtime redraw.
537
- */
529
+ /** 向 app 提交共享 TUI 刷新请求。 */
538
530
  requestRender(): void {
539
- this.requestEditorRender?.();
540
531
  this.context.render.request();
541
532
  }
542
533
  }
@@ -24,7 +24,7 @@ export type EditorFactoryRuntime = {
24
24
  readonly getAgentDurationMs: () => number;
25
25
  readonly isAgentActive: () => boolean;
26
26
  readonly getProjectRoot: () => string | undefined;
27
- readonly onRender: (requestRender: () => void) => void;
27
+ readonly onRender: (requestRender: (force: boolean) => void) => void;
28
28
  readonly onDecorationActive: (active: boolean) => void;
29
29
  };
30
30
 
@@ -44,7 +44,7 @@ export function createEditorFactory(
44
44
  theme: EditorTheme,
45
45
  keybindings: KeybindingsManager,
46
46
  ) => {
47
- runtime.onRender(() => tui.requestRender());
47
+ runtime.onRender((force) => tui.requestRender(force));
48
48
  return new PolishedEditor(
49
49
  tui,
50
50
  theme,
@@ -78,7 +78,7 @@ export function createWrappedEditorFactory(
78
78
  theme: EditorTheme,
79
79
  keybindings: KeybindingsManager,
80
80
  ) => {
81
- runtime.onRender(() => tui.requestRender());
81
+ runtime.onRender((force) => tui.requestRender(force));
82
82
  return new WrappedPolishedEditor(
83
83
  baseFactory(tui, theme, keybindings),
84
84
  runtime.sessionTheme,