@selesai/code 0.9.15 → 0.10.1

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