pi-one-ui 0.3.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/CHANGELOG.md +45 -1
- package/README.en.md +33 -28
- package/README.md +11 -5
- package/extensions/app/commands/settings-previews.ts +23 -56
- package/extensions/app/config/shell.ts +26 -187
- package/extensions/app/panel.ts +42 -24
- package/extensions/app/presets.ts +2 -2
- package/extensions/app/runtime/tui-runtime.ts +1 -26
- package/extensions/layouts/context/renderer/index.ts +15 -0
- package/extensions/layouts/context/renderer/tool/toggle-render-cache.ts +333 -0
- package/extensions/layouts/editor/controller.ts +16 -62
- package/extensions/layouts/editor/factory.ts +2 -20
- package/extensions/layouts/editor/index.ts +0 -6
- package/extensions/layouts/editor/minimalist-editor.ts +11 -57
- package/extensions/layouts/editor/ui.ts +60 -605
- package/extensions/layouts/footer/footer.ts +7 -3
- package/extensions/layouts/working-line/interaction-summary.ts +47 -7
- package/extensions/layouts/working-line/working-line.ts +24 -3
- package/extensions/shared/icons.ts +0 -4
- package/extensions/shared/style.ts +48 -0
- package/extensions/tools/patch-keys.ts +5 -0
- package/package.json +2 -3
- package/themes/cc-dark.json +4 -1
- package/themes/cc-light.json +4 -1
- package/extensions/layouts/editor/accent-rail-editor.ts +0 -112
- package/extensions/layouts/editor/accent-rail-layout-patch.ts +0 -530
- package/extensions/layouts/editor/completion-menu.ts +0 -183
|
@@ -47,6 +47,10 @@ import {
|
|
|
47
47
|
setMessageDisplayTheme,
|
|
48
48
|
} from "./tool/message-display.ts";
|
|
49
49
|
import { clearAllAnimations } from "./tool/result.ts";
|
|
50
|
+
import {
|
|
51
|
+
installToggleRenderCache,
|
|
52
|
+
type ToggleRenderCacheHooks,
|
|
53
|
+
} from "./tool/toggle-render-cache.ts";
|
|
50
54
|
|
|
51
55
|
/**
|
|
52
56
|
* Claude Code Style for pi — 装配入口。
|
|
@@ -143,6 +147,7 @@ export default function (
|
|
|
143
147
|
| {
|
|
144
148
|
defaultMode: DefaultModeHooks;
|
|
145
149
|
toolGrouping: ToolGroupingHooks;
|
|
150
|
+
toggleRenderCache: ToggleRenderCacheHooks;
|
|
146
151
|
compactMode: CompactModeHooks;
|
|
147
152
|
disposeMessageDisplay: () => void;
|
|
148
153
|
disposeToolExpandedBackground: () => void;
|
|
@@ -162,12 +167,15 @@ export default function (
|
|
|
162
167
|
});
|
|
163
168
|
compactModeHooks = compactMode;
|
|
164
169
|
const disposeMessageDisplay = installMessageDisplayRendering();
|
|
170
|
+
// 跨 toggle 重建缓存必须在展开背景 patch 之下(背景后装外层,shutdown 先拆)。
|
|
171
|
+
const toggleRenderCache = installToggleRenderCache();
|
|
165
172
|
// 展开背景必须在 compact-mode 之后装,shutdown 时先于 compact 释放。
|
|
166
173
|
const disposeToolExpandedBackground = installToolExpandedBackground();
|
|
167
174
|
deactivateLegacyCompactionRendering();
|
|
168
175
|
installation = {
|
|
169
176
|
defaultMode,
|
|
170
177
|
toolGrouping,
|
|
178
|
+
toggleRenderCache,
|
|
171
179
|
compactMode,
|
|
172
180
|
disposeMessageDisplay,
|
|
173
181
|
disposeToolExpandedBackground,
|
|
@@ -180,6 +188,9 @@ export default function (
|
|
|
180
188
|
applyStyleMode(mode, ctx, installation?.toolGrouping),
|
|
181
189
|
updateConfig: (partial, ctx) => {
|
|
182
190
|
updateConfig(partial);
|
|
191
|
+
// 配置(如 expandedPreviewMaxLines / excludeRenderers)变化可能改变
|
|
192
|
+
// 渲染结果:丢弃工具卡跨 toggle 缓存,下次 updateDisplay 重建。
|
|
193
|
+
installation?.toggleRenderCache.clear();
|
|
183
194
|
refreshCurrentContext(ctx, installation?.toolGrouping);
|
|
184
195
|
},
|
|
185
196
|
});
|
|
@@ -213,6 +224,7 @@ export default function (
|
|
|
213
224
|
installToolMouseInteraction(ctx, mouseOwner);
|
|
214
225
|
if (!hooks) return;
|
|
215
226
|
hooks.toolGrouping.setTheme(ctx.ui.theme);
|
|
227
|
+
hooks.toggleRenderCache.setTheme(ctx.ui.theme);
|
|
216
228
|
setMessageDisplayTheme(ctx.ui.theme);
|
|
217
229
|
ctx.ui.setStatus("ccstyle", undefined);
|
|
218
230
|
// Collect the resumed context before syncing compact patches and expansion state.
|
|
@@ -231,6 +243,7 @@ export default function (
|
|
|
231
243
|
installToolMouseInteraction(ctx, mouseOwner);
|
|
232
244
|
if (!hooks) return;
|
|
233
245
|
hooks.toolGrouping.setTheme(ctx.ui.theme);
|
|
246
|
+
hooks.toggleRenderCache.setTheme(ctx.ui.theme);
|
|
234
247
|
setMessageDisplayTheme(ctx.ui.theme);
|
|
235
248
|
syncCompactMode(ctx);
|
|
236
249
|
scheduleSessionRender(() => {
|
|
@@ -248,6 +261,7 @@ export default function (
|
|
|
248
261
|
|
|
249
262
|
pi.on("tool_execution_start", async (_event, ctx) => {
|
|
250
263
|
installation?.toolGrouping.setTheme(ctx.ui.theme);
|
|
264
|
+
installation?.toggleRenderCache.setTheme(ctx.ui.theme);
|
|
251
265
|
});
|
|
252
266
|
|
|
253
267
|
pi.on("session_shutdown", async () => {
|
|
@@ -260,6 +274,7 @@ export default function (
|
|
|
260
274
|
current.defaultMode.shutdown();
|
|
261
275
|
current.toolGrouping.shutdown();
|
|
262
276
|
current.disposeToolExpandedBackground();
|
|
277
|
+
current.toggleRenderCache.shutdown();
|
|
263
278
|
current.compactMode.shutdown();
|
|
264
279
|
compactModeHooks = undefined;
|
|
265
280
|
current.disposeMessageDisplay();
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 工具卡 updateDisplay 跨 toggle 重建缓存(on/compact 共用,off 关闭)。
|
|
3
|
+
*
|
|
4
|
+
* 原生 ToolExecutionComponent.updateDisplay 每次调用都会 clear 后重新调用
|
|
5
|
+
* call/result renderer 生成全新组件,新组件内部行缓存为空,导致折叠/展开
|
|
6
|
+
* 切换(Ctrl+O、group.setExpanded、鼠标点击)每次都要重新 wrap 输出全文,
|
|
7
|
+
* 大输出下可达数百毫秒同步阻塞。
|
|
8
|
+
*
|
|
9
|
+
* 这里按工具实例缓存(collapsed/expanded 两槽)的渲染指纹:内容指纹
|
|
10
|
+
* (args/result/isPartial/executionStarted/argsComplete/showImages/mode/theme)
|
|
11
|
+
* 未变化时跳过原生全量重建;容器内当前槽位与目标槽位不同时,把目标槽构建
|
|
12
|
+
* 时缓存的 call/result 组件重新装入对应壳容器(selfRenderContainer /
|
|
13
|
+
* contentBox / contentText)并恢复派生字段,实现零重建的来回切换。
|
|
14
|
+
*
|
|
15
|
+
* 失效面:result/args 等输入变化(指纹比较)、模式切换(mode 入指纹)、
|
|
16
|
+
* 主题或配置变化(setTheme / clear 由装配层在既有回调中驱动)。
|
|
17
|
+
* fallback 分支(contentText,无 renderer 定义的工具)不做组件级切换,
|
|
18
|
+
* 切槽时退化为原生重建,与现状一致。
|
|
19
|
+
*/
|
|
20
|
+
import { ToolExecutionComponent } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { config } from "../../../../app/config/renderer.ts";
|
|
22
|
+
import {
|
|
23
|
+
patchRegistry,
|
|
24
|
+
TOGGLE_RENDER_CACHE_PATCH,
|
|
25
|
+
} from "../../../../tools/patch-keys.ts";
|
|
26
|
+
import { scheduleAnimation } from "./result.ts";
|
|
27
|
+
|
|
28
|
+
type RenderSlot = {
|
|
29
|
+
/** 输入指纹(args/result/isPartial 等,不含 expanded)。 */
|
|
30
|
+
inputFingerprint: unknown[];
|
|
31
|
+
/** 构建时的派生组件引用(call/result),用于同槽外部污染检测。 */
|
|
32
|
+
components: [unknown, unknown];
|
|
33
|
+
hideComponent: boolean;
|
|
34
|
+
/** 构建时实际挂载的壳载体:box / self / text / null(未挂候选容器)。 */
|
|
35
|
+
shell: "box" | "self" | "text" | null;
|
|
36
|
+
callComponent: unknown;
|
|
37
|
+
resultComponent: unknown;
|
|
38
|
+
/** rendererState 中 ccstyle 渲染层的指针快照(切槽重装时恢复)。 */
|
|
39
|
+
stateSnapshot: [unknown, unknown, unknown];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type ComponentCache = {
|
|
43
|
+
expanded?: RenderSlot;
|
|
44
|
+
collapsed?: RenderSlot;
|
|
45
|
+
/** 容器当前装着的槽位(重建/装载后更新)。 */
|
|
46
|
+
current?: "expanded" | "collapsed";
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
let componentCaches = new WeakMap<object, ComponentCache>();
|
|
50
|
+
|
|
51
|
+
type ToggleRenderCachePatch = {
|
|
52
|
+
active: boolean;
|
|
53
|
+
prototype: any;
|
|
54
|
+
installed: () => void;
|
|
55
|
+
original: () => void;
|
|
56
|
+
theme: unknown;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type ToggleRenderCacheHooks = {
|
|
60
|
+
setTheme(theme: unknown): void;
|
|
61
|
+
/** 配置/主题等外部状态变化后丢弃全部缓存(下次 updateDisplay 重建)。 */
|
|
62
|
+
clear(): void;
|
|
63
|
+
shutdown(): void;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** 渲染输入指纹:不含 expanded(由槽位区分),不含组件引用。 */
|
|
67
|
+
function fingerprintOf(
|
|
68
|
+
component: any,
|
|
69
|
+
patch: ToggleRenderCachePatch,
|
|
70
|
+
): unknown[] {
|
|
71
|
+
return [
|
|
72
|
+
component.args,
|
|
73
|
+
component.result,
|
|
74
|
+
component.isPartial === true,
|
|
75
|
+
component.executionStarted === true,
|
|
76
|
+
component.argsComplete === true,
|
|
77
|
+
component.showImages === true,
|
|
78
|
+
config.mode,
|
|
79
|
+
patch.theme,
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function componentsOf(component: any): [unknown, unknown] {
|
|
84
|
+
return [component.callRendererComponent, component.resultRendererComponent];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function sameComponents(
|
|
88
|
+
left: [unknown, unknown],
|
|
89
|
+
right: [unknown, unknown],
|
|
90
|
+
): boolean {
|
|
91
|
+
return left[0] === right[0] && left[1] === right[1];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function sameFingerprint(left: unknown[], right: unknown[]): boolean {
|
|
95
|
+
if (left.length !== right.length) return false;
|
|
96
|
+
for (let i = 0; i < left.length; i++) {
|
|
97
|
+
if (left[i] !== right[i]) return false;
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 从 children 中识别当前挂载的候选壳(无副作用,不调用 getRenderShell)。 */
|
|
103
|
+
function mountedShell(component: any): "box" | "self" | "text" | null {
|
|
104
|
+
const children = Array.isArray(component?.children) ? component.children : [];
|
|
105
|
+
for (const child of children) {
|
|
106
|
+
if (child === component.contentBox) return "box";
|
|
107
|
+
if (child === component.selfRenderContainer) return "self";
|
|
108
|
+
if (child === component.contentText) return "text";
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function shellContainer(
|
|
114
|
+
component: any,
|
|
115
|
+
shell: "box" | "self" | "text" | null,
|
|
116
|
+
): any {
|
|
117
|
+
if (shell === "box") return component.contentBox;
|
|
118
|
+
if (shell === "self") return component.selfRenderContainer;
|
|
119
|
+
if (shell === "text") return component.contentText;
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** 与 default-mode 的 syncToolShell 同款:把目标壳换入 children,排除其余候选。 */
|
|
124
|
+
function syncShell(component: any, shell: "box" | "self" | "text"): void {
|
|
125
|
+
const target = shellContainer(component, shell);
|
|
126
|
+
if (!target || !Array.isArray(component.children)) return;
|
|
127
|
+
const candidates = new Set(
|
|
128
|
+
[
|
|
129
|
+
component.contentText,
|
|
130
|
+
component.contentBox,
|
|
131
|
+
component.selfRenderContainer,
|
|
132
|
+
].filter(Boolean),
|
|
133
|
+
);
|
|
134
|
+
const indexes = component.children
|
|
135
|
+
.map((child: any, index: number) => (candidates.has(child) ? index : -1))
|
|
136
|
+
.filter((index: number) => index >= 0);
|
|
137
|
+
const targetIndex = indexes[0];
|
|
138
|
+
if (targetIndex === undefined) return;
|
|
139
|
+
component.children[targetIndex] = target;
|
|
140
|
+
for (const index of indexes.sort(
|
|
141
|
+
(left: number, right: number) => right - left,
|
|
142
|
+
)) {
|
|
143
|
+
if (index !== targetIndex) component.children.splice(index, 1);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function stateSnapshotOf(component: any): [unknown, unknown, unknown] {
|
|
148
|
+
const state = component?.rendererState;
|
|
149
|
+
return [
|
|
150
|
+
state?.ccstyleIoView,
|
|
151
|
+
state?.ccstyleExpandedIoView,
|
|
152
|
+
state?.ccstyleToolVisualState,
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function restoreStateSnapshot(
|
|
157
|
+
component: any,
|
|
158
|
+
slotKey: "expanded" | "collapsed",
|
|
159
|
+
snapshot: [unknown, unknown, unknown],
|
|
160
|
+
): void {
|
|
161
|
+
if (
|
|
162
|
+
component?.rendererState === undefined &&
|
|
163
|
+
snapshot.every((v) => v === undefined)
|
|
164
|
+
) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const state = (component.rendererState ??= {});
|
|
168
|
+
// ioView/visualState 每槽构建时都会写入,按槽恢复;
|
|
169
|
+
// expandedIoView 是跨槽保留指针(折叠 renderer 不清),只在展开槽恢复。
|
|
170
|
+
state.ccstyleIoView = snapshot[0];
|
|
171
|
+
state.ccstyleToolVisualState = snapshot[1];
|
|
172
|
+
if (slotKey === "expanded") state.ccstyleExpandedIoView = snapshot[2];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 把缓存槽的 call/result 组件装回目标壳容器并恢复派生字段。
|
|
177
|
+
* 同时恢复 ccstyle 渲染层的 rendererState 指针快照(ioView / expandedIoView /
|
|
178
|
+
* visualState),补齐切槽跳过 renderer 调用导致的状态缺失;pending 展开时
|
|
179
|
+
* 重新调度加载动画。text fallback 槽(无容器装载语义)返回 false,调用方
|
|
180
|
+
* 退化为重建。
|
|
181
|
+
*/
|
|
182
|
+
function mountSlot(
|
|
183
|
+
component: any,
|
|
184
|
+
slotKey: "expanded" | "collapsed",
|
|
185
|
+
slot: RenderSlot,
|
|
186
|
+
): boolean {
|
|
187
|
+
if (!slot.shell || slot.shell === "text") return false;
|
|
188
|
+
const container = shellContainer(component, slot.shell);
|
|
189
|
+
if (!container || typeof container.clear !== "function") return false;
|
|
190
|
+
syncShell(component, slot.shell);
|
|
191
|
+
container.clear();
|
|
192
|
+
if (slot.callComponent !== undefined)
|
|
193
|
+
container.addChild(slot.callComponent as any);
|
|
194
|
+
if (slot.resultComponent !== undefined)
|
|
195
|
+
container.addChild(slot.resultComponent as any);
|
|
196
|
+
component.callRendererComponent = slot.callComponent;
|
|
197
|
+
component.resultRendererComponent = slot.resultComponent;
|
|
198
|
+
component.hideComponent = slot.hideComponent;
|
|
199
|
+
restoreStateSnapshot(component, slotKey, slot.stateSnapshot);
|
|
200
|
+
if (slot.resultComponent !== undefined) {
|
|
201
|
+
// pending(流式)且已展开:恢复加载动画调度(默认 renderResult 的副作用)。
|
|
202
|
+
if (component.isPartial === true || component.executionStarted === true) {
|
|
203
|
+
try {
|
|
204
|
+
scheduleAnimation(component);
|
|
205
|
+
} catch {
|
|
206
|
+
// 动画调度失败不影响装载结果。
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function installToggleRenderCache(): ToggleRenderCacheHooks {
|
|
214
|
+
const previous = patchRegistry.get<ToggleRenderCachePatch>(
|
|
215
|
+
TOGGLE_RENDER_CACHE_PATCH,
|
|
216
|
+
);
|
|
217
|
+
// /reload 残留的旧补丁先停用,链上以本次安装为准。
|
|
218
|
+
if (previous) previous.active = false;
|
|
219
|
+
const prototype = ToolExecutionComponent.prototype as any;
|
|
220
|
+
const original = prototype.updateDisplay;
|
|
221
|
+
const patch: ToggleRenderCachePatch = {
|
|
222
|
+
active: true,
|
|
223
|
+
prototype,
|
|
224
|
+
installed: undefined as any,
|
|
225
|
+
original,
|
|
226
|
+
theme: undefined,
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
patch.installed = function (this: any) {
|
|
230
|
+
if (!patch.active || config.mode === "off") {
|
|
231
|
+
return patch.original.call(this);
|
|
232
|
+
}
|
|
233
|
+
const self = this;
|
|
234
|
+
if (
|
|
235
|
+
!self ||
|
|
236
|
+
typeof self.expanded !== "boolean" ||
|
|
237
|
+
typeof self.hideComponent !== "boolean"
|
|
238
|
+
) {
|
|
239
|
+
return patch.original.call(this);
|
|
240
|
+
}
|
|
241
|
+
const slotKey: "expanded" | "collapsed" = self.expanded
|
|
242
|
+
? "expanded"
|
|
243
|
+
: "collapsed";
|
|
244
|
+
const fingerprint = fingerprintOf(self, patch);
|
|
245
|
+
const componentsNow = componentsOf(self);
|
|
246
|
+
let entry = componentCaches.get(self);
|
|
247
|
+
const slot = entry?.[slotKey];
|
|
248
|
+
|
|
249
|
+
const rebuild = () => {
|
|
250
|
+
entry ??= {};
|
|
251
|
+
componentCaches.set(self, entry);
|
|
252
|
+
// 防止 renderer 复用另一槽的组件实例:pi 的 renderResult 通过
|
|
253
|
+
// context.lastComponent(= resultRendererComponent 字段)复用组件,
|
|
254
|
+
// 跨槽复用会把另一槽缓存的内容覆盖污染(如 bash 的 rebuildln 重建
|
|
255
|
+
// children)。重置字段强制新建,保证两槽各自持有独立实例。
|
|
256
|
+
const otherKey: "expanded" | "collapsed" =
|
|
257
|
+
slotKey === "expanded" ? "collapsed" : "expanded";
|
|
258
|
+
const other = entry[otherKey];
|
|
259
|
+
if (other && other.shell !== "text") {
|
|
260
|
+
if (self.callRendererComponent === other.callComponent) {
|
|
261
|
+
self.callRendererComponent = undefined;
|
|
262
|
+
}
|
|
263
|
+
if (self.resultRendererComponent === other.resultComponent) {
|
|
264
|
+
self.resultRendererComponent = undefined;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
patch.original.call(this);
|
|
268
|
+
// 输入指纹取重建后的状态快照;组件引用由重建产物决定。
|
|
269
|
+
const shell = mountedShell(self);
|
|
270
|
+
const built: RenderSlot = {
|
|
271
|
+
inputFingerprint: fingerprintOf(self, patch),
|
|
272
|
+
components: componentsOf(self),
|
|
273
|
+
hideComponent: self.hideComponent === true,
|
|
274
|
+
shell,
|
|
275
|
+
callComponent:
|
|
276
|
+
shell === "text"
|
|
277
|
+
? undefined
|
|
278
|
+
: (self.callRendererComponent ?? undefined),
|
|
279
|
+
resultComponent:
|
|
280
|
+
shell === "text"
|
|
281
|
+
? undefined
|
|
282
|
+
: (self.resultRendererComponent ?? undefined),
|
|
283
|
+
stateSnapshot: stateSnapshotOf(self),
|
|
284
|
+
};
|
|
285
|
+
entry[slotKey] = built;
|
|
286
|
+
entry.current = slotKey;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const inputsMatch =
|
|
290
|
+
slot && sameFingerprint(slot.inputFingerprint, fingerprint);
|
|
291
|
+
if (inputsMatch && entry) {
|
|
292
|
+
if (entry.current === slotKey) {
|
|
293
|
+
if (sameComponents(slot.components, componentsNow)) {
|
|
294
|
+
// 容器内容仍是该槽,无重建必要;仅恢复可能被外部改写的派生字段。
|
|
295
|
+
self.hideComponent = slot.hideComponent;
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// 外部(第三方 renderer、测试)替换了派生组件:走重建刷新。
|
|
299
|
+
rebuild();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (!mountSlot(self, slotKey, slot)) {
|
|
303
|
+
// text fallback 或防御性失败:退化为原生重建。
|
|
304
|
+
rebuild();
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
entry.current = slotKey;
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
rebuild();
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
prototype.updateDisplay = patch.installed;
|
|
314
|
+
patchRegistry.install(TOGGLE_RENDER_CACHE_PATCH, patch);
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
setTheme(theme: unknown) {
|
|
318
|
+
patch.theme = theme;
|
|
319
|
+
},
|
|
320
|
+
clear() {
|
|
321
|
+
componentCaches = new WeakMap();
|
|
322
|
+
},
|
|
323
|
+
shutdown() {
|
|
324
|
+
if (!patch.active) return;
|
|
325
|
+
patch.active = false;
|
|
326
|
+
if (prototype.updateDisplay === patch.installed) {
|
|
327
|
+
prototype.updateDisplay = patch.original;
|
|
328
|
+
}
|
|
329
|
+
patchRegistry.dispose(TOGGLE_RENDER_CACHE_PATCH, patch);
|
|
330
|
+
componentCaches = new WeakMap();
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
@@ -9,11 +9,6 @@ import {
|
|
|
9
9
|
import type { RenderScheduler } from "../../app/runtime/render-scheduler.ts";
|
|
10
10
|
import type { SessionLifecycle } from "../../app/runtime/session-lifecycle.ts";
|
|
11
11
|
import type { FooterState } from "../footer/index.ts";
|
|
12
|
-
import {
|
|
13
|
-
type AccentRailLayoutPatchDiagnostic,
|
|
14
|
-
installHostAccentRailLayoutPatch,
|
|
15
|
-
retainAccentRailLayoutPatchInstallation,
|
|
16
|
-
} from "./accent-rail-layout-patch.ts";
|
|
17
12
|
import {
|
|
18
13
|
type EditorTransferFailureReason,
|
|
19
14
|
replaceEditorComponentWithExpandedText,
|
|
@@ -41,8 +36,6 @@ export type EditorLayoutControllerContext = {
|
|
|
41
36
|
readonly sessionLifecycle: SessionLifecycle;
|
|
42
37
|
readonly render: Pick<RenderScheduler, "request">;
|
|
43
38
|
readonly getThinkingLevel: () => ReturnType<ExtensionAPI["getThinkingLevel"]>;
|
|
44
|
-
readonly getContextWindow: (ctx: ExtensionContext) => number | undefined;
|
|
45
|
-
readonly getContextPercent: (ctx: ExtensionContext) => number | undefined;
|
|
46
39
|
readonly getAgentDurationMs: () => number;
|
|
47
40
|
readonly isAgentActive: () => boolean;
|
|
48
41
|
readonly isAgentDurationActive: () => boolean;
|
|
@@ -50,10 +43,6 @@ export type EditorLayoutControllerContext = {
|
|
|
50
43
|
readonly getProjectRoot: () => string | undefined;
|
|
51
44
|
readonly onProjectRequirementChanged: () => void;
|
|
52
45
|
readonly onModelLabelChanged: (ctx: ExtensionContext) => void;
|
|
53
|
-
readonly recordLayoutDiagnostic: (
|
|
54
|
-
diagnostic: AccentRailLayoutPatchDiagnostic,
|
|
55
|
-
version?: string,
|
|
56
|
-
) => void;
|
|
57
46
|
};
|
|
58
47
|
|
|
59
48
|
type EditorInstallMode = "none" | "standalone" | "wrapper";
|
|
@@ -73,8 +62,6 @@ export class EditorLayoutController {
|
|
|
73
62
|
private minimalistDecorationActive = false;
|
|
74
63
|
private minimalistDurationUpdatesActive = false;
|
|
75
64
|
private stopMinimalistDurationUpdates: () => void = () => {};
|
|
76
|
-
private accentRailLayoutPatchCleanup: () => void = () => {};
|
|
77
|
-
private accentRailLayoutPatchInstallSerial = 0;
|
|
78
65
|
|
|
79
66
|
/**
|
|
80
67
|
* Creates an Editor controller with shared runtime selectors.
|
|
@@ -85,37 +72,6 @@ export class EditorLayoutController {
|
|
|
85
72
|
this.context = context;
|
|
86
73
|
}
|
|
87
74
|
|
|
88
|
-
/**
|
|
89
|
-
* Installs the host Accent Rail patch for a new session generation.
|
|
90
|
-
*
|
|
91
|
-
* @param ctx Active Pi extension context.
|
|
92
|
-
* @returns A promise that settles after patch retention is attempted.
|
|
93
|
-
*/
|
|
94
|
-
async startSession(ctx: ExtensionContext): Promise<void> {
|
|
95
|
-
const lifecycleGeneration =
|
|
96
|
-
this.context.sessionLifecycle.currentGeneration();
|
|
97
|
-
const installSerial = ++this.accentRailLayoutPatchInstallSerial;
|
|
98
|
-
this.accentRailLayoutPatchCleanup();
|
|
99
|
-
this.accentRailLayoutPatchCleanup = () => {};
|
|
100
|
-
if (!this.isTuiContext(ctx)) return;
|
|
101
|
-
|
|
102
|
-
const result = await retainAccentRailLayoutPatchInstallation(
|
|
103
|
-
() => installHostAccentRailLayoutPatch(this.ownerToken),
|
|
104
|
-
() =>
|
|
105
|
-
this.context.sessionLifecycle.isCurrent(lifecycleGeneration) &&
|
|
106
|
-
installSerial === this.accentRailLayoutPatchInstallSerial,
|
|
107
|
-
(layoutPatch) => {
|
|
108
|
-
this.accentRailLayoutPatchCleanup = layoutPatch.cleanup;
|
|
109
|
-
this.context.recordLayoutDiagnostic(
|
|
110
|
-
layoutPatch.diagnostic,
|
|
111
|
-
layoutPatch.version,
|
|
112
|
-
);
|
|
113
|
-
},
|
|
114
|
-
);
|
|
115
|
-
if (result === "failed")
|
|
116
|
-
this.context.recordLayoutDiagnostic("host-module-unavailable");
|
|
117
|
-
}
|
|
118
|
-
|
|
119
75
|
/**
|
|
120
76
|
* Installs or reconciles the configured Editor for an active session.
|
|
121
77
|
*
|
|
@@ -187,10 +143,15 @@ export class EditorLayoutController {
|
|
|
187
143
|
): { applied: boolean; reason?: string } {
|
|
188
144
|
this.context.saveComponent(patch);
|
|
189
145
|
let result: EditorChangeResult | undefined;
|
|
190
|
-
if (patch.
|
|
191
|
-
|
|
192
|
-
|
|
146
|
+
if (patch.style === "off") {
|
|
147
|
+
// off 透传原生渲染:不替换工厂,既有编辑器实例与 overlay preFocus
|
|
148
|
+
// 保持有效(面板关闭后仍能聚焦输入框)。
|
|
193
149
|
this.setMinimalistDecorationActive(false);
|
|
150
|
+
} else if (patch.style === "on" && this.isTuiContext(ctx)) {
|
|
151
|
+
// on 收敛所有权:未安装或第三方接管时安装回来;
|
|
152
|
+
// 已有自身工厂时 installEditor 短路,不产生替换。
|
|
153
|
+
result = this.reconcile(ctx);
|
|
154
|
+
}
|
|
194
155
|
if (patch.modelLabel !== undefined) this.context.onModelLabelChanged(ctx);
|
|
195
156
|
this.context.onProjectRequirementChanged();
|
|
196
157
|
this.requestRender();
|
|
@@ -240,7 +201,7 @@ export class EditorLayoutController {
|
|
|
240
201
|
Boolean(
|
|
241
202
|
this.activeTuiContext && this.ownsActiveFactory(this.activeTuiContext),
|
|
242
203
|
) &&
|
|
243
|
-
editor.style === "
|
|
204
|
+
editor.style === "on" &&
|
|
244
205
|
(minimalist.showGit || minimalist.pathDisplay === "project")
|
|
245
206
|
);
|
|
246
207
|
}
|
|
@@ -355,9 +316,6 @@ export class EditorLayoutController {
|
|
|
355
316
|
}
|
|
356
317
|
}
|
|
357
318
|
this.clearEditorOwnership();
|
|
358
|
-
++this.accentRailLayoutPatchInstallSerial;
|
|
359
|
-
this.accentRailLayoutPatchCleanup();
|
|
360
|
-
this.accentRailLayoutPatchCleanup = () => {};
|
|
361
319
|
this.activeTuiContext = undefined;
|
|
362
320
|
}
|
|
363
321
|
|
|
@@ -375,7 +333,7 @@ export class EditorLayoutController {
|
|
|
375
333
|
this.minimalistDecorationActive &&
|
|
376
334
|
this.isEditorEnabled() &&
|
|
377
335
|
this.ownsActiveFactory(ctx) &&
|
|
378
|
-
config.style === "
|
|
336
|
+
config.style === "on" &&
|
|
379
337
|
config.styles.minimalist.showTimer,
|
|
380
338
|
);
|
|
381
339
|
if (!needed) {
|
|
@@ -412,14 +370,17 @@ export class EditorLayoutController {
|
|
|
412
370
|
}
|
|
413
371
|
|
|
414
372
|
/**
|
|
415
|
-
* Returns whether the configured Editor
|
|
373
|
+
* Returns whether the configured Editor factory can be installed.
|
|
416
374
|
*
|
|
417
|
-
*
|
|
375
|
+
* style "off" restores Pi's native editor: the factory is not installed and
|
|
376
|
+
* any third-party or native editor keeps ownership.
|
|
377
|
+
*
|
|
378
|
+
* @returns Whether the Editor is on and supported.
|
|
418
379
|
*/
|
|
419
380
|
private isEditorEnabled(): boolean {
|
|
420
381
|
const editor = this.context.getConfig().components.editor;
|
|
421
382
|
return (
|
|
422
|
-
editor.
|
|
383
|
+
editor.style === "on" &&
|
|
423
384
|
!hasUnsupportedComponentStyle(this.context.getConfig(), "editor")
|
|
424
385
|
);
|
|
425
386
|
}
|
|
@@ -516,8 +477,6 @@ export class EditorLayoutController {
|
|
|
516
477
|
getConfig: this.context.getConfig,
|
|
517
478
|
getState: this.context.getState,
|
|
518
479
|
getThinkingLevel: this.context.getThinkingLevel,
|
|
519
|
-
getContextWindow: this.context.getContextWindow,
|
|
520
|
-
getContextPercent: this.context.getContextPercent,
|
|
521
480
|
getAgentDurationMs: this.context.getAgentDurationMs,
|
|
522
481
|
isAgentActive: this.context.isAgentActive,
|
|
523
482
|
getProjectRoot: this.context.getProjectRoot,
|
|
@@ -526,11 +485,6 @@ export class EditorLayoutController {
|
|
|
526
485
|
},
|
|
527
486
|
onDecorationActive: (active) =>
|
|
528
487
|
this.setMinimalistDecorationActive(active),
|
|
529
|
-
isAccentRailActive: () =>
|
|
530
|
-
this.context.sessionLifecycle.isCurrent() &&
|
|
531
|
-
this.ownsActiveFactory(ctx) &&
|
|
532
|
-
this.isEditorEnabled() &&
|
|
533
|
-
this.context.getConfig().components.editor.style === "accent-rail",
|
|
534
488
|
};
|
|
535
489
|
}
|
|
536
490
|
|
|
@@ -7,7 +7,6 @@ import type {
|
|
|
7
7
|
import type { EditorTheme, TUI } from "@earendil-works/pi-tui";
|
|
8
8
|
import type { PolishedTuiConfig } from "../../app/config/shell.ts";
|
|
9
9
|
import { type FooterState, modelLabelFor } from "../footer/index.ts";
|
|
10
|
-
import { markAccentRailLayoutEditor } from "./accent-rail-layout-patch.ts";
|
|
11
10
|
import {
|
|
12
11
|
type EditorFactory,
|
|
13
12
|
markEditorFactory,
|
|
@@ -22,14 +21,11 @@ export type EditorFactoryRuntime = {
|
|
|
22
21
|
readonly getConfig: () => PolishedTuiConfig;
|
|
23
22
|
readonly getState: () => FooterState;
|
|
24
23
|
readonly getThinkingLevel: () => ReturnType<ExtensionAPI["getThinkingLevel"]>;
|
|
25
|
-
readonly getContextWindow: (ctx: ExtensionContext) => number | undefined;
|
|
26
|
-
readonly getContextPercent: (ctx: ExtensionContext) => number | undefined;
|
|
27
24
|
readonly getAgentDurationMs: () => number;
|
|
28
25
|
readonly isAgentActive: () => boolean;
|
|
29
26
|
readonly getProjectRoot: () => string | undefined;
|
|
30
27
|
readonly onRender: (requestRender: () => void) => void;
|
|
31
28
|
readonly onDecorationActive: (active: boolean) => void;
|
|
32
|
-
readonly isAccentRailActive: () => boolean;
|
|
33
29
|
};
|
|
34
30
|
|
|
35
31
|
/**
|
|
@@ -49,7 +45,7 @@ export function createEditorFactory(
|
|
|
49
45
|
keybindings: KeybindingsManager,
|
|
50
46
|
) => {
|
|
51
47
|
runtime.onRender(() => tui.requestRender());
|
|
52
|
-
|
|
48
|
+
return new PolishedEditor(
|
|
53
49
|
tui,
|
|
54
50
|
theme,
|
|
55
51
|
keybindings,
|
|
@@ -60,12 +56,6 @@ export function createEditorFactory(
|
|
|
60
56
|
() => editorDecoration(ctx, runtime),
|
|
61
57
|
runtime.onDecorationActive,
|
|
62
58
|
);
|
|
63
|
-
markAccentRailLayoutEditor(
|
|
64
|
-
editor,
|
|
65
|
-
runtime.ownerToken,
|
|
66
|
-
runtime.isAccentRailActive,
|
|
67
|
-
);
|
|
68
|
-
return editor;
|
|
69
59
|
}) as ZentuiEditorFactory;
|
|
70
60
|
return markEditorFactory(factory, runtime.ownerToken);
|
|
71
61
|
}
|
|
@@ -89,7 +79,7 @@ export function createWrappedEditorFactory(
|
|
|
89
79
|
keybindings: KeybindingsManager,
|
|
90
80
|
) => {
|
|
91
81
|
runtime.onRender(() => tui.requestRender());
|
|
92
|
-
|
|
82
|
+
return new WrappedPolishedEditor(
|
|
93
83
|
baseFactory(tui, theme, keybindings),
|
|
94
84
|
runtime.sessionTheme,
|
|
95
85
|
runtime.getConfig,
|
|
@@ -98,12 +88,6 @@ export function createWrappedEditorFactory(
|
|
|
98
88
|
() => editorDecoration(ctx, runtime),
|
|
99
89
|
runtime.onDecorationActive,
|
|
100
90
|
);
|
|
101
|
-
markAccentRailLayoutEditor(
|
|
102
|
-
editor,
|
|
103
|
-
runtime.ownerToken,
|
|
104
|
-
runtime.isAccentRailActive,
|
|
105
|
-
);
|
|
106
|
-
return editor;
|
|
107
91
|
}) as ZentuiEditorFactory;
|
|
108
92
|
return markWrappedEditorFactory(factory, baseFactory, runtime.ownerToken);
|
|
109
93
|
}
|
|
@@ -150,8 +134,6 @@ function editorDecoration(
|
|
|
150
134
|
costLabel: state.costLabel,
|
|
151
135
|
modelLabel: modelLabelFor(state, config.components.editor.modelLabel),
|
|
152
136
|
thinkingLevel: runtime.getThinkingLevel(),
|
|
153
|
-
contextPercent: runtime.getContextPercent(ctx),
|
|
154
|
-
contextWindow: runtime.getContextWindow(ctx),
|
|
155
137
|
sessionName: ctx.sessionManager.getSessionName() ?? "",
|
|
156
138
|
agentDurationMs: runtime.getAgentDurationMs(),
|
|
157
139
|
agentActive: runtime.isAgentActive(),
|