@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,164 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ isValidUserMessageOsc8Payload,
4
+ sanitizeRenderedUserMessageLines,
5
+ sanitizeRenderedUserMessageText,
6
+ sanitizeUserMessageSourceText,
7
+ } from "../extensions/zentui/user-message-osc";
8
+
9
+ const safeTerminators = ["\x07", "\x1b\\"];
10
+
11
+ describe("sanitizeUserMessageSourceText", () => {
12
+ it.each(safeTerminators)("strips structurally valid 7-bit OSC 8 controls (%j)", (end) => {
13
+ const open = `\x1b]8;id=docs;https://example.test/a${end}`;
14
+ const close = `\x1b]8;;${end}`;
15
+ expect(sanitizeUserMessageSourceText(`before ${open}link${close} after`)).toBe(
16
+ "before link after",
17
+ );
18
+ });
19
+
20
+ it("strips a balanced link spanning source rows while preserving text", () => {
21
+ const open = "\x1b]8;;https://example.test\x07";
22
+ const close = "\x1b]8;;\x07";
23
+ expect(sanitizeUserMessageSourceText(`${open}first\nsecond${close}`)).toBe("first\nsecond");
24
+ });
25
+
26
+ it.each([
27
+ ["unmatched open", "\x1b]8;;https://example.test\x07before link after", "before link after"],
28
+ ["stray close", "before\x1b]8;;\x07 after", "before after"],
29
+ [
30
+ "nested open",
31
+ "\x1b]8;;https://first.test\x07first \x1b]8;;https://second.test\x07second\x1b]8;;\x07",
32
+ "first second",
33
+ ],
34
+ ] as const)(
35
+ "strips every OSC 8 control for an unbalanced %s stream",
36
+ (_name, source, visible) => {
37
+ const output = sanitizeUserMessageSourceText(source);
38
+ expect(output).toBe(visible);
39
+ expect(output).not.toContain("\x1b]8;");
40
+ },
41
+ );
42
+
43
+ it.each([
44
+ ["C1 introducer and terminator", "\x9d", "\x9c"],
45
+ ["C1 introducer with BEL", "\x9d", "\x07"],
46
+ ["7-bit introducer with C1 terminator", "\x1b]", "\x9c"],
47
+ ] as const)("strips unsupported %s OSC 8 controls", (_name, start, end) => {
48
+ const open = `${start}8;;https://example.test${end}`;
49
+ const close = `${start}8;;${end}`;
50
+ expect(sanitizeUserMessageSourceText(`before ${open}link${close} after`)).toBe(
51
+ "before link after",
52
+ );
53
+ });
54
+
55
+ it.each(["0;title", "2;title", "52;c;c2VjcmV0", "133;A", "9;text", "hello"])(
56
+ "removes complete non-OSC-8 command %j and its payload",
57
+ (payload) => {
58
+ expect(sanitizeUserMessageSourceText(`before\x1b]${payload}\x07after`)).toBe("beforeafter");
59
+ expect(sanitizeUserMessageSourceText(`before\x9d${payload}\x9cafter`)).toBe("beforeafter");
60
+ },
61
+ );
62
+
63
+ it.each([
64
+ "8",
65
+ "8;params",
66
+ "88;;https://example.test",
67
+ "8;badparam;https://example.test",
68
+ "8;bad key=value;https://example.test",
69
+ "8;;https://example.test/has space",
70
+ "8;id=x;",
71
+ ])("removes malformed OSC 8 payload %j", (payload) => {
72
+ expect(sanitizeUserMessageSourceText(`left\x1b]${payload}\x07right`)).toBe("leftright");
73
+ });
74
+
75
+ it("neutralizes unterminated and nested OSC without consuming later visible text", () => {
76
+ expect(sanitizeUserMessageSourceText("left\x1b]133;A;visible")).toBe("leftvisible");
77
+ expect(
78
+ sanitizeUserMessageSourceText("left\x1b]2;title\x1b]8;;https://safe.test\x07right"),
79
+ ).toBe("left2;titleright");
80
+ });
81
+
82
+ it.each([
83
+ ["7-bit CSI", "a\x1b[2Jb", "ab"],
84
+ ["C1 CSI", "a\x9b2Jb", "ab"],
85
+ ["7-bit DCS", "a\x1bPpayload\x1b\\b", "ab"],
86
+ ["C1 DCS", "a\x90payload\x9cb", "ab"],
87
+ ["7-bit SOS", "a\x1bXpayload\x1b\\b", "ab"],
88
+ ["C1 SOS", "a\x98payload\x9cb", "ab"],
89
+ ["7-bit PM", "a\x1b^payload\x1b\\b", "ab"],
90
+ ["C1 PM", "a\x9epayload\x9cb", "ab"],
91
+ ["7-bit APC", "a\x1b_payload\x1b\\b", "ab"],
92
+ ["C1 APC", "a\x9fpayload\x9cb", "ab"],
93
+ ["BEL", "a\x07b", "ab"],
94
+ ["C0", "a\x01b", "ab"],
95
+ ["DEL", "a\x7fb", "ab"],
96
+ ["standalone C1", "a\x85b", "ab"],
97
+ ["generic escape", "a\x1b(0b", "ab"],
98
+ ] as const)("removes %s terminal controls", (_name, source, expected) => {
99
+ expect(sanitizeUserMessageSourceText(source)).toBe(expected);
100
+ });
101
+
102
+ it("preserves Markdown-safe structural whitespace", () => {
103
+ expect(sanitizeUserMessageSourceText("a\tb\nc")).toBe("a\tb\nc");
104
+ });
105
+
106
+ it("strips many raw links in one linear source pass", () => {
107
+ const open = "\x1b]8;;https://example.test\x07";
108
+ const close = "\x1b]8;;\x07";
109
+ const source = Array.from({ length: 20_000 }, (_, index) => `${open}${index}${close}`).join(
110
+ " ",
111
+ );
112
+ const expected = Array.from({ length: 20_000 }, (_, index) => String(index)).join(" ");
113
+ expect(sanitizeUserMessageSourceText(source)).toBe(expected);
114
+ });
115
+ });
116
+
117
+ describe("sanitizeRenderedUserMessageText", () => {
118
+ it("preserves only SGR and structurally valid 7-bit OSC 8", () => {
119
+ const sgr = "\x1b[38;5;202mstyled\x1b[0m";
120
+ const open = "\x1b]8;;https://safe.example\x1b\\";
121
+ const close = "\x1b]8;;\x1b\\";
122
+ const source = `before ${sgr} ${open}link${close} \x1b]52;c;CLIP\x07\x1b[2J\x1bPSECRET\x1b\\\x07 after`;
123
+ const sanitized = sanitizeRenderedUserMessageText(source);
124
+ expect(sanitized).toContain(sgr);
125
+ expect(sanitized).toContain(`${open}link${close}`);
126
+ expect(sanitized).toContain("before");
127
+ expect(sanitized).toContain("after");
128
+ expect(sanitized).not.toContain("CLIP");
129
+ expect(sanitized).not.toContain("SECRET");
130
+ expect(sanitized).not.toContain("\x1b[2J");
131
+ expect(sanitized).not.toContain("\x07 after");
132
+ });
133
+
134
+ it("balances predecessor hyperlinks across rendered rows", () => {
135
+ const open = "\x1b]8;;https://safe.example\x07";
136
+ const close = "\x1b]8;;\x07";
137
+ const sgr = "\x1b[31mred\x1b[0m";
138
+ expect(sanitizeRenderedUserMessageLines([`${open}first`, `second${close} ${sgr}`])).toEqual([
139
+ `${open}first`,
140
+ `second${close} ${sgr}`,
141
+ ]);
142
+ expect(sanitizeRenderedUserMessageLines([`${open}first`, `second ${sgr}`])).toEqual([
143
+ "first",
144
+ `second ${sgr}`,
145
+ ]);
146
+ expect(sanitizeRenderedUserMessageLines(["first", `second\x1b]8;;\x07 ${sgr}`])).toEqual([
147
+ "first",
148
+ `second ${sgr}`,
149
+ ]);
150
+ });
151
+ });
152
+
153
+ describe("isValidUserMessageOsc8Payload", () => {
154
+ it("accepts parameter entries and custom URI schemes", () => {
155
+ expect(isValidUserMessageOsc8Payload("8;id=x:foo=bar;custom:target")).toBe(true);
156
+ });
157
+
158
+ it.each(["8;id=x;bad\nuri", "8;i\x1bd=x;https://x", "8;id=x;https://x\x9c"])(
159
+ "rejects controls in params and URIs",
160
+ (payload) => {
161
+ expect(isValidUserMessageOsc8Payload(payload)).toBe(false);
162
+ },
163
+ );
164
+ });
@@ -0,0 +1,292 @@
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 PolishedTuiConfig,
8
+ type UserMessageStyle,
9
+ } from "../extensions/zentui/config";
10
+ import {
11
+ sanitizeRenderedUserMessageText,
12
+ sanitizeUserMessageSourceText,
13
+ } from "../extensions/zentui/user-message-osc";
14
+ import {
15
+ renderUserMessageStyle,
16
+ userMessageStyleCacheKey,
17
+ } from "../extensions/zentui/user-message-styles";
18
+
19
+ const userMessageStyles = ["framed", "framed-copy-friendly", "compact", "labeled"] as const;
20
+
21
+ function config(style: UserMessageStyle): PolishedTuiConfig {
22
+ const value = structuredClone(defaultConfig);
23
+ value.components.userMessages.style = style;
24
+ return value;
25
+ }
26
+
27
+ function render(style: UserMessageStyle, text: string, width: number, theme?: Theme): string[] {
28
+ return renderUserMessageStyle({ text, width, theme, config: config(style) });
29
+ }
30
+
31
+ function plain(value: string): string {
32
+ return value.replaceAll(/\x1b\[[0-?]*[ -/]*[@-~]/g, "");
33
+ }
34
+
35
+ function ansiTheme(): Theme {
36
+ return {
37
+ fg: (_color: string, text: string) => `\x1b[34m${text}\x1b[0m`,
38
+ bold: (text: string) => `\x1b[1m${text}\x1b[0m`,
39
+ italic: (text: string) => text,
40
+ underline: (text: string) => text,
41
+ strikethrough: (text: string) => text,
42
+ } as Theme;
43
+ }
44
+
45
+ describe("pure user-message styles", () => {
46
+ it("preserves exact framed chrome and padding", () => {
47
+ expect(render("framed", "Hello", 12)).toEqual([
48
+ "────────────",
49
+ "│ ",
50
+ "│ Hello ",
51
+ "│ ",
52
+ "────────────",
53
+ ]);
54
+ });
55
+
56
+ it("adds one leading space to copy-friendly framed message text", () => {
57
+ expect(render("framed-copy-friendly", "Hello", 12)).toEqual([
58
+ "────────────",
59
+ "",
60
+ " Hello ",
61
+ "",
62
+ "────────────",
63
+ ]);
64
+ });
65
+
66
+ it("keeps copy-friendly ANSI and wide-Unicode body rows padded within width", () => {
67
+ for (let width = 2; width <= 24; width += 1) {
68
+ const lines = render("framed-copy-friendly", "界🙂 \x1b[31mwide\x1b[0m", width, ansiTheme());
69
+ const body = lines.slice(2, -2);
70
+ expect(body.length).toBeGreaterThan(0);
71
+ expect(body.every((line) => visibleWidth(line) === width)).toBe(true);
72
+ }
73
+ });
74
+
75
+ it("renders compact multiline and blank Markdown rows with an unpadded rail", () => {
76
+ const lines = render("compact", "First\n\nFinal", 16);
77
+ expect(lines).toEqual(["│ First", "│ ", "│ Final"]);
78
+ expect(lines.every((line) => !line.endsWith(" "))).toBe(true);
79
+ });
80
+
81
+ it("renders a fixed User label and no extra body padding", () => {
82
+ expect(render("labeled", "Hello\nContinued", 20)).toEqual([
83
+ "╭─ User ───────────╮",
84
+ "│ Hello │",
85
+ "│ Continued │",
86
+ "╰──────────────────╯",
87
+ ]);
88
+ });
89
+
90
+ it("preserves Markdown rendering across all styles", () => {
91
+ for (const style of userMessageStyles) {
92
+ const output = plain(
93
+ render(style, "**bold**\n\n- item\n\n> quote\n\n```ts\ncode\n```", 40).join("\n"),
94
+ );
95
+ expect(output).toContain("bold");
96
+ expect(output).toContain("item");
97
+ expect(output).toContain("quote");
98
+ expect(output).toContain("code");
99
+ expect(output).not.toContain("**bold**");
100
+ }
101
+ });
102
+
103
+ it("degrades atomically at compact and labeled narrow thresholds", () => {
104
+ expect(render("compact", "界a", 2)).toEqual(["界", "a"]);
105
+ expect(render("compact", "abc", 3)[0]).toBe("│ a");
106
+ for (const width of [3, 4, 5, 6, 7, 8]) {
107
+ const lines = render("labeled", "abc", width);
108
+ const body = lines.slice(1, -1);
109
+ expect(lines[0]).toMatch(/^╭─*╮$/);
110
+ expect(lines.at(-1)).toMatch(/^╰─*╯$/);
111
+ expect(lines.join("\n")).not.toContain("User");
112
+ expect(lines.every((line) => visibleWidth(line) === width)).toBe(true);
113
+ expect(body.every((line) => line.startsWith("│") && line.endsWith("│"))).toBe(true);
114
+ expect(body.map((line) => line.slice(1, -1).trim()).join("")).toContain("abc");
115
+ }
116
+ for (const width of [1, 2]) {
117
+ const joined = render("labeled", "abc", width).join("\n");
118
+ expect(joined).not.toMatch(/[╭╮╰╯│]/);
119
+ }
120
+ expect(render("labeled", "abc", 9)[0]).toBe("╭─ User ╮");
121
+ });
122
+
123
+ it("returns a marker-safe row at non-positive widths and clamps ANSI/wide Unicode", () => {
124
+ for (const style of userMessageStyles) {
125
+ expect(render(style, "hello", 0)).toEqual([""]);
126
+ for (let width = 1; width <= 24; width += 1) {
127
+ const lines = render(style, "界🙂 \x1b[31mwide\x1b[0m", width, ansiTheme());
128
+ expect(lines.every((line) => visibleWidth(line) <= width)).toBe(true);
129
+ }
130
+ }
131
+ });
132
+
133
+ it("throws rendering failures for the shared adapter to handle", () => {
134
+ const failingTheme = {
135
+ ...ansiTheme(),
136
+ fg() {
137
+ throw new Error("theme failed");
138
+ },
139
+ } as unknown as Theme;
140
+ for (const style of userMessageStyles) {
141
+ expect(() => render(style, "hello", 20, failingTheme)).toThrow("theme failed");
142
+ }
143
+ });
144
+
145
+ it("strips every raw OSC 8 control while preserving visible text", () => {
146
+ const pairs = [
147
+ ["\x1b]8;;https://example.com\x07", "\x1b]8;;\x07"],
148
+ ["\x1b]8;;https://example.com\x1b\\", "\x1b]8;;\x1b\\"],
149
+ ["\x9d8;;https://example.com\x9c", "\x9d8;;\x9c"],
150
+ ] as const;
151
+ for (const style of userMessageStyles) {
152
+ for (const [open, close] of pairs) {
153
+ const output = render(style, `before ${open}VISIBLE${close}\nafter`, 80).join("\n");
154
+ expect(output).not.toContain(open);
155
+ expect(output).not.toContain(close);
156
+ expect(plain(output).replaceAll(/[^\p{L}]/gu, "")).toContain("beforeVISIBLEafter");
157
+ }
158
+ }
159
+ });
160
+
161
+ it("strips raw OSC 8 independently of Markdown source context", () => {
162
+ const open = "\x1b]8;;https://raw.example\x07";
163
+ const close = "\x1b]8;;\x07";
164
+ const contexts = [
165
+ `${open}VISIBLE${close}`,
166
+ `[docs](https://markdown.example/${open}VISIBLE${close})`,
167
+ `![image](https://markdown.example/${open}VISIBLE${close})`,
168
+ `\` ${open}VISIBLE${close} \``,
169
+ `\`\`foo \`\`\` bar \` ${open}VISIBLE${close} \`\``,
170
+ `~~~\n${open}VISIBLE${close}\n~~~`,
171
+ `[foo\nbar]: ${open}VISIBLE${close}\n\n[foo bar]`,
172
+ `<https://markdown.example/${open}VISIBLE${close}>`,
173
+ `See,https://markdown.example/a_(b)/${open}VISIBLE${close}!`,
174
+ `[broken](https://markdown.example/${open}VISIBLE${close}`,
175
+ `(https://markdown.example/${open}VISIBLE${close})`,
176
+ ];
177
+ for (const style of userMessageStyles) {
178
+ for (const source of contexts) {
179
+ const output = render(style, `${source} TAIL`, 160).join("\n");
180
+ expect(output).not.toContain(open);
181
+ expect(output).not.toContain(close);
182
+ expect(output).toContain("TAIL");
183
+ }
184
+ }
185
+ });
186
+
187
+ it("preserves Pi Markdown link rendering after raw-source sanitization", () => {
188
+ const markdown = "[docs](https://markdown.example) <https://autolink.example>";
189
+ const hostile = "\x1b]52;c;c2VjcmV0\x07";
190
+ expect(sanitizeUserMessageSourceText(markdown)).toBe(markdown);
191
+ for (const style of userMessageStyles) {
192
+ const clean = render(style, markdown, 160).join("\n");
193
+ const mixed = render(style, `${hostile}${markdown}`, 160).join("\n");
194
+ expect(mixed).toBe(clean);
195
+ expect(stripVTControlCharacters(mixed)).toContain("docs");
196
+ expect(mixed).toContain("https://markdown.example");
197
+ expect(mixed).toContain("https://autolink.example");
198
+ if (clean.includes("\x1b]8;")) expect(sanitizeRenderedUserMessageText(clean)).toBe(clean);
199
+ expect(mixed).not.toContain("\x1b]52");
200
+ expect(mixed).not.toContain("c2VjcmV0");
201
+ }
202
+ });
203
+
204
+ it.each([
205
+ ["unmatched open", "\x1b]8;;https://example.com\x07raw"],
206
+ ["stray close", "raw\x1b]8;;\x07"],
207
+ ["C1 unmatched open", "\x9d8;;https://example.com\x9craw"],
208
+ [
209
+ "nested open",
210
+ "\x1b]8;;https://first.example\x07one \x1b]8;;https://second.example\x07two\x1b]8;;\x07",
211
+ ],
212
+ ] as const)("strips raw %s OSC 8 streams", (_name, source) => {
213
+ for (const style of userMessageStyles) {
214
+ const output = render(style, source, 80).join("\n");
215
+ expect(output).not.toContain("\x1b]8;");
216
+ expect(output).not.toContain("\x9d");
217
+ expect(output).toMatch(/raw|one/);
218
+ }
219
+ });
220
+
221
+ it("removes raw non-OSC terminal controls before Markdown", () => {
222
+ const hostile = [
223
+ "\x1b[2J",
224
+ "\x9b2J",
225
+ "\x1bPsecret\x1b\\",
226
+ "\x90secret\x9c",
227
+ "\x1bXsecret\x1b\\",
228
+ "\x1b^secret\x1b\\",
229
+ "\x1b_secret\x1b\\",
230
+ "\x07",
231
+ "\x01",
232
+ "\x7f",
233
+ ].join("");
234
+ for (const style of userMessageStyles) {
235
+ const output = render(style, `before${hostile}after`, 80).join("\n");
236
+ expect(output).toContain("beforeafter");
237
+ expect(output).not.toMatch(/[\x00-\x08\x0b-\x1f\x7f-\x9f]/);
238
+ expect(output).not.toContain("secret");
239
+ }
240
+ });
241
+
242
+ it("supports theme and terminal color sources", () => {
243
+ const themed = config("labeled");
244
+ const themeLines = renderUserMessageStyle({
245
+ text: "hello",
246
+ width: 20,
247
+ theme: ansiTheme(),
248
+ config: themed,
249
+ });
250
+ expect(themeLines.join("\n")).toContain("\x1b[");
251
+
252
+ const terminal = config("compact");
253
+ terminal.components.userMessages.colorSource = "terminal";
254
+ terminal.colors.editorAccent = "fg:202";
255
+ const terminalLines = renderUserMessageStyle({
256
+ text: "hello",
257
+ width: 20,
258
+ theme: ansiTheme(),
259
+ config: terminal,
260
+ });
261
+ expect(terminalLines.join("\n")).toContain("\x1b[");
262
+ });
263
+
264
+ it("uses only style-relevant cache-key inputs", () => {
265
+ for (const style of userMessageStyles) {
266
+ const base = config(style);
267
+ const key = userMessageStyleCacheKey(base);
268
+ const unrelated = structuredClone(base);
269
+ unrelated.components.footer.style =
270
+ unrelated.components.footer.style === "hidden" ? "native" : "hidden";
271
+ expect(userMessageStyleCacheKey(unrelated)).toBe(key);
272
+
273
+ const accent = structuredClone(base);
274
+ accent.colors.editorAccent = "red";
275
+ expect(userMessageStyleCacheKey(accent) === key).toBe(style === "framed-copy-friendly");
276
+
277
+ const border = structuredClone(base);
278
+ border.colors.editorBorder = "blue";
279
+ expect(userMessageStyleCacheKey(border) === key).toBe(style === "compact");
280
+
281
+ const rail = structuredClone(base);
282
+ rail.icons.rail = "┃";
283
+ expect(userMessageStyleCacheKey(rail) === key).toBe(
284
+ style === "labeled" || style === "framed-copy-friendly",
285
+ );
286
+ }
287
+
288
+ expect(userMessageStyleCacheKey(config("framed-copy-friendly"))).not.toBe(
289
+ userMessageStyleCacheKey(config("framed")),
290
+ );
291
+ });
292
+ });