@dungle-scrubs/tallow 0.9.3 → 0.9.6

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 (207) hide show
  1. package/dist/cli.js +7 -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 +24 -10
  6. package/dist/interactive-mode-patch.d.ts.map +1 -1
  7. package/dist/interactive-mode-patch.js +285 -148
  8. package/dist/interactive-mode-patch.js.map +1 -1
  9. package/dist/interactive-reset.d.ts +49 -0
  10. package/dist/interactive-reset.d.ts.map +1 -0
  11. package/dist/interactive-reset.js +40 -0
  12. package/dist/interactive-reset.js.map +1 -0
  13. package/dist/pi-tui-editor-patch.d.ts +10 -0
  14. package/dist/pi-tui-editor-patch.d.ts.map +1 -0
  15. package/dist/pi-tui-editor-patch.js +159 -0
  16. package/dist/pi-tui-editor-patch.js.map +1 -0
  17. package/dist/pi-tui-patch.d.ts +2 -0
  18. package/dist/pi-tui-patch.d.ts.map +1 -0
  19. package/dist/pi-tui-patch.js +563 -0
  20. package/dist/pi-tui-patch.js.map +1 -0
  21. package/dist/pi-tui-settings-list-patch.d.ts +11 -0
  22. package/dist/pi-tui-settings-list-patch.d.ts.map +1 -0
  23. package/dist/pi-tui-settings-list-patch.js +38 -0
  24. package/dist/pi-tui-settings-list-patch.js.map +1 -0
  25. package/dist/reset-diagnostics.d.ts +69 -0
  26. package/dist/reset-diagnostics.d.ts.map +1 -0
  27. package/dist/reset-diagnostics.js +41 -0
  28. package/dist/reset-diagnostics.js.map +1 -0
  29. package/dist/sdk.d.ts +5 -21
  30. package/dist/sdk.d.ts.map +1 -1
  31. package/dist/sdk.js +180 -149
  32. package/dist/sdk.js.map +1 -1
  33. package/dist/workspace-transition-interactive.d.ts +1 -0
  34. package/dist/workspace-transition-interactive.d.ts.map +1 -1
  35. package/dist/workspace-transition-interactive.js +7 -17
  36. package/dist/workspace-transition-interactive.js.map +1 -1
  37. package/extensions/__integration__/audit-findings.test.ts +6 -16
  38. package/extensions/__integration__/teams-runtime.test.ts +4 -1
  39. package/extensions/_icons/index.ts +2 -4
  40. package/extensions/_shared/__tests__/image-metadata.test.ts +33 -0
  41. package/extensions/_shared/__tests__/terminal-links.test.ts +18 -0
  42. package/extensions/_shared/image-metadata.ts +99 -0
  43. package/extensions/_shared/inline-preview.ts +1 -1
  44. package/extensions/_shared/pid-registry.ts +0 -1
  45. package/extensions/_shared/terminal-links.ts +22 -0
  46. package/extensions/ask-user-question-tool/index.ts +0 -3
  47. package/extensions/clear/__tests__/clear.test.ts +270 -3
  48. package/extensions/command-expansion/index.ts +1 -1
  49. package/extensions/context-files/index.ts +5 -1
  50. package/extensions/context-fork/__tests__/context-fork.test.ts +94 -1
  51. package/extensions/context-fork/extension.json +1 -1
  52. package/extensions/context-fork/index.ts +32 -0
  53. package/extensions/edit-tool-enhanced/index.ts +2 -1
  54. package/extensions/hooks/index.ts +33 -11
  55. package/extensions/loop/index.ts +14 -1
  56. package/extensions/lsp/index.ts +64 -13
  57. package/extensions/lsp/package.json +2 -2
  58. package/extensions/permissions/__tests__/permissions.test.ts +4 -4
  59. package/extensions/random-spinner/index.ts +7 -642
  60. package/extensions/read-tool-enhanced/index.ts +6 -8
  61. package/extensions/render-stabilizer/__tests__/render-stabilizer.test.ts +4 -5
  62. package/extensions/render-stabilizer/index.ts +6 -6
  63. package/extensions/show-system-prompt/__tests__/show-system-prompt.test.ts +1 -1
  64. package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +26 -0
  65. package/extensions/slash-command-bridge/index.ts +14 -2
  66. package/extensions/subagent-tool/index.ts +1 -1
  67. package/extensions/subagent-tool/model-resolver.ts +274 -7
  68. package/extensions/tasks/__tests__/state-ui.test.ts +3 -3
  69. package/extensions/tasks/__tests__/widget-subagents.test.ts +2 -2
  70. package/extensions/tasks/commands/register-tasks-extension.ts +10 -10
  71. package/extensions/tasks/state/index.ts +1 -1
  72. package/extensions/tasks/ui/index.ts +2 -7
  73. package/extensions/teams-tool/tools/register-extension.ts +1 -3
  74. package/extensions/web-search-tool/index.ts +2 -1
  75. package/extensions/write-tool-enhanced/__tests__/write-tool-enhanced.test.ts +21 -6
  76. package/extensions/write-tool-enhanced/index.ts +2 -1
  77. package/node_modules/@mariozechner/pi-tui/README.md +56 -34
  78. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts +18 -13
  79. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts.map +1 -1
  80. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js +182 -113
  81. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js.map +1 -1
  82. package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js +3 -3
  83. package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js.map +1 -1
  84. package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts +45 -36
  85. package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts.map +1 -1
  86. package/node_modules/@mariozechner/pi-tui/dist/components/editor.js +489 -325
  87. package/node_modules/@mariozechner/pi-tui/dist/components/editor.js.map +1 -1
  88. package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts +1 -99
  89. package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts.map +1 -1
  90. package/node_modules/@mariozechner/pi-tui/dist/components/image.js +17 -192
  91. package/node_modules/@mariozechner/pi-tui/dist/components/image.js.map +1 -1
  92. package/node_modules/@mariozechner/pi-tui/dist/components/input.d.ts.map +1 -1
  93. package/node_modules/@mariozechner/pi-tui/dist/components/input.js +57 -60
  94. package/node_modules/@mariozechner/pi-tui/dist/components/input.js.map +1 -1
  95. package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts +2 -69
  96. package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts.map +1 -1
  97. package/node_modules/@mariozechner/pi-tui/dist/components/loader.js +5 -102
  98. package/node_modules/@mariozechner/pi-tui/dist/components/loader.js.map +1 -1
  99. package/node_modules/@mariozechner/pi-tui/dist/components/markdown.d.ts.map +1 -1
  100. package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js +111 -53
  101. package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js.map +1 -1
  102. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts +19 -1
  103. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts.map +1 -1
  104. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js +78 -67
  105. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js.map +1 -1
  106. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts +0 -2
  107. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts.map +1 -1
  108. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js +12 -23
  109. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js.map +1 -1
  110. package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +8 -10
  111. package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
  112. package/node_modules/@mariozechner/pi-tui/dist/index.js +6 -9
  113. package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
  114. package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +108 -238
  115. package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
  116. package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +108 -365
  117. package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
  118. package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +33 -48
  119. package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
  120. package/node_modules/@mariozechner/pi-tui/dist/keys.js +239 -155
  121. package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
  122. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts +14 -94
  123. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts.map +1 -1
  124. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js +44 -186
  125. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js.map +1 -1
  126. package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +13 -58
  127. package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
  128. package/node_modules/@mariozechner/pi-tui/dist/terminal.js +78 -111
  129. package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
  130. package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +24 -110
  131. package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
  132. package/node_modules/@mariozechner/pi-tui/dist/tui.js +188 -435
  133. package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
  134. package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts +0 -18
  135. package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts.map +1 -1
  136. package/node_modules/@mariozechner/pi-tui/dist/utils.js +251 -119
  137. package/node_modules/@mariozechner/pi-tui/dist/utils.js.map +1 -1
  138. package/node_modules/@mariozechner/pi-tui/package.json +6 -6
  139. package/node_modules/@mariozechner/pi-tui/src/__tests__/__snapshots__/render.test.ts.snap +3 -40
  140. package/node_modules/@mariozechner/pi-tui/src/__tests__/image-component.test.ts +71 -81
  141. package/node_modules/@mariozechner/pi-tui/src/__tests__/render.test.ts +0 -33
  142. package/node_modules/@mariozechner/pi-tui/src/__tests__/terminal-image.test.ts +93 -334
  143. package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +1 -1
  144. package/node_modules/@mariozechner/pi-tui/src/__tests__/utils.test.ts +11 -196
  145. package/node_modules/@mariozechner/pi-tui/src/autocomplete.ts +228 -142
  146. package/node_modules/@mariozechner/pi-tui/src/components/cancellable-loader.ts +3 -3
  147. package/node_modules/@mariozechner/pi-tui/src/components/editor.ts +624 -390
  148. package/node_modules/@mariozechner/pi-tui/src/components/image.ts +17 -227
  149. package/node_modules/@mariozechner/pi-tui/src/components/input.ts +71 -63
  150. package/node_modules/@mariozechner/pi-tui/src/components/loader.ts +5 -137
  151. package/node_modules/@mariozechner/pi-tui/src/components/markdown.ts +143 -52
  152. package/node_modules/@mariozechner/pi-tui/src/components/select-list.ts +136 -70
  153. package/node_modules/@mariozechner/pi-tui/src/components/settings-list.ts +11 -23
  154. package/node_modules/@mariozechner/pi-tui/src/index.ts +17 -36
  155. package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +148 -421
  156. package/node_modules/@mariozechner/pi-tui/src/keys.ts +253 -181
  157. package/node_modules/@mariozechner/pi-tui/src/terminal-image.ts +51 -252
  158. package/node_modules/@mariozechner/pi-tui/src/terminal.ts +78 -133
  159. package/node_modules/@mariozechner/pi-tui/src/tui.ts +202 -478
  160. package/node_modules/@mariozechner/pi-tui/src/utils.ts +289 -125
  161. package/node_modules/@mariozechner/pi-tui/tsconfig.build.json +1 -0
  162. package/package.json +13 -13
  163. package/packages/tallow-tui/node_modules/@types/mime-types/README.md +8 -2
  164. package/packages/tallow-tui/node_modules/@types/mime-types/index.d.ts +6 -0
  165. package/packages/tallow-tui/node_modules/@types/mime-types/package.json +9 -3
  166. package/packages/tallow-tui/node_modules/get-east-asian-width/lookup-data.js +18 -0
  167. package/packages/tallow-tui/node_modules/get-east-asian-width/lookup.js +116 -384
  168. package/packages/tallow-tui/node_modules/get-east-asian-width/package.json +5 -4
  169. package/packages/tallow-tui/node_modules/get-east-asian-width/utilities.js +24 -0
  170. package/packages/tallow-tui/node_modules/marked/README.md +5 -4
  171. package/packages/tallow-tui/node_modules/marked/bin/main.js +10 -8
  172. package/packages/tallow-tui/node_modules/marked/bin/marked.js +2 -1
  173. package/packages/tallow-tui/node_modules/marked/lib/marked.d.ts +156 -125
  174. package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js +67 -2179
  175. package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js.map +3 -3
  176. package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js +67 -2201
  177. package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js.map +3 -3
  178. package/packages/tallow-tui/node_modules/marked/man/marked.1 +4 -2
  179. package/packages/tallow-tui/node_modules/marked/man/marked.1.md +2 -1
  180. package/packages/tallow-tui/node_modules/marked/package.json +26 -34
  181. package/runtime/model-metadata-overrides.ts +10 -1
  182. package/runtime/pid-schema.ts +26 -6
  183. package/skills/tallow-expert/SKILL.md +1 -3
  184. package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts +0 -32
  185. package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts.map +0 -1
  186. package/node_modules/@mariozechner/pi-tui/dist/border-styles.js +0 -46
  187. package/node_modules/@mariozechner/pi-tui/dist/border-styles.js.map +0 -1
  188. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts +0 -52
  189. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts.map +0 -1
  190. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js +0 -89
  191. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js.map +0 -1
  192. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts +0 -14
  193. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts.map +0 -1
  194. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js +0 -55
  195. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js.map +0 -1
  196. package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-change-listener.test.ts +0 -121
  197. package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-ghost-text.test.ts +0 -112
  198. package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +0 -134
  199. package/node_modules/@mariozechner/pi-tui/src/__tests__/settings-list.test.ts +0 -49
  200. package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +0 -555
  201. package/node_modules/@mariozechner/pi-tui/src/border-styles.ts +0 -60
  202. package/node_modules/@mariozechner/pi-tui/src/components/bordered-box.ts +0 -113
  203. package/node_modules/@mariozechner/pi-tui/src/test-utils/capability-env.ts +0 -56
  204. package/packages/tallow-tui/node_modules/marked/lib/marked.cjs +0 -2211
  205. package/packages/tallow-tui/node_modules/marked/lib/marked.cjs.map +0 -7
  206. package/packages/tallow-tui/node_modules/marked/lib/marked.d.cts +0 -728
  207. package/packages/tallow-tui/node_modules/marked/marked.min.js +0 -69
@@ -1,12 +1,3 @@
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
1
  import {
11
2
  getCapabilities,
12
3
  getImageDimensions,
@@ -15,24 +6,6 @@ import {
15
6
  renderImage,
16
7
  } from "../terminal-image.js";
17
8
  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
9
 
37
10
  export interface ImageTheme {
38
11
  fallbackColor: (str: string) => string;
@@ -44,54 +17,8 @@ export interface ImageOptions {
44
17
  filename?: string;
45
18
  /** Kitty image ID. If provided, reuses this ID (for animations/updates). */
46
19
  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
20
  }
84
21
 
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
22
  export class Image implements Component {
96
23
  private base64Data: string;
97
24
  private mimeType: string;
@@ -117,46 +44,24 @@ export class Image implements Component {
117
44
  this.dimensions = dimensions ||
118
45
  getImageDimensions(base64Data, mimeType) || { widthPx: 800, heightPx: 600 };
119
46
  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
47
  }
127
48
 
128
- /**
129
- * Get the Kitty image ID used by this image (if any).
130
- * @returns Image ID or undefined
131
- */
49
+ /** Get the Kitty image ID used by this image (if any). */
132
50
  getImageId(): number | undefined {
133
51
  return this.imageId;
134
52
  }
135
53
 
136
- /** Clears cached render output so the next render() recomputes. */
137
54
  invalidate(): void {
138
55
  this.cachedLines = undefined;
139
56
  this.cachedWidth = undefined;
140
57
  }
141
58
 
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
59
  render(width: number): string[] {
149
60
  if (this.cachedLines && this.cachedWidth === width) {
150
61
  return this.cachedLines;
151
62
  }
152
63
 
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();
64
+ const maxWidth = Math.min(width - 2, this.options.maxWidthCells ?? 60);
160
65
 
161
66
  const caps = getCapabilities();
162
67
  let lines: string[];
@@ -164,23 +69,32 @@ export class Image implements Component {
164
69
  if (caps.images) {
165
70
  const result = renderImage(this.base64Data, this.dimensions, {
166
71
  maxWidthCells: maxWidth,
167
- maxHeightCells: maxHeight,
168
72
  imageId: this.imageId,
169
73
  });
170
74
 
171
75
  if (result) {
76
+ // Store the image ID for later cleanup
172
77
  if (result.imageId) {
173
78
  this.imageId = result.imageId;
174
79
  }
175
80
 
176
- lines = showBorder
177
- ? this.buildBorderedImage(result.sequence, result.rows, result.columns)
178
- : this.buildUnborderedImage(result.sequence, result.rows, result.columns);
81
+ // Return `rows` lines so TUI accounts for image height
82
+ // First (rows-1) lines are empty (TUI clears them)
83
+ // Last line: move cursor back up, then output image sequence
84
+ lines = [];
85
+ for (let i = 0; i < result.rows - 1; i++) {
86
+ lines.push("");
87
+ }
88
+ // Move cursor up to first row, then output image
89
+ const moveUp = result.rows > 1 ? `\x1b[${result.rows - 1}A` : "";
90
+ lines.push(moveUp + result.sequence);
179
91
  } else {
180
- lines = this.buildFallback(showBorder);
92
+ const fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);
93
+ lines = [this.theme.fallbackColor(fallback)];
181
94
  }
182
95
  } else {
183
- lines = this.buildFallback(showBorder);
96
+ const fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);
97
+ lines = [this.theme.fallbackColor(fallback)];
184
98
  }
185
99
 
186
100
  this.cachedLines = lines;
@@ -188,128 +102,4 @@ export class Image implements Component {
188
102
 
189
103
  return lines;
190
104
  }
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
105
  }
@@ -1,8 +1,15 @@
1
- import { getEditorKeybindings } from "../keybindings.js";
1
+ import { getKeybindings } from "../keybindings.js";
2
+ import { decodeKittyPrintable } from "../keys.js";
2
3
  import { KillRing } from "../kill-ring.js";
3
4
  import { type Component, CURSOR_MARKER, type Focusable } from "../tui.js";
4
5
  import { UndoStack } from "../undo-stack.js";
5
- import { getSegmenter, isPunctuationChar, isWhitespaceChar, visibleWidth } from "../utils.js";
6
+ import {
7
+ getSegmenter,
8
+ isPunctuationChar,
9
+ isWhitespaceChar,
10
+ sliceByColumn,
11
+ visibleWidth,
12
+ } from "../utils.js";
6
13
 
7
14
  const segmenter = getSegmenter();
8
15
 
@@ -81,69 +88,69 @@ export class Input implements Component, Focusable {
81
88
  return;
82
89
  }
83
90
 
84
- const kb = getEditorKeybindings();
91
+ const kb = getKeybindings();
85
92
 
86
93
  // Escape/Cancel
87
- if (kb.matches(data, "selectCancel")) {
94
+ if (kb.matches(data, "tui.select.cancel")) {
88
95
  if (this.onEscape) this.onEscape();
89
96
  return;
90
97
  }
91
98
 
92
99
  // Undo
93
- if (kb.matches(data, "undo")) {
100
+ if (kb.matches(data, "tui.editor.undo")) {
94
101
  this.undo();
95
102
  return;
96
103
  }
97
104
 
98
105
  // Submit
99
- if (kb.matches(data, "submit") || data === "\n") {
106
+ if (kb.matches(data, "tui.input.submit") || data === "\n") {
100
107
  if (this.onSubmit) this.onSubmit(this.value);
101
108
  return;
102
109
  }
103
110
 
104
111
  // Deletion
105
- if (kb.matches(data, "deleteCharBackward")) {
112
+ if (kb.matches(data, "tui.editor.deleteCharBackward")) {
106
113
  this.handleBackspace();
107
114
  return;
108
115
  }
109
116
 
110
- if (kb.matches(data, "deleteCharForward")) {
117
+ if (kb.matches(data, "tui.editor.deleteCharForward")) {
111
118
  this.handleForwardDelete();
112
119
  return;
113
120
  }
114
121
 
115
- if (kb.matches(data, "deleteWordBackward")) {
122
+ if (kb.matches(data, "tui.editor.deleteWordBackward")) {
116
123
  this.deleteWordBackwards();
117
124
  return;
118
125
  }
119
126
 
120
- if (kb.matches(data, "deleteWordForward")) {
127
+ if (kb.matches(data, "tui.editor.deleteWordForward")) {
121
128
  this.deleteWordForward();
122
129
  return;
123
130
  }
124
131
 
125
- if (kb.matches(data, "deleteToLineStart")) {
132
+ if (kb.matches(data, "tui.editor.deleteToLineStart")) {
126
133
  this.deleteToLineStart();
127
134
  return;
128
135
  }
129
136
 
130
- if (kb.matches(data, "deleteToLineEnd")) {
137
+ if (kb.matches(data, "tui.editor.deleteToLineEnd")) {
131
138
  this.deleteToLineEnd();
132
139
  return;
133
140
  }
134
141
 
135
142
  // Kill ring actions
136
- if (kb.matches(data, "yank")) {
143
+ if (kb.matches(data, "tui.editor.yank")) {
137
144
  this.yank();
138
145
  return;
139
146
  }
140
- if (kb.matches(data, "yankPop")) {
147
+ if (kb.matches(data, "tui.editor.yankPop")) {
141
148
  this.yankPop();
142
149
  return;
143
150
  }
144
151
 
145
152
  // Cursor movement
146
- if (kb.matches(data, "cursorLeft")) {
153
+ if (kb.matches(data, "tui.editor.cursorLeft")) {
147
154
  this.lastAction = null;
148
155
  if (this.cursor > 0) {
149
156
  const beforeCursor = this.value.slice(0, this.cursor);
@@ -154,7 +161,7 @@ export class Input implements Component, Focusable {
154
161
  return;
155
162
  }
156
163
 
157
- if (kb.matches(data, "cursorRight")) {
164
+ if (kb.matches(data, "tui.editor.cursorRight")) {
158
165
  this.lastAction = null;
159
166
  if (this.cursor < this.value.length) {
160
167
  const afterCursor = this.value.slice(this.cursor);
@@ -165,28 +172,38 @@ export class Input implements Component, Focusable {
165
172
  return;
166
173
  }
167
174
 
168
- if (kb.matches(data, "cursorLineStart")) {
175
+ if (kb.matches(data, "tui.editor.cursorLineStart")) {
169
176
  this.lastAction = null;
170
177
  this.cursor = 0;
171
178
  return;
172
179
  }
173
180
 
174
- if (kb.matches(data, "cursorLineEnd")) {
181
+ if (kb.matches(data, "tui.editor.cursorLineEnd")) {
175
182
  this.lastAction = null;
176
183
  this.cursor = this.value.length;
177
184
  return;
178
185
  }
179
186
 
180
- if (kb.matches(data, "cursorWordLeft")) {
187
+ if (kb.matches(data, "tui.editor.cursorWordLeft")) {
181
188
  this.moveWordBackwards();
182
189
  return;
183
190
  }
184
191
 
185
- if (kb.matches(data, "cursorWordRight")) {
192
+ if (kb.matches(data, "tui.editor.cursorWordRight")) {
186
193
  this.moveWordForwards();
187
194
  return;
188
195
  }
189
196
 
197
+ // Kitty CSI-u printable character (e.g. \x1b[97u for 'a').
198
+ // Terminals with Kitty protocol flag 1 (disambiguate) send CSI-u for all keys,
199
+ // including plain printable characters. Decode before the control-char check
200
+ // since CSI-u sequences contain \x1b which would be rejected.
201
+ const kittyPrintable = decodeKittyPrintable(data);
202
+ if (kittyPrintable !== undefined) {
203
+ this.insertCharacter(kittyPrintable);
204
+ return;
205
+ }
206
+
190
207
  // Regular character input - accept printable characters including Unicode,
191
208
  // but reject control characters (C0: 0x00-0x1F, DEL: 0x7F, C1: 0x80-0x9F)
192
209
  const hasControlChars = [...data].some((ch) => {
@@ -421,7 +438,11 @@ export class Input implements Component, Focusable {
421
438
  this.pushUndo();
422
439
 
423
440
  // Clean the pasted text - remove newlines and carriage returns
424
- const cleanText = pastedText.replace(/\r\n/g, "").replace(/\r/g, "").replace(/\n/g, "");
441
+ const cleanText = pastedText
442
+ .replace(/\r\n/g, "")
443
+ .replace(/\r/g, "")
444
+ .replace(/\n/g, "")
445
+ .replace(/\t/g, " ");
425
446
 
426
447
  // Insert at cursor position
427
448
  this.value = this.value.slice(0, this.cursor) + cleanText + this.value.slice(this.cursor);
@@ -443,56 +464,43 @@ export class Input implements Component, Focusable {
443
464
 
444
465
  let visibleText = "";
445
466
  let cursorDisplay = this.cursor;
467
+ const totalWidth = visibleWidth(this.value);
446
468
 
447
- if (this.value.length < availableWidth) {
469
+ if (totalWidth < availableWidth) {
448
470
  // Everything fits (leave room for cursor at end)
449
471
  visibleText = this.value;
450
472
  } else {
451
473
  // Need horizontal scrolling
452
- // Reserve one character for cursor if it's at the end
474
+ // Reserve one column for cursor if it's at the end
453
475
  const scrollWidth = this.cursor === this.value.length ? availableWidth - 1 : availableWidth;
454
- const halfWidth = Math.floor(scrollWidth / 2);
455
-
456
- const findValidStart = (start: number) => {
457
- while (start < this.value.length) {
458
- const charCode = this.value.charCodeAt(start);
459
- // this is low surrogate, not a valid start
460
- if (charCode >= 0xdc00 && charCode < 0xe000) {
461
- start++;
462
- continue;
463
- }
464
- break;
476
+ const cursorCol = visibleWidth(this.value.slice(0, this.cursor));
477
+
478
+ if (scrollWidth > 0) {
479
+ const halfWidth = Math.floor(scrollWidth / 2);
480
+ let startCol = 0;
481
+
482
+ if (cursorCol < halfWidth) {
483
+ // Cursor near start
484
+ startCol = 0;
485
+ } else if (cursorCol > totalWidth - halfWidth) {
486
+ // Cursor near end
487
+ startCol = Math.max(0, totalWidth - scrollWidth);
488
+ } else {
489
+ // Cursor in middle
490
+ startCol = Math.max(0, cursorCol - halfWidth);
465
491
  }
466
- return start;
467
- };
468
-
469
- const findValidEnd = (end: number) => {
470
- while (end > 0) {
471
- const charCode = this.value.charCodeAt(end - 1);
472
- // this is high surrogate, might be split.
473
- if (charCode >= 0xd800 && charCode < 0xdc00) {
474
- end--;
475
- continue;
476
- }
477
- break;
478
- }
479
- return end;
480
- };
481
-
482
- if (this.cursor < halfWidth) {
483
- // Cursor near start
484
- visibleText = this.value.slice(0, findValidEnd(scrollWidth));
485
- cursorDisplay = this.cursor;
486
- } else if (this.cursor > this.value.length - halfWidth) {
487
- // Cursor near end
488
- const start = findValidStart(this.value.length - scrollWidth);
489
- visibleText = this.value.slice(start);
490
- cursorDisplay = this.cursor - start;
492
+
493
+ visibleText = sliceByColumn(this.value, startCol, scrollWidth, true);
494
+ const beforeCursor = sliceByColumn(
495
+ this.value,
496
+ startCol,
497
+ Math.max(0, cursorCol - startCol),
498
+ true
499
+ );
500
+ cursorDisplay = beforeCursor.length;
491
501
  } else {
492
- // Cursor in middle
493
- const start = findValidStart(this.cursor - halfWidth);
494
- visibleText = this.value.slice(start, findValidEnd(start + scrollWidth));
495
- cursorDisplay = halfWidth;
502
+ visibleText = "";
503
+ cursorDisplay = 0;
496
504
  }
497
505
  }
498
506