pi-zentui 0.29.1 → 0.29.3
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/docs/configuration.md +1 -1
- package/extensions/zentui/index.ts +15 -20
- package/extensions/zentui/layered-editor.ts +3 -3
- package/extensions/zentui/minimalist-editor.ts +26 -12
- package/extensions/zentui/thinking-experimental.ts +18 -2
- package/extensions/zentui/user-message.ts +27 -16
- package/extensions/zentui/working-line.ts +29 -36
- package/package.json +1 -1
package/docs/configuration.md
CHANGED
|
@@ -776,7 +776,7 @@ Rail, Tree, and Streaming share one private `AssistantMessageComponent` wrapper.
|
|
|
776
776
|
|
|
777
777
|
Rail parses each native contiguous thinking run and shows every label in that run. Tree shows the latest five in each run; neither aggregates across intervening text or tool blocks. Both follow Pi's thinking visibility. Labels come from headings, top-level list items, and blank-line-separated prose. Complete strict 7-bit CSI SGR styling is stripped before parsing; every other terminal control, unsafe or unstructured content, malformed or over-limit input, unterminated fences/math, and unsupported structure leave that complete run native. Fenced code, Mermaid, display math, and indented nested content remain opaque bodies.
|
|
778
778
|
|
|
779
|
-
Each selected SGR-free label is rendered as Markdown by a fresh Pi `Markdown` with the host child's exact theme, default `thinkingText`/italic style, and transform options. Native emphasis, code, links, HTML, LaTeX, and custom theme/transform callbacks therefore remain authoritative. Host horizontal padding is applied externally. Each label is exactly one terminal row; Pi TUI's ANSI/OSC/grapheme-aware width utilities crop the first rendered row and reserve one cell for `…` only when required. Empty, image, non-text, impossible-width, or throwing output restores the whole native run. Connectors are separate from Markdown and call the current `theme.fg("accent", connector)`
|
|
779
|
+
Each selected SGR-free label is rendered as Markdown by a fresh Pi `Markdown` with the host child's exact theme, default `thinkingText`/italic style, and transform options. Native emphasis, code, links, HTML, LaTeX, and custom theme/transform callbacks therefore remain authoritative. Host horizontal padding is applied externally. Each label is exactly one terminal row; Pi TUI's ANSI/OSC/grapheme-aware width utilities crop the first rendered row and reserve one cell for `…` only when required. Empty, image, non-text, impossible-width, or throwing output restores the whole native run. Connectors are separate from Markdown and call the current `theme.fg("accent", connector)` when building a layout, so custom themes directly control their appearance. Successful layouts are reused while width and connector-theme identity are unchanged; a width or theme identity change or invalidation refreshes them. Same-object theme mutations require invalidation. Visible forms are:
|
|
780
780
|
|
|
781
781
|
```text
|
|
782
782
|
│ Thinking ┆ Thinking
|
|
@@ -336,9 +336,9 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
|
|
|
336
336
|
return undefined;
|
|
337
337
|
}
|
|
338
338
|
};
|
|
339
|
-
// Layered visibility
|
|
340
|
-
// Data, demand, and timer consumers
|
|
341
|
-
//
|
|
339
|
+
// Layered visibility does not grant installation ownership: editor install/restore stays exclusive.
|
|
340
|
+
// Data, demand, and timer consumers use these helpers; Working-line placement additionally
|
|
341
|
+
// requires focused, rendered border capability before its controller hides its own host row.
|
|
342
342
|
// Ownership counts before Pi constructs its editor; layered visibility requires the recorded
|
|
343
343
|
// instance to remain reachable through the current factory.
|
|
344
344
|
const layeredVisibleEditor = () => layeredEditor.editorVisibleThrough(currentEditorFactory());
|
|
@@ -402,17 +402,13 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
|
|
|
402
402
|
undefined,
|
|
403
403
|
(ctx) => {
|
|
404
404
|
if (!activeTuiContext || ctx.ui !== activeTuiContext.ui) return false;
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
return false;
|
|
408
|
-
}
|
|
405
|
+
// Factory visibility alone is insufficient: the current focused editor must have
|
|
406
|
+
// rendered safe border geometry before the separately owned Working row can hide.
|
|
409
407
|
return Boolean(
|
|
410
|
-
|
|
411
|
-
sessionLifecycle.isCurrent(activeEditor.generation) &&
|
|
412
|
-
activeEditor.factory === installedEditorFactory &&
|
|
408
|
+
sessionLifecycle.isCurrent() &&
|
|
413
409
|
effectiveEditorEnabled() &&
|
|
414
410
|
currentConfig.components.editor.style !== "accent-rail" &&
|
|
415
|
-
|
|
411
|
+
visibleEditor()?.canEmbedWorkingLineBorder(),
|
|
416
412
|
);
|
|
417
413
|
},
|
|
418
414
|
);
|
|
@@ -939,10 +935,7 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
|
|
|
939
935
|
return observed;
|
|
940
936
|
};
|
|
941
937
|
|
|
942
|
-
//
|
|
943
|
-
const isActiveEditor = (editor: PolishedEditor | WrappedPolishedEditor, generation: number) =>
|
|
944
|
-
sessionLifecycle.isCurrent(generation) && activeEditor?.editor === editor;
|
|
945
|
-
// Read-only: also true for an editor still visible beneath a foreign wrapper.
|
|
938
|
+
// Also true for an editor still visible beneath a foreign wrapper; installation stays exclusive.
|
|
946
939
|
const isVisibleEditor = (editor: PolishedEditor | WrappedPolishedEditor, generation: number) =>
|
|
947
940
|
sessionLifecycle.isCurrent(generation) && visibleEditor() === editor;
|
|
948
941
|
|
|
@@ -950,7 +943,7 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
|
|
|
950
943
|
editor: PolishedEditor | WrappedPolishedEditor,
|
|
951
944
|
generation: number,
|
|
952
945
|
) => {
|
|
953
|
-
if (!
|
|
946
|
+
if (!isVisibleEditor(editor, generation) || !activeTuiContext || !workingLineSessionReady)
|
|
954
947
|
return;
|
|
955
948
|
if (!host.skinOnly) workingLine.reconcile(activeTuiContext);
|
|
956
949
|
if (workingLine.isAvailable()) requestEditorRepaint();
|
|
@@ -966,10 +959,12 @@ export default function (pi: ExtensionAPI, host: ZentuiHost = {}) {
|
|
|
966
959
|
const editorWorkingLineFrame = (
|
|
967
960
|
editor: PolishedEditor | WrappedPolishedEditor,
|
|
968
961
|
generation: number,
|
|
969
|
-
) =>
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
962
|
+
) => {
|
|
963
|
+
if (host.skinOnly || !sessionLifecycle.isCurrent(generation)) return undefined;
|
|
964
|
+
// Even a stale instance render must release a row whose current border disappeared.
|
|
965
|
+
const frame = workingLine.currentWorkingLineFrame();
|
|
966
|
+
return isVisibleEditor(editor, generation) ? frame : undefined;
|
|
967
|
+
};
|
|
973
968
|
|
|
974
969
|
/** Records a freshly constructed editor as owned (exclusive) or visible beneath a foreign wrapper. */
|
|
975
970
|
const registerConstructedEditor = (
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* and redraw requests. The record is valid only while the outer factory observed at construction is
|
|
9
9
|
* still the current editor factory, so any replacement of the chain invalidates it.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Construction alone cannot authorize hiding the Working row: its controller additionally requires
|
|
12
|
+
* a focused editor that has rendered safe border geometry. A wrapper that renders then discards those
|
|
13
|
+
* rows remains undetectable; supporting such wrappers requires an explicit host visibility contract.
|
|
14
14
|
*/
|
|
15
15
|
export class LayeredEditorConsumer<Factory, Editor = unknown> {
|
|
16
16
|
private record?: {
|
|
@@ -172,9 +172,11 @@ function renderTopLeft(
|
|
|
172
172
|
metadata: MinimalistEditorMetadata,
|
|
173
173
|
uiTheme: Theme,
|
|
174
174
|
config: ZentuiConfig,
|
|
175
|
+
renderBorder: (text: string) => string,
|
|
175
176
|
includeSessionName = true,
|
|
176
177
|
): string {
|
|
177
178
|
const source = config.components.editor.colorSource;
|
|
179
|
+
const separator = safeThemeFg(uiTheme, "muted", " · ");
|
|
178
180
|
const bashMode = bashModeLabel(inputText);
|
|
179
181
|
const parts: string[] = [];
|
|
180
182
|
if (bashMode) {
|
|
@@ -206,16 +208,20 @@ function renderTopLeft(
|
|
|
206
208
|
? sanitizeEditorMetadataText(metadata.sessionName ?? "")
|
|
207
209
|
: "";
|
|
208
210
|
if (config.components.editor.styles.minimalist.showSessionName && sessionName) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
211
|
+
return joinStyled(
|
|
212
|
+
[
|
|
213
|
+
joinStyled(parts, separator),
|
|
214
|
+
renderStyleForSource(
|
|
215
|
+
uiTheme,
|
|
216
|
+
source,
|
|
217
|
+
componentColor(config, "editor", "sessionName"),
|
|
218
|
+
sessionName,
|
|
219
|
+
),
|
|
220
|
+
],
|
|
221
|
+
metadata.workingLineFrame ? renderBorder(" ─ ") : separator,
|
|
216
222
|
);
|
|
217
223
|
}
|
|
218
|
-
return joinStyled(parts,
|
|
224
|
+
return joinStyled(parts, separator);
|
|
219
225
|
}
|
|
220
226
|
|
|
221
227
|
function renderTopRight(
|
|
@@ -448,6 +454,7 @@ function renderTemplateBuiltin(
|
|
|
448
454
|
{ cwd: "", sessionName: metadata.sessionName },
|
|
449
455
|
uiTheme,
|
|
450
456
|
explicitConfig,
|
|
457
|
+
renderBorder,
|
|
451
458
|
);
|
|
452
459
|
case "turn_duration":
|
|
453
460
|
return renderTopLeft(
|
|
@@ -455,6 +462,7 @@ function renderTemplateBuiltin(
|
|
|
455
462
|
{ cwd: "", agentDurationMs: metadata.agentDurationMs, agentActive: metadata.agentActive },
|
|
456
463
|
uiTheme,
|
|
457
464
|
explicitConfig,
|
|
465
|
+
renderBorder,
|
|
458
466
|
);
|
|
459
467
|
case "model":
|
|
460
468
|
isolated.modelLabel = metadata.modelLabel;
|
|
@@ -693,8 +701,8 @@ export function renderMinimalistFrame({
|
|
|
693
701
|
if (!count || !/^[1-9]\d*$/.test(count)) return "";
|
|
694
702
|
return safeThemeFg(uiTheme, "muted", `${direction === "above" ? "↑" : "↓"} ${count} more`);
|
|
695
703
|
};
|
|
696
|
-
const topMetadata = renderTopLeft(inputText, metadata, uiTheme, config);
|
|
697
|
-
const topOperational = renderTopLeft(inputText, metadata, uiTheme, config, false);
|
|
704
|
+
const topMetadata = renderTopLeft(inputText, metadata, uiTheme, config, renderBorder);
|
|
705
|
+
const topOperational = renderTopLeft(inputText, metadata, uiTheme, config, renderBorder, false);
|
|
698
706
|
const topViewport = viewportLabel("above", viewport?.above);
|
|
699
707
|
const topLeft = joinStyled([topViewport, topMetadata], separator);
|
|
700
708
|
const topRightBudget = Math.max(0, width - 8 - visibleWidth(topLeft));
|
|
@@ -775,6 +783,7 @@ export function renderMinimalistFrame({
|
|
|
775
783
|
{ cwd: "", workingLineFrame: metadata.workingLineFrame },
|
|
776
784
|
uiTheme,
|
|
777
785
|
config,
|
|
786
|
+
renderBorder,
|
|
778
787
|
false,
|
|
779
788
|
)
|
|
780
789
|
: "";
|
|
@@ -784,13 +793,18 @@ export function renderMinimalistFrame({
|
|
|
784
793
|
if (visibleWidth(prefix) > budget) prefix = operational;
|
|
785
794
|
if (visibleWidth(prefix) > budget)
|
|
786
795
|
prefix = truncateToWidth(prefix, Math.max(0, budget), "…");
|
|
796
|
+
const metadataSeparator =
|
|
797
|
+
operational && metadata.workingLineFrame ? renderBorder(" ─ ") : separator;
|
|
787
798
|
rendered = renderMinimalistTemplate(
|
|
788
799
|
format,
|
|
789
800
|
resolve,
|
|
790
801
|
customNames,
|
|
791
|
-
Math.max(
|
|
802
|
+
Math.max(
|
|
803
|
+
0,
|
|
804
|
+
budget - visibleWidth(prefix) - (prefix ? visibleWidth(metadataSeparator) : 0),
|
|
805
|
+
),
|
|
792
806
|
);
|
|
793
|
-
return joinStyled([prefix, rendered],
|
|
807
|
+
return joinStyled([prefix, rendered], metadataSeparator);
|
|
794
808
|
}
|
|
795
809
|
switch (slot) {
|
|
796
810
|
case "topLeft": {
|
|
@@ -218,6 +218,8 @@ function croppedMarkdownRow(markdown: Markdown, source: string, width: number):
|
|
|
218
218
|
/** A width-bounded private replacement for one native contiguous thinking run. */
|
|
219
219
|
export class ThinkingStepsRows implements Component {
|
|
220
220
|
private readonly title: Markdown;
|
|
221
|
+
// Content and presentation are constructor-fixed; retain only the last successful layout.
|
|
222
|
+
private renderedCache: { width: number; theme: AccentTheme; rows: string[] } | undefined;
|
|
221
223
|
private readonly labels: Array<{ source: string; markdown: Markdown }>;
|
|
222
224
|
|
|
223
225
|
constructor(
|
|
@@ -246,12 +248,23 @@ export class ThinkingStepsRows implements Component {
|
|
|
246
248
|
}
|
|
247
249
|
|
|
248
250
|
private renderNative(width: number): string[] {
|
|
251
|
+
this.renderedCache = undefined;
|
|
249
252
|
this.onPresentation?.(false);
|
|
250
253
|
return this.native.render(width);
|
|
251
254
|
}
|
|
252
255
|
|
|
253
256
|
render(width: number): string[] {
|
|
254
257
|
try {
|
|
258
|
+
let theme: AccentTheme | undefined;
|
|
259
|
+
const cached = this.renderedCache;
|
|
260
|
+
if (cached?.width === width) {
|
|
261
|
+
// The host theme getter is a read-only lookup, including on cache hits.
|
|
262
|
+
theme = this.getTheme();
|
|
263
|
+
if (cached.theme === theme) {
|
|
264
|
+
this.onPresentation?.(true);
|
|
265
|
+
return [...cached.rows];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
255
268
|
const outer = this.shape.paddingX;
|
|
256
269
|
const innerWidth = Math.floor(width) - outer * 2;
|
|
257
270
|
const titleConnector = this.mode === "rail" ? "│ " : "┆ ";
|
|
@@ -276,7 +289,7 @@ export class ThinkingStepsRows implements Component {
|
|
|
276
289
|
if (!label) return this.renderNative(width);
|
|
277
290
|
renderedLabels.push({ connector, label });
|
|
278
291
|
}
|
|
279
|
-
|
|
292
|
+
theme ??= this.getTheme();
|
|
280
293
|
const left = " ".repeat(outer);
|
|
281
294
|
const right = " ".repeat(outer);
|
|
282
295
|
const rows = [
|
|
@@ -286,14 +299,17 @@ export class ThinkingStepsRows implements Component {
|
|
|
286
299
|
),
|
|
287
300
|
];
|
|
288
301
|
if (!rows.every((row) => visibleWidth(row) <= width)) return this.renderNative(width);
|
|
302
|
+
this.renderedCache = { width, theme, rows };
|
|
289
303
|
this.onPresentation?.(true);
|
|
290
|
-
|
|
304
|
+
// Containers and decorators may mutate their returned array.
|
|
305
|
+
return [...rows];
|
|
291
306
|
} catch {
|
|
292
307
|
return this.renderNative(width);
|
|
293
308
|
}
|
|
294
309
|
}
|
|
295
310
|
|
|
296
311
|
invalidate(): void {
|
|
312
|
+
this.renderedCache = undefined;
|
|
297
313
|
this.onInvalidate?.();
|
|
298
314
|
this.native.invalidate();
|
|
299
315
|
this.title.invalidate();
|
|
@@ -54,7 +54,7 @@ function isNativeMarkdown(renderer: unknown): boolean {
|
|
|
54
54
|
|
|
55
55
|
const sanitizedMarkdownCache = new WeakMap<
|
|
56
56
|
object,
|
|
57
|
-
{ lines: unknown; text: string; options: unknown }
|
|
57
|
+
{ lines: unknown; sourceText: string; text: string; options: unknown }
|
|
58
58
|
>();
|
|
59
59
|
|
|
60
60
|
// Framing eligibility is deliberately narrower than the source trust boundary.
|
|
@@ -74,7 +74,14 @@ function withSanitizedMarkdownSources<T>(instance: unknown, render: (adapted: bo
|
|
|
74
74
|
if (!isRecord(options.value)) return;
|
|
75
75
|
const transform = options.value.transform;
|
|
76
76
|
if (transform !== undefined && typeof transform !== "function") return;
|
|
77
|
-
const
|
|
77
|
+
const cached = sanitizedMarkdownCache.get(value);
|
|
78
|
+
// Source sanitization depends only on the immutable raw string, not on
|
|
79
|
+
// width, theme, options or native output invalidation. Reuse just that
|
|
80
|
+
// result; the guarded native render and its cache checks still run.
|
|
81
|
+
const source =
|
|
82
|
+
cached && cached.sourceText === text.value
|
|
83
|
+
? cached.text
|
|
84
|
+
: sanitizeUserMessageSourceText(text.value);
|
|
78
85
|
const safeOptions = {
|
|
79
86
|
...options.value,
|
|
80
87
|
...(transform
|
|
@@ -86,7 +93,6 @@ function withSanitizedMarkdownSources<T>(instance: unknown, render: (adapted: bo
|
|
|
86
93
|
}
|
|
87
94
|
: {}),
|
|
88
95
|
};
|
|
89
|
-
const cached = sanitizedMarkdownCache.get(value);
|
|
90
96
|
restore.push(() => {
|
|
91
97
|
Object.defineProperty(value, "text", text);
|
|
92
98
|
Object.defineProperty(value, "options", options);
|
|
@@ -95,6 +101,7 @@ function withSanitizedMarkdownSources<T>(instance: unknown, render: (adapted: bo
|
|
|
95
101
|
// them normally, and cleanup needs no retained component references.
|
|
96
102
|
sanitizedMarkdownCache.set(value, {
|
|
97
103
|
lines: value.cachedText === source ? value.cachedLines : undefined,
|
|
104
|
+
sourceText: text.value,
|
|
98
105
|
text: source,
|
|
99
106
|
options: options.value,
|
|
100
107
|
});
|
|
@@ -172,18 +179,20 @@ function nativeMarkdown(instance: PatchableUserMessagePrototype):
|
|
|
172
179
|
}
|
|
173
180
|
const children = direct;
|
|
174
181
|
if (!Array.isArray(children) || children.length !== 1) return undefined;
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
182
|
+
// Pi >=1.0 holds the Markdown directly (it pads/colors itself); older Pi
|
|
183
|
+
// wrapped it in a Box. Accept both shapes.
|
|
184
|
+
let renderer = children[0];
|
|
185
|
+
const direct1 = isNativeMarkdown(renderer);
|
|
186
|
+
if (isRecord(renderer) && !direct1) {
|
|
187
|
+
const box = renderer;
|
|
188
|
+
if (!Array.isArray(box.children) || box.children.length !== 1) return undefined;
|
|
189
|
+
renderer = box.children[0];
|
|
190
|
+
}
|
|
178
191
|
if (!isNativeMarkdown(renderer)) return undefined;
|
|
179
192
|
const child = renderer as unknown as Record<string, unknown>;
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
child.paddingY !== 0 ||
|
|
184
|
-
!isRecord(child.theme)
|
|
185
|
-
)
|
|
186
|
-
return undefined;
|
|
193
|
+
if (typeof child.text !== "string" || !isRecord(child.theme)) return undefined;
|
|
194
|
+
// Box-wrapped (older Pi) Markdown must be unpadded; Pi >=1.0 pads it itself.
|
|
195
|
+
if (!direct1 && (child.paddingX !== 0 || child.paddingY !== 0)) return undefined;
|
|
187
196
|
for (const key of [
|
|
188
197
|
"heading",
|
|
189
198
|
"link",
|
|
@@ -210,6 +219,8 @@ function nativeMarkdown(instance: PatchableUserMessagePrototype):
|
|
|
210
219
|
)
|
|
211
220
|
return undefined;
|
|
212
221
|
if (child.defaultTextStyle !== undefined && !isRecord(child.defaultTextStyle)) return undefined;
|
|
222
|
+
// Pi >=1.0 puts the message background in defaultTextStyle; Zentui frames draw their own.
|
|
223
|
+
const { bgColor: _bg, ...textStyle } = (child.defaultTextStyle ?? {}) as Record<string, unknown>;
|
|
213
224
|
return {
|
|
214
225
|
renderer: renderer as unknown as Markdown,
|
|
215
226
|
text: child.text,
|
|
@@ -217,9 +228,9 @@ function nativeMarkdown(instance: PatchableUserMessagePrototype):
|
|
|
217
228
|
theme: child.theme as unknown as NonNullable<
|
|
218
229
|
UserMessageStyleRenderInput["markdown"]
|
|
219
230
|
>["theme"],
|
|
220
|
-
defaultTextStyle: child.defaultTextStyle
|
|
221
|
-
|
|
222
|
-
|
|
231
|
+
defaultTextStyle: (child.defaultTextStyle === undefined
|
|
232
|
+
? undefined
|
|
233
|
+
: textStyle) as NonNullable<UserMessageStyleRenderInput["markdown"]>["defaultTextStyle"],
|
|
223
234
|
options: child.options as NonNullable<UserMessageStyleRenderInput["markdown"]>["options"],
|
|
224
235
|
},
|
|
225
236
|
};
|
|
@@ -434,20 +434,26 @@ export function normalizeWorkingLineStyleSpec(value: ColorSpec | undefined): Col
|
|
|
434
434
|
return tokens.join(" ");
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
-
function
|
|
437
|
+
function workingLineTierRenderer(
|
|
438
438
|
theme: ThemeLike,
|
|
439
439
|
config: WorkingLineComponentConfig,
|
|
440
440
|
colors: PolishedTuiColors,
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
441
|
+
): (tier: Tier, text: string) => string {
|
|
442
|
+
// Palette validation is invariant within this build, but must not survive a
|
|
443
|
+
// metric/config/theme rebuild. Resolve lazily so unused tiers remain untouched.
|
|
444
|
+
const tierStyles = new Map<Tier, ColorSpec | undefined>();
|
|
445
|
+
return (tier, text) => {
|
|
446
|
+
if (!tierStyles.has(tier)) {
|
|
447
|
+
tierStyles.set(tier, normalizeWorkingLineStyleSpec(workingLineColor(config, colors, tier)));
|
|
448
|
+
}
|
|
449
|
+
return renderStyleForSourceOrFallback(
|
|
450
|
+
theme,
|
|
451
|
+
config.colorSource,
|
|
452
|
+
tierStyles.get(tier),
|
|
453
|
+
WORKING_LINE_FALLBACKS[tier],
|
|
454
|
+
text,
|
|
455
|
+
);
|
|
456
|
+
};
|
|
451
457
|
}
|
|
452
458
|
|
|
453
459
|
/** Resolve the fixed, nonanimated high-tier style used by persisted Turn summaries. */
|
|
@@ -484,9 +490,7 @@ export function snapshotWorkingLineHighStyle(
|
|
|
484
490
|
}
|
|
485
491
|
|
|
486
492
|
function renderAnimatedText(
|
|
487
|
-
|
|
488
|
-
config: WorkingLineComponentConfig,
|
|
489
|
-
colors: PolishedTuiColors,
|
|
493
|
+
render: (tier: Tier, text: string) => string,
|
|
490
494
|
cells: GraphemeCell[],
|
|
491
495
|
width: number,
|
|
492
496
|
tick: number,
|
|
@@ -504,7 +508,7 @@ function renderAnimatedText(
|
|
|
504
508
|
if (previous?.tier === tier) previous.text += cell.text;
|
|
505
509
|
else runs.push({ tier, text: cell.text });
|
|
506
510
|
}
|
|
507
|
-
return runs.map((run) =>
|
|
511
|
+
return runs.map((run) => render(run.tier, run.text)).join("");
|
|
508
512
|
}
|
|
509
513
|
|
|
510
514
|
export type WorkingLineRuntimeSegments = {
|
|
@@ -813,8 +817,7 @@ function spinnerRowCells(
|
|
|
813
817
|
function renderWorkingLineSchedule(
|
|
814
818
|
definition: ScheduleDefinition,
|
|
815
819
|
config: WorkingLineComponentConfig,
|
|
816
|
-
|
|
817
|
-
theme: ThemeLike,
|
|
820
|
+
render: (tier: Tier, text: string) => string,
|
|
818
821
|
rowCells: GraphemeCell[],
|
|
819
822
|
spinnerCells: Map<string, GraphemeCell[]> | undefined,
|
|
820
823
|
width: number,
|
|
@@ -834,15 +837,7 @@ function renderWorkingLineSchedule(
|
|
|
834
837
|
const renderTextForTick = (tick: number): string => {
|
|
835
838
|
let cachedText = textRenderCache.get(tick);
|
|
836
839
|
if (cachedText === undefined) {
|
|
837
|
-
cachedText = renderAnimatedText(
|
|
838
|
-
theme,
|
|
839
|
-
config,
|
|
840
|
-
colors,
|
|
841
|
-
rowCells,
|
|
842
|
-
width,
|
|
843
|
-
tick,
|
|
844
|
-
config.textAnimation,
|
|
845
|
-
);
|
|
840
|
+
cachedText = renderAnimatedText(render, rowCells, width, tick, config.textAnimation);
|
|
846
841
|
textRenderCache.set(tick, cachedText);
|
|
847
842
|
}
|
|
848
843
|
return cachedText;
|
|
@@ -856,7 +851,7 @@ function renderWorkingLineSchedule(
|
|
|
856
851
|
let cachedSpinner = spinnerRenderCache.get(index);
|
|
857
852
|
if (cachedSpinner === undefined) {
|
|
858
853
|
const glyph = spinner.frames[index] ?? spinner.frames[0];
|
|
859
|
-
cachedSpinner =
|
|
854
|
+
cachedSpinner = render("high", glyph);
|
|
860
855
|
spinnerRenderCache.set(index, cachedSpinner);
|
|
861
856
|
}
|
|
862
857
|
return cachedSpinner;
|
|
@@ -874,9 +869,7 @@ function renderWorkingLineSchedule(
|
|
|
874
869
|
spinner.frames[state.spinnerTick % spinner.frames.length] ?? spinner.frames[0];
|
|
875
870
|
const frame = config.animateSpinnerColor
|
|
876
871
|
? `${renderAnimatedText(
|
|
877
|
-
|
|
878
|
-
config,
|
|
879
|
-
colors,
|
|
872
|
+
render,
|
|
880
873
|
spinnerCells?.get(spinnerGlyph) ?? rowCells,
|
|
881
874
|
width,
|
|
882
875
|
state.textTick,
|
|
@@ -934,6 +927,7 @@ function buildPreparedWorkingLineFrames(
|
|
|
934
927
|
const textOrigin = config.animateSpinnerColor ? 0 : spinnerWidth + 1;
|
|
935
928
|
const textCycle = textPeriod(config.textAnimation, animatedTextWidth);
|
|
936
929
|
const textPhase = normalizedPhaseTick(textStartTick, textCycle);
|
|
930
|
+
const render = workingLineTierRenderer(theme, config, colors);
|
|
937
931
|
|
|
938
932
|
if (config.textAnimation === "disabled") {
|
|
939
933
|
const frameStates = Array.from({ length: spinner.frames.length }, (_, index) => ({
|
|
@@ -943,7 +937,7 @@ function buildPreparedWorkingLineFrames(
|
|
|
943
937
|
let codeUnits = 0;
|
|
944
938
|
const frames = frameStates.map((state) => {
|
|
945
939
|
const glyph = spinner.frames[state.spinnerTick % spinner.frames.length] ?? spinner.frames[0];
|
|
946
|
-
const frame = `${
|
|
940
|
+
const frame = `${render("mid", `${glyph} ${composed.row}`)}${SGR_RESET}`;
|
|
947
941
|
codeUnits += frame.length;
|
|
948
942
|
if (codeUnits > MAX_WORKING_LINE_FRAME_CODE_UNITS)
|
|
949
943
|
throw new Error("Working-line animation exceeds its memory cap");
|
|
@@ -985,8 +979,7 @@ function buildPreparedWorkingLineFrames(
|
|
|
985
979
|
const rendered = renderWorkingLineSchedule(
|
|
986
980
|
exact,
|
|
987
981
|
config,
|
|
988
|
-
|
|
989
|
-
theme,
|
|
982
|
+
render,
|
|
990
983
|
rowCells,
|
|
991
984
|
spinnerCells,
|
|
992
985
|
animatedTextWidth,
|
|
@@ -1029,8 +1022,7 @@ function buildPreparedWorkingLineFrames(
|
|
|
1029
1022
|
const rendered = renderWorkingLineSchedule(
|
|
1030
1023
|
fallback,
|
|
1031
1024
|
config,
|
|
1032
|
-
|
|
1033
|
-
theme,
|
|
1025
|
+
render,
|
|
1034
1026
|
rowCells,
|
|
1035
1027
|
spinnerCells,
|
|
1036
1028
|
animatedTextWidth,
|
|
@@ -1064,6 +1056,7 @@ export function buildWorkingLineSpinnerFrames(
|
|
|
1064
1056
|
): { frames: string[]; frameStates: WorkingLineFrameState[]; intervalMs: number } {
|
|
1065
1057
|
const spinner = WORKING_LINE_SPINNERS[config.spinner];
|
|
1066
1058
|
workingLineSpinnerWidth(config.spinner);
|
|
1059
|
+
const render = workingLineTierRenderer(theme, config, colors);
|
|
1067
1060
|
const phase = Number.isFinite(startTick) ? Math.max(0, Math.floor(startTick)) : 0;
|
|
1068
1061
|
const frameStates = Array.from({ length: spinner.frames.length }, (_, index) => ({
|
|
1069
1062
|
spinnerTick: phase + index,
|
|
@@ -1071,7 +1064,7 @@ export function buildWorkingLineSpinnerFrames(
|
|
|
1071
1064
|
}));
|
|
1072
1065
|
const frames = frameStates.map((state) => {
|
|
1073
1066
|
const glyph = spinner.frames[state.spinnerTick % spinner.frames.length] ?? spinner.frames[0];
|
|
1074
|
-
return `${
|
|
1067
|
+
return `${render("high", glyph)}${SGR_RESET}`;
|
|
1075
1068
|
});
|
|
1076
1069
|
return { frames, frameStates, intervalMs: config.spinnerIntervalMs };
|
|
1077
1070
|
}
|
package/package.json
CHANGED