@dungle-scrubs/tallow 0.8.21 → 0.8.23

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 (217) hide show
  1. package/dist/cli.js +35 -4
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config.d.ts +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/interactive-mode-patch.d.ts +2 -0
  6. package/dist/interactive-mode-patch.d.ts.map +1 -1
  7. package/dist/interactive-mode-patch.js +82 -0
  8. package/dist/interactive-mode-patch.js.map +1 -1
  9. package/dist/sdk.d.ts +17 -0
  10. package/dist/sdk.d.ts.map +1 -1
  11. package/dist/sdk.js +68 -1
  12. package/dist/sdk.js.map +1 -1
  13. package/dist/workspace-transition-relay.d.ts +40 -7
  14. package/dist/workspace-transition-relay.d.ts.map +1 -1
  15. package/dist/workspace-transition-relay.js +81 -16
  16. package/dist/workspace-transition-relay.js.map +1 -1
  17. package/extensions/__integration__/background-task-widget-ownership.test.ts +216 -0
  18. package/extensions/__integration__/claude-hooks-compat.test.ts +156 -0
  19. package/extensions/__integration__/slash-command-bridge.test.ts +169 -23
  20. package/extensions/_shared/atomic-write.ts +1 -1
  21. package/extensions/_shared/bordered-box.ts +102 -0
  22. package/extensions/_shared/interop-events.ts +5 -0
  23. package/extensions/_shared/pid-registry.ts +1 -1
  24. package/extensions/agent-commands-tool/index.ts +4 -1
  25. package/extensions/background-task-tool/__tests__/lifecycle.test.ts +50 -25
  26. package/extensions/background-task-tool/index.ts +139 -221
  27. package/extensions/bash-tool-enhanced/index.ts +1 -75
  28. package/extensions/cd-tool/index.ts +2 -2
  29. package/extensions/context-fork/spawn.ts +4 -1
  30. package/extensions/health/index.ts +6 -6
  31. package/extensions/hooks/__tests__/claude-compat.test.ts +35 -0
  32. package/extensions/hooks/__tests__/subprocess-hardening.test.ts +73 -0
  33. package/extensions/hooks/index.ts +27 -4
  34. package/extensions/loop/__tests__/loop.test.ts +168 -4
  35. package/extensions/loop/extension.json +6 -5
  36. package/extensions/loop/index.ts +242 -31
  37. package/extensions/plan-mode-tool/__tests__/agent-end-execution.test.ts +373 -0
  38. package/extensions/plan-mode-tool/index.ts +103 -41
  39. package/extensions/prompt-suggestions/__tests__/editor-compatibility.test.ts +42 -0
  40. package/extensions/prompt-suggestions/index.ts +41 -6
  41. package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +267 -671
  42. package/extensions/slash-command-bridge/extension.json +1 -1
  43. package/extensions/slash-command-bridge/index.ts +230 -116
  44. package/extensions/subagent-tool/index.ts +2 -2
  45. package/extensions/subagent-tool/process.ts +4 -5
  46. package/extensions/tasks/commands/register-tasks-extension.ts +41 -0
  47. package/extensions/teams-tool/__tests__/peer-messaging.test.ts +29 -24
  48. package/extensions/teams-tool/dashboard.ts +3 -5
  49. package/extensions/teams-tool/dispatch/auto-dispatch.ts +18 -1
  50. package/extensions/teams-tool/tools/teammate-tools.ts +9 -6
  51. package/extensions/wezterm-pane-control/__tests__/index.test.ts +88 -4
  52. package/extensions/wezterm-pane-control/index.ts +113 -8
  53. package/package.json +6 -4
  54. package/packages/tallow-tui/README.md +51 -0
  55. package/packages/tallow-tui/dist/autocomplete.d.ts +48 -0
  56. package/packages/tallow-tui/dist/autocomplete.d.ts.map +1 -0
  57. package/packages/tallow-tui/dist/autocomplete.js +564 -0
  58. package/packages/tallow-tui/dist/autocomplete.js.map +1 -0
  59. package/packages/tallow-tui/dist/border-styles.d.ts +32 -0
  60. package/packages/tallow-tui/dist/border-styles.d.ts.map +1 -0
  61. package/packages/tallow-tui/dist/border-styles.js +46 -0
  62. package/packages/tallow-tui/dist/border-styles.js.map +1 -0
  63. package/packages/tallow-tui/dist/components/bordered-box.d.ts +52 -0
  64. package/packages/tallow-tui/dist/components/bordered-box.d.ts.map +1 -0
  65. package/packages/tallow-tui/dist/components/bordered-box.js +89 -0
  66. package/packages/tallow-tui/dist/components/bordered-box.js.map +1 -0
  67. package/packages/tallow-tui/dist/components/box.d.ts +22 -0
  68. package/packages/tallow-tui/dist/components/box.d.ts.map +1 -0
  69. package/packages/tallow-tui/dist/components/box.js +104 -0
  70. package/packages/tallow-tui/dist/components/box.js.map +1 -0
  71. package/packages/tallow-tui/dist/components/cancellable-loader.d.ts +22 -0
  72. package/packages/tallow-tui/dist/components/cancellable-loader.d.ts.map +1 -0
  73. package/packages/tallow-tui/dist/components/cancellable-loader.js +35 -0
  74. package/packages/tallow-tui/dist/components/cancellable-loader.js.map +1 -0
  75. package/packages/tallow-tui/dist/components/editor.d.ts +240 -0
  76. package/packages/tallow-tui/dist/components/editor.d.ts.map +1 -0
  77. package/packages/tallow-tui/dist/components/editor.js +1766 -0
  78. package/packages/tallow-tui/dist/components/editor.js.map +1 -0
  79. package/packages/tallow-tui/dist/components/image.d.ts +126 -0
  80. package/packages/tallow-tui/dist/components/image.d.ts.map +1 -0
  81. package/packages/tallow-tui/dist/components/image.js +245 -0
  82. package/packages/tallow-tui/dist/components/image.js.map +1 -0
  83. package/packages/tallow-tui/dist/components/input.d.ts +37 -0
  84. package/packages/tallow-tui/dist/components/input.d.ts.map +1 -0
  85. package/packages/tallow-tui/dist/components/input.js +439 -0
  86. package/packages/tallow-tui/dist/components/input.js.map +1 -0
  87. package/packages/tallow-tui/dist/components/loader.d.ts +88 -0
  88. package/packages/tallow-tui/dist/components/loader.d.ts.map +1 -0
  89. package/packages/tallow-tui/dist/components/loader.js +146 -0
  90. package/packages/tallow-tui/dist/components/loader.js.map +1 -0
  91. package/packages/tallow-tui/dist/components/markdown.d.ts +95 -0
  92. package/packages/tallow-tui/dist/components/markdown.d.ts.map +1 -0
  93. package/packages/tallow-tui/dist/components/markdown.js +633 -0
  94. package/packages/tallow-tui/dist/components/markdown.js.map +1 -0
  95. package/packages/tallow-tui/dist/components/select-list.d.ts +32 -0
  96. package/packages/tallow-tui/dist/components/select-list.d.ts.map +1 -0
  97. package/packages/tallow-tui/dist/components/select-list.js +156 -0
  98. package/packages/tallow-tui/dist/components/select-list.js.map +1 -0
  99. package/packages/tallow-tui/dist/components/settings-list.d.ts +50 -0
  100. package/packages/tallow-tui/dist/components/settings-list.d.ts.map +1 -0
  101. package/packages/tallow-tui/dist/components/settings-list.js +189 -0
  102. package/packages/tallow-tui/dist/components/settings-list.js.map +1 -0
  103. package/packages/tallow-tui/dist/components/spacer.d.ts +12 -0
  104. package/packages/tallow-tui/dist/components/spacer.d.ts.map +1 -0
  105. package/packages/tallow-tui/dist/components/spacer.js +23 -0
  106. package/packages/tallow-tui/dist/components/spacer.js.map +1 -0
  107. package/packages/tallow-tui/dist/components/text.d.ts +19 -0
  108. package/packages/tallow-tui/dist/components/text.d.ts.map +1 -0
  109. package/packages/tallow-tui/dist/components/text.js +91 -0
  110. package/packages/tallow-tui/dist/components/text.js.map +1 -0
  111. package/packages/tallow-tui/dist/components/truncated-text.d.ts +13 -0
  112. package/packages/tallow-tui/dist/components/truncated-text.d.ts.map +1 -0
  113. package/packages/tallow-tui/dist/components/truncated-text.js +51 -0
  114. package/packages/tallow-tui/dist/components/truncated-text.js.map +1 -0
  115. package/packages/tallow-tui/dist/editor-component.d.ts +50 -0
  116. package/packages/tallow-tui/dist/editor-component.d.ts.map +1 -0
  117. package/packages/tallow-tui/dist/editor-component.js +2 -0
  118. package/packages/tallow-tui/dist/editor-component.js.map +1 -0
  119. package/packages/tallow-tui/dist/fuzzy.d.ts +16 -0
  120. package/packages/tallow-tui/dist/fuzzy.d.ts.map +1 -0
  121. package/packages/tallow-tui/dist/fuzzy.js +107 -0
  122. package/packages/tallow-tui/dist/fuzzy.js.map +1 -0
  123. package/packages/tallow-tui/dist/index.d.ts +25 -0
  124. package/packages/tallow-tui/dist/index.d.ts.map +1 -0
  125. package/packages/tallow-tui/dist/index.js +35 -0
  126. package/packages/tallow-tui/dist/index.js.map +1 -0
  127. package/packages/tallow-tui/dist/keybindings.d.ts +39 -0
  128. package/packages/tallow-tui/dist/keybindings.d.ts.map +1 -0
  129. package/packages/tallow-tui/dist/keybindings.js +114 -0
  130. package/packages/tallow-tui/dist/keybindings.js.map +1 -0
  131. package/packages/tallow-tui/dist/keys.d.ts +168 -0
  132. package/packages/tallow-tui/dist/keys.d.ts.map +1 -0
  133. package/packages/tallow-tui/dist/keys.js +971 -0
  134. package/packages/tallow-tui/dist/keys.js.map +1 -0
  135. package/packages/tallow-tui/dist/kill-ring.d.ts +28 -0
  136. package/packages/tallow-tui/dist/kill-ring.d.ts.map +1 -0
  137. package/packages/tallow-tui/dist/kill-ring.js +44 -0
  138. package/packages/tallow-tui/dist/kill-ring.js.map +1 -0
  139. package/packages/tallow-tui/dist/stdin-buffer.d.ts +48 -0
  140. package/packages/tallow-tui/dist/stdin-buffer.d.ts.map +1 -0
  141. package/packages/tallow-tui/dist/stdin-buffer.js +317 -0
  142. package/packages/tallow-tui/dist/stdin-buffer.js.map +1 -0
  143. package/packages/tallow-tui/dist/terminal-image.d.ts +161 -0
  144. package/packages/tallow-tui/dist/terminal-image.d.ts.map +1 -0
  145. package/packages/tallow-tui/dist/terminal-image.js +460 -0
  146. package/packages/tallow-tui/dist/terminal-image.js.map +1 -0
  147. package/packages/tallow-tui/dist/terminal.d.ts +102 -0
  148. package/packages/tallow-tui/dist/terminal.d.ts.map +1 -0
  149. package/packages/tallow-tui/dist/terminal.js +263 -0
  150. package/packages/tallow-tui/dist/terminal.js.map +1 -0
  151. package/packages/tallow-tui/dist/test-utils/capability-env.d.ts +14 -0
  152. package/packages/tallow-tui/dist/test-utils/capability-env.d.ts.map +1 -0
  153. package/packages/tallow-tui/dist/test-utils/capability-env.js +55 -0
  154. package/packages/tallow-tui/dist/test-utils/capability-env.js.map +1 -0
  155. package/packages/tallow-tui/dist/tui.d.ts +239 -0
  156. package/packages/tallow-tui/dist/tui.d.ts.map +1 -0
  157. package/packages/tallow-tui/dist/tui.js +1058 -0
  158. package/packages/tallow-tui/dist/tui.js.map +1 -0
  159. package/packages/tallow-tui/dist/undo-stack.d.ts +17 -0
  160. package/packages/tallow-tui/dist/undo-stack.d.ts.map +1 -0
  161. package/packages/tallow-tui/dist/undo-stack.js +25 -0
  162. package/packages/tallow-tui/dist/undo-stack.js.map +1 -0
  163. package/packages/tallow-tui/dist/utils.d.ts +96 -0
  164. package/packages/tallow-tui/dist/utils.d.ts.map +1 -0
  165. package/packages/tallow-tui/dist/utils.js +843 -0
  166. package/packages/tallow-tui/dist/utils.js.map +1 -0
  167. package/packages/tallow-tui/package.json +24 -0
  168. package/packages/tallow-tui/src/__tests__/__snapshots__/render.test.ts.snap +121 -0
  169. package/packages/tallow-tui/src/__tests__/editor-border.test.ts +72 -0
  170. package/packages/tallow-tui/src/__tests__/editor-change-listener.test.ts +121 -0
  171. package/packages/tallow-tui/src/__tests__/editor-ghost-text.test.ts +112 -0
  172. package/packages/tallow-tui/src/__tests__/fuzzy.test.ts +91 -0
  173. package/packages/tallow-tui/src/__tests__/image-component.test.ts +113 -0
  174. package/packages/tallow-tui/src/__tests__/keys.test.ts +141 -0
  175. package/packages/tallow-tui/src/__tests__/render.test.ts +179 -0
  176. package/packages/tallow-tui/src/__tests__/stdin-buffer.test.ts +82 -0
  177. package/packages/tallow-tui/src/__tests__/terminal-image.test.ts +363 -0
  178. package/packages/tallow-tui/src/__tests__/tui-diff-regression.test.ts +454 -0
  179. package/packages/tallow-tui/src/__tests__/tui-render-scheduling.test.ts +256 -0
  180. package/packages/tallow-tui/src/__tests__/utils.test.ts +259 -0
  181. package/packages/tallow-tui/src/autocomplete.ts +716 -0
  182. package/packages/tallow-tui/src/border-styles.ts +60 -0
  183. package/packages/tallow-tui/src/components/bordered-box.ts +113 -0
  184. package/packages/tallow-tui/src/components/box.ts +137 -0
  185. package/packages/tallow-tui/src/components/cancellable-loader.ts +40 -0
  186. package/packages/tallow-tui/src/components/editor.ts +2143 -0
  187. package/packages/tallow-tui/src/components/image.ts +315 -0
  188. package/packages/tallow-tui/src/components/input.ts +522 -0
  189. package/packages/tallow-tui/src/components/loader.ts +187 -0
  190. package/packages/tallow-tui/src/components/markdown.ts +780 -0
  191. package/packages/tallow-tui/src/components/select-list.ts +197 -0
  192. package/packages/tallow-tui/src/components/settings-list.ts +264 -0
  193. package/packages/tallow-tui/src/components/spacer.ts +28 -0
  194. package/packages/tallow-tui/src/components/text.ts +113 -0
  195. package/packages/tallow-tui/src/components/truncated-text.ts +65 -0
  196. package/packages/tallow-tui/src/editor-component.ts +92 -0
  197. package/packages/tallow-tui/src/fuzzy.ts +133 -0
  198. package/packages/tallow-tui/src/index.ts +118 -0
  199. package/packages/tallow-tui/src/keybindings.ts +183 -0
  200. package/packages/tallow-tui/src/keys.ts +1189 -0
  201. package/packages/tallow-tui/src/kill-ring.ts +46 -0
  202. package/packages/tallow-tui/src/stdin-buffer.ts +386 -0
  203. package/packages/tallow-tui/src/terminal-image.ts +619 -0
  204. package/packages/tallow-tui/src/terminal.ts +350 -0
  205. package/packages/tallow-tui/src/test-utils/capability-env.ts +56 -0
  206. package/packages/tallow-tui/src/tui.ts +1336 -0
  207. package/packages/tallow-tui/src/undo-stack.ts +28 -0
  208. package/packages/tallow-tui/src/utils.ts +948 -0
  209. package/packages/tallow-tui/tsconfig.build.json +21 -0
  210. package/runtime/agent-runner.ts +20 -0
  211. package/runtime/atomic-write.ts +8 -0
  212. package/runtime/otel.ts +12 -0
  213. package/runtime/resolve-module.ts +23 -0
  214. package/runtime/runtime-path-provider.ts +12 -0
  215. package/runtime/runtime-provenance.ts +17 -0
  216. package/runtime/workspace-transition-relay.ts +21 -0
  217. package/runtime/workspace-transition.ts +29 -0
@@ -0,0 +1,315 @@
1
+ /**
2
+ * Image component for the TUI.
3
+ * Renders images via Kitty/iTerm2 protocols with optional border framing.
4
+ * Preserves aspect ratio and avoids small-image upscaling.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ import { type BorderStyle, ROUNDED } from "../border-styles.js";
10
+ import {
11
+ getCapabilities,
12
+ getImageDimensions,
13
+ type ImageDimensions,
14
+ imageFallback,
15
+ renderImage,
16
+ } from "../terminal-image.js";
17
+ import type { Component } from "../tui.js";
18
+ import { hyperlink } from "../utils.js";
19
+
20
+ /**
21
+ * Pending file path for the next Image instance.
22
+ * Set by external code (e.g. a tool_result hook) before Image construction.
23
+ * Consumed once by the next Image constructor, then cleared.
24
+ */
25
+ let pendingFilePath: string | undefined;
26
+
27
+ /**
28
+ * Set the file path for the next Image instance to be constructed.
29
+ * Called before Image creation so the component can render a clickable link.
30
+ *
31
+ * @param path - Absolute file path, or undefined to clear
32
+ */
33
+ export function setNextImageFilePath(path: string | undefined): void {
34
+ pendingFilePath = path;
35
+ }
36
+
37
+ export interface ImageTheme {
38
+ fallbackColor: (str: string) => string;
39
+ }
40
+
41
+ export interface ImageOptions {
42
+ maxWidthCells?: number;
43
+ maxHeightCells?: number;
44
+ filename?: string;
45
+ /** Kitty image ID. If provided, reuses this ID (for animations/updates). */
46
+ imageId?: number;
47
+ /** Show a border around the image. Default: true. */
48
+ border?: boolean;
49
+ /** Border style. Default: ROUNDED. */
50
+ borderStyle?: BorderStyle;
51
+ /** Color function for border characters. */
52
+ borderColorFn?: (str: string) => string;
53
+ /** Absolute file path — enables a clickable OSC 8 file:// link below the image. */
54
+ filePath?: string;
55
+ }
56
+
57
+ /** Default fraction of terminal rows available for images when no explicit maxHeightCells is set. */
58
+ const DEFAULT_MAX_HEIGHT_RATIO = 0.9;
59
+
60
+ /** Minimum dynamic max height when terminal row count is available. */
61
+ const MIN_DYNAMIC_MAX_HEIGHT_CELLS = 10;
62
+
63
+ /** Border overhead: │ + space on each side = 4 columns. */
64
+ const BORDER_OVERHEAD = 4;
65
+
66
+ /**
67
+ * Calculate a dynamic default max image height from terminal size.
68
+ *
69
+ * Uses a high percentage of the current terminal height to avoid unnecessary
70
+ * downscaling while still preventing images from consuming the full viewport.
71
+ * Returns undefined when terminal row count is unavailable, which disables the
72
+ * height cap unless explicitly provided via options.maxHeightCells.
73
+ *
74
+ * @returns Dynamic max height in terminal rows, or undefined
75
+ */
76
+ function getDefaultMaxHeightCells(): number | undefined {
77
+ const terminalRows = process.stdout.rows;
78
+ if (!terminalRows || terminalRows <= 0) return undefined;
79
+ return Math.max(
80
+ MIN_DYNAMIC_MAX_HEIGHT_CELLS,
81
+ Math.floor(terminalRows * DEFAULT_MAX_HEIGHT_RATIO)
82
+ );
83
+ }
84
+
85
+ /**
86
+ * TUI component that renders an inline image using terminal graphics protocols.
87
+ * Falls back to a text placeholder when the terminal lacks image support.
88
+ *
89
+ * Features:
90
+ * - Dynamic height cap (~90% of terminal rows) by default (overridable via maxHeightCells)
91
+ * - Small images not upscaled beyond native pixel width
92
+ * - Rounded border frame by default (configurable or disableable)
93
+ * - Kitty warping fix: omits r= param so terminal auto-calculates rows
94
+ */
95
+ export class Image implements Component {
96
+ private base64Data: string;
97
+ private mimeType: string;
98
+ private dimensions: ImageDimensions;
99
+ private theme: ImageTheme;
100
+ private options: ImageOptions;
101
+ private imageId?: number;
102
+
103
+ private cachedLines?: string[];
104
+ private cachedWidth?: number;
105
+
106
+ constructor(
107
+ base64Data: string,
108
+ mimeType: string,
109
+ theme: ImageTheme,
110
+ options: ImageOptions = {},
111
+ dimensions?: ImageDimensions
112
+ ) {
113
+ this.base64Data = base64Data;
114
+ this.mimeType = mimeType;
115
+ this.theme = theme;
116
+ this.options = options;
117
+ this.dimensions = dimensions ||
118
+ getImageDimensions(base64Data, mimeType) || { widthPx: 800, heightPx: 600 };
119
+ this.imageId = options.imageId;
120
+
121
+ // Auto-consume pending file path if not explicitly provided
122
+ if (!this.options.filePath && pendingFilePath) {
123
+ this.options = { ...this.options, filePath: pendingFilePath };
124
+ pendingFilePath = undefined;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Get the Kitty image ID used by this image (if any).
130
+ * @returns Image ID or undefined
131
+ */
132
+ getImageId(): number | undefined {
133
+ return this.imageId;
134
+ }
135
+
136
+ /** Clears cached render output so the next render() recomputes. */
137
+ invalidate(): void {
138
+ this.cachedLines = undefined;
139
+ this.cachedWidth = undefined;
140
+ }
141
+
142
+ /**
143
+ * Render the image into terminal lines.
144
+ *
145
+ * @param width - Available terminal width in columns
146
+ * @returns Array of strings (lines) for the TUI to output
147
+ */
148
+ render(width: number): string[] {
149
+ if (this.cachedLines && this.cachedWidth === width) {
150
+ return this.cachedLines;
151
+ }
152
+
153
+ const showBorder = this.options.border === true;
154
+ // Ignore maxWidthCells from upstream — it's hardcoded to 60 in
155
+ // pi-coding-agent and squashes landscape images. Use the full
156
+ // available terminal width instead; height capping and natural-width
157
+ // clamping in calculateImageLayout prevent oversized output.
158
+ const maxWidth = this.getSafeMaxWidthCells(width, showBorder);
159
+ const maxHeight = this.options.maxHeightCells ?? getDefaultMaxHeightCells();
160
+
161
+ const caps = getCapabilities();
162
+ let lines: string[];
163
+
164
+ if (caps.images) {
165
+ const result = renderImage(this.base64Data, this.dimensions, {
166
+ maxWidthCells: maxWidth,
167
+ maxHeightCells: maxHeight,
168
+ imageId: this.imageId,
169
+ });
170
+
171
+ if (result) {
172
+ if (result.imageId) {
173
+ this.imageId = result.imageId;
174
+ }
175
+
176
+ lines = showBorder
177
+ ? this.buildBorderedImage(result.sequence, result.rows, result.columns)
178
+ : this.buildUnborderedImage(result.sequence, result.rows, result.columns);
179
+ } else {
180
+ lines = this.buildFallback(showBorder);
181
+ }
182
+ } else {
183
+ lines = this.buildFallback(showBorder);
184
+ }
185
+
186
+ this.cachedLines = lines;
187
+ this.cachedWidth = width;
188
+
189
+ return lines;
190
+ }
191
+
192
+ /**
193
+ * Resolve a safe image width budget from the current pane width.
194
+ *
195
+ * Keeps width math deterministic for ultra-narrow panes by enforcing a
196
+ * minimum of 1 cell after reserved spacing and optional border overhead.
197
+ *
198
+ * @param width - Current pane width in terminal columns
199
+ * @param showBorder - Whether bordered rendering is enabled
200
+ * @returns Positive max width in cells for `renderImage`
201
+ */
202
+ private getSafeMaxWidthCells(width: number, showBorder: boolean): number {
203
+ const borderCols = showBorder ? BORDER_OVERHEAD : 0;
204
+ const availableWidth = Math.floor(width) - 2 - borderCols;
205
+ return Math.max(1, availableWidth);
206
+ }
207
+
208
+ /**
209
+ * Wraps visible text in an OSC 8 file:// hyperlink if filePath is set.
210
+ * Returns the text unchanged when no filePath is configured.
211
+ *
212
+ * @param text - Visible content to wrap (spaces, border chars, etc.)
213
+ * @returns Text optionally wrapped in OSC 8 escape sequences
214
+ */
215
+ private wrapFileLink(text: string): string {
216
+ if (!this.options.filePath) return text;
217
+ return hyperlink(`file://${encodeURI(this.options.filePath)}`, text);
218
+ }
219
+
220
+ /**
221
+ * Builds image output without a border (original behavior).
222
+ * First N-1 lines are filled with OSC 8–wrapped spaces (when filePath
223
+ * is set) so the image area is a clickable link in the text layer.
224
+ * Last line moves cursor up and outputs the image escape sequence.
225
+ *
226
+ * @param sequence - Terminal escape sequence for the image
227
+ * @param rows - Number of terminal rows the image occupies
228
+ * @param columns - Number of terminal columns the image occupies
229
+ * @returns Lines array for the TUI
230
+ */
231
+ private buildUnborderedImage(sequence: string, rows: number, columns: number): string[] {
232
+ const lines: string[] = [];
233
+ const filler = this.wrapFileLink(" ".repeat(columns));
234
+ for (let i = 0; i < rows - 1; i++) {
235
+ lines.push(filler);
236
+ }
237
+ const moveUp = rows > 1 ? `\x1b[${rows - 1}A` : "";
238
+ lines.push(`${this.wrapFileLink(" ".repeat(columns))}${moveUp}\x1b[${columns}D${sequence}`);
239
+ return lines;
240
+ }
241
+
242
+ /**
243
+ * Builds image output wrapped in a border.
244
+ * Border characters occupy the text layer; the image fills the inner
245
+ * cell range via the graphics layer (Kitty/iTerm2).
246
+ *
247
+ * The last content line outputs a full bordered row (text layer), then
248
+ * repositions the cursor back to the first content row and places the
249
+ * image sequence. The graphics layer draws over the inner spaces while
250
+ * border characters at the edges remain visible.
251
+ *
252
+ * @param sequence - Terminal escape sequence for the image
253
+ * @param rows - Number of terminal rows the image occupies
254
+ * @param columns - Number of terminal columns the image occupies
255
+ * @returns Lines array for the TUI
256
+ */
257
+ private buildBorderedImage(sequence: string, rows: number, columns: number): string[] {
258
+ const style = this.options.borderStyle ?? ROUNDED;
259
+ const colorFn = this.options.borderColorFn ?? ((s: string) => s);
260
+
261
+ const innerWidth = columns;
262
+ const totalWidth = innerWidth + BORDER_OVERHEAD;
263
+
264
+ const top = colorFn(style.topLeft + style.horizontal.repeat(totalWidth - 2) + style.topRight);
265
+ const bottom = colorFn(
266
+ style.bottomLeft + style.horizontal.repeat(totalWidth - 2) + style.bottomRight
267
+ );
268
+ const leftBorder = `${colorFn(style.vertical)} `;
269
+ const rightBorder = ` ${colorFn(style.vertical)}`;
270
+ const emptyInner = this.wrapFileLink(" ".repeat(innerWidth));
271
+ const borderedLine = leftBorder + emptyInner + rightBorder;
272
+
273
+ const lines: string[] = [top];
274
+
275
+ // Bordered empty lines — image fills the inner area via the graphics layer
276
+ for (let i = 0; i < rows - 1; i++) {
277
+ lines.push(borderedLine);
278
+ }
279
+
280
+ // Last content line: full bordered text, then reposition cursor for image placement.
281
+ // CUU (cursor up) + CUB (cursor backward) positions to column 2 of the first content row.
282
+ const moveUp = rows > 1 ? `\x1b[${rows - 1}A` : "";
283
+ const moveToImageStart = `\x1b[${totalWidth - 2}D`;
284
+ lines.push(borderedLine + moveUp + moveToImageStart + sequence);
285
+
286
+ lines.push(bottom);
287
+
288
+ return lines;
289
+ }
290
+
291
+ /**
292
+ * Builds fallback text when the terminal doesn't support images.
293
+ * Optionally wrapped in a border for visual consistency.
294
+ *
295
+ * @param showBorder - Whether to wrap the fallback in a border
296
+ * @returns Lines array for the TUI
297
+ */
298
+ private buildFallback(showBorder: boolean): string[] {
299
+ const fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);
300
+ const text = this.theme.fallbackColor(fallback);
301
+
302
+ if (showBorder) {
303
+ const style = this.options.borderStyle ?? ROUNDED;
304
+ const colorFn = this.options.borderColorFn ?? ((s: string) => s);
305
+ const innerWidth = fallback.length + 2; // 1 space padding each side
306
+ const top = colorFn(style.topLeft + style.horizontal.repeat(innerWidth) + style.topRight);
307
+ const bottom = colorFn(
308
+ style.bottomLeft + style.horizontal.repeat(innerWidth) + style.bottomRight
309
+ );
310
+ return [top, `${colorFn(style.vertical)} ${text} ${colorFn(style.vertical)}`, bottom];
311
+ }
312
+
313
+ return [text];
314
+ }
315
+ }