pi-one-ui 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/extensions/app/config/renderer.ts +452 -0
  4. package/extensions/app/config/shell.ts +2484 -0
  5. package/extensions/app/ownership.ts +18 -0
  6. package/extensions/app/panel.ts +533 -0
  7. package/extensions/app/presets.ts +36 -0
  8. package/extensions/features/agent-summary/core.ts +194 -0
  9. package/extensions/features/agent-summary/index.ts +37 -0
  10. package/extensions/features/compact-thinking.ts +1262 -0
  11. package/extensions/features/context.ts +857 -0
  12. package/extensions/features/reference/index.ts +643 -0
  13. package/extensions/features/reference/session.ts +278 -0
  14. package/extensions/features/reference/subagent.ts +218 -0
  15. package/extensions/features/shell/aliases.ts +28 -0
  16. package/extensions/features/shell/flush-docked-bash.ts +39 -0
  17. package/extensions/features/shell/startup-header.ts +357 -0
  18. package/extensions/features/shell/working-message.ts +226 -0
  19. package/extensions/index.ts +52 -0
  20. package/extensions/shell/accent-rail-editor.ts +112 -0
  21. package/extensions/shell/accent-rail-layout-patch.ts +530 -0
  22. package/extensions/shell/completion-menu.ts +186 -0
  23. package/extensions/shell/editor-metadata-format.ts +287 -0
  24. package/extensions/shell/editor-transfer.ts +73 -0
  25. package/extensions/shell/extension-status.ts +102 -0
  26. package/extensions/shell/footer-format.ts +388 -0
  27. package/extensions/shell/footer-layout.ts +144 -0
  28. package/extensions/shell/footer.ts +1035 -0
  29. package/extensions/shell/format.ts +577 -0
  30. package/extensions/shell/git.ts +434 -0
  31. package/extensions/shell/icons.ts +261 -0
  32. package/extensions/shell/index.ts +1659 -0
  33. package/extensions/shell/interaction-summary.ts +1194 -0
  34. package/extensions/shell/live-context.ts +82 -0
  35. package/extensions/shell/minimalist-editor.ts +548 -0
  36. package/extensions/shell/package-version.ts +675 -0
  37. package/extensions/shell/project-refresh.ts +130 -0
  38. package/extensions/shell/project-state.ts +60 -0
  39. package/extensions/shell/prototype-patch-registry.ts +183 -0
  40. package/extensions/shell/runtime.ts +915 -0
  41. package/extensions/shell/selector-border.ts +109 -0
  42. package/extensions/shell/session-lifecycle.ts +60 -0
  43. package/extensions/shell/settings-command.ts +2144 -0
  44. package/extensions/shell/settings-previews.ts +137 -0
  45. package/extensions/shell/state.ts +83 -0
  46. package/extensions/shell/style.ts +480 -0
  47. package/extensions/shell/telemetry.ts +90 -0
  48. package/extensions/shell/ui.ts +1326 -0
  49. package/extensions/shell/user-message-osc.ts +279 -0
  50. package/extensions/shell/user-message-styles.ts +266 -0
  51. package/extensions/shell/user-message.ts +226 -0
  52. package/extensions/shell/working-line-messages.ts +24 -0
  53. package/extensions/shell/working-line-spinners.ts +54 -0
  54. package/extensions/shell/working-line.ts +1603 -0
  55. package/extensions/tools/ansi-color.ts +44 -0
  56. package/extensions/tools/ansi-text.ts +52 -0
  57. package/extensions/tools/component-tree.ts +37 -0
  58. package/extensions/tools/format.ts +28 -0
  59. package/extensions/tools/fullscreen-detect.ts +11 -0
  60. package/extensions/tools/patch-keys.ts +158 -0
  61. package/extensions/tools/sgr-mouse.ts +59 -0
  62. package/extensions/tools/tool-loading-icon.ts +20 -0
  63. package/extensions/tools/tool-result-sanitize.ts +27 -0
  64. package/extensions/transcript/index.ts +65 -0
  65. package/extensions/transcript/renderer/compact-mode.ts +1443 -0
  66. package/extensions/transcript/renderer/default-mode.ts +735 -0
  67. package/extensions/transcript/renderer/index.ts +286 -0
  68. package/extensions/transcript/renderer/markdown-enhance.ts +325 -0
  69. package/extensions/transcript/renderer/mouse/hover.ts +175 -0
  70. package/extensions/transcript/renderer/mouse/interaction.ts +1016 -0
  71. package/extensions/transcript/renderer/mouse/layout.ts +207 -0
  72. package/extensions/transcript/renderer/mouse/packets.ts +96 -0
  73. package/extensions/transcript/renderer/mouse/scroll.ts +211 -0
  74. package/extensions/transcript/renderer/tool/diff/ATTRIBUTION.md +28 -0
  75. package/extensions/transcript/renderer/tool/diff/ansi-utils.ts +128 -0
  76. package/extensions/transcript/renderer/tool/diff/diff-component.ts +112 -0
  77. package/extensions/transcript/renderer/tool/diff/diff-edit-render.ts +255 -0
  78. package/extensions/transcript/renderer/tool/diff/diff-header.ts +184 -0
  79. package/extensions/transcript/renderer/tool/diff/diff-highlight.ts +131 -0
  80. package/extensions/transcript/renderer/tool/diff/diff-inline.ts +350 -0
  81. package/extensions/transcript/renderer/tool/diff/diff-layout.ts +870 -0
  82. package/extensions/transcript/renderer/tool/diff/diff-limits.ts +189 -0
  83. package/extensions/transcript/renderer/tool/diff/diff-palette.ts +436 -0
  84. package/extensions/transcript/renderer/tool/diff/diff-parse.ts +536 -0
  85. package/extensions/transcript/renderer/tool/diff/diff-presentation.ts +79 -0
  86. package/extensions/transcript/renderer/tool/diff/diff-renderer.ts +29 -0
  87. package/extensions/transcript/renderer/tool/diff/diff-text.ts +57 -0
  88. package/extensions/transcript/renderer/tool/diff/diff-write-render.ts +629 -0
  89. package/extensions/transcript/renderer/tool/diff/index.ts +147 -0
  90. package/extensions/transcript/renderer/tool/diff/line-width-safety.ts +100 -0
  91. package/extensions/transcript/renderer/tool/diff/shiki-highlight.ts +104 -0
  92. package/extensions/transcript/renderer/tool/diff/write-execution.ts +124 -0
  93. package/extensions/transcript/renderer/tool/grouping.ts +619 -0
  94. package/extensions/transcript/renderer/tool/message-display.ts +183 -0
  95. package/extensions/transcript/renderer/tool/names.ts +195 -0
  96. package/extensions/transcript/renderer/tool/result.ts +842 -0
  97. package/extensions/transcript/renderer/tool/show-more-hint.ts +42 -0
  98. package/extensions/transcript/renderer/transcript-refresh.ts +79 -0
  99. package/package.json +58 -0
  100. package/themes/cc-dark.json +91 -0
  101. package/themes/cc-light.json +91 -0
@@ -0,0 +1,82 @@
1
+ import type { AssistantMessage, Usage } from "@earendil-works/pi-ai";
2
+ import type { SessionLifecycle } from "./session-lifecycle.ts";
3
+
4
+ export type LiveContextOverride = {
5
+ tokens: number;
6
+ };
7
+
8
+ function usageComponent(value: unknown): number {
9
+ return typeof value === "number" && Number.isFinite(value) && value > 0
10
+ ? value
11
+ : 0;
12
+ }
13
+
14
+ export function calculateLiveContextTokens(
15
+ usage: Usage | undefined,
16
+ ): number | undefined {
17
+ if (!usage) return undefined;
18
+ const totalTokens = usageComponent(usage.totalTokens);
19
+ if (totalTokens > 0) return totalTokens;
20
+ const calculated =
21
+ usageComponent(usage.input) +
22
+ usageComponent(usage.output) +
23
+ usageComponent(usage.cacheRead) +
24
+ usageComponent(usage.cacheWrite);
25
+ return calculated > 0 ? calculated : undefined;
26
+ }
27
+
28
+ export function liveContextFromMessage(
29
+ message: unknown,
30
+ ): LiveContextOverride | undefined {
31
+ if (!message || typeof message !== "object") return undefined;
32
+ const assistant = message as Partial<AssistantMessage>;
33
+ if (assistant.role !== "assistant") return undefined;
34
+ if (assistant.stopReason === "error" || assistant.stopReason === "aborted")
35
+ return undefined;
36
+ const tokens = calculateLiveContextTokens(assistant.usage);
37
+ return tokens === undefined ? undefined : { tokens };
38
+ }
39
+
40
+ export class LiveContextController {
41
+ private readonly lifecycle: SessionLifecycle;
42
+ private readonly requestRender: () => void;
43
+ private override: LiveContextOverride | undefined;
44
+ private cancelScheduledRender: (() => void) | undefined;
45
+ private scheduledGeneration: number | undefined;
46
+
47
+ constructor(lifecycle: SessionLifecycle, requestRender: () => void) {
48
+ this.lifecycle = lifecycle;
49
+ this.requestRender = requestRender;
50
+ }
51
+
52
+ get(): LiveContextOverride | undefined {
53
+ return this.override;
54
+ }
55
+
56
+ update(message: unknown): boolean {
57
+ const next = liveContextFromMessage(message);
58
+ if (!next || !this.lifecycle.isCurrent()) return false;
59
+ this.override = next;
60
+ const generation = this.lifecycle.currentGeneration();
61
+ if (this.cancelScheduledRender && this.scheduledGeneration !== generation) {
62
+ this.cancelScheduledRender = undefined;
63
+ this.scheduledGeneration = undefined;
64
+ }
65
+ if (!this.cancelScheduledRender) {
66
+ this.scheduledGeneration = generation;
67
+ this.cancelScheduledRender = this.lifecycle.defer(() => {
68
+ this.cancelScheduledRender = undefined;
69
+ this.scheduledGeneration = undefined;
70
+ if (this.override) this.requestRender();
71
+ }, 250);
72
+ }
73
+ return true;
74
+ }
75
+
76
+ clear(): void {
77
+ this.override = undefined;
78
+ this.cancelScheduledRender?.();
79
+ this.cancelScheduledRender = undefined;
80
+ this.scheduledGeneration = undefined;
81
+ }
82
+ }
@@ -0,0 +1,548 @@
1
+ import { basename, isAbsolute, relative, sep } from "node:path";
2
+ import type { Theme } from "@earendil-works/pi-coding-agent";
3
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
4
+ import type { ZentuiConfig } from "../app/config/shell.ts";
5
+ import { sanitizeEditorMetadataText } from "./editor-metadata-format.ts";
6
+ import {
7
+ buildContextGauge,
8
+ contextColorTier,
9
+ formatCount,
10
+ formatCwdLabel,
11
+ formatElapsedDuration,
12
+ } from "./format.ts";
13
+
14
+ export { formatElapsedDuration } from "./format.ts";
15
+
16
+ import {
17
+ EDITOR_ACCENT_FALLBACK,
18
+ EDITOR_BORDER_FALLBACK,
19
+ renderStyleForSource,
20
+ renderStyleForSourceOrFallback,
21
+ safeThemeFg,
22
+ } from "./style.ts";
23
+
24
+ const MINIMALIST_MODEL_FALLBACK = {
25
+ theme: "syntaxKeyword",
26
+ terminal: "bold purple",
27
+ };
28
+ const MINIMALIST_THINKING_FALLBACK = {
29
+ theme: "warning",
30
+ terminal: "bold yellow",
31
+ };
32
+ const MINIMALIST_ADAPTIVE_TERMINAL_THINKING_FALLBACKS: Record<string, string> =
33
+ {
34
+ minimal: "bright-black",
35
+ low: "blue",
36
+ medium: "cyan",
37
+ high: "yellow",
38
+ xhigh: "red",
39
+ max: "bright-red",
40
+ };
41
+ const MINIMALIST_BRANCH_FALLBACK = {
42
+ theme: "bold syntaxKeyword",
43
+ terminal: "bold blue",
44
+ };
45
+
46
+ export type MinimalistEditorMetadata = {
47
+ cwd: string;
48
+ projectRoot?: string;
49
+ branch?: string;
50
+ dirty?: boolean;
51
+ ahead?: number;
52
+ behind?: number;
53
+ costLabel?: string;
54
+ modelLabel?: string;
55
+ thinkingLevel?: string;
56
+ contextPercent?: number;
57
+ contextWindow?: number;
58
+ sessionName?: string;
59
+ agentDurationMs?: number;
60
+ agentActive?: boolean;
61
+ };
62
+
63
+ export type MinimalistFrameOptions = {
64
+ width: number;
65
+ editorLines: string[];
66
+ autocompleteLines?: string[];
67
+ viewport?: {
68
+ above?: string;
69
+ below?: string;
70
+ };
71
+ inputText: string;
72
+ metadata: MinimalistEditorMetadata;
73
+ uiTheme: Theme;
74
+ config: ZentuiConfig;
75
+ borderColor?: (text: string) => string;
76
+ };
77
+
78
+ function fillLine(content: string, width: number): string {
79
+ const truncated = truncateToWidth(content, Math.max(0, width), "");
80
+ return `${truncated}${" ".repeat(Math.max(0, width - visibleWidth(truncated)))}`;
81
+ }
82
+
83
+ function clampLines(lines: string[], width: number): string[] {
84
+ return lines.map((line) => truncateToWidth(line, Math.max(0, width), ""));
85
+ }
86
+
87
+ export function renderFramedAutocompleteRows({
88
+ width,
89
+ lines,
90
+ renderBorder,
91
+ }: {
92
+ width: number;
93
+ lines: string[];
94
+ renderBorder: (text: string) => string;
95
+ }): string[] {
96
+ if (width <= 4 || lines.length === 0) return clampLines(lines, width);
97
+ const contentWidth = width - 4;
98
+ return clampLines(
99
+ [
100
+ `${renderBorder("├")}${renderBorder("─".repeat(width - 2))}${renderBorder("┤")}`,
101
+ ...lines.map(
102
+ (line) =>
103
+ `${renderBorder("│")} ${fillLine(line, contentWidth)} ${renderBorder("│")}`,
104
+ ),
105
+ ],
106
+ width,
107
+ );
108
+ }
109
+
110
+ function joinStyled(parts: string[], separator: string): string {
111
+ return parts.filter(Boolean).join(separator);
112
+ }
113
+
114
+ function thinkingStyle(
115
+ config: ZentuiConfig,
116
+ level: string,
117
+ ): string | undefined {
118
+ switch (level.toLowerCase()) {
119
+ case "minimal":
120
+ return (
121
+ config.colors.editorThinkingMinimal ?? config.colors.editorThinking
122
+ );
123
+ case "low":
124
+ return config.colors.editorThinkingLow ?? config.colors.editorThinking;
125
+ case "medium":
126
+ return config.colors.editorThinkingMedium ?? config.colors.editorThinking;
127
+ case "high":
128
+ return config.colors.editorThinkingHigh ?? config.colors.editorThinking;
129
+ case "xhigh":
130
+ return config.colors.editorThinkingXhigh ?? config.colors.editorThinking;
131
+ case "max":
132
+ return (
133
+ config.colors.editorThinkingMax ??
134
+ config.colors.editorThinkingXhigh ??
135
+ config.colors.editorThinking
136
+ );
137
+ default:
138
+ return config.colors.editorThinking;
139
+ }
140
+ }
141
+
142
+ function renderTopLeft(
143
+ inputText: string,
144
+ metadata: MinimalistEditorMetadata,
145
+ uiTheme: Theme,
146
+ config: ZentuiConfig,
147
+ includeSessionName = true,
148
+ ): string {
149
+ const source = config.components.editor.colorSource;
150
+ const trimmed = inputText.trimStart();
151
+ const bashMode = trimmed.startsWith("!!")
152
+ ? "no-context"
153
+ : trimmed.startsWith("!")
154
+ ? "shell"
155
+ : "";
156
+ const parts: string[] = [];
157
+ if (bashMode) {
158
+ parts.push(
159
+ bashMode === "no-context"
160
+ ? safeThemeFg(uiTheme, "muted", "$")
161
+ : safeThemeFg(uiTheme, "bashMode", "$"),
162
+ );
163
+ }
164
+ if (
165
+ config.components.editor.styles.minimalist.showTimer &&
166
+ metadata.agentDurationMs !== undefined
167
+ ) {
168
+ const duration = formatElapsedDuration(metadata.agentDurationMs);
169
+ parts.push(
170
+ metadata.agentActive
171
+ ? renderStyleForSourceOrFallback(
172
+ uiTheme,
173
+ source,
174
+ config.colors.sessionDuration,
175
+ EDITOR_ACCENT_FALLBACK,
176
+ duration,
177
+ )
178
+ : safeThemeFg(uiTheme, "muted", duration),
179
+ );
180
+ }
181
+ const sessionName = includeSessionName
182
+ ? sanitizeEditorMetadataText(metadata.sessionName ?? "")
183
+ : "";
184
+ if (
185
+ config.components.editor.styles.minimalist.showSessionName &&
186
+ sessionName
187
+ ) {
188
+ parts.push(
189
+ renderStyleForSource(
190
+ uiTheme,
191
+ source,
192
+ config.colors.sessionName,
193
+ sessionName,
194
+ ),
195
+ );
196
+ }
197
+ return joinStyled(parts, safeThemeFg(uiTheme, "muted", " · "));
198
+ }
199
+
200
+ function renderTopRight(
201
+ metadata: MinimalistEditorMetadata,
202
+ uiTheme: Theme,
203
+ config: ZentuiConfig,
204
+ availableWidth: number,
205
+ renderBorder: (text: string) => string,
206
+ renderThinking: (text: string) => string,
207
+ ): string {
208
+ const source = config.components.editor.colorSource;
209
+ const parts: string[] = [];
210
+ const joinParts = (values: string[]) =>
211
+ values
212
+ .map((part, index) =>
213
+ index > 0 ? `${renderBorder(" – ")}${part}` : part,
214
+ )
215
+ .join("");
216
+ const cost = config.components.editor.styles.minimalist.showCost
217
+ ? sanitizeEditorMetadataText(metadata.costLabel ?? "")
218
+ : "";
219
+ if (cost) {
220
+ parts.push(renderStyleForSource(uiTheme, source, config.colors.cost, cost));
221
+ }
222
+ const model = sanitizeEditorMetadataText(metadata.modelLabel ?? "");
223
+ if (model) {
224
+ parts.push(
225
+ renderStyleForSourceOrFallback(
226
+ uiTheme,
227
+ source,
228
+ config.colors.editorModel,
229
+ MINIMALIST_MODEL_FALLBACK,
230
+ model,
231
+ ),
232
+ );
233
+ }
234
+ const thinking = sanitizeEditorMetadataText(metadata.thinkingLevel ?? "");
235
+ if (thinking && thinking.toLowerCase() !== "off") {
236
+ parts.push(renderThinking(thinking));
237
+ }
238
+ if (
239
+ metadata.contextPercent !== undefined &&
240
+ Number.isFinite(metadata.contextPercent)
241
+ ) {
242
+ const percent = Math.round(
243
+ Math.max(0, Math.min(999, metadata.contextPercent)),
244
+ );
245
+ const tier = contextColorTier(
246
+ percent,
247
+ config.components.editor.styles.minimalist.contextThresholds,
248
+ );
249
+ const style =
250
+ tier === "error"
251
+ ? config.colors.contextError
252
+ : tier === "warning"
253
+ ? config.colors.contextWarning
254
+ : config.colors.contextNormal;
255
+ const total =
256
+ config.components.editor.styles.minimalist.contextFormat ===
257
+ "percent-total" &&
258
+ metadata.contextWindow !== undefined &&
259
+ Number.isFinite(metadata.contextWindow) &&
260
+ metadata.contextWindow > 0
261
+ ? `/${formatCount(metadata.contextWindow)}`
262
+ : "";
263
+ const text = `${percent}%${total}`;
264
+ let context = renderStyleForSource(uiTheme, source, style, text);
265
+ if (config.components.editor.styles.minimalist.contextGauge) {
266
+ for (const gaugeWidth of [5, 3]) {
267
+ const gauge = `[${buildContextGauge(percent, gaugeWidth, config.icons.mode === "ascii")}] ${text}`;
268
+ const styledGauge = renderStyleForSource(uiTheme, source, style, gauge);
269
+ const candidate = joinParts([...parts, styledGauge]);
270
+ if (visibleWidth(candidate) <= availableWidth) {
271
+ context = styledGauge;
272
+ break;
273
+ }
274
+ }
275
+ }
276
+ parts.push(context);
277
+ }
278
+ return joinParts(parts);
279
+ }
280
+
281
+ function renderBottomLeft(
282
+ metadata: MinimalistEditorMetadata,
283
+ uiTheme: Theme,
284
+ config: ZentuiConfig,
285
+ ): string {
286
+ if (!config.components.editor.styles.minimalist.showGit) return "";
287
+ const source = config.components.editor.colorSource;
288
+ const branch = sanitizeEditorMetadataText(metadata.branch ?? "");
289
+ const parts = branch
290
+ ? [
291
+ renderStyleForSourceOrFallback(
292
+ uiTheme,
293
+ source,
294
+ config.colors.editorGitBranch,
295
+ MINIMALIST_BRANCH_FALLBACK,
296
+ branch,
297
+ ),
298
+ ]
299
+ : [];
300
+ if (metadata.dirty) {
301
+ parts.push(
302
+ renderStyleForSource(uiTheme, source, config.colors.gitStatus, "*"),
303
+ );
304
+ }
305
+ if ((metadata.ahead ?? 0) > 0) {
306
+ parts.push(safeThemeFg(uiTheme, "success", `↑${metadata.ahead}`));
307
+ }
308
+ if ((metadata.behind ?? 0) > 0) {
309
+ parts.push(safeThemeFg(uiTheme, "error", `↓${metadata.behind}`));
310
+ }
311
+ return parts.join(" ");
312
+ }
313
+
314
+ function minimalistCwdLabel(
315
+ metadata: MinimalistEditorMetadata,
316
+ config: ZentuiConfig,
317
+ ): string {
318
+ const full = () =>
319
+ formatCwdLabel(metadata.cwd, "", { mode: "full", depth: 0 });
320
+ if (config.components.editor.styles.minimalist.pathDisplay === "full")
321
+ return full();
322
+ if (config.components.editor.styles.minimalist.pathDisplay === "compact")
323
+ return basename(metadata.cwd) || metadata.cwd;
324
+ if (!metadata.projectRoot) return full();
325
+
326
+ const pathFromRoot = relative(metadata.projectRoot, metadata.cwd);
327
+ if (
328
+ pathFromRoot === ".." ||
329
+ pathFromRoot.startsWith(`..${sep}`) ||
330
+ isAbsolute(pathFromRoot)
331
+ ) {
332
+ return full();
333
+ }
334
+ const project = basename(metadata.projectRoot) || metadata.projectRoot;
335
+ return pathFromRoot ? `${project}/${pathFromRoot}` : project;
336
+ }
337
+
338
+ function renderBottomRight(
339
+ metadata: MinimalistEditorMetadata,
340
+ uiTheme: Theme,
341
+ config: ZentuiConfig,
342
+ ): string {
343
+ const cwd = sanitizeEditorMetadataText(minimalistCwdLabel(metadata, config));
344
+ return cwd
345
+ ? renderStyleForSource(
346
+ uiTheme,
347
+ config.components.editor.colorSource,
348
+ config.colors.cwd,
349
+ cwd,
350
+ )
351
+ : "";
352
+ }
353
+
354
+ function renderLabeledBorder(options: {
355
+ width: number;
356
+ left: string;
357
+ leftFallbacks?: string[];
358
+ right: string;
359
+ leftCorner: string;
360
+ rightCorner: string;
361
+ renderBorder: (text: string) => string;
362
+ }): string {
363
+ const innerWidth = Math.max(0, options.width - 2);
364
+ let left = options.left;
365
+ let right = options.right;
366
+ const fitLabels = () => {
367
+ const overhead = (left ? 3 : 1) + (right ? 3 : 1);
368
+ const budget = Math.max(0, innerWidth - overhead);
369
+ const leftNatural = visibleWidth(left);
370
+ const rightNatural = visibleWidth(right);
371
+ if (leftNatural + rightNatural <= budget) return false;
372
+
373
+ let leftBudget = left ? budget : 0;
374
+ let rightBudget = right ? budget : 0;
375
+ if (left && right) {
376
+ leftBudget = Math.ceil(budget / 2);
377
+ rightBudget = budget - leftBudget;
378
+ if (leftNatural < leftBudget) {
379
+ leftBudget = leftNatural;
380
+ rightBudget = budget - leftBudget;
381
+ } else if (rightNatural < rightBudget) {
382
+ rightBudget = rightNatural;
383
+ leftBudget = budget - rightBudget;
384
+ }
385
+ }
386
+ left = leftBudget > 0 ? truncateToWidth(left, leftBudget, "…") : "";
387
+ right = rightBudget > 0 ? truncateToWidth(right, rightBudget, "…") : "";
388
+ return leftBudget < leftNatural;
389
+ };
390
+ let leftTruncated = fitLabels();
391
+ for (const fallback of options.leftFallbacks ?? []) {
392
+ if (!leftTruncated) break;
393
+ left = fallback;
394
+ right = options.right;
395
+ leftTruncated = fitLabels();
396
+ }
397
+ const partWidth = (label: string) => (label ? visibleWidth(label) + 3 : 1);
398
+ let leftWidth = partWidth(left);
399
+ let rightWidth = partWidth(right);
400
+ if (leftWidth + rightWidth > innerWidth) {
401
+ left = "";
402
+ right = "";
403
+ leftWidth = 1;
404
+ rightWidth = 1;
405
+ }
406
+
407
+ const fillWidth = Math.max(0, innerWidth - leftWidth - rightWidth);
408
+ const leftPart = left
409
+ ? `${options.renderBorder("─ ")}${left}${options.renderBorder(" ")}`
410
+ : options.renderBorder("─");
411
+ const rightPart = right
412
+ ? `${options.renderBorder(" ")}${right}${options.renderBorder(" ─")}`
413
+ : options.renderBorder("─");
414
+ return `${options.renderBorder(options.leftCorner)}${leftPart}${options.renderBorder(
415
+ "─".repeat(fillWidth),
416
+ )}${rightPart}${options.renderBorder(options.rightCorner)}`;
417
+ }
418
+
419
+ export function renderMinimalistFrame({
420
+ width,
421
+ editorLines,
422
+ autocompleteLines = [],
423
+ viewport,
424
+ inputText,
425
+ metadata,
426
+ uiTheme,
427
+ config,
428
+ borderColor,
429
+ }: MinimalistFrameOptions): string[] {
430
+ if (width <= 4) return clampLines(editorLines, width);
431
+ const contentWidth = Math.max(0, width - 4);
432
+ const source = config.components.editor.colorSource;
433
+ const adaptive = config.components.editor.borderColorMode === "adaptive";
434
+ const thinking = sanitizeEditorMetadataText(metadata.thinkingLevel ?? "");
435
+ const activeThinking =
436
+ thinking && thinking.toLowerCase() !== "off" ? thinking : "";
437
+ const renderStaticBorder = (text: string) =>
438
+ renderStyleForSourceOrFallback(
439
+ uiTheme,
440
+ source,
441
+ config.colors.editorBorder,
442
+ EDITOR_BORDER_FALLBACK,
443
+ text,
444
+ );
445
+ const terminalAdaptiveThinkingStyle = activeThinking
446
+ ? (thinkingStyle(config, activeThinking) ??
447
+ MINIMALIST_ADAPTIVE_TERMINAL_THINKING_FALLBACKS[
448
+ activeThinking.toLowerCase()
449
+ ] ??
450
+ MINIMALIST_THINKING_FALLBACK.terminal)
451
+ : undefined;
452
+ const renderBorder = (text: string) => {
453
+ if (!adaptive) return renderStaticBorder(text);
454
+ if (source === "terminal") {
455
+ return terminalAdaptiveThinkingStyle
456
+ ? renderStyleForSource(
457
+ uiTheme,
458
+ source,
459
+ terminalAdaptiveThinkingStyle,
460
+ text,
461
+ )
462
+ : renderStaticBorder(text);
463
+ }
464
+ if (!borderColor) return renderStaticBorder(text);
465
+ try {
466
+ const rendered = borderColor(text);
467
+ return typeof rendered === "string" ? rendered : renderStaticBorder(text);
468
+ } catch {
469
+ return renderStaticBorder(text);
470
+ }
471
+ };
472
+ const renderStaticThinking = (text: string) =>
473
+ renderStyleForSourceOrFallback(
474
+ uiTheme,
475
+ source,
476
+ thinkingStyle(config, text),
477
+ MINIMALIST_THINKING_FALLBACK,
478
+ text,
479
+ );
480
+ const renderThinking = adaptive ? renderBorder : renderStaticThinking;
481
+ const separator = safeThemeFg(uiTheme, "muted", " · ");
482
+ const viewportLabel = (
483
+ direction: "above" | "below",
484
+ count: string | undefined,
485
+ ) => {
486
+ if (!count || !/^[1-9]\d*$/.test(count)) return "";
487
+ return safeThemeFg(
488
+ uiTheme,
489
+ "muted",
490
+ `${direction === "above" ? "↑" : "↓"} ${count} more`,
491
+ );
492
+ };
493
+ const topMetadata = renderTopLeft(inputText, metadata, uiTheme, config);
494
+ const topOperational = renderTopLeft(
495
+ inputText,
496
+ metadata,
497
+ uiTheme,
498
+ config,
499
+ false,
500
+ );
501
+ const topViewport = viewportLabel("above", viewport?.above);
502
+ const topLeft = joinStyled([topViewport, topMetadata], separator);
503
+ const topRightBudget = Math.max(0, width - 8 - visibleWidth(topLeft));
504
+ const topFallbacks = [
505
+ joinStyled([topViewport, topOperational], separator),
506
+ topOperational,
507
+ ].filter(
508
+ (value, index, values) =>
509
+ value !== topLeft && values.indexOf(value) === index,
510
+ );
511
+ const top = renderLabeledBorder({
512
+ width,
513
+ left: topLeft,
514
+ leftFallbacks: topFallbacks,
515
+ right: renderTopRight(
516
+ metadata,
517
+ uiTheme,
518
+ config,
519
+ topRightBudget,
520
+ renderBorder,
521
+ renderThinking,
522
+ ),
523
+ leftCorner: "╭",
524
+ rightCorner: "╮",
525
+ renderBorder,
526
+ });
527
+ const bottomMetadata = renderBottomLeft(metadata, uiTheme, config);
528
+ const bottomViewport = viewportLabel("below", viewport?.below);
529
+ const bottom = renderLabeledBorder({
530
+ width,
531
+ left: joinStyled([bottomViewport, bottomMetadata], separator),
532
+ leftFallbacks: bottomViewport ? [bottomMetadata] : undefined,
533
+ right: renderBottomRight(metadata, uiTheme, config),
534
+ leftCorner: "╰",
535
+ rightCorner: "╯",
536
+ renderBorder,
537
+ });
538
+ const content = editorLines.map(
539
+ (line) =>
540
+ `${renderBorder("│")} ${fillLine(line, contentWidth)} ${renderBorder("│")}`,
541
+ );
542
+ const autocomplete = renderFramedAutocompleteRows({
543
+ width,
544
+ lines: autocompleteLines,
545
+ renderBorder,
546
+ });
547
+ return clampLines([top, ...content, ...autocomplete, bottom], width);
548
+ }