@selesai/code 0.10.1 → 0.11.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 (128) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/config.d.ts +5 -3
  3. package/dist/config.js +6 -4
  4. package/dist/core/skills.d.ts +2 -0
  5. package/dist/core/skills.js +1 -0
  6. package/dist/defaults/settings.json +0 -3
  7. package/dist/extensions/auto-session-name.test.ts +157 -0
  8. package/dist/extensions/auto-session-name.ts +96 -0
  9. package/dist/extensions/cost-reconcile.test.ts +76 -0
  10. package/dist/extensions/cost-reconcile.ts +278 -0
  11. package/dist/extensions/enable-readonly-tools.test.ts +11 -0
  12. package/dist/extensions/enable-readonly-tools.ts +7 -3
  13. package/dist/extensions/package.json +3 -0
  14. package/dist/extensions/pi-tool-display/CHANGELOG.md +329 -0
  15. package/dist/extensions/pi-tool-display/LICENSE +21 -0
  16. package/dist/extensions/pi-tool-display/README.md +417 -0
  17. package/dist/extensions/pi-tool-display/config/config.example.json +40 -0
  18. package/dist/extensions/pi-tool-display/index.ts +3 -0
  19. package/dist/extensions/pi-tool-display/package-lock.json +2555 -0
  20. package/dist/extensions/pi-tool-display/package.json +86 -0
  21. package/dist/extensions/pi-tool-display/src/agent-dir.ts +35 -0
  22. package/dist/extensions/pi-tool-display/src/ansi-utils.ts +116 -0
  23. package/dist/extensions/pi-tool-display/src/bash-display.ts +213 -0
  24. package/dist/extensions/pi-tool-display/src/capabilities.ts +88 -0
  25. package/dist/extensions/pi-tool-display/src/config-modal.ts +541 -0
  26. package/dist/extensions/pi-tool-display/src/config-store.ts +304 -0
  27. package/dist/extensions/pi-tool-display/src/debug-logger.ts +150 -0
  28. package/dist/extensions/pi-tool-display/src/diff-presentation.ts +72 -0
  29. package/dist/extensions/pi-tool-display/src/diff-renderer.ts +2517 -0
  30. package/dist/extensions/pi-tool-display/src/disposable.ts +34 -0
  31. package/dist/extensions/pi-tool-display/src/extension-lifecycle.ts +14 -0
  32. package/dist/extensions/pi-tool-display/src/index.ts +109 -0
  33. package/dist/extensions/pi-tool-display/src/line-width-safety.ts +93 -0
  34. package/dist/extensions/pi-tool-display/src/modal-icons.ts +57 -0
  35. package/dist/extensions/pi-tool-display/src/pending-diff-preview.ts +370 -0
  36. package/dist/extensions/pi-tool-display/src/presets.ts +117 -0
  37. package/dist/extensions/pi-tool-display/src/render-utils.ts +186 -0
  38. package/dist/extensions/pi-tool-display/src/settings-inspector-modal.ts +475 -0
  39. package/dist/extensions/pi-tool-display/src/thinking-label.ts +343 -0
  40. package/dist/extensions/pi-tool-display/src/tool-call-box.ts +181 -0
  41. package/dist/extensions/pi-tool-display/src/tool-metadata.ts +139 -0
  42. package/dist/extensions/pi-tool-display/src/tool-overrides.ts +2168 -0
  43. package/dist/extensions/pi-tool-display/src/types.ts +109 -0
  44. package/dist/extensions/pi-tool-display/src/user-message-box-markdown.ts +64 -0
  45. package/dist/extensions/pi-tool-display/src/user-message-box-native.ts +66 -0
  46. package/dist/extensions/pi-tool-display/src/user-message-box-patch.ts +72 -0
  47. package/dist/extensions/pi-tool-display/src/user-message-box-renderer.ts +419 -0
  48. package/dist/extensions/pi-tool-display/src/user-message-box-utils.ts +106 -0
  49. package/dist/extensions/pi-tool-display/src/write-display-utils.ts +31 -0
  50. package/dist/extensions/pi-tool-display/src/zellij-modal.ts +1027 -0
  51. package/dist/extensions/pi-tool-display/tests/active-backlog-red.test.ts +65 -0
  52. package/dist/extensions/pi-tool-display/tests/ansi-utils.test.ts +345 -0
  53. package/dist/extensions/pi-tool-display/tests/bash-display.test.ts +442 -0
  54. package/dist/extensions/pi-tool-display/tests/capabilities-edge.test.ts +360 -0
  55. package/dist/extensions/pi-tool-display/tests/capabilities.test.ts +54 -0
  56. package/dist/extensions/pi-tool-display/tests/config-modal.test.ts +360 -0
  57. package/dist/extensions/pi-tool-display/tests/config-store.test.ts +87 -0
  58. package/dist/extensions/pi-tool-display/tests/custom-tool-overrides.test.ts +415 -0
  59. package/dist/extensions/pi-tool-display/tests/debug-logger-edge.test.ts +630 -0
  60. package/dist/extensions/pi-tool-display/tests/debug-logger.test.ts +62 -0
  61. package/dist/extensions/pi-tool-display/tests/diff-renderer-ansi.test.ts +55 -0
  62. package/dist/extensions/pi-tool-display/tests/diff-renderer-edge.test.ts +886 -0
  63. package/dist/extensions/pi-tool-display/tests/diff-renderer-numbering.test.ts +135 -0
  64. package/dist/extensions/pi-tool-display/tests/diff-renderer-width.test.ts +454 -0
  65. package/dist/extensions/pi-tool-display/tests/diff-renderer-wrap.test.ts +169 -0
  66. package/dist/extensions/pi-tool-display/tests/index-integration.test.ts +354 -0
  67. package/dist/extensions/pi-tool-display/tests/pending-diff-preview.test.ts +79 -0
  68. package/dist/extensions/pi-tool-display/tests/presets-edge.test.ts +229 -0
  69. package/dist/extensions/pi-tool-display/tests/presets.test.ts +26 -0
  70. package/dist/extensions/pi-tool-display/tests/reload-behavior.test.ts +1241 -0
  71. package/dist/extensions/pi-tool-display/tests/render-utils.test.ts +710 -0
  72. package/dist/extensions/pi-tool-display/tests/settings-inspector.test.ts +450 -0
  73. package/dist/extensions/pi-tool-display/tests/thinking-label-edge.test.ts +904 -0
  74. package/dist/extensions/pi-tool-display/tests/thinking-label.test.ts +72 -0
  75. package/dist/extensions/pi-tool-display/tests/tool-call-box.test.ts +105 -0
  76. package/dist/extensions/pi-tool-display/tests/tool-overrides-config.test.ts +613 -0
  77. package/dist/extensions/pi-tool-display/tests/tool-overrides-mcp.test.ts +499 -0
  78. package/dist/extensions/pi-tool-display/tests/tool-overrides-registration.test.ts +315 -0
  79. package/dist/extensions/pi-tool-display/tests/tool-overrides-write-meta.test.ts +68 -0
  80. package/dist/extensions/pi-tool-display/tests/tool-ui-utils.test.ts +413 -0
  81. package/dist/extensions/pi-tool-display/tests/user-message-box-performance-red.test.ts +176 -0
  82. package/dist/extensions/pi-tool-display/tests/user-message-box.test.ts +908 -0
  83. package/dist/extensions/pi-tool-display/tests/zellij-modal.test.ts +848 -0
  84. package/dist/extensions/pi-tool-display/tool-display-api-consumer.d.ts +30 -0
  85. package/dist/extensions/pi-tool-display/tool-display-api-consumer.js +40 -0
  86. package/dist/extensions/pi-zentui/extensions/zentui/format.ts +46 -1
  87. package/dist/extensions/pi-zentui/extensions/zentui/index.ts +10 -0
  88. package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +1 -1
  89. package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +3 -3
  90. package/dist/extensions/pi-zentui/test/format.test.ts +50 -2
  91. package/dist/modes/interactive/components/startup-box.d.ts +38 -0
  92. package/dist/modes/interactive/components/startup-box.js +90 -0
  93. package/dist/modes/interactive/interactive-mode.d.ts +3 -0
  94. package/dist/modes/interactive/interactive-mode.js +85 -122
  95. package/dist/skills/agent-browser/SKILL.md +1 -0
  96. package/dist/skills/batch-grill-me/SKILL.md +1 -0
  97. package/dist/skills/brandkit/SKILL.md +1 -0
  98. package/dist/skills/design-references/SKILL.md +1 -0
  99. package/dist/skills/design-taste-frontend/SKILL.md +1 -0
  100. package/dist/skills/design-taste-frontend-v1/SKILL.md +1 -0
  101. package/dist/skills/full-output-enforcement/SKILL.md +1 -0
  102. package/dist/skills/gpt-taste/SKILL.md +1 -0
  103. package/dist/skills/grill-me/SKILL.md +1 -0
  104. package/dist/skills/handoff/SKILL.md +1 -0
  105. package/dist/skills/handoff-text/SKILL.md +1 -0
  106. package/dist/skills/high-end-visual-design/SKILL.md +1 -0
  107. package/dist/skills/image-to-code/SKILL.md +1 -0
  108. package/dist/skills/imagegen-frontend-mobile/SKILL.md +1 -0
  109. package/dist/skills/imagegen-frontend-web/SKILL.md +1 -0
  110. package/dist/skills/implanger/SKILL.md +1 -0
  111. package/dist/skills/improve-codebase/SKILL.md +1 -0
  112. package/dist/skills/industrial-brutalist-ui/SKILL.md +1 -0
  113. package/dist/skills/minimalist-ui/SKILL.md +1 -0
  114. package/dist/skills/pi-subagents/SKILL.md +1 -0
  115. package/dist/skills/planger/SKILL.md +1 -0
  116. package/dist/skills/ponytail/SKILL.md +1 -0
  117. package/dist/skills/ponytail-audit/SKILL.md +1 -0
  118. package/dist/skills/ponytail-debt/SKILL.md +1 -0
  119. package/dist/skills/ponytail-gain/SKILL.md +1 -0
  120. package/dist/skills/ponytail-help/SKILL.md +1 -0
  121. package/dist/skills/ponytail-review/SKILL.md +1 -0
  122. package/dist/skills/redesign-existing-projects/SKILL.md +1 -0
  123. package/dist/skills/selesai-handoff/SKILL.md +1 -0
  124. package/dist/skills/stitch-design-taste/SKILL.md +1 -0
  125. package/dist/skills/unlazy/SKILL.md +1 -0
  126. package/dist/skills/web-design-guidelines/SKILL.md +1 -0
  127. package/dist/skills/workflow/SKILL.md +1 -0
  128. package/package.json +3 -3
@@ -0,0 +1,908 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ extractUserMessageMarkdownState,
5
+ } from "../src/user-message-box-markdown.ts";
6
+ import {
7
+ patchUserMessageRenderPrototype,
8
+ type PatchableUserMessagePrototype,
9
+ } from "../src/user-message-box-patch.ts";
10
+ import {
11
+ patchNativeUserMessagePrototype,
12
+ shouldBypassUserMessageMarkdownRebuild,
13
+ createUserMessageMarkdownLineRenderer,
14
+ } from "../src/user-message-box-renderer.ts";
15
+ import {
16
+ addUserMessageVerticalPadding,
17
+ applyUserMessageBackground,
18
+ normalizeUserMessageContentLine,
19
+ normalizeUserMessageContentLines,
20
+ type UserMessageBackgroundTheme,
21
+ } from "../src/user-message-box-utils.ts";
22
+
23
+ // ===========================================================================
24
+ // Issue #10: OSC 133 prompt marker stripping
25
+ // Each variant: BEL-terminated (\x07) and ST-terminated (\x1b\\)
26
+ // ===========================================================================
27
+
28
+ test("osc133 strips BEL-terminated A marker from content", () => {
29
+ assert.equal(
30
+ normalizeUserMessageContentLine("before\x1b]133;A\x07after"),
31
+ "beforeafter",
32
+ );
33
+ });
34
+
35
+ test("osc133 strips BEL-terminated B marker from content", () => {
36
+ assert.equal(
37
+ normalizeUserMessageContentLine("\x1b]133;B\x07middle"),
38
+ "middle",
39
+ );
40
+ });
41
+
42
+ test("osc133 strips BEL-terminated C marker from content", () => {
43
+ assert.equal(
44
+ normalizeUserMessageContentLine("data\x1b]133;C\x07"),
45
+ "data",
46
+ );
47
+ });
48
+
49
+ test("osc133 strips BEL-terminated D marker from content", () => {
50
+ assert.equal(
51
+ normalizeUserMessageContentLine("\x1b]133;D\x07trailing"),
52
+ "trailing",
53
+ );
54
+ });
55
+
56
+ test("osc133 strips ST-terminated A marker", () => {
57
+ assert.equal(
58
+ normalizeUserMessageContentLine("lead\x1b]133;A\x1b\\tail"),
59
+ "leadtail",
60
+ );
61
+ });
62
+
63
+ test("osc133 strips ST-terminated B marker", () => {
64
+ assert.equal(
65
+ normalizeUserMessageContentLine("\x1b]133;B\x1b\\inner"),
66
+ "inner",
67
+ );
68
+ });
69
+
70
+ test("osc133 strips ST-terminated C marker", () => {
71
+ assert.equal(
72
+ normalizeUserMessageContentLine("\x1b]133;C\x1b\\"),
73
+ "",
74
+ );
75
+ });
76
+
77
+ test("osc133 strips ST-terminated D marker", () => {
78
+ assert.equal(
79
+ normalizeUserMessageContentLine("\x1b]133;D\x1b\\data"),
80
+ "data",
81
+ );
82
+ });
83
+
84
+ test("osc133 strips multiple markers in a single line", () => {
85
+ assert.equal(
86
+ normalizeUserMessageContentLine("\x1b]133;A\x07hello\x1b]133;B\x07 world\x1b]133;C\x07"),
87
+ "hello world",
88
+ );
89
+ });
90
+
91
+ test("osc133 strips marker with optional semicolon-delimited parameters", () => {
92
+ assert.equal(
93
+ normalizeUserMessageContentLine("\x1b]133;A;url=https://example.com\x07text\x1b]133;B\x07"),
94
+ "text",
95
+ );
96
+ });
97
+
98
+ test("osc133 markers are stripped via applyUserMessageBackground path", () => {
99
+ assert.equal(
100
+ applyUserMessageBackground(undefined, "hello\x1b]133;A\x07"),
101
+ "hello",
102
+ );
103
+ });
104
+
105
+ test("osc133-only line is treated as visually empty", () => {
106
+ assert.equal(
107
+ normalizeUserMessageContentLine("\x1b]133;A\x07\x1b]133;B\x07"),
108
+ "",
109
+ );
110
+ });
111
+
112
+ // ===========================================================================
113
+ // Nested Box → Markdown structures
114
+ // ===========================================================================
115
+
116
+ test("extract finds deeply nested markdown child at depth 10", () => {
117
+ const theme = { heading: () => "" };
118
+ let child: Record<string, unknown> = {
119
+ text: "deep markdown",
120
+ theme,
121
+ };
122
+ for (let i = 0; i < 10; i++) {
123
+ child = { children: [child] };
124
+ }
125
+ const extracted = extractUserMessageMarkdownState({ children: [child] });
126
+ assert.equal(extracted?.text, "deep markdown");
127
+ assert.equal(extracted?.theme, theme);
128
+ });
129
+
130
+ test("extract returns first markdown child via DFS (not last)", () => {
131
+ const themeA = { heading: () => "a" };
132
+ const themeB = { heading: () => "b" };
133
+ const extracted = extractUserMessageMarkdownState({
134
+ children: [
135
+ { text: "first", theme: themeA },
136
+ { text: "second", theme: themeB },
137
+ ],
138
+ });
139
+ assert.equal(extracted?.text, "first");
140
+ assert.equal(extracted?.theme, themeA);
141
+ });
142
+
143
+ test("extract returns undefined when no markdown child exists", () => {
144
+ const extracted = extractUserMessageMarkdownState({
145
+ children: [{ lines: 3 }, { other: "data" }],
146
+ });
147
+ assert.equal(extracted, undefined);
148
+ });
149
+
150
+ test("extract returns undefined when child has theme but no text string", () => {
151
+ const extracted = extractUserMessageMarkdownState({
152
+ children: [{ theme: { heading: () => "" } }],
153
+ });
154
+ assert.equal(extracted, undefined);
155
+ });
156
+
157
+ test("extract returns markdown when top-level value itself is markdown-like", () => {
158
+ const theme = { heading: () => "" };
159
+ const extracted = extractUserMessageMarkdownState({
160
+ text: "self markdown",
161
+ theme,
162
+ } as never);
163
+ assert.equal(extracted?.text, "self markdown");
164
+ assert.equal(extracted?.theme, theme);
165
+ });
166
+
167
+ // ===========================================================================
168
+ // Malformed children on extractUserMessageMarkdownState
169
+ // ===========================================================================
170
+
171
+ test("extract handles null children gracefully", () => {
172
+ assert.equal(extractUserMessageMarkdownState({ children: null }), undefined);
173
+ });
174
+
175
+ test("extract handles undefined children (missing property) gracefully", () => {
176
+ assert.equal(extractUserMessageMarkdownState({}), undefined);
177
+ });
178
+
179
+ test("extract handles children as a plain string (non-array)", () => {
180
+ assert.equal(
181
+ extractUserMessageMarkdownState({ children: "not-an-array" as never }),
182
+ undefined,
183
+ );
184
+ });
185
+
186
+ test("extract handles children as a number", () => {
187
+ assert.equal(
188
+ extractUserMessageMarkdownState({ children: 42 as never }),
189
+ undefined,
190
+ );
191
+ });
192
+
193
+ test("extract handles array children containing only non-object items", () => {
194
+ assert.equal(
195
+ extractUserMessageMarkdownState({ children: [null, 42, "str", undefined, true] }),
196
+ undefined,
197
+ );
198
+ });
199
+
200
+ test("extract handles null top-level value", () => {
201
+ assert.equal(extractUserMessageMarkdownState(null as never), undefined);
202
+ });
203
+
204
+ test("extract handles undefined top-level value", () => {
205
+ assert.equal(extractUserMessageMarkdownState(undefined as never), undefined);
206
+ });
207
+
208
+ test("extract handles circular reference (stack overflow expected)", () => {
209
+ const circular: Record<string, unknown> = { children: [] };
210
+ circular.children = [circular];
211
+ // findMarkdownChild recurses without cycle detection, so this throws.
212
+ // In production the caller wraps extract in try/catch.
213
+ assert.throws(() => {
214
+ extractUserMessageMarkdownState(circular);
215
+ });
216
+ });
217
+
218
+ // ===========================================================================
219
+ // defaultTextStyle bgColor sanitization
220
+ // ===========================================================================
221
+
222
+ test("extract strips bgColor but keeps other defaultTextStyle props", () => {
223
+ const extracted = extractUserMessageMarkdownState({
224
+ children: [{
225
+ text: "styled",
226
+ theme: {},
227
+ defaultTextStyle: { color: "red", bgColor: () => "", bold: true },
228
+ }],
229
+ });
230
+ assert.equal(extracted?.defaultTextStyle?.color, "red");
231
+ assert.equal(extracted?.defaultTextStyle?.bold, true);
232
+ assert.equal("bgColor" in (extracted?.defaultTextStyle ?? {}), false);
233
+ });
234
+
235
+ test("extract returns undefined defaultTextStyle when all props were bgColor-only", () => {
236
+ const extracted = extractUserMessageMarkdownState({
237
+ children: [{
238
+ text: "text",
239
+ theme: {},
240
+ defaultTextStyle: { bgColor: () => "" },
241
+ }],
242
+ });
243
+ assert.equal(extracted?.defaultTextStyle, undefined);
244
+ });
245
+
246
+ test("extract returns undefined defaultTextStyle when value is not a record", () => {
247
+ const extracted = extractUserMessageMarkdownState({
248
+ children: [{
249
+ text: "raw",
250
+ theme: {},
251
+ defaultTextStyle: "string" as never,
252
+ }],
253
+ });
254
+ assert.equal(extracted?.defaultTextStyle, undefined);
255
+ });
256
+
257
+ // ===========================================================================
258
+ // applyUserMessageBackground with various theme objects
259
+ // ===========================================================================
260
+
261
+ test("applyBg on empty theme object returns sanitized text unchanged", () => {
262
+ const result = applyUserMessageBackground({}, "\x1b[31mred\x1b[0m");
263
+ // \x1b[0m → \x1b[39;22;23;24;25;27;28;29m (reset sans bg params)
264
+ assert.ok(result.includes("\x1b[31mred"));
265
+ assert.doesNotMatch(result, /\x1b\[0m/);
266
+ });
267
+
268
+ test("applyBg with empty string returns empty string", () => {
269
+ assert.equal(applyUserMessageBackground({ getBgAnsi: () => "\x1b[48;5;24m" }, ""), "");
270
+ assert.equal(applyUserMessageBackground(undefined, ""), "");
271
+ });
272
+
273
+ test("applyBg ignores getBgAnsi when it is not a function and falls to bg", () => {
274
+ const result = applyUserMessageBackground(
275
+ {
276
+ getBgAnsi: "not-a-fn" as never,
277
+ bg: (_color, text) => `wrapped(${text})`,
278
+ },
279
+ "hello",
280
+ );
281
+ assert.equal(result, "wrapped(hello)");
282
+ });
283
+
284
+ test("applyBg falls back to bg when getBgAnsi throws", () => {
285
+ const result = applyUserMessageBackground(
286
+ {
287
+ getBgAnsi: () => { throw new Error("oops"); },
288
+ bg: (_color, text) => `fallback(${text})`,
289
+ },
290
+ "hello",
291
+ );
292
+ assert.equal(result, "fallback(hello)");
293
+ });
294
+
295
+ test("applyBg returns sanitized text when both getBgAnsi and bg throw", () => {
296
+ const result = applyUserMessageBackground(
297
+ {
298
+ getBgAnsi: () => { throw new Error("e1"); },
299
+ bg: (_color, _text) => { throw new Error("e2"); },
300
+ },
301
+ "\x1b[32mtext",
302
+ );
303
+ assert.equal(result, "\x1b[32mtext");
304
+ });
305
+
306
+ test("applyBg returns sanitized text when theme has neither method", () => {
307
+ const result = applyUserMessageBackground(
308
+ { unrelated: true } as never,
309
+ "\x1b[33myellow\x1b[0m",
310
+ );
311
+ assert.ok(result.includes("\x1b[33myellow"));
312
+ });
313
+
314
+ test("applyBg passes userMessageBg constant to getBgAnsi", () => {
315
+ let capturedColor = "";
316
+ applyUserMessageBackground(
317
+ {
318
+ getBgAnsi: (color) => {
319
+ capturedColor = color;
320
+ return "\x1b[48;5;24m";
321
+ },
322
+ },
323
+ "test",
324
+ );
325
+ assert.equal(capturedColor, "userMessageBg");
326
+ });
327
+
328
+ test("applyBg passes userMessageBg constant to bg fallback", () => {
329
+ let capturedColor = "";
330
+ const result = applyUserMessageBackground(
331
+ {
332
+ bg: (color, text) => {
333
+ capturedColor = color;
334
+ return `[${color}]${text}`;
335
+ },
336
+ },
337
+ "text",
338
+ );
339
+ assert.equal(capturedColor, "userMessageBg");
340
+ assert.equal(result, "[userMessageBg]text");
341
+ });
342
+
343
+ // ===========================================================================
344
+ // Background double-application prevention (Issue #8 / #3)
345
+ // ===========================================================================
346
+
347
+ test("applyBg strips internal SGR 49 and appends exactly one reset at end", () => {
348
+ const rendered = applyUserMessageBackground(
349
+ { getBgAnsi: () => "\x1b[48;5;24m" },
350
+ "\x1b[31mhello\x1b[49m",
351
+ );
352
+ const fortyNineMatches = rendered.match(/\x1b\[49m/g);
353
+ assert.equal(fortyNineMatches?.length ?? 0, 1);
354
+ assert.ok(rendered.endsWith("\x1b[49m"));
355
+ });
356
+
357
+ test("applyBg strips multiple SGR 49 occurrences, appends single reset", () => {
358
+ const rendered = applyUserMessageBackground(
359
+ { getBgAnsi: () => "\x1b[48;5;24m" },
360
+ "\x1b[49mstart\x1b[49m middle\x1b[49m",
361
+ );
362
+ const fortyNineMatches = rendered.match(/\x1b\[49m/g);
363
+ assert.equal(fortyNineMatches?.length ?? 0, 1);
364
+ assert.ok(rendered.endsWith("\x1b[49m"));
365
+ });
366
+
367
+ test("applyBg strips existing background SGR before applying new bg via getBgAnsi", () => {
368
+ const rendered = applyUserMessageBackground(
369
+ { getBgAnsi: () => "\x1b[48;5;24m" },
370
+ "\x1b[48;5;160mtext\x1b[49m",
371
+ );
372
+ // Original bg (48;5;160) stripped
373
+ assert.doesNotMatch(rendered, /\x1b\[48;5;160m/);
374
+ // New bg applied exactly once
375
+ const bg24Matches = rendered.match(/\x1b\[48;5;24m/g);
376
+ assert.equal(bg24Matches?.length ?? 0, 1);
377
+ // Reset appended exactly once
378
+ const fortyNineMatches = rendered.match(/\x1b\[49m/g);
379
+ assert.equal(fortyNineMatches?.length ?? 0, 1);
380
+ assert.ok(rendered.endsWith("\x1b[49m"));
381
+ });
382
+
383
+ test("applyBg via bg function does not double-wrap", () => {
384
+ const rendered = applyUserMessageBackground(
385
+ { bg: (_color, text) => `[bg]${text}[/bg]` },
386
+ "\x1b[48;5;24mhello",
387
+ );
388
+ assert.ok(rendered.startsWith("[bg]"));
389
+ assert.ok(rendered.includes("hello"));
390
+ // The SGR 48;5;24 is stripped by sanitizeAnsiForThemedOutput
391
+ assert.doesNotMatch(rendered, /\x1b\[48;5;24m/);
392
+ });
393
+
394
+ // ===========================================================================
395
+ // normalizeUserMessageContentLines edge cases
396
+ // ===========================================================================
397
+
398
+ test("normalizeLines empty array returns empty array", () => {
399
+ assert.deepEqual(normalizeUserMessageContentLines([]), []);
400
+ });
401
+
402
+ test("normalizeLines trims leading and trailing whitespace-only lines", () => {
403
+ assert.deepEqual(
404
+ normalizeUserMessageContentLines([" ", "content", "\t"]),
405
+ ["content"],
406
+ );
407
+ });
408
+
409
+ test("normalizeLines preserves interior empty lines", () => {
410
+ assert.deepEqual(
411
+ normalizeUserMessageContentLines(["start", "", "end"]),
412
+ ["start", "", "end"],
413
+ );
414
+ });
415
+
416
+ test("normalizeLines trims only edge lines, not interior whitespace", () => {
417
+ assert.deepEqual(
418
+ normalizeUserMessageContentLines(["", " indented ", "middle", ""]),
419
+ [" indented ", "middle"],
420
+ );
421
+ });
422
+
423
+ test("normalizeLines treats leading OSC133-only lines as visually empty and trims them", () => {
424
+ assert.deepEqual(
425
+ normalizeUserMessageContentLines([
426
+ "\x1b]133;A\x07\x1b]133;B\x07 ",
427
+ "actual",
428
+ "\x1b]133;C\x07\u001b[0m",
429
+ ]),
430
+ ["actual"],
431
+ );
432
+ });
433
+
434
+ test("normalizeLines handles array with all visually empty lines", () => {
435
+ assert.deepEqual(
436
+ normalizeUserMessageContentLines(["", " ", "\x1b[0m"]),
437
+ [],
438
+ );
439
+ });
440
+
441
+ test("normalizeLines preserves lines with trailing spaces that are not edge lines", () => {
442
+ // Trailing spaces on interior lines are preserved
443
+ const result = normalizeUserMessageContentLines(["hello ", "world"]);
444
+ assert.equal(result[0], "hello ");
445
+ assert.equal(result[1], "world");
446
+ });
447
+
448
+ // ===========================================================================
449
+ // normalizeUserMessageContentLine edge cases
450
+ // ===========================================================================
451
+
452
+ test("normalizeLine preserves tab characters", () => {
453
+ assert.equal(normalizeUserMessageContentLine("\thello\tworld"), "\thello\tworld");
454
+ });
455
+
456
+ test("normalizeLine preserves non-SGR control characters", () => {
457
+ assert.equal(normalizeUserMessageContentLine("a\x00b\x01c"), "a\x00b\x01c");
458
+ });
459
+
460
+ test("normalizeLine handles very long line without truncation", () => {
461
+ const longLine = "x".repeat(10000);
462
+ assert.equal(normalizeUserMessageContentLine(longLine), longLine);
463
+ });
464
+
465
+ test("normalizeLine handles very long line with ANSI sequences", () => {
466
+ const result = normalizeUserMessageContentLine(
467
+ "\x1b[31m" + "x".repeat(5000) + "\x1b[0m" + "y".repeat(5000),
468
+ );
469
+ // Content survives; SGR 0 is converted to reset params sans bg
470
+ assert.ok(result.includes("x".repeat(5000)));
471
+ assert.ok(result.includes("y".repeat(5000)));
472
+ });
473
+
474
+ test("normalizeLine returns empty for line with only SGR control sequences", () => {
475
+ assert.equal(normalizeUserMessageContentLine("\x1b[0m\x1b[31m\x1b[0m"), "");
476
+ });
477
+
478
+ test("normalizeLine returns empty for empty string", () => {
479
+ assert.equal(normalizeUserMessageContentLine(""), "");
480
+ });
481
+
482
+ test("normalizeLine returns empty for whitespace-only string", () => {
483
+ assert.equal(normalizeUserMessageContentLine(" \t "), "");
484
+ });
485
+
486
+ test("normalizeLine preserves hyperlinks (OSC 8) while stripping OSC 133", () => {
487
+ const hyperlink = "\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x1b\\";
488
+ const result = normalizeUserMessageContentLine(hyperlink);
489
+ // OSC 8 hyperlinks should be preserved, not stripped
490
+ assert.equal(result, hyperlink);
491
+ });
492
+
493
+ // ===========================================================================
494
+ // addUserMessageVerticalPadding edge cases
495
+ // ===========================================================================
496
+
497
+ test("addPadding on empty array returns two empty padding lines", () => {
498
+ assert.deepEqual(addUserMessageVerticalPadding([]), ["", ""]);
499
+ });
500
+
501
+ test("addPadding on single line adds one blank line before and after", () => {
502
+ assert.deepEqual(addUserMessageVerticalPadding(["only"]), ["", "only", ""]);
503
+ });
504
+
505
+ test("addPadding on already-padded input adds another layer of padding", () => {
506
+ assert.deepEqual(
507
+ addUserMessageVerticalPadding(["", "content", ""]),
508
+ ["", "", "content", "", ""],
509
+ );
510
+ });
511
+
512
+ test("addPadding on multi-line input pads all sides equally", () => {
513
+ const result = addUserMessageVerticalPadding(["a", "b", "c"]);
514
+ assert.equal(result.length, 5);
515
+ assert.equal(result[0], "");
516
+ assert.equal(result[result.length - 1], "");
517
+ assert.deepEqual(result.slice(1, -1), ["a", "b", "c"]);
518
+ });
519
+
520
+ // ===========================================================================
521
+ // patchUserMessageRenderPrototype version handling
522
+ // ===========================================================================
523
+
524
+ test("patchRenderPrototype skips when prototype.render is not a function", () => {
525
+ const prototype: PatchableUserMessagePrototype = {
526
+ render: undefined as never,
527
+ };
528
+ patchUserMessageRenderPrototype(prototype, 7, (orig) => orig);
529
+ assert.equal(prototype.render, undefined);
530
+ });
531
+
532
+ test("patchRenderPrototype skips when already patched with same version", () => {
533
+ let buildCallCount = 0;
534
+ const originalRender = (width: number) => [`orig:${width}`];
535
+ const prototype: PatchableUserMessagePrototype = {
536
+ render: originalRender,
537
+ __piUserMessageOriginalRender: originalRender,
538
+ __piUserMessageNativePatched: true,
539
+ __piUserMessagePatchVersion: 7,
540
+ };
541
+ patchUserMessageRenderPrototype(prototype, 7, (_orig) => {
542
+ buildCallCount++;
543
+ return (width) => [`patched:${width}`];
544
+ });
545
+ assert.equal(buildCallCount, 0);
546
+ assert.equal(prototype.render, originalRender);
547
+ });
548
+
549
+ test("patchRenderPrototype reapplies when version is different (upgrade)", () => {
550
+ const originalRender = (width: number) => [`orig:${width}`];
551
+ const prototype: PatchableUserMessagePrototype = {
552
+ render: (width: number) => [`stale:${width}`],
553
+ __piUserMessageOriginalRender: originalRender,
554
+ __piUserMessageNativePatched: true,
555
+ __piUserMessagePatchVersion: 1,
556
+ };
557
+ patchUserMessageRenderPrototype(prototype, 7, (baseRender) => {
558
+ return function (this: unknown, width: number) {
559
+ return [`v7:${baseRender.call(this, width)}`];
560
+ };
561
+ });
562
+ assert.deepEqual(prototype.render(42), ["v7:orig:42"]);
563
+ assert.equal(prototype.__piUserMessagePatchVersion, 7);
564
+ });
565
+
566
+ test("patchRenderPrototype saves original render when __piUserMessageOriginalRender is absent", () => {
567
+ const originalRender = (width: number) => [`orig:${width}`];
568
+ const prototype: PatchableUserMessagePrototype = {
569
+ render: originalRender,
570
+ };
571
+ patchUserMessageRenderPrototype(prototype, 7, (baseRender) => {
572
+ return function (this: unknown, width: number) {
573
+ return [`patched:${baseRender.call(this, width)}`];
574
+ };
575
+ });
576
+ assert.equal(prototype.__piUserMessageOriginalRender, originalRender);
577
+ assert.notEqual(prototype.render, originalRender);
578
+ assert.deepEqual(prototype.render(10), ["patched:orig:10"]);
579
+ });
580
+
581
+ test("patchRenderPrototype saves original and patches when same version but original undefined", () => {
582
+ const originalRender = (width: number) => [`r:${width}`];
583
+ const prototype: PatchableUserMessagePrototype = {
584
+ render: originalRender,
585
+ __piUserMessageOriginalRender: undefined,
586
+ __piUserMessageNativePatched: true,
587
+ __piUserMessagePatchVersion: 5,
588
+ };
589
+ // Guard requires all three: patched + same version + original IS function.
590
+ // Since original is undefined, the function saves it and applies patch.
591
+ patchUserMessageRenderPrototype(prototype, 5, (baseRender) => {
592
+ return function (this: unknown, width: number) {
593
+ return [`patched:${baseRender.call(this, width)}`];
594
+ };
595
+ });
596
+ assert.equal(prototype.__piUserMessageOriginalRender, originalRender);
597
+ assert.deepEqual(prototype.render(10), ["patched:r:10"]);
598
+ assert.equal(prototype.__piUserMessagePatchVersion, 5);
599
+ });
600
+
601
+ test("patchRenderPrototype saves original and patches when version differs and original undefined", () => {
602
+ const originalRender = (width: number) => [`r:${width}`];
603
+ const prototype: PatchableUserMessagePrototype = {
604
+ render: originalRender,
605
+ __piUserMessageOriginalRender: undefined,
606
+ __piUserMessageNativePatched: true,
607
+ __piUserMessagePatchVersion: 5,
608
+ };
609
+ // Version differs (5 !== 7) so guard doesn't skip.
610
+ // Original is undefined, so the function saves it and applies patch.
611
+ patchUserMessageRenderPrototype(prototype, 7, (baseRender) => {
612
+ return function (this: unknown, width: number) {
613
+ return [`v7:${baseRender.call(this, width)}`];
614
+ };
615
+ });
616
+ assert.equal(prototype.__piUserMessageOriginalRender, originalRender);
617
+ assert.deepEqual(prototype.render(10), ["v7:r:10"]);
618
+ assert.equal(prototype.__piUserMessagePatchVersion, 7);
619
+ });
620
+
621
+ // ===========================================================================
622
+ // patchNativeUserMessagePrototype: width threshold and enable guard
623
+ // ===========================================================================
624
+
625
+ test("nativeRender width < 8 bypasses native rendering", () => {
626
+ let capturedWidth: number | undefined;
627
+ const prototype: PatchableUserMessagePrototype = {
628
+ render: (width: number) => {
629
+ capturedWidth = width;
630
+ return [`orig:${width}`];
631
+ },
632
+ };
633
+ patchNativeUserMessagePrototype(prototype, () => undefined, () => true);
634
+ assert.deepEqual(prototype.render(7), ["orig:7"]);
635
+ assert.equal(capturedWidth, 7);
636
+ });
637
+
638
+ test("nativeRender width exactly 8 triggers native rendering", () => {
639
+ const prototype: PatchableUserMessagePrototype = {
640
+ render: () => ["content"],
641
+ };
642
+ patchNativeUserMessagePrototype(prototype, () => undefined, () => true);
643
+ const rendered = prototype.render(8);
644
+ assert.ok(rendered.some((line) => line.includes("╭")), "should have top border");
645
+ assert.ok(rendered.some((line) => line.includes("╰")), "should have bottom border");
646
+ });
647
+
648
+ test("nativeRender width 0 bypasses native rendering", () => {
649
+ let capturedWidth: number | undefined;
650
+ const prototype: PatchableUserMessagePrototype = {
651
+ render: (width: number) => {
652
+ capturedWidth = width;
653
+ return [`orig:${width}`];
654
+ },
655
+ };
656
+ patchNativeUserMessagePrototype(prototype, () => undefined, () => true);
657
+ assert.deepEqual(prototype.render(0), ["orig:0"]);
658
+ assert.equal(capturedWidth, 0);
659
+ });
660
+
661
+ test("nativeRender negative width bypasses native rendering", () => {
662
+ let capturedWidth: number | undefined;
663
+ const prototype: PatchableUserMessagePrototype = {
664
+ render: (width: number) => {
665
+ capturedWidth = width;
666
+ return [`orig:${width}`];
667
+ },
668
+ };
669
+ patchNativeUserMessagePrototype(prototype, () => undefined, () => true);
670
+ assert.deepEqual(prototype.render(-5), ["orig:0"]);
671
+ assert.equal(capturedWidth, 0);
672
+ });
673
+
674
+ test("nativeRender fractional width floors before threshold check", () => {
675
+ let capturedWidth: number | undefined;
676
+ const prototype: PatchableUserMessagePrototype = {
677
+ render: (width: number) => {
678
+ capturedWidth = width;
679
+ return [`orig:${width}`];
680
+ },
681
+ };
682
+ patchNativeUserMessagePrototype(prototype, () => undefined, () => true);
683
+ // 7.5 floors to 7 → < 8 → bypass
684
+ assert.deepEqual(prototype.render(7.5), ["orig:7"]);
685
+ assert.equal(capturedWidth, 7);
686
+ });
687
+
688
+ test("nativeRender isEnabled false bypasses native rendering regardless of width", () => {
689
+ let capturedWidth: number | undefined;
690
+ const prototype: PatchableUserMessagePrototype = {
691
+ render: (width: number) => {
692
+ capturedWidth = width;
693
+ return [`orig:${width}`];
694
+ },
695
+ };
696
+ patchNativeUserMessagePrototype(prototype, () => undefined, () => false);
697
+ assert.deepEqual(prototype.render(100), ["orig:100"]);
698
+ assert.equal(capturedWidth, 100);
699
+ });
700
+
701
+ test("nativeRender produces top margin spacer and border when enabled and width >= 8", () => {
702
+ const prototype: PatchableUserMessagePrototype = {
703
+ render: () => ["user message body"],
704
+ };
705
+ patchNativeUserMessagePrototype(prototype, () => undefined, () => true);
706
+ const rendered = prototype.render(40);
707
+ assert.equal(rendered[0], "", "first line is top margin spacer");
708
+ assert.ok(rendered[1]?.includes("╭"), "second line has top border");
709
+ assert.ok(rendered.some((l) => l.includes("│")), "has content border lines");
710
+ assert.ok(rendered.some((l) => l.includes("╰")), "has bottom border");
711
+ });
712
+
713
+ test("nativeRender prototype without render function does not crash", () => {
714
+ const prototype: Partial<PatchableUserMessagePrototype> = {};
715
+ patchNativeUserMessagePrototype(
716
+ prototype as PatchableUserMessagePrototype,
717
+ () => undefined,
718
+ () => true,
719
+ );
720
+ assert.equal((prototype as PatchableUserMessagePrototype).render, undefined);
721
+ });
722
+
723
+ test("nativeRender invokes original for empty body content", () => {
724
+ let calledWith = -1;
725
+ const prototype: PatchableUserMessagePrototype = {
726
+ render: (w) => {
727
+ calledWith = w;
728
+ return [];
729
+ },
730
+ };
731
+ patchNativeUserMessagePrototype(prototype, () => undefined, () => true);
732
+ prototype.render(8);
733
+ // original called with content-width = max(1, 8-2-1*2) = max(1, 4) = 4
734
+ assert.equal(calledWith, 4);
735
+ });
736
+
737
+ // ===========================================================================
738
+ // shouldBypassUserMessageMarkdownRebuild edge cases
739
+ // ===========================================================================
740
+
741
+ test("bypass guard allows short text within limits", () => {
742
+ assert.equal(
743
+ shouldBypassUserMessageMarkdownRebuild({ text: "short", theme: {} }),
744
+ false,
745
+ );
746
+ });
747
+
748
+ test("bypass guard triggers on text over 100000 chars", () => {
749
+ assert.equal(
750
+ shouldBypassUserMessageMarkdownRebuild({
751
+ text: "x".repeat(100001),
752
+ theme: {},
753
+ }),
754
+ true,
755
+ );
756
+ });
757
+
758
+ test("bypass guard triggers when line count exceeds 2000", () => {
759
+ assert.equal(
760
+ shouldBypassUserMessageMarkdownRebuild({
761
+ text: `${"line\n".repeat(2000)}tail`,
762
+ theme: {},
763
+ }),
764
+ true,
765
+ );
766
+ });
767
+
768
+ test("bypass guard does NOT bypass exactly 2000 lines (threshold is > 2000)", () => {
769
+ // 1999 newlines = 2000 lines; guard checks > 2000, so 2000 should NOT bypass
770
+ assert.equal(
771
+ shouldBypassUserMessageMarkdownRebuild({
772
+ text: "a\n".repeat(1999) + "a",
773
+ theme: {},
774
+ }),
775
+ false,
776
+ );
777
+ });
778
+
779
+ // ===========================================================================
780
+ // createUserMessageMarkdownLineRenderer cache behavior
781
+ // ===========================================================================
782
+
783
+ test("markdown line renderer cache hits on identical state and width", () => {
784
+ let rendererCreationCount = 0;
785
+ let renderCallCount = 0;
786
+ const renderMarkdown = createUserMessageMarkdownLineRenderer((state) => {
787
+ rendererCreationCount++;
788
+ return {
789
+ render(width: number) {
790
+ renderCallCount++;
791
+ return [`${state.text}:${width}`];
792
+ },
793
+ };
794
+ });
795
+
796
+ const instance = {};
797
+ const theme = { heading: () => "" };
798
+ const state = { text: "cached", theme, defaultTextStyle: { bold: true } };
799
+
800
+ assert.deepEqual(renderMarkdown(instance, state, 24), ["cached:24"]);
801
+ assert.deepEqual(renderMarkdown(instance, state, 24), ["cached:24"]);
802
+ assert.equal(rendererCreationCount, 1);
803
+ assert.equal(renderCallCount, 1);
804
+ });
805
+
806
+ test("markdown line renderer re-renders on width change with same renderer", () => {
807
+ let rendererCreationCount = 0;
808
+ let renderCallCount = 0;
809
+ const renderMarkdown = createUserMessageMarkdownLineRenderer((state) => {
810
+ rendererCreationCount++;
811
+ return {
812
+ render(width: number) {
813
+ renderCallCount++;
814
+ return [`${state.text}:${width}`];
815
+ },
816
+ };
817
+ });
818
+
819
+ const instance = {};
820
+ const theme = { heading: () => "" };
821
+ const state = { text: "width-test", theme };
822
+
823
+ assert.deepEqual(renderMarkdown(instance, state, 10), ["width-test:10"]);
824
+ assert.deepEqual(renderMarkdown(instance, state, 20), ["width-test:20"]);
825
+ assert.equal(rendererCreationCount, 1);
826
+ assert.equal(renderCallCount, 2);
827
+ });
828
+
829
+ test("markdown line renderer creates new renderer on state change", () => {
830
+ let rendererCreationCount = 0;
831
+ const renderMarkdown = createUserMessageMarkdownLineRenderer((state) => {
832
+ rendererCreationCount++;
833
+ return {
834
+ render(_width: number) {
835
+ return [`${state.text}`];
836
+ },
837
+ };
838
+ });
839
+
840
+ const instance = {};
841
+ const theme = { heading: () => "" };
842
+
843
+ assert.deepEqual(renderMarkdown(instance, { text: "a", theme }, 10), ["a"]);
844
+ assert.deepEqual(renderMarkdown(instance, { text: "b", theme }, 10), ["b"]);
845
+ assert.equal(rendererCreationCount, 2);
846
+ });
847
+
848
+ test("markdown line renderer uses separate cache per instance (WeakMap)", () => {
849
+ let rendererCreationCount = 0;
850
+ const renderMarkdown = createUserMessageMarkdownLineRenderer((state) => {
851
+ rendererCreationCount++;
852
+ return {
853
+ render(_width: number) {
854
+ return [`${state.text}`];
855
+ },
856
+ };
857
+ });
858
+
859
+ const instanceA = {};
860
+ const instanceB = {};
861
+ const theme = { heading: () => "" };
862
+ const state = { text: "shared", theme };
863
+
864
+ assert.deepEqual(renderMarkdown(instanceA, state, 10), ["shared"]);
865
+ assert.deepEqual(renderMarkdown(instanceB, state, 10), ["shared"]);
866
+ // Each instance gets its own cache entry, so two renderers created
867
+ assert.equal(rendererCreationCount, 2);
868
+ });
869
+
870
+ // ===========================================================================
871
+ // End-to-end: patchNativeUserMessagePrototype with real theme
872
+ // ===========================================================================
873
+
874
+ test("nativeRender applies theme coloring via fg and bold", () => {
875
+ const prototype: PatchableUserMessagePrototype = {
876
+ render: () => ["hello world"],
877
+ };
878
+ const theme = {
879
+ fg: (_color: string, text: string) => `[${_color}]${text}`,
880
+ bold: (text: string) => `*${text}*`,
881
+ };
882
+ patchNativeUserMessagePrototype(prototype, () => theme, () => true);
883
+
884
+ const rendered = prototype.render(30);
885
+ // Title should be bold + accent-colored
886
+ const titleLine = rendered.find((l) => l.includes("user")) ?? "";
887
+ assert.ok(titleLine.includes("* user *") || titleLine.includes("[accent]"));
888
+ // Border should be border-colored
889
+ const topBorder = rendered.find((l) => l.includes("╭")) ?? "";
890
+ assert.ok(topBorder.includes("[border]"));
891
+ });
892
+
893
+ test("nativeRender builds correct box structure with all required line types", () => {
894
+ const prototype: PatchableUserMessagePrototype = {
895
+ render: () => ["line one", "line two"],
896
+ };
897
+ patchNativeUserMessagePrototype(prototype, () => undefined, () => true);
898
+
899
+ const rendered = prototype.render(30);
900
+ // Structure: [spacer, top-border, padding, content-line, padding, content-line, padding, bottom-border]
901
+ // With vertical padding of 1, each content line gets padding on both sides:
902
+ // But actually, addUserMessageVerticalPadding adds 1 padding before and after ALL content:
903
+ // So: spacer(blank), top-border, blank, content1, content2, blank, bottom-border
904
+ // = 7 lines total
905
+ assert.ok(rendered.length >= 5);
906
+ assert.equal(rendered.filter((l) => l.includes("│")).length, 4); // 2 content + 2 padding
907
+ assert.equal(rendered.filter((l) => l.includes("╭") || l.includes("╰")).length, 2);
908
+ });