@selesai/code 0.9.14 → 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 (263) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +6 -9
  3. package/dist/defaults/models.json +79 -29
  4. package/dist/defaults/settings.json +3 -7
  5. package/dist/extensions/package.json +3 -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/docs/plans/auto-model-routing-extension.md +490 -0
  193. package/package.json +3 -3
  194. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  195. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  196. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  197. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  198. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  199. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  200. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  201. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  202. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  203. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  204. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  205. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  206. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  207. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  208. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  209. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  210. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  211. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  212. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  213. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  214. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  215. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  216. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  217. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  218. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  219. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  220. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  221. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  222. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  223. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  224. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  225. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  226. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  227. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  228. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  229. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  230. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  231. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  232. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  233. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  234. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  235. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  236. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  237. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  238. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  239. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  240. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  241. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  242. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  243. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  244. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  245. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  246. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  247. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  248. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  249. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  250. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  251. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  252. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  253. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  254. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  255. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  256. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  257. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  258. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  259. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  260. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  261. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  262. package/dist/extensions/preview-tools-disabled.ts +0 -11
  263. 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
+ });