@luan.sh/pi-libtui 0.3.8

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 (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +208 -0
  3. package/native-revision +1 -0
  4. package/package.json +58 -0
  5. package/src/activity-presentation.ts +75 -0
  6. package/src/appearance.ts +632 -0
  7. package/src/background-surface.ts +125 -0
  8. package/src/color/palette.ts +186 -0
  9. package/src/color/preview.ts +157 -0
  10. package/src/color/resolver.ts +75 -0
  11. package/src/color/theme.ts +531 -0
  12. package/src/component-stack.ts +247 -0
  13. package/src/content/markdown-code.ts +60 -0
  14. package/src/content/terminal-text.ts +135 -0
  15. package/src/content/text.ts +105 -0
  16. package/src/controls/action-panel.ts +420 -0
  17. package/src/controls/dialog-button-bar.ts +287 -0
  18. package/src/controls/multi-select.ts +467 -0
  19. package/src/controls/picker-panel.ts +447 -0
  20. package/src/controls/screen-icon-actions.ts +134 -0
  21. package/src/controls/searchable-select.ts +394 -0
  22. package/src/controls/select-box.ts +224 -0
  23. package/src/controls/selectable-list.ts +433 -0
  24. package/src/controls/selection-action-bar.ts +382 -0
  25. package/src/controls/semantic-input.ts +23 -0
  26. package/src/controls/tab.ts +189 -0
  27. package/src/cursor.ts +288 -0
  28. package/src/decoration/editor-pills.ts +156 -0
  29. package/src/decoration/glyphs.ts +326 -0
  30. package/src/decoration/pointer-interaction.ts +123 -0
  31. package/src/decoration/powerline-pill.ts +104 -0
  32. package/src/decoration/status.ts +185 -0
  33. package/src/decoration/transient-pill.ts +176 -0
  34. package/src/diff/index.ts +21 -0
  35. package/src/diff/model.ts +76 -0
  36. package/src/diff/parse.ts +310 -0
  37. package/src/diff/pierre-syntax.ts +86 -0
  38. package/src/diff/render.ts +504 -0
  39. package/src/diff/structured.ts +264 -0
  40. package/src/diff/view.ts +75 -0
  41. package/src/editor/chrome.ts +341 -0
  42. package/src/editor/composition.ts +53 -0
  43. package/src/editor/layout.ts +81 -0
  44. package/src/editor/presentation.ts +35 -0
  45. package/src/editor/protocol.ts +188 -0
  46. package/src/editor.ts +41 -0
  47. package/src/extension.ts +30 -0
  48. package/src/folding.ts +192 -0
  49. package/src/host/cursor-bridge.ts +210 -0
  50. package/src/host/editor-bridge.ts +148 -0
  51. package/src/host/extension-host.ts +231 -0
  52. package/src/host/mouse-bridge.ts +212 -0
  53. package/src/host/mouse-dispatch.ts +400 -0
  54. package/src/host/mouse-input.ts +85 -0
  55. package/src/host/mouse-targets.ts +238 -0
  56. package/src/host/pi-layout-adapter.ts +298 -0
  57. package/src/host/pi-native-selection.ts +227 -0
  58. package/src/host/split-pane-bridge.ts +720 -0
  59. package/src/host/transcript-bridge.ts +236 -0
  60. package/src/index.ts +334 -0
  61. package/src/line-layout.ts +6 -0
  62. package/src/motion.ts +1409 -0
  63. package/src/mouse/events.ts +55 -0
  64. package/src/mouse/fullscreen-layout.ts +260 -0
  65. package/src/mouse/registry.ts +348 -0
  66. package/src/mouse.ts +37 -0
  67. package/src/native-binary.ts +189 -0
  68. package/src/overlay/anchored.ts +91 -0
  69. package/src/overlay/detail-card.ts +158 -0
  70. package/src/overlay/dialog.ts +266 -0
  71. package/src/overlay/floating.ts +106 -0
  72. package/src/overlay/framed-editor.ts +236 -0
  73. package/src/overlay/fullscreen.ts +170 -0
  74. package/src/overlay/hover-tooltip.ts +109 -0
  75. package/src/overlay/modal-mount.ts +118 -0
  76. package/src/panels.ts +207 -0
  77. package/src/render-cache.ts +80 -0
  78. package/src/render-epoch.ts +48 -0
  79. package/src/request-animation.ts +160 -0
  80. package/src/scrollbar.ts +27 -0
  81. package/src/selection.ts +148 -0
  82. package/src/split-pane.ts +232 -0
  83. package/src/status-presentation.ts +411 -0
  84. package/src/stream.ts +543 -0
  85. package/src/syntax.ts +316 -0
  86. package/src/terminal/bridge-client.ts +243 -0
  87. package/src/terminal/embedding.ts +6 -0
  88. package/src/terminal/output.ts +151 -0
  89. package/src/terminal/projection.ts +362 -0
  90. package/src/terminal/pty-host.ts +263 -0
  91. package/src/terminal/pty-pane.ts +308 -0
  92. package/src/terminal-colors.ts +334 -0
  93. package/src/tool/action.ts +170 -0
  94. package/src/tool/activity.ts +500 -0
  95. package/src/tool/call-preview.ts +44 -0
  96. package/src/tool/disclosure-action.ts +89 -0
  97. package/src/tool/expanded-region-viewport.ts +103 -0
  98. package/src/tool/index.ts +26 -0
  99. package/src/tool/output.ts +271 -0
  100. package/src/tool/transcript.ts +150 -0
  101. package/src/tool/view-region.ts +581 -0
  102. package/test/appearance.test.ts +183 -0
  103. package/test/bounded-stream.test.ts +150 -0
  104. package/test/bridge-client.test.ts +42 -0
  105. package/test/color-preview.test.ts +23 -0
  106. package/test/component-stack.test.ts +206 -0
  107. package/test/components.test.ts +1165 -0
  108. package/test/content-text.test.ts +21 -0
  109. package/test/cursor-bridge.test.ts +151 -0
  110. package/test/cursor.test.ts +147 -0
  111. package/test/editor-chrome.test.ts +119 -0
  112. package/test/editor-layout.test.ts +30 -0
  113. package/test/editor-registry.test.ts +203 -0
  114. package/test/editor-theme.test.ts +33 -0
  115. package/test/editor-token-pill.test.ts +72 -0
  116. package/test/floating-overlay.test.ts +57 -0
  117. package/test/folding.test.ts +147 -0
  118. package/test/harmonious-theme.test.ts +23 -0
  119. package/test/interaction.test.ts +104 -0
  120. package/test/key-icons.test.ts +62 -0
  121. package/test/markdown-code.test.ts +24 -0
  122. package/test/modal-overlay-mount.test.ts +94 -0
  123. package/test/motion.test.ts +649 -0
  124. package/test/mouse/bridge.test.ts +1302 -0
  125. package/test/mouse/extension.test.ts +205 -0
  126. package/test/mouse/registry.test.ts +140 -0
  127. package/test/mouse/screen-decoration.test.ts +167 -0
  128. package/test/native-binary.test.ts +52 -0
  129. package/test/palette.test.ts +32 -0
  130. package/test/panels.test.ts +74 -0
  131. package/test/pointer-components.test.ts +151 -0
  132. package/test/progress.test.ts +54 -0
  133. package/test/pty-host.test.ts +126 -0
  134. package/test/pty-pane.test.ts +62 -0
  135. package/test/public-surface.test.ts +75 -0
  136. package/test/render-cache.test.ts +25 -0
  137. package/test/render-epoch.test.ts +119 -0
  138. package/test/request-animation.test.ts +193 -0
  139. package/test/screen-icon-actions.test.ts +69 -0
  140. package/test/select-box.test.ts +86 -0
  141. package/test/select-pointer.test.ts +369 -0
  142. package/test/selectable-list.test.ts +258 -0
  143. package/test/selection/registry.test.ts +44 -0
  144. package/test/selection-action-bar.test.ts +204 -0
  145. package/test/split-pane-bridge.test.ts +944 -0
  146. package/test/split-pane.test.ts +134 -0
  147. package/test/status-presentation.test.ts +39 -0
  148. package/test/syntax.test.ts +59 -0
  149. package/test/terminal-colors.test.ts +115 -0
  150. package/test/terminal-embedding.test.ts +12 -0
  151. package/test/terminal-projection.test.ts +312 -0
  152. package/test/theme-colors.test.ts +175 -0
  153. package/test/tool-output.test.ts +160 -0
  154. package/test/tool-transcript.test.ts +1190 -0
  155. package/test/unified-diff.test.ts +691 -0
  156. package/themes/harmonious.json +61 -0
  157. package/tsconfig.json +16 -0
@@ -0,0 +1,411 @@
1
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
+ import {
3
+ getTuiAppearance,
4
+ type TuiStatusPresentationStyle,
5
+ type TuiAnimationSpeed,
6
+ type TuiRequestPhase,
7
+ } from "./appearance.ts";
8
+ import type { TuiForegroundColor, TuiHue, TuiTheme } from "./color/theme.ts";
9
+ import { animationSpeedMultiplier } from "./motion.ts";
10
+
11
+ /** Render one intentional full status-row animation. */
12
+ export function statusPresentationFrame(
13
+ colors: TuiTheme,
14
+ style: TuiStatusPresentationStyle,
15
+ elapsedMs: number,
16
+ width: number,
17
+ options: { animationSpeed?: TuiAnimationSpeed; phase?: TuiRequestPhase; reducedMotion?: boolean } = {},
18
+ ): string {
19
+ if (style === "standard") return "";
20
+ const boundedWidth = Math.max(10, Math.min(120, Math.floor(width)));
21
+ const scaledElapsed = options.reducedMotion
22
+ ? 0
23
+ : Math.max(0, elapsedMs) * animationSpeedMultiplier(options.animationSpeed ?? getTuiAppearance().animationSpeed);
24
+ const frame = scaledElapsed / 60;
25
+ const phase = options.phase ?? "working";
26
+ const rendered = renderStatusPresentation(colors, style, frame, boundedWidth, phase);
27
+ return visibleWidth(rendered) <= boundedWidth ? rendered : truncateToWidth(rendered, boundedWidth, "");
28
+ }
29
+
30
+ /** Produce message text for inline presentations that replace only the ordinary phase label. */
31
+ export function activityMessageFrame(
32
+ elapsedMs: number,
33
+ options: { animationSpeed?: TuiAnimationSpeed; reducedMotion?: boolean } = {},
34
+ ): string {
35
+ const scaledElapsed = options.reducedMotion
36
+ ? 0
37
+ : Math.max(0, elapsedMs) * animationSpeedMultiplier(options.animationSpeed ?? getTuiAppearance().animationSpeed);
38
+ const { shown, cursor } = typewriterParts(scaledElapsed / 60);
39
+ return `${shown}${cursor}`;
40
+ }
41
+
42
+ function renderStatusPresentation(
43
+ colors: TuiTheme,
44
+ style: Exclude<TuiStatusPresentationStyle, "standard">,
45
+ frame: number,
46
+ width: number,
47
+ phase: TuiRequestPhase,
48
+ ): string {
49
+ switch (style) {
50
+ case "neural-pulse":
51
+ return neuralPulse(colors, frame, width);
52
+ case "plasma-wave":
53
+ return plasmaWave(colors, frame, width);
54
+ case "pacman":
55
+ return pacman(colors, frame, width);
56
+ case "matrix":
57
+ return matrix(colors, frame, width);
58
+ case "pipeline":
59
+ return pipeline(colors, frame);
60
+ case "starfield":
61
+ return starfield(colors, frame, width);
62
+ case "fire":
63
+ return fire(colors, frame, width);
64
+ case "icon-morph":
65
+ return iconMorph(colors, frame);
66
+ case "brainstorm":
67
+ return brainstorm(colors, frame);
68
+ case "dev-constellation":
69
+ return devConstellation(colors, frame);
70
+ case "pi-pulse":
71
+ return piPulse(colors, frame);
72
+ case "orbit-dots":
73
+ return orbitDots(colors, frame, phase);
74
+ case "neon-bounce":
75
+ return neonBounce(colors, frame, width);
76
+ case "block-wave":
77
+ return blockWave(colors, frame, width);
78
+ case "conveyor":
79
+ return conveyor(colors, frame, width);
80
+ case "accordion":
81
+ return accordion(colors, frame, width);
82
+ }
83
+ }
84
+
85
+ function neuralPulse(colors: TuiTheme, frame: number, width: number): string {
86
+ const nodes = Math.max(3, Math.min(14, Math.floor(width / 4)));
87
+ let output = "";
88
+ for (let index = 0; index < nodes; index += 1) {
89
+ const distance = positiveModulo(index - frame * 0.5, nodes);
90
+ output += colors.fg(neuralTone(distance), distance < 7 ? "●" : "○");
91
+ if (index < nodes - 1) {
92
+ const connectorDistance = positiveModulo(index + 0.5 - frame * 0.5, nodes);
93
+ output += colors.fg(neuralTone(connectorDistance), "──");
94
+ }
95
+ }
96
+ return output;
97
+ }
98
+
99
+ function neuralTone(distance: number): TuiForegroundColor {
100
+ if (distance >= 7) return "text.muted";
101
+ const hues: readonly TuiHue[] = ["blue", "blue", "magenta", "magenta", "magenta", "blue", "blue"];
102
+ const shades = [2, 3, 4, 5, 4, 3, 2] as const;
103
+ const index = Math.floor(distance);
104
+ return { hue: hues[index] ?? "blue", shade: shades[index] ?? 2 };
105
+ }
106
+
107
+ function plasmaWave(colors: TuiTheme, frame: number, width: number): string {
108
+ const glyphs = [" ", "·", "∘", "○", "◎", "●", "◉", "█"] as const;
109
+ const hues: readonly TuiHue[] = ["magenta", "blue", "cyan", "green", "yellow", "red"];
110
+ return Array.from({ length: width }, (_, index) => {
111
+ const wave =
112
+ (Math.sin(index * 0.15 + frame * 0.08) +
113
+ Math.sin(index * 0.1 + frame * 0.06) +
114
+ Math.sin(Math.abs(index) * 0.15 + frame * 0.1)) /
115
+ 3;
116
+ const level = Math.max(0, Math.min(glyphs.length - 1, Math.round(((wave + 1) / 2) * (glyphs.length - 1))));
117
+ return colors.fg(
118
+ { hue: hues[(index + Math.floor(frame / 2)) % hues.length] ?? "blue", shade: level > 4 ? 5 : 3 },
119
+ glyphs[level]!,
120
+ );
121
+ }).join("");
122
+ }
123
+
124
+ function pacman(colors: TuiTheme, frame: number, width: number): string {
125
+ const size = Math.min(40, width);
126
+ const course = size + 8;
127
+ const head = Math.floor(frame) % course;
128
+ const ghost = (Math.floor(frame) - 4 + course) % course;
129
+ return Array.from({ length: size }, (_, index) => {
130
+ if (head < size && index === head) return colors.fg("warning", Math.floor(frame) % 4 < 2 ? "ᗧ" : "●");
131
+ if (ghost < size && index === ghost) return colors.fg({ hue: "red", shade: 5 }, "ᗣ");
132
+ if (head >= size || index > head)
133
+ return colors.fg(index % 8 === 0 ? "text.primary" : { hue: "yellow", shade: 3 }, index % 8 === 0 ? "●" : "·");
134
+ return " ";
135
+ }).join("");
136
+ }
137
+
138
+ const MATRIX_CHARACTERS = "ハミヒーウシナモニサワツオリ012789Z";
139
+
140
+ function matrix(colors: TuiTheme, frame: number, width: number): string {
141
+ const cells = Array.from({ length: width }, () => " ");
142
+ for (let seed = 0; seed < Math.max(15, Math.floor(width * 0.4)); seed += 1) {
143
+ const speed = 0.3 + deterministicUnit(seed, 9) * 0.5;
144
+ const position = Math.floor((frame * speed + deterministicUnit(seed, 4) * 100) % (width + 5));
145
+ for (let trail = 0; trail < 3; trail += 1) {
146
+ const index = position - trail;
147
+ if (index < 0 || index >= width || cells[index] !== " ") continue;
148
+ const glyph = MATRIX_CHARACTERS[(seed + trail + Math.floor(frame)) % MATRIX_CHARACTERS.length]!;
149
+ cells[index] = colors.fg({ hue: "green", shade: (5 - trail) as 3 | 4 | 5 }, glyph);
150
+ }
151
+ }
152
+ return cells.join("");
153
+ }
154
+
155
+ const PIPELINE_ICONS = ["\uf0e7", "\uf013", "\uf121", "\uf0ad", "\uf00c"] as const;
156
+
157
+ function pipeline(colors: TuiTheme, frame: number): string {
158
+ const gap = 5;
159
+ const total = PIPELINE_ICONS.length * (gap + 1) + 1;
160
+ const pulse = (frame * 0.4) % total;
161
+ return PIPELINE_ICONS.map((glyph, index) => {
162
+ const start = index * (gap + 1);
163
+ const active = pulse >= start && pulse < start + gap + 1;
164
+ const icon = colors.fg(
165
+ { hue: ["yellow", "blue", "green", "red", "cyan"][index] as TuiHue, shade: active ? 5 : 2 },
166
+ `${glyph} `,
167
+ );
168
+ if (index === PIPELINE_ICONS.length - 1) return icon;
169
+ const track = Array.from({ length: gap }, (_, offset) => {
170
+ const position = start + 1 + offset;
171
+ return colors.fg(
172
+ Math.abs(pulse - position) < 1.5 ? "text.primary" : pulse > position ? "accent" : "text.muted",
173
+ Math.abs(pulse - position) < 1.5 ? "═" : "─",
174
+ );
175
+ }).join("");
176
+ return `${icon}${track}`;
177
+ }).join("");
178
+ }
179
+
180
+ function starfield(colors: TuiTheme, frame: number, width: number): string {
181
+ const cells = Array.from({ length: width }, () => " ");
182
+ const glyphs = ["·", "∙", "•", "✦", "★"] as const;
183
+ for (let seed = 0; seed < Math.max(20, Math.floor(width * 0.6)); seed += 1) {
184
+ const speed = 0.2 + deterministicUnit(seed, 3) * 1.2;
185
+ const layer = Math.min(glyphs.length - 1, Math.floor(speed / 0.3));
186
+ const position = Math.floor((deterministicUnit(seed, 7) * width + frame * speed) % width);
187
+ const shade = Math.max(1, Math.min(5, layer + 1)) as 1 | 2 | 3 | 4 | 5;
188
+ cells[position] = colors.fg({ hue: "blue", shade }, glyphs[layer]!);
189
+ }
190
+ return cells.join("");
191
+ }
192
+
193
+ const FIRE_GLYPHS = " .:-=+*#%@█";
194
+
195
+ function fire(colors: TuiTheme, frame: number, width: number): string {
196
+ const frameIndex = Math.floor(frame);
197
+ const rows = Array.from({ length: 4 }, () => new Float64Array(width));
198
+ for (let index = 0; index < width; index += 1) {
199
+ const fuel = deterministicUnit(index, frameIndex);
200
+ rows[3]![index] = fuel > 0.35 ? 1 : deterministicUnit(index + width, frameIndex) * 0.5;
201
+ }
202
+ for (let row = 2; row >= 0; row -= 1) {
203
+ for (let index = 0; index < width; index += 1) {
204
+ const below = rows[row + 1]!;
205
+ rows[row]![index] = (below[(index - 1 + width) % width]! + below[index]! + below[(index + 1) % width]!) / 3.1;
206
+ }
207
+ }
208
+ return Array.from(rows[0]!, (heat) => {
209
+ const level = Math.floor(heat * (FIRE_GLYPHS.length - 1));
210
+ const tone =
211
+ level > 7
212
+ ? "warning"
213
+ : ({
214
+ hue: level > 4 ? "yellow" : "red",
215
+ shade: Math.max(2, Math.min(5, 2 + Math.floor(level / 3))) as 2 | 3 | 4 | 5,
216
+ } as const);
217
+ return colors.fg(tone, FIRE_GLYPHS[level]!);
218
+ }).join("");
219
+ }
220
+
221
+ const MORPH_ICONS = ["\uf0eb", "\uf013", "\uf0e7", "\uf135", "\uf005", "\uf06d", "\uf0ac", "\uf004"] as const;
222
+
223
+ function iconMorph(colors: TuiTheme, frame: number): string {
224
+ const duration = 25;
225
+ const position =
226
+ ((frame % (MORPH_ICONS.length * duration)) + MORPH_ICONS.length * duration) % (MORPH_ICONS.length * duration);
227
+ const index = Math.floor(position / duration);
228
+ const progress = (position % duration) / duration;
229
+ const transition = "░▒▓█▓▒░";
230
+ const glyph =
231
+ progress < 0.3
232
+ ? MORPH_ICONS[index]!
233
+ : progress < 0.7
234
+ ? transition[Math.min(transition.length - 1, Math.floor(((progress - 0.3) / 0.4) * transition.length))]!
235
+ : MORPH_ICONS[(index + 1) % MORPH_ICONS.length]!;
236
+ const trail = Array.from({ length: 20 }, (_, seed) =>
237
+ deterministicUnit(seed, Math.floor(frame)) < 0.25 ? (seed % 3 === 0 ? "✦" : "·") : " ",
238
+ ).join("");
239
+ return `${colors.fg({ hue: ["yellow", "blue", "magenta", "cyan"][index % 4] as TuiHue, shade: 5 }, glyph)} ${colors.fg("warning", trail)}`;
240
+ }
241
+
242
+ const BRAINSTORM_PHASES = [
243
+ ["\ue30d", "calm"],
244
+ ["\ue302", "thinking."],
245
+ ["\ue318", "thinking.."],
246
+ ["\ue31d", "EUREKA!"],
247
+ ["\ue30b", "insight!"],
248
+ ["\ue302", "processing"],
249
+ ] as const;
250
+
251
+ function brainstorm(colors: TuiTheme, frame: number): string {
252
+ const state = BRAINSTORM_PHASES[Math.floor(frame / 35) % BRAINSTORM_PHASES.length]!;
253
+ const pulseShade = Math.max(2, Math.min(5, Math.round(3.5 + Math.sin(frame * 0.15) * 1.5))) as 2 | 3 | 4 | 5;
254
+ const sparks = Array.from({ length: 15 }, (_, seed) =>
255
+ deterministicUnit(seed, Math.floor(frame)) < 0.15 ? (seed % 3 === 0 ? "⚡" : "✦") : " ",
256
+ ).join("");
257
+ return `\x1b[1m${colors.fg({ hue: "yellow", shade: pulseShade }, `${state[0]} ${state[1]}`)}\x1b[22m ${colors.fg("accent", sparks)}`;
258
+ }
259
+
260
+ const DEV_ICONS = ["\ue796", "\ue718", "\ue73c", "\ue7a8", "\uf13b", "\ue61e"] as const;
261
+
262
+ function devConstellation(colors: TuiTheme, frame: number): string {
263
+ const gap = 5;
264
+ const pulse = (frame * 0.4) % (DEV_ICONS.length * (gap + 1) - 1);
265
+ return DEV_ICONS.map((glyph, index) => {
266
+ const node = index * (gap + 1);
267
+ const icon = colors.fg(
268
+ {
269
+ hue: ["blue", "green", "yellow", "magenta", "cyan", "red"][index] as TuiHue,
270
+ shade: Math.abs(pulse - node) < 1.5 ? 5 : 3,
271
+ },
272
+ glyph,
273
+ );
274
+ if (index === DEV_ICONS.length - 1) return icon;
275
+ return `${icon}${Array.from({ length: gap }, (_, offset) => colors.fg(Math.abs(pulse - (node + 1 + offset)) < 1 ? "text.primary" : "text.muted", Math.abs(pulse - (node + 1 + offset)) < 2.5 ? "─" : "·")).join("")}`;
276
+ }).join("");
277
+ }
278
+
279
+ function piPulse(colors: TuiTheme, frame: number): string {
280
+ const hue = ["magenta", "blue", "cyan"][Math.floor(frame * 0.3) % 3] as TuiHue;
281
+ const trailGlyphs = "·∘○◎●";
282
+ const trail = Array.from({ length: 20 }, (_, index) => {
283
+ const level = Math.floor(((Math.sin((index / 20) * Math.PI * 2 + frame * 0.1) + 1) / 2) * (trailGlyphs.length - 1));
284
+ return colors.fg({ hue, shade: Math.max(2, 5 - Math.floor(index / 7)) as 2 | 3 | 4 | 5 }, trailGlyphs[level]!);
285
+ }).join("");
286
+ return `${colors.fg({ hue, shade: 5 }, "\ue22c")} ${trail}`;
287
+ }
288
+
289
+ const TYPEWRITER_MESSAGES = [
290
+ "Engaging warp drive...",
291
+ "Running diagnostics...",
292
+ "Recalibrating sensors...",
293
+ "Scanning the horizon...",
294
+ "Channeling the cosmos...",
295
+ "Weaving neural threads...",
296
+ "Parsing the matrix...",
297
+ ] as const;
298
+
299
+ function typewriterParts(frame: number): { shown: string; cursor: string } {
300
+ const cycle = TYPEWRITER_MESSAGES.reduce((total, message) => total + message.length + 30, 0);
301
+ let position = positiveModulo(Math.floor(frame), cycle);
302
+ let message: (typeof TYPEWRITER_MESSAGES)[number] = TYPEWRITER_MESSAGES[0]!;
303
+ for (const candidate of TYPEWRITER_MESSAGES) {
304
+ const duration = candidate.length + 30;
305
+ if (position < duration) {
306
+ message = candidate;
307
+ break;
308
+ }
309
+ position -= duration;
310
+ }
311
+ const progress = Math.min(message.length, position + 1);
312
+ const shown = message.slice(0, Math.min(message.length, progress));
313
+ const cursor = progress < message.length ? ["✦", "✧", "⚡", "★", "·"][Math.floor(frame) % 5]! : "";
314
+ return { shown, cursor };
315
+ }
316
+
317
+ function orbitDots(colors: TuiTheme, frame: number, phase: TuiRequestPhase): string {
318
+ const glyphs = ["·", "∘", "○", "●", "◉", "●", "○", "∘"] as const;
319
+ const dots = Array.from({ length: 5 }, (_, index) => {
320
+ const norm = (Math.sin(frame * 0.12 - index * 0.8) + 1) / 2;
321
+ const glyph = glyphs[Math.floor(norm * (glyphs.length - 1))]!;
322
+ return colors.fg(
323
+ {
324
+ hue: ["magenta", "blue", "cyan"][Math.floor(index + frame * 0.1) % 3] as TuiHue,
325
+ shade: Math.max(2, Math.min(5, Math.round(2 + norm * 3))) as 2 | 3 | 4 | 5,
326
+ },
327
+ `${norm > 0.7 ? `\x1b[1m${glyph}\x1b[22m` : glyph} `,
328
+ );
329
+ }).join("");
330
+ return `${dots} ${colors.fg("accent", phaseLabel(phase))}${colors.fg("text.secondary", ellipsis(frame))}`;
331
+ }
332
+
333
+ function neonBounce(colors: TuiTheme, frame: number, width: number): string {
334
+ const size = Math.max(1, width - 2);
335
+ const cycle = (frame * 0.6) % (size * 2);
336
+ const head = Math.floor(cycle < size ? cycle : size * 2 - cycle);
337
+ const cells = Array.from({ length: size }, () => " ");
338
+ for (let age = 4; age >= 0; age -= 1) {
339
+ const priorCycle = ((((frame - age) * 0.6) % (size * 2)) + size * 2) % (size * 2);
340
+ const position = Math.floor(priorCycle < size ? priorCycle : size * 2 - priorCycle);
341
+ if (position >= 0 && position < size)
342
+ cells[position] = colors.fg(
343
+ { hue: "magenta", shade: Math.max(1, 5 - age) as 1 | 2 | 3 | 4 | 5 },
344
+ ["█", "▓", "▒", "░", "·"][age]!,
345
+ );
346
+ }
347
+ if (head >= 0 && head < size) cells[head] = colors.fg("accent", "█");
348
+ return `${colors.fg("text.muted", "▐")}${cells.join("")}${colors.fg("text.muted", "▌")}`;
349
+ }
350
+
351
+ function blockWave(colors: TuiTheme, frame: number, width: number): string {
352
+ const size = Math.max(10, Math.min(28, width));
353
+ const half = (size - 1) / 2;
354
+ const sweep = (frame * 0.22) % (half * 2);
355
+ const head = sweep <= half ? sweep : half * 2 - sweep;
356
+ return Array.from({ length: size }, (_, index) => {
357
+ const distance = Math.min(Math.abs(index - head), Math.abs(index - (size - 1 - head)));
358
+ const glyph = distance < 0.6 ? "█" : distance < 1.3 ? "▓" : distance < 2.2 ? "▒" : "·";
359
+ return colors.fg(distance < 2.2 ? "accent" : "text.muted", glyph);
360
+ }).join("");
361
+ }
362
+
363
+ function conveyor(colors: TuiTheme, frame: number, width: number): string {
364
+ const size = Math.max(10, Math.min(32, width));
365
+ const head = (Math.floor(frame * 0.45) % (size + 4)) - 2;
366
+ return blockTrack(colors, size, [
367
+ { position: head - 2, glyph: "▒" },
368
+ { position: head - 1, glyph: "▓" },
369
+ { position: head, glyph: "█" },
370
+ ]);
371
+ }
372
+
373
+ function accordion(colors: TuiTheme, frame: number, width: number): string {
374
+ const size = Math.max(10, Math.min(32, width));
375
+ return blockTrack(colors, size, [
376
+ { position: accordionPosition(frame - 8, size), glyph: "▒" },
377
+ { position: accordionPosition(frame - 4, size), glyph: "▓" },
378
+ { position: accordionPosition(frame, size), glyph: "█" },
379
+ ]);
380
+ }
381
+
382
+ function accordionPosition(frame: number, width: number): number {
383
+ const phase = ((((frame % 90) + 90) % 90) / 90) * Math.PI * 2;
384
+ return ((1 - Math.cos(phase)) / 2) * (width - 1);
385
+ }
386
+
387
+ function blockTrack(colors: TuiTheme, width: number, blocks: readonly { position: number; glyph: string }[]): string {
388
+ const cells = Array.from({ length: width }, () => colors.fg("text.muted", "░"));
389
+ for (const block of blocks) {
390
+ const position = Math.round(block.position);
391
+ if (position >= 0 && position < width) cells[position] = colors.fg("accent", block.glyph);
392
+ }
393
+ return cells.join("");
394
+ }
395
+
396
+ function positiveModulo(value: number, modulus: number): number {
397
+ return ((value % modulus) + modulus) % modulus;
398
+ }
399
+
400
+ function phaseLabel(phase: TuiRequestPhase): string {
401
+ return phase === "thinking" ? "Thinking" : phase === "tool" ? "Running" : "Working";
402
+ }
403
+
404
+ function ellipsis(frame: number): string {
405
+ return [".", "..", "...", ""][Math.floor(frame / 10) % 4]!;
406
+ }
407
+
408
+ function deterministicUnit(seed: number, frame: number): number {
409
+ const value = Math.sin(seed * 12.9898 + frame * 78.233) * 43_758.5453;
410
+ return value - Math.floor(value);
411
+ }