@selesai/code 0.9.15 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/CHANGELOG.md +27 -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/modes/interactive/components/settings-selector.d.ts +1 -1
  118. package/dist/modes/interactive/interactive-mode.js +1 -1
  119. package/dist/skills/design-references/SKILL.md +31 -0
  120. package/dist/skills/design-references/references/clickhouse/DESIGN.md +544 -0
  121. package/dist/themes/adventure.json +84 -0
  122. package/dist/themes/adwaita-dark.json +84 -0
  123. package/dist/themes/arcoiris.json +84 -0
  124. package/dist/themes/arthur.json +84 -0
  125. package/dist/themes/atom.json +84 -0
  126. package/dist/themes/aura.json +84 -0
  127. package/dist/themes/black-metal-bathory.json +84 -0
  128. package/dist/themes/black-metal-burzum.json +84 -0
  129. package/dist/themes/black-metal-khold.json +84 -0
  130. package/dist/themes/box.json +84 -0
  131. package/dist/themes/brogrammer.json +84 -0
  132. package/dist/themes/carbonfox.json +84 -0
  133. package/dist/themes/catppuccin-mocha.json +84 -0
  134. package/dist/themes/citruszest.json +84 -0
  135. package/dist/themes/cursor-dark.json +84 -0
  136. package/dist/themes/cutie-pro.json +84 -0
  137. package/dist/themes/dark-modern.json +84 -0
  138. package/dist/themes/dark-pastel.json +84 -0
  139. package/dist/themes/dimmed-monokai.json +84 -0
  140. package/dist/themes/doom-peacock.json +84 -0
  141. package/dist/themes/dracula-plus.json +84 -0
  142. package/dist/themes/dracula.json +80 -0
  143. package/dist/themes/earthsong.json +84 -0
  144. package/dist/themes/everforest-dark-hard.json +84 -0
  145. package/dist/themes/everforest-dark.json +80 -0
  146. package/dist/themes/fahrenheit.json +84 -0
  147. package/dist/themes/flatland.json +84 -0
  148. package/dist/themes/flexoki-dark.json +84 -0
  149. package/dist/themes/front-end-delight.json +84 -0
  150. package/dist/themes/fun-forrest.json +84 -0
  151. package/dist/themes/galizur.json +84 -0
  152. package/dist/themes/github-dark-colorblind.json +84 -0
  153. package/dist/themes/github-dark-high-contrast.json +84 -0
  154. package/dist/themes/glacier.json +84 -0
  155. package/dist/themes/gruber-darker.json +84 -0
  156. package/dist/themes/gruvbox-dark-hard.json +84 -0
  157. package/dist/themes/gruvbox-dark.json +84 -0
  158. package/dist/themes/gruvbox-material.json +84 -0
  159. package/dist/themes/guezwhoz.json +84 -0
  160. package/dist/themes/hacktober.json +84 -0
  161. package/dist/themes/hardcore.json +84 -0
  162. package/dist/themes/havn-skumring.json +84 -0
  163. package/dist/themes/ic-orange-ppl.json +84 -0
  164. package/dist/themes/iterm2-smoooooth.json +84 -0
  165. package/dist/themes/iterm2-tango-dark.json +84 -0
  166. package/dist/themes/japanesque.json +84 -0
  167. package/dist/themes/jellybeans.json +84 -0
  168. package/dist/themes/kanagawa-wave.json +84 -0
  169. package/dist/themes/kurokula.json +84 -0
  170. package/dist/themes/later-this-evening.json +84 -0
  171. package/dist/themes/lovelace.json +84 -0
  172. package/dist/themes/material-darker.json +84 -0
  173. package/dist/themes/matte-black.json +84 -0
  174. package/dist/themes/mellow.json +84 -0
  175. package/dist/themes/miasma.json +84 -0
  176. package/dist/themes/nord.json +80 -0
  177. package/dist/themes/nvim-dark.json +84 -0
  178. package/dist/themes/one-dark.json +80 -0
  179. package/dist/themes/popping-and-locking.json +84 -0
  180. package/dist/themes/rose-pine.json +80 -0
  181. package/dist/themes/sea-shells.json +84 -0
  182. package/dist/themes/sleepy-hollow.json +84 -0
  183. package/dist/themes/smyck.json +84 -0
  184. package/dist/themes/solarized-dark.json +80 -0
  185. package/dist/themes/tokyo-night.json +80 -0
  186. package/dist/themes/tomorrow-night-bright.json +84 -0
  187. package/dist/themes/tomorrow-night-burns.json +84 -0
  188. package/dist/themes/tomorrow-night.json +84 -0
  189. package/dist/themes/twilight.json +84 -0
  190. package/dist/themes/vague.json +84 -0
  191. package/dist/themes/vesper.json +84 -0
  192. package/dist/themes/xcode-dark-hc.json +84 -0
  193. package/docs/custom-provider.md +0 -1
  194. package/package.json +3 -3
  195. package/dist/extensions/auto-model/auto-model.test.ts +0 -18
  196. package/dist/extensions/auto-model/classifier.ts +0 -23
  197. package/dist/extensions/auto-model/config.ts +0 -51
  198. package/dist/extensions/auto-model/index.ts +0 -22
  199. package/dist/extensions/auto-model/lifecycle.test.ts +0 -24
  200. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  201. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  202. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  203. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  204. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  205. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  206. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  207. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  208. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  209. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  210. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  211. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  212. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  213. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  214. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  215. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  216. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  217. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  218. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  219. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  220. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  221. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  222. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  223. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  224. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  225. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  226. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  227. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  228. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  229. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  230. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  231. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  232. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  233. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  234. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  235. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  236. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  237. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  238. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  239. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  240. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  241. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  242. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  243. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  244. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  245. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  246. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  247. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  248. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  249. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  250. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  251. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  252. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  253. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  254. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  255. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  256. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  257. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  258. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  259. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  260. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  261. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  262. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  263. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  264. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  265. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  266. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  267. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  268. package/dist/extensions/preview-tools-disabled.ts +0 -11
  269. package/dist/themes/powerline-footer/theme.json +0 -33
@@ -0,0 +1,730 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+ import type { Theme } from "@earendil-works/pi-coding-agent";
3
+ import { Loader, visibleWidth } from "@earendil-works/pi-tui";
4
+ import { beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ const stripTerminalSequences = stripVTControlCharacters;
7
+
8
+ const runtime = vi.hoisted(() => ({
9
+ enabled: true,
10
+ custom: true,
11
+ message: "Stable",
12
+ spinner: "star-bloom" as "star-bloom" | "pulse",
13
+ spinnerIntervalMs: 100,
14
+ textIntervalMs: 60,
15
+ }));
16
+
17
+ vi.mock("../extensions/zentui/config", async (importOriginal) => {
18
+ const actual = await importOriginal<typeof import("../extensions/zentui/config")>();
19
+ return {
20
+ ...actual,
21
+ ensureConfigExists() {},
22
+ loadConfig: () => {
23
+ const config = structuredClone(actual.defaultConfig);
24
+ config.projectRefreshIntervalMs = 0;
25
+ config.components.editor.enabled = false;
26
+ config.components.userMessages.enabled = false;
27
+ config.components.selectorBorders.enabled = false;
28
+ config.components.footer.style = "native";
29
+ config.components.workingLine.enabled = runtime.enabled;
30
+ config.components.workingLine.spinner = runtime.spinner;
31
+ config.components.workingLine.spinnerIntervalMs = runtime.spinnerIntervalMs;
32
+ config.components.workingLine.textIntervalMs = runtime.textIntervalMs;
33
+ config.components.workingLine.messages = {
34
+ custom: runtime.custom,
35
+ values: [runtime.message],
36
+ };
37
+ return config;
38
+ },
39
+ };
40
+ });
41
+
42
+ import zentui from "../extensions/zentui/index";
43
+
44
+ type Handler = (event: unknown, ctx: unknown) => unknown | Promise<unknown>;
45
+
46
+ function theme(): Theme {
47
+ return {
48
+ fg(color: string, text: string) {
49
+ const codes: Record<string, number> = { dim: 90, muted: 36, accent: 96 };
50
+ return `\x1b[${codes[color] ?? 37}m${text}\x1b[0m`;
51
+ },
52
+ bold: (text: string) => `\x1b[1m${text}\x1b[0m`,
53
+ } as Theme;
54
+ }
55
+
56
+ function loaderPhase(rendered: string): [string, number | undefined] {
57
+ const stripped = stripTerminalSequences(rendered);
58
+ const plain = stripped.trimStart();
59
+ const separatorIndex = plain.indexOf(" ");
60
+ if (separatorIndex < 0) throw new Error("Expected a spinner separator in the working row");
61
+ const spinner = plain.slice(0, separatorIndex);
62
+ const textOffset =
63
+ visibleWidth(stripped) - visibleWidth(plain) + visibleWidth(plain.slice(0, separatorIndex + 1));
64
+ const marker = [...rendered.matchAll(/\x1b\[96m\x1b\[1m/g)].find((match) => {
65
+ if (match.index === undefined) return false;
66
+ return visibleWidth(stripTerminalSequences(rendered.slice(0, match.index))) >= textOffset;
67
+ });
68
+ return [
69
+ spinner,
70
+ marker?.index === undefined
71
+ ? undefined
72
+ : visibleWidth(stripTerminalSequences(rendered.slice(0, marker.index))) - textOffset,
73
+ ];
74
+ }
75
+
76
+ function loadExtension() {
77
+ const handlers = new Map<string, Handler[]>();
78
+ zentui({
79
+ registerEntryRenderer() {},
80
+ appendEntry() {},
81
+ on(name: string, handler: Handler) {
82
+ handlers.set(name, [...(handlers.get(name) ?? []), handler]);
83
+ },
84
+ registerCommand() {},
85
+ getThinkingLevel: () => "off",
86
+ } as never);
87
+ return handlers;
88
+ }
89
+
90
+ async function emit(
91
+ handlers: Map<string, Handler[]>,
92
+ name: string,
93
+ ctx: unknown,
94
+ event: unknown = {},
95
+ ) {
96
+ for (const handler of handlers.get(name) ?? []) await handler(event, ctx);
97
+ }
98
+
99
+ function harness() {
100
+ const calls: Array<[string, unknown?]> = [];
101
+ const forbidden = vi.fn();
102
+ let indicator: { frames?: string[]; intervalMs?: number } | undefined;
103
+ let message = "";
104
+ let loader: Loader | undefined;
105
+ const ui = {
106
+ theme: theme(),
107
+ getEditorComponent: () => undefined,
108
+ setEditorComponent: forbidden,
109
+ setFooter: forbidden,
110
+ setWidget: forbidden,
111
+ setWorkingVisible: forbidden,
112
+ setWorkingMessage(value?: string) {
113
+ calls.push(["message", value]);
114
+ message = value ?? "Working...";
115
+ loader?.setMessage(message);
116
+ },
117
+ setWorkingIndicator(value?: unknown) {
118
+ calls.push(["indicator", value]);
119
+ indicator = value as typeof indicator;
120
+ loader?.setIndicator(indicator);
121
+ },
122
+ };
123
+ return {
124
+ calls,
125
+ forbidden,
126
+ activateLoader() {
127
+ loader = new Loader(
128
+ { requestRender() {} } as never,
129
+ (text) => text,
130
+ (text) => text,
131
+ message,
132
+ indicator,
133
+ );
134
+ return loader;
135
+ },
136
+ ctx: {
137
+ hasUI: true,
138
+ mode: "tui",
139
+ cwd: process.cwd(),
140
+ model: undefined,
141
+ getContextUsage: () => null,
142
+ sessionManager: { getBranch: () => [], getSessionName: () => undefined },
143
+ ui,
144
+ },
145
+ };
146
+ }
147
+
148
+ beforeEach(() => {
149
+ runtime.enabled = true;
150
+ runtime.custom = true;
151
+ runtime.message = "Stable";
152
+ runtime.spinner = "star-bloom";
153
+ runtime.spinnerIntervalMs = 100;
154
+ runtime.textIntervalMs = 60;
155
+ });
156
+
157
+ describe("working-line extension lifecycle integration", () => {
158
+ it("wires full-row rebuilds, authoritative usage, parallel tools, and isolated cleanup", async () => {
159
+ const handlers = loadExtension();
160
+ const current = harness();
161
+ const row = () => {
162
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] } | undefined;
163
+ return stripTerminalSequences(indicator?.frames?.[0] ?? "");
164
+ };
165
+ await emit(handlers, "session_start", current.ctx);
166
+ expect(current.calls.slice(0, 2).map(([name, value]) => [name, value])).toEqual([
167
+ ["message", ""],
168
+ ["indicator", expect.any(Object)],
169
+ ]);
170
+ await emit(handlers, "agent_start", current.ctx);
171
+ await emit(handlers, "turn_start", current.ctx, { turnIndex: 0, timestamp: Date.now() });
172
+ const partialAssistant = {
173
+ role: "assistant",
174
+ usage: { input: 100, output: 4 },
175
+ content: [],
176
+ api: "google-generative-ai",
177
+ provider: "google",
178
+ model: "test",
179
+ stopReason: "toolUse",
180
+ timestamp: Date.now(),
181
+ };
182
+ const laterPartialAssistant = { ...partialAssistant, usage: { input: 120, output: 7 } };
183
+ const finalAssistant = { ...partialAssistant, usage: { input: 150, output: 9 } };
184
+ await emit(handlers, "message_update", current.ctx, { message: partialAssistant });
185
+ expect(row()).toMatch(/Stable · \d+s · ↑100 ↓4/);
186
+ const beforeDuplicate = current.calls.length;
187
+ await emit(handlers, "message_update", current.ctx, { message: partialAssistant });
188
+ expect(current.calls).toHaveLength(beforeDuplicate);
189
+ await emit(handlers, "message_update", current.ctx, { message: laterPartialAssistant });
190
+ expect(row()).toMatch(/Stable · \d+s · ↑120 ↓7/);
191
+ await emit(handlers, "message_end", current.ctx, { message: finalAssistant });
192
+ expect(row()).toMatch(/Stable · \d+s · ↑150 ↓9/);
193
+ await emit(handlers, "tool_execution_start", current.ctx, {
194
+ toolCallId: "one",
195
+ toolName: "read",
196
+ });
197
+ expect(row()).toMatch(/Stable · read · \d+s · ↑150 ↓9/);
198
+ await emit(handlers, "tool_execution_start", current.ctx, {
199
+ toolCallId: "two",
200
+ toolName: "bash",
201
+ });
202
+ expect(row()).toMatch(/Stable · bash · \d+s · ↑150 ↓9/);
203
+ await emit(handlers, "tool_execution_end", current.ctx, { toolCallId: "two" });
204
+ expect(row()).toMatch(/Stable · read · \d+s · ↑150 ↓9/);
205
+ await emit(handlers, "tool_execution_end", current.ctx, { toolCallId: "one" });
206
+ expect(row()).toMatch(/Stable · \d+s · ↑150 ↓9/);
207
+ await emit(handlers, "turn_start", current.ctx, {
208
+ turnIndex: 1,
209
+ timestamp: Date.now(),
210
+ });
211
+ expect(row()).toContain("↑150 ↓9");
212
+ await emit(handlers, "agent_end", current.ctx);
213
+ expect(current.calls.filter(([name]) => name === "message")).toEqual([["message", ""]]);
214
+ expect(current.forbidden).not.toHaveBeenCalled();
215
+ const beforeLateEnds = current.calls.length;
216
+ await emit(handlers, "message_end", current.ctx, {
217
+ message: { role: "user", usage: { input: 99, output: 99 } },
218
+ });
219
+ await emit(handlers, "message_end", current.ctx, {
220
+ message: { ...finalAssistant, responseId: "late-after-agent" },
221
+ });
222
+ expect(current.calls).toHaveLength(beforeLateEnds);
223
+ await emit(handlers, "session_shutdown", current.ctx);
224
+ expect(current.calls.slice(-2).map(([name, value]) => [name, value])).toEqual([
225
+ ["indicator", undefined],
226
+ ["message", undefined],
227
+ ]);
228
+ expect(current.forbidden).not.toHaveBeenCalled();
229
+ });
230
+
231
+ it("compacts live provider usage", async () => {
232
+ const handlers = loadExtension();
233
+ const current = harness();
234
+ const row = () => {
235
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] } | undefined;
236
+ return stripTerminalSequences(indicator?.frames?.[0] ?? "");
237
+ };
238
+ const message = (input: number, output: number) => ({
239
+ role: "assistant",
240
+ usage: { input, output },
241
+ content: [],
242
+ responseId: "reported",
243
+ });
244
+
245
+ await emit(handlers, "session_start", current.ctx);
246
+ await emit(handlers, "agent_start", current.ctx);
247
+ await emit(handlers, "turn_start", current.ctx);
248
+ await emit(handlers, "message_update", current.ctx, { message: message(27_000, 1_400) });
249
+ expect(row()).toContain("↑27k ↓1.4k");
250
+ await emit(handlers, "message_end", current.ctx, { message: message(27_000, 1_400) });
251
+ expect(row()).toContain("↑27k ↓1.4k");
252
+ });
253
+
254
+ it("visibly reconciles an estimated 1.0k output to exact 999", async () => {
255
+ const handlers = loadExtension();
256
+ const current = harness();
257
+ const row = () => {
258
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] } | undefined;
259
+ return stripTerminalSequences(indicator?.frames?.[0] ?? "");
260
+ };
261
+ const message = (output: number) => ({
262
+ role: "assistant",
263
+ usage: { input: 0, output },
264
+ content: [],
265
+ responseId: "estimated-boundary",
266
+ });
267
+
268
+ await emit(handlers, "session_start", current.ctx);
269
+ await emit(handlers, "agent_start", current.ctx);
270
+ await emit(handlers, "turn_start", current.ctx);
271
+ const partial = message(998);
272
+ await emit(handlers, "message_update", current.ctx, { message: partial });
273
+ await emit(handlers, "message_update", current.ctx, {
274
+ message: partial,
275
+ assistantMessageEvent: {
276
+ type: "text_delta",
277
+ contentIndex: 0,
278
+ delta: "abcdefgh",
279
+ partial: { content: [{ type: "text", text: "abcdefgh" }] },
280
+ },
281
+ });
282
+ expect(row()).toContain("↑0 ↓1.0k");
283
+ await emit(handlers, "message_end", current.ctx, { message: message(999) });
284
+ expect(row()).toContain("↑0 ↓999");
285
+ });
286
+
287
+ it("keeps cumulative billion-scale totals compact across continuations", async () => {
288
+ runtime.message = "Response usage stays visible here";
289
+ const handlers = loadExtension();
290
+ const current = harness();
291
+ const row = () => {
292
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] } | undefined;
293
+ return stripTerminalSequences(indicator?.frames?.[0] ?? "");
294
+ };
295
+ const assistant = (input: number, output: number, responseId: string) => ({
296
+ role: "assistant",
297
+ usage: { input, output },
298
+ content: [],
299
+ api: "openai-completions",
300
+ provider: "openai-compatible",
301
+ model: "test",
302
+ stopReason: "toolUse",
303
+ timestamp: Date.now(),
304
+ responseId,
305
+ });
306
+
307
+ await emit(handlers, "session_start", current.ctx);
308
+ await emit(handlers, "agent_start", current.ctx);
309
+ await emit(handlers, "turn_start", current.ctx);
310
+ await emit(handlers, "message_end", current.ctx, {
311
+ message: assistant(1_000_000_000, 1_000_000_000, "first"),
312
+ });
313
+ expect(row()).toContain("↑1000M ↓1000M");
314
+
315
+ await emit(handlers, "turn_start", current.ctx);
316
+ expect(row()).toContain("↑1000M ↓1000M");
317
+ await emit(handlers, "tool_execution_start", current.ctx, {
318
+ toolCallId: "wide",
319
+ toolName: "123456789012345678",
320
+ });
321
+ const writesBeforeLiveUsage = current.calls.length;
322
+ await emit(handlers, "message_update", current.ctx, {
323
+ message: assistant(12, 3, "second"),
324
+ });
325
+ expect(row()).toContain("↑1000M ↓1000M");
326
+ expect(current.calls).toHaveLength(writesBeforeLiveUsage);
327
+
328
+ await emit(handlers, "tool_execution_end", current.ctx, { toolCallId: "wide" });
329
+ await emit(handlers, "message_end", current.ctx, {
330
+ message: assistant(14, 5, "second"),
331
+ });
332
+ expect(row()).toContain("↑1000M ↓1000M");
333
+
334
+ await emit(handlers, "turn_start", current.ctx);
335
+ await emit(handlers, "message_end", current.ctx, {
336
+ message: assistant(499_986, 499_995, "third"),
337
+ });
338
+ expect(row()).toContain("↑1001M ↓1001M");
339
+ });
340
+
341
+ it("keeps committed tokens through retry lifecycle and initial zero usage, then accumulates final usage", async () => {
342
+ const handlers = loadExtension();
343
+ const current = harness();
344
+ const row = () => {
345
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] } | undefined;
346
+ return stripTerminalSequences(indicator?.frames?.[0] ?? "");
347
+ };
348
+ const assistant = (input: number, output: number, responseId: string) => ({
349
+ role: "assistant",
350
+ usage: { input, output },
351
+ provider: "mistral",
352
+ responseId,
353
+ });
354
+
355
+ await emit(handlers, "session_start", current.ctx);
356
+ await emit(handlers, "agent_start", current.ctx);
357
+ await emit(handlers, "turn_start", current.ctx);
358
+ await emit(handlers, "message_end", current.ctx, {
359
+ message: assistant(20, 4, "committed"),
360
+ });
361
+ expect(row()).toContain("↑20 ↓4");
362
+
363
+ await emit(handlers, "agent_end", current.ctx);
364
+ expect(row()).toContain("↑20 ↓4");
365
+ await emit(handlers, "agent_start", current.ctx);
366
+ expect(row()).toContain("↑20 ↓4");
367
+ await emit(handlers, "turn_start", current.ctx);
368
+ expect(row()).toContain("↑20 ↓4");
369
+ await emit(handlers, "message_update", current.ctx, {
370
+ message: assistant(0, 0, "retry"),
371
+ });
372
+ expect(row()).toContain("↑20 ↓4");
373
+ await emit(handlers, "message_end", current.ctx, {
374
+ message: assistant(5, 2, "retry"),
375
+ });
376
+ expect(row()).toContain("↑25 ↓6");
377
+ });
378
+
379
+ it("shows OpenAI Codex Responses placeholder usage as a zero estimate until exact final usage", async () => {
380
+ const handlers = loadExtension();
381
+ const current = harness();
382
+ const row = () => {
383
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] } | undefined;
384
+ return stripTerminalSequences(indicator?.frames?.[0] ?? "");
385
+ };
386
+ const partial = {
387
+ role: "assistant",
388
+ content: [{ type: "text", text: "streaming" }],
389
+ // Codex Responses reports exact usage only on terminal response.completed.
390
+ usage: { input: 0, output: 0 },
391
+ provider: "openai-codex",
392
+ model: "gpt-5.4",
393
+ responseId: "terminal-only",
394
+ };
395
+ await emit(handlers, "session_start", current.ctx);
396
+ await emit(handlers, "agent_start", current.ctx);
397
+ await emit(handlers, "turn_start", current.ctx);
398
+ const writesBeforeUpdates = current.calls.length;
399
+ await emit(handlers, "message_update", current.ctx, { message: partial });
400
+ await emit(handlers, "message_update", current.ctx, { message: partial });
401
+ await emit(handlers, "message_update", current.ctx, {
402
+ message: { role: "assistant", usage: { input: 2.5, output: 1 } },
403
+ });
404
+ await emit(handlers, "message_update", current.ctx, {
405
+ message: { role: "user", usage: { input: 99, output: 99 } },
406
+ });
407
+ expect(current.calls).toHaveLength(writesBeforeUpdates + 1);
408
+ expect(row()).toContain("↑0 ↓0");
409
+ await emit(handlers, "message_end", current.ctx, {
410
+ message: { ...partial, usage: { input: 42, output: 6 } },
411
+ });
412
+ expect(row()).toContain("↑42 ↓6");
413
+ });
414
+
415
+ it("preserves approximation across rejected finals and ignores late messages", async () => {
416
+ const handlers = loadExtension();
417
+ const current = harness();
418
+ const row = () => {
419
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] } | undefined;
420
+ return stripTerminalSequences(indicator?.frames?.[0] ?? "");
421
+ };
422
+ const assistantMessage = (input: number, output: number, responseId: string) => ({
423
+ role: "assistant",
424
+ usage: { input, output },
425
+ content: [],
426
+ responseId,
427
+ });
428
+ const partial = assistantMessage(0, 0, "active");
429
+ await emit(handlers, "session_start", current.ctx);
430
+ await emit(handlers, "agent_start", current.ctx);
431
+ await emit(handlers, "turn_start", current.ctx);
432
+ await emit(handlers, "message_end", current.ctx, {
433
+ message: assistantMessage(1, 1, "already-committed"),
434
+ });
435
+ await emit(handlers, "turn_start", current.ctx);
436
+ await emit(handlers, "message_update", current.ctx, { message: partial });
437
+ expect(row()).toContain("↑1 ↓1");
438
+ const beforeRejected = current.calls.length;
439
+ await emit(handlers, "message_end", current.ctx, {
440
+ message: assistantMessage(90, 9, "mismatched"),
441
+ });
442
+ expect(current.calls).toHaveLength(beforeRejected);
443
+ expect(row()).toContain("↑1 ↓1");
444
+ await emit(handlers, "message_end", current.ctx, {
445
+ message: assistantMessage(90, 9, "already-committed"),
446
+ });
447
+ expect(current.calls).toHaveLength(beforeRejected);
448
+ expect(row()).toContain("↑1 ↓1");
449
+ const beforeLateMessages = current.calls.length;
450
+ await emit(handlers, "message_end", current.ctx, {
451
+ message: { role: "user", usage: { input: 99, output: 99 } },
452
+ });
453
+ await emit(handlers, "message_end", current.ctx, {
454
+ message: assistantMessage(99, 99, "late-unmatched"),
455
+ });
456
+ expect(current.calls).toHaveLength(beforeLateMessages);
457
+ expect(row()).toContain("↑1 ↓1");
458
+ await emit(handlers, "message_end", current.ctx, {
459
+ message: assistantMessage(7, 2, "active"),
460
+ });
461
+ expect(row()).toContain("↑8 ↓3");
462
+ const beforeDuplicate = current.calls.length;
463
+ await emit(handlers, "message_end", current.ctx, {
464
+ message: assistantMessage(99, 99, "active"),
465
+ });
466
+ expect(current.calls).toHaveLength(beforeDuplicate);
467
+ expect(row()).toContain("↑8 ↓3");
468
+ });
469
+
470
+ it("streams OpenCode tool-call arguments immediately and reconciles whole-interaction totals", async () => {
471
+ const handlers = loadExtension();
472
+ const current = harness();
473
+ const rows = () =>
474
+ current.calls
475
+ .filter(([name, value]) => name === "indicator" && value !== undefined)
476
+ .map(([, value]) =>
477
+ stripTerminalSequences((value as { frames?: string[] }).frames?.[0] ?? ""),
478
+ );
479
+ const message = (
480
+ input: number,
481
+ output: number,
482
+ responseId: string,
483
+ content: unknown[] = [],
484
+ ) => ({
485
+ role: "assistant",
486
+ usage: { input, output },
487
+ content,
488
+ api: "openai-completions",
489
+ provider: "opencode",
490
+ model: "openai/gpt-5",
491
+ stopReason: "toolUse",
492
+ timestamp: Date.now(),
493
+ responseId,
494
+ });
495
+ const toolCall = { type: "toolCall", id: "call-1", name: "bash", arguments: {} };
496
+
497
+ await emit(handlers, "session_start", current.ctx);
498
+ await emit(handlers, "agent_start", current.ctx);
499
+ await emit(handlers, "turn_start", current.ctx);
500
+ await emit(handlers, "message_end", current.ctx, { message: message(10, 2, "first") });
501
+ await emit(handlers, "turn_start", current.ctx);
502
+ const partial = message(0, 0, "tool-response", [toolCall]);
503
+ await emit(handlers, "message_update", current.ctx, {
504
+ message: partial,
505
+ assistantMessageEvent: {
506
+ type: "toolcall_start",
507
+ contentIndex: 0,
508
+ partial,
509
+ },
510
+ });
511
+ const beforeDeltas = current.calls.length;
512
+ await emit(handlers, "message_update", current.ctx, {
513
+ message: partial,
514
+ assistantMessageEvent: {
515
+ type: "toolcall_delta",
516
+ contentIndex: 0,
517
+ delta: '{"command":',
518
+ partial,
519
+ },
520
+ });
521
+ await emit(handlers, "message_update", current.ctx, {
522
+ message: partial,
523
+ assistantMessageEvent: {
524
+ type: "toolcall_delta",
525
+ contentIndex: 0,
526
+ delta: '{"command":',
527
+ partial,
528
+ },
529
+ });
530
+ expect(current.calls).toHaveLength(beforeDeltas + 2);
531
+ expect(rows().some((row) => row.includes("↑10 ↓5"))).toBe(true);
532
+ expect(rows().at(-1)).toContain("↑10 ↓8");
533
+ expect(rows().at(-1)).not.toMatch(/thinking|thought for/);
534
+ const beforeEnd = current.calls.length;
535
+ await emit(handlers, "message_update", current.ctx, {
536
+ message: partial,
537
+ assistantMessageEvent: {
538
+ type: "toolcall_end",
539
+ contentIndex: 0,
540
+ toolCall,
541
+ partial,
542
+ },
543
+ });
544
+ expect(current.calls).toHaveLength(beforeEnd);
545
+ await emit(handlers, "message_end", current.ctx, {
546
+ message: message(15, 4, "tool-response", [toolCall]),
547
+ });
548
+ expect(rows().at(-1)).toContain("↑25 ↓6");
549
+ expect(rows().at(-1)).not.toMatch(/thinking|thought for/);
550
+ });
551
+
552
+ it("preserves estimated values on malformed finals and reconciles exact finals", async () => {
553
+ const handlers = loadExtension();
554
+ const current = harness();
555
+ const row = () => {
556
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] } | undefined;
557
+ return stripTerminalSequences(indicator?.frames?.[0] ?? "");
558
+ };
559
+ const message = (input: number, output: number, responseId: string) => ({
560
+ role: "assistant",
561
+ usage: { input, output },
562
+ content: [],
563
+ responseId,
564
+ });
565
+
566
+ await emit(handlers, "session_start", current.ctx);
567
+ await emit(handlers, "agent_start", current.ctx);
568
+ await emit(handlers, "turn_start", current.ctx);
569
+ await emit(handlers, "message_update", current.ctx, {
570
+ message: message(0, 0, "malformed"),
571
+ assistantMessageEvent: {
572
+ type: "text_delta",
573
+ contentIndex: 0,
574
+ delta: "abcdefghijkl",
575
+ partial: { content: [{ type: "text", text: "abcdefghijkl" }] },
576
+ },
577
+ });
578
+ expect(row()).toContain("↑0 ↓3");
579
+ await emit(handlers, "message_end", current.ctx, {
580
+ message: message(-1, 9, "malformed"),
581
+ });
582
+ expect(row()).toContain("↑0 ↓3");
583
+
584
+ await emit(handlers, "turn_start", current.ctx);
585
+ await emit(handlers, "message_update", current.ctx, {
586
+ message: message(0, 0, "exact"),
587
+ assistantMessageEvent: {
588
+ type: "text_delta",
589
+ contentIndex: 0,
590
+ delta: "abcdefgh",
591
+ partial: { content: [{ type: "text", text: "abcdefgh" }] },
592
+ },
593
+ });
594
+ expect(row()).toContain("↑0 ↓2");
595
+ await emit(handlers, "message_end", current.ctx, {
596
+ message: message(4, 1, "exact"),
597
+ });
598
+ expect(row()).toContain("↑4 ↓1");
599
+ });
600
+
601
+ it("ignores an all-zero placeholder after nonzero live usage", async () => {
602
+ const handlers = loadExtension();
603
+ const current = harness();
604
+ const row = () => {
605
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] } | undefined;
606
+ return stripTerminalSequences(indicator?.frames?.[0] ?? "");
607
+ };
608
+ const message = (input: number, output: number) => ({
609
+ role: "assistant",
610
+ usage: { input, output },
611
+ provider: "mistral",
612
+ responseId: "zero-snapshot",
613
+ });
614
+ await emit(handlers, "session_start", current.ctx);
615
+ await emit(handlers, "agent_start", current.ctx);
616
+ await emit(handlers, "turn_start", current.ctx);
617
+ await emit(handlers, "message_update", current.ctx, { message: message(20, 4) });
618
+ expect(row()).toContain("↑20 ↓4");
619
+ await emit(handlers, "message_update", current.ctx, { message: message(0, 0) });
620
+ expect(row()).toContain("↑20 ↓4");
621
+ await emit(handlers, "message_end", current.ctx, { message: message(25, 7) });
622
+ expect(row()).toContain("↑25 ↓7");
623
+ });
624
+
625
+ it.each(["star-bloom", "pulse"] as const)(
626
+ "rebases the %s spinner and animated text on delayed Loader activation and later rebuilds",
627
+ async (spinner) => {
628
+ runtime.spinner = spinner;
629
+ vi.useFakeTimers();
630
+ vi.setSystemTime(0);
631
+ const handlers = loadExtension();
632
+ const current = harness();
633
+ let loader: Loader | undefined;
634
+ try {
635
+ await emit(handlers, "session_start", current.ctx);
636
+ vi.advanceTimersByTime(5000);
637
+ loader = current.activateLoader();
638
+ const activatedAtFrameZero = loader.render(80)[1] ?? "";
639
+ await emit(handlers, "agent_start", current.ctx);
640
+ await emit(handlers, "turn_start", current.ctx, { turnIndex: 0, timestamp: Date.now() });
641
+ expect(current.calls.filter(([name]) => name === "indicator")).toHaveLength(2);
642
+ expect(loaderPhase(loader.render(80)[1] ?? "")).toEqual(loaderPhase(activatedAtFrameZero));
643
+
644
+ vi.advanceTimersByTime(900);
645
+ const beforeRebuild = loader.render(80)[1] ?? "";
646
+ const beforePhase = loaderPhase(beforeRebuild);
647
+ expect(beforePhase[1], "expected animated-text high tier before rebuild").toBeDefined();
648
+ const assistant = {
649
+ role: "assistant",
650
+ usage: { input: 12, output: 3 },
651
+ content: [],
652
+ api: "google-generative-ai",
653
+ provider: "google",
654
+ model: "test",
655
+ stopReason: "toolUse",
656
+ timestamp: Date.now(),
657
+ };
658
+ await emit(handlers, "message_update", current.ctx, { message: assistant });
659
+ const afterRebuild = loader.render(80)[1] ?? "";
660
+ expect(stripTerminalSequences(afterRebuild)).toContain("↑12 ↓3");
661
+ const afterPhase = loaderPhase(afterRebuild);
662
+ expect(afterPhase[1], "expected animated-text high tier after rebuild").toBeDefined();
663
+ expect(afterPhase).toEqual(beforePhase);
664
+ const writesAfterRebuild = current.calls.length;
665
+ await emit(handlers, "message_update", current.ctx, { message: assistant });
666
+ expect(current.calls).toHaveLength(writesAfterRebuild);
667
+ } finally {
668
+ loader?.stop();
669
+ await emit(handlers, "session_shutdown", current.ctx);
670
+ vi.useRealTimers();
671
+ }
672
+ },
673
+ );
674
+
675
+ it("regenerates repeated meaningful streaming rows within the worst bounded scheduler budget", async () => {
676
+ runtime.message = "m".repeat(43);
677
+ runtime.spinnerIntervalMs = 997;
678
+ runtime.textIntervalMs = 900;
679
+ const handlers = loadExtension();
680
+ const current = harness();
681
+ await emit(handlers, "session_start", current.ctx);
682
+ await emit(handlers, "agent_start", current.ctx);
683
+ await emit(handlers, "turn_start", current.ctx);
684
+ const callsBeforeStreaming = current.calls.length;
685
+ const started = performance.now();
686
+ for (let output = 1; output <= 20; output++) {
687
+ await emit(handlers, "message_update", current.ctx, {
688
+ message: {
689
+ role: "assistant",
690
+ usage: { input: 999_999_999, output },
691
+ responseId: "performance-stream",
692
+ },
693
+ });
694
+ }
695
+ const elapsedMs = performance.now() - started;
696
+ expect(current.calls).toHaveLength(callsBeforeStreaming + 20);
697
+ expect(elapsedMs).toBeLessThan(10_000);
698
+ await emit(handlers, "session_shutdown", current.ctx);
699
+ });
700
+
701
+ it("owns the full fallback row while custom messages are off and releases both APIs", async () => {
702
+ runtime.custom = false;
703
+ const handlers = loadExtension();
704
+ const current = harness();
705
+ await emit(handlers, "session_start", current.ctx);
706
+ await emit(handlers, "agent_start", current.ctx);
707
+ await emit(handlers, "turn_start", current.ctx, { turnIndex: 0, timestamp: Date.now() });
708
+ await emit(handlers, "agent_end", current.ctx);
709
+ expect(current.calls[0]).toEqual(["message", ""]);
710
+ const indicator = current.calls.at(-1)?.[1] as { frames?: string[] };
711
+ expect(stripTerminalSequences(indicator.frames?.[0] ?? "")).toContain("Working…");
712
+ await emit(handlers, "session_shutdown", current.ctx);
713
+ expect(current.calls.slice(-2)).toEqual([
714
+ ["indicator", undefined],
715
+ ["message", undefined],
716
+ ]);
717
+ });
718
+
719
+ it("makes zero working-row calls when startup is disabled", async () => {
720
+ runtime.enabled = false;
721
+ const handlers = loadExtension();
722
+ const current = harness();
723
+ await emit(handlers, "session_start", current.ctx);
724
+ await emit(handlers, "agent_start", current.ctx);
725
+ await emit(handlers, "agent_end", current.ctx);
726
+ await emit(handlers, "session_shutdown", current.ctx);
727
+ expect(current.calls).toEqual([]);
728
+ expect(current.forbidden).not.toHaveBeenCalled();
729
+ });
730
+ });