@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,413 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+ import { visibleWidth } from "@earendil-works/pi-tui";
7
+ import {
8
+ countWriteContentLines,
9
+ getWriteContentSizeBytes,
10
+ shouldRenderWriteCallSummary,
11
+ } from "../src/write-display-utils.ts";
12
+ import {
13
+ extractUserMessageMarkdownState,
14
+ } from "../src/user-message-box-markdown.ts";
15
+ import {
16
+ createUserMessageMarkdownLineRenderer,
17
+ patchNativeUserMessagePrototype,
18
+ shouldBypassUserMessageMarkdownRebuild,
19
+ } from "../src/user-message-box-renderer.ts";
20
+ import {
21
+ patchUserMessageRenderPrototype,
22
+ type PatchableUserMessagePrototype,
23
+ } from "../src/user-message-box-patch.ts";
24
+ import {
25
+ addUserMessageVerticalPadding,
26
+ applyUserMessageBackground,
27
+ normalizeUserMessageContentLine,
28
+ normalizeUserMessageContentLines,
29
+ } from "../src/user-message-box-utils.ts";
30
+ import {
31
+ buildCollapsedDiffHintText,
32
+ clampRenderedLineToWidth,
33
+ } from "../src/line-width-safety.ts";
34
+ import { buildPendingEditPreviewData } from "../src/pending-diff-preview.ts";
35
+
36
+ const codePointWidthOps = {
37
+ measure: (text: string): number => [...text].length,
38
+ truncate: (text: string, maxWidth: number): string =>
39
+ [...text].slice(0, Math.max(0, maxWidth)).join(""),
40
+ };
41
+
42
+
43
+ test("pending edit preview matches LF edit input against CRLF files", () => {
44
+ const baseDir = mkdtempSync(join(tmpdir(), "pi-tool-display-preview-"));
45
+
46
+ try {
47
+ const filePath = join(baseDir, "sample.txt");
48
+ writeFileSync(filePath, "alpha\r\nbeta\r\ngamma\r\n", "utf8");
49
+
50
+ const preview = buildPendingEditPreviewData(
51
+ {
52
+ path: "sample.txt",
53
+ edits: [
54
+ {
55
+ oldText: "alpha\nbeta",
56
+ newText: "alpha\nupdated",
57
+ },
58
+ ],
59
+ },
60
+ baseDir,
61
+ );
62
+
63
+ assert.equal(preview?.notice, undefined);
64
+ assert.equal(preview?.nextContent, "alpha\r\nupdated\r\ngamma\r\n");
65
+ assert.equal(preview?.previousContent, "alpha\r\nbeta\r\ngamma\r\n");
66
+ } finally {
67
+ rmSync(baseDir, { recursive: true, force: true });
68
+ }
69
+ });
70
+
71
+ test("pending edit preview reports a concise notice for true edit mismatches", () => {
72
+ const baseDir = mkdtempSync(join(tmpdir(), "pi-tool-display-preview-miss-"));
73
+
74
+ try {
75
+ writeFileSync(join(baseDir, "sample.txt"), "alpha\nbeta\n", "utf8");
76
+
77
+ const preview = buildPendingEditPreviewData(
78
+ {
79
+ path: "sample.txt",
80
+ edits: [
81
+ {
82
+ oldText: "missing",
83
+ newText: "updated",
84
+ },
85
+ ],
86
+ },
87
+ baseDir,
88
+ );
89
+
90
+ assert.equal(preview?.nextContent, undefined);
91
+ assert.equal(preview?.notice, "Preview not shown: edit #1 did not match the current file contents.");
92
+ } finally {
93
+ rmSync(baseDir, { recursive: true, force: true });
94
+ }
95
+ });
96
+
97
+ test("write call summary moves metrics onto the first line when the result header omits them", () => {
98
+ assert.equal(
99
+ shouldRenderWriteCallSummary({
100
+ hasContent: true,
101
+ hasDetailedResultHeader: false,
102
+ }),
103
+ true,
104
+ );
105
+ assert.equal(
106
+ shouldRenderWriteCallSummary({
107
+ hasContent: true,
108
+ hasDetailedResultHeader: true,
109
+ }),
110
+ false,
111
+ );
112
+ assert.equal(
113
+ shouldRenderWriteCallSummary({
114
+ hasContent: false,
115
+ hasDetailedResultHeader: false,
116
+ }),
117
+ false,
118
+ );
119
+ });
120
+
121
+ test("write content line counting ignores trailing newlines", () => {
122
+ assert.equal(countWriteContentLines("alpha\nbeta\n"), 2);
123
+ assert.equal(countWriteContentLines("alpha\r\nbeta"), 2);
124
+ assert.equal(countWriteContentLines(""), 0);
125
+ });
126
+
127
+ test("write content size uses utf-8 bytes", () => {
128
+ assert.equal(getWriteContentSizeBytes("hello"), 5);
129
+ assert.equal(getWriteContentSizeBytes("é"), 2);
130
+ });
131
+
132
+ test("user message patch reapplies on reload when an older patch version is already present", () => {
133
+ const originalRender = (width: number): string[] => [`original:${width}`];
134
+ const prototype: PatchableUserMessagePrototype = {
135
+ render: (width: number): string[] => [`stale:${width}`],
136
+ __piUserMessageOriginalRender: originalRender,
137
+ __piUserMessageNativePatched: true,
138
+ __piUserMessagePatchVersion: 1,
139
+ };
140
+
141
+ patchUserMessageRenderPrototype(prototype, 5, (baseRender) => {
142
+ return function patched(width: number): string[] {
143
+ return [`patched:${baseRender.call(this, width).join("|")}`];
144
+ };
145
+ });
146
+
147
+ assert.deepEqual(prototype.render(14), ["patched:original:14"]);
148
+ assert.equal(prototype.__piUserMessagePatchVersion, 5);
149
+ assert.equal(prototype.__piUserMessageNativePatched, true);
150
+ });
151
+
152
+ test("user message markdown extraction removes nested background styling", () => {
153
+ const theme = { heading: () => "" };
154
+ const color = (text: string): string => text;
155
+ const extracted = extractUserMessageMarkdownState({
156
+ children: [
157
+ { lines: 1 },
158
+ {
159
+ text: "Hello **WezTerm**",
160
+ theme,
161
+ defaultTextStyle: {
162
+ color,
163
+ bgColor: (text: string): string => text,
164
+ bold: true,
165
+ },
166
+ },
167
+ ],
168
+ });
169
+
170
+ assert.equal(extracted?.text, "Hello **WezTerm**");
171
+ assert.equal(extracted?.theme, theme);
172
+ assert.equal(extracted?.defaultTextStyle?.color, color);
173
+ assert.equal(extracted?.defaultTextStyle?.bold, true);
174
+ assert.equal("bgColor" in (extracted?.defaultTextStyle ?? {}), false);
175
+ });
176
+
177
+ test("user message markdown extraction finds markdown nested inside the native box", () => {
178
+ const theme = { heading: () => "" };
179
+ const color = (text: string): string => text;
180
+ const extracted = extractUserMessageMarkdownState({
181
+ children: [
182
+ {
183
+ children: [
184
+ {
185
+ text: "Nested **markdown**",
186
+ theme,
187
+ defaultTextStyle: {
188
+ color,
189
+ bgColor: (text: string): string => text,
190
+ },
191
+ },
192
+ ],
193
+ },
194
+ ],
195
+ });
196
+
197
+ assert.equal(extracted?.text, "Nested **markdown**");
198
+ assert.equal(extracted?.theme, theme);
199
+ assert.equal(extracted?.defaultTextStyle?.color, color);
200
+ assert.equal("bgColor" in (extracted?.defaultTextStyle ?? {}), false);
201
+ });
202
+
203
+ test("user message markdown renderer reuses cached markdown renders for identical state", () => {
204
+ let rendererCreationCount = 0;
205
+ let renderCallCount = 0;
206
+ const renderMarkdown = createUserMessageMarkdownLineRenderer((state) => {
207
+ rendererCreationCount++;
208
+ return {
209
+ render(width: number): string[] {
210
+ renderCallCount++;
211
+ return [`${state.text}:${width}`];
212
+ },
213
+ };
214
+ });
215
+
216
+ const instance = {};
217
+ const theme = { heading: () => "" };
218
+
219
+ assert.deepEqual(
220
+ renderMarkdown(instance, { text: "cached", theme, defaultTextStyle: { bold: true } }, 24),
221
+ ["cached:24"],
222
+ );
223
+ assert.deepEqual(
224
+ renderMarkdown(instance, { text: "cached", theme, defaultTextStyle: { bold: true } }, 24),
225
+ ["cached:24"],
226
+ );
227
+ assert.equal(rendererCreationCount, 1);
228
+ assert.equal(renderCallCount, 1);
229
+
230
+ assert.deepEqual(
231
+ renderMarkdown(instance, { text: "cached", theme, defaultTextStyle: { bold: true } }, 48),
232
+ ["cached:48"],
233
+ );
234
+ assert.equal(rendererCreationCount, 1);
235
+ assert.equal(renderCallCount, 2);
236
+
237
+ assert.deepEqual(
238
+ renderMarkdown(instance, { text: "updated", theme, defaultTextStyle: { bold: true } }, 48),
239
+ ["updated:48"],
240
+ );
241
+ assert.equal(rendererCreationCount, 2);
242
+ assert.equal(renderCallCount, 3);
243
+ });
244
+
245
+ test("user message markdown rebuild guard bypasses oversized payloads", () => {
246
+ assert.equal(
247
+ shouldBypassUserMessageMarkdownRebuild({
248
+ text: "short message",
249
+ theme: {},
250
+ }),
251
+ false,
252
+ );
253
+ assert.equal(
254
+ shouldBypassUserMessageMarkdownRebuild({
255
+ text: `${"line\n".repeat(2000)}tail`,
256
+ theme: {},
257
+ }),
258
+ true,
259
+ );
260
+ });
261
+
262
+ test("user message renderer adds one top and bottom padding row inside the box", () => {
263
+ assert.deepEqual(addUserMessageVerticalPadding(["Ping"]), ["", "Ping", ""]);
264
+ assert.deepEqual(addUserMessageVerticalPadding(["Line 1", "Line 2"]), ["", "Line 1", "Line 2", ""]);
265
+ });
266
+
267
+ test("native user message renderer inserts one blank spacer line before the box", () => {
268
+ const prototype: PatchableUserMessagePrototype = {
269
+ render: () => ["Original user content"],
270
+ };
271
+
272
+ patchNativeUserMessagePrototype(prototype, () => undefined, () => true);
273
+
274
+ const rendered = prototype.render(24);
275
+
276
+ assert.equal(rendered[0], "");
277
+ assert.match(rendered[1] ?? "", /^╭/);
278
+ });
279
+
280
+ test("native user message renderer wraps body at the padded content width", () => {
281
+ const contentWidth = 16;
282
+ const totalWidth = contentWidth + 4;
283
+ const message = `${"X".repeat(contentWidth)}YZ`;
284
+ const requestedWidths: number[] = [];
285
+ const prototype: PatchableUserMessagePrototype = {
286
+ render: (width: number): string[] => {
287
+ requestedWidths.push(width);
288
+ const lines: string[] = [];
289
+ for (let index = 0; index < message.length; index += width) {
290
+ lines.push(message.slice(index, index + width));
291
+ }
292
+ return lines;
293
+ },
294
+ };
295
+
296
+ patchNativeUserMessagePrototype(prototype, () => undefined, () => true);
297
+
298
+ const rendered = prototype.render(totalWidth);
299
+ const borderedRows = rendered.filter((line) =>
300
+ line.startsWith("│") || line.startsWith("╭") || line.startsWith("╰"),
301
+ );
302
+
303
+ assert.equal(requestedWidths[0], contentWidth);
304
+ assert.ok(
305
+ rendered.some((line) => line.includes("YZ")),
306
+ "tail content should wrap instead of being clipped",
307
+ );
308
+ assert.ok(borderedRows.every((line) => visibleWidth(line) === totalWidth));
309
+ });
310
+
311
+ test("user message blank ansi lines are cleared before wrapping", () => {
312
+ assert.equal(normalizeUserMessageContentLine("\u001b[0m"), "");
313
+ assert.equal(normalizeUserMessageContentLine(" \u001b[31m\u001b[0m"), "");
314
+ assert.equal(normalizeUserMessageContentLine("\u001b]133;A\u0007 \u001b]133;B\u0007"), "");
315
+
316
+ const normalized = normalizeUserMessageContentLine("\u001b[31mhello\u001b[0m");
317
+ assert.match(normalized, /hello/);
318
+ assert.doesNotMatch(normalized, /\u001b\[0m/);
319
+
320
+ const withoutOsc = normalizeUserMessageContentLine("\u001b]133;A\u0007hello\u001b]133;B\u0007");
321
+ assert.equal(withoutOsc, "hello");
322
+
323
+ const hyperlink = "\u001b]8;;https://example.com\u001b\\link\u001b]8;;\u001b\\";
324
+ assert.equal(normalizeUserMessageContentLine(hyperlink), hyperlink);
325
+ });
326
+
327
+ test("user message background painting keeps trailing padding inside explicit background cells", () => {
328
+ const rendered = applyUserMessageBackground(
329
+ {
330
+ getBgAnsi: () => "\u001b[48;5;24m",
331
+ },
332
+ "\u001b[31mhello\u001b[0m ",
333
+ );
334
+
335
+ assert.ok(rendered.startsWith("\u001b[48;5;24m"));
336
+ assert.match(rendered, /hello/);
337
+ assert.equal(rendered.indexOf("\u001b[49m"), rendered.lastIndexOf("\u001b[49m"));
338
+ assert.equal(rendered.slice(rendered.lastIndexOf("\u001b[49m") - 3, rendered.lastIndexOf("\u001b[49m")), " ");
339
+ assert.doesNotMatch(rendered, /\u001b\[0m/);
340
+
341
+ const hyperlink = "\u001b]8;;https://example.com\u001b\\link\u001b]8;;\u001b\\";
342
+ const hyperlinkRendered = applyUserMessageBackground(undefined, hyperlink);
343
+ assert.equal(hyperlinkRendered, hyperlink);
344
+ });
345
+
346
+ test("user message background painting falls back to theme bg helper", () => {
347
+ const rendered = applyUserMessageBackground(
348
+ {
349
+ bg: (_color, text) => `bg(${text})`,
350
+ },
351
+ "\u001b[32mworld\u001b[49m",
352
+ );
353
+
354
+ assert.equal(rendered, "bg(\u001b[32mworld)");
355
+ });
356
+
357
+ test("user message content trims edge padding but preserves intentional interior breaks", () => {
358
+ assert.deepEqual(normalizeUserMessageContentLines(["", "hello", ""]), ["hello"]);
359
+ assert.deepEqual(normalizeUserMessageContentLines(["", "hello", "", "world", ""]), [
360
+ "hello",
361
+ "",
362
+ "world",
363
+ ]);
364
+ assert.deepEqual(normalizeUserMessageContentLines(["\u001b[0m", " "]), []);
365
+ assert.deepEqual(
366
+ normalizeUserMessageContentLines([
367
+ "\u001b]133;A\u0007 ",
368
+ "hello",
369
+ "\u001b]133;B\u0007\u001b]133;C\u0007 ",
370
+ ]),
371
+ ["hello"],
372
+ );
373
+ });
374
+
375
+ test("collapsed diff hint shrinks to fit narrow panes", () => {
376
+ const hint = buildCollapsedDiffHintText(
377
+ {
378
+ remainingLines: 3970,
379
+ hiddenHunks: 1,
380
+ },
381
+ 35,
382
+ codePointWidthOps,
383
+ );
384
+
385
+ assert.ok(codePointWidthOps.measure(hint) <= 35);
386
+ assert.match(hint, /3970/);
387
+ assert.doesNotMatch(hint, /Ctrl\+O/);
388
+ });
389
+
390
+ test("collapsed diff hint keeps full guidance when width allows", () => {
391
+ const hint = buildCollapsedDiffHintText(
392
+ {
393
+ remainingLines: 12,
394
+ hiddenHunks: 2,
395
+ },
396
+ 80,
397
+ codePointWidthOps,
398
+ );
399
+
400
+ assert.equal(hint, "… (12 more diff lines • 2 more hunks • Ctrl+O to expand)");
401
+ });
402
+
403
+ test("line width clamp never returns text wider than the requested width", () => {
404
+ const clamped = clampRenderedLineToWidth(
405
+ "… (3970 more diff lines • Ctrl+O to expand)",
406
+ 35,
407
+ codePointWidthOps,
408
+ );
409
+
410
+ assert.ok(codePointWidthOps.measure(clamped) <= 35);
411
+ assert.equal(clampRenderedLineToWidth("hello", 0, codePointWidthOps), "");
412
+ });
413
+
@@ -0,0 +1,176 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ patchNativeUserMessagePrototype,
5
+ type PatchableUserMessagePrototype,
6
+ type UserMessageTheme,
7
+ } from "../src/user-message-box-renderer.ts";
8
+
9
+ function createCountingTheme(label: string): UserMessageTheme & { calls: string[] } {
10
+ const calls: string[] = [];
11
+ return {
12
+ calls,
13
+ fg(color: string, text: string): string {
14
+ calls.push(`${label}:fg:${color}:${text}`);
15
+ return `[${label}:${color}]${text}`;
16
+ },
17
+ bold(text: string): string {
18
+ calls.push(`${label}:bold:${text}`);
19
+ return `[${label}:bold]${text}`;
20
+ },
21
+ };
22
+ }
23
+
24
+ function markdownUserMessageInstance(text: string, theme: object = {}): { children: unknown[] } {
25
+ return {
26
+ children: [
27
+ {
28
+ text,
29
+ theme,
30
+ },
31
+ ],
32
+ };
33
+ }
34
+
35
+ function renderLineCount(lines: string[], needle: string): number {
36
+ return lines.filter((line) => line.includes(needle)).length;
37
+ }
38
+
39
+ // RED coverage for the reported long-session slowdown: these tests describe the
40
+ // expected production fix but intentionally do not implement it here.
41
+
42
+ test("native user message border render reuses final boxed output for identical rerenders", () => {
43
+ let originalRenderCount = 0;
44
+ const theme = createCountingTheme("stable");
45
+ const prototype: PatchableUserMessagePrototype = {
46
+ render(width: number) {
47
+ originalRenderCount++;
48
+ return [`stable body at ${width}`];
49
+ },
50
+ };
51
+ const instance = {};
52
+
53
+ patchNativeUserMessagePrototype(prototype, () => theme, () => true);
54
+
55
+ const first = prototype.render.call(instance, 42);
56
+ const themeCallsAfterFirstRender = theme.calls.length;
57
+ const second = prototype.render.call(instance, 42);
58
+
59
+ assert.equal(originalRenderCount, 1, "identical rerender should not call the original body renderer again");
60
+ assert.equal(theme.calls.length, themeCallsAfterFirstRender, "identical rerender should not recolor/rebuild border lines");
61
+ assert.equal(second, first, "identical rerender should return the cached final bordered string[]");
62
+ });
63
+
64
+ test("native user message final output cache invalidates by width and theme without rebuilding unchanged body", () => {
65
+ let originalRenderCount = 0;
66
+ let activeTheme = createCountingTheme("a");
67
+ const prototype: PatchableUserMessagePrototype = {
68
+ render(width: number) {
69
+ originalRenderCount++;
70
+ return [`body rendered for ${width}`];
71
+ },
72
+ };
73
+ const instance = {};
74
+
75
+ patchNativeUserMessagePrototype(prototype, () => activeTheme, () => true);
76
+
77
+ const width40ThemeA = prototype.render.call(instance, 40);
78
+ const width40ThemeAAgain = prototype.render.call(instance, 40);
79
+ const width50ThemeA = prototype.render.call(instance, 50);
80
+ activeTheme = createCountingTheme("b");
81
+ const width50ThemeB = prototype.render.call(instance, 50);
82
+
83
+ assert.equal(width40ThemeAAgain, width40ThemeA, "same width/theme should hit final output cache");
84
+ assert.notEqual(width50ThemeA, width40ThemeA, "different width must invalidate final output cache");
85
+ assert.notDeepEqual(width50ThemeB, width50ThemeA, "different theme must invalidate final output cache");
86
+ assert.equal(originalRenderCount, 2, "body renderer should run only once per distinct content width");
87
+ });
88
+
89
+ test("native user message final output cache does not return stale output after message state changes", () => {
90
+ const markdownTheme = {};
91
+ const instance = markdownUserMessageInstance("first message", markdownTheme);
92
+ const prototype: PatchableUserMessagePrototype = {
93
+ render() {
94
+ throw new Error("markdown child should provide body lines without falling back to original render");
95
+ },
96
+ };
97
+
98
+ patchNativeUserMessagePrototype(prototype, () => undefined, () => true);
99
+
100
+ const first = prototype.render.call(instance, 48);
101
+ instance.children = [{ text: "second message", theme: markdownTheme }];
102
+ const second = prototype.render.call(instance, 48);
103
+
104
+ assert.notEqual(second, first, "changed markdown state should invalidate the final output cache reference");
105
+ assert.ok(first.some((line) => line.includes("first message")), "first render should include the original message");
106
+ assert.ok(second.some((line) => line.includes("second message")), "second render should include the changed message");
107
+ assert.equal(second.some((line) => line.includes("first message")), false, "changed message should not reuse stale boxed output");
108
+ });
109
+
110
+ test("native user message final output cache invalidates when the enabled state changes", () => {
111
+ let enabled = true;
112
+ const originalLines = ["disabled native fallback"];
113
+ const prototype: PatchableUserMessagePrototype = {
114
+ render() {
115
+ return originalLines;
116
+ },
117
+ };
118
+ const instance = {};
119
+
120
+ patchNativeUserMessagePrototype(prototype, () => undefined, () => enabled);
121
+
122
+ const enabledRender = prototype.render.call(instance, 52);
123
+ const enabledRenderAgain = prototype.render.call(instance, 52);
124
+ enabled = false;
125
+ const disabledRender = prototype.render.call(instance, 52);
126
+
127
+ assert.equal(enabledRenderAgain, enabledRender, "same width/theme/enabled state should hit final output cache");
128
+ assert.equal(disabledRender, originalLines, "disabled native rendering must bypass any previously cached boxed output");
129
+ assert.notEqual(disabledRender, enabledRender, "enabled-state change should invalidate final output cache");
130
+ });
131
+
132
+ test("native user message skips custom boxing for markdown text over the character threshold", () => {
133
+ let originalRenderCount = 0;
134
+ const theme = createCountingTheme("huge-text");
135
+ const originalLines = ["huge body fallback"];
136
+ const instance = markdownUserMessageInstance("x".repeat(100_001));
137
+ const prototype: PatchableUserMessagePrototype = {
138
+ render(width: number) {
139
+ originalRenderCount++;
140
+ assert.equal(width, 80, "oversized messages should bypass native boxing before content-width shrinking");
141
+ return originalLines;
142
+ },
143
+ };
144
+
145
+ patchNativeUserMessagePrototype(prototype, () => theme, () => true);
146
+
147
+ const rendered = prototype.render.call(instance, 80);
148
+
149
+ assert.equal(originalRenderCount, 1);
150
+ assert.equal(rendered, originalLines, "oversized markdown text should return the original render output without custom boxing");
151
+ assert.equal(renderLineCount(rendered, "│"), 0, "oversized markdown text should avoid per-line custom box work");
152
+ assert.equal(theme.calls.length, 0, "oversized markdown text should not spend time coloring native border lines");
153
+ });
154
+
155
+ test("native user message skips custom boxing for markdown text over the line-count threshold", () => {
156
+ let originalRenderCount = 0;
157
+ const theme = createCountingTheme("huge-lines");
158
+ const originalLines = ["line-count fallback"];
159
+ const instance = markdownUserMessageInstance(`${"line\n".repeat(2000)}tail`);
160
+ const prototype: PatchableUserMessagePrototype = {
161
+ render(width: number) {
162
+ originalRenderCount++;
163
+ assert.equal(width, 72, "line-heavy messages should bypass native boxing before content-width shrinking");
164
+ return originalLines;
165
+ },
166
+ };
167
+
168
+ patchNativeUserMessagePrototype(prototype, () => theme, () => true);
169
+
170
+ const rendered = prototype.render.call(instance, 72);
171
+
172
+ assert.equal(originalRenderCount, 1);
173
+ assert.equal(rendered, originalLines, "line-heavy markdown should return the original render output without custom boxing");
174
+ assert.equal(renderLineCount(rendered, "│"), 0, "line-heavy markdown should avoid per-line custom box work");
175
+ assert.equal(theme.calls.length, 0, "line-heavy markdown should not spend time coloring native border lines");
176
+ });