@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,347 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+ import type { Theme } from "@earendil-works/pi-coding-agent";
3
+ import { visibleWidth } from "@earendil-works/pi-tui";
4
+ import { describe, expect, it } from "vitest";
5
+ import {
6
+ defaultConfig,
7
+ type EditorStyle,
8
+ type PolishedTuiConfig,
9
+ type UserMessageStyle,
10
+ } from "../extensions/zentui/config";
11
+ import {
12
+ renderEditorSettingsPreview,
13
+ renderUserMessageSettingsPreview,
14
+ SETTINGS_PREVIEW_MAX_ROWS,
15
+ SETTINGS_PREVIEW_MAX_WIDTH,
16
+ } from "../extensions/zentui/settings-previews";
17
+
18
+ function theme(offset = 0): Theme {
19
+ return {
20
+ fg: (color: string, text: string) => {
21
+ const index =
22
+ ([...color].reduce((total, character) => total + character.charCodeAt(0), 0) + offset) %
23
+ 200;
24
+ return `\x1b[38;5;${index}m${text}\x1b[0m`;
25
+ },
26
+ bg: (_color: string, text: string) => `\x1b[48;5;234m${text}\x1b[49m`,
27
+ getBgAnsi: () => "\x1b[48;5;234m",
28
+ bold: (text: string) => `\x1b[1m${text}\x1b[0m`,
29
+ italic: (text: string) => text,
30
+ underline: (text: string) => text,
31
+ strikethrough: (text: string) => text,
32
+ } as unknown as Theme;
33
+ }
34
+
35
+ function config(): PolishedTuiConfig {
36
+ const current = structuredClone(defaultConfig);
37
+ current.icons.mode = "nerd";
38
+ current.components.editor.enabled = true;
39
+ current.components.editor.viewportIndicators = true;
40
+ current.components.editor.styles.opencode.completionMenu = "palette";
41
+ current.components.editor.styles["opencode-copy-friendly"].completionMenu = "palette";
42
+ current.components.editor.modelLabel = "id";
43
+ current.components.editor.borderColorMode = "static";
44
+ return current;
45
+ }
46
+
47
+ function plain(lines: string[]): string {
48
+ return stripVTControlCharacters(lines.join("\n"));
49
+ }
50
+
51
+ function expectOnlyTrustedSgr(rows: string[], hostileText: readonly string[] = []): void {
52
+ for (const row of rows) {
53
+ expect(row).not.toMatch(/[\r\n]/);
54
+ for (let index = 0; index < row.length; ) {
55
+ const code = row.charCodeAt(index);
56
+ if (code === 0x1b) {
57
+ const sgr = /^\x1b\[[0-9:;]*m/.exec(row.slice(index));
58
+ expect(sgr, `unexpected terminal escape in ${JSON.stringify(row)}`).not.toBeNull();
59
+ index += sgr?.[0].length ?? 1;
60
+ continue;
61
+ }
62
+ expect(
63
+ code >= 0x20 && !(code >= 0x7f && code <= 0x9f),
64
+ `unexpected control U+${code.toString(16).padStart(4, "0")} in ${JSON.stringify(row)}`,
65
+ ).toBe(true);
66
+ index += 1;
67
+ }
68
+ }
69
+ const raw = rows.join("");
70
+ for (const text of hostileText) expect(raw).not.toContain(text);
71
+ }
72
+
73
+ const widths = [0, 1, 4, 20, 60, 72, 100];
74
+
75
+ describe("settings previews", () => {
76
+ it.each(widths)("bounds deterministic Editor output at width %i", (width) => {
77
+ const current = config();
78
+ const first = renderEditorSettingsPreview(current, theme(), width);
79
+ expect(first).toEqual(renderEditorSettingsPreview(current, theme(), width));
80
+ expect(first.length).toBeLessThanOrEqual(SETTINGS_PREVIEW_MAX_ROWS);
81
+ if (width <= 0) expect(first).toEqual([]);
82
+ if (first.length > 0) expect(visibleWidth(first.at(-1) ?? "")).toBeGreaterThan(0);
83
+ for (const row of first)
84
+ expect(visibleWidth(row)).toBeLessThanOrEqual(Math.min(width, SETTINGS_PREVIEW_MAX_WIDTH));
85
+ });
86
+
87
+ it.each(widths)("bounds deterministic User-message output at width %i", (width) => {
88
+ const current = config();
89
+ const first = renderUserMessageSettingsPreview(current, theme(), width);
90
+ expect(first).toEqual(renderUserMessageSettingsPreview(current, theme(), width));
91
+ expect(first.length).toBeLessThanOrEqual(SETTINGS_PREVIEW_MAX_ROWS);
92
+ if (width <= 0) expect(first).toEqual([]);
93
+ if (first.length > 0) expect(visibleWidth(first.at(-1) ?? "")).toBeGreaterThan(0);
94
+ for (const row of first)
95
+ expect(visibleWidth(row)).toBeLessThanOrEqual(Math.min(width, SETTINGS_PREVIEW_MAX_WIDTH));
96
+ });
97
+
98
+ it("renders the defining structure of all four Editor styles without status cues", () => {
99
+ const outputs = Object.fromEntries(
100
+ (["opencode", "opencode-copy-friendly", "accent-rail", "minimalist"] as EditorStyle[]).map(
101
+ (style) => {
102
+ const current = config();
103
+ current.components.editor.style = style;
104
+ return [style, plain(renderEditorSettingsPreview(current, theme(), 72))];
105
+ },
106
+ ),
107
+ ) as Record<EditorStyle, string>;
108
+ expect(new Set(Object.values(outputs)).size).toBe(4);
109
+ for (const output of Object.values(outputs)) {
110
+ expect(output).not.toContain("Editor preview");
111
+ expect(output).not.toContain("preview enabled");
112
+ expect(output).not.toContain("preview disabled");
113
+ }
114
+ expect(outputs.opencode).toContain("│ Explain this change safely.");
115
+ expect(outputs.opencode).toContain("─── ↑ 2 more");
116
+ expect(outputs.opencode).toContain("↑↓ Navigate");
117
+ expect(outputs.opencode).not.toContain("→ settings");
118
+ expect(outputs["opencode-copy-friendly"]).toContain("\nExplain this change safely.");
119
+ expect(outputs["opencode-copy-friendly"]).toContain("↑↓ Navigate");
120
+ expect(outputs["opencode-copy-friendly"]).toContain("\n sonnet-4");
121
+ expect(outputs["opencode-copy-friendly"]).not.toContain("│ Explain this change safely.");
122
+ expect(outputs["accent-rail"]).toContain("▎ Explain this change safely.");
123
+ expect(outputs["accent-rail"]).toContain("▎ settings Open settings");
124
+ expect(outputs["accent-rail"]).toContain(" files Search files");
125
+ expect(outputs["accent-rail"]).not.toContain("sonnet-4");
126
+ expect(outputs.minimalist).toContain("╭─ ↑ 2 more");
127
+ expect(outputs.minimalist).toContain("╰─ ↓ 3 more");
128
+ expect(outputs.minimalist).toContain("feat/settings-previews");
129
+ const disabled = config();
130
+ disabled.components.editor.enabled = false;
131
+ const disabledOutput = plain(renderEditorSettingsPreview(disabled, theme(), 72));
132
+ expect(disabledOutput).toContain("Explain this change safely.");
133
+ expect(disabledOutput).not.toContain("preview");
134
+ });
135
+
136
+ it("previews transparent palette and native completion menus independently for both Opencode styles", () => {
137
+ const current = config();
138
+ const renderRows = () => renderEditorSettingsPreview(current, theme(), 72);
139
+ const render = () => plain(renderRows());
140
+ expect(render()).toContain("↑↓ Navigate");
141
+ expect(render()).not.toContain("→ settings");
142
+ expect(render()).not.toContain("(1/47)");
143
+ expect(renderRows().join("\n")).not.toContain("\x1b[48;5;234m");
144
+
145
+ current.components.editor.styles.opencode.completionMenu = "native";
146
+ expect(render()).not.toContain("↑↓ Navigate");
147
+ expect(render()).toContain("→ settings");
148
+ expect(render()).toContain("(1/47)");
149
+ expect(current.components.editor.styles["opencode-copy-friendly"].completionMenu).toBe(
150
+ "palette",
151
+ );
152
+
153
+ current.components.editor.style = "opencode-copy-friendly";
154
+ expect(render()).toContain("↑↓ Navigate");
155
+ expect(render()).not.toContain("(1/47)");
156
+ current.components.editor.styles["opencode-copy-friendly"].completionMenu = "native";
157
+ expect(render()).not.toContain("↑↓ Navigate");
158
+ expect(render()).toContain("→ settings");
159
+ expect(render()).toContain("(1/47)");
160
+ });
161
+
162
+ it("passes enabled and disabled viewport indicators through the Accent Rail preview", () => {
163
+ const current = config();
164
+ current.components.editor.style = "accent-rail";
165
+ const render = () => plain(renderEditorSettingsPreview(current, theme(), 72));
166
+
167
+ current.components.editor.viewportIndicators = true;
168
+ expect(render()).toContain("▎ ↑ 2 more");
169
+ expect(render()).toContain("▎ ↓ 3 more");
170
+
171
+ current.components.editor.viewportIndicators = false;
172
+ expect(render()).not.toContain("↑ 2 more");
173
+ expect(render()).not.toContain("↓ 3 more");
174
+ expect(render()).toContain("▎ Explain this change safely.");
175
+ });
176
+
177
+ it("previews filled and transparent Accent Rail surfaces", () => {
178
+ const current = config();
179
+ current.components.editor.style = "accent-rail";
180
+ const render = () => renderEditorSettingsPreview(current, theme(), 72).join("\n");
181
+ const filled = render();
182
+ expect(filled).toContain("\x1b[48;5;234m");
183
+ expect(plain(filled.split("\n"))).toContain("▎ settings Open settings");
184
+
185
+ current.components.editor.styles["accent-rail"].transparent = true;
186
+ const transparent = render();
187
+ expect(transparent).not.toContain("\x1b[48;5;234m");
188
+ expect(plain(transparent.split("\n"))).toContain("▎ settings Open settings");
189
+ });
190
+
191
+ it("responds to Editor visible settings and selected metadata format", () => {
192
+ const current = config();
193
+ const render = () => renderEditorSettingsPreview(current, theme(), 72).join("\n");
194
+ const baseline = render();
195
+ current.components.editor.colorSource = "terminal";
196
+ expect(render()).not.toBe(baseline);
197
+ current.components.editor.modelLabel = "name";
198
+ expect(plain(renderEditorSettingsPreview(current, theme(), 72))).toContain("Sonnet 4");
199
+ current.components.editor.styles.opencode.metadataFormat = "$provider";
200
+ expect(plain(renderEditorSettingsPreview(current, theme(), 72))).toContain("Anthropic");
201
+ current.components.editor.viewportIndicators = false;
202
+ expect(plain(renderEditorSettingsPreview(current, theme(), 72))).not.toContain("↑ 2 more");
203
+ current.components.editor.viewportIndicators = true;
204
+ expect(plain(renderEditorSettingsPreview(current, theme(), 72))).toContain("↑ 2 more");
205
+ const staticBorder = render();
206
+ current.components.editor.borderColorMode = "adaptive";
207
+ expect(render()).not.toBe(staticBorder);
208
+ });
209
+
210
+ it("responds to every applicable Minimalist setting and thresholds", () => {
211
+ const current = config();
212
+ current.components.editor.style = "minimalist";
213
+ const minimalist = current.components.editor.styles.minimalist;
214
+ const render = () => renderEditorSettingsPreview(current, theme(), 72).join("\n");
215
+ const changes = [
216
+ () => (minimalist.pathDisplay = "full"),
217
+ () => (minimalist.contextFormat = "percent-total"),
218
+ () => (minimalist.contextGauge = !minimalist.contextGauge),
219
+ () => (minimalist.showSessionName = !minimalist.showSessionName),
220
+ () => (minimalist.showTimer = !minimalist.showTimer),
221
+ () => (minimalist.showCost = !minimalist.showCost),
222
+ () => (minimalist.showGit = !minimalist.showGit),
223
+ () => (minimalist.contextThresholds = { warning: 80, error: 90 }),
224
+ ];
225
+ for (const [index, change] of changes.entries()) {
226
+ const before = render();
227
+ change();
228
+ expect(render(), `Minimalist change ${index}`).not.toBe(before);
229
+ }
230
+ });
231
+
232
+ it("renders the defining structure of all four User-message styles without status cues", () => {
233
+ const outputs = Object.fromEntries(
234
+ (["framed", "framed-copy-friendly", "compact", "labeled"] as UserMessageStyle[]).map(
235
+ (style) => {
236
+ const current = config();
237
+ current.components.userMessages.style = style;
238
+ return [style, plain(renderUserMessageSettingsPreview(current, theme(), 72))];
239
+ },
240
+ ),
241
+ ) as Record<UserMessageStyle, string>;
242
+ expect(new Set(Object.values(outputs)).size).toBe(4);
243
+ for (const output of Object.values(outputs)) {
244
+ expect(output).not.toContain("User message preview");
245
+ expect(output).not.toContain("preview enabled");
246
+ expect(output).not.toContain("preview disabled");
247
+ }
248
+ expect(outputs.framed).toContain("│ Please review this change safely.");
249
+ expect(outputs.framed).toContain("────────────────");
250
+ const framed = config();
251
+ framed.components.userMessages.style = "framed";
252
+ const framedRows = plain(renderUserMessageSettingsPreview(framed, theme(), 72)).split("\n");
253
+ expect(framedRows.at(-1)).toMatch(/^─+$/);
254
+ expect(framedRows.at(-2)).toMatch(/^│ +$/);
255
+ expect(outputs["framed-copy-friendly"]).toContain("\n Please review this change safely.");
256
+ expect(outputs["framed-copy-friendly"]).not.toContain("│ Please review");
257
+ const copyFriendly = config();
258
+ copyFriendly.components.userMessages.style = "framed-copy-friendly";
259
+ const copyFriendlyRows = plain(
260
+ renderUserMessageSettingsPreview(copyFriendly, theme(), 72),
261
+ ).split("\n");
262
+ expect(copyFriendlyRows.at(-1)).toMatch(/^─+$/);
263
+ expect(copyFriendlyRows.at(-2)).toBe("");
264
+ expect(outputs.compact).toContain("│ Please review this change safely.");
265
+ expect(outputs.compact).not.toContain("────────────────");
266
+ expect(outputs.labeled).toContain("╭─ User ─");
267
+ expect(outputs.labeled).toContain("╰────");
268
+ const current = config();
269
+ const terminal = renderUserMessageSettingsPreview(current, theme(), 72).join("\n");
270
+ current.components.userMessages.colorSource = "terminal";
271
+ expect(renderUserMessageSettingsPreview(current, theme(), 72).join("\n")).not.toBe(terminal);
272
+ current.components.userMessages.enabled = false;
273
+ const disabledOutput = plain(renderUserMessageSettingsPreview(current, theme(), 72));
274
+ expect(disabledOutput).toContain("Please review this change safely.");
275
+ expect(disabledOutput).not.toContain("preview");
276
+ });
277
+
278
+ it("sanitizes hostile source and configured icons before trusted preview styling", () => {
279
+ const current = config();
280
+ current.icons.rail = "\x1b]2;RAIL-OSC\x07│\u009b31m\u009dRAIL-C1\u009c\u009b0m";
281
+ current.icons.editorPrompt =
282
+ "❯\x1b]8;;https://PROMPT-OSC.evil.invalid\x07\x1b]8;;\x07\x1b]2;TRUNCATED-OSC";
283
+ current.icons.ahead = "↑\x1bP$qARROW-DCS\x1b\\";
284
+ current.icons.behind = "↓\u009dARROW-C1\u009c";
285
+
286
+ current.components.editor.styles["accent-rail"].rail =
287
+ "▎\x1b]2;ACCENT-RAIL-OSC\x07\u009b31m\u009dACCENT-RAIL-C1\u009c\u009b0m";
288
+
289
+ for (const style of [
290
+ "opencode",
291
+ "opencode-copy-friendly",
292
+ "accent-rail",
293
+ "minimalist",
294
+ ] as EditorStyle[]) {
295
+ current.components.editor.style = style;
296
+ expectOnlyTrustedSgr(renderEditorSettingsPreview(current, theme(), 72), [
297
+ "RAIL-OSC",
298
+ "RAIL-C1",
299
+ "PROMPT-OSC",
300
+ "ARROW-DCS",
301
+ "ARROW-C1",
302
+ "evil.invalid",
303
+ "TRUNCATED-OSC",
304
+ "ACCENT-RAIL-OSC",
305
+ "ACCENT-RAIL-C1",
306
+ ]);
307
+ }
308
+ current.components.editor.style = "opencode";
309
+ expect(plain(renderEditorSettingsPreview(current, theme(), 72))).toContain("│");
310
+ current.components.editor.style = "opencode-copy-friendly";
311
+ expect(plain(renderEditorSettingsPreview(current, theme(), 72))).toContain("❯");
312
+ expect(current.icons.rail).toContain("RAIL-OSC");
313
+ expect(current.icons.editorPrompt).toContain("TRUNCATED-OSC");
314
+
315
+ for (const style of [
316
+ "framed",
317
+ "framed-copy-friendly",
318
+ "compact",
319
+ "labeled",
320
+ ] as UserMessageStyle[]) {
321
+ current.components.userMessages.style = style;
322
+ const rows = renderUserMessageSettingsPreview(
323
+ current,
324
+ theme(),
325
+ 72,
326
+ "safe \x1b[31mred\x1b[0m \x1b]2;SOURCE-OSC\x07 title \u009b32mgreen\u009b0m\nnext",
327
+ );
328
+ expectOnlyTrustedSgr(rows, ["RAIL-OSC", "RAIL-C1", "SOURCE-OSC"]);
329
+ const rendered = plain(rows);
330
+ expect(rendered).toContain("safe red");
331
+ expect(rendered).toContain("green");
332
+ expect(rendered).toContain("next");
333
+ }
334
+ });
335
+
336
+ it("re-renders from the current theme without cached preview rows", () => {
337
+ const current = config();
338
+ const firstEditor = renderEditorSettingsPreview(current, theme(0), 72);
339
+ const secondEditor = renderEditorSettingsPreview(current, theme(37), 72);
340
+ const firstMessage = renderUserMessageSettingsPreview(current, theme(0), 72);
341
+ const secondMessage = renderUserMessageSettingsPreview(current, theme(37), 72);
342
+ expect(secondEditor).not.toEqual(firstEditor);
343
+ expect(secondMessage).not.toEqual(firstMessage);
344
+ expect(plain(secondEditor)).toBe(plain(firstEditor));
345
+ expect(plain(secondMessage)).toBe(plain(firstMessage));
346
+ });
347
+ });
@@ -0,0 +1,84 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { describe, expect, it } from "vitest";
3
+ import { emptyGitStatus } from "../extensions/zentui/git";
4
+ import { createInitialState, modelLabelFor, syncState } from "../extensions/zentui/state";
5
+
6
+ function makeCtx(model: unknown) {
7
+ return {
8
+ model,
9
+ sessionManager: { getBranch: () => [] },
10
+ getContextUsage: () => undefined,
11
+ } as unknown as ExtensionContext;
12
+ }
13
+
14
+ const model = { id: "gpt-5.6-terra", name: "GPT-5.6 Terra", provider: "openai" };
15
+
16
+ describe("syncState model label", () => {
17
+ it("retains raw model fields and formats independent label sources", () => {
18
+ const state = createInitialState(emptyGitStatus());
19
+ syncState(state, makeCtx(model), "");
20
+ expect(state.modelId).toBe("gpt-5.6-terra");
21
+ expect(state.modelName).toBe("GPT-5.6 Terra");
22
+ expect(state.modelLabel).toBe("gpt-5.6-terra");
23
+ expect(modelLabelFor(state, "id")).toBe("gpt-5.6-terra");
24
+ expect(modelLabelFor(state, "name")).toBe("GPT-5.6 Terra");
25
+ });
26
+
27
+ it("falls back to the id when the name is empty", () => {
28
+ const state = createInitialState(emptyGitStatus());
29
+ syncState(state, makeCtx({ ...model, name: "" }), "");
30
+ expect(modelLabelFor(state, "name")).toBe("gpt-5.6-terra");
31
+ expect(state.modelName).toBe("");
32
+ });
33
+
34
+ it("shows no-model when there is no active model and clears raw fields", () => {
35
+ const state = createInitialState(emptyGitStatus());
36
+ syncState(state, makeCtx(undefined), "");
37
+ expect(modelLabelFor(state, "name")).toBe("no-model");
38
+ expect(modelLabelFor(state, "id")).toBe("no-model");
39
+ expect(state.modelId).toBe("");
40
+ expect(state.modelName).toBe("");
41
+ expect(state.modelLabel).toBe("no-model");
42
+ });
43
+
44
+ it("stores cache atoms and clears optional markers on later synchronization", () => {
45
+ const entry = {
46
+ type: "message",
47
+ message: {
48
+ role: "assistant",
49
+ usage: {
50
+ input: 100,
51
+ output: 20,
52
+ cacheRead: 1_200,
53
+ cacheWrite: 300,
54
+ cost: { total: 1 },
55
+ },
56
+ },
57
+ };
58
+ const ctx = {
59
+ ...makeCtx(model),
60
+ sessionManager: { getBranch: () => [entry], getEntries: () => [entry] },
61
+ };
62
+ const state = createInitialState(emptyGitStatus());
63
+
64
+ syncState(state, ctx as never, "", {
65
+ subscription: true,
66
+ autoCompaction: true,
67
+ });
68
+ expect(state).toMatchObject({
69
+ cacheReadLabel: "R1.2k",
70
+ cacheWriteLabel: "W300",
71
+ subscription: true,
72
+ autoCompaction: true,
73
+ });
74
+ expect(state.tokenLabel).not.toContain("R1.2k");
75
+
76
+ syncState(state, makeCtx(model), "", {});
77
+ expect(state).toMatchObject({
78
+ cacheReadLabel: "",
79
+ cacheWriteLabel: "",
80
+ subscription: false,
81
+ autoCompaction: false,
82
+ });
83
+ });
84
+ });