@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,419 @@
1
+ import {
2
+ Markdown,
3
+ truncateToWidth,
4
+ visibleWidth,
5
+ type DefaultTextStyle,
6
+ type MarkdownTheme,
7
+ } from "@earendil-works/pi-tui";
8
+ import {
9
+ patchUserMessageRenderPrototype,
10
+ type PatchableUserMessagePrototype,
11
+ } from "./user-message-box-patch.js";
12
+ import {
13
+ extractUserMessageMarkdownState,
14
+ type UserMessageMarkdownState,
15
+ } from "./user-message-box-markdown.js";
16
+
17
+ export type { PatchableUserMessagePrototype } from "./user-message-box-patch.js";
18
+ import {
19
+ addUserMessageVerticalPadding,
20
+ applyUserMessageBackground,
21
+ normalizeUserMessageContentLine,
22
+ normalizeUserMessageContentLines,
23
+ type UserMessageBackgroundTheme,
24
+ } from "./user-message-box-utils.js";
25
+
26
+ export interface UserMessageTheme extends UserMessageBackgroundTheme {
27
+ fg(color: string, text: string): string;
28
+ bold?(text: string): string;
29
+ }
30
+
31
+ interface CachedUserMessageMarkdownRenderer {
32
+ text: string;
33
+ theme: unknown;
34
+ defaultTextStyle?: Record<string, unknown>;
35
+ renderer: { render(width: number): string[] };
36
+ renderedWidth: number;
37
+ renderedLines: string[];
38
+ }
39
+
40
+ interface CachedUserMessageFinalOutput {
41
+ width: number;
42
+ theme: UserMessageTheme | undefined;
43
+ hasMarkdownState: boolean;
44
+ text?: string;
45
+ markdownTheme?: unknown;
46
+ defaultTextStyle?: Record<string, unknown>;
47
+ output: string[];
48
+ }
49
+
50
+ interface CachedUserMessageBodyLines {
51
+ width: number;
52
+ lines: string[];
53
+ }
54
+
55
+ const MIN_BORDER_WIDTH = 8;
56
+ const TITLE_TEXT = " user ";
57
+ const CONTENT_HORIZONTAL_PADDING_COLUMNS = 1;
58
+ const USER_MESSAGE_TOP_MARGIN_LINES = 1;
59
+ const USER_MESSAGE_PATCH_VERSION = 8;
60
+ const MAX_USER_MESSAGE_MARKDOWN_TEXT_LENGTH = 100_000;
61
+ const MAX_USER_MESSAGE_MARKDOWN_LINE_COUNT = 2_000;
62
+
63
+ function colorBorder(theme: UserMessageTheme | undefined, text: string): string {
64
+ if (!text || !theme) {
65
+ return text;
66
+ }
67
+
68
+ try {
69
+ return theme.fg("border", text);
70
+ } catch {
71
+ return text;
72
+ }
73
+ }
74
+
75
+ function colorTitle(theme: UserMessageTheme | undefined, title: string): string {
76
+ if (!title) {
77
+ return title;
78
+ }
79
+
80
+ const base = theme?.bold ? theme.bold(title) : title;
81
+ if (!theme) {
82
+ return base;
83
+ }
84
+
85
+ try {
86
+ return theme.fg("accent", base);
87
+ } catch {
88
+ return base;
89
+ }
90
+ }
91
+
92
+ function colorUserBackground(
93
+ theme: UserMessageTheme | undefined,
94
+ text: string,
95
+ ): string {
96
+ return applyUserMessageBackground(theme, text);
97
+ }
98
+
99
+ function computeBoxInnerWidth(totalWidth: number): number {
100
+ return Math.max(0, totalWidth - 2);
101
+ }
102
+
103
+ function buildTopBorder(
104
+ totalWidth: number,
105
+ theme: UserMessageTheme | undefined,
106
+ ): string {
107
+ const innerWidth = computeBoxInnerWidth(totalWidth);
108
+ const title = truncateToWidth(TITLE_TEXT, innerWidth, "");
109
+ const fill = "─".repeat(Math.max(0, innerWidth - visibleWidth(title)));
110
+ const row = `${colorBorder(theme, "╭")}${colorTitle(theme, title)}${colorBorder(theme, `${fill}╮`)}`;
111
+
112
+ return colorUserBackground(theme, row);
113
+ }
114
+
115
+ function buildBottomBorder(
116
+ totalWidth: number,
117
+ theme: UserMessageTheme | undefined,
118
+ ): string {
119
+ const innerWidth = computeBoxInnerWidth(totalWidth);
120
+ const row = `${colorBorder(theme, "╰")}${colorBorder(theme, `${"─".repeat(innerWidth)}╯`)}`;
121
+
122
+ return colorUserBackground(theme, row);
123
+ }
124
+
125
+ function getUserMessageContentWidth(totalWidth: number): number {
126
+ return Math.max(
127
+ 1,
128
+ totalWidth - 2 - CONTENT_HORIZONTAL_PADDING_COLUMNS * 2,
129
+ );
130
+ }
131
+
132
+ function wrapContentLine(
133
+ line: string,
134
+ totalWidth: number,
135
+ theme: UserMessageTheme | undefined,
136
+ ): string {
137
+ const sidePadding = " ".repeat(CONTENT_HORIZONTAL_PADDING_COLUMNS);
138
+ const innerWidth = getUserMessageContentWidth(totalWidth);
139
+ const normalizedLine = normalizeUserMessageContentLine(line);
140
+ const content = truncateToWidth(normalizedLine, innerWidth, "", true);
141
+ const padding = " ".repeat(Math.max(0, innerWidth - visibleWidth(content)));
142
+ const row = `${colorBorder(theme, "│")}${sidePadding}${content}${padding}${sidePadding}${colorBorder(theme, "│")}`;
143
+
144
+ return colorUserBackground(theme, row);
145
+ }
146
+
147
+ function createMarkdownRenderer(
148
+ markdownState: UserMessageMarkdownState,
149
+ ): { render(width: number): string[] } {
150
+ return new Markdown(
151
+ markdownState.text,
152
+ 0,
153
+ 0,
154
+ markdownState.theme as MarkdownTheme,
155
+ markdownState.defaultTextStyle as DefaultTextStyle | undefined,
156
+ );
157
+ }
158
+
159
+ function countUserMessageLines(text: string, maxLines: number): number {
160
+ let lineCount = 1;
161
+ for (const character of text) {
162
+ if (character !== "\n") {
163
+ continue;
164
+ }
165
+
166
+ lineCount++;
167
+ if (lineCount > maxLines) {
168
+ return lineCount;
169
+ }
170
+ }
171
+
172
+ return lineCount;
173
+ }
174
+
175
+ function hasSameDefaultTextStyle(
176
+ left: Record<string, unknown> | undefined,
177
+ right: Record<string, unknown> | undefined,
178
+ ): boolean {
179
+ if (left === right) {
180
+ return true;
181
+ }
182
+ if (!left || !right) {
183
+ return left === right;
184
+ }
185
+
186
+ const leftKeys = Object.keys(left);
187
+ const rightKeys = Object.keys(right);
188
+ if (leftKeys.length !== rightKeys.length) {
189
+ return false;
190
+ }
191
+
192
+ for (const key of leftKeys) {
193
+ if (left[key] !== right[key]) {
194
+ return false;
195
+ }
196
+ }
197
+
198
+ return true;
199
+ }
200
+
201
+ function hasSameMarkdownState(
202
+ cached: Pick<CachedUserMessageMarkdownRenderer, "text" | "theme" | "defaultTextStyle">,
203
+ state: UserMessageMarkdownState,
204
+ ): boolean {
205
+ return cached.text === state.text
206
+ && cached.theme === state.theme
207
+ && hasSameDefaultTextStyle(cached.defaultTextStyle, state.defaultTextStyle);
208
+ }
209
+
210
+ function hasSameFinalOutputState(
211
+ cached: CachedUserMessageFinalOutput,
212
+ width: number,
213
+ theme: UserMessageTheme | undefined,
214
+ markdownState: UserMessageMarkdownState | undefined,
215
+ ): boolean {
216
+ if (cached.width !== width || cached.theme !== theme) {
217
+ return false;
218
+ }
219
+
220
+ if (!markdownState) {
221
+ return !cached.hasMarkdownState;
222
+ }
223
+
224
+ return cached.hasMarkdownState
225
+ && hasSameMarkdownState(
226
+ {
227
+ text: cached.text ?? "",
228
+ theme: cached.markdownTheme,
229
+ defaultTextStyle: cached.defaultTextStyle,
230
+ },
231
+ markdownState,
232
+ );
233
+ }
234
+
235
+ function toFinalOutputCacheEntry(
236
+ width: number,
237
+ theme: UserMessageTheme | undefined,
238
+ markdownState: UserMessageMarkdownState | undefined,
239
+ output: string[],
240
+ ): CachedUserMessageFinalOutput {
241
+ if (!markdownState) {
242
+ return {
243
+ width,
244
+ theme,
245
+ hasMarkdownState: false,
246
+ output,
247
+ };
248
+ }
249
+
250
+ return {
251
+ width,
252
+ theme,
253
+ hasMarkdownState: true,
254
+ text: markdownState.text,
255
+ markdownTheme: markdownState.theme,
256
+ defaultTextStyle: markdownState.defaultTextStyle,
257
+ output,
258
+ };
259
+ }
260
+
261
+ export function shouldBypassUserMessageMarkdownRebuild(
262
+ markdownState: UserMessageMarkdownState,
263
+ ): boolean {
264
+ if (markdownState.text.length > MAX_USER_MESSAGE_MARKDOWN_TEXT_LENGTH) {
265
+ return true;
266
+ }
267
+
268
+ return countUserMessageLines(
269
+ markdownState.text,
270
+ MAX_USER_MESSAGE_MARKDOWN_LINE_COUNT,
271
+ ) > MAX_USER_MESSAGE_MARKDOWN_LINE_COUNT;
272
+ }
273
+
274
+ export function createUserMessageMarkdownLineRenderer(
275
+ buildRenderer: (
276
+ markdownState: UserMessageMarkdownState,
277
+ ) => { render(width: number): string[] } = createMarkdownRenderer,
278
+ ): (
279
+ instance: object,
280
+ markdownState: UserMessageMarkdownState,
281
+ width: number,
282
+ ) => string[] {
283
+ const cache = new WeakMap<object, CachedUserMessageMarkdownRenderer>();
284
+
285
+ return (instance, markdownState, width) => {
286
+ const cached = cache.get(instance);
287
+ const canReuseRenderer = cached
288
+ ? hasSameMarkdownState(cached, markdownState)
289
+ : false;
290
+
291
+ if (canReuseRenderer && cached?.renderedWidth === width) {
292
+ return cached.renderedLines;
293
+ }
294
+
295
+ const renderer = canReuseRenderer && cached
296
+ ? cached.renderer
297
+ : buildRenderer(markdownState);
298
+ const renderedLines = renderer.render(width);
299
+
300
+ cache.set(instance, {
301
+ text: markdownState.text,
302
+ theme: markdownState.theme,
303
+ defaultTextStyle: markdownState.defaultTextStyle,
304
+ renderer,
305
+ renderedWidth: width,
306
+ renderedLines,
307
+ });
308
+
309
+ return renderedLines;
310
+ };
311
+ }
312
+
313
+ const renderCachedUserMessageMarkdownLines =
314
+ createUserMessageMarkdownLineRenderer();
315
+
316
+ function renderUserMessageBodyLines(
317
+ instance: unknown,
318
+ innerWidth: number,
319
+ originalRender: (width: number) => string[],
320
+ markdownState: UserMessageMarkdownState | undefined,
321
+ originalBodyLineCache?: WeakMap<object, CachedUserMessageBodyLines>,
322
+ ): string[] {
323
+ if (typeof instance !== "object" || instance === null) {
324
+ return originalRender.call(instance, innerWidth) as string[];
325
+ }
326
+
327
+ if (!markdownState) {
328
+ const cached = originalBodyLineCache?.get(instance);
329
+ if (cached?.width === innerWidth) {
330
+ return cached.lines;
331
+ }
332
+
333
+ const lines = originalRender.call(instance, innerWidth) as string[];
334
+ originalBodyLineCache?.set(instance, { width: innerWidth, lines });
335
+ return lines;
336
+ }
337
+
338
+ if (shouldBypassUserMessageMarkdownRebuild(markdownState)) {
339
+ return originalRender.call(instance, innerWidth) as string[];
340
+ }
341
+
342
+ try {
343
+ return renderCachedUserMessageMarkdownLines(
344
+ instance,
345
+ markdownState,
346
+ innerWidth,
347
+ );
348
+ } catch {
349
+ return originalRender.call(instance, innerWidth) as string[];
350
+ }
351
+ }
352
+
353
+ export function patchNativeUserMessagePrototype(
354
+ prototype: PatchableUserMessagePrototype,
355
+ getTheme: () => UserMessageTheme | undefined,
356
+ isEnabled: () => boolean,
357
+ ): void {
358
+ const finalOutputCache = new WeakMap<object, CachedUserMessageFinalOutput>();
359
+ const originalBodyLineCache = new WeakMap<object, CachedUserMessageBodyLines>();
360
+
361
+ patchUserMessageRenderPrototype(
362
+ prototype,
363
+ USER_MESSAGE_PATCH_VERSION,
364
+ (originalRender) =>
365
+ function renderWithNativeUserBorder(width: number): string[] {
366
+ const safeWidth = Math.max(0, Math.floor(width));
367
+ if (!isEnabled() || safeWidth < MIN_BORDER_WIDTH) {
368
+ return originalRender.call(this, safeWidth) as string[];
369
+ }
370
+
371
+ const canCacheFinalOutput = typeof this === "object" && this !== null;
372
+ const markdownState = canCacheFinalOutput
373
+ ? extractUserMessageMarkdownState(this as { children?: unknown[] })
374
+ : undefined;
375
+ if (markdownState && shouldBypassUserMessageMarkdownRebuild(markdownState)) {
376
+ return originalRender.call(this, safeWidth) as string[];
377
+ }
378
+
379
+ const theme = getTheme();
380
+ if (canCacheFinalOutput) {
381
+ const cached = finalOutputCache.get(this as object);
382
+ if (cached && hasSameFinalOutputState(cached, safeWidth, theme, markdownState)) {
383
+ return cached.output;
384
+ }
385
+ }
386
+
387
+ const innerWidth = getUserMessageContentWidth(safeWidth);
388
+ const lines = renderUserMessageBodyLines(
389
+ this,
390
+ innerWidth,
391
+ originalRender,
392
+ markdownState,
393
+ originalBodyLineCache,
394
+ );
395
+ const contentLines = normalizeUserMessageContentLines(lines);
396
+ const paddedContentLines = addUserMessageVerticalPadding(
397
+ contentLines.length > 0 ? contentLines : [""],
398
+ );
399
+
400
+ const output = [
401
+ ...Array.from({ length: USER_MESSAGE_TOP_MARGIN_LINES }, () => ""),
402
+ buildTopBorder(safeWidth, theme),
403
+ ...paddedContentLines.map((renderLine) =>
404
+ wrapContentLine(renderLine, safeWidth, theme),
405
+ ),
406
+ buildBottomBorder(safeWidth, theme),
407
+ ];
408
+
409
+ if (canCacheFinalOutput) {
410
+ finalOutputCache.set(
411
+ this as object,
412
+ toFinalOutputCacheEntry(safeWidth, theme, markdownState, output),
413
+ );
414
+ }
415
+
416
+ return output;
417
+ },
418
+ );
419
+ }
@@ -0,0 +1,106 @@
1
+ import { ANSI_SGR_PATTERN, sanitizeAnsiForThemedOutput } from "./ansi-utils.js";
2
+
3
+ const OSC_PROMPT_CONTROL_SEQUENCE_PATTERN = /\x1b\](?:133|633);[A-Z](?:;[^\x07\x1b]*)?(?:\x07|\x1b\\)/g;
4
+ const USER_MESSAGE_BACKGROUND = "userMessageBg";
5
+ const ANSI_BG_RESET = "\x1b[49m";
6
+ const USER_MESSAGE_VERTICAL_PADDING_LINES = 1;
7
+
8
+ export interface UserMessageBackgroundTheme {
9
+ bg?(color: string, text: string): string;
10
+ getBgAnsi?(color: string): string;
11
+ }
12
+
13
+ function hasPromptControlOscSequence(text: string): boolean {
14
+ return text.includes("\x1b]133;") || text.includes("\x1b]633;");
15
+ }
16
+
17
+ function stripOscPromptControlSequences(text: string): string {
18
+ if (!text || !hasPromptControlOscSequence(text)) {
19
+ return text;
20
+ }
21
+
22
+ // Strip prompt-control OSC sequences only. OSC 8 hyperlinks are intentionally
23
+ // preserved because they carry renderable terminal hyperlink metadata.
24
+ return text.replace(OSC_PROMPT_CONTROL_SEQUENCE_PATTERN, "");
25
+ }
26
+
27
+ function sanitizeUserMessageAnsi(text: string): string {
28
+ return sanitizeAnsiForThemedOutput(stripOscPromptControlSequences(text));
29
+ }
30
+
31
+ export function applyUserMessageBackground(
32
+ theme: UserMessageBackgroundTheme | undefined,
33
+ text: string,
34
+ ): string {
35
+ if (!text) {
36
+ return text;
37
+ }
38
+
39
+ const sanitized = sanitizeUserMessageAnsi(text);
40
+ if (!theme) {
41
+ return sanitized;
42
+ }
43
+
44
+ try {
45
+ if (typeof theme.getBgAnsi === "function") {
46
+ return `${theme.getBgAnsi(USER_MESSAGE_BACKGROUND)}${sanitized}${ANSI_BG_RESET}`;
47
+ }
48
+ } catch (themeError) {
49
+ void themeError;
50
+ }
51
+
52
+ try {
53
+ if (typeof theme.bg === "function") {
54
+ return theme.bg(USER_MESSAGE_BACKGROUND, sanitized);
55
+ }
56
+ } catch (themeError) {
57
+ void themeError;
58
+ }
59
+
60
+ return sanitized;
61
+ }
62
+
63
+ function isVisuallyEmptyLine(line: string): boolean {
64
+ const withoutControlSequences = stripOscPromptControlSequences(line)
65
+ .replace(ANSI_SGR_PATTERN, "");
66
+ return withoutControlSequences.trim().length === 0;
67
+ }
68
+
69
+ function trimEdgePadding(lines: string[]): string[] {
70
+ let start = 0;
71
+ while (start < lines.length && isVisuallyEmptyLine(lines[start] ?? "")) {
72
+ start++;
73
+ }
74
+
75
+ let end = lines.length;
76
+ while (end > start && isVisuallyEmptyLine(lines[end - 1] ?? "")) {
77
+ end--;
78
+ }
79
+
80
+ return lines.slice(start, end);
81
+ }
82
+
83
+ export function normalizeUserMessageContentLines(lines: string[]): string[] {
84
+ const normalizedLines = trimEdgePadding(lines);
85
+ if (normalizedLines.length === 0) {
86
+ return [];
87
+ }
88
+
89
+ return normalizedLines;
90
+ }
91
+
92
+ export function normalizeUserMessageContentLine(line: string): string {
93
+ if (isVisuallyEmptyLine(line)) {
94
+ return "";
95
+ }
96
+
97
+ return sanitizeUserMessageAnsi(line);
98
+ }
99
+
100
+ export function addUserMessageVerticalPadding(lines: string[]): string[] {
101
+ const padding = Array.from(
102
+ { length: USER_MESSAGE_VERTICAL_PADDING_LINES },
103
+ () => "",
104
+ );
105
+ return [...padding, ...lines, ...padding];
106
+ }
@@ -0,0 +1,31 @@
1
+ export interface WriteCallSummaryOptions {
2
+ hasContent: boolean;
3
+ hasDetailedResultHeader: boolean;
4
+ }
5
+
6
+ export function splitWriteContentLines(content: string): string[] {
7
+ if (!content) {
8
+ return [];
9
+ }
10
+
11
+ const normalized = content.replace(/\r/g, "");
12
+ const lines = normalized.split("\n");
13
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
14
+ lines.pop();
15
+ }
16
+ return lines;
17
+ }
18
+
19
+ export function countWriteContentLines(value: unknown): number {
20
+ return typeof value === "string" ? splitWriteContentLines(value).length : 0;
21
+ }
22
+
23
+ export function getWriteContentSizeBytes(value: unknown): number {
24
+ return typeof value === "string" ? Buffer.byteLength(value, "utf8") : 0;
25
+ }
26
+
27
+ export function shouldRenderWriteCallSummary(
28
+ options: WriteCallSummaryOptions,
29
+ ): boolean {
30
+ return options.hasContent && !options.hasDetailedResultHeader;
31
+ }