@selesai/code 0.9.15 → 0.10.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.
Files changed (267) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +6 -9
  3. package/dist/defaults/models.json +2 -2
  4. package/dist/defaults/settings.json +3 -7
  5. package/dist/extensions/package.json +2 -4
  6. package/dist/extensions/pi-zentui/.editorconfig +12 -0
  7. package/dist/extensions/pi-zentui/.github/CODEOWNERS +1 -0
  8. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/bug_report.yml +110 -0
  9. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config.yml +8 -0
  10. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config_question.yml +56 -0
  11. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/feature_request.yml +63 -0
  12. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/rendering_issue.yml +106 -0
  13. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/runtime_detection.yml +103 -0
  14. package/dist/extensions/pi-zentui/.github/dependabot.yml +47 -0
  15. package/dist/extensions/pi-zentui/.github/pull_request_template.md +34 -0
  16. package/dist/extensions/pi-zentui/.github/release.yml +18 -0
  17. package/dist/extensions/pi-zentui/.github/workflows/ci.yml +79 -0
  18. package/dist/extensions/pi-zentui/.github/workflows/publish.yml +63 -0
  19. package/dist/extensions/pi-zentui/AGENTS.md +107 -0
  20. package/dist/extensions/pi-zentui/CONTRIBUTING.md +68 -0
  21. package/dist/extensions/pi-zentui/LICENSE +21 -0
  22. package/dist/extensions/pi-zentui/README.md +188 -0
  23. package/dist/extensions/pi-zentui/SECURITY.md +18 -0
  24. package/dist/extensions/pi-zentui/assets/main-cover.png +0 -0
  25. package/dist/extensions/pi-zentui/assets/screenshots/editor-accent-rail.png +0 -0
  26. package/dist/extensions/pi-zentui/assets/screenshots/editor-minimalist.png +0 -0
  27. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode-copy-friendly.png +0 -0
  28. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode.png +0 -0
  29. package/dist/extensions/pi-zentui/assets/screenshots/user-message-compact.png +0 -0
  30. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed-copy-friendly.png +0 -0
  31. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed.png +0 -0
  32. package/dist/extensions/pi-zentui/assets/screenshots/user-message-labeled.png +0 -0
  33. package/dist/extensions/pi-zentui/assets/zentui.png +0 -0
  34. package/dist/extensions/pi-zentui/biome.json +31 -0
  35. package/dist/extensions/pi-zentui/docs/configuration.md +509 -0
  36. package/dist/extensions/pi-zentui/docs/footer-format.md +162 -0
  37. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-editor.ts +99 -0
  38. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-layout-patch.ts +457 -0
  39. package/dist/extensions/pi-zentui/extensions/zentui/completion-menu.ts +157 -0
  40. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +2100 -0
  41. package/dist/extensions/pi-zentui/extensions/zentui/editor-metadata-format.ts +321 -0
  42. package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +73 -0
  43. package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +89 -0
  44. package/dist/extensions/pi-zentui/extensions/zentui/footer-format.ts +364 -0
  45. package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +122 -0
  46. package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +812 -0
  47. package/dist/extensions/pi-zentui/extensions/zentui/format.ts +568 -0
  48. package/dist/extensions/pi-zentui/extensions/zentui/git.ts +384 -0
  49. package/dist/extensions/pi-zentui/extensions/zentui/icons.ts +252 -0
  50. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +1406 -0
  51. package/dist/extensions/pi-zentui/extensions/zentui/interaction-summary.ts +1049 -0
  52. package/dist/extensions/pi-zentui/extensions/zentui/live-context.ts +75 -0
  53. package/dist/extensions/pi-zentui/extensions/zentui/minimalist-editor.ts +463 -0
  54. package/dist/extensions/pi-zentui/extensions/zentui/package-version.ts +651 -0
  55. package/dist/extensions/pi-zentui/extensions/zentui/project-refresh.ts +120 -0
  56. package/dist/extensions/pi-zentui/extensions/zentui/project-state.ts +59 -0
  57. package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +166 -0
  58. package/dist/extensions/pi-zentui/extensions/zentui/repository-root.ts +98 -0
  59. package/dist/extensions/pi-zentui/extensions/zentui/runtime.ts +841 -0
  60. package/dist/extensions/pi-zentui/extensions/zentui/selector-border.ts +93 -0
  61. package/dist/extensions/pi-zentui/extensions/zentui/session-lifecycle.ts +60 -0
  62. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +1796 -0
  63. package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +128 -0
  64. package/dist/extensions/pi-zentui/extensions/zentui/state.ts +94 -0
  65. package/dist/extensions/pi-zentui/extensions/zentui/style.ts +424 -0
  66. package/dist/extensions/pi-zentui/extensions/zentui/telemetry.ts +82 -0
  67. package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +1290 -0
  68. package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +248 -0
  69. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +215 -0
  70. package/dist/extensions/pi-zentui/extensions/zentui/user-message.ts +206 -0
  71. package/dist/extensions/pi-zentui/extensions/zentui/working-line-messages.ts +24 -0
  72. package/dist/extensions/pi-zentui/extensions/zentui/working-line-spinners.ts +54 -0
  73. package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +1433 -0
  74. package/dist/extensions/pi-zentui/package-lock.json +5311 -0
  75. package/dist/extensions/pi-zentui/package.json +60 -0
  76. package/dist/extensions/pi-zentui/test/accent-rail-editor.test.ts +232 -0
  77. package/dist/extensions/pi-zentui/test/accent-rail-layout-patch.test.ts +669 -0
  78. package/dist/extensions/pi-zentui/test/completion-menu.test.ts +265 -0
  79. package/dist/extensions/pi-zentui/test/config.test.ts +3119 -0
  80. package/dist/extensions/pi-zentui/test/editor-metadata-format.test.ts +405 -0
  81. package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +260 -0
  82. package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +1286 -0
  83. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +7196 -0
  84. package/dist/extensions/pi-zentui/test/extension-status.test.ts +182 -0
  85. package/dist/extensions/pi-zentui/test/fixtures/starship-nerd-font-symbols.toml +172 -0
  86. package/dist/extensions/pi-zentui/test/footer-format.test.ts +431 -0
  87. package/dist/extensions/pi-zentui/test/footer-layout.test.ts +113 -0
  88. package/dist/extensions/pi-zentui/test/footer-repository-path.test.ts +123 -0
  89. package/dist/extensions/pi-zentui/test/format.test.ts +865 -0
  90. package/dist/extensions/pi-zentui/test/git.test.ts +212 -0
  91. package/dist/extensions/pi-zentui/test/icons.test.ts +69 -0
  92. package/dist/extensions/pi-zentui/test/interaction-summary.test.ts +1402 -0
  93. package/dist/extensions/pi-zentui/test/live-context-integration.test.ts +378 -0
  94. package/dist/extensions/pi-zentui/test/live-context.test.ts +139 -0
  95. package/dist/extensions/pi-zentui/test/minimalist-editor.test.ts +811 -0
  96. package/dist/extensions/pi-zentui/test/package-version.test.ts +321 -0
  97. package/dist/extensions/pi-zentui/test/project-refresh.test.ts +250 -0
  98. package/dist/extensions/pi-zentui/test/project-state.test.ts +159 -0
  99. package/dist/extensions/pi-zentui/test/repository-root.test.ts +108 -0
  100. package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +825 -0
  101. package/dist/extensions/pi-zentui/test/runtime.test.ts +183 -0
  102. package/dist/extensions/pi-zentui/test/session-lifecycle.test.ts +40 -0
  103. package/dist/extensions/pi-zentui/test/settings-command.test.ts +1381 -0
  104. package/dist/extensions/pi-zentui/test/settings-previews.test.ts +347 -0
  105. package/dist/extensions/pi-zentui/test/state.test.ts +84 -0
  106. package/dist/extensions/pi-zentui/test/telemetry-lifecycle-integration.test.ts +276 -0
  107. package/dist/extensions/pi-zentui/test/telemetry.test.ts +105 -0
  108. package/dist/extensions/pi-zentui/test/turn-summary-lifecycle.integration.test.ts +327 -0
  109. package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +164 -0
  110. package/dist/extensions/pi-zentui/test/user-message-styles.test.ts +292 -0
  111. package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +730 -0
  112. package/dist/extensions/pi-zentui/test/working-line-messages.test.ts +34 -0
  113. package/dist/extensions/pi-zentui/test/working-line.test.ts +1990 -0
  114. package/dist/extensions/pi-zentui/tsconfig.json +16 -0
  115. package/dist/extensions/tps.test.ts +38 -0
  116. package/dist/extensions/{pi-powerline-footer/tps.ts → tps.ts} +4 -0
  117. package/dist/skills/design-references/SKILL.md +31 -0
  118. package/dist/skills/design-references/references/clickhouse/DESIGN.md +544 -0
  119. package/dist/themes/adventure.json +84 -0
  120. package/dist/themes/adwaita-dark.json +84 -0
  121. package/dist/themes/arcoiris.json +84 -0
  122. package/dist/themes/arthur.json +84 -0
  123. package/dist/themes/atom.json +84 -0
  124. package/dist/themes/aura.json +84 -0
  125. package/dist/themes/black-metal-bathory.json +84 -0
  126. package/dist/themes/black-metal-burzum.json +84 -0
  127. package/dist/themes/black-metal-khold.json +84 -0
  128. package/dist/themes/box.json +84 -0
  129. package/dist/themes/brogrammer.json +84 -0
  130. package/dist/themes/carbonfox.json +84 -0
  131. package/dist/themes/catppuccin-mocha.json +84 -0
  132. package/dist/themes/citruszest.json +84 -0
  133. package/dist/themes/cursor-dark.json +84 -0
  134. package/dist/themes/cutie-pro.json +84 -0
  135. package/dist/themes/dark-modern.json +84 -0
  136. package/dist/themes/dark-pastel.json +84 -0
  137. package/dist/themes/dimmed-monokai.json +84 -0
  138. package/dist/themes/doom-peacock.json +84 -0
  139. package/dist/themes/dracula-plus.json +84 -0
  140. package/dist/themes/dracula.json +80 -0
  141. package/dist/themes/earthsong.json +84 -0
  142. package/dist/themes/everforest-dark-hard.json +84 -0
  143. package/dist/themes/everforest-dark.json +80 -0
  144. package/dist/themes/fahrenheit.json +84 -0
  145. package/dist/themes/flatland.json +84 -0
  146. package/dist/themes/flexoki-dark.json +84 -0
  147. package/dist/themes/front-end-delight.json +84 -0
  148. package/dist/themes/fun-forrest.json +84 -0
  149. package/dist/themes/galizur.json +84 -0
  150. package/dist/themes/github-dark-colorblind.json +84 -0
  151. package/dist/themes/github-dark-high-contrast.json +84 -0
  152. package/dist/themes/glacier.json +84 -0
  153. package/dist/themes/gruber-darker.json +84 -0
  154. package/dist/themes/gruvbox-dark-hard.json +84 -0
  155. package/dist/themes/gruvbox-dark.json +84 -0
  156. package/dist/themes/gruvbox-material.json +84 -0
  157. package/dist/themes/guezwhoz.json +84 -0
  158. package/dist/themes/hacktober.json +84 -0
  159. package/dist/themes/hardcore.json +84 -0
  160. package/dist/themes/havn-skumring.json +84 -0
  161. package/dist/themes/ic-orange-ppl.json +84 -0
  162. package/dist/themes/iterm2-smoooooth.json +84 -0
  163. package/dist/themes/iterm2-tango-dark.json +84 -0
  164. package/dist/themes/japanesque.json +84 -0
  165. package/dist/themes/jellybeans.json +84 -0
  166. package/dist/themes/kanagawa-wave.json +84 -0
  167. package/dist/themes/kurokula.json +84 -0
  168. package/dist/themes/later-this-evening.json +84 -0
  169. package/dist/themes/lovelace.json +84 -0
  170. package/dist/themes/material-darker.json +84 -0
  171. package/dist/themes/matte-black.json +84 -0
  172. package/dist/themes/mellow.json +84 -0
  173. package/dist/themes/miasma.json +84 -0
  174. package/dist/themes/nord.json +80 -0
  175. package/dist/themes/nvim-dark.json +84 -0
  176. package/dist/themes/one-dark.json +80 -0
  177. package/dist/themes/popping-and-locking.json +84 -0
  178. package/dist/themes/rose-pine.json +80 -0
  179. package/dist/themes/sea-shells.json +84 -0
  180. package/dist/themes/sleepy-hollow.json +84 -0
  181. package/dist/themes/smyck.json +84 -0
  182. package/dist/themes/solarized-dark.json +80 -0
  183. package/dist/themes/tokyo-night.json +80 -0
  184. package/dist/themes/tomorrow-night-bright.json +84 -0
  185. package/dist/themes/tomorrow-night-burns.json +84 -0
  186. package/dist/themes/tomorrow-night.json +84 -0
  187. package/dist/themes/twilight.json +84 -0
  188. package/dist/themes/vague.json +84 -0
  189. package/dist/themes/vesper.json +84 -0
  190. package/dist/themes/xcode-dark-hc.json +84 -0
  191. package/docs/custom-provider.md +0 -1
  192. package/package.json +3 -3
  193. package/dist/extensions/auto-model/auto-model.test.ts +0 -18
  194. package/dist/extensions/auto-model/classifier.ts +0 -23
  195. package/dist/extensions/auto-model/config.ts +0 -51
  196. package/dist/extensions/auto-model/index.ts +0 -22
  197. package/dist/extensions/auto-model/lifecycle.test.ts +0 -24
  198. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  199. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  200. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  201. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  202. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  203. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  204. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  205. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  206. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  207. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  208. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  209. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  210. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  211. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  212. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  213. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  214. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  215. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  216. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  217. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  218. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  219. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  220. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  221. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  222. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  223. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  224. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  225. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  226. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  227. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  228. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  229. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  230. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  231. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  232. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  233. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  234. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  235. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  236. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  237. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  238. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  239. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  240. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  241. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  242. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  243. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  244. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  245. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  246. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  247. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  248. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  249. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  250. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  251. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  252. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  253. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  254. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  255. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  256. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  257. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  258. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  259. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  260. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  261. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  262. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  263. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  264. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  265. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  266. package/dist/extensions/preview-tools-disabled.ts +0 -11
  267. package/dist/themes/powerline-footer/theme.json +0 -33
@@ -0,0 +1,1990 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { stripVTControlCharacters } from "node:util";
3
+ import type { Theme } from "@earendil-works/pi-coding-agent";
4
+ import { Loader, visibleWidth } from "@earendil-works/pi-tui";
5
+ import { describe, expect, it, vi } from "vitest";
6
+ import { defaultConfig, type PolishedTuiConfig } from "../extensions/zentui/config";
7
+ import {
8
+ AgentDurationClock,
9
+ BUILT_IN_WORKING_LINE_MESSAGES,
10
+ buildWorkingLineFrames,
11
+ buildWorkingLinePreviewFrames,
12
+ buildWorkingLineSpinnerFrames,
13
+ composeWorkingLineRow,
14
+ effectiveWorkingLineMessages,
15
+ formatWorkingLineThought,
16
+ formatWorkingLineTokens,
17
+ MAX_WORKING_LINE_ENTRIES_EXAMINED,
18
+ MAX_WORKING_LINE_FRAME_CELLS,
19
+ MAX_WORKING_LINE_FRAME_CODE_UNITS,
20
+ MAX_WORKING_LINE_FRAMES,
21
+ MAX_WORKING_LINE_MESSAGE_CELLS,
22
+ MAX_WORKING_LINE_NORMALIZED_CODE_UNITS,
23
+ MAX_WORKING_LINE_RAW_CODE_UNITS,
24
+ MAX_WORKING_LINE_ROW_CELLS,
25
+ MAX_WORKING_LINE_STYLE_CODE_UNITS,
26
+ MAX_WORKING_LINE_STYLE_TOKENS,
27
+ normalizeWorkingLineMessage,
28
+ normalizeWorkingLineMessages,
29
+ normalizeWorkingLineStyleSpec,
30
+ normalizeWorkingLineToolLabel,
31
+ remapWorkingLineTextTick,
32
+ selectWorkingLineMessage,
33
+ snapshotWorkingLineHighStyle,
34
+ WORKING_LINE_SPINNERS,
35
+ WorkingLineController,
36
+ workingLineSpinnerWidth,
37
+ } from "../extensions/zentui/working-line";
38
+
39
+ function theme(): Theme {
40
+ return {
41
+ fg(color: string, text: string) {
42
+ const codes: Record<string, number> = { dim: 90, muted: 36, accent: 96 };
43
+ return `\x1b[${codes[color] ?? 37}m${text}\x1b[0m`;
44
+ },
45
+ bold(text: string) {
46
+ return `\x1b[1m${text}\x1b[0m`;
47
+ },
48
+ } as Theme;
49
+ }
50
+
51
+ function config(): PolishedTuiConfig {
52
+ return structuredClone(defaultConfig);
53
+ }
54
+
55
+ const stripTerminalSequences = stripVTControlCharacters;
56
+
57
+ function strippedFrames(frames: string[]): string[] {
58
+ return frames.map((frame) => stripTerminalSequences(frame));
59
+ }
60
+
61
+ function highTierStart(frame: string): number | undefined {
62
+ const marker = [...frame.matchAll(/\x1b\[96m\x1b\[1m/g)][1];
63
+ if (marker?.index === undefined) return undefined;
64
+ const stripped = stripTerminalSequences(frame);
65
+ const separatorIndex = stripped.indexOf(" ");
66
+ const rowOffset = visibleWidth(stripped.slice(0, separatorIndex + 1));
67
+ return visibleWidth(stripTerminalSequences(frame.slice(0, marker.index))) - rowOffset;
68
+ }
69
+
70
+ function phaseSignature(frame: string): [string, number | undefined] {
71
+ return [stripTerminalSequences(frame).split(" ", 1)[0] ?? "", highTierStart(frame)];
72
+ }
73
+
74
+ function phaseMotionSignature(frames: string[], index: number) {
75
+ const current = phaseSignature(frames[index % frames.length] ?? "");
76
+ const next = phaseSignature(frames[(index + 1) % frames.length] ?? "");
77
+ return {
78
+ spinnerGlyph: current[0],
79
+ textPosition: current[1],
80
+ textDirection:
81
+ current[1] === undefined || next[1] === undefined
82
+ ? undefined
83
+ : Math.sign(next[1] - current[1]),
84
+ };
85
+ }
86
+
87
+ function gcdForTest(left: number, right: number): number {
88
+ let a = left;
89
+ let b = right;
90
+ while (b !== 0) [a, b] = [b, a % b];
91
+ return a;
92
+ }
93
+
94
+ describe("working-line token formatting", () => {
95
+ it("formats approximate and exact output identically while validating metadata", () => {
96
+ expect(
97
+ formatWorkingLineTokens({ input: 27_000, output: 1_400, outputApproximate: false }),
98
+ ).toBe("↑27k ↓1.4k");
99
+ expect(formatWorkingLineTokens({ input: 27_000, output: 1_400, outputApproximate: true })).toBe(
100
+ "↑27k ↓1.4k",
101
+ );
102
+ expect(
103
+ formatWorkingLineTokens({ input: 1, output: 2, outputApproximate: "yes" } as never),
104
+ ).toBeUndefined();
105
+ });
106
+
107
+ it("reserves compact maximum-safe tokens within the complete 80-cell row", () => {
108
+ const current = config();
109
+ const composed = composeWorkingLineRow(current.components.workingLine, "x".repeat(43), {
110
+ tokens: {
111
+ input: Number.MAX_SAFE_INTEGER,
112
+ output: Number.MAX_SAFE_INTEGER,
113
+ outputApproximate: true,
114
+ },
115
+ });
116
+ expect(composed.row).toContain("↑9007199255M ↓9007199255M");
117
+ expect(visibleWidth(composed.row)).toBeLessThanOrEqual(MAX_WORKING_LINE_FRAME_CELLS - 2);
118
+ });
119
+ });
120
+
121
+ describe("working-line presets and frame generation", () => {
122
+ it("ships pinned Funky UI Pulse provenance and the required MIT notices", () => {
123
+ const source = readFileSync(
124
+ new URL("../extensions/zentui/working-line-spinners.ts", import.meta.url),
125
+ "utf8",
126
+ );
127
+ expect(source).toContain(
128
+ "https://github.com/pi-zza/funky-ui/blob/e9aed319a4de61c2b2a5e99009821efa7b67b178/packages/funky-ui/extensions/funky-ui/animations.ts",
129
+ );
130
+ expect(source).toContain("37d82d565c1f5a9aa9b31d4b1711fa5604eb04a1");
131
+ expect(source).toContain("Copyright (c) FammasMaz/pi-cc-tools contributors");
132
+ expect(source).toContain("Copyright (c) 2026 Marko Nakic");
133
+ expect(source).toContain("Permission is hereby granted, free of charge");
134
+ });
135
+
136
+ it("defines the exact fixed-width spinner frames without preset-owned cadences", () => {
137
+ expect(WORKING_LINE_SPINNERS).toEqual({
138
+ braille: {
139
+ frames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
140
+ },
141
+ "star-bloom": {
142
+ frames: ["·", "✦", "✧", "✶", "✧", "✦"],
143
+ },
144
+ pinwheel: { frames: ["-", "\\", "|", "/"] },
145
+ "claude-inspired": { frames: ["·", "✢", "✳", "✶", "✻", "✽"] },
146
+ pulse: { frames: ["⠀⠶⠀", "⠰⣿⠆", "⢾⣉⡷", "⣏⠀⣹", "⡁⠀⢈"] },
147
+ });
148
+ for (const [id, preset] of Object.entries(WORKING_LINE_SPINNERS)) {
149
+ const widths = new Set(preset.frames.map((frame) => visibleWidth(frame)));
150
+ expect(widths).toEqual(new Set([id === "pulse" ? 3 : 1]));
151
+ expect(workingLineSpinnerWidth(id as keyof typeof WORKING_LINE_SPINNERS)).toBe(
152
+ id === "pulse" ? 3 : 1,
153
+ );
154
+ }
155
+ });
156
+
157
+ it("rejects zero-width and inconsistent spinner definitions defensively", () => {
158
+ const mutable = WORKING_LINE_SPINNERS as unknown as { pulse: { frames: string[] } };
159
+ const original = mutable.pulse.frames;
160
+ try {
161
+ mutable.pulse.frames = ["", ""];
162
+ expect(() => workingLineSpinnerWidth("pulse")).toThrow(/positive fixed width/);
163
+ mutable.pulse.frames = ["⠶", "⠰⣿⠆"];
164
+ expect(() => workingLineSpinnerWidth("pulse")).toThrow(/positive fixed width/);
165
+ } finally {
166
+ mutable.pulse.frames = original;
167
+ }
168
+ });
169
+
170
+ it("builds standalone spinner frames for preset validation", () => {
171
+ const current = config();
172
+ current.components.workingLine.messages.custom = false;
173
+ const generated = buildWorkingLineSpinnerFrames(
174
+ current.components.workingLine,
175
+ current.colors,
176
+ theme(),
177
+ );
178
+ expect(strippedFrames(generated.frames)).toEqual(["·", "✦", "✧", "✶", "✧", "✦"]);
179
+ const offset = buildWorkingLineSpinnerFrames(
180
+ current.components.workingLine,
181
+ current.colors,
182
+ theme(),
183
+ 7,
184
+ );
185
+ expect(strippedFrames(offset.frames)).toEqual(["✦", "✧", "✶", "✧", "✦", "·"]);
186
+ expect(generated.frames.some((frame) => frame.includes("Working"))).toBe(false);
187
+ });
188
+
189
+ it("renders exact owned Pulse frames in preset order", () => {
190
+ const current = config();
191
+ current.components.workingLine.spinner = "pulse";
192
+ current.components.workingLine.spinnerIntervalMs = 180;
193
+ current.components.workingLine.textAnimation = "disabled";
194
+ const expected = ["⠀⠶⠀", "⠰⣿⠆", "⢾⣉⡷", "⣏⠀⣹", "⡁⠀⢈"];
195
+ const generated = buildWorkingLineFrames(
196
+ current.components.workingLine,
197
+ current.colors,
198
+ theme(),
199
+ "Ready",
200
+ );
201
+ expect(strippedFrames(generated.frames)).toEqual(expected.map((frame) => `${frame} Ready`));
202
+ expect(generated.intervalMs).toBe(180);
203
+ });
204
+
205
+ it.each([
206
+ ["classic", "braille"],
207
+ ["classic", "pulse"],
208
+ ["kitt", "braille"],
209
+ ["kitt", "pulse"],
210
+ ] as const)("keeps the %s %s spinner fixed while text tiers move", (textAnimation, spinner) => {
211
+ const current = config();
212
+ current.components.workingLine.spinner = spinner;
213
+ current.components.workingLine.messages.custom = true;
214
+ current.components.workingLine.textAnimation = textAnimation;
215
+ current.components.workingLine.colorSource = "terminal";
216
+ current.colors.workingLineLow = "bright-black";
217
+ current.colors.workingLineMid = "cyan";
218
+ current.colors.workingLineHigh = "bold green";
219
+ const generated = buildWorkingLineFrames(
220
+ current.components.workingLine,
221
+ current.colors,
222
+ theme(),
223
+ "Ready",
224
+ { tool: "read", elapsedMs: 1000, tokens: { input: 12, output: 3 } },
225
+ );
226
+ const preset = WORKING_LINE_SPINNERS[spinner];
227
+ const observedGlyphs = new Set<string>();
228
+ const textHighPositions = new Set<number>();
229
+ for (const [index, frame] of generated.frames.entries()) {
230
+ const state = generated.frameStates[index];
231
+ const glyph = preset.frames[(state?.spinnerTick ?? 0) % preset.frames.length] ?? "";
232
+ const spinnerPrefix = `\x1b[1;32m${glyph}\x1b[0m `;
233
+ expect(frame.startsWith(spinnerPrefix)).toBe(true);
234
+ observedGlyphs.add(glyph);
235
+ const text = frame.slice(spinnerPrefix.length);
236
+ for (const match of text.matchAll(/\x1b\[1;32m/g)) {
237
+ textHighPositions.add(visibleWidth(stripTerminalSequences(text.slice(0, match.index))));
238
+ }
239
+ }
240
+ expect(observedGlyphs).toEqual(new Set(preset.frames));
241
+ expect(generated.frames.some((frame) => frame.includes("\x1b[90m"))).toBe(true);
242
+ expect(generated.frames.some((frame) => frame.includes("\x1b[36m"))).toBe(true);
243
+ expect(textHighPositions.size).toBeGreaterThan(1);
244
+ expect(generated.textWidth).toBe(visibleWidth(generated.row));
245
+ expect(generated.textOrigin).toBe(workingLineSpinnerWidth(spinner) + 1);
246
+ });
247
+
248
+ it.each([
249
+ ["classic", "braille"],
250
+ ["classic", "pulse"],
251
+ ["kitt", "braille"],
252
+ ["kitt", "pulse"],
253
+ ] as const)(
254
+ "includes the %s %s spinner and separator in the color sweep",
255
+ (animation, spinner) => {
256
+ const current = config();
257
+ current.components.workingLine.spinner = spinner;
258
+ current.components.workingLine.textAnimation = animation;
259
+ current.components.workingLine.animateSpinnerColor = true;
260
+ current.components.workingLine.colorSource = "terminal";
261
+ current.colors.workingLineLow = "bright-black";
262
+ current.colors.workingLineMid = "cyan";
263
+ current.colors.workingLineHigh = "bold green";
264
+ const generated = buildWorkingLineFrames(
265
+ current.components.workingLine,
266
+ current.colors,
267
+ theme(),
268
+ "Ready",
269
+ );
270
+ expect(generated.textOrigin).toBe(0);
271
+ expect(generated.textWidth).toBe(
272
+ workingLineSpinnerWidth(spinner) + 1 + visibleWidth(generated.row),
273
+ );
274
+ expect(generated.frames.some((frame) => frame.startsWith("\x1b[90m"))).toBe(true);
275
+ expect(generated.frames.some((frame) => frame.startsWith("\x1b[1;32m"))).toBe(true);
276
+ },
277
+ );
278
+
279
+ it("keeps Static rendering identical when spinner-color participation changes", () => {
280
+ const current = config();
281
+ current.components.workingLine.spinner = "pulse";
282
+ current.components.workingLine.textAnimation = "disabled";
283
+ const fixed = buildWorkingLineFrames(
284
+ current.components.workingLine,
285
+ current.colors,
286
+ theme(),
287
+ "Ready",
288
+ );
289
+ current.components.workingLine.animateSpinnerColor = true;
290
+ const ignored = buildWorkingLineFrames(
291
+ current.components.workingLine,
292
+ current.colors,
293
+ theme(),
294
+ "Ready",
295
+ );
296
+ expect(ignored.frames).toEqual(fixed.frames);
297
+ expect(ignored.frameStates).toEqual(fixed.frameStates);
298
+ });
299
+
300
+ it.each([30, 100, 180, 1000] as const)(
301
+ "keeps the canonical configured %s ms cadence for Pulse",
302
+ (intervalMs) => {
303
+ const current = config();
304
+ current.components.workingLine.spinner = "pulse";
305
+ current.components.workingLine.spinnerIntervalMs = intervalMs;
306
+ const owned = buildWorkingLineFrames(
307
+ { ...current.components.workingLine, messages: { custom: true, values: [] } },
308
+ current.colors,
309
+ theme(),
310
+ "Ready",
311
+ );
312
+ expect(owned.intervalMs).toBeGreaterThanOrEqual(30);
313
+ expect(owned.scheduler.effectiveSpinnerIntervalMs).toBeCloseTo(intervalMs, 8);
314
+ },
315
+ );
316
+
317
+ it.each([
318
+ ["classic", 160, 30],
319
+ ["kitt", 40, 30],
320
+ ["disabled", 6, 100],
321
+ ] as const)("generates deterministic %s full-row frames", (textAnimation, count, intervalMs) => {
322
+ const current = config();
323
+ current.components.workingLine.textAnimation = textAnimation;
324
+ const result = buildWorkingLineFrames(
325
+ current.components.workingLine,
326
+ current.colors,
327
+ theme(),
328
+ "Working…",
329
+ );
330
+ expect(result.frames).toHaveLength(count);
331
+ expect(result.intervalMs).toBe(intervalMs);
332
+ expect(result.frames.every((frame) => frame.endsWith("\x1b[0m"))).toBe(true);
333
+ expect(new Set(strippedFrames(result.frames))).toEqual(
334
+ new Set(["· Working…", "✦ Working…", "✧ Working…", "✶ Working…"]),
335
+ );
336
+ expect(result.frames.some((frame) => frame.includes("\x1b[1m"))).toBe(
337
+ textAnimation !== "disabled",
338
+ );
339
+ });
340
+
341
+ it.each([
342
+ [100, 60],
343
+ [60, 40],
344
+ [160, 100],
345
+ ] as const)("schedules exact independent %s/%s ms cadences", (spinnerMs, textMs) => {
346
+ const current = config();
347
+ current.components.workingLine.spinnerIntervalMs = spinnerMs;
348
+ current.components.workingLine.textIntervalMs = textMs;
349
+ const generated = buildWorkingLineFrames(
350
+ current.components.workingLine,
351
+ current.colors,
352
+ theme(),
353
+ "Working…",
354
+ );
355
+ expect(generated.scheduler).toMatchObject({
356
+ effectiveSpinnerIntervalMs: spinnerMs,
357
+ effectiveTextIntervalMs: textMs,
358
+ exact: true,
359
+ spinnerSeamless: true,
360
+ textSeamless: true,
361
+ });
362
+ expect(generated.intervalMs).toBe(Math.max(30, gcdForTest(spinnerMs, textMs)));
363
+ });
364
+
365
+ it("regenerates repeated worst-case streaming rows within a bounded budget", () => {
366
+ const current = config();
367
+ const component = current.components.workingLine;
368
+ component.spinner = "star-bloom";
369
+ component.spinnerIntervalMs = 997;
370
+ component.textIntervalMs = 900;
371
+ component.textAnimation = "classic";
372
+ component.animateSpinnerColor = false;
373
+ const started = performance.now();
374
+ let generatedFrames = 0;
375
+ for (let output = 1; output <= 20; output++) {
376
+ const generated = buildWorkingLineFrames(
377
+ component,
378
+ current.colors,
379
+ theme(),
380
+ "m".repeat(MAX_WORKING_LINE_MESSAGE_CELLS),
381
+ {
382
+ tool: "t".repeat(18),
383
+ elapsedMs: 99_999_999,
384
+ thought: { durationMs: 9_999_999, active: true },
385
+ tokens: { input: 999_999_999, output, outputApproximate: true },
386
+ },
387
+ );
388
+ generatedFrames += generated.frames.length;
389
+ expect(generated.frames.length).toBeLessThanOrEqual(MAX_WORKING_LINE_FRAMES);
390
+ expect(generated.frames.reduce((sum, frame) => sum + frame.length, 0)).toBeLessThanOrEqual(
391
+ MAX_WORKING_LINE_FRAME_CODE_UNITS,
392
+ );
393
+ }
394
+ expect(generatedFrames).toBe(19_940);
395
+ expect(performance.now() - started).toBeLessThan(10_000);
396
+ });
397
+
398
+ it("permits one Classic text reset per bounded fallback array cycle", () => {
399
+ const current = config();
400
+ current.components.workingLine.spinner = "star-bloom";
401
+ current.components.workingLine.textAnimation = "classic";
402
+ current.components.workingLine.spinnerIntervalMs = 997;
403
+ current.components.workingLine.textIntervalMs = 900;
404
+ current.components.workingLine.animateSpinnerColor = false;
405
+ const started = performance.now();
406
+ const generated = buildWorkingLineFrames(
407
+ current.components.workingLine,
408
+ current.colors,
409
+ theme(),
410
+ "1234567",
411
+ );
412
+ expect(generated.frames).toHaveLength(997);
413
+ expect(generated.frames.length).toBeLessThanOrEqual(MAX_WORKING_LINE_FRAMES);
414
+ expect(generated.frames.reduce((sum, frame) => sum + frame.length, 0)).toBeLessThanOrEqual(
415
+ MAX_WORKING_LINE_FRAME_CODE_UNITS,
416
+ );
417
+ expect(generated.scheduler).toMatchObject({
418
+ quantumMs: 30,
419
+ effectiveSpinnerIntervalMs: 997,
420
+ exact: false,
421
+ spinnerSeamless: true,
422
+ textSeamless: false,
423
+ spinnerAdvances: 30,
424
+ textAdvances: 33,
425
+ });
426
+ expect(generated.textWidth).toBe(7);
427
+ const textCycle = 15;
428
+ expect(new Set(generated.frameStates.map(({ textTick }) => textTick)).size).toBe(textCycle);
429
+ expect(generated.scheduler.textAdvances % textCycle).toBe(3);
430
+ expect(generated.scheduler.effectiveTextIntervalMs).toBeCloseTo(29910 / 33, 8);
431
+ expect(performance.now() - started).toBeLessThan(2000);
432
+ });
433
+
434
+ it.each([
435
+ ["braille", 10],
436
+ ["star-bloom", 6],
437
+ ["pinwheel", 4],
438
+ ["claude-inspired", 6],
439
+ ["pulse", 5],
440
+ ] as const)("uses configured cadence with the %s static frames", (spinner, frameCount) => {
441
+ const current = config();
442
+ current.components.workingLine.spinner = spinner;
443
+ current.components.workingLine.spinnerIntervalMs = 160;
444
+ current.components.workingLine.textAnimation = "disabled";
445
+ const result = buildWorkingLineFrames(
446
+ current.components.workingLine,
447
+ current.colors,
448
+ theme(),
449
+ "Ready",
450
+ );
451
+ expect(result.intervalMs).toBe(160);
452
+ expect(result.frames).toHaveLength(frameCount);
453
+ });
454
+
455
+ it.each([
456
+ [
457
+ "classic",
458
+ { braille: 170, "star-bloom": 510, pinwheel: 340, "claude-inspired": 510, pulse: 85 },
459
+ ],
460
+ [
461
+ "kitt",
462
+ { braille: 790, "star-bloom": 474, pinwheel: 316, "claude-inspired": 474, pulse: 790 },
463
+ ],
464
+ ] as const)(
465
+ "keeps every maximum-width %s spinner animated within hard caps",
466
+ (animation, _counts) => {
467
+ const current = config();
468
+ current.components.workingLine.messages.custom = true;
469
+ current.components.workingLine.textAnimation = animation;
470
+ const runtime = {
471
+ tool: "y".repeat(18),
472
+ elapsedMs: 3_600_000,
473
+ tokens: { input: 12, output: 2 },
474
+ };
475
+ for (const spinner of Object.keys(WORKING_LINE_SPINNERS) as Array<
476
+ keyof typeof WORKING_LINE_SPINNERS
477
+ >) {
478
+ current.components.workingLine.spinner = spinner;
479
+ const generated = buildWorkingLineFrames(
480
+ current.components.workingLine,
481
+ current.colors,
482
+ theme(),
483
+ "x".repeat(MAX_WORKING_LINE_MESSAGE_CELLS),
484
+ runtime,
485
+ );
486
+ expect(visibleWidth(stripTerminalSequences(generated.frames[0] ?? ""))).toBe(
487
+ MAX_WORKING_LINE_FRAME_CELLS,
488
+ );
489
+ expect(generated.frames.length).toBeGreaterThan(
490
+ WORKING_LINE_SPINNERS[spinner].frames.length,
491
+ );
492
+ expect(generated.textAnimation).toBe(animation);
493
+ expect(generated.frames.length).toBeLessThanOrEqual(MAX_WORKING_LINE_FRAMES);
494
+ expect(generated.frames.reduce((sum, frame) => sum + frame.length, 0)).toBeLessThanOrEqual(
495
+ MAX_WORKING_LINE_FRAME_CODE_UNITS,
496
+ );
497
+ }
498
+ },
499
+ );
500
+
501
+ it.each([
502
+ ["classic", 170],
503
+ ["kitt", 790],
504
+ ] as const)(
505
+ "keeps maximum-code-unit Braille %s rows grapheme-complete and under the memory cap",
506
+ (animation, _count) => {
507
+ const combiningMessage = `x${"x".repeat(41)}y${"\u0338".repeat(213)}`;
508
+ const zwjMessage = `${"x".repeat(41)}${"👩‍".repeat(71)}💻`;
509
+ for (const message of [combiningMessage, zwjMessage]) {
510
+ expect(message).toHaveLength(MAX_WORKING_LINE_NORMALIZED_CODE_UNITS);
511
+ expect(visibleWidth(message)).toBe(MAX_WORKING_LINE_MESSAGE_CELLS);
512
+ expect(normalizeWorkingLineMessage(`${message}z`)).toBe(message);
513
+
514
+ const current = config();
515
+ current.components.workingLine.spinner = "braille";
516
+ current.components.workingLine.messages.custom = true;
517
+ current.components.workingLine.textAnimation = animation;
518
+ current.components.workingLine.colorSource = "terminal";
519
+ const maximumStyle = "underline fg:#ffffff bg:#ffffff";
520
+ current.colors.workingLineLow = maximumStyle;
521
+ current.colors.workingLineMid = maximumStyle;
522
+ current.colors.workingLineHigh = maximumStyle;
523
+ const generated = buildWorkingLineFrames(
524
+ current.components.workingLine,
525
+ current.colors,
526
+ theme(),
527
+ `${message}z`,
528
+ {
529
+ tool: "y".repeat(18),
530
+ elapsedMs: 3_600_000,
531
+ tokens: { input: 1, output: 2 },
532
+ },
533
+ );
534
+ expect(generated.message).toBe(message);
535
+ expect(generated.frames.length).toBeGreaterThan(
536
+ WORKING_LINE_SPINNERS.braille.frames.length,
537
+ );
538
+ expect(generated.textAnimation).toBe(animation);
539
+ expect(visibleWidth(stripTerminalSequences(generated.frames[0] ?? ""))).toBe(
540
+ MAX_WORKING_LINE_FRAME_CELLS,
541
+ );
542
+ expect(generated.frames.reduce((sum, frame) => sum + frame.length, 0)).toBeLessThanOrEqual(
543
+ MAX_WORKING_LINE_FRAME_CODE_UNITS,
544
+ );
545
+ for (const frame of strippedFrames(generated.frames))
546
+ expect(frame.slice(2)).toBe(generated.row);
547
+ }
548
+ },
549
+ );
550
+
551
+ it.each(["classic", "kitt"] as const)(
552
+ "deduplicates adversarial repeated palette tokens without disabling %s animation",
553
+ (animation) => {
554
+ const current = config();
555
+ current.components.workingLine.spinner = "braille";
556
+ current.components.workingLine.messages.custom = true;
557
+ current.components.workingLine.textAnimation = animation;
558
+ current.components.workingLine.colorSource = "terminal";
559
+ current.colors.workingLineLow = "dim ".repeat(1000);
560
+ current.colors.workingLineMid = "cyan ".repeat(1000);
561
+ current.colors.workingLineHigh = "bold ".repeat(1000);
562
+ const generated = buildWorkingLineFrames(
563
+ current.components.workingLine,
564
+ current.colors,
565
+ theme(),
566
+ "x".repeat(MAX_WORKING_LINE_MESSAGE_CELLS),
567
+ { tool: "y".repeat(18), elapsedMs: 3_600_000, tokens: { input: 1, output: 2 } },
568
+ );
569
+ expect(generated.textAnimation).toBe(animation);
570
+ expect(generated.frames.length).toBeGreaterThan(WORKING_LINE_SPINNERS.braille.frames.length);
571
+ expect(generated.frames.reduce((sum, frame) => sum + frame.length, 0)).toBeLessThanOrEqual(
572
+ MAX_WORKING_LINE_FRAME_CODE_UNITS,
573
+ );
574
+ expect(generated.frames.some((frame) => frame.includes("\x1b[1m"))).toBe(true);
575
+ },
576
+ );
577
+
578
+ it("bounds and canonicalizes Working-line-only palette specs", () => {
579
+ expect(normalizeWorkingLineStyleSpec("bold ".repeat(1000))).toBe("bold");
580
+ expect(normalizeWorkingLineStyleSpec("DIMMED dim fg:202 FG:202 bold bold")).toBe(
581
+ "dim fg:202 bold",
582
+ );
583
+ expect(normalizeWorkingLineStyleSpec("bold italic underline red")).toBe(
584
+ "bold italic underline red",
585
+ );
586
+ expect(normalizeWorkingLineStyleSpec("bold italic underline red dim")).toBeUndefined();
587
+ expect(MAX_WORKING_LINE_STYLE_TOKENS).toBe(4);
588
+ expect(MAX_WORKING_LINE_STYLE_CODE_UNITS).toBe(48);
589
+ });
590
+
591
+ it.each([
592
+ ["no modifier", [], "\x1b[38;5;202m"],
593
+ ["one modifier", ["bold"], "\x1b[38;5;202m\x1b[1m"],
594
+ ["two modifiers", ["bold", "italic"], "\x1b[38;5;202m\x1b[3m\x1b[1m"],
595
+ ["three modifiers", ["bold", "italic", "underline"], "\x1b[38;5;202m\x1b[4m\x1b[3m\x1b[1m"],
596
+ ] as const)("persists theme high style with %s", (_label, modifiers, expected) => {
597
+ const current = config();
598
+ current.components.workingLine.colorSource = "theme";
599
+ current.colors.workingLineHigh = [...modifiers, "accent"].join(" ");
600
+ const separateTheme = {
601
+ fg: (_color: string, text: string) => `\x1b[38;5;202m${text}\x1b[0m`,
602
+ bold: (text: string) => `\x1b[1m${text}\x1b[0m`,
603
+ italic: (text: string) => `\x1b[3m${text}\x1b[0m`,
604
+ underline: (text: string) => `\x1b[4m${text}\x1b[0m`,
605
+ };
606
+ expect(
607
+ snapshotWorkingLineHighStyle(separateTheme, current.components.workingLine, current.colors),
608
+ ).toBe(expected);
609
+ });
610
+
611
+ it.each([
612
+ ["named", "bold italic underline cyan", "\x1b[1;3;4;36m"],
613
+ ["256-color", "bold italic underline fg:202", "\x1b[1;3;4;38;5;202m"],
614
+ ["truecolor", "bold italic underline #bf5700", "\x1b[1;3;4;38;2;191;87;0m"],
615
+ ] as const)("persists combined terminal %s high style", (_label, style, expected) => {
616
+ const current = config();
617
+ current.components.workingLine.colorSource = "terminal";
618
+ current.colors.workingLineHigh = style;
619
+ expect(
620
+ snapshotWorkingLineHighStyle(theme(), current.components.workingLine, current.colors),
621
+ ).toBe(expected);
622
+ });
623
+
624
+ it("reverses full-row KITT motion without duplicating the wording endpoint", () => {
625
+ const current = config();
626
+ current.components.workingLine.textAnimation = "kitt";
627
+ const { frames } = buildWorkingLineFrames(
628
+ current.components.workingLine,
629
+ current.colors,
630
+ theme(),
631
+ "abcdefgh",
632
+ );
633
+ const brightHead = (frame: string) => frame.match(/\x1b\[96m\x1b\[1m([a-h])/)?.[1];
634
+ const movingHeads = frames
635
+ .map(brightHead)
636
+ .filter((value): value is string => value !== undefined)
637
+ .filter((value, index, values) => index === 0 || value !== values[index - 1]);
638
+ expect(movingHeads.slice(0, 9)).toEqual(["a", "b", "c", "d", "e", "f", "g", "h", "g"]);
639
+ });
640
+
641
+ it.each(["你好世界", "e\u0301lan", "👩‍💻 works", "👨‍👩‍👧‍👦 family"])(
642
+ "keeps graphemes and visible text stable for %s",
643
+ (message) => {
644
+ const current = config();
645
+ const result = buildWorkingLineFrames(
646
+ current.components.workingLine,
647
+ current.colors,
648
+ theme(),
649
+ message,
650
+ );
651
+ const stripped = strippedFrames(result.frames);
652
+ const expectedWidth = visibleWidth(
653
+ `${WORKING_LINE_SPINNERS["star-bloom"].frames[0]} ${result.message}`,
654
+ );
655
+ for (const frame of stripped) {
656
+ expect(frame.slice(2)).toBe(result.message);
657
+ expect(visibleWidth(frame)).toBe(expectedWidth);
658
+ }
659
+ },
660
+ );
661
+
662
+ it.each(["界", "😀"])("gives every occupied KITT head cell the high tier for %s", (message) => {
663
+ const current = config();
664
+ current.components.workingLine.textAnimation = "kitt";
665
+ const result = buildWorkingLineFrames(
666
+ current.components.workingLine,
667
+ current.colors,
668
+ theme(),
669
+ `${message}a`,
670
+ );
671
+ expect(result.frames.some((frame) => frame.includes(`\x1b[96m\x1b[1m${message}`))).toBe(true);
672
+ const expectedWidth = visibleWidth(`· ${result.message}`);
673
+ for (const frame of strippedFrames(result.frames)) {
674
+ expect(frame.slice(2)).toBe(result.message);
675
+ expect(visibleWidth(frame)).toBe(expectedWidth);
676
+ }
677
+ });
678
+
679
+ it.each(["star-bloom", "pulse"] as const)(
680
+ "fits the complete maximum %s row through Pi's real Loader without a wrapped blank row",
681
+ (spinner) => {
682
+ const current = config();
683
+ current.components.workingLine.spinner = spinner;
684
+ current.components.workingLine.textAnimation = "disabled";
685
+ current.components.workingLine.messages.custom = true;
686
+ const generated = buildWorkingLineFrames(
687
+ current.components.workingLine,
688
+ current.colors,
689
+ theme(),
690
+ "x".repeat(MAX_WORKING_LINE_MESSAGE_CELLS),
691
+ {
692
+ tool: "y".repeat(18),
693
+ elapsedMs: 3_600_000,
694
+ tokens: { input: 12, output: 2 },
695
+ },
696
+ );
697
+ const [frame] = generated.frames;
698
+ expect(visibleWidth(stripTerminalSequences(frame))).toBe(MAX_WORKING_LINE_FRAME_CELLS);
699
+ const loader = new Loader(
700
+ { requestRender() {} } as never,
701
+ (text) => text,
702
+ (text) => text,
703
+ "",
704
+ { frames: [frame], intervalMs: generated.intervalMs },
705
+ );
706
+ try {
707
+ const rendered = loader.render(MAX_WORKING_LINE_ROW_CELLS);
708
+ expect(rendered).toHaveLength(2);
709
+ expect(stripTerminalSequences(rendered[1] ?? "")).toBe(
710
+ ` ${stripTerminalSequences(frame)} `,
711
+ );
712
+ expect(visibleWidth(rendered[1] ?? "")).toBe(MAX_WORKING_LINE_ROW_CELLS);
713
+ } finally {
714
+ loader.stop();
715
+ }
716
+ },
717
+ );
718
+
719
+ it("compensates for the real Loader resetting every installed indicator to frame zero", () => {
720
+ vi.useFakeTimers();
721
+ const loader = new Loader(
722
+ { requestRender() {} } as never,
723
+ (text) => text,
724
+ (text) => text,
725
+ "",
726
+ { frames: ["zero", "one", "two"], intervalMs: 30 },
727
+ );
728
+ try {
729
+ vi.advanceTimersByTime(60);
730
+ expect(stripTerminalSequences(loader.render(20)[1] ?? "")).toContain("two");
731
+ loader.setIndicator({ frames: ["sampled-two", "next"], intervalMs: 30 });
732
+ expect(stripTerminalSequences(loader.render(20)[1] ?? "")).toContain("sampled-two");
733
+ } finally {
734
+ loader.stop();
735
+ vi.useRealTimers();
736
+ }
737
+ });
738
+
739
+ it("honors independent working-line palette overrides", () => {
740
+ const current = config();
741
+ current.components.workingLine.colorSource = "terminal";
742
+ current.components.workingLine.textAnimation = "disabled";
743
+ current.colors.workingLineMid = "bold red";
744
+ current.colors.workingLineHigh = "bold green";
745
+ const [frame] = buildWorkingLineFrames(
746
+ current.components.workingLine,
747
+ current.colors,
748
+ theme(),
749
+ "Ready",
750
+ ).frames;
751
+ expect(frame).not.toContain("\x1b[1;32m");
752
+ expect(frame).toContain("\x1b[1;31m· Ready");
753
+ });
754
+
755
+ it.each(["classic", "kitt"] as const)(
756
+ "moves %s color through the complete composed row and preserves phase offsets",
757
+ (animation) => {
758
+ const current = config();
759
+ current.components.workingLine.messages.custom = true;
760
+ current.components.workingLine.textAnimation = animation;
761
+ const runtime = { tool: "read", elapsedMs: 62_000, tokens: { input: 12, output: 3 } };
762
+ const generated = buildWorkingLineFrames(
763
+ current.components.workingLine,
764
+ current.colors,
765
+ theme(),
766
+ "Ready",
767
+ runtime,
768
+ );
769
+ expect(new Set(strippedFrames(generated.frames))).toEqual(
770
+ new Set([
771
+ "· Ready · read · 1m02s · ↑12 ↓3",
772
+ "✦ Ready · read · 1m02s · ↑12 ↓3",
773
+ "✧ Ready · read · 1m02s · ↑12 ↓3",
774
+ "✶ Ready · read · 1m02s · ↑12 ↓3",
775
+ ]),
776
+ );
777
+ expect(
778
+ generated.frames.some((frame) =>
779
+ [...frame.matchAll(/\x1b\[96m\x1b\[1m([^\x1b]*)/g)].some((match) =>
780
+ match[1]?.includes("↓"),
781
+ ),
782
+ ),
783
+ ).toBe(true);
784
+ const offset = buildWorkingLineFrames(
785
+ current.components.workingLine,
786
+ current.colors,
787
+ theme(),
788
+ "Ready",
789
+ runtime,
790
+ 7,
791
+ 7,
792
+ );
793
+ expect(offset.frameStates[0]).toEqual({ spinnerTick: 7, textTick: 7 });
794
+ },
795
+ );
796
+
797
+ it("keeps preview mode and configured segment presence stable", () => {
798
+ const current = config();
799
+ current.components.workingLine.messages = { custom: true, values: ["Configured"] };
800
+ current.components.workingLine.segments = {
801
+ tool: true,
802
+ elapsed: false,
803
+ thought: false,
804
+ tokens: true,
805
+ };
806
+ const generated = buildWorkingLinePreviewFrames(
807
+ current.components.workingLine,
808
+ current.colors,
809
+ theme(),
810
+ );
811
+ for (const frame of strippedFrames(generated.frames)) {
812
+ expect(frame).toMatch(/^[·✦✧✶] Configured · read · ↑1.2k ↓56$/);
813
+ expect(frame).not.toContain("1m02s");
814
+ }
815
+ current.components.workingLine.messages.custom = false;
816
+ const fallback = strippedFrames(
817
+ buildWorkingLinePreviewFrames(current.components.workingLine, current.colors, theme()).frames,
818
+ );
819
+ expect(fallback.every((frame) => frame.includes(" Working… · read · ↑1.2k ↓56"))).toBe(true);
820
+ });
821
+ });
822
+
823
+ describe("working-line phase mapping", () => {
824
+ it.each([
825
+ ["classic", 5],
826
+ ["kitt", 15],
827
+ ] as const)(
828
+ "preserves %s position and direction across spinner-color domain changes",
829
+ (animation, tick) => {
830
+ const included = remapWorkingLineTextTick(animation, 10, tick, animation, 12, 2, 0);
831
+ const restored = remapWorkingLineTextTick(animation, 12, included, animation, 10, 0, 2);
832
+ expect(restored).toBe(tick);
833
+ },
834
+ );
835
+ });
836
+
837
+ describe("working-line message normalization and selection", () => {
838
+ it("strips terminal/control/bidi data, normalizes whitespace, and truncates safely", () => {
839
+ const unsafe =
840
+ "\x1b[31m hello\x1b[0m\tworld\n\x1b]8;;https://bad.test\x07link\x1b]8;;\x07\u202E ";
841
+ expect(normalizeWorkingLineMessage(unsafe)).toBe("hello world link");
842
+ expect(normalizeWorkingLineMessage(`${"界".repeat(30)}👩‍💻`)).toBe(
843
+ "界".repeat(Math.floor(MAX_WORKING_LINE_MESSAGE_CELLS / 2)),
844
+ );
845
+ expect(visibleWidth(normalizeWorkingLineMessage("a".repeat(100)))).toBe(
846
+ MAX_WORKING_LINE_MESSAGE_CELLS,
847
+ );
848
+ });
849
+
850
+ it("removes C1 sequences and embedded invisible controls while preserving grapheme joiners", () => {
851
+ const unsafe =
852
+ "\u009b31mRed\u009b0m \u009d2;title\u0007Visible \u0090secret\u009c Done\u200b\u206a\u206f";
853
+ expect(normalizeWorkingLineMessage(unsafe)).toBe("Red Visible Done");
854
+ expect(normalizeWorkingLineMessage("\u200b\u206a\u206f\u034f")).toBe("");
855
+ expect(normalizeWorkingLineMessage("pay\u034fpal word\u2060joiner bidi\u200emark")).toBe(
856
+ "paypal wordjoiner bidimark",
857
+ );
858
+ expect(normalizeWorkingLineMessage("👩‍💻 ❤️ 👨‍👩‍👧‍👦 क्‍ष")).toBe("👩‍💻 ❤️ 👨‍👩‍👧‍👦 क्‍ष");
859
+ });
860
+
861
+ it("bounds raw normalization and invalid-entry examination before output caps", () => {
862
+ expect(
863
+ normalizeWorkingLineMessage(
864
+ `${"\u200b".repeat(MAX_WORKING_LINE_RAW_CODE_UNITS)}Visible beyond the raw cap`,
865
+ ),
866
+ ).toBe("");
867
+ expect(normalizeWorkingLineMessage("x".repeat(1_000_000))).toBe(
868
+ "x".repeat(MAX_WORKING_LINE_MESSAGE_CELLS),
869
+ );
870
+ expect(
871
+ normalizeWorkingLineMessage(`${"\u200b".repeat(MAX_WORKING_LINE_RAW_CODE_UNITS - 1)}👩‍💻`),
872
+ ).toBe("");
873
+ const combiningGrapheme = `x${"\u0338".repeat(20)}`;
874
+ const combiningOutput = normalizeWorkingLineMessage(combiningGrapheme.repeat(43));
875
+ expect(combiningOutput).toBe(
876
+ combiningGrapheme.repeat(
877
+ Math.floor(MAX_WORKING_LINE_NORMALIZED_CODE_UNITS / combiningGrapheme.length),
878
+ ),
879
+ );
880
+ expect(combiningOutput.length).toBeLessThanOrEqual(MAX_WORKING_LINE_NORMALIZED_CODE_UNITS);
881
+ const oversizedZwjGrapheme = `${"👩‍".repeat(100)}💻`;
882
+ expect(normalizeWorkingLineMessage(oversizedZwjGrapheme)).toBe("");
883
+ const fallback = buildWorkingLineFrames(
884
+ config().components.workingLine,
885
+ config().colors,
886
+ theme(),
887
+ oversizedZwjGrapheme,
888
+ );
889
+ expect(fallback.message).toBe("Working…");
890
+ expect(fallback.frames.reduce((sum, frame) => sum + frame.length, 0)).toBeLessThanOrEqual(
891
+ MAX_WORKING_LINE_FRAME_CODE_UNITS,
892
+ );
893
+ const values = [
894
+ ...Array.from({ length: 31 }, (_, index) => `valid-${index}`),
895
+ ...Array.from({ length: MAX_WORKING_LINE_ENTRIES_EXAMINED - 32 }, () => "\u200b"),
896
+ "last-valid",
897
+ "not-examined",
898
+ ];
899
+ const normalized = normalizeWorkingLineMessages(values);
900
+ expect(normalized).toHaveLength(32);
901
+ expect(normalized.at(-1)).toBe("last-valid");
902
+ expect(normalized).not.toContain("not-examined");
903
+ });
904
+
905
+ it("deduplicates normalized values, discards empties, and caps the pool", () => {
906
+ const values = [
907
+ " A ",
908
+ "A",
909
+ "\x1b[31mA\x1b[0m",
910
+ "\n",
911
+ ...Array.from({ length: 60 }, (_, index) => `m${index}`),
912
+ ];
913
+ const normalized = normalizeWorkingLineMessages(values);
914
+ expect(normalized[0]).toBe("A");
915
+ expect(normalized).toHaveLength(48);
916
+ expect(new Set(normalized).size).toBe(48);
917
+ });
918
+
919
+ it("supports custom toggle, fallback, and uniform endpoint-safe selection", () => {
920
+ const current = config().components.workingLine;
921
+ expect(BUILT_IN_WORKING_LINE_MESSAGES).toHaveLength(16);
922
+ expect(effectiveWorkingLineMessages(current)).toEqual(BUILT_IN_WORKING_LINE_MESSAGES);
923
+ current.messages = { custom: true, values: ["Custom"] };
924
+ expect(effectiveWorkingLineMessages(current)).toEqual(["Custom"]);
925
+ current.messages.values = [];
926
+ expect(effectiveWorkingLineMessages(current)).toEqual(["Working…"]);
927
+ current.messages.values = ["\u200b\u206a"];
928
+ expect(effectiveWorkingLineMessages(current)).toEqual(["Working…"]);
929
+ current.messages.custom = false;
930
+ const random = vi.fn(() => 0.9);
931
+ expect(selectWorkingLineMessage(current, random)).toBe("Working…");
932
+ expect(random).not.toHaveBeenCalled();
933
+ current.messages.custom = true;
934
+ current.messages.values = ["A", "B"];
935
+ expect(selectWorkingLineMessage(current, () => 0)).toBe("A");
936
+ expect(selectWorkingLineMessage(current, () => 0.999999)).toBe("B");
937
+ expect(selectWorkingLineMessage(current, () => 1)).toBe("B");
938
+ });
939
+ });
940
+
941
+ describe("working-line row composition", () => {
942
+ it("sanitizes tools and applies bounded wording, tool, elapsed, token priority", () => {
943
+ const component = config().components.workingLine;
944
+ component.messages.custom = true;
945
+ expect(
946
+ normalizeWorkingLineToolLabel(
947
+ "\x1b[31mread\x1b[0m \x1b]2;title\x07safe \u009b32mtool\u009b0m \u009dsecret\u009c",
948
+ ),
949
+ ).toBe("read safe tool");
950
+ expect(normalizeWorkingLineToolLabel("a".repeat(30))).toBe(`${"a".repeat(17)}…`);
951
+ const all = composeWorkingLineRow(component, "Working…", {
952
+ tool: "read",
953
+ elapsedMs: 62_000,
954
+ tokens: { input: 1234, output: 56 },
955
+ });
956
+ expect(all.row).toBe("Working… · read · 1m02s · ↑1.2k ↓56");
957
+ expect(2 + visibleWidth(all.row) + 1 + 2).toBeLessThanOrEqual(MAX_WORKING_LINE_ROW_CELLS);
958
+ component.spinner = "pulse";
959
+ const constrained = composeWorkingLineRow(component, "x".repeat(43), {
960
+ tool: "parallel-tool-label-too-long",
961
+ elapsedMs: 360_000_000,
962
+ tokens: { input: Number.MAX_SAFE_INTEGER, output: Number.MAX_SAFE_INTEGER },
963
+ });
964
+ expect(constrained.row).toBe(`${"x".repeat(43)} · ↑9007199255M ↓9007199255M`);
965
+ expect(workingLineSpinnerWidth("pulse") + 1 + visibleWidth(constrained.row)).toBeLessThan(
966
+ MAX_WORKING_LINE_FRAME_CELLS,
967
+ );
968
+ expect(constrained.row).not.toContain("parallel");
969
+ });
970
+
971
+ it("reserves exact Tokens, then allocates Message, Elapsed, and truncated Tool within 80 cells", () => {
972
+ const component = config().components.workingLine;
973
+ const composed = composeWorkingLineRow(component, "x".repeat(43), {
974
+ tool: "parallel-tool-label",
975
+ elapsedMs: 62_000,
976
+ tokens: { input: 12, output: 3 },
977
+ });
978
+ expect(composed.row).toBe(`${"x".repeat(43)} · parallel-to… · 1m02s · ↑12 ↓3`);
979
+ expect(composed.row.indexOf("1m02s")).toBeLessThan(composed.row.indexOf("↑12 ↓3"));
980
+ expect(composed.row).not.toContain("parallel-tool-label");
981
+ expect(workingLineSpinnerWidth(component.spinner) + 1 + visibleWidth(composed.row) + 3).toBe(
982
+ MAX_WORKING_LINE_ROW_CELLS,
983
+ );
984
+ });
985
+
986
+ it("honors independent segment toggles for custom and fallback wording", () => {
987
+ const component = config().components.workingLine;
988
+ const runtime = { tool: "read", elapsedMs: 1_000, tokens: { input: 12, output: 3 } };
989
+ component.segments = { tool: false, elapsed: true, thought: false, tokens: false };
990
+ expect(composeWorkingLineRow(component, "Configured", runtime).row).toBe("Configured · 1s");
991
+ component.messages.custom = false;
992
+ expect(composeWorkingLineRow(component, "Working…", runtime).row).toBe("Working… · 1s");
993
+ });
994
+ });
995
+
996
+ describe("working-line thought segment", () => {
997
+ it("formats active zero and floors active/inactive duration consistently", () => {
998
+ expect(formatWorkingLineThought({ durationMs: 0, active: true })).toBe("thinking 0s");
999
+ expect(formatWorkingLineThought({ durationMs: 10_999, active: true })).toBe("thinking 10s");
1000
+ expect(formatWorkingLineThought({ durationMs: 10_999, active: false })).toBe("thought for 10s");
1001
+ expect(formatWorkingLineThought({ durationMs: 0, active: false })).toBeUndefined();
1002
+ });
1003
+
1004
+ it("uses Thought before Elapsed and Tool allocation while preserving token text and row cap", () => {
1005
+ const component = config().components.workingLine;
1006
+ component.spinner = "pulse";
1007
+ const composed = composeWorkingLineRow(component, "Ready", {
1008
+ tool: "parallel-tool-label",
1009
+ elapsedMs: Number.MAX_SAFE_INTEGER,
1010
+ thought: { durationMs: 10_000, active: true },
1011
+ tokens: { input: Number.MAX_SAFE_INTEGER, output: Number.MAX_SAFE_INTEGER },
1012
+ });
1013
+ expect(composed.row).toContain("↑9007199255M ↓9007199255M");
1014
+ expect(composed.row).toContain("thinking");
1015
+ expect(composed.row).not.toContain("parallel-tool-label");
1016
+ expect(
1017
+ workingLineSpinnerWidth(component.spinner) + 1 + visibleWidth(composed.row) + 3,
1018
+ ).toBeLessThanOrEqual(MAX_WORKING_LINE_ROW_CELLS);
1019
+ });
1020
+ });
1021
+
1022
+ describe("working-line runtime ownership", () => {
1023
+ function runtime(
1024
+ enabled: boolean,
1025
+ random = () => 0.75,
1026
+ now: () => number = Date.now,
1027
+ getTheme: () => Theme = theme,
1028
+ ) {
1029
+ const current = config();
1030
+ current.components.workingLine.enabled = enabled;
1031
+ current.components.workingLine.messages = { custom: true, values: ["A", "B"] };
1032
+ const clock = new AgentDurationClock();
1033
+ const calls: Array<[string, unknown?]> = [];
1034
+ const ctx = {
1035
+ hasUI: true,
1036
+ mode: "tui",
1037
+ ui: {
1038
+ setWorkingMessage(value?: string) {
1039
+ calls.push(["message", value]);
1040
+ },
1041
+ setWorkingIndicator(value?: unknown) {
1042
+ calls.push(["indicator", value]);
1043
+ },
1044
+ },
1045
+ };
1046
+ return {
1047
+ current,
1048
+ clock,
1049
+ calls,
1050
+ ctx,
1051
+ controller: new WorkingLineController(() => current, getTheme, clock, random, now),
1052
+ };
1053
+ }
1054
+
1055
+ it("makes exactly zero working-row calls while disabled", () => {
1056
+ const harness = runtime(false);
1057
+ harness.controller.startSession(harness.ctx);
1058
+ harness.clock.start();
1059
+ harness.controller.startAgent(harness.ctx);
1060
+ harness.controller.startTurn(harness.ctx);
1061
+ harness.controller.finishAgent(harness.ctx);
1062
+ harness.controller.dispose(harness.ctx);
1063
+ harness.clock.reset();
1064
+ expect(harness.calls).toEqual([]);
1065
+ });
1066
+
1067
+ it("fails open when either public capability is missing", () => {
1068
+ const harness = runtime(true);
1069
+ for (const ui of [{ setWorkingMessage: vi.fn() }, { setWorkingIndicator: vi.fn() }]) {
1070
+ const result = harness.controller.startSession({ hasUI: true, mode: "tui", ui });
1071
+ expect(result.applied).toBe(false);
1072
+ expect(Object.values(ui).every((fn) => !fn.mock.calls.length)).toBe(true);
1073
+ }
1074
+ expect(harness.calls).toEqual([]);
1075
+ });
1076
+
1077
+ it("owns and styles the full fallback row with no RNG when custom messages are off", () => {
1078
+ const random = vi.fn(() => 0.75);
1079
+ const harness = runtime(true, random);
1080
+ harness.current.components.workingLine.spinner = "pulse";
1081
+ harness.current.components.workingLine.messages.custom = false;
1082
+ harness.controller.startSession(harness.ctx);
1083
+ harness.clock.start();
1084
+ harness.controller.startAgent(harness.ctx);
1085
+ harness.controller.startTurn(harness.ctx);
1086
+ harness.controller.updateTokens({ input: 12, output: 3 }, harness.ctx);
1087
+ expect(random).not.toHaveBeenCalled();
1088
+ expect(harness.calls[0]).toEqual(["message", ""]);
1089
+ const indicator = harness.calls.at(-1)?.[1] as { frames: string[] } | undefined;
1090
+ expect(
1091
+ strippedFrames(indicator?.frames ?? []).every((frame) =>
1092
+ frame.includes("Working… · 0s · ↑12 ↓3"),
1093
+ ),
1094
+ ).toBe(true);
1095
+ harness.controller.dispose(harness.ctx);
1096
+ expect(harness.calls.slice(-2)).toEqual([
1097
+ ["indicator", undefined],
1098
+ ["message", undefined],
1099
+ ]);
1100
+ });
1101
+
1102
+ it("does not rewrite Static frames for text-speed or spinner-color participation changes", () => {
1103
+ const harness = runtime(true);
1104
+ harness.current.components.workingLine.textAnimation = "disabled";
1105
+ harness.controller.startSession(harness.ctx);
1106
+ harness.calls.length = 0;
1107
+ harness.current.components.workingLine.textIntervalMs = 40;
1108
+ harness.current.components.workingLine.animateSpinnerColor = true;
1109
+ expect(harness.controller.reconcile(harness.ctx).applied).toBe(true);
1110
+ expect(harness.calls).toEqual([]);
1111
+ });
1112
+
1113
+ it("selects exactly once per turn_start and keeps that message through tool activity", () => {
1114
+ const random = vi.fn(() => 0.75);
1115
+ const harness = runtime(true, random);
1116
+ harness.controller.startSession(harness.ctx);
1117
+ expect(harness.controller.currentMessage()).toBe("A");
1118
+ harness.clock.start();
1119
+ harness.controller.startAgent(harness.ctx);
1120
+ expect(random).not.toHaveBeenCalled();
1121
+ harness.controller.startTurn(harness.ctx);
1122
+ expect(random).toHaveBeenCalledTimes(1);
1123
+ expect(harness.controller.currentMessage()).toBe("B");
1124
+ harness.controller.startTool("one", "read", harness.ctx);
1125
+ harness.controller.finishTool("one", harness.ctx);
1126
+ harness.current.components.workingLine.spinner = "pinwheel";
1127
+ harness.controller.reconcile(harness.ctx);
1128
+ expect(random).toHaveBeenCalledTimes(1);
1129
+ expect(harness.controller.currentMessage()).toBe("B");
1130
+ harness.controller.startTurn(harness.ctx);
1131
+ expect(random).toHaveBeenCalledTimes(2);
1132
+ });
1133
+
1134
+ it("reconciles live message edits deterministically without rerolling the turn", () => {
1135
+ const random = vi.fn(() => 0.75);
1136
+ const harness = runtime(true, random);
1137
+ harness.controller.startSession(harness.ctx);
1138
+ harness.controller.startTurn(harness.ctx);
1139
+ expect(harness.controller.currentMessage()).toBe("B");
1140
+ harness.current.components.workingLine.messages.values = ["B", "C"];
1141
+ harness.controller.reconcile(harness.ctx);
1142
+ expect(harness.controller.currentMessage()).toBe("B");
1143
+ harness.current.components.workingLine.messages.values = ["C"];
1144
+ harness.controller.reconcile(harness.ctx);
1145
+ expect(harness.controller.currentMessage()).toBe("C");
1146
+ harness.current.components.workingLine.messages.custom = false;
1147
+ harness.controller.reconcile(harness.ctx);
1148
+ expect(harness.controller.currentMessage()).toBe("Working…");
1149
+ harness.current.components.workingLine.messages = { custom: true, values: ["A", "B"] };
1150
+ harness.controller.reconcile(harness.ctx);
1151
+ expect(harness.controller.currentMessage()).toBe("A");
1152
+ expect(random).toHaveBeenCalledTimes(1);
1153
+ });
1154
+
1155
+ it("reinstalls full rows only for visible token/tool changes and tracks parallel tools", () => {
1156
+ const harness = runtime(true);
1157
+ harness.current.components.workingLine.segments.elapsed = false;
1158
+ harness.controller.startSession(harness.ctx);
1159
+ expect(harness.calls[0]).toEqual(["message", ""]);
1160
+ harness.clock.start();
1161
+ harness.controller.startAgent(harness.ctx);
1162
+ harness.controller.startTurn(harness.ctx);
1163
+ harness.calls.length = 0;
1164
+ const usage = { role: "assistant" as const, usage: { input: 120, output: 7 } };
1165
+ harness.controller.updateTokens(usage.usage, harness.ctx);
1166
+ harness.controller.updateTokens(usage.usage, harness.ctx);
1167
+ harness.controller.startTool("a", "read", harness.ctx);
1168
+ harness.controller.startTool("b", "bash", harness.ctx);
1169
+ harness.controller.startTool("b", "bash", harness.ctx);
1170
+ const text = () =>
1171
+ stripTerminalSequences(
1172
+ (harness.calls.at(-1)?.[1] as { frames?: string[] } | undefined)?.frames?.[0] ?? "",
1173
+ );
1174
+ expect(text()).toMatch(/B · bash · ↑120 ↓7/);
1175
+ harness.controller.finishTool("b", harness.ctx);
1176
+ expect(text()).toMatch(/B · read · ↑120 ↓7/);
1177
+ harness.controller.finishTool("a", harness.ctx);
1178
+ expect(text()).toMatch(/B · ↑120 ↓7/);
1179
+ expect(harness.calls.every(([name]) => name === "indicator")).toBe(true);
1180
+ expect(harness.calls).toHaveLength(5);
1181
+ harness.controller.finishTool("missing", harness.ctx);
1182
+ expect(harness.calls).toHaveLength(5);
1183
+ harness.controller.startTurn(harness.ctx);
1184
+ expect(text()).toMatch(/↑120 ↓7/);
1185
+ expect(harness.calls).toHaveLength(5);
1186
+ harness.current.components.workingLine.segments.tool = false;
1187
+ harness.controller.reconcile(harness.ctx);
1188
+ expect(harness.calls).toHaveLength(5);
1189
+ harness.controller.dispose(harness.ctx);
1190
+ harness.clock.reset();
1191
+ });
1192
+
1193
+ it.each([
1194
+ ["classic", "star-bloom", 900],
1195
+ ["classic", "pulse", 900],
1196
+ ["kitt", "star-bloom", 1400],
1197
+ ["kitt", "pulse", 1400],
1198
+ ] as const)(
1199
+ "preserves %s spatial position, KITT direction, and %s phase across token/tool widths",
1200
+ (animation, spinner, firstChangeAt) => {
1201
+ let now = 0;
1202
+ const harness = runtime(
1203
+ true,
1204
+ () => 0,
1205
+ () => now,
1206
+ );
1207
+ harness.current.components.workingLine.messages.values = ["Stable"];
1208
+ harness.current.components.workingLine.spinner = spinner;
1209
+ harness.current.components.workingLine.textAnimation = animation;
1210
+ harness.current.components.workingLine.segments.elapsed = false;
1211
+ harness.controller.startSession(harness.ctx);
1212
+ harness.controller.startAgent(harness.ctx);
1213
+ harness.controller.startTurn(harness.ctx);
1214
+ const activation = harness.calls.at(-1)?.[1] as {
1215
+ frames: string[];
1216
+ intervalMs: number;
1217
+ };
1218
+
1219
+ now = firstChangeAt;
1220
+ const activationIndex =
1221
+ Math.floor(firstChangeAt / activation.intervalMs) % activation.frames.length;
1222
+ harness.controller.updateTokens({ input: 12, output: 3 }, harness.ctx);
1223
+ const withTokens = harness.calls.at(-1)?.[1] as { frames: string[] };
1224
+ const beforeSignature = phaseSignature(activation.frames[activationIndex] ?? "");
1225
+ const afterSignature = phaseSignature(withTokens.frames[0] ?? "");
1226
+ expect(afterSignature[0]).toBe(beforeSignature[0]);
1227
+ if (beforeSignature[1] !== undefined) expect(afterSignature[1]).toBe(beforeSignature[1]);
1228
+
1229
+ now += 100;
1230
+ harness.controller.startTool("one", "read", harness.ctx);
1231
+ const withTool = harness.calls.at(-1)?.[1] as { frames: string[]; intervalMs: number };
1232
+ const carriedRemainder = firstChangeAt % withTool.intervalMs;
1233
+ const withTokensIndex = Math.floor((carriedRemainder + 100) / withTool.intervalMs);
1234
+ expect(phaseSignature(withTool.frames[0] ?? "")).toEqual(
1235
+ phaseSignature(withTokens.frames[withTokensIndex] ?? ""),
1236
+ );
1237
+ },
1238
+ );
1239
+
1240
+ it.each([
1241
+ [
1242
+ "speed",
1243
+ (current: PolishedTuiConfig): void => {
1244
+ current.components.workingLine.spinnerIntervalMs = 60;
1245
+ },
1246
+ ],
1247
+ [
1248
+ "preset",
1249
+ (current: PolishedTuiConfig): void => {
1250
+ current.components.workingLine.spinner = "pulse";
1251
+ },
1252
+ ],
1253
+ [
1254
+ "width",
1255
+ (current: PolishedTuiConfig): void => {
1256
+ current.components.workingLine.messages.values = ["A much wider stable message"];
1257
+ },
1258
+ ],
1259
+ [
1260
+ "animation",
1261
+ (current: PolishedTuiConfig): void => {
1262
+ current.components.workingLine.textAnimation = "kitt";
1263
+ },
1264
+ ],
1265
+ ] as const)("carries fractional phase across a live %s change", (_kind, change) => {
1266
+ let now = 0;
1267
+ const harness = runtime(
1268
+ true,
1269
+ () => 0,
1270
+ () => now,
1271
+ );
1272
+ harness.current.components.workingLine.messages.values = ["Stable"];
1273
+ harness.current.components.workingLine.spinnerIntervalMs = 100;
1274
+ harness.current.components.workingLine.textIntervalMs = 100;
1275
+ harness.current.components.workingLine.segments.elapsed = false;
1276
+ harness.controller.startSession(harness.ctx);
1277
+ const before = harness.calls.at(-1)?.[1] as { frames: string[]; intervalMs: number };
1278
+
1279
+ now = 250;
1280
+ change(harness.current);
1281
+ harness.controller.reconcile(harness.ctx);
1282
+ const after = harness.calls.at(-1)?.[1] as { frames: string[]; intervalMs: number };
1283
+ const sampledIndex = Math.floor(250 / before.intervalMs);
1284
+ if (_kind !== "preset") {
1285
+ expect(phaseSignature(after.frames[0] ?? "")[0]).toBe(
1286
+ phaseSignature(before.frames[sampledIndex] ?? "")[0],
1287
+ );
1288
+ } else {
1289
+ expect(after.frames[0]).toBeDefined();
1290
+ }
1291
+
1292
+ now = 250 + Math.ceil(after.intervalMs / 2);
1293
+ harness.controller.updateTokens({ input: 1, output: 1 }, harness.ctx);
1294
+ const carried = harness.calls.at(-1)?.[1] as { frames: string[] };
1295
+ expect(phaseSignature(carried.frames[0] ?? "")[0]).toBe(
1296
+ phaseSignature(after.frames[1] ?? "")[0],
1297
+ );
1298
+ });
1299
+
1300
+ it("advances logical frame zero on rapid rebuilds without relying on Loader ticks", () => {
1301
+ let now = 0;
1302
+ const harness = runtime(
1303
+ true,
1304
+ () => 0,
1305
+ () => now,
1306
+ );
1307
+ harness.current.components.workingLine.messages.values = ["Stable"];
1308
+ harness.current.components.workingLine.spinner = "pulse";
1309
+ harness.current.components.workingLine.spinnerIntervalMs = 100;
1310
+ harness.current.components.workingLine.textAnimation = "disabled";
1311
+ harness.current.components.workingLine.segments.elapsed = false;
1312
+ harness.controller.startSession(harness.ctx);
1313
+ const original = harness.calls.at(-1)?.[1] as { frames: string[] };
1314
+ for (now = 20; now <= 200; now += 20) {
1315
+ harness.controller.updateTokens({ input: now, output: 1 }, harness.ctx);
1316
+ const rebuilt = harness.calls.at(-1)?.[1] as { frames: string[] };
1317
+ const expectedIndex = Math.floor(now / 100);
1318
+ expect(phaseSignature(rebuilt.frames[0] ?? "")[0]).toBe(
1319
+ phaseSignature(original.frames[expectedIndex] ?? "")[0],
1320
+ );
1321
+ }
1322
+ });
1323
+
1324
+ it("matches uninterrupted exact independent spinner/text schedules during rapid rebuilds", () => {
1325
+ let now = 0;
1326
+ const harness = runtime(
1327
+ true,
1328
+ () => 0,
1329
+ () => now,
1330
+ );
1331
+ harness.current.components.workingLine.messages.values = ["Stable"];
1332
+ harness.current.components.workingLine.spinnerIntervalMs = 100;
1333
+ harness.current.components.workingLine.textIntervalMs = 60;
1334
+ harness.current.components.workingLine.textAnimation = "classic";
1335
+ harness.current.components.workingLine.segments.elapsed = false;
1336
+ harness.controller.startSession(harness.ctx);
1337
+ const uninterrupted = harness.calls.at(-1)?.[1] as { frames: string[]; intervalMs: number };
1338
+ expect(uninterrupted.intervalMs).toBe(30);
1339
+ for (now = 30; now <= 600; now += 30) {
1340
+ harness.controller.updateTokens({ input: now, output: 1 }, harness.ctx);
1341
+ const rebuilt = harness.calls.at(-1)?.[1] as { frames: string[] };
1342
+ expect(phaseSignature(rebuilt.frames[0] ?? "")).toEqual(
1343
+ phaseSignature(uninterrupted.frames[(now / 30) % uninterrupted.frames.length] ?? ""),
1344
+ );
1345
+ }
1346
+ });
1347
+
1348
+ it("preserves bounded fallback phase across its frame-array wrap", () => {
1349
+ let now = 0;
1350
+ const harness = runtime(
1351
+ true,
1352
+ () => 0,
1353
+ () => now,
1354
+ );
1355
+ harness.current.components.workingLine.messages.values = ["Stable"];
1356
+ harness.current.components.workingLine.spinnerIntervalMs = 997;
1357
+ harness.current.components.workingLine.textIntervalMs = 900;
1358
+ harness.current.components.workingLine.textAnimation = "classic";
1359
+ harness.current.components.workingLine.segments.elapsed = false;
1360
+ harness.controller.startSession(harness.ctx);
1361
+ const uninterrupted = harness.calls.at(-1)?.[1] as { frames: string[]; intervalMs: number };
1362
+ const wrapAt = uninterrupted.frames.length * uninterrupted.intervalMs;
1363
+ now = wrapAt + uninterrupted.intervalMs;
1364
+ harness.controller.updateTokens({ input: 1, output: 1 }, harness.ctx);
1365
+ const rebuilt = harness.calls.at(-1)?.[1] as { frames: string[] };
1366
+ expect(phaseSignature(rebuilt.frames[0] ?? "")).toEqual(
1367
+ phaseSignature(uninterrupted.frames[1] ?? ""),
1368
+ );
1369
+ });
1370
+
1371
+ it("bounds visible Loader reset jitter below one replacement interval", () => {
1372
+ vi.useFakeTimers();
1373
+ try {
1374
+ let now = 0;
1375
+ let loader: Loader | undefined;
1376
+ const harness = runtime(
1377
+ true,
1378
+ () => 0,
1379
+ () => now,
1380
+ );
1381
+ harness.current.components.workingLine.messages.values = ["Stable"];
1382
+ harness.current.components.workingLine.spinnerIntervalMs = 100;
1383
+ harness.current.components.workingLine.textAnimation = "disabled";
1384
+ harness.current.components.workingLine.segments.elapsed = false;
1385
+ const recordIndicator = harness.ctx.ui.setWorkingIndicator.bind(harness.ctx.ui);
1386
+ harness.ctx.ui.setWorkingIndicator = (value?: unknown) => {
1387
+ recordIndicator(value);
1388
+ if (value !== undefined) {
1389
+ const indicator = value as { frames: string[]; intervalMs: number };
1390
+ if (loader) loader.setIndicator(indicator);
1391
+ else
1392
+ loader = new Loader(
1393
+ { requestRender() {} } as never,
1394
+ (text) => text,
1395
+ (text) => text,
1396
+ "",
1397
+ indicator,
1398
+ );
1399
+ }
1400
+ };
1401
+ harness.controller.startSession(harness.ctx);
1402
+ now = 90;
1403
+ harness.controller.updateTokens({ input: 1, output: 1 }, harness.ctx);
1404
+ const replacement = harness.calls.at(-1)?.[1] as { frames: string[]; intervalMs: number };
1405
+ const logicalBoundaryAt = 100;
1406
+ const visibleReplacementBoundaryAt = now + replacement.intervalMs;
1407
+ expect(visibleReplacementBoundaryAt - logicalBoundaryAt).toBeLessThan(replacement.intervalMs);
1408
+ expect(stripTerminalSequences(loader?.render(40)[1] ?? "")).toContain(
1409
+ stripTerminalSequences(replacement.frames[0] ?? ""),
1410
+ );
1411
+ now = 100;
1412
+ harness.controller.updateTokens({ input: 2, output: 1 }, harness.ctx);
1413
+ const corrected = harness.calls.at(-1)?.[1] as { frames: string[] };
1414
+ expect(phaseSignature(corrected.frames[0] ?? "")[0]).toBe(
1415
+ phaseSignature(replacement.frames[1] ?? "")[0],
1416
+ );
1417
+ loader?.stop();
1418
+ } finally {
1419
+ vi.useRealTimers();
1420
+ }
1421
+ });
1422
+
1423
+ it("rebases frame zero at successful agent_start installation time", () => {
1424
+ let now = 0;
1425
+ const harness = runtime(
1426
+ true,
1427
+ () => 0,
1428
+ () => now,
1429
+ );
1430
+ harness.current.components.workingLine.messages.values = ["Stable"];
1431
+ harness.current.components.workingLine.spinner = "pulse";
1432
+ harness.current.components.workingLine.spinnerIntervalMs = 100;
1433
+ harness.current.components.workingLine.textAnimation = "disabled";
1434
+ harness.current.components.workingLine.segments.elapsed = false;
1435
+ harness.controller.startSession(harness.ctx);
1436
+ now = 250;
1437
+ harness.controller.startAgent(harness.ctx);
1438
+ const rebased = harness.calls.at(-1)?.[1] as { frames: string[] };
1439
+ expect(phaseSignature(rebased.frames[0] ?? "")[0]).toBe("⠀⠶⠀");
1440
+ now = 349;
1441
+ harness.controller.updateTokens({ input: 1, output: 1 }, harness.ctx);
1442
+ const beforeTick = harness.calls.at(-1)?.[1] as { frames: string[] };
1443
+ expect(phaseSignature(beforeTick.frames[0] ?? "")[0]).toBe("⠀⠶⠀");
1444
+ now = 350;
1445
+ harness.controller.updateTokens({ input: 2, output: 1 }, harness.ctx);
1446
+ const afterTick = harness.calls.at(-1)?.[1] as { frames: string[] };
1447
+ expect(phaseSignature(afterTick.frames[0] ?? "")[0]).toBe("⠰⣿⠆");
1448
+ });
1449
+
1450
+ it.each(["classic", "kitt"] as const)(
1451
+ "keeps %s on its logical epoch when frame generation and setters advance the clock",
1452
+ (animation) => {
1453
+ let now = 0;
1454
+ let generationPending = true;
1455
+ const delayedTheme = theme();
1456
+ const baseFg = delayedTheme.fg.bind(delayedTheme);
1457
+ delayedTheme.fg = (color, text) => {
1458
+ if (generationPending) {
1459
+ now += 250;
1460
+ generationPending = false;
1461
+ }
1462
+ return baseFg(color, text);
1463
+ };
1464
+ const harness = runtime(
1465
+ true,
1466
+ () => 0,
1467
+ () => now,
1468
+ () => delayedTheme,
1469
+ );
1470
+ const control = runtime(
1471
+ true,
1472
+ () => 0,
1473
+ () => now,
1474
+ );
1475
+ for (const current of [harness.current, control.current]) {
1476
+ current.components.workingLine.messages.values = ["Stable"];
1477
+ current.components.workingLine.spinner = "pulse";
1478
+ current.components.workingLine.spinnerIntervalMs = 100;
1479
+ current.components.workingLine.textIntervalMs = 100;
1480
+ current.components.workingLine.textAnimation = animation;
1481
+ current.components.workingLine.segments.elapsed = false;
1482
+ }
1483
+ control.controller.startSession(control.ctx);
1484
+ const recordIndicator = harness.ctx.ui.setWorkingIndicator.bind(harness.ctx.ui);
1485
+ harness.ctx.ui.setWorkingIndicator = (value?: unknown) => {
1486
+ recordIndicator(value);
1487
+ now += 40;
1488
+ generationPending = true;
1489
+ };
1490
+ harness.controller.startSession(harness.ctx);
1491
+ expect(now).toBe(290);
1492
+
1493
+ now += 560;
1494
+ control.controller.updateTokens({ input: 12, output: 3 }, control.ctx);
1495
+ harness.controller.updateTokens({ input: 12, output: 3 }, harness.ctx);
1496
+ const expected = control.calls.at(-1)?.[1] as { frames: string[] };
1497
+ const rebuilt = harness.calls.at(-1)?.[1] as { frames: string[] };
1498
+ expect(phaseMotionSignature(rebuilt.frames, 0)).toEqual(
1499
+ phaseMotionSignature(expected.frames, 0),
1500
+ );
1501
+
1502
+ now += 560;
1503
+ control.controller.startTool("one", "read", control.ctx);
1504
+ harness.controller.startTool("one", "read", harness.ctx);
1505
+ const expectedAgain = control.calls.at(-1)?.[1] as { frames: string[] };
1506
+ const rebuiltAgain = harness.calls.at(-1)?.[1] as { frames: string[] };
1507
+ expect(phaseMotionSignature(rebuiltAgain.frames, 0)).toEqual(
1508
+ phaseMotionSignature(expectedAgain.frames, 0),
1509
+ );
1510
+ },
1511
+ );
1512
+
1513
+ it("recovers the last successful epoch when reconcile mutates then throws and retries on tokens", () => {
1514
+ let now = 0;
1515
+ let failSetter = false;
1516
+ let setterCalls = 0;
1517
+ let failedAtMs = 0;
1518
+ let indicatorSurface: unknown;
1519
+ let failedAttempt: { frames: string[]; intervalMs: number } | undefined;
1520
+ const harness = runtime(
1521
+ true,
1522
+ () => 0,
1523
+ () => now,
1524
+ );
1525
+ const control = runtime(
1526
+ true,
1527
+ () => 0,
1528
+ () => now,
1529
+ );
1530
+ const recordIndicator = harness.ctx.ui.setWorkingIndicator.bind(harness.ctx.ui);
1531
+ harness.ctx.ui.setWorkingIndicator = (value?: unknown) => {
1532
+ setterCalls += 1;
1533
+ indicatorSurface = value;
1534
+ if (failSetter) {
1535
+ failSetter = false;
1536
+ failedAtMs = now;
1537
+ failedAttempt = value as { frames: string[]; intervalMs: number };
1538
+ now += 4500;
1539
+ throw new Error("setter unavailable");
1540
+ }
1541
+ recordIndicator(value);
1542
+ };
1543
+ for (const current of [harness.current, control.current]) {
1544
+ current.components.workingLine.messages.values = ["Stable"];
1545
+ current.components.workingLine.spinner = "pulse";
1546
+ current.components.workingLine.spinnerIntervalMs = 100;
1547
+ current.components.workingLine.textIntervalMs = 100;
1548
+ current.components.workingLine.textAnimation = "classic";
1549
+ current.components.workingLine.segments.elapsed = false;
1550
+ }
1551
+ harness.controller.startSession(harness.ctx);
1552
+ control.controller.startSession(control.ctx);
1553
+ const lastSuccessfulSurface = indicatorSurface;
1554
+
1555
+ now = 100;
1556
+ for (const current of [harness.current, control.current]) {
1557
+ current.components.workingLine.spinnerIntervalMs = 200;
1558
+ current.components.workingLine.textIntervalMs = 200;
1559
+ current.components.workingLine.textAnimation = "kitt";
1560
+ }
1561
+ failSetter = true;
1562
+ expect(harness.controller.reconcile(harness.ctx).applied).toBe(false);
1563
+ expect(indicatorSurface).toBe(lastSuccessfulSurface);
1564
+ failSetter = false;
1565
+ control.controller.updateTokens({ input: 12, output: 3 }, control.ctx);
1566
+ harness.controller.updateTokens({ input: 12, output: 3 }, harness.ctx);
1567
+
1568
+ expect(setterCalls).toBe(4);
1569
+ expect(harness.calls.filter(([name]) => name === "indicator")).toHaveLength(3);
1570
+ const retried = harness.calls.at(-1)?.[1] as { frames: string[] };
1571
+ const expected = control.calls.at(-1)?.[1] as { frames: string[] };
1572
+ expect(phaseMotionSignature(retried.frames, 0)).toEqual(
1573
+ phaseMotionSignature(expected.frames, 0),
1574
+ );
1575
+ const failed = failedAttempt as unknown as { frames: string[]; intervalMs: number };
1576
+ const incorrectlyPublishedIndex =
1577
+ Math.floor((now - failedAtMs) / failed.intervalMs) % failed.frames.length;
1578
+ expect(phaseMotionSignature(retried.frames, 0)).not.toEqual(
1579
+ phaseMotionSignature(failed.frames, incorrectlyPublishedIndex),
1580
+ );
1581
+ });
1582
+
1583
+ it("preserves the elapsed subscription while recovering a transient reinstall failure", () => {
1584
+ vi.useFakeTimers();
1585
+ try {
1586
+ const harness = runtime(true, () => 0);
1587
+ harness.current.components.workingLine.messages.values = ["Stable"];
1588
+ let failSetter = false;
1589
+ const setter = harness.ctx.ui.setWorkingIndicator.bind(harness.ctx.ui);
1590
+ harness.ctx.ui.setWorkingIndicator = (value?: unknown) => {
1591
+ if (failSetter) {
1592
+ failSetter = false;
1593
+ throw new Error("mutated before throwing");
1594
+ }
1595
+ setter(value);
1596
+ };
1597
+ harness.controller.startSession(harness.ctx);
1598
+ harness.clock.start();
1599
+ harness.controller.startAgent(harness.ctx);
1600
+ expect(vi.getTimerCount()).toBe(1);
1601
+ harness.current.components.workingLine.spinnerIntervalMs += 1;
1602
+ failSetter = true;
1603
+ expect(harness.controller.reconcile(harness.ctx).applied).toBe(false);
1604
+ expect(vi.getTimerCount()).toBe(1);
1605
+ harness.controller.updateTokens({ input: 1, output: 1 }, harness.ctx);
1606
+ expect(vi.getTimerCount()).toBe(1);
1607
+ harness.controller.dispose(harness.ctx);
1608
+ harness.clock.reset();
1609
+ } finally {
1610
+ vi.useRealTimers();
1611
+ }
1612
+ });
1613
+
1614
+ it("restores the previous message and phase when startTurn installation fails", () => {
1615
+ let now = 0;
1616
+ let failSetter = false;
1617
+ let indicatorSurface: unknown;
1618
+ const harness = runtime(
1619
+ true,
1620
+ () => 0.75,
1621
+ () => now,
1622
+ );
1623
+ harness.current.components.workingLine.messages.values = ["A", "B"];
1624
+ harness.current.components.workingLine.spinner = "pulse";
1625
+ harness.current.components.workingLine.spinnerIntervalMs = 100;
1626
+ harness.current.components.workingLine.textAnimation = "disabled";
1627
+ harness.current.components.workingLine.segments.elapsed = false;
1628
+ const setter = harness.ctx.ui.setWorkingIndicator.bind(harness.ctx.ui);
1629
+ harness.ctx.ui.setWorkingIndicator = (value?: unknown) => {
1630
+ indicatorSurface = value;
1631
+ if (failSetter) {
1632
+ failSetter = false;
1633
+ now = 250;
1634
+ throw new Error("mutated before throwing");
1635
+ }
1636
+ setter(value);
1637
+ };
1638
+ harness.controller.startSession(harness.ctx);
1639
+ const lastSuccessfulSurface = indicatorSurface;
1640
+ expect(harness.controller.currentMessage()).toBe("A");
1641
+
1642
+ failSetter = true;
1643
+ expect(harness.controller.startTurn(harness.ctx).applied).toBe(false);
1644
+ expect(indicatorSurface).toBe(lastSuccessfulSurface);
1645
+ expect(harness.controller.currentMessage()).toBe("A");
1646
+ harness.controller.updateTokens({ input: 1, output: 1 }, harness.ctx);
1647
+ const retried = harness.calls.at(-1)?.[1] as { frames: string[] };
1648
+ expect(stripTerminalSequences(retried.frames[0] ?? "")).toContain("A");
1649
+ expect(phaseSignature(retried.frames[0] ?? "")[0]).toBe("⢾⣉⡷");
1650
+ });
1651
+
1652
+ it("does not publish a failed forced agent-start rebase epoch", () => {
1653
+ let now = 0;
1654
+ let failSetter = false;
1655
+ let indicatorSurface: unknown;
1656
+ const harness = runtime(
1657
+ true,
1658
+ () => 0,
1659
+ () => now,
1660
+ );
1661
+ harness.current.components.workingLine.messages.values = ["Stable"];
1662
+ harness.current.components.workingLine.spinner = "pulse";
1663
+ harness.current.components.workingLine.spinnerIntervalMs = 100;
1664
+ harness.current.components.workingLine.textAnimation = "disabled";
1665
+ harness.current.components.workingLine.segments.elapsed = false;
1666
+ const setter = harness.ctx.ui.setWorkingIndicator.bind(harness.ctx.ui);
1667
+ harness.ctx.ui.setWorkingIndicator = (value?: unknown) => {
1668
+ indicatorSurface = value;
1669
+ if (failSetter) {
1670
+ failSetter = false;
1671
+ now = 350;
1672
+ throw new Error("mutated before throwing");
1673
+ }
1674
+ setter(value);
1675
+ };
1676
+ harness.controller.startSession(harness.ctx);
1677
+ const lastSuccessfulSurface = indicatorSurface;
1678
+ now = 250;
1679
+ failSetter = true;
1680
+ harness.controller.startAgent(harness.ctx);
1681
+ expect(indicatorSurface).toBe(lastSuccessfulSurface);
1682
+ harness.controller.updateTokens({ input: 1, output: 1 }, harness.ctx);
1683
+ const retried = harness.calls.at(-1)?.[1] as { frames: string[] };
1684
+ expect(phaseSignature(retried.frames[0] ?? "")[0]).toBe("⣏⠀⣹");
1685
+ });
1686
+
1687
+ it("releases ownership when recovery also throws and waits for a clean reinstall", () => {
1688
+ const current = config();
1689
+ current.components.workingLine.enabled = true;
1690
+ current.components.workingLine.messages.values = ["Stable"];
1691
+ current.components.workingLine.segments.elapsed = false;
1692
+ const calls: string[] = [];
1693
+ let indicatorSurface: unknown;
1694
+ let messageSurface: string | undefined;
1695
+ let failureSets = 0;
1696
+ const ctx = {
1697
+ hasUI: true,
1698
+ mode: "tui",
1699
+ ui: {
1700
+ setWorkingMessage(value?: string) {
1701
+ messageSurface = value;
1702
+ calls.push(value === undefined ? "message-reset" : "message-set");
1703
+ },
1704
+ setWorkingIndicator(value?: unknown) {
1705
+ indicatorSurface = value;
1706
+ calls.push(value === undefined ? "indicator-reset" : "indicator-set");
1707
+ if (value !== undefined && failureSets > 0) {
1708
+ failureSets -= 1;
1709
+ throw new Error("setter mutated before throwing");
1710
+ }
1711
+ },
1712
+ },
1713
+ };
1714
+ const controller = new WorkingLineController(() => current, theme);
1715
+ expect(controller.startSession(ctx).applied).toBe(true);
1716
+ current.components.workingLine.spinnerIntervalMs += 1;
1717
+ failureSets = 2;
1718
+ expect(controller.reconcile(ctx).applied).toBe(false);
1719
+ expect(calls.slice(-4)).toEqual([
1720
+ "indicator-set",
1721
+ "indicator-set",
1722
+ "indicator-reset",
1723
+ "message-reset",
1724
+ ]);
1725
+ expect(indicatorSurface).toBeUndefined();
1726
+ expect(messageSurface).toBeUndefined();
1727
+ const afterRelease = calls.length;
1728
+ controller.updateTokens({ input: 1, output: 1 }, ctx);
1729
+ expect(calls).toHaveLength(afterRelease);
1730
+ expect(controller.reconcile(ctx).applied).toBe(true);
1731
+ expect(calls.slice(-2)).toEqual(["message-set", "indicator-set"]);
1732
+ });
1733
+
1734
+ it.each(["classic", "kitt"] as const)(
1735
+ "preserves %s phase across the 9s to 10s elapsed-width change",
1736
+ (animation) => {
1737
+ vi.useFakeTimers();
1738
+ vi.setSystemTime(0);
1739
+ try {
1740
+ const harness = runtime(true, () => 0);
1741
+ harness.current.components.workingLine.messages.values = ["Stable"];
1742
+ harness.current.components.workingLine.textAnimation = animation;
1743
+ harness.controller.startSession(harness.ctx);
1744
+ harness.clock.start(0);
1745
+ harness.controller.startAgent(harness.ctx);
1746
+ harness.controller.startTurn(harness.ctx);
1747
+ vi.advanceTimersByTime(9000);
1748
+ const atNine = harness.calls.at(-1)?.[1] as { frames: string[]; intervalMs: number };
1749
+ expect(stripTerminalSequences(atNine.frames[0] ?? "")).toContain(" · 9s");
1750
+ vi.advanceTimersByTime(1000);
1751
+ const atTen = harness.calls.at(-1)?.[1] as { frames: string[] };
1752
+ expect(stripTerminalSequences(atTen.frames[0] ?? "")).toContain(" · 10s");
1753
+ const sampledIndex = Math.floor(1000 / atNine.intervalMs) % atNine.frames.length;
1754
+ expect(phaseSignature(atTen.frames[0] ?? "")).toEqual(
1755
+ phaseSignature(atNine.frames[sampledIndex] ?? ""),
1756
+ );
1757
+ harness.controller.dispose(harness.ctx);
1758
+ harness.clock.reset();
1759
+ } finally {
1760
+ vi.useRealTimers();
1761
+ }
1762
+ },
1763
+ );
1764
+
1765
+ it("writes elapsed only when its one-second label changes", () => {
1766
+ vi.useFakeTimers();
1767
+ const harness = runtime(true);
1768
+ harness.controller.startSession(harness.ctx);
1769
+ harness.clock.start();
1770
+ harness.controller.startAgent(harness.ctx);
1771
+ harness.controller.startTurn(harness.ctx);
1772
+ harness.calls.length = 0;
1773
+ vi.advanceTimersByTime(999);
1774
+ expect(harness.calls).toEqual([]);
1775
+ vi.advanceTimersByTime(1);
1776
+ expect(harness.calls).toHaveLength(1);
1777
+ expect(harness.calls[0]?.[0]).toBe("indicator");
1778
+ expect(
1779
+ stripTerminalSequences(
1780
+ (harness.calls[0]?.[1] as { frames?: string[] } | undefined)?.frames?.[0] ?? "",
1781
+ ),
1782
+ ).toContain(" · 1s");
1783
+ vi.advanceTimersByTime(1000);
1784
+ expect(harness.calls).toHaveLength(2);
1785
+ expect(
1786
+ stripTerminalSequences(
1787
+ (harness.calls.at(-1)?.[1] as { frames?: string[] } | undefined)?.frames?.[0] ?? "",
1788
+ ),
1789
+ ).toContain(" · 2s");
1790
+ harness.controller.dispose(harness.ctx);
1791
+ harness.clock.reset();
1792
+ vi.useRealTimers();
1793
+ });
1794
+
1795
+ it("keeps one shared clock subscription through frequent eligible updates", () => {
1796
+ vi.useFakeTimers();
1797
+ try {
1798
+ const harness = runtime(true);
1799
+ harness.controller.startSession(harness.ctx);
1800
+ harness.clock.start();
1801
+ harness.controller.startAgent(harness.ctx);
1802
+ harness.controller.startTurn(harness.ctx);
1803
+ for (let elapsed = 0; elapsed < 1_000; elapsed += 100) {
1804
+ harness.controller.updateMetrics(
1805
+ { input: elapsed, output: 1 },
1806
+ { durationMs: elapsed, active: true },
1807
+ harness.ctx,
1808
+ );
1809
+ harness.controller.reconcile(harness.ctx);
1810
+ vi.advanceTimersByTime(100);
1811
+ expect(vi.getTimerCount()).toBe(1);
1812
+ }
1813
+ const latest = stripTerminalSequences(
1814
+ (harness.calls.at(-1)?.[1] as { frames?: string[] } | undefined)?.frames?.[0] ?? "",
1815
+ );
1816
+ expect(latest).toContain(" · 1s");
1817
+ harness.controller.dispose(harness.ctx);
1818
+ expect(vi.getTimerCount()).toBe(0);
1819
+ } finally {
1820
+ vi.useRealTimers();
1821
+ }
1822
+ });
1823
+
1824
+ it("transitions thought-only clock subscription strictly with active thought", () => {
1825
+ vi.useFakeTimers();
1826
+ try {
1827
+ const harness = runtime(true);
1828
+ harness.current.components.workingLine.segments.elapsed = false;
1829
+ harness.current.components.workingLine.segments.thought = true;
1830
+ harness.controller.startSession(harness.ctx);
1831
+ harness.clock.start();
1832
+ harness.controller.startAgent(harness.ctx);
1833
+ expect(vi.getTimerCount()).toBe(0);
1834
+ harness.controller.updateMetrics(undefined, { durationMs: 0, active: true }, harness.ctx);
1835
+ expect(vi.getTimerCount()).toBe(1);
1836
+ harness.controller.updateMetrics(undefined, { durationMs: 1, active: true }, harness.ctx);
1837
+ harness.controller.startTurn(harness.ctx);
1838
+ expect(vi.getTimerCount()).toBe(1);
1839
+ harness.controller.updateMetrics(undefined, { durationMs: 2, active: false }, harness.ctx);
1840
+ expect(vi.getTimerCount()).toBe(0);
1841
+ harness.current.components.workingLine.enabled = false;
1842
+ harness.controller.reconcile(harness.ctx);
1843
+ expect(vi.getTimerCount()).toBe(0);
1844
+ harness.current.components.workingLine.enabled = true;
1845
+ harness.controller.reconcile(harness.ctx);
1846
+ harness.controller.updateMetrics(undefined, { durationMs: 3, active: true }, harness.ctx);
1847
+ expect(vi.getTimerCount()).toBe(1);
1848
+ harness.controller.finishAgent(harness.ctx);
1849
+ expect(vi.getTimerCount()).toBe(0);
1850
+ } finally {
1851
+ vi.useRealTimers();
1852
+ }
1853
+ });
1854
+
1855
+ it("releases a first message setter that mutates then throws before a clean retry", () => {
1856
+ const current = config();
1857
+ current.components.workingLine.enabled = true;
1858
+ const calls: string[] = [];
1859
+ let failFirstMessageSet = true;
1860
+ const ctx = {
1861
+ hasUI: true,
1862
+ mode: "tui",
1863
+ ui: {
1864
+ setWorkingMessage(value?: string) {
1865
+ calls.push(value === undefined ? "message-reset" : "message-set");
1866
+ if (value === "" && failFirstMessageSet) {
1867
+ failFirstMessageSet = false;
1868
+ throw new Error("message setter mutated before throwing");
1869
+ }
1870
+ },
1871
+ setWorkingIndicator(value?: unknown) {
1872
+ calls.push(value === undefined ? "indicator-reset" : "indicator-set");
1873
+ },
1874
+ },
1875
+ };
1876
+ const controller = new WorkingLineController(() => current, theme);
1877
+
1878
+ expect(controller.startSession(ctx).applied).toBe(false);
1879
+ expect(calls).toEqual(["message-set", "message-reset"]);
1880
+ controller.dispose(ctx);
1881
+ expect(calls).toEqual(["message-set", "message-reset"]);
1882
+
1883
+ expect(controller.startSession(ctx).applied).toBe(true);
1884
+ expect(calls.slice(-2)).toEqual(["message-set", "indicator-set"]);
1885
+ const afterRetry = calls.length;
1886
+ expect(controller.reconcile(ctx).applied).toBe(true);
1887
+ expect(calls).toHaveLength(afterRetry);
1888
+ controller.dispose(ctx);
1889
+ controller.dispose(ctx);
1890
+ expect(calls.slice(-2)).toEqual(["indicator-reset", "message-reset"]);
1891
+ });
1892
+
1893
+ it("releases a first indicator setter that mutates then throws before a phase-clean retry", () => {
1894
+ const current = config();
1895
+ current.components.workingLine.enabled = true;
1896
+ current.components.workingLine.messages.values = ["Stable"];
1897
+ current.components.workingLine.segments.elapsed = false;
1898
+ let now = 0;
1899
+ let failFirstIndicatorSet = true;
1900
+ const calls: string[] = [];
1901
+ let failedFrames: string[] | undefined;
1902
+ let retriedFrames: string[] | undefined;
1903
+ const ctx = {
1904
+ hasUI: true,
1905
+ mode: "tui",
1906
+ ui: {
1907
+ setWorkingMessage(value?: string) {
1908
+ calls.push(value === undefined ? "message-reset" : "message-set");
1909
+ },
1910
+ setWorkingIndicator(value?: unknown) {
1911
+ calls.push(value === undefined ? "indicator-reset" : "indicator-set");
1912
+ if (value !== undefined && failFirstIndicatorSet) {
1913
+ failedFrames = (value as { frames: string[] }).frames;
1914
+ failFirstIndicatorSet = false;
1915
+ now = 500;
1916
+ throw new Error("indicator setter mutated before throwing");
1917
+ }
1918
+ if (value !== undefined) retriedFrames = (value as { frames: string[] }).frames;
1919
+ },
1920
+ },
1921
+ };
1922
+ const controller = new WorkingLineController(
1923
+ () => current,
1924
+ theme,
1925
+ new AgentDurationClock(),
1926
+ () => 0,
1927
+ () => now,
1928
+ );
1929
+
1930
+ expect(controller.startSession(ctx).applied).toBe(false);
1931
+ expect(calls).toEqual(["message-set", "indicator-set", "indicator-reset", "message-reset"]);
1932
+ controller.dispose(ctx);
1933
+ expect(calls).toHaveLength(4);
1934
+
1935
+ expect(controller.startSession(ctx).applied).toBe(true);
1936
+ expect(calls.slice(-2)).toEqual(["message-set", "indicator-set"]);
1937
+ expect(retriedFrames).toEqual(failedFrames);
1938
+ const afterRetry = calls.length;
1939
+ expect(controller.reconcile(ctx).applied).toBe(true);
1940
+ expect(calls).toHaveLength(afterRetry);
1941
+ controller.dispose(ctx);
1942
+ controller.dispose(ctx);
1943
+ expect(calls.slice(-2)).toEqual(["indicator-reset", "message-reset"]);
1944
+ });
1945
+
1946
+ it("contains cleanup API failures and stays idempotent", () => {
1947
+ const current = config();
1948
+ current.components.workingLine.enabled = true;
1949
+ current.components.workingLine.messages.custom = true;
1950
+ const calls: string[] = [];
1951
+ const ctx = {
1952
+ hasUI: true,
1953
+ mode: "tui",
1954
+ ui: {
1955
+ setWorkingMessage(value?: string) {
1956
+ calls.push(value === undefined ? "message-reset" : "message-set");
1957
+ },
1958
+ setWorkingIndicator(value?: unknown) {
1959
+ calls.push(value === undefined ? "indicator-reset" : "indicator-set");
1960
+ if (value === undefined) throw new Error("reset unavailable");
1961
+ },
1962
+ },
1963
+ };
1964
+ const controller = new WorkingLineController(() => current, theme);
1965
+ expect(controller.startSession(ctx).applied).toBe(true);
1966
+ expect(() => controller.dispose(ctx)).not.toThrow();
1967
+ expect(() => controller.dispose(ctx)).not.toThrow();
1968
+ expect(calls).toEqual(["message-set", "indicator-set", "indicator-reset", "message-reset"]);
1969
+ });
1970
+
1971
+ it("live-enables and resets indicator then message exactly once on disable or cleanup", () => {
1972
+ const harness = runtime(false);
1973
+ harness.controller.startTurn(harness.ctx);
1974
+ harness.current.components.workingLine.enabled = true;
1975
+ expect(harness.controller.reconcile(harness.ctx).applied).toBe(true);
1976
+ expect(harness.controller.currentMessage()).toBe("B");
1977
+ harness.current.components.workingLine.enabled = false;
1978
+ harness.controller.reconcile(harness.ctx);
1979
+ harness.controller.dispose(harness.ctx);
1980
+ harness.controller.dispose(harness.ctx);
1981
+ expect(
1982
+ harness.calls.map(([name, value]) => [name, value === undefined ? "reset" : "set"]),
1983
+ ).toEqual([
1984
+ ["message", "set"],
1985
+ ["indicator", "set"],
1986
+ ["indicator", "reset"],
1987
+ ["message", "reset"],
1988
+ ]);
1989
+ });
1990
+ });