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
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
formatUsernameHostLabel,
|
|
23
23
|
} from "../../shared/format.ts";
|
|
24
24
|
import { resolveRuntimeSymbol } from "../../shared/icons.ts";
|
|
25
|
-
import { renderStyleForSource } from "../../shared/style.ts";
|
|
25
|
+
import { renderSourceColor, renderStyleForSource } from "../../shared/style.ts";
|
|
26
26
|
import { sanitizeEditorMetadataText } from "../editor/editor-metadata-format.ts";
|
|
27
27
|
import {
|
|
28
28
|
collectExtensionStatusSegments,
|
|
@@ -322,10 +322,12 @@ export function installFooter(
|
|
|
322
322
|
separatorText[config.components.footer.styles.starship.separator],
|
|
323
323
|
);
|
|
324
324
|
const innerWidth = Math.max(1, width - 2);
|
|
325
|
-
const cwdLabel =
|
|
325
|
+
const cwdLabel = renderSourceColor(
|
|
326
326
|
theme,
|
|
327
327
|
colorSource,
|
|
328
328
|
config.colors.cwd,
|
|
329
|
+
"cwd",
|
|
330
|
+
"bold cyan",
|
|
329
331
|
formattedCwd,
|
|
330
332
|
);
|
|
331
333
|
const needsSessionName =
|
|
@@ -932,10 +934,12 @@ export function installFooter(
|
|
|
932
934
|
|
|
933
935
|
const chunkBudget = compactChunkBudget(innerWidth);
|
|
934
936
|
const compactCwdLabel = truncateToWidth(
|
|
935
|
-
|
|
937
|
+
renderSourceColor(
|
|
936
938
|
theme,
|
|
937
939
|
colorSource,
|
|
938
940
|
config.colors.cwd,
|
|
941
|
+
"cwd",
|
|
942
|
+
"bold cyan",
|
|
939
943
|
sanitizeEditorMetadataText(
|
|
940
944
|
formatCwdLabel(ctx.cwd, config.icons.cwd, {
|
|
941
945
|
mode: "basename",
|
|
@@ -10,6 +10,7 @@ import { isSafeSgrStylePrefix } from "../../shared/style.ts";
|
|
|
10
10
|
import { renderWorkingLineHigh } from "./working-line.ts";
|
|
11
11
|
|
|
12
12
|
export const TURN_SUMMARY_ENTRY_TYPE = "zentui-turn-summary";
|
|
13
|
+
export const MIN_OUTPUT_RATE_WINDOW_MS = 500;
|
|
13
14
|
const TURN_SUMMARY_VERSION = 3;
|
|
14
15
|
const SGR_RESET = "\x1b[0m";
|
|
15
16
|
|
|
@@ -18,6 +19,7 @@ export type LiveTokenDisplay = Readonly<{
|
|
|
18
19
|
input: number;
|
|
19
20
|
output: number;
|
|
20
21
|
outputApproximate: boolean;
|
|
22
|
+
outputTokensPerSecond?: number;
|
|
21
23
|
}>;
|
|
22
24
|
export type ThoughtSnapshot = Readonly<{ durationMs: number; active: boolean }>;
|
|
23
25
|
|
|
@@ -70,6 +72,8 @@ type ResponseState = {
|
|
|
70
72
|
outputAnchor: number;
|
|
71
73
|
outputAnchorCodePoints: number;
|
|
72
74
|
liveOutputFloor: number;
|
|
75
|
+
startedAt: number;
|
|
76
|
+
outputTokensPerSecond?: number;
|
|
73
77
|
lastDisplay?: LiveTokenDisplay;
|
|
74
78
|
estimateIncomplete: boolean;
|
|
75
79
|
estimateEnabled: boolean;
|
|
@@ -515,7 +519,7 @@ export class InteractionMetricsTracker {
|
|
|
515
519
|
this.commitSnapshot(run, run.response);
|
|
516
520
|
this.closeResponse(run, run.response, now);
|
|
517
521
|
}
|
|
518
|
-
this.openResponse(run);
|
|
522
|
+
this.openResponse(run, now);
|
|
519
523
|
}
|
|
520
524
|
|
|
521
525
|
messageUpdate(
|
|
@@ -549,7 +553,7 @@ export class InteractionMetricsTracker {
|
|
|
549
553
|
contentChanged = this.applyContentEvent(response, validated);
|
|
550
554
|
}
|
|
551
555
|
const usageChanged = tokens ? this.applyLiveUsage(response, tokens) : false;
|
|
552
|
-
const display = this.liveDisplay(response);
|
|
556
|
+
const display = this.liveDisplay(response, now);
|
|
553
557
|
const displayChanged = !this.sameDisplay(response.lastDisplay, display);
|
|
554
558
|
response.lastDisplay = display;
|
|
555
559
|
return {
|
|
@@ -570,20 +574,31 @@ export class InteractionMetricsTracker {
|
|
|
570
574
|
if (response.responseIdKey && key && response.responseIdKey !== key)
|
|
571
575
|
return { status: "rejected" };
|
|
572
576
|
if (!response.responseIdKey && key) response.responseIdKey = key;
|
|
573
|
-
const preCloseDisplay = this.liveDisplay(response);
|
|
574
577
|
let source: "final" | "last-snapshot" | "no-snapshot";
|
|
575
578
|
if (tokens) {
|
|
576
579
|
response.snapshot = tokens;
|
|
577
580
|
response.hasSnapshot = true;
|
|
581
|
+
this.updateOutputRate(response, tokens.output, now);
|
|
578
582
|
source = "final";
|
|
579
583
|
} else source = response.hasSnapshot ? "last-snapshot" : "no-snapshot";
|
|
584
|
+
const preCloseDisplay = this.liveDisplay(
|
|
585
|
+
response,
|
|
586
|
+
tokens ? undefined : now,
|
|
587
|
+
);
|
|
580
588
|
this.commitSnapshot(run, response);
|
|
581
589
|
const committed = this.totalTokens();
|
|
582
590
|
const preserveEstimate =
|
|
583
591
|
source !== "final" && response.lastDisplay?.outputApproximate === true;
|
|
592
|
+
const outputRate = preCloseDisplay.outputTokensPerSecond;
|
|
584
593
|
const displayTokens = preserveEstimate
|
|
585
594
|
? { ...preCloseDisplay, outputApproximate: true }
|
|
586
|
-
: {
|
|
595
|
+
: {
|
|
596
|
+
...committed,
|
|
597
|
+
outputApproximate: false,
|
|
598
|
+
...(outputRate === undefined
|
|
599
|
+
? {}
|
|
600
|
+
: { outputTokensPerSecond: outputRate }),
|
|
601
|
+
};
|
|
587
602
|
run.lastClosedDisplay = preserveEstimate ? displayTokens : undefined;
|
|
588
603
|
this.closeResponse(run, response, now);
|
|
589
604
|
return { status: "accepted", tokens: committed, displayTokens, source };
|
|
@@ -770,7 +785,11 @@ export class InteractionMetricsTracker {
|
|
|
770
785
|
);
|
|
771
786
|
}
|
|
772
787
|
|
|
773
|
-
private openResponse(
|
|
788
|
+
private openResponse(
|
|
789
|
+
run: RunState,
|
|
790
|
+
now: number,
|
|
791
|
+
key?: string,
|
|
792
|
+
): ResponseState {
|
|
774
793
|
run.responseOrdinal = safeAdd(run.responseOrdinal, 1);
|
|
775
794
|
run.lastClosedDisplay = undefined;
|
|
776
795
|
const estimateEnabled =
|
|
@@ -787,6 +806,7 @@ export class InteractionMetricsTracker {
|
|
|
787
806
|
outputAnchor: 0,
|
|
788
807
|
outputAnchorCodePoints: 0,
|
|
789
808
|
liveOutputFloor: 0,
|
|
809
|
+
startedAt: safeTime(now),
|
|
790
810
|
estimateIncomplete: !estimateEnabled,
|
|
791
811
|
estimateEnabled,
|
|
792
812
|
};
|
|
@@ -996,7 +1016,7 @@ export class InteractionMetricsTracker {
|
|
|
996
1016
|
return true;
|
|
997
1017
|
}
|
|
998
1018
|
|
|
999
|
-
private liveDisplay(response: ResponseState): LiveTokenDisplay {
|
|
1019
|
+
private liveDisplay(response: ResponseState, now?: number): LiveTokenDisplay {
|
|
1000
1020
|
const committed = this.committedTokens();
|
|
1001
1021
|
const postAnchorCodePoints = Math.max(
|
|
1002
1022
|
0,
|
|
@@ -1013,6 +1033,8 @@ export class InteractionMetricsTracker {
|
|
|
1013
1033
|
responseOutput === response.outputAnchor &&
|
|
1014
1034
|
!response.estimateIncomplete;
|
|
1015
1035
|
response.liveOutputFloor = responseOutput;
|
|
1036
|
+
if (now !== undefined) this.updateOutputRate(response, responseOutput, now);
|
|
1037
|
+
const outputRate = response.outputTokensPerSecond;
|
|
1016
1038
|
return {
|
|
1017
1039
|
input: safeAdd(
|
|
1018
1040
|
committed.input,
|
|
@@ -1020,9 +1042,26 @@ export class InteractionMetricsTracker {
|
|
|
1020
1042
|
),
|
|
1021
1043
|
output: safeAdd(committed.output, responseOutput),
|
|
1022
1044
|
outputApproximate: !exactCoverage,
|
|
1045
|
+
...(outputRate === undefined
|
|
1046
|
+
? {}
|
|
1047
|
+
: { outputTokensPerSecond: outputRate }),
|
|
1023
1048
|
};
|
|
1024
1049
|
}
|
|
1025
1050
|
|
|
1051
|
+
private updateOutputRate(
|
|
1052
|
+
response: ResponseState,
|
|
1053
|
+
responseOutput: number,
|
|
1054
|
+
now: number,
|
|
1055
|
+
): void {
|
|
1056
|
+
const elapsedMs = durationMs(response.startedAt, now);
|
|
1057
|
+
const rate =
|
|
1058
|
+
elapsedMs >= MIN_OUTPUT_RATE_WINDOW_MS && responseOutput > 0
|
|
1059
|
+
? Math.min(Number.MAX_SAFE_INTEGER, (responseOutput * 1000) / elapsedMs)
|
|
1060
|
+
: undefined;
|
|
1061
|
+
response.outputTokensPerSecond =
|
|
1062
|
+
rate !== undefined && rate >= 0.1 ? rate : undefined;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1026
1065
|
private runTokens(run: RunState): InteractionTokens {
|
|
1027
1066
|
return run.response?.hasSnapshot
|
|
1028
1067
|
? addTokens(run.committed, run.response.snapshot)
|
|
@@ -1104,7 +1143,8 @@ export class InteractionMetricsTracker {
|
|
|
1104
1143
|
return (
|
|
1105
1144
|
left?.input === right?.input &&
|
|
1106
1145
|
left?.output === right?.output &&
|
|
1107
|
-
left?.outputApproximate === right?.outputApproximate
|
|
1146
|
+
left?.outputApproximate === right?.outputApproximate &&
|
|
1147
|
+
left?.outputTokensPerSecond === right?.outputTokensPerSecond
|
|
1108
1148
|
);
|
|
1109
1149
|
}
|
|
1110
1150
|
}
|
|
@@ -550,7 +550,12 @@ export type WorkingLineRuntimeSegments = {
|
|
|
550
550
|
tool?: string;
|
|
551
551
|
elapsedMs?: number;
|
|
552
552
|
thought?: { durationMs: number; active: boolean };
|
|
553
|
-
tokens?: {
|
|
553
|
+
tokens?: {
|
|
554
|
+
input: number;
|
|
555
|
+
output: number;
|
|
556
|
+
outputApproximate?: boolean;
|
|
557
|
+
outputTokensPerSecond?: number;
|
|
558
|
+
};
|
|
554
559
|
};
|
|
555
560
|
|
|
556
561
|
export type WorkingLineFrameState = {
|
|
@@ -617,11 +622,18 @@ export function formatWorkingLineTokens(
|
|
|
617
622
|
tokens.input < 0 ||
|
|
618
623
|
tokens.output < 0 ||
|
|
619
624
|
(tokens.outputApproximate !== undefined &&
|
|
620
|
-
typeof tokens.outputApproximate !== "boolean")
|
|
625
|
+
typeof tokens.outputApproximate !== "boolean") ||
|
|
626
|
+
(tokens.outputTokensPerSecond !== undefined &&
|
|
627
|
+
(!Number.isFinite(tokens.outputTokensPerSecond) ||
|
|
628
|
+
tokens.outputTokensPerSecond < 0))
|
|
621
629
|
) {
|
|
622
630
|
return undefined;
|
|
623
631
|
}
|
|
624
|
-
|
|
632
|
+
const outputRate =
|
|
633
|
+
tokens.outputTokensPerSecond && tokens.outputTokensPerSecond > 0
|
|
634
|
+
? ` ⚡${formatCount(Math.round(tokens.outputTokensPerSecond * 10) / 10)} tok/s`
|
|
635
|
+
: "";
|
|
636
|
+
return `↑${formatCount(tokens.input)} ↓${formatCount(tokens.output)}${outputRate}`;
|
|
625
637
|
}
|
|
626
638
|
|
|
627
639
|
function truncateWithEllipsis(value: string, capacity: number): string {
|
|
@@ -1174,6 +1186,15 @@ export class WorkingLineController {
|
|
|
1174
1186
|
startTurn(ctx: WorkingLineContext): WorkingLineReconcileResult {
|
|
1175
1187
|
const config = this.getConfig().components.workingLine;
|
|
1176
1188
|
this.activeTools.clear();
|
|
1189
|
+
if (this.tokens?.outputTokensPerSecond !== undefined) {
|
|
1190
|
+
this.tokens = {
|
|
1191
|
+
input: this.tokens.input,
|
|
1192
|
+
output: this.tokens.output,
|
|
1193
|
+
...(this.tokens.outputApproximate === undefined
|
|
1194
|
+
? {}
|
|
1195
|
+
: { outputApproximate: this.tokens.outputApproximate }),
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1177
1198
|
const selectedMessage = selectWorkingLineMessage(config, this.random);
|
|
1178
1199
|
if (!config.enabled) {
|
|
1179
1200
|
this.selectedMessage = selectedMessage;
|
|
@@ -23,7 +23,6 @@ export type IconGlyphs = {
|
|
|
23
23
|
deleted: string;
|
|
24
24
|
typechanged: string;
|
|
25
25
|
cacheHit: string;
|
|
26
|
-
editorPrompt: string;
|
|
27
26
|
rail: string;
|
|
28
27
|
username: string;
|
|
29
28
|
time: string;
|
|
@@ -48,7 +47,6 @@ export const ICON_GLYPH_KEYS = [
|
|
|
48
47
|
"deleted",
|
|
49
48
|
"typechanged",
|
|
50
49
|
"cacheHit",
|
|
51
|
-
"editorPrompt",
|
|
52
50
|
"rail",
|
|
53
51
|
"username",
|
|
54
52
|
"time",
|
|
@@ -79,7 +77,6 @@ export const NERD_DEFAULT_ICONS: IconGlyphs = {
|
|
|
79
77
|
deleted: "✘",
|
|
80
78
|
typechanged: "T",
|
|
81
79
|
cacheHit: "",
|
|
82
|
-
editorPrompt: "",
|
|
83
80
|
rail: "│",
|
|
84
81
|
username: "",
|
|
85
82
|
time: "",
|
|
@@ -103,7 +100,6 @@ export const ASCII_DEFAULT_ICONS: IconGlyphs = {
|
|
|
103
100
|
deleted: "x",
|
|
104
101
|
typechanged: "T",
|
|
105
102
|
cacheHit: "c",
|
|
106
|
-
editorPrompt: "",
|
|
107
103
|
rail: "|",
|
|
108
104
|
username: "@",
|
|
109
105
|
time: "t",
|
|
@@ -427,6 +427,54 @@ export function renderStyleForSourceOrFallback(
|
|
|
427
427
|
return renderStyleForSource(theme, source, style ?? fallbackStyle, text);
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
+
/**
|
|
431
|
+
* 主题画板存在性探测:pi Theme 暴露 fgColors Map;测试或其他实现可
|
|
432
|
+
* 提供 hasThemeToken 回调,缺失时保守按“无该 token”处理。
|
|
433
|
+
*/
|
|
434
|
+
function themeHasToken(theme: ThemeLike, token: string): boolean {
|
|
435
|
+
if (
|
|
436
|
+
typeof (theme as { hasThemeToken?: (t: string) => boolean })
|
|
437
|
+
.hasThemeToken === "function"
|
|
438
|
+
) {
|
|
439
|
+
return (
|
|
440
|
+
theme as unknown as { hasThemeToken(t: string): boolean }
|
|
441
|
+
).hasThemeToken(token);
|
|
442
|
+
}
|
|
443
|
+
const fgColors = (theme as unknown as { fgColors?: Map<string, string> })
|
|
444
|
+
.fgColors;
|
|
445
|
+
return fgColors instanceof Map && fgColors.has(token);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 颜色统一渲染入口(配置优先,其次主题 token,最后原生默认):
|
|
450
|
+
* - 配置了 spec:按 colorSource 现有语义解释(theme=token/hex,terminal=终端色);
|
|
451
|
+
* - 未配置且 theme 源:主题定义了 themeToken 时用主题色(可指向 vars 变量),
|
|
452
|
+
* 否则回落原生默认 themeDefault;
|
|
453
|
+
* - 未配置且 terminal 源:使用固定终端默认 terminalDefault。
|
|
454
|
+
*/
|
|
455
|
+
export function renderSourceColor(
|
|
456
|
+
theme: ThemeLike,
|
|
457
|
+
source: ColorSource,
|
|
458
|
+
spec: ColorSpec | undefined,
|
|
459
|
+
themeToken: string,
|
|
460
|
+
themeDefault: ColorSpec,
|
|
461
|
+
text: string,
|
|
462
|
+
): string {
|
|
463
|
+
if (typeof spec === "string" && spec.trim() !== "") {
|
|
464
|
+
return renderStyleForSource(theme, source, spec, text);
|
|
465
|
+
}
|
|
466
|
+
// 未配置时:theme 源优先主题专有 token,terminal 源与主题缺 token 时
|
|
467
|
+
// 都回落 Pi 原生默认(主题语义色),保证两种 colorSource 默认观感一致。
|
|
468
|
+
if (source !== "terminal" && themeHasToken(theme, themeToken)) {
|
|
469
|
+
try {
|
|
470
|
+
return theme.fg(themeToken, text);
|
|
471
|
+
} catch {
|
|
472
|
+
// 主题色解析失败时继续回退原生默认。
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return renderThemeStyle(theme, themeDefault, text);
|
|
476
|
+
}
|
|
477
|
+
|
|
430
478
|
export function renderStyleForSourceOrFallbackStrict(
|
|
431
479
|
theme: ThemeLike,
|
|
432
480
|
source: ColorSource,
|
|
@@ -52,6 +52,11 @@ export const TOOL_EXPANDED_BACKGROUND_PATCH = Symbol.for(
|
|
|
52
52
|
"pi.ccstyle.tool-expanded-background-patch",
|
|
53
53
|
);
|
|
54
54
|
|
|
55
|
+
// ── 工具 updateDisplay 跨 toggle 重建缓存 ──
|
|
56
|
+
export const TOGGLE_RENDER_CACHE_PATCH = Symbol.for(
|
|
57
|
+
"pi.ccstyle.toggle-render-cache-patch",
|
|
58
|
+
);
|
|
59
|
+
|
|
55
60
|
// ── 消息组件补丁 ──
|
|
56
61
|
export const MESSAGE_DISPLAY_PATCH = Symbol.for(
|
|
57
62
|
"pi.ccstyle.message-display-patch",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-one-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A single, customizable Pi UI package with layout-oriented Context and Shell rendering for Pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -58,8 +58,7 @@
|
|
|
58
58
|
"themes": [
|
|
59
59
|
"./themes/cc-dark.json",
|
|
60
60
|
"./themes/cc-light.json"
|
|
61
|
-
]
|
|
62
|
-
"image": "https://raw.githubusercontent.com/minuque/pi-cc-extensions/main/assets/readme/preview.png"
|
|
61
|
+
]
|
|
63
62
|
},
|
|
64
63
|
"devDependencies": {
|
|
65
64
|
"@biomejs/biome": "^2.5.5",
|
package/themes/cc-dark.json
CHANGED
package/themes/cc-light.json
CHANGED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import type { ZentuiConfig } from "../../app/config/shell.ts";
|
|
4
|
-
import {
|
|
5
|
-
renderStyleForSourceOrFallback,
|
|
6
|
-
type SourceStyleFallback,
|
|
7
|
-
} from "../../shared/style.ts";
|
|
8
|
-
import {
|
|
9
|
-
applyOwnedLayoutBackground,
|
|
10
|
-
fillTerminalLine,
|
|
11
|
-
renderCompletionRows,
|
|
12
|
-
} from "./completion-menu.ts";
|
|
13
|
-
import { sanitizeEditorMetadataText } from "./editor-metadata-format.ts";
|
|
14
|
-
|
|
15
|
-
export const ACCENT_RAIL_CHROME_WIDTH = 2;
|
|
16
|
-
|
|
17
|
-
const ACCENT_RAIL_FALLBACK: SourceStyleFallback = {
|
|
18
|
-
theme: "syntaxNumber",
|
|
19
|
-
terminal: "fg:215",
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
export type AccentRailViewport = {
|
|
23
|
-
above?: string;
|
|
24
|
-
below?: string;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export type AccentRailEditorFrameOptions = {
|
|
28
|
-
width: number;
|
|
29
|
-
editorLines: string[];
|
|
30
|
-
autocompleteLines?: string[];
|
|
31
|
-
viewport?: AccentRailViewport;
|
|
32
|
-
uiTheme: Theme;
|
|
33
|
-
config: ZentuiConfig;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
function clampLines(lines: string[], width: number): string[] {
|
|
37
|
-
const maxWidth = Math.max(0, width);
|
|
38
|
-
return lines.map((line) => truncateToWidth(line, maxWidth, ""));
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function selectedRail(config: ZentuiConfig): string {
|
|
42
|
-
const style = config.components.editor.styles["accent-rail"];
|
|
43
|
-
const fallback = config.icons.mode === "ascii" ? "|" : "▎";
|
|
44
|
-
const configured =
|
|
45
|
-
config.icons.mode === "ascii" ? style.asciiRail : style.rail;
|
|
46
|
-
const sanitized = sanitizeEditorMetadataText(configured);
|
|
47
|
-
const glyph = truncateToWidth(sanitized, 1, "");
|
|
48
|
-
return visibleWidth(glyph) === 1 ? glyph : fallback;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function viewportLabel(
|
|
52
|
-
direction: "above" | "below",
|
|
53
|
-
count: string | undefined,
|
|
54
|
-
): string | undefined {
|
|
55
|
-
if (!count || !/^[1-9]\d*$/.test(count)) return undefined;
|
|
56
|
-
return `${direction === "above" ? "↑" : "↓"} ${count} more`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Pure compact accent-rail composition shared by live rendering and settings previews. */
|
|
60
|
-
export function renderAccentRailEditorFrame({
|
|
61
|
-
width,
|
|
62
|
-
editorLines,
|
|
63
|
-
autocompleteLines = [],
|
|
64
|
-
viewport = {},
|
|
65
|
-
uiTheme,
|
|
66
|
-
config,
|
|
67
|
-
}: AccentRailEditorFrameOptions): string[] {
|
|
68
|
-
if (width < ACCENT_RAIL_CHROME_WIDTH + 1) {
|
|
69
|
-
return clampLines([...editorLines, ...autocompleteLines], width);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const contentWidth = width - ACCENT_RAIL_CHROME_WIDTH;
|
|
73
|
-
const rail = renderStyleForSourceOrFallback(
|
|
74
|
-
uiTheme,
|
|
75
|
-
config.components.editor.colorSource,
|
|
76
|
-
config.colors.editorRail,
|
|
77
|
-
ACCENT_RAIL_FALLBACK,
|
|
78
|
-
selectedRail(config),
|
|
79
|
-
);
|
|
80
|
-
const above = config.components.editor.viewportIndicators
|
|
81
|
-
? viewportLabel("above", viewport.above)
|
|
82
|
-
: undefined;
|
|
83
|
-
const below = config.components.editor.viewportIndicators
|
|
84
|
-
? viewportLabel("below", viewport.below)
|
|
85
|
-
: undefined;
|
|
86
|
-
const rows = [
|
|
87
|
-
...(above ? [above] : []),
|
|
88
|
-
...editorLines,
|
|
89
|
-
...(below ? [below] : []),
|
|
90
|
-
];
|
|
91
|
-
const transparent =
|
|
92
|
-
config.components.editor.styles["accent-rail"].transparent;
|
|
93
|
-
const layout = rows.map((line) => {
|
|
94
|
-
const railCell = transparent
|
|
95
|
-
? rail
|
|
96
|
-
: applyOwnedLayoutBackground(uiTheme, rail);
|
|
97
|
-
const bodyText = ` ${fillTerminalLine(line, contentWidth)}`;
|
|
98
|
-
const body = transparent
|
|
99
|
-
? bodyText
|
|
100
|
-
: applyOwnedLayoutBackground(uiTheme, bodyText);
|
|
101
|
-
return `${railCell}${body}`;
|
|
102
|
-
});
|
|
103
|
-
const autocompleteLayout = renderCompletionRows({
|
|
104
|
-
lines: autocompleteLines,
|
|
105
|
-
width,
|
|
106
|
-
theme: uiTheme,
|
|
107
|
-
selectedPrefix: `${rail} `,
|
|
108
|
-
ownedBackground: !transparent,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
return clampLines([...layout, ...autocompleteLayout], width);
|
|
112
|
-
}
|