pi-zentui 0.15.0 → 0.15.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/extensions/zentui/editor-transfer.ts +73 -0
- package/extensions/zentui/format.ts +106 -35
- package/extensions/zentui/index.ts +169 -66
- package/extensions/zentui/prototype-patch-registry.ts +8 -1
- package/extensions/zentui/selector-border.ts +11 -5
- package/extensions/zentui/ui.ts +166 -88
- package/extensions/zentui/user-message.ts +24 -18
- package/package.json +4 -4
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type EditorComponentFactory = (...args: never[]) => unknown;
|
|
2
|
+
|
|
3
|
+
export type EditorTransferUi<Factory = EditorComponentFactory> = {
|
|
4
|
+
getEditorText?: () => unknown;
|
|
5
|
+
setEditorText?: (text: string) => void;
|
|
6
|
+
setEditorComponent?: (factory: Factory | undefined) => void;
|
|
7
|
+
getEditorComponent?: () => Factory | undefined;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type EditorTransferFailureReason =
|
|
11
|
+
| "unsupported-transfer-api"
|
|
12
|
+
| "editor-factory-snapshot-failed"
|
|
13
|
+
| "editor-text-snapshot-failed"
|
|
14
|
+
| "editor-text-preparation-failed"
|
|
15
|
+
| "editor-replacement-failed-with-rollback"
|
|
16
|
+
| "editor-replacement-rollback-failed";
|
|
17
|
+
|
|
18
|
+
export type EditorTransferResult =
|
|
19
|
+
| { ok: true }
|
|
20
|
+
| { ok: false; reason: EditorTransferFailureReason };
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Prepare the active editor's public expanded text before Pi replaces its factory.
|
|
24
|
+
* This intentionally transfers prompt contents only; editor-private state is never inspected.
|
|
25
|
+
*/
|
|
26
|
+
export function replaceEditorComponentWithExpandedText<Factory>(
|
|
27
|
+
ui: EditorTransferUi<Factory>,
|
|
28
|
+
factory: Factory | undefined,
|
|
29
|
+
): EditorTransferResult {
|
|
30
|
+
if (
|
|
31
|
+
typeof ui.getEditorText !== "function" ||
|
|
32
|
+
typeof ui.setEditorText !== "function" ||
|
|
33
|
+
typeof ui.setEditorComponent !== "function" ||
|
|
34
|
+
typeof ui.getEditorComponent !== "function"
|
|
35
|
+
) {
|
|
36
|
+
return { ok: false, reason: "unsupported-transfer-api" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let previousFactory: Factory | undefined;
|
|
40
|
+
try {
|
|
41
|
+
previousFactory = ui.getEditorComponent();
|
|
42
|
+
} catch {
|
|
43
|
+
return { ok: false, reason: "editor-factory-snapshot-failed" };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let expandedText: unknown;
|
|
47
|
+
try {
|
|
48
|
+
expandedText = ui.getEditorText();
|
|
49
|
+
} catch {
|
|
50
|
+
return { ok: false, reason: "editor-text-snapshot-failed" };
|
|
51
|
+
}
|
|
52
|
+
if (typeof expandedText !== "string") {
|
|
53
|
+
return { ok: false, reason: "editor-text-snapshot-failed" };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
ui.setEditorText(expandedText);
|
|
58
|
+
} catch {
|
|
59
|
+
return { ok: false, reason: "editor-text-preparation-failed" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
ui.setEditorComponent(factory);
|
|
64
|
+
} catch {
|
|
65
|
+
try {
|
|
66
|
+
ui.setEditorComponent(previousFactory);
|
|
67
|
+
return { ok: false, reason: "editor-replacement-failed-with-rollback" };
|
|
68
|
+
} catch {
|
|
69
|
+
return { ok: false, reason: "editor-replacement-rollback-failed" };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { ok: true };
|
|
73
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { homedir, hostname, userInfo } from "node:os";
|
|
2
|
-
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
3
2
|
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
4
3
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
4
|
import type {
|
|
@@ -81,23 +80,40 @@ export type UsageTotals = {
|
|
|
81
80
|
|
|
82
81
|
export type ContextColorTier = "normal" | "warning" | "error";
|
|
83
82
|
|
|
83
|
+
type SessionUsage = {
|
|
84
|
+
input?: unknown;
|
|
85
|
+
output?: unknown;
|
|
86
|
+
cacheRead?: unknown;
|
|
87
|
+
cacheWrite?: unknown;
|
|
88
|
+
cost?: unknown;
|
|
89
|
+
};
|
|
90
|
+
|
|
84
91
|
type SessionEntry = {
|
|
85
92
|
type?: string;
|
|
86
93
|
id?: string | number;
|
|
87
94
|
timestamp?: string | number;
|
|
95
|
+
usage?: SessionUsage;
|
|
88
96
|
message?: {
|
|
89
97
|
role?: string;
|
|
90
|
-
usage?:
|
|
98
|
+
usage?: SessionUsage;
|
|
91
99
|
};
|
|
92
100
|
};
|
|
93
101
|
|
|
102
|
+
type SelectedUsage = {
|
|
103
|
+
usage: SessionUsage | undefined;
|
|
104
|
+
location: "message" | "entry";
|
|
105
|
+
isAssistant: boolean;
|
|
106
|
+
};
|
|
107
|
+
|
|
94
108
|
type UsageCacheEntry = {
|
|
95
109
|
key: string;
|
|
96
110
|
totals: UsageTotals;
|
|
97
111
|
};
|
|
98
112
|
|
|
113
|
+
const MAX_USAGE_TOTAL = Number.MAX_VALUE;
|
|
114
|
+
|
|
99
115
|
let usageTotalsCache: UsageCacheEntry | undefined;
|
|
100
|
-
let
|
|
116
|
+
let usageTotalsAggregationPassCount = 0;
|
|
101
117
|
|
|
102
118
|
export function formatCount(value: number): string {
|
|
103
119
|
if (value < 1000) return value.toString();
|
|
@@ -130,26 +146,78 @@ function calculateCacheHitRate(
|
|
|
130
146
|
cacheWrite: number,
|
|
131
147
|
): number | undefined {
|
|
132
148
|
const promptTokens = input + cacheRead + cacheWrite;
|
|
133
|
-
|
|
149
|
+
if (promptTokens === 0) return undefined;
|
|
150
|
+
if (Number.isFinite(promptTokens)) return (cacheRead / promptTokens) * 100;
|
|
151
|
+
|
|
152
|
+
const scale = Math.max(input, cacheRead, cacheWrite);
|
|
153
|
+
const scaledPromptTokens = input / scale + cacheRead / scale + cacheWrite / scale;
|
|
154
|
+
return (cacheRead / scale / scaledPromptTokens) * 100;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function normalizeUsageNumber(value: unknown): number {
|
|
158
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function usageCostTotal(usage: SessionUsage | undefined): number {
|
|
162
|
+
if (typeof usage?.cost !== "object" || usage.cost === null) return 0;
|
|
163
|
+
return normalizeUsageNumber((usage.cost as { total?: unknown }).total);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function addUsageTotal(total: number, value: number): number {
|
|
167
|
+
const sum = total + value;
|
|
168
|
+
return Number.isFinite(sum) ? sum : MAX_USAGE_TOTAL;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function usageForEntry(entry: SessionEntry): SelectedUsage | undefined {
|
|
172
|
+
if (entry.type === "message") {
|
|
173
|
+
const role = entry.message?.role;
|
|
174
|
+
if (role !== "assistant" && role !== "toolResult") return undefined;
|
|
175
|
+
return {
|
|
176
|
+
usage: entry.message?.usage,
|
|
177
|
+
location: "message",
|
|
178
|
+
isAssistant: role === "assistant",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
182
|
+
return { usage: entry.usage, location: "entry", isAssistant: false };
|
|
183
|
+
}
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function normalizedUsage(usage: SessionUsage | undefined) {
|
|
188
|
+
return {
|
|
189
|
+
input: normalizeUsageNumber(usage?.input),
|
|
190
|
+
output: normalizeUsageNumber(usage?.output),
|
|
191
|
+
cacheRead: normalizeUsageNumber(usage?.cacheRead),
|
|
192
|
+
cacheWrite: normalizeUsageNumber(usage?.cacheWrite),
|
|
193
|
+
cost: usageCostTotal(usage),
|
|
194
|
+
};
|
|
134
195
|
}
|
|
135
196
|
|
|
136
|
-
function entryIdentity(entry: SessionEntry
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
197
|
+
function entryIdentity(entry: SessionEntry): string {
|
|
198
|
+
const selected = usageForEntry(entry);
|
|
199
|
+
if (!selected) return "unsupported";
|
|
200
|
+
const usage = normalizedUsage(selected.usage);
|
|
201
|
+
return JSON.stringify([
|
|
202
|
+
entry.id ?? null,
|
|
203
|
+
entry.timestamp ?? null,
|
|
204
|
+
entry.type ?? null,
|
|
205
|
+
entry.message?.role ?? null,
|
|
206
|
+
selected.location,
|
|
207
|
+
usage.input,
|
|
208
|
+
usage.output,
|
|
209
|
+
usage.cacheRead,
|
|
210
|
+
usage.cacheWrite,
|
|
211
|
+
usage.cost,
|
|
212
|
+
]);
|
|
143
213
|
}
|
|
144
214
|
|
|
145
215
|
function buildUsageFingerprint(entries: readonly SessionEntry[]): string {
|
|
146
|
-
|
|
147
|
-
const last = entries[entries.length - 1];
|
|
148
|
-
return `${entries.length}\0${entryIdentity(first)}\0${entryIdentity(last)}`;
|
|
216
|
+
return entries.map(entryIdentity).join("\0");
|
|
149
217
|
}
|
|
150
218
|
|
|
151
219
|
function computeUsageTotals(entries: readonly SessionEntry[]): UsageTotals {
|
|
152
|
-
|
|
220
|
+
usageTotalsAggregationPassCount += 1;
|
|
153
221
|
let input = 0;
|
|
154
222
|
let output = 0;
|
|
155
223
|
let cacheRead = 0;
|
|
@@ -158,18 +226,18 @@ function computeUsageTotals(entries: readonly SessionEntry[]): UsageTotals {
|
|
|
158
226
|
let cost = 0;
|
|
159
227
|
|
|
160
228
|
for (const entry of entries) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
229
|
+
const selected = usageForEntry(entry);
|
|
230
|
+
if (!selected) continue;
|
|
231
|
+
const usage = normalizedUsage(selected.usage);
|
|
232
|
+
|
|
233
|
+
input = addUsageTotal(input, usage.input);
|
|
234
|
+
output = addUsageTotal(output, usage.output);
|
|
235
|
+
cacheRead = addUsageTotal(cacheRead, usage.cacheRead);
|
|
236
|
+
cacheWrite = addUsageTotal(cacheWrite, usage.cacheWrite);
|
|
237
|
+
cost = addUsageTotal(cost, usage.cost);
|
|
238
|
+
if (selected.isAssistant) {
|
|
239
|
+
latestCacheHitRate = calculateCacheHitRate(usage.input, usage.cacheRead, usage.cacheWrite);
|
|
240
|
+
}
|
|
173
241
|
}
|
|
174
242
|
|
|
175
243
|
return Object.freeze({ input, output, cacheRead, cacheWrite, latestCacheHitRate, cost });
|
|
@@ -179,23 +247,26 @@ export function invalidateUsageTotalsCache(): void {
|
|
|
179
247
|
usageTotalsCache = undefined;
|
|
180
248
|
}
|
|
181
249
|
|
|
182
|
-
/** Test helper:
|
|
183
|
-
export function
|
|
184
|
-
return
|
|
250
|
+
/** Test helper: counts aggregation passes only; every cache lookup still fingerprints all entries. */
|
|
251
|
+
export function __usageTotalsAggregationPassCount(): number {
|
|
252
|
+
return usageTotalsAggregationPassCount;
|
|
185
253
|
}
|
|
186
254
|
|
|
187
|
-
/** Test helper: reset
|
|
255
|
+
/** Test helper: reset the aggregation-pass counter and cached totals. */
|
|
188
256
|
export function __resetUsageTotalsCacheForTests(): void {
|
|
189
257
|
usageTotalsCache = undefined;
|
|
190
|
-
|
|
258
|
+
usageTotalsAggregationPassCount = 0;
|
|
191
259
|
}
|
|
192
260
|
|
|
193
261
|
export function getUsageTotals(ctx: ExtensionContext): UsageTotals {
|
|
194
262
|
const sessionManager = ctx.sessionManager as {
|
|
195
|
-
getEntries?: () => SessionEntry[];
|
|
196
|
-
getBranch: () => SessionEntry[];
|
|
263
|
+
getEntries?: () => readonly SessionEntry[];
|
|
264
|
+
getBranch: () => readonly SessionEntry[];
|
|
197
265
|
};
|
|
198
|
-
const entries =
|
|
266
|
+
const entries =
|
|
267
|
+
typeof sessionManager.getEntries === "function"
|
|
268
|
+
? sessionManager.getEntries()
|
|
269
|
+
: sessionManager.getBranch();
|
|
199
270
|
const key = buildUsageFingerprint(entries);
|
|
200
271
|
if (usageTotalsCache?.key === key) return usageTotalsCache.totals;
|
|
201
272
|
|
|
@@ -42,6 +42,10 @@ import {
|
|
|
42
42
|
saveUiFeaturesPatch,
|
|
43
43
|
type UiFeaturesConfig,
|
|
44
44
|
} from "./config";
|
|
45
|
+
import {
|
|
46
|
+
type EditorTransferFailureReason,
|
|
47
|
+
replaceEditorComponentWithExpandedText,
|
|
48
|
+
} from "./editor-transfer";
|
|
45
49
|
import {
|
|
46
50
|
disposeFixedEditor,
|
|
47
51
|
installFixedEditorProbe,
|
|
@@ -80,10 +84,30 @@ type ZentuiEditorFactory = EditorFactory & {
|
|
|
80
84
|
|
|
81
85
|
type ApplyUiResult = {
|
|
82
86
|
editorBlocked: boolean;
|
|
87
|
+
editorReason?: string;
|
|
83
88
|
};
|
|
84
89
|
|
|
90
|
+
type EditorChangeResult = { ok: true } | { ok: false; reason: string };
|
|
91
|
+
|
|
85
92
|
type EditorInstallMode = "none" | "standalone" | "wrapper";
|
|
86
93
|
|
|
94
|
+
function editorTransferFailureMessage(reason: EditorTransferFailureReason): string {
|
|
95
|
+
switch (reason) {
|
|
96
|
+
case "unsupported-transfer-api":
|
|
97
|
+
return "this Pi version cannot safely transfer expanded editor text; reload Pi to apply this change";
|
|
98
|
+
case "editor-factory-snapshot-failed":
|
|
99
|
+
return "the current editor factory could not be read safely; reload Pi to apply this change";
|
|
100
|
+
case "editor-text-snapshot-failed":
|
|
101
|
+
return "expanded editor text could not be read safely; reload Pi to apply this change";
|
|
102
|
+
case "editor-text-preparation-failed":
|
|
103
|
+
return "expanded editor text could not be prepared safely; reload Pi to apply this change";
|
|
104
|
+
case "editor-replacement-failed-with-rollback":
|
|
105
|
+
return "the editor replacement failed; the previous factory was reapplied, but editor instance identity is not guaranteed";
|
|
106
|
+
case "editor-replacement-rollback-failed":
|
|
107
|
+
return "the editor replacement and previous-factory rollback both failed; reload Pi before editing";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
87
111
|
function isZentuiEditorFactory(factory: EditorFactory | undefined): boolean {
|
|
88
112
|
return Boolean((factory as ZentuiEditorFactory | undefined)?.[ZENTUI_EDITOR_FACTORY]);
|
|
89
113
|
}
|
|
@@ -266,15 +290,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
266
290
|
if (footerInstalled) scheduleProjectRefresh(ctx, { force: true });
|
|
267
291
|
};
|
|
268
292
|
|
|
269
|
-
const installPrototypePatches = () => {
|
|
270
|
-
if (prototypePatchesInstalled) return;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
293
|
+
const installPrototypePatches = (): boolean => {
|
|
294
|
+
if (prototypePatchesInstalled) return true;
|
|
295
|
+
let cleanupSelectorBorderStyle: (() => void) | undefined;
|
|
296
|
+
try {
|
|
297
|
+
cleanupSelectorBorderStyle = installSelectorBorderStyle(getActiveTheme, getCurrentConfig);
|
|
298
|
+
const cleanupUserMessageStyle = installUserMessageStyle(getActiveTheme, getCurrentConfig);
|
|
299
|
+
cleanupPrototypePatches = () => {
|
|
300
|
+
cleanupSelectorBorderStyle?.();
|
|
301
|
+
cleanupUserMessageStyle();
|
|
302
|
+
};
|
|
303
|
+
prototypePatchesInstalled = true;
|
|
304
|
+
return true;
|
|
305
|
+
} catch {
|
|
306
|
+
try {
|
|
307
|
+
cleanupSelectorBorderStyle?.();
|
|
308
|
+
} catch {
|
|
309
|
+
// Best effort: each installer is locally transactional as well.
|
|
310
|
+
}
|
|
311
|
+
cleanupPrototypePatches = () => {};
|
|
312
|
+
prototypePatchesInstalled = false;
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
278
315
|
};
|
|
279
316
|
|
|
280
317
|
const uninstallPrototypePatches = () => {
|
|
@@ -283,6 +320,43 @@ export default function (pi: ExtensionAPI) {
|
|
|
283
320
|
prototypePatchesInstalled = false;
|
|
284
321
|
};
|
|
285
322
|
|
|
323
|
+
const clearEditorOwnership = () => {
|
|
324
|
+
wrappedEditorFactory = undefined;
|
|
325
|
+
installedEditorFactory = undefined;
|
|
326
|
+
editorInstallMode = "none";
|
|
327
|
+
editorInstalled = false;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const trackZentuiEditorFactory = (factory: EditorFactory) => {
|
|
331
|
+
const baseFactory = getZentuiEditorBaseFactory(factory);
|
|
332
|
+
wrappedEditorFactory = baseFactory;
|
|
333
|
+
installedEditorFactory = factory;
|
|
334
|
+
editorInstallMode = baseFactory ? "wrapper" : "standalone";
|
|
335
|
+
editorInstalled = true;
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const observeEditorFactory = (
|
|
339
|
+
ctx: ExtensionContext,
|
|
340
|
+
): { known: true; factory: EditorFactory | undefined } | { known: false } => {
|
|
341
|
+
try {
|
|
342
|
+
return { known: true, factory: ctx.ui.getEditorComponent() };
|
|
343
|
+
} catch {
|
|
344
|
+
return { known: false };
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const reconcileObservedEditorOwnership = (ctx: ExtensionContext) => {
|
|
349
|
+
const observed = observeEditorFactory(ctx);
|
|
350
|
+
if (!observed.known) return observed;
|
|
351
|
+
if (observed.factory && isZentuiEditorFactory(observed.factory)) {
|
|
352
|
+
trackZentuiEditorFactory(observed.factory);
|
|
353
|
+
} else {
|
|
354
|
+
uninstallPrototypePatches();
|
|
355
|
+
clearEditorOwnership();
|
|
356
|
+
}
|
|
357
|
+
return observed;
|
|
358
|
+
};
|
|
359
|
+
|
|
286
360
|
const makeEditorFactory = (ctx: ExtensionContext): ZentuiEditorFactory => {
|
|
287
361
|
const sessionTheme = ctx.ui.theme;
|
|
288
362
|
const factory = ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) =>
|
|
@@ -329,53 +403,73 @@ export default function (pi: ExtensionAPI) {
|
|
|
329
403
|
return factory;
|
|
330
404
|
};
|
|
331
405
|
|
|
332
|
-
const
|
|
406
|
+
const replaceEditor = (
|
|
407
|
+
ctx: ExtensionContext,
|
|
408
|
+
factory: EditorFactory | undefined,
|
|
409
|
+
): EditorChangeResult => {
|
|
410
|
+
const result = replaceEditorComponentWithExpandedText(ctx.ui, factory);
|
|
411
|
+
return result.ok ? result : { ok: false, reason: editorTransferFailureMessage(result.reason) };
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const installEditor = (ctx: ExtensionContext): EditorChangeResult => {
|
|
333
415
|
const currentFactory = ctx.ui.getEditorComponent();
|
|
334
416
|
if (currentFactory && currentFactory === installedEditorFactory) {
|
|
335
417
|
editorInstalled = true;
|
|
336
|
-
return true;
|
|
418
|
+
return { ok: true };
|
|
337
419
|
}
|
|
338
420
|
|
|
339
|
-
installPrototypePatches();
|
|
340
421
|
const currentZentuiBaseFactory = getZentuiEditorBaseFactory(currentFactory);
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
ctx
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
ctx.ui.setEditorComponent(nextFactory);
|
|
359
|
-
installedEditorFactory = nextFactory;
|
|
360
|
-
editorInstallMode = "standalone";
|
|
422
|
+
const baseFactory =
|
|
423
|
+
currentZentuiBaseFactory ??
|
|
424
|
+
(currentFactory && !isZentuiEditorFactory(currentFactory) ? currentFactory : undefined);
|
|
425
|
+
const nextFactory = baseFactory
|
|
426
|
+
? makeWrappedEditorFactory(ctx, baseFactory)
|
|
427
|
+
: makeEditorFactory(ctx);
|
|
428
|
+
const replacement = replaceEditor(ctx, nextFactory);
|
|
429
|
+
if (!replacement.ok) return replacement;
|
|
430
|
+
if (!installPrototypePatches()) {
|
|
431
|
+
const rollback = replaceEditor(ctx, currentFactory);
|
|
432
|
+
reconcileObservedEditorOwnership(ctx);
|
|
433
|
+
return {
|
|
434
|
+
ok: false,
|
|
435
|
+
reason: rollback.ok
|
|
436
|
+
? "editor chrome patch installation failed; the previous factory was reapplied, but editor instance identity is not guaranteed"
|
|
437
|
+
: "editor chrome patch installation failed and the previous factory could not be restored; reload Pi before editing",
|
|
438
|
+
};
|
|
361
439
|
}
|
|
362
|
-
|
|
363
|
-
|
|
440
|
+
|
|
441
|
+
trackZentuiEditorFactory(nextFactory);
|
|
442
|
+
return { ok: true };
|
|
364
443
|
};
|
|
365
444
|
|
|
366
|
-
const uninstallEditor = (ctx: ExtensionContext):
|
|
367
|
-
const
|
|
368
|
-
if (
|
|
445
|
+
const uninstallEditor = (ctx: ExtensionContext): EditorChangeResult => {
|
|
446
|
+
const observed = observeEditorFactory(ctx);
|
|
447
|
+
if (!observed.known) {
|
|
448
|
+
return {
|
|
449
|
+
ok: false,
|
|
450
|
+
reason:
|
|
451
|
+
"the current editor factory could not be observed safely; reload Pi to apply this change",
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
const currentFactory = observed.factory;
|
|
455
|
+
if (!currentFactory || !isZentuiEditorFactory(currentFactory)) {
|
|
456
|
+
uninstallPrototypePatches();
|
|
457
|
+
clearEditorOwnership();
|
|
458
|
+
return { ok: true };
|
|
459
|
+
}
|
|
369
460
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
461
|
+
const replacement = replaceEditor(
|
|
462
|
+
ctx,
|
|
463
|
+
getZentuiEditorBaseFactory(currentFactory) ??
|
|
464
|
+
(editorInstallMode === "wrapper" && wrappedEditorFactory
|
|
465
|
+
? wrappedEditorFactory
|
|
466
|
+
: undefined),
|
|
373
467
|
);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
return true;
|
|
468
|
+
if (!replacement.ok) return replacement;
|
|
469
|
+
|
|
470
|
+
uninstallPrototypePatches();
|
|
471
|
+
clearEditorOwnership();
|
|
472
|
+
return { ok: true };
|
|
379
473
|
};
|
|
380
474
|
|
|
381
475
|
const installStatusLine = (ctx: ExtensionContext) => {
|
|
@@ -413,12 +507,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
413
507
|
const result: ApplyUiResult = { editorBlocked: false };
|
|
414
508
|
if (!isTuiContext(ctx)) return result;
|
|
415
509
|
activeTheme = ctx.ui.theme;
|
|
510
|
+
let editorChange: EditorChangeResult | undefined;
|
|
416
511
|
if (currentConfig.features.editor) {
|
|
417
512
|
const currentFactory = ctx.ui.getEditorComponent();
|
|
418
513
|
const editorMissingOrReplaced = !editorInstalled || !isZentuiEditorFactory(currentFactory);
|
|
419
|
-
if (editorMissingOrReplaced)
|
|
514
|
+
if (editorMissingOrReplaced) editorChange = installEditor(ctx);
|
|
420
515
|
} else if (editorInstalled || prototypePatchesInstalled) {
|
|
421
|
-
|
|
516
|
+
editorChange = uninstallEditor(ctx);
|
|
517
|
+
}
|
|
518
|
+
if (editorChange && !editorChange.ok) {
|
|
519
|
+
result.editorBlocked = true;
|
|
520
|
+
result.editorReason = editorChange.reason;
|
|
422
521
|
}
|
|
423
522
|
|
|
424
523
|
if (currentConfig.features.statusLine) {
|
|
@@ -450,11 +549,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
450
549
|
const scheduleEditorReconciliation = (ctx: ExtensionContext) => {
|
|
451
550
|
sessionLifecycle.defer(() => {
|
|
452
551
|
if (!isTuiContext(ctx) || !currentConfig.features.editor) return;
|
|
453
|
-
const
|
|
454
|
-
if (
|
|
455
|
-
|
|
552
|
+
const observed = observeEditorFactory(ctx);
|
|
553
|
+
if (!observed.known || observed.factory === installedEditorFactory) return;
|
|
554
|
+
if (!observed.factory || !isZentuiEditorFactory(observed.factory)) {
|
|
555
|
+
uninstallPrototypePatches();
|
|
556
|
+
clearEditorOwnership();
|
|
456
557
|
refresh();
|
|
558
|
+
return;
|
|
457
559
|
}
|
|
560
|
+
trackZentuiEditorFactory(observed.factory);
|
|
561
|
+
refresh();
|
|
458
562
|
});
|
|
459
563
|
};
|
|
460
564
|
|
|
@@ -464,28 +568,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
464
568
|
try {
|
|
465
569
|
disposeFixedEditor(ctx);
|
|
466
570
|
if (isTuiContext(ctx)) removeFixedEditorProbe(ctx);
|
|
467
|
-
uninstallPrototypePatches();
|
|
468
571
|
stopSessionTimer();
|
|
469
572
|
stopProjectRefresh();
|
|
470
573
|
requestFooterRender = undefined;
|
|
471
574
|
getActiveExtensionStatuses = () => new Map();
|
|
472
575
|
if (isTuiContext(ctx)) {
|
|
473
576
|
ctx.ui.setFooter(undefined);
|
|
474
|
-
const
|
|
475
|
-
if (
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
577
|
+
const before = observeEditorFactory(ctx);
|
|
578
|
+
if (before.known) {
|
|
579
|
+
const currentFactory = before.factory;
|
|
580
|
+
if (currentFactory && isZentuiEditorFactory(currentFactory)) {
|
|
581
|
+
replaceEditor(
|
|
582
|
+
ctx,
|
|
583
|
+
getZentuiEditorBaseFactory(currentFactory) ??
|
|
584
|
+
(editorInstallMode === "wrapper" && wrappedEditorFactory
|
|
585
|
+
? wrappedEditorFactory
|
|
586
|
+
: undefined),
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
reconcileObservedEditorOwnership(ctx);
|
|
482
590
|
}
|
|
483
591
|
}
|
|
484
|
-
|
|
485
|
-
installedEditorFactory = undefined;
|
|
486
|
-
editorInstallMode = "none";
|
|
592
|
+
uninstallPrototypePatches();
|
|
487
593
|
footerInstalled = false;
|
|
488
|
-
editorInstalled = false;
|
|
489
594
|
activeTheme = undefined;
|
|
490
595
|
} finally {
|
|
491
596
|
requestFooterRender = undefined;
|
|
@@ -520,9 +625,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
520
625
|
const result = applyConfiguredUi(ctx);
|
|
521
626
|
return {
|
|
522
627
|
applied: !(patch.editor !== undefined && result.editorBlocked),
|
|
523
|
-
reason: result.editorBlocked
|
|
524
|
-
? "another extension is currently managing the editor; reload Pi to apply this change"
|
|
525
|
-
: undefined,
|
|
628
|
+
reason: result.editorBlocked ? result.editorReason : undefined,
|
|
526
629
|
};
|
|
527
630
|
},
|
|
528
631
|
setFooterSegments(patch: Partial<FooterSegmentsConfig>, ctx: ExtensionContext) {
|
|
@@ -79,6 +79,7 @@ export function installPrototypePatch(
|
|
|
79
79
|
if (!(record && record.method === method && target[method] === record.wrapper)) {
|
|
80
80
|
const predecessor = target[method];
|
|
81
81
|
if (typeof predecessor !== "function") {
|
|
82
|
+
if (registry.size === 0) delete target[ZENTUI_PROTOTYPE_PATCH_REGISTRY];
|
|
82
83
|
throw new TypeError(`Cannot patch ${method}: predecessor is not a function`);
|
|
83
84
|
}
|
|
84
85
|
const nextRecord: PatchRecord = {
|
|
@@ -98,7 +99,13 @@ export function installPrototypePatch(
|
|
|
98
99
|
nextRecord.wrapper = wrapper;
|
|
99
100
|
record = nextRecord;
|
|
100
101
|
registry.set(adapter, record);
|
|
101
|
-
|
|
102
|
+
try {
|
|
103
|
+
target[method] = wrapper;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
registry.delete(adapter);
|
|
106
|
+
if (registry.size === 0) delete target[ZENTUI_PROTOTYPE_PATCH_REGISTRY];
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
const token = Symbol(adapter);
|
|
@@ -65,11 +65,17 @@ export function installSelectorBorderStyle(
|
|
|
65
65
|
getTheme,
|
|
66
66
|
getConfig,
|
|
67
67
|
);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
68
|
+
let cleanupSettings: Cleanup;
|
|
69
|
+
try {
|
|
70
|
+
cleanupSettings = patchSelectorBorderStyle(
|
|
71
|
+
SettingsSelectorComponent.prototype as unknown as PatchableSelectorPrototype,
|
|
72
|
+
getTheme,
|
|
73
|
+
getConfig,
|
|
74
|
+
);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
cleanupModel();
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
73
79
|
return () => {
|
|
74
80
|
cleanupModel();
|
|
75
81
|
cleanupSettings();
|
package/extensions/zentui/ui.ts
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
safeThemeFg,
|
|
18
18
|
} from "./style";
|
|
19
19
|
|
|
20
|
-
const
|
|
20
|
+
const LEGACY_SPLIT_POLISHED_FRAME = Symbol.for("pi-zentui.polished-frame");
|
|
21
21
|
|
|
22
22
|
type ViewportCounts = {
|
|
23
23
|
above?: string;
|
|
@@ -30,7 +30,12 @@ type PolishedFrameSplit = {
|
|
|
30
30
|
viewport: ViewportCounts;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
type PolishedFrameProvenance = {
|
|
34
|
+
rows: readonly string[];
|
|
35
|
+
split: PolishedFrameSplit;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const POLISHED_FRAME_SPLITS = new WeakMap<string[], PolishedFrameProvenance>();
|
|
34
39
|
|
|
35
40
|
type AutocompleteEditorInternals = {
|
|
36
41
|
autocompleteList?: Pick<Component, "render">;
|
|
@@ -58,7 +63,6 @@ type WrappedEditor = EditorComponent &
|
|
|
58
63
|
setAutocompleteProvider?: (provider: AutocompleteProvider) => void;
|
|
59
64
|
setPaddingX?: (padding: number) => void;
|
|
60
65
|
setAutocompleteMaxVisible?: (maxVisible: number) => void;
|
|
61
|
-
[SPLIT_POLISHED_FRAME]?: (lines: string[]) => PolishedFrameSplit | undefined;
|
|
62
66
|
};
|
|
63
67
|
|
|
64
68
|
type EditorMeta = {
|
|
@@ -78,9 +82,58 @@ type PolishedFrameOptions = {
|
|
|
78
82
|
modelMeta: EditorMeta;
|
|
79
83
|
thinkingLevel: string | undefined;
|
|
80
84
|
rightStatus?: string;
|
|
81
|
-
|
|
85
|
+
ownedFrame?: PolishedFrameSplit;
|
|
86
|
+
trustedBaseFrame?: boolean;
|
|
82
87
|
};
|
|
83
88
|
|
|
89
|
+
type PolishedFrameResult = { lines: string[]; decorated: boolean };
|
|
90
|
+
|
|
91
|
+
type AutocompleteCount = { known: true; count: number } | { known: false };
|
|
92
|
+
|
|
93
|
+
function isStringArray(value: unknown): value is string[] {
|
|
94
|
+
return Array.isArray(value) && value.every((line) => typeof line === "string");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isViewportCounts(value: unknown): value is ViewportCounts {
|
|
98
|
+
if (!value || typeof value !== "object") return false;
|
|
99
|
+
const counts = value as Record<string, unknown>;
|
|
100
|
+
return [counts.above, counts.below].every(
|
|
101
|
+
(count) => count === undefined || (typeof count === "string" && /^[1-9]\d*$/.test(count)),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isPolishedFrameSplit(value: unknown, baseLineCount: number): value is PolishedFrameSplit {
|
|
106
|
+
if (!value || typeof value !== "object") return false;
|
|
107
|
+
const split = value as Record<string, unknown>;
|
|
108
|
+
return (
|
|
109
|
+
isStringArray(split.editorLines) &&
|
|
110
|
+
isStringArray(split.trailingLines) &&
|
|
111
|
+
split.trailingLines.length <= baseLineCount &&
|
|
112
|
+
isViewportCounts(split.viewport)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readAutocompleteCount(
|
|
117
|
+
source: AutocompleteEditorInternals,
|
|
118
|
+
width: number,
|
|
119
|
+
baseLineCount: number,
|
|
120
|
+
): AutocompleteCount {
|
|
121
|
+
try {
|
|
122
|
+
const showing = source.isShowingAutocomplete;
|
|
123
|
+
if (typeof showing !== "function") return { known: true, count: 0 };
|
|
124
|
+
if (!showing.call(source)) return { known: true, count: 0 };
|
|
125
|
+
const list = source.autocompleteList;
|
|
126
|
+
if (!list || typeof list.render !== "function") return { known: false };
|
|
127
|
+
const rendered = list.render(width);
|
|
128
|
+
if (!isStringArray(rendered) || rendered.length <= 0 || rendered.length >= baseLineCount) {
|
|
129
|
+
return { known: false };
|
|
130
|
+
}
|
|
131
|
+
return { known: true, count: rendered.length };
|
|
132
|
+
} catch {
|
|
133
|
+
return { known: false };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
84
137
|
function clampRenderedLines(lines: string[], width: number): string[] {
|
|
85
138
|
const maxWidth = Math.max(0, width);
|
|
86
139
|
return lines.map((line) => truncateToWidth(line, maxWidth, ""));
|
|
@@ -264,42 +317,49 @@ function renderPolishedFrame({
|
|
|
264
317
|
modelMeta,
|
|
265
318
|
thinkingLevel,
|
|
266
319
|
rightStatus,
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
320
|
+
ownedFrame,
|
|
321
|
+
trustedBaseFrame = false,
|
|
322
|
+
}: PolishedFrameOptions): PolishedFrameResult {
|
|
323
|
+
if (width <= 2) return { lines: clampRenderedLines(baseRendered, width), decorated: false };
|
|
270
324
|
|
|
271
325
|
const reset = "\x1b[0m";
|
|
272
326
|
const colorSource = config.colorSources.editor;
|
|
273
327
|
const { prompt, promptWidth, rail, railWidth } = getEditorChromeWidths(config, uiTheme, reset);
|
|
274
328
|
const innerWidth = Math.max(0, width - railWidth);
|
|
275
329
|
const copyFriendlyContinuation = " ".repeat(promptWidth);
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
330
|
+
|
|
331
|
+
if (baseRendered.length < 2) {
|
|
332
|
+
return { lines: clampRenderedLines(baseRendered, width), decorated: false };
|
|
333
|
+
}
|
|
334
|
+
if (ownedFrame && !isPolishedFrameSplit(ownedFrame, baseRendered.length)) {
|
|
335
|
+
return { lines: clampRenderedLines(baseRendered, width), decorated: false };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const autocomplete = ownedFrame
|
|
339
|
+
? { known: true, count: ownedFrame.trailingLines.length }
|
|
340
|
+
: readAutocompleteCount(autocompleteSource, innerWidth, baseRendered.length);
|
|
341
|
+
if (!autocomplete.known) {
|
|
342
|
+
return { lines: clampRenderedLines(baseRendered, width), decorated: false };
|
|
343
|
+
}
|
|
289
344
|
const editorFrame =
|
|
290
|
-
!ownedFrame &&
|
|
291
|
-
? baseRendered.slice(0, -
|
|
345
|
+
!ownedFrame && autocomplete.count > 0
|
|
346
|
+
? baseRendered.slice(0, -autocomplete.count)
|
|
292
347
|
: baseRendered;
|
|
293
348
|
const autocompleteLines = ownedFrame
|
|
294
349
|
? ownedFrame.trailingLines
|
|
295
|
-
:
|
|
296
|
-
? baseRendered.slice(-
|
|
350
|
+
: autocomplete.count > 0
|
|
351
|
+
? baseRendered.slice(-autocomplete.count)
|
|
297
352
|
: [];
|
|
298
|
-
if (editorFrame.length < 2)
|
|
353
|
+
if (editorFrame.length < 2) {
|
|
354
|
+
return { lines: clampRenderedLines(baseRendered, width), decorated: false };
|
|
355
|
+
}
|
|
299
356
|
|
|
300
|
-
const editorLines = ownedFrame?.editorLines ?? editorFrame.slice(1, -1);
|
|
301
357
|
const parsedTop = parseEditorBorder(editorFrame[0] ?? "", "above");
|
|
302
358
|
const parsedBottom = parseEditorBorder(editorFrame.at(-1) ?? "", "below");
|
|
359
|
+
if (!ownedFrame && !trustedBaseFrame && (!parsedTop || !parsedBottom)) {
|
|
360
|
+
return { lines: clampRenderedLines(baseRendered, width), decorated: false };
|
|
361
|
+
}
|
|
362
|
+
const editorLines = ownedFrame?.editorLines ?? editorFrame.slice(1, -1);
|
|
303
363
|
const viewport = ownedFrame?.viewport ?? {
|
|
304
364
|
above: parsedTop?.count,
|
|
305
365
|
below: parsedBottom?.count,
|
|
@@ -365,11 +425,14 @@ function renderPolishedFrame({
|
|
|
365
425
|
|
|
366
426
|
const clamped = clampRenderedLines(renderedLines, width);
|
|
367
427
|
POLISHED_FRAME_SPLITS.set(clamped, {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
428
|
+
rows: Object.freeze([...clamped]),
|
|
429
|
+
split: {
|
|
430
|
+
editorLines,
|
|
431
|
+
trailingLines: autocompleteLines.length > 0 ? clamped.slice(-autocompleteLines.length) : [],
|
|
432
|
+
viewport,
|
|
433
|
+
},
|
|
371
434
|
});
|
|
372
|
-
return clamped;
|
|
435
|
+
return { lines: clamped, decorated: true };
|
|
373
436
|
}
|
|
374
437
|
|
|
375
438
|
export class PolishedEditor extends CustomEditor {
|
|
@@ -404,34 +467,53 @@ export class PolishedEditor extends CustomEditor {
|
|
|
404
467
|
const { railWidth } = getEditorChromeWidths(config, this.uiTheme, "\x1b[0m");
|
|
405
468
|
const innerWidth = Math.max(0, width - railWidth);
|
|
406
469
|
const rendered = super.render(innerWidth);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
return (
|
|
422
|
-
POLISHED_FRAME_SPLITS.get(lines) ?? splitPolishedFrame(lines, this.getConfig(), this.uiTheme)
|
|
423
|
-
);
|
|
470
|
+
try {
|
|
471
|
+
return renderPolishedFrame({
|
|
472
|
+
width,
|
|
473
|
+
baseRendered: rendered,
|
|
474
|
+
autocompleteSource: this as unknown as AutocompleteEditorInternals,
|
|
475
|
+
uiTheme: this.uiTheme,
|
|
476
|
+
config,
|
|
477
|
+
modelMeta: this.getModelMeta(),
|
|
478
|
+
thinkingLevel: this.getThinkingLevel(),
|
|
479
|
+
trustedBaseFrame: true,
|
|
480
|
+
}).lines;
|
|
481
|
+
} catch {
|
|
482
|
+
return clampRenderedLines(rendered, width);
|
|
483
|
+
}
|
|
424
484
|
}
|
|
425
485
|
}
|
|
426
486
|
|
|
427
487
|
export class WrappedPolishedEditor implements EditorComponent {
|
|
488
|
+
declare readonly addToHistory?: (text: string) => void;
|
|
489
|
+
declare readonly insertTextAtCursor?: (text: string) => void;
|
|
490
|
+
declare readonly setAutocompleteProvider?: (provider: AutocompleteProvider) => void;
|
|
491
|
+
declare readonly setPaddingX?: (padding: number) => void;
|
|
492
|
+
declare readonly setAutocompleteMaxVisible?: (maxVisible: number) => void;
|
|
493
|
+
|
|
428
494
|
constructor(
|
|
429
495
|
private readonly base: WrappedEditor,
|
|
430
496
|
private readonly uiTheme: Theme,
|
|
431
497
|
private readonly getConfig: () => PolishedTuiConfig,
|
|
432
498
|
private readonly getModelMeta: () => EditorMeta,
|
|
433
499
|
private readonly getThinkingLevel: () => string | undefined,
|
|
434
|
-
) {
|
|
500
|
+
) {
|
|
501
|
+
if (typeof base.addToHistory === "function") {
|
|
502
|
+
this.addToHistory = (text) => base.addToHistory?.(text);
|
|
503
|
+
}
|
|
504
|
+
if (typeof base.insertTextAtCursor === "function") {
|
|
505
|
+
this.insertTextAtCursor = (text) => base.insertTextAtCursor?.(text);
|
|
506
|
+
}
|
|
507
|
+
if (typeof base.setAutocompleteProvider === "function") {
|
|
508
|
+
this.setAutocompleteProvider = (provider) => base.setAutocompleteProvider?.(provider);
|
|
509
|
+
}
|
|
510
|
+
if (typeof base.setPaddingX === "function") {
|
|
511
|
+
this.setPaddingX = (padding) => base.setPaddingX?.(padding);
|
|
512
|
+
}
|
|
513
|
+
if (typeof base.setAutocompleteMaxVisible === "function") {
|
|
514
|
+
this.setAutocompleteMaxVisible = (maxVisible) => base.setAutocompleteMaxVisible?.(maxVisible);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
435
517
|
|
|
436
518
|
get focused(): boolean {
|
|
437
519
|
return Boolean(this.base.focused);
|
|
@@ -516,26 +598,42 @@ export class WrappedPolishedEditor implements EditorComponent {
|
|
|
516
598
|
const config = this.getConfig();
|
|
517
599
|
const { railWidth } = getEditorChromeWidths(config, this.uiTheme, "\x1b[0m");
|
|
518
600
|
const innerWidth = Math.max(0, width - railWidth);
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
width,
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
601
|
+
let rendered: string[];
|
|
602
|
+
try {
|
|
603
|
+
rendered = this.base.render(innerWidth);
|
|
604
|
+
} catch {
|
|
605
|
+
return clampRenderedLines(this.base.render(width), width);
|
|
606
|
+
}
|
|
607
|
+
let result: PolishedFrameResult | undefined;
|
|
608
|
+
try {
|
|
609
|
+
const provenance = POLISHED_FRAME_SPLITS.get(rendered);
|
|
610
|
+
const provenanceMatches = Boolean(
|
|
611
|
+
provenance &&
|
|
612
|
+
provenance.rows.length === rendered.length &&
|
|
613
|
+
provenance.rows.every((line, index) => line === rendered[index]),
|
|
614
|
+
);
|
|
615
|
+
const ownedFrame = provenanceMatches ? provenance?.split : undefined;
|
|
616
|
+
const hasMutatedProvenance = Boolean(provenance && !provenanceMatches);
|
|
617
|
+
const hasUntrustedLegacySplitter = LEGACY_SPLIT_POLISHED_FRAME in this.base;
|
|
618
|
+
const hasUnprovenPolishedFrame =
|
|
619
|
+
!ownedFrame && Boolean(splitPolishedFrame(rendered, config, this.uiTheme));
|
|
620
|
+
if (!hasMutatedProvenance && !hasUntrustedLegacySplitter && !hasUnprovenPolishedFrame) {
|
|
621
|
+
result = renderPolishedFrame({
|
|
622
|
+
width,
|
|
623
|
+
baseRendered: rendered,
|
|
624
|
+
autocompleteSource: this.base,
|
|
625
|
+
uiTheme: this.uiTheme,
|
|
626
|
+
config,
|
|
627
|
+
modelMeta: this.getModelMeta(),
|
|
628
|
+
thinkingLevel: this.getThinkingLevel(),
|
|
629
|
+
rightStatus: readVimStatus(this.base, this.uiTheme),
|
|
630
|
+
ownedFrame,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
} catch {
|
|
634
|
+
// Decoration is optional; re-render the base at the caller's width below.
|
|
635
|
+
}
|
|
636
|
+
return result?.decorated ? result.lines : clampRenderedLines(this.base.render(width), width);
|
|
539
637
|
}
|
|
540
638
|
|
|
541
639
|
invalidate(): void {
|
|
@@ -554,30 +652,10 @@ export class WrappedPolishedEditor implements EditorComponent {
|
|
|
554
652
|
this.base.setText(text);
|
|
555
653
|
}
|
|
556
654
|
|
|
557
|
-
addToHistory(text: string): void {
|
|
558
|
-
this.base.addToHistory?.(text);
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
insertTextAtCursor(text: string): void {
|
|
562
|
-
this.base.insertTextAtCursor?.(text);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
655
|
getExpandedText(): string {
|
|
566
656
|
return this.base.getExpandedText?.() ?? this.base.getText();
|
|
567
657
|
}
|
|
568
658
|
|
|
569
|
-
setAutocompleteProvider(provider: AutocompleteProvider): void {
|
|
570
|
-
this.base.setAutocompleteProvider?.(provider);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
setPaddingX(padding: number): void {
|
|
574
|
-
this.base.setPaddingX?.(padding);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
setAutocompleteMaxVisible(maxVisible: number): void {
|
|
578
|
-
this.base.setAutocompleteMaxVisible?.(maxVisible);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
659
|
getLines(): string[] {
|
|
582
660
|
return this.base.getLines?.() ?? this.base.getText().split("\n");
|
|
583
661
|
}
|
|
@@ -225,24 +225,30 @@ export function installUserMessageStyle(
|
|
|
225
225
|
return Reflect.apply(predecessor, receiver, args);
|
|
226
226
|
},
|
|
227
227
|
);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
228
|
+
let cleanupRender: Cleanup;
|
|
229
|
+
try {
|
|
230
|
+
cleanupRender = installPrototypePatch(
|
|
231
|
+
prototype,
|
|
232
|
+
"render",
|
|
233
|
+
"user-message-render",
|
|
234
|
+
({ predecessor, receiver, args }) => {
|
|
235
|
+
const width = args[0];
|
|
236
|
+
if (typeof width !== "number") return Reflect.apply(predecessor, receiver, args);
|
|
237
|
+
const lines = renderZentuiUserMessage(
|
|
238
|
+
receiver as PatchableUserMessagePrototype,
|
|
239
|
+
width,
|
|
240
|
+
getTheme(),
|
|
241
|
+
getConfig(),
|
|
242
|
+
);
|
|
243
|
+
if (!lines) return Reflect.apply(predecessor, receiver, args);
|
|
244
|
+
if (lines.length === 0) return lines;
|
|
245
|
+
return withPromptZoneMarkers(lines);
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
} catch (error) {
|
|
249
|
+
cleanupInvalidate();
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
246
252
|
let cleaned = false;
|
|
247
253
|
return () => {
|
|
248
254
|
if (cleaned) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-zentui",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "A Starship-inspired statusline and Opencode-style TUI for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@biomejs/biome": "^2.5.0",
|
|
53
|
-
"@earendil-works/pi-ai": "^0.
|
|
54
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
55
|
-
"@earendil-works/pi-tui": "^0.
|
|
53
|
+
"@earendil-works/pi-ai": "^0.82.1",
|
|
54
|
+
"@earendil-works/pi-coding-agent": "^0.82.1",
|
|
55
|
+
"@earendil-works/pi-tui": "^0.82.1",
|
|
56
56
|
"@types/node": "^26.0.0",
|
|
57
57
|
"typescript": "^7.0.2",
|
|
58
58
|
"vitest": "^4.1.8"
|