@selesai/code 0.9.15 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +6 -9
  3. package/dist/defaults/models.json +2 -2
  4. package/dist/defaults/settings.json +3 -7
  5. package/dist/extensions/package.json +2 -4
  6. package/dist/extensions/pi-zentui/.editorconfig +12 -0
  7. package/dist/extensions/pi-zentui/.github/CODEOWNERS +1 -0
  8. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/bug_report.yml +110 -0
  9. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config.yml +8 -0
  10. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config_question.yml +56 -0
  11. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/feature_request.yml +63 -0
  12. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/rendering_issue.yml +106 -0
  13. package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/runtime_detection.yml +103 -0
  14. package/dist/extensions/pi-zentui/.github/dependabot.yml +47 -0
  15. package/dist/extensions/pi-zentui/.github/pull_request_template.md +34 -0
  16. package/dist/extensions/pi-zentui/.github/release.yml +18 -0
  17. package/dist/extensions/pi-zentui/.github/workflows/ci.yml +79 -0
  18. package/dist/extensions/pi-zentui/.github/workflows/publish.yml +63 -0
  19. package/dist/extensions/pi-zentui/AGENTS.md +107 -0
  20. package/dist/extensions/pi-zentui/CONTRIBUTING.md +68 -0
  21. package/dist/extensions/pi-zentui/LICENSE +21 -0
  22. package/dist/extensions/pi-zentui/README.md +188 -0
  23. package/dist/extensions/pi-zentui/SECURITY.md +18 -0
  24. package/dist/extensions/pi-zentui/assets/main-cover.png +0 -0
  25. package/dist/extensions/pi-zentui/assets/screenshots/editor-accent-rail.png +0 -0
  26. package/dist/extensions/pi-zentui/assets/screenshots/editor-minimalist.png +0 -0
  27. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode-copy-friendly.png +0 -0
  28. package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode.png +0 -0
  29. package/dist/extensions/pi-zentui/assets/screenshots/user-message-compact.png +0 -0
  30. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed-copy-friendly.png +0 -0
  31. package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed.png +0 -0
  32. package/dist/extensions/pi-zentui/assets/screenshots/user-message-labeled.png +0 -0
  33. package/dist/extensions/pi-zentui/assets/zentui.png +0 -0
  34. package/dist/extensions/pi-zentui/biome.json +31 -0
  35. package/dist/extensions/pi-zentui/docs/configuration.md +509 -0
  36. package/dist/extensions/pi-zentui/docs/footer-format.md +162 -0
  37. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-editor.ts +99 -0
  38. package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-layout-patch.ts +457 -0
  39. package/dist/extensions/pi-zentui/extensions/zentui/completion-menu.ts +157 -0
  40. package/dist/extensions/pi-zentui/extensions/zentui/config.ts +2100 -0
  41. package/dist/extensions/pi-zentui/extensions/zentui/editor-metadata-format.ts +321 -0
  42. package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +73 -0
  43. package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +89 -0
  44. package/dist/extensions/pi-zentui/extensions/zentui/footer-format.ts +364 -0
  45. package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +122 -0
  46. package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +812 -0
  47. package/dist/extensions/pi-zentui/extensions/zentui/format.ts +568 -0
  48. package/dist/extensions/pi-zentui/extensions/zentui/git.ts +384 -0
  49. package/dist/extensions/pi-zentui/extensions/zentui/icons.ts +252 -0
  50. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +1406 -0
  51. package/dist/extensions/pi-zentui/extensions/zentui/interaction-summary.ts +1049 -0
  52. package/dist/extensions/pi-zentui/extensions/zentui/live-context.ts +75 -0
  53. package/dist/extensions/pi-zentui/extensions/zentui/minimalist-editor.ts +463 -0
  54. package/dist/extensions/pi-zentui/extensions/zentui/package-version.ts +651 -0
  55. package/dist/extensions/pi-zentui/extensions/zentui/project-refresh.ts +120 -0
  56. package/dist/extensions/pi-zentui/extensions/zentui/project-state.ts +59 -0
  57. package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +166 -0
  58. package/dist/extensions/pi-zentui/extensions/zentui/repository-root.ts +98 -0
  59. package/dist/extensions/pi-zentui/extensions/zentui/runtime.ts +841 -0
  60. package/dist/extensions/pi-zentui/extensions/zentui/selector-border.ts +93 -0
  61. package/dist/extensions/pi-zentui/extensions/zentui/session-lifecycle.ts +60 -0
  62. package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +1796 -0
  63. package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +128 -0
  64. package/dist/extensions/pi-zentui/extensions/zentui/state.ts +94 -0
  65. package/dist/extensions/pi-zentui/extensions/zentui/style.ts +424 -0
  66. package/dist/extensions/pi-zentui/extensions/zentui/telemetry.ts +82 -0
  67. package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +1290 -0
  68. package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +248 -0
  69. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +215 -0
  70. package/dist/extensions/pi-zentui/extensions/zentui/user-message.ts +206 -0
  71. package/dist/extensions/pi-zentui/extensions/zentui/working-line-messages.ts +24 -0
  72. package/dist/extensions/pi-zentui/extensions/zentui/working-line-spinners.ts +54 -0
  73. package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +1433 -0
  74. package/dist/extensions/pi-zentui/package-lock.json +5311 -0
  75. package/dist/extensions/pi-zentui/package.json +60 -0
  76. package/dist/extensions/pi-zentui/test/accent-rail-editor.test.ts +232 -0
  77. package/dist/extensions/pi-zentui/test/accent-rail-layout-patch.test.ts +669 -0
  78. package/dist/extensions/pi-zentui/test/completion-menu.test.ts +265 -0
  79. package/dist/extensions/pi-zentui/test/config.test.ts +3119 -0
  80. package/dist/extensions/pi-zentui/test/editor-metadata-format.test.ts +405 -0
  81. package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +260 -0
  82. package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +1286 -0
  83. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +7196 -0
  84. package/dist/extensions/pi-zentui/test/extension-status.test.ts +182 -0
  85. package/dist/extensions/pi-zentui/test/fixtures/starship-nerd-font-symbols.toml +172 -0
  86. package/dist/extensions/pi-zentui/test/footer-format.test.ts +431 -0
  87. package/dist/extensions/pi-zentui/test/footer-layout.test.ts +113 -0
  88. package/dist/extensions/pi-zentui/test/footer-repository-path.test.ts +123 -0
  89. package/dist/extensions/pi-zentui/test/format.test.ts +865 -0
  90. package/dist/extensions/pi-zentui/test/git.test.ts +212 -0
  91. package/dist/extensions/pi-zentui/test/icons.test.ts +69 -0
  92. package/dist/extensions/pi-zentui/test/interaction-summary.test.ts +1402 -0
  93. package/dist/extensions/pi-zentui/test/live-context-integration.test.ts +378 -0
  94. package/dist/extensions/pi-zentui/test/live-context.test.ts +139 -0
  95. package/dist/extensions/pi-zentui/test/minimalist-editor.test.ts +811 -0
  96. package/dist/extensions/pi-zentui/test/package-version.test.ts +321 -0
  97. package/dist/extensions/pi-zentui/test/project-refresh.test.ts +250 -0
  98. package/dist/extensions/pi-zentui/test/project-state.test.ts +159 -0
  99. package/dist/extensions/pi-zentui/test/repository-root.test.ts +108 -0
  100. package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +825 -0
  101. package/dist/extensions/pi-zentui/test/runtime.test.ts +183 -0
  102. package/dist/extensions/pi-zentui/test/session-lifecycle.test.ts +40 -0
  103. package/dist/extensions/pi-zentui/test/settings-command.test.ts +1381 -0
  104. package/dist/extensions/pi-zentui/test/settings-previews.test.ts +347 -0
  105. package/dist/extensions/pi-zentui/test/state.test.ts +84 -0
  106. package/dist/extensions/pi-zentui/test/telemetry-lifecycle-integration.test.ts +276 -0
  107. package/dist/extensions/pi-zentui/test/telemetry.test.ts +105 -0
  108. package/dist/extensions/pi-zentui/test/turn-summary-lifecycle.integration.test.ts +327 -0
  109. package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +164 -0
  110. package/dist/extensions/pi-zentui/test/user-message-styles.test.ts +292 -0
  111. package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +730 -0
  112. package/dist/extensions/pi-zentui/test/working-line-messages.test.ts +34 -0
  113. package/dist/extensions/pi-zentui/test/working-line.test.ts +1990 -0
  114. package/dist/extensions/pi-zentui/tsconfig.json +16 -0
  115. package/dist/extensions/tps.test.ts +38 -0
  116. package/dist/extensions/{pi-powerline-footer/tps.ts → tps.ts} +4 -0
  117. package/dist/skills/design-references/SKILL.md +31 -0
  118. package/dist/skills/design-references/references/clickhouse/DESIGN.md +544 -0
  119. package/dist/themes/adventure.json +84 -0
  120. package/dist/themes/adwaita-dark.json +84 -0
  121. package/dist/themes/arcoiris.json +84 -0
  122. package/dist/themes/arthur.json +84 -0
  123. package/dist/themes/atom.json +84 -0
  124. package/dist/themes/aura.json +84 -0
  125. package/dist/themes/black-metal-bathory.json +84 -0
  126. package/dist/themes/black-metal-burzum.json +84 -0
  127. package/dist/themes/black-metal-khold.json +84 -0
  128. package/dist/themes/box.json +84 -0
  129. package/dist/themes/brogrammer.json +84 -0
  130. package/dist/themes/carbonfox.json +84 -0
  131. package/dist/themes/catppuccin-mocha.json +84 -0
  132. package/dist/themes/citruszest.json +84 -0
  133. package/dist/themes/cursor-dark.json +84 -0
  134. package/dist/themes/cutie-pro.json +84 -0
  135. package/dist/themes/dark-modern.json +84 -0
  136. package/dist/themes/dark-pastel.json +84 -0
  137. package/dist/themes/dimmed-monokai.json +84 -0
  138. package/dist/themes/doom-peacock.json +84 -0
  139. package/dist/themes/dracula-plus.json +84 -0
  140. package/dist/themes/dracula.json +80 -0
  141. package/dist/themes/earthsong.json +84 -0
  142. package/dist/themes/everforest-dark-hard.json +84 -0
  143. package/dist/themes/everforest-dark.json +80 -0
  144. package/dist/themes/fahrenheit.json +84 -0
  145. package/dist/themes/flatland.json +84 -0
  146. package/dist/themes/flexoki-dark.json +84 -0
  147. package/dist/themes/front-end-delight.json +84 -0
  148. package/dist/themes/fun-forrest.json +84 -0
  149. package/dist/themes/galizur.json +84 -0
  150. package/dist/themes/github-dark-colorblind.json +84 -0
  151. package/dist/themes/github-dark-high-contrast.json +84 -0
  152. package/dist/themes/glacier.json +84 -0
  153. package/dist/themes/gruber-darker.json +84 -0
  154. package/dist/themes/gruvbox-dark-hard.json +84 -0
  155. package/dist/themes/gruvbox-dark.json +84 -0
  156. package/dist/themes/gruvbox-material.json +84 -0
  157. package/dist/themes/guezwhoz.json +84 -0
  158. package/dist/themes/hacktober.json +84 -0
  159. package/dist/themes/hardcore.json +84 -0
  160. package/dist/themes/havn-skumring.json +84 -0
  161. package/dist/themes/ic-orange-ppl.json +84 -0
  162. package/dist/themes/iterm2-smoooooth.json +84 -0
  163. package/dist/themes/iterm2-tango-dark.json +84 -0
  164. package/dist/themes/japanesque.json +84 -0
  165. package/dist/themes/jellybeans.json +84 -0
  166. package/dist/themes/kanagawa-wave.json +84 -0
  167. package/dist/themes/kurokula.json +84 -0
  168. package/dist/themes/later-this-evening.json +84 -0
  169. package/dist/themes/lovelace.json +84 -0
  170. package/dist/themes/material-darker.json +84 -0
  171. package/dist/themes/matte-black.json +84 -0
  172. package/dist/themes/mellow.json +84 -0
  173. package/dist/themes/miasma.json +84 -0
  174. package/dist/themes/nord.json +80 -0
  175. package/dist/themes/nvim-dark.json +84 -0
  176. package/dist/themes/one-dark.json +80 -0
  177. package/dist/themes/popping-and-locking.json +84 -0
  178. package/dist/themes/rose-pine.json +80 -0
  179. package/dist/themes/sea-shells.json +84 -0
  180. package/dist/themes/sleepy-hollow.json +84 -0
  181. package/dist/themes/smyck.json +84 -0
  182. package/dist/themes/solarized-dark.json +80 -0
  183. package/dist/themes/tokyo-night.json +80 -0
  184. package/dist/themes/tomorrow-night-bright.json +84 -0
  185. package/dist/themes/tomorrow-night-burns.json +84 -0
  186. package/dist/themes/tomorrow-night.json +84 -0
  187. package/dist/themes/twilight.json +84 -0
  188. package/dist/themes/vague.json +84 -0
  189. package/dist/themes/vesper.json +84 -0
  190. package/dist/themes/xcode-dark-hc.json +84 -0
  191. package/docs/custom-provider.md +0 -1
  192. package/package.json +3 -3
  193. package/dist/extensions/auto-model/auto-model.test.ts +0 -18
  194. package/dist/extensions/auto-model/classifier.ts +0 -23
  195. package/dist/extensions/auto-model/config.ts +0 -51
  196. package/dist/extensions/auto-model/index.ts +0 -22
  197. package/dist/extensions/auto-model/lifecycle.test.ts +0 -24
  198. package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
  199. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
  200. package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
  201. package/dist/extensions/pi-powerline-footer/README.md +0 -513
  202. package/dist/extensions/pi-powerline-footer/banner.png +0 -0
  203. package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
  204. package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
  205. package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
  206. package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
  207. package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
  208. package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
  209. package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
  210. package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
  211. package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
  212. package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
  213. package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
  214. package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
  215. package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
  216. package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
  217. package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
  218. package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
  219. package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
  220. package/dist/extensions/pi-powerline-footer/package.json +0 -49
  221. package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
  222. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
  223. package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
  224. package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
  225. package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
  226. package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
  227. package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
  228. package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
  229. package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
  230. package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
  231. package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
  232. package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
  233. package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
  234. package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
  235. package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
  236. package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
  237. package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
  238. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
  239. package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
  240. package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
  241. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
  242. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
  243. package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
  244. package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
  245. package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
  246. package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
  247. package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
  248. package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
  249. package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
  250. package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
  251. package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
  252. package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
  253. package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
  254. package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
  255. package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
  256. package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
  257. package/dist/extensions/pi-powerline-footer/theme.json +0 -12
  258. package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
  259. package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
  260. package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
  261. package/dist/extensions/pi-powerline-footer/types.ts +0 -235
  262. package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
  263. package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
  264. package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
  265. package/dist/extensions/preview-tools-disabled.test.ts +0 -37
  266. package/dist/extensions/preview-tools-disabled.ts +0 -11
  267. package/dist/themes/powerline-footer/theme.json +0 -33
@@ -0,0 +1,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
+ });