@visual-z/xeno 0.2.1 → 0.4.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 (282) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +126 -34
  3. package/docs/app-server.md +84 -0
  4. package/docs/install.md +51 -13
  5. package/docs/providers.md +24 -3
  6. package/package.json +7 -5
  7. package/src/apps/app-server.ts +28 -0
  8. package/src/apps/approve.ts +3 -3
  9. package/src/apps/cli.ts +25 -10
  10. package/src/apps/commands.ts +45 -8
  11. package/src/apps/completions.ts +78 -22
  12. package/src/apps/control-transport.ts +89 -0
  13. package/src/apps/image-draft.ts +46 -0
  14. package/src/apps/model-catalog.ts +2 -1
  15. package/src/apps/project-trust.ts +2 -42
  16. package/src/apps/provider-connect.ts +346 -0
  17. package/src/apps/render.ts +22 -47
  18. package/src/apps/resources.ts +5 -2
  19. package/src/apps/shell.ts +333 -1542
  20. package/src/apps/startup-input.ts +45 -9
  21. package/src/apps/startup.ts +149 -94
  22. package/src/apps/ui/app.ts +1553 -0
  23. package/src/apps/ui/control-model.ts +26 -0
  24. package/src/apps/ui/extension-host.ts +194 -0
  25. package/src/apps/ui/image-cache.ts +78 -0
  26. package/src/apps/ui/palette.ts +88 -0
  27. package/src/apps/ui/project.ts +127 -0
  28. package/src/apps/ui/standalone.ts +146 -0
  29. package/src/apps/ui/status-model.ts +68 -0
  30. package/src/apps/ui/surface.ts +86 -0
  31. package/src/apps/ui/theme.ts +86 -0
  32. package/src/core/acceptance.ts +107 -0
  33. package/src/core/action-defaults.ts +47 -295
  34. package/src/core/benchmark.ts +2 -2
  35. package/src/core/branch-receipt.ts +24 -0
  36. package/src/core/checkpoints.ts +64 -0
  37. package/src/core/churn.ts +2 -0
  38. package/src/core/command-artifacts.ts +94 -0
  39. package/src/core/command-contract.ts +35 -0
  40. package/src/core/completion-action.ts +20 -0
  41. package/src/core/completion-review.ts +37 -21
  42. package/src/core/context-detail.ts +37 -0
  43. package/src/core/context-policy.ts +74 -1
  44. package/src/core/continuation.ts +17 -2
  45. package/src/core/control-protocol.ts +62 -0
  46. package/src/core/control-view.ts +42 -0
  47. package/src/core/derive.ts +29 -3
  48. package/src/core/draft-images.ts +56 -0
  49. package/src/core/environment.ts +145 -0
  50. package/src/core/evaluation-coverage.ts +74 -0
  51. package/src/core/event-validation.ts +81 -6
  52. package/src/core/events.ts +99 -3
  53. package/src/core/evidence-links.ts +7 -0
  54. package/src/core/evidence.ts +13 -3
  55. package/src/core/execution.ts +8 -2
  56. package/src/core/experiment.ts +133 -0
  57. package/src/core/export.ts +9 -0
  58. package/src/core/failure-recovery.ts +173 -0
  59. package/src/core/file-precondition.ts +72 -0
  60. package/src/core/handoff.ts +65 -4
  61. package/src/core/harness-diagnostics.ts +206 -0
  62. package/src/core/history-query.ts +38 -2
  63. package/src/core/history.ts +38 -1
  64. package/src/core/integration-health.ts +34 -0
  65. package/src/core/keymap.ts +8 -20
  66. package/src/core/model-picker.ts +64 -0
  67. package/src/core/native-eval.ts +25 -0
  68. package/src/core/note-guidance.ts +34 -0
  69. package/src/core/oauth-fields.ts +44 -0
  70. package/src/core/observation-view.ts +49 -8
  71. package/src/core/permissions.ts +24 -2
  72. package/src/core/process-receipt.ts +105 -0
  73. package/src/core/process-state.ts +37 -0
  74. package/src/core/program-status.ts +31 -0
  75. package/src/core/project.ts +70 -19
  76. package/src/core/prompt-policy.ts +37 -0
  77. package/src/core/query-eval-contract.ts +14 -0
  78. package/src/core/read-observation.ts +82 -0
  79. package/src/core/recovery-corpus.ts +17 -0
  80. package/src/core/recovery-evaluation.ts +60 -0
  81. package/src/core/recovery-sources.ts +74 -0
  82. package/src/core/request-progress.ts +87 -0
  83. package/src/core/request-state.ts +114 -0
  84. package/src/core/runtime-target.ts +94 -0
  85. package/src/core/search-observation.ts +97 -0
  86. package/src/core/session-tree.ts +147 -0
  87. package/src/core/settings.ts +33 -10
  88. package/src/core/skills.ts +44 -0
  89. package/src/core/sql-query.ts +211 -0
  90. package/src/core/theme.ts +36 -58
  91. package/src/core/tool-contract.ts +51 -2
  92. package/src/core/tool-discovery.ts +87 -0
  93. package/src/core/tool-failure.ts +60 -0
  94. package/src/core/tool-input.ts +6 -0
  95. package/src/core/tool-mode-protocol.ts +25 -0
  96. package/src/core/transcript.ts +19 -4
  97. package/src/core/trial-lifecycle.ts +14 -0
  98. package/src/core/tui-mode.ts +29 -0
  99. package/src/core/update-check.ts +97 -0
  100. package/src/core/version.ts +55 -0
  101. package/src/core/work-state.ts +49 -3
  102. package/src/core/working-set.ts +91 -0
  103. package/src/runtime/auth.ts +11 -4
  104. package/src/runtime/branch-jobs.ts +36 -0
  105. package/src/runtime/catalog.ts +13 -3
  106. package/src/runtime/clipboard.ts +15 -4
  107. package/src/runtime/command-artifacts.ts +64 -0
  108. package/src/runtime/control-connection.ts +104 -0
  109. package/src/runtime/environment-tools.ts +60 -0
  110. package/src/runtime/evidence-notes.ts +4 -2
  111. package/src/runtime/exec-sessions.ts +206 -61
  112. package/src/runtime/execution-control.ts +1 -0
  113. package/src/runtime/extension-ui.ts +22 -30
  114. package/src/runtime/extensions.ts +95 -16
  115. package/src/runtime/harness-comparison.ts +27 -7
  116. package/src/runtime/harness-eval.ts +462 -0
  117. package/src/runtime/history-cache.ts +57 -0
  118. package/src/runtime/history-tools.ts +62 -12
  119. package/src/runtime/integrations.ts +210 -0
  120. package/src/runtime/loop.ts +1088 -282
  121. package/src/runtime/model-discovery.ts +208 -0
  122. package/src/runtime/model.ts +9 -0
  123. package/src/runtime/native-eval-fixtures.ts +33 -0
  124. package/src/runtime/native-eval.ts +146 -0
  125. package/src/runtime/oauth.ts +80 -3
  126. package/src/runtime/pi-provider.ts +169 -12
  127. package/src/runtime/program-status.ts +38 -0
  128. package/src/runtime/provider-connect.ts +109 -0
  129. package/src/runtime/provider-presets.ts +87 -0
  130. package/src/runtime/provider-request.ts +13 -2
  131. package/src/runtime/provider.ts +23 -9
  132. package/src/runtime/query-eval-verifier.ts +28 -0
  133. package/src/runtime/recovery-coverage.ts +71 -0
  134. package/src/runtime/recovery-evidence.ts +76 -0
  135. package/src/runtime/recovery-fixtures.ts +116 -0
  136. package/src/runtime/runtime-target.ts +149 -0
  137. package/src/runtime/session-control.ts +198 -0
  138. package/src/runtime/tool-broker.ts +137 -0
  139. package/src/runtime/tool-discovery.ts +57 -0
  140. package/src/runtime/tool-mode-eval.ts +146 -0
  141. package/src/runtime/tool-queue.ts +77 -0
  142. package/src/runtime/tools.ts +628 -183
  143. package/src/runtime/trial-control.ts +45 -0
  144. package/src/runtime/update-check.ts +47 -0
  145. package/src/runtime/window-trial.ts +2 -1
  146. package/src/runtime/working-notes.ts +33 -3
  147. package/src/runtime/workspace-lifecycle.ts +97 -0
  148. package/src/store/acceptance.ts +15 -0
  149. package/src/store/checkpoint-history.ts +43 -0
  150. package/src/store/command-artifacts.ts +96 -0
  151. package/src/store/command.ts +65 -37
  152. package/src/store/file-checkpoints.ts +104 -0
  153. package/src/store/file-index.ts +13 -1
  154. package/src/store/git-checkpoints.ts +67 -0
  155. package/src/store/observations.ts +61 -20
  156. package/src/store/process-receipts.ts +34 -0
  157. package/src/store/query-source.ts +13 -8
  158. package/src/store/recovery.ts +2 -3
  159. package/src/store/replay.ts +16 -4
  160. package/src/store/runtime-target.ts +64 -0
  161. package/src/store/search.ts +171 -132
  162. package/src/store/session-archive.ts +4 -0
  163. package/src/store/source-cancellation.ts +6 -0
  164. package/src/store/update-cache.ts +36 -0
  165. package/src/store/workspace-observation.ts +113 -0
  166. package/src/tui/ansi-spans.ts +126 -0
  167. package/src/tui/style.ts +19 -16
  168. package/src/tui/ui/atomic-draft.ts +121 -0
  169. package/src/tui/ui/canvas.ts +96 -0
  170. package/src/tui/ui/colors.ts +70 -0
  171. package/src/tui/ui/composer.ts +200 -0
  172. package/src/tui/ui/control-panel.ts +23 -0
  173. package/src/tui/ui/dialog.ts +379 -0
  174. package/src/tui/ui/draft.ts +23 -0
  175. package/src/tui/ui/edge.ts +186 -0
  176. package/src/tui/ui/image-size.ts +40 -0
  177. package/src/tui/ui/ink.ts +210 -0
  178. package/src/tui/ui/items.ts +40 -0
  179. package/src/tui/ui/keys.ts +40 -0
  180. package/src/tui/ui/layout.ts +74 -0
  181. package/src/tui/ui/list-model.ts +86 -0
  182. package/src/tui/ui/list-view.ts +172 -0
  183. package/src/tui/ui/mascot.ts +67 -0
  184. package/src/tui/ui/messages.ts +518 -0
  185. package/src/tui/ui/motion.ts +28 -0
  186. package/src/tui/ui/paths.ts +10 -0
  187. package/src/tui/ui/pictures.ts +78 -0
  188. package/src/tui/ui/screen.ts +421 -0
  189. package/src/tui/ui/status.ts +279 -0
  190. package/src/tui/ui/styled.ts +85 -0
  191. package/src/tui/ui/testing.ts +5 -0
  192. package/src/tui/ui/tool-card.ts +150 -0
  193. package/src/apps/extension-ui.ts +0 -330
  194. package/src/apps/resource-browser.ts +0 -75
  195. package/src/apps/session-browser.ts +0 -113
  196. package/src/apps/session-picker.ts +0 -55
  197. package/src/tui/ansi.ts +0 -67
  198. package/src/tui/autocomplete.ts +0 -29
  199. package/src/tui/chat-viewport.ts +0 -47
  200. package/src/tui/complete.ts +0 -87
  201. package/src/tui/editor.ts +0 -940
  202. package/src/tui/host.ts +0 -264
  203. package/src/tui/input.ts +0 -75
  204. package/src/tui/keys.ts +0 -378
  205. package/src/tui/markdown.ts +0 -17
  206. package/src/tui/render.ts +0 -153
  207. package/src/tui/select.ts +0 -258
  208. package/src/tui/settings.ts +0 -44
  209. package/src/tui/statusline.ts +0 -162
  210. package/src/tui/system-theme.ts +0 -700
  211. package/src/tui/terminal.ts +0 -30
  212. package/src/tui/text.ts +0 -52
  213. package/src/tui/toolview.ts +0 -386
  214. package/src/tui/transcript.ts +0 -123
  215. package/src/tui/tree.ts +0 -471
  216. package/src/vendor/pi-tui/LICENSE.upstream +0 -21
  217. package/src/vendor/pi-tui/UPSTREAM.json +0 -50
  218. package/src/vendor/pi-tui/VENDOR.md +0 -25
  219. package/src/vendor/pi-tui/alt-screen-search.ts +0 -327
  220. package/src/vendor/pi-tui/autocomplete.ts +0 -860
  221. package/src/vendor/pi-tui/colors.ts +0 -367
  222. package/src/vendor/pi-tui/components/alt-screen-flash.ts +0 -51
  223. package/src/vendor/pi-tui/components/box.ts +0 -166
  224. package/src/vendor/pi-tui/components/cancellable-loader.ts +0 -40
  225. package/src/vendor/pi-tui/components/editor.ts +0 -2472
  226. package/src/vendor/pi-tui/components/h-stack.ts +0 -44
  227. package/src/vendor/pi-tui/components/image.ts +0 -127
  228. package/src/vendor/pi-tui/components/input.ts +0 -494
  229. package/src/vendor/pi-tui/components/loader.ts +0 -101
  230. package/src/vendor/pi-tui/components/markdown.ts +0 -1021
  231. package/src/vendor/pi-tui/components/mouse-region.ts +0 -33
  232. package/src/vendor/pi-tui/components/scroll-view.ts +0 -224
  233. package/src/vendor/pi-tui/components/select-list.ts +0 -273
  234. package/src/vendor/pi-tui/components/settings-list.ts +0 -328
  235. package/src/vendor/pi-tui/components/spacer.ts +0 -28
  236. package/src/vendor/pi-tui/components/stack.ts +0 -154
  237. package/src/vendor/pi-tui/components/text.ts +0 -107
  238. package/src/vendor/pi-tui/components/truncated-text.ts +0 -65
  239. package/src/vendor/pi-tui/components/v-stack.ts +0 -33
  240. package/src/vendor/pi-tui/editor-component.ts +0 -74
  241. package/src/vendor/pi-tui/fuzzy.ts +0 -138
  242. package/src/vendor/pi-tui/index.ts +0 -186
  243. package/src/vendor/pi-tui/keybindings.ts +0 -320
  244. package/src/vendor/pi-tui/keys.ts +0 -1401
  245. package/src/vendor/pi-tui/kill-ring.ts +0 -46
  246. package/src/vendor/pi-tui/latex.ts +0 -1506
  247. package/src/vendor/pi-tui/layout-node.ts +0 -51
  248. package/src/vendor/pi-tui/layout.ts +0 -449
  249. package/src/vendor/pi-tui/native/UPSTREAM.json +0 -23
  250. package/src/vendor/pi-tui/native/clipboard.h +0 -208
  251. package/src/vendor/pi-tui/native/darwin/README.md +0 -22
  252. package/src/vendor/pi-tui/native/darwin/build.sh +0 -69
  253. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-arm64/darwin-platform.node +0 -0
  254. package/src/vendor/pi-tui/native/darwin/prebuilds/darwin-x64/darwin-platform.node +0 -0
  255. package/src/vendor/pi-tui/native/darwin/src/darwin-platform.m +0 -132
  256. package/src/vendor/pi-tui/native/linux/README.md +0 -23
  257. package/src/vendor/pi-tui/native/linux/build.sh +0 -65
  258. package/src/vendor/pi-tui/native/linux/prebuilds/linux-arm64/linux-platform-x11.node +0 -0
  259. package/src/vendor/pi-tui/native/linux/prebuilds/linux-x64/linux-platform-x11.node +0 -0
  260. package/src/vendor/pi-tui/native/linux/src/clipboard-worker.h +0 -70
  261. package/src/vendor/pi-tui/native/linux/src/linux-platform-x11.c +0 -274
  262. package/src/vendor/pi-tui/native/napi.h +0 -91
  263. package/src/vendor/pi-tui/native/win32/README.md +0 -31
  264. package/src/vendor/pi-tui/native/win32/build.mjs +0 -232
  265. package/src/vendor/pi-tui/native/win32/prebuilds/win32-arm64/win32-platform.node +0 -0
  266. package/src/vendor/pi-tui/native/win32/prebuilds/win32-x64/win32-platform.node +0 -0
  267. package/src/vendor/pi-tui/native/win32/src/win32-platform.c +0 -241
  268. package/src/vendor/pi-tui/native-modifiers.ts +0 -13
  269. package/src/vendor/pi-tui/native-module-path.ts +0 -31
  270. package/src/vendor/pi-tui/native-platform.ts +0 -65
  271. package/src/vendor/pi-tui/oklab.ts +0 -233
  272. package/src/vendor/pi-tui/stdin-buffer.ts +0 -444
  273. package/src/vendor/pi-tui/terminal-colors.ts +0 -91
  274. package/src/vendor/pi-tui/terminal-image.ts +0 -731
  275. package/src/vendor/pi-tui/terminal.ts +0 -554
  276. package/src/vendor/pi-tui/tui-alt-screen.ts +0 -1750
  277. package/src/vendor/pi-tui/tui-main-screen.ts +0 -655
  278. package/src/vendor/pi-tui/tui.ts +0 -1493
  279. package/src/vendor/pi-tui/undo-stack.ts +0 -28
  280. package/src/vendor/pi-tui/utils.ts +0 -1385
  281. package/src/vendor/pi-tui/wheel-scroll.ts +0 -82
  282. package/src/vendor/pi-tui/word-navigation.ts +0 -117
@@ -1,731 +0,0 @@
1
- import { execSync } from "node:child_process";
2
- import { homedir } from "node:os";
3
- import { isAbsolute } from "node:path";
4
- import { pathToFileURL } from "node:url";
5
- import type { TerminalColorMode } from "./colors.ts";
6
-
7
- export type ImageProtocol = "kitty" | "iterm2" | null;
8
-
9
- export interface TerminalCapabilities {
10
- images: ImageProtocol;
11
- trueColor: boolean;
12
- hyperlinks: boolean;
13
- }
14
-
15
- export interface CellDimensions {
16
- widthPx: number;
17
- heightPx: number;
18
- }
19
-
20
- export interface ImageDimensions {
21
- widthPx: number;
22
- heightPx: number;
23
- }
24
-
25
- export interface ImageRenderOptions {
26
- maxWidthCells?: number;
27
- maxHeightCells?: number;
28
- preserveAspectRatio?: boolean;
29
- /** Kitty image ID. If provided, reuses/replaces existing image with this ID. */
30
- imageId?: number;
31
- /** Whether Kitty should apply its default cursor movement after placement. */
32
- moveCursor?: boolean;
33
- }
34
-
35
- let cachedCapabilities: TerminalCapabilities | null = null;
36
- let capabilityOverrides: Partial<TerminalCapabilities> = {};
37
-
38
- // Default cell dimensions - updated by TUI when terminal responds to query
39
- let cellDimensions: CellDimensions = { widthPx: 9, heightPx: 18 };
40
-
41
- export function getCellDimensions(): CellDimensions {
42
- return cellDimensions;
43
- }
44
-
45
- export function setCellDimensions(dims: CellDimensions): void {
46
- cellDimensions = dims;
47
- }
48
-
49
- /**
50
- * Checks whether the attached tmux client forwards OSC 8 hyperlinks to the
51
- * outer terminal. tmux only re-emits them when its `client_termfeatures` lists
52
- * `hyperlinks`, and strips them otherwise. On any error fallbacks `false`.
53
- */
54
- function probeTmuxHyperlinks(): boolean {
55
- try {
56
- const termfeatures = execSync("tmux display-message -p '#{client_termfeatures}'", {
57
- encoding: "utf8",
58
- timeout: 250,
59
- stdio: ["ignore", "pipe", "ignore"],
60
- });
61
- return termfeatures
62
- .split(",")
63
- .map((feature) => feature.trim())
64
- .includes("hyperlinks");
65
- } catch {
66
- return false;
67
- }
68
- }
69
-
70
- function detectCapabilitiesFromEnvironment(tmuxForwardsHyperlink: () => boolean): TerminalCapabilities {
71
- const termProgram = process.env.TERM_PROGRAM?.toLowerCase() || "";
72
- const terminalEmulator = process.env.TERMINAL_EMULATOR?.toLowerCase() || "";
73
- const term = process.env.TERM?.toLowerCase() || "";
74
- const colorTerm = process.env.COLORTERM?.toLowerCase() || "";
75
- const hasTrueColorHint = colorTerm === "truecolor" || colorTerm === "24bit" || term.endsWith("-direct");
76
- const isWindowsConsole = process.platform === "win32";
77
-
78
- // Emit OSC 8 hyperlinks only when tmux confirms it forwards.
79
- // Image protocols are unreliable under tmux, so leave `images: null`.
80
- if (process.env.TMUX || term.startsWith("tmux")) {
81
- return { images: null, trueColor: hasTrueColorHint, hyperlinks: tmuxForwardsHyperlink() };
82
- }
83
-
84
- // screen does not forward OSC 8 hyperlinks, so keep them off there.
85
- if (term.startsWith("screen")) {
86
- return { images: null, trueColor: hasTrueColorHint, hyperlinks: false };
87
- }
88
-
89
- if (process.env.KITTY_WINDOW_ID || termProgram === "kitty") {
90
- return { images: "kitty", trueColor: true, hyperlinks: true };
91
- }
92
-
93
- if (termProgram === "ghostty" || term.includes("ghostty") || process.env.GHOSTTY_RESOURCES_DIR) {
94
- return { images: "kitty", trueColor: true, hyperlinks: true };
95
- }
96
-
97
- if (process.env.WEZTERM_PANE || termProgram === "wezterm") {
98
- return { images: "kitty", trueColor: true, hyperlinks: true };
99
- }
100
-
101
- // Warp supports the Kitty graphics protocol and OSC 8 hyperlinks.
102
- if (termProgram === "warpterminal" || process.env.WARP_SESSION_ID || process.env.WARP_TERMINAL_SESSION_UUID) {
103
- return { images: "kitty", trueColor: true, hyperlinks: true };
104
- }
105
-
106
- if (process.env.ITERM_SESSION_ID || termProgram === "iterm.app") {
107
- return { images: "iterm2", trueColor: true, hyperlinks: true };
108
- }
109
-
110
- if (process.env.WT_SESSION) {
111
- return { images: null, trueColor: true, hyperlinks: true };
112
- }
113
-
114
- if (termProgram === "alacritty" || termProgram === "vscode" || termProgram === "zed") {
115
- return { images: null, trueColor: true, hyperlinks: true };
116
- }
117
-
118
- if (terminalEmulator === "jetbrains-jediterm") {
119
- return { images: null, trueColor: true, hyperlinks: false };
120
- }
121
-
122
- // Windows Terminal does not always set WT_SESSION, for example when it hosts
123
- // a cmd.exe launched directly from Win+R. Modern Windows consoles support
124
- // truecolor; keep hyperlinks off unless we positively detected support above.
125
- if (isWindowsConsole) {
126
- return { images: null, trueColor: true, hyperlinks: false };
127
- }
128
-
129
- // Unknown terminal: be conservative. OSC 8 is rendered invisibly as "just
130
- // text" on terminals that swallow it, which means the URL disappears from
131
- // the rendered output. Default to the legacy `text (url)` behavior unless we
132
- // have positively identified a hyperlink-capable terminal above.
133
- return { images: null, trueColor: hasTrueColorHint, hyperlinks: false };
134
- }
135
-
136
- function parseBooleanCapabilityOverride(value: string | undefined): boolean | undefined {
137
- return value === "1" ? true : value === "0" ? false : undefined;
138
- }
139
-
140
- export function detectCapabilities(tmuxForwardsHyperlink: () => boolean = probeTmuxHyperlinks): TerminalCapabilities {
141
- const hyperlinks = parseBooleanCapabilityOverride(process.env.PI_HYPERLINKS);
142
- const detected = detectCapabilitiesFromEnvironment(
143
- hyperlinks === undefined ? tmuxForwardsHyperlink : () => hyperlinks,
144
- );
145
- const imageProtocol = process.env.PI_IMAGE_PROTOCOL?.toLowerCase();
146
- const images =
147
- imageProtocol === "kitty" || imageProtocol === "iterm2"
148
- ? imageProtocol
149
- : imageProtocol === "none" || imageProtocol === "0"
150
- ? null
151
- : undefined;
152
- const trueColor = parseBooleanCapabilityOverride(process.env.PI_TRUE_COLOR);
153
- return {
154
- ...detected,
155
- ...(images !== undefined ? { images } : {}),
156
- ...(trueColor !== undefined ? { trueColor } : {}),
157
- ...(hyperlinks !== undefined ? { hyperlinks } : {}),
158
- };
159
- }
160
-
161
- export function getCapabilities(): TerminalCapabilities {
162
- if (!cachedCapabilities) {
163
- const hyperlinks = capabilityOverrides.hyperlinks;
164
- cachedCapabilities = {
165
- ...detectCapabilities(hyperlinks === undefined ? undefined : () => hyperlinks),
166
- ...capabilityOverrides,
167
- };
168
- }
169
- return cachedCapabilities;
170
- }
171
-
172
- export function getTerminalColorMode(capabilities: TerminalCapabilities = getCapabilities()): TerminalColorMode {
173
- return capabilities.trueColor ? "truecolor" : "256color";
174
- }
175
-
176
- export function resetCapabilitiesCache(): void {
177
- cachedCapabilities = null;
178
- }
179
-
180
- /** Override selected auto-detected capabilities. */
181
- export function setCapabilityOverrides(overrides: Partial<TerminalCapabilities>): void {
182
- if (
183
- capabilityOverrides.images === overrides.images &&
184
- capabilityOverrides.trueColor === overrides.trueColor &&
185
- capabilityOverrides.hyperlinks === overrides.hyperlinks
186
- ) {
187
- return;
188
- }
189
- capabilityOverrides = { ...overrides };
190
- cachedCapabilities = null;
191
- }
192
-
193
- /** Override the cached capabilities. Useful in tests to exercise both code paths. */
194
- export function setCapabilities(caps: TerminalCapabilities): void {
195
- cachedCapabilities = caps;
196
- }
197
-
198
- const KITTY_PREFIX = "\x1b_G";
199
- const ITERM2_PREFIX = "\x1b]1337;File=";
200
-
201
- export function isImageLine(line: string): boolean {
202
- // Fast path: sequence at line start (single-row images)
203
- if (line.startsWith(KITTY_PREFIX) || line.startsWith(ITERM2_PREFIX)) {
204
- return true;
205
- }
206
- // Slow path: sequence elsewhere (multi-row images have cursor-up prefix)
207
- return line.includes(KITTY_PREFIX) || line.includes(ITERM2_PREFIX);
208
- }
209
-
210
- /**
211
- * Generate a random image ID for Kitty graphics protocol.
212
- * Uses random IDs to avoid collisions between different module instances
213
- * (e.g., main app vs extensions).
214
- */
215
- export function allocateImageId(): number {
216
- // Use random ID in range [1, 0xffffffff] to avoid collisions
217
- return Math.floor(Math.random() * 0xfffffffe) + 1;
218
- }
219
-
220
- export function encodeKitty(
221
- base64Data: string,
222
- options: {
223
- columns?: number;
224
- rows?: number;
225
- imageId?: number;
226
- /** Whether Kitty should apply its default cursor movement after placement. Default: true. */
227
- moveCursor?: boolean;
228
- } = {},
229
- ): string {
230
- const CHUNK_SIZE = 4096;
231
-
232
- const params: string[] = ["a=T", "f=100", "q=2"];
233
-
234
- if (options.moveCursor === false) params.push("C=1");
235
- if (options.columns) params.push(`c=${options.columns}`);
236
- if (options.rows) params.push(`r=${options.rows}`);
237
- if (options.imageId) params.push(`i=${options.imageId}`);
238
-
239
- if (base64Data.length <= CHUNK_SIZE) {
240
- return `\x1b_G${params.join(",")};${base64Data}\x1b\\`;
241
- }
242
-
243
- const chunks: string[] = [];
244
- let offset = 0;
245
- let isFirst = true;
246
-
247
- while (offset < base64Data.length) {
248
- const chunk = base64Data.slice(offset, offset + CHUNK_SIZE);
249
- const isLast = offset + CHUNK_SIZE >= base64Data.length;
250
-
251
- if (isFirst) {
252
- chunks.push(`\x1b_G${params.join(",")},m=1;${chunk}\x1b\\`);
253
- isFirst = false;
254
- } else if (isLast) {
255
- chunks.push(`\x1b_Gm=0;${chunk}\x1b\\`);
256
- } else {
257
- chunks.push(`\x1b_Gm=1;${chunk}\x1b\\`);
258
- }
259
-
260
- offset += CHUNK_SIZE;
261
- }
262
-
263
- return chunks.join("");
264
- }
265
-
266
- /**
267
- * Delete a Kitty graphics image by ID.
268
- * Uses uppercase 'I' to also free the image data.
269
- */
270
- export function deleteKittyImage(imageId: number): string {
271
- return `\x1b_Ga=d,d=I,i=${imageId},q=2\x1b\\`;
272
- }
273
-
274
- /**
275
- * Delete all visible Kitty graphics images.
276
- * Uses uppercase 'A' to also free the image data.
277
- */
278
- export function deleteAllKittyImages(): string {
279
- return "\x1b_Ga=d,d=A,q=2\x1b\\";
280
- }
281
-
282
- /** Delete all visible Kitty placements while retaining their uploaded image data. */
283
- export function deleteAllKittyPlacements(): string {
284
- return "\x1b_Ga=d,d=a,q=2\x1b\\";
285
- }
286
-
287
- export function encodeITerm2(
288
- base64Data: string,
289
- options: {
290
- width?: number | string;
291
- height?: number | string;
292
- name?: string;
293
- preserveAspectRatio?: boolean;
294
- inline?: boolean;
295
- } = {},
296
- ): string {
297
- const params: string[] = [
298
- `inline=${options.inline !== false ? 1 : 0}`,
299
- `size=${Buffer.byteLength(base64Data, "base64")}`,
300
- ];
301
-
302
- if (options.width !== undefined) params.push(`width=${options.width}`);
303
- if (options.height !== undefined) params.push(`height=${options.height}`);
304
- if (options.name) {
305
- const nameBase64 = Buffer.from(options.name).toString("base64");
306
- params.push(`name=${nameBase64}`);
307
- }
308
- if (options.preserveAspectRatio === false) {
309
- params.push("preserveAspectRatio=0");
310
- }
311
-
312
- return `\x1b]1337;File=${params.join(";")}:${base64Data}\x07`;
313
- }
314
-
315
- export interface ImageCellSize {
316
- columns: number;
317
- rows: number;
318
- }
319
-
320
- export interface KittyImageMetadata extends ImageCellSize {
321
- imageId: number;
322
- widthPx: number;
323
- heightPx: number;
324
- }
325
-
326
- interface RegisteredKittyImageMetadata extends KittyImageMetadata {
327
- transmissionGeneration: number;
328
- }
329
-
330
- export interface KittyImagePlacement {
331
- imageId: number;
332
- transmissionGeneration: number;
333
- transmissionBytes: number;
334
- estimatedDecodedBytes: number;
335
- sequence: string;
336
- replacementLine: string;
337
- }
338
-
339
- const kittyImageMetadata = new Map<number, RegisteredKittyImageMetadata>();
340
- let kittyTransmissionGeneration = 0;
341
-
342
- export function registerKittyImageMetadata(metadata: KittyImageMetadata): void {
343
- kittyTransmissionGeneration += 1;
344
- kittyImageMetadata.delete(metadata.imageId);
345
- kittyImageMetadata.set(metadata.imageId, { ...metadata, transmissionGeneration: kittyTransmissionGeneration });
346
- if (kittyImageMetadata.size > 1000) {
347
- const oldestImageId = kittyImageMetadata.keys().next().value;
348
- if (oldestImageId !== undefined) kittyImageMetadata.delete(oldestImageId);
349
- }
350
- }
351
-
352
- function getRegisteredKittyImageMetadata(line: string): RegisteredKittyImageMetadata | undefined {
353
- const controls = /\x1b_G([^;]*);/.exec(line)?.[1];
354
- if (!controls) return undefined;
355
- const imageId = /(?:^|,)i=(\d+)(?:,|$)/.exec(controls)?.[1];
356
- return imageId === undefined ? undefined : kittyImageMetadata.get(Number.parseInt(imageId, 10));
357
- }
358
-
359
- export function getKittyImageMetadata(line: string): KittyImageMetadata | undefined {
360
- const metadata = getRegisteredKittyImageMetadata(line);
361
- if (!metadata) return undefined;
362
- return {
363
- imageId: metadata.imageId,
364
- columns: metadata.columns,
365
- rows: metadata.rows,
366
- widthPx: metadata.widthPx,
367
- heightPx: metadata.heightPx,
368
- };
369
- }
370
-
371
- const KITTY_PLACEMENT_CONTROL_KEYS = new Set([
372
- "i",
373
- "p",
374
- "x",
375
- "y",
376
- "w",
377
- "h",
378
- "X",
379
- "Y",
380
- "c",
381
- "r",
382
- "C",
383
- "U",
384
- "z",
385
- "P",
386
- "Q",
387
- "H",
388
- "V",
389
- ]);
390
-
391
- /** Build a placement-only command for an image line emitted by {@link renderImage}. */
392
- export function getKittyImagePlacement(line: string): KittyImagePlacement | undefined {
393
- const match = /\x1b_G([^;]*);/.exec(line);
394
- const metadata = getRegisteredKittyImageMetadata(line);
395
- if (!match || !metadata) return undefined;
396
-
397
- let commandStart = match.index;
398
- let commandControls = match[1];
399
- let transmissionEnd: number;
400
- while (true) {
401
- const terminator = line.indexOf("\x1b\\", commandStart + KITTY_PREFIX.length);
402
- if (terminator === -1) return undefined;
403
- transmissionEnd = terminator + 2;
404
- if (!/(?:^|,)m=1(?:,|$)/.test(commandControls)) break;
405
- commandStart = transmissionEnd;
406
- if (!line.startsWith(KITTY_PREFIX, commandStart)) return undefined;
407
- const controlsEnd = line.indexOf(";", commandStart + KITTY_PREFIX.length);
408
- if (controlsEnd === -1) return undefined;
409
- commandControls = line.slice(commandStart + KITTY_PREFIX.length, controlsEnd);
410
- }
411
-
412
- const controls = match[1]
413
- .split(",")
414
- .filter((control) => KITTY_PLACEMENT_CONTROL_KEYS.has(control.split("=", 1)[0] ?? ""));
415
- const sequence = `\x1b_Ga=p,q=2,${controls.join(",")}\x1b\\`;
416
- return {
417
- imageId: metadata.imageId,
418
- transmissionGeneration: metadata.transmissionGeneration,
419
- transmissionBytes: transmissionEnd - match.index,
420
- estimatedDecodedBytes: metadata.widthPx * metadata.heightPx * 4,
421
- sequence,
422
- replacementLine: `${line.slice(0, match.index)}${sequence}${line.slice(transmissionEnd)}`,
423
- };
424
- }
425
-
426
- export function cropKittyImageLine(line: string, hiddenRows: number, visibleRows: number): string {
427
- const metadata = getKittyImageMetadata(line);
428
- const match = /\x1b_G([^;]*);/.exec(line);
429
- if (!metadata || !match || hiddenRows < 0 || hiddenRows >= metadata.rows || visibleRows <= 0) return line;
430
- const croppedRows = Math.min(visibleRows, metadata.rows - hiddenRows);
431
- if (hiddenRows === 0 && croppedRows === metadata.rows) return line;
432
- const sourceY = Math.floor((metadata.heightPx * hiddenRows) / metadata.rows);
433
- const sourceEnd = Math.ceil((metadata.heightPx * (hiddenRows + croppedRows)) / metadata.rows);
434
- const sourceHeight = Math.max(1, Math.min(metadata.heightPx, sourceEnd) - sourceY);
435
- const controls = match[1].split(",").filter((control) => !/^[yhr]=/.test(control));
436
- controls.push(`y=${sourceY}`, `h=${sourceHeight}`, `r=${croppedRows}`);
437
- return `${line.slice(0, match.index)}\x1b_G${controls.join(",")};${line.slice(match.index + match[0].length)}`;
438
- }
439
-
440
- function chooseLessDistortedCellCount(upperCount: number, idealCount: number): number {
441
- if (upperCount <= 1) return upperCount;
442
-
443
- const lowerCount = upperCount - 1;
444
- const upperDistortion = Math.max(upperCount / idealCount, idealCount / upperCount);
445
- const lowerDistortion = Math.max(lowerCount / idealCount, idealCount / lowerCount);
446
- return lowerDistortion < upperDistortion ? lowerCount : upperCount;
447
- }
448
-
449
- export function calculateImageCellSize(
450
- imageDimensions: ImageDimensions,
451
- maxWidthCells: number,
452
- maxHeightCells?: number,
453
- cellDimensions: CellDimensions = { widthPx: 9, heightPx: 18 },
454
- optimizeAspectRatio = false,
455
- ): ImageCellSize {
456
- const maxWidth = Math.max(1, Math.floor(maxWidthCells));
457
- const maxHeight = maxHeightCells === undefined ? undefined : Math.max(1, Math.floor(maxHeightCells));
458
- const imageWidth = Math.max(1, imageDimensions.widthPx);
459
- const imageHeight = Math.max(1, imageDimensions.heightPx);
460
-
461
- const widthScale = (maxWidth * cellDimensions.widthPx) / imageWidth;
462
- const heightScale = maxHeight === undefined ? widthScale : (maxHeight * cellDimensions.heightPx) / imageHeight;
463
- const scale = Math.min(widthScale, heightScale);
464
-
465
- const scaledWidthPx = imageWidth * scale;
466
- const scaledHeightPx = imageHeight * scale;
467
- let columns = Math.max(1, Math.min(maxWidth, Math.ceil(scaledWidthPx / cellDimensions.widthPx)));
468
- const heightRows = scaledHeightPx / cellDimensions.heightPx;
469
- let rows = Math.max(1, Math.ceil(heightRows));
470
- if (maxHeight !== undefined) {
471
- rows = Math.min(maxHeight, rows);
472
- }
473
-
474
- if (!optimizeAspectRatio) {
475
- return { columns, rows };
476
- }
477
-
478
- if (widthScale <= heightScale) {
479
- const idealRows = (columns * cellDimensions.widthPx * imageHeight) / (imageWidth * cellDimensions.heightPx);
480
- rows = chooseLessDistortedCellCount(rows, idealRows);
481
- } else {
482
- const idealColumns = (rows * cellDimensions.heightPx * imageWidth) / (imageHeight * cellDimensions.widthPx);
483
- columns = chooseLessDistortedCellCount(columns, idealColumns);
484
- }
485
-
486
- return { columns, rows };
487
- }
488
-
489
- export function calculateImageRows(
490
- imageDimensions: ImageDimensions,
491
- targetWidthCells: number,
492
- cellDimensions: CellDimensions = { widthPx: 9, heightPx: 18 },
493
- ): number {
494
- return calculateImageCellSize(imageDimensions, targetWidthCells, undefined, cellDimensions).rows;
495
- }
496
-
497
- export function getPngDimensions(base64Data: string): ImageDimensions | null {
498
- try {
499
- const buffer = Buffer.from(base64Data, "base64");
500
-
501
- if (buffer.length < 24) {
502
- return null;
503
- }
504
-
505
- if (buffer[0] !== 0x89 || buffer[1] !== 0x50 || buffer[2] !== 0x4e || buffer[3] !== 0x47) {
506
- return null;
507
- }
508
-
509
- const width = buffer.readUInt32BE(16);
510
- const height = buffer.readUInt32BE(20);
511
-
512
- return { widthPx: width, heightPx: height };
513
- } catch {
514
- return null;
515
- }
516
- }
517
-
518
- export function getJpegDimensions(base64Data: string): ImageDimensions | null {
519
- try {
520
- const buffer = Buffer.from(base64Data, "base64");
521
-
522
- if (buffer.length < 2) {
523
- return null;
524
- }
525
-
526
- if (buffer[0] !== 0xff || buffer[1] !== 0xd8) {
527
- return null;
528
- }
529
-
530
- let offset = 2;
531
- while (offset < buffer.length - 9) {
532
- if (buffer[offset] !== 0xff) {
533
- offset++;
534
- continue;
535
- }
536
-
537
- const marker = buffer[offset + 1];
538
-
539
- if (marker >= 0xc0 && marker <= 0xc2) {
540
- const height = buffer.readUInt16BE(offset + 5);
541
- const width = buffer.readUInt16BE(offset + 7);
542
- return { widthPx: width, heightPx: height };
543
- }
544
-
545
- if (offset + 3 >= buffer.length) {
546
- return null;
547
- }
548
- const length = buffer.readUInt16BE(offset + 2);
549
- if (length < 2) {
550
- return null;
551
- }
552
- offset += 2 + length;
553
- }
554
-
555
- return null;
556
- } catch {
557
- return null;
558
- }
559
- }
560
-
561
- export function getGifDimensions(base64Data: string): ImageDimensions | null {
562
- try {
563
- const buffer = Buffer.from(base64Data, "base64");
564
-
565
- if (buffer.length < 10) {
566
- return null;
567
- }
568
-
569
- const sig = buffer.slice(0, 6).toString("ascii");
570
- if (sig !== "GIF87a" && sig !== "GIF89a") {
571
- return null;
572
- }
573
-
574
- const width = buffer.readUInt16LE(6);
575
- const height = buffer.readUInt16LE(8);
576
-
577
- return { widthPx: width, heightPx: height };
578
- } catch {
579
- return null;
580
- }
581
- }
582
-
583
- export function getWebpDimensions(base64Data: string): ImageDimensions | null {
584
- try {
585
- const buffer = Buffer.from(base64Data, "base64");
586
-
587
- if (buffer.length < 30) {
588
- return null;
589
- }
590
-
591
- const riff = buffer.slice(0, 4).toString("ascii");
592
- const webp = buffer.slice(8, 12).toString("ascii");
593
- if (riff !== "RIFF" || webp !== "WEBP") {
594
- return null;
595
- }
596
-
597
- const chunk = buffer.slice(12, 16).toString("ascii");
598
- if (chunk === "VP8 ") {
599
- if (buffer.length < 30) return null;
600
- const width = buffer.readUInt16LE(26) & 0x3fff;
601
- const height = buffer.readUInt16LE(28) & 0x3fff;
602
- return { widthPx: width, heightPx: height };
603
- } else if (chunk === "VP8L") {
604
- if (buffer.length < 25) return null;
605
- const bits = buffer.readUInt32LE(21);
606
- const width = (bits & 0x3fff) + 1;
607
- const height = ((bits >> 14) & 0x3fff) + 1;
608
- return { widthPx: width, heightPx: height };
609
- } else if (chunk === "VP8X") {
610
- if (buffer.length < 30) return null;
611
- const width = (buffer[24] | (buffer[25] << 8) | (buffer[26] << 16)) + 1;
612
- const height = (buffer[27] | (buffer[28] << 8) | (buffer[29] << 16)) + 1;
613
- return { widthPx: width, heightPx: height };
614
- }
615
-
616
- return null;
617
- } catch {
618
- return null;
619
- }
620
- }
621
-
622
- export function getImageDimensions(base64Data: string, mimeType: string): ImageDimensions | null {
623
- if (mimeType === "image/png") {
624
- return getPngDimensions(base64Data);
625
- }
626
- if (mimeType === "image/jpeg") {
627
- return getJpegDimensions(base64Data);
628
- }
629
- if (mimeType === "image/gif") {
630
- return getGifDimensions(base64Data);
631
- }
632
- if (mimeType === "image/webp") {
633
- return getWebpDimensions(base64Data);
634
- }
635
- return null;
636
- }
637
-
638
- export function renderImage(
639
- base64Data: string,
640
- imageDimensions: ImageDimensions,
641
- options: ImageRenderOptions = {},
642
- ): { sequence: string; columns: number; rows: number; imageId?: number } | null {
643
- const caps = getCapabilities();
644
-
645
- if (!caps.images) {
646
- return null;
647
- }
648
-
649
- const maxWidth = options.maxWidthCells ?? 80;
650
- // Reduce Kitty's cell-aligned distortion without shrinking iTerm2 reservations.
651
- const size = calculateImageCellSize(
652
- imageDimensions,
653
- maxWidth,
654
- options.maxHeightCells,
655
- getCellDimensions(),
656
- caps.images === "kitty",
657
- );
658
-
659
- if (caps.images === "kitty") {
660
- if (options.imageId !== undefined) {
661
- registerKittyImageMetadata({
662
- imageId: options.imageId,
663
- columns: size.columns,
664
- rows: size.rows,
665
- widthPx: imageDimensions.widthPx,
666
- heightPx: imageDimensions.heightPx,
667
- });
668
- }
669
- const sequence = encodeKitty(base64Data, {
670
- columns: size.columns,
671
- rows: size.rows,
672
- imageId: options.imageId,
673
- moveCursor: options.moveCursor,
674
- });
675
- return { sequence, columns: size.columns, rows: size.rows, imageId: options.imageId };
676
- }
677
-
678
- if (caps.images === "iterm2") {
679
- const sequence = encodeITerm2(base64Data, {
680
- width: size.columns,
681
- height: "auto",
682
- preserveAspectRatio: options.preserveAspectRatio ?? true,
683
- });
684
- return { sequence, columns: size.columns, rows: size.rows };
685
- }
686
-
687
- return null;
688
- }
689
-
690
- /**
691
- * Wrap text in an OSC 8 hyperlink sequence.
692
- * The text is rendered as a clickable hyperlink in terminals that support OSC 8
693
- * (Ghostty, Kitty, WezTerm, iTerm2, VSCode, and others).
694
- * In terminals that do not support OSC 8, the escape sequences are ignored
695
- * and only the plain text is displayed.
696
- *
697
- * @param text - The visible text to display
698
- * @param url - The URL to link to
699
- */
700
- export function hyperlink(text: string, url: string): string {
701
- return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
702
- }
703
-
704
- /** Shorten home-prefixed absolute paths to ~/... for compact display. */
705
- function shortenImagePath(filename: string): string {
706
- const home = homedir();
707
- if (home && (filename === home || filename.startsWith(`${home}/`) || filename.startsWith(`${home}\\`))) {
708
- return `~${filename.slice(home.length)}`;
709
- }
710
- return filename;
711
- }
712
-
713
- /**
714
- * Text fallback when the terminal cannot render inline images.
715
- * Absolute paths are shown shortened (~/...) and, when OSC 8 hyperlinks are
716
- * available, linked to file:// so the full path remains openable.
717
- */
718
- export function imageFallback(mimeType: string, dimensions?: ImageDimensions, filename?: string): string {
719
- const parts: string[] = [];
720
- if (filename) {
721
- const display = shortenImagePath(filename);
722
- if (getCapabilities().hyperlinks && isAbsolute(filename)) {
723
- parts.push(hyperlink(display, pathToFileURL(filename).href));
724
- } else {
725
- parts.push(display);
726
- }
727
- }
728
- parts.push(`[${mimeType}]`);
729
- if (dimensions) parts.push(`${dimensions.widthPx}x${dimensions.heightPx}`);
730
- return `[Image: ${parts.join(" ")}]`;
731
- }