@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,44 +0,0 @@
1
- import { compositeTuiLine } from "../tui.ts";
2
- import { visibleWidth } from "../utils.ts";
3
- import { allocateStackSizes, Stack, type StackChild, type StackOptions, visibleStackEntries } from "./stack.ts";
4
-
5
- export class HStack extends Stack {
6
- protected readonly layoutType = "hstack" as const;
7
-
8
- constructor(children: StackChild[] = [], options: StackOptions = {}) {
9
- super(children, options);
10
- }
11
-
12
- override render(width: number): string[] {
13
- const safeWidth = Math.max(1, width);
14
- const viewport = { width: safeWidth, height: Number.MAX_SAFE_INTEGER };
15
- const entries = visibleStackEntries(this.entries, viewport);
16
- if (entries.length === 0) return [];
17
-
18
- const intrinsicWidths = entries.map((entry) => {
19
- const lines = entry.component.render(safeWidth);
20
- return lines.reduce((max, line) => Math.max(max, visibleWidth(line)), 0);
21
- });
22
- const widths = allocateStackSizes(entries, intrinsicWidths, safeWidth, this.gap);
23
- const rendered = entries.map((entry, index) =>
24
- widths[index] === 0 ? [] : entry.component.render(widths[index]!),
25
- );
26
- const height = rendered.reduce((max, lines) => Math.max(max, lines.length), 0);
27
- const result = Array.from({ length: height }, () => "");
28
- let x = 0;
29
- for (let index = 0; index < rendered.length; index++) {
30
- const lines = rendered[index]!;
31
- const childWidth = widths[index]!;
32
- let offset = 0;
33
- if (this.align === "center") offset = Math.floor((height - lines.length) / 2);
34
- else if (this.align === "end") offset = height - lines.length;
35
- for (let row = 0; row < lines.length; row++) {
36
- const target = row + offset;
37
- if (target < 0 || target >= result.length) continue;
38
- result[target] = compositeTuiLine(result[target]!, lines[row]!, x, childWidth, safeWidth);
39
- }
40
- x += childWidth + this.gap;
41
- }
42
- return result;
43
- }
44
- }
@@ -1,127 +0,0 @@
1
- import {
2
- allocateImageId,
3
- getCapabilities,
4
- getCellDimensions,
5
- getImageDimensions,
6
- type ImageDimensions,
7
- imageFallback,
8
- renderImage,
9
- } from "../terminal-image.ts";
10
- import type { Component } from "../tui.ts";
11
- import { truncateToWidth } from "../utils.ts";
12
-
13
- export interface ImageTheme {
14
- fallbackColor: (str: string) => string;
15
- }
16
-
17
- export interface ImageOptions {
18
- maxWidthCells?: number;
19
- maxHeightCells?: number;
20
- filename?: string;
21
- /** Kitty image ID. If provided, reuses this ID (for animations/updates). */
22
- imageId?: number;
23
- }
24
-
25
- export class Image implements Component {
26
- private base64Data: string;
27
- private mimeType: string;
28
- private dimensions: ImageDimensions;
29
- private theme: ImageTheme;
30
- private options: ImageOptions;
31
- private imageId?: number;
32
-
33
- private cachedLines?: string[];
34
- private cachedWidth?: number;
35
-
36
- constructor(
37
- base64Data: string,
38
- mimeType: string,
39
- theme: ImageTheme,
40
- options: ImageOptions = {},
41
- dimensions?: ImageDimensions,
42
- ) {
43
- this.base64Data = base64Data;
44
- this.mimeType = mimeType;
45
- this.theme = theme;
46
- this.options = options;
47
- this.dimensions = dimensions || getImageDimensions(base64Data, mimeType) || { widthPx: 800, heightPx: 600 };
48
- this.imageId = options.imageId;
49
- }
50
-
51
- /** Get the Kitty image ID used by this image (if any). */
52
- getImageId(): number | undefined {
53
- return this.imageId;
54
- }
55
-
56
- invalidate(): void {
57
- this.cachedLines = undefined;
58
- this.cachedWidth = undefined;
59
- }
60
-
61
- render(width: number): string[] {
62
- if (this.cachedLines && this.cachedWidth === width) {
63
- return this.cachedLines;
64
- }
65
-
66
- const maxWidth = Math.max(1, Math.min(width - 2, this.options.maxWidthCells ?? 60));
67
- const cellDimensions = getCellDimensions();
68
- const defaultMaxHeight = Math.max(1, Math.ceil((maxWidth * cellDimensions.widthPx) / cellDimensions.heightPx));
69
- const maxHeight = this.options.maxHeightCells ?? defaultMaxHeight;
70
-
71
- const caps = getCapabilities();
72
- let lines: string[];
73
-
74
- if (caps.images) {
75
- if (caps.images === "kitty" && this.imageId === undefined) {
76
- this.imageId = allocateImageId();
77
- }
78
- const result = renderImage(this.base64Data, this.dimensions, {
79
- maxWidthCells: maxWidth,
80
- maxHeightCells: maxHeight,
81
- imageId: this.imageId,
82
- moveCursor: false,
83
- });
84
-
85
- if (result) {
86
- // Store the image ID for later cleanup
87
- if (result.imageId) {
88
- this.imageId = result.imageId;
89
- }
90
-
91
- if (caps.images === "kitty") {
92
- // For Kitty: C=1 prevents cursor movement.
93
- // Don't need the cursor movement.
94
- lines = [result.sequence];
95
-
96
- // Return `rows` lines so TUI accounts for image height.
97
- for (let i = 0; i < result.rows - 1; i++) {
98
- lines.push("");
99
- }
100
- } else {
101
- // Return `rows` lines so TUI accounts for image height.
102
- // First (rows-1) lines are empty and cleared before the image is drawn.
103
- // Last line: move cursor back up, draw the image, then move back down
104
- // so TUI cursor accounting stays inside the scroll area.
105
- lines = [];
106
- for (let i = 0; i < result.rows - 1; i++) {
107
- lines.push("");
108
- }
109
- const rowOffset = result.rows - 1;
110
- const moveUp = rowOffset > 0 ? `\x1b[${rowOffset}A` : "";
111
- lines.push(moveUp + result.sequence);
112
- }
113
- } else {
114
- const fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);
115
- lines = [truncateToWidth(this.theme.fallbackColor(fallback), width)];
116
- }
117
- } else {
118
- const fallback = imageFallback(this.mimeType, this.dimensions, this.options.filename);
119
- lines = [truncateToWidth(this.theme.fallbackColor(fallback), width)];
120
- }
121
-
122
- this.cachedLines = lines;
123
- this.cachedWidth = width;
124
-
125
- return lines;
126
- }
127
- }
@@ -1,494 +0,0 @@
1
- import { getKeybindings } from "../keybindings.ts";
2
- import { decodeKittyPrintable } from "../keys.ts";
3
- import { KillRing } from "../kill-ring.ts";
4
- import { type Component, CURSOR_MARKER, type Focusable, type TuiMouseEvent, type TuiMouseEventResult } from "../tui.ts";
5
- import { UndoStack } from "../undo-stack.ts";
6
- import { getGraphemeSegmenter, isWhitespaceChar, sliceByColumn, truncateToWidth, visibleWidth } from "../utils.ts";
7
- import { findWordBackward, findWordForward } from "../word-navigation.ts";
8
-
9
- const segmenter = getGraphemeSegmenter();
10
-
11
- interface InputState {
12
- value: string;
13
- cursor: number;
14
- }
15
-
16
- export interface InputOptions {
17
- prompt?: string;
18
- placeholder?: string;
19
- placeholderStyle?: (text: string) => string;
20
- }
21
-
22
- /**
23
- * Input component - single-line text input with horizontal scrolling
24
- */
25
- export class Input implements Component, Focusable {
26
- private value: string = "";
27
- private cursor: number = 0; // Cursor position in the value
28
- private readonly prompt: string;
29
- private readonly placeholder: string;
30
- private readonly placeholderStyle: (text: string) => string;
31
- private renderedStartColumn = 0;
32
- public onSubmit?: (value: string) => void;
33
- public onEscape?: () => void;
34
-
35
- /** Focusable interface - set by TUI when focus changes */
36
- focused: boolean = false;
37
-
38
- // Bracketed paste mode buffering
39
- private pasteBuffer: string = "";
40
- private isInPaste: boolean = false;
41
-
42
- // Kill ring for Emacs-style kill/yank operations
43
- private killRing = new KillRing();
44
- private lastAction: "kill" | "yank" | "type-word" | null = null;
45
-
46
- // Undo support
47
- private undoStack = new UndoStack<InputState>();
48
-
49
- constructor(options: InputOptions = {}) {
50
- this.prompt = options.prompt ?? "> ";
51
- this.placeholder = options.placeholder ?? "";
52
- this.placeholderStyle = options.placeholderStyle ?? ((text) => text);
53
- }
54
-
55
- getValue(): string {
56
- return this.value;
57
- }
58
-
59
- setValue(value: string): void {
60
- this.value = value;
61
- this.cursor = Math.min(this.cursor, value.length);
62
- }
63
-
64
- handleInput(data: string): void {
65
- // Handle bracketed paste mode
66
- // Start of paste: \x1b[200~
67
- // End of paste: \x1b[201~
68
-
69
- // Check if we're starting a bracketed paste
70
- if (data.includes("\x1b[200~")) {
71
- this.isInPaste = true;
72
- this.pasteBuffer = "";
73
- data = data.replace("\x1b[200~", "");
74
- }
75
-
76
- // If we're in a paste, buffer the data
77
- if (this.isInPaste) {
78
- // Check if this chunk contains the end marker
79
- this.pasteBuffer += data;
80
-
81
- const endIndex = this.pasteBuffer.indexOf("\x1b[201~");
82
- if (endIndex !== -1) {
83
- // Extract the pasted content
84
- const pasteContent = this.pasteBuffer.substring(0, endIndex);
85
-
86
- // Process the complete paste
87
- this.handlePaste(pasteContent);
88
-
89
- // Reset paste state
90
- this.isInPaste = false;
91
-
92
- // Handle any remaining input after the paste marker
93
- const remaining = this.pasteBuffer.substring(endIndex + 6); // 6 = length of \x1b[201~
94
- this.pasteBuffer = "";
95
- if (remaining) {
96
- this.handleInput(remaining);
97
- }
98
- }
99
- return;
100
- }
101
-
102
- const kb = getKeybindings();
103
-
104
- // Escape/Cancel
105
- if (kb.matches(data, "tui.select.cancel")) {
106
- if (this.onEscape) this.onEscape();
107
- return;
108
- }
109
-
110
- // Undo
111
- if (kb.matches(data, "tui.editor.undo")) {
112
- this.undo();
113
- return;
114
- }
115
-
116
- // Submit
117
- if (kb.matches(data, "tui.input.submit") || data === "\n") {
118
- if (this.onSubmit) this.onSubmit(this.value);
119
- return;
120
- }
121
-
122
- // Deletion
123
- if (kb.matches(data, "tui.editor.deleteCharBackward")) {
124
- this.handleBackspace();
125
- return;
126
- }
127
-
128
- if (kb.matches(data, "tui.editor.deleteCharForward")) {
129
- this.handleForwardDelete();
130
- return;
131
- }
132
-
133
- if (kb.matches(data, "tui.editor.deleteWordBackward")) {
134
- this.deleteWordBackwards();
135
- return;
136
- }
137
-
138
- if (kb.matches(data, "tui.editor.deleteWordForward")) {
139
- this.deleteWordForward();
140
- return;
141
- }
142
-
143
- if (kb.matches(data, "tui.editor.deleteToLineStart")) {
144
- this.deleteToLineStart();
145
- return;
146
- }
147
-
148
- if (kb.matches(data, "tui.editor.deleteToLineEnd")) {
149
- this.deleteToLineEnd();
150
- return;
151
- }
152
-
153
- // Kill ring actions
154
- if (kb.matches(data, "tui.editor.yank")) {
155
- this.yank();
156
- return;
157
- }
158
- if (kb.matches(data, "tui.editor.yankPop")) {
159
- this.yankPop();
160
- return;
161
- }
162
-
163
- // Cursor movement
164
- if (kb.matches(data, "tui.editor.cursorLeft")) {
165
- this.lastAction = null;
166
- if (this.cursor > 0) {
167
- const beforeCursor = this.value.slice(0, this.cursor);
168
- const graphemes = [...segmenter.segment(beforeCursor)];
169
- const lastGrapheme = graphemes[graphemes.length - 1];
170
- this.cursor -= lastGrapheme ? lastGrapheme.segment.length : 1;
171
- }
172
- return;
173
- }
174
-
175
- if (kb.matches(data, "tui.editor.cursorRight")) {
176
- this.lastAction = null;
177
- if (this.cursor < this.value.length) {
178
- const afterCursor = this.value.slice(this.cursor);
179
- const graphemes = [...segmenter.segment(afterCursor)];
180
- const firstGrapheme = graphemes[0];
181
- this.cursor += firstGrapheme ? firstGrapheme.segment.length : 1;
182
- }
183
- return;
184
- }
185
-
186
- if (kb.matches(data, "tui.editor.cursorLineStart")) {
187
- this.lastAction = null;
188
- this.cursor = 0;
189
- return;
190
- }
191
-
192
- if (kb.matches(data, "tui.editor.cursorLineEnd")) {
193
- this.lastAction = null;
194
- this.cursor = this.value.length;
195
- return;
196
- }
197
-
198
- if (kb.matches(data, "tui.editor.cursorWordLeft")) {
199
- this.moveWordBackwards();
200
- return;
201
- }
202
-
203
- if (kb.matches(data, "tui.editor.cursorWordRight")) {
204
- this.moveWordForwards();
205
- return;
206
- }
207
-
208
- // Kitty CSI-u printable character (e.g. \x1b[97u for 'a').
209
- // Terminals with Kitty protocol flag 1 (disambiguate) send CSI-u for all keys,
210
- // including plain printable characters. Decode before the control-char check
211
- // since CSI-u sequences contain \x1b which would be rejected.
212
- const kittyPrintable = decodeKittyPrintable(data);
213
- if (kittyPrintable !== undefined) {
214
- this.insertCharacter(kittyPrintable);
215
- return;
216
- }
217
-
218
- // Regular character input - accept printable characters including Unicode,
219
- // but reject control characters (C0: 0x00-0x1F, DEL: 0x7F, C1: 0x80-0x9F)
220
- const hasControlChars = [...data].some((ch) => {
221
- const code = ch.charCodeAt(0);
222
- return code < 32 || code === 0x7f || (code >= 0x80 && code <= 0x9f);
223
- });
224
- if (!hasControlChars) {
225
- this.insertCharacter(data);
226
- }
227
- }
228
-
229
- handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
230
- if (event.type !== "press" || event.button !== "left" || event.y !== 0) return undefined;
231
- const visibleColumn = Math.max(0, event.x - 2);
232
- const targetColumn = this.renderedStartColumn + visibleColumn;
233
- let currentColumn = 0;
234
- this.cursor = this.value.length;
235
- for (const grapheme of segmenter.segment(this.value)) {
236
- const nextColumn = currentColumn + visibleWidth(grapheme.segment);
237
- if (targetColumn < nextColumn) {
238
- this.cursor = grapheme.index;
239
- break;
240
- }
241
- currentColumn = nextColumn;
242
- }
243
- this.lastAction = null;
244
- return { handled: true, focus: true };
245
- }
246
-
247
- private insertCharacter(char: string): void {
248
- // Undo coalescing: consecutive word chars coalesce into one undo unit
249
- if (isWhitespaceChar(char) || this.lastAction !== "type-word") {
250
- this.pushUndo();
251
- }
252
- this.lastAction = "type-word";
253
-
254
- this.value = this.value.slice(0, this.cursor) + char + this.value.slice(this.cursor);
255
- this.cursor += char.length;
256
- }
257
-
258
- private handleBackspace(): void {
259
- this.lastAction = null;
260
- if (this.cursor > 0) {
261
- this.pushUndo();
262
- const beforeCursor = this.value.slice(0, this.cursor);
263
- const graphemes = [...segmenter.segment(beforeCursor)];
264
- const lastGrapheme = graphemes[graphemes.length - 1];
265
- const graphemeLength = lastGrapheme ? lastGrapheme.segment.length : 1;
266
- this.value = this.value.slice(0, this.cursor - graphemeLength) + this.value.slice(this.cursor);
267
- this.cursor -= graphemeLength;
268
- }
269
- }
270
-
271
- private handleForwardDelete(): void {
272
- this.lastAction = null;
273
- if (this.cursor < this.value.length) {
274
- this.pushUndo();
275
- const afterCursor = this.value.slice(this.cursor);
276
- const graphemes = [...segmenter.segment(afterCursor)];
277
- const firstGrapheme = graphemes[0];
278
- const graphemeLength = firstGrapheme ? firstGrapheme.segment.length : 1;
279
- this.value = this.value.slice(0, this.cursor) + this.value.slice(this.cursor + graphemeLength);
280
- }
281
- }
282
-
283
- private deleteToLineStart(): void {
284
- if (this.cursor === 0) return;
285
- this.pushUndo();
286
- const deletedText = this.value.slice(0, this.cursor);
287
- this.killRing.push(deletedText, { prepend: true, accumulate: this.lastAction === "kill" });
288
- this.lastAction = "kill";
289
- this.value = this.value.slice(this.cursor);
290
- this.cursor = 0;
291
- }
292
-
293
- private deleteToLineEnd(): void {
294
- if (this.cursor >= this.value.length) return;
295
- this.pushUndo();
296
- const deletedText = this.value.slice(this.cursor);
297
- this.killRing.push(deletedText, { prepend: false, accumulate: this.lastAction === "kill" });
298
- this.lastAction = "kill";
299
- this.value = this.value.slice(0, this.cursor);
300
- }
301
-
302
- private deleteWordBackwards(): void {
303
- if (this.cursor === 0) return;
304
-
305
- // Save lastAction before cursor movement (moveWordBackwards resets it)
306
- const wasKill = this.lastAction === "kill";
307
-
308
- this.pushUndo();
309
-
310
- const oldCursor = this.cursor;
311
- this.moveWordBackwards();
312
- const deleteFrom = this.cursor;
313
- this.cursor = oldCursor;
314
-
315
- const deletedText = this.value.slice(deleteFrom, this.cursor);
316
- this.killRing.push(deletedText, { prepend: true, accumulate: wasKill });
317
- this.lastAction = "kill";
318
-
319
- this.value = this.value.slice(0, deleteFrom) + this.value.slice(this.cursor);
320
- this.cursor = deleteFrom;
321
- }
322
-
323
- private deleteWordForward(): void {
324
- if (this.cursor >= this.value.length) return;
325
-
326
- // Save lastAction before cursor movement (moveWordForwards resets it)
327
- const wasKill = this.lastAction === "kill";
328
-
329
- this.pushUndo();
330
-
331
- const oldCursor = this.cursor;
332
- this.moveWordForwards();
333
- const deleteTo = this.cursor;
334
- this.cursor = oldCursor;
335
-
336
- const deletedText = this.value.slice(this.cursor, deleteTo);
337
- this.killRing.push(deletedText, { prepend: false, accumulate: wasKill });
338
- this.lastAction = "kill";
339
-
340
- this.value = this.value.slice(0, this.cursor) + this.value.slice(deleteTo);
341
- }
342
-
343
- private yank(): void {
344
- const text = this.killRing.peek();
345
- if (!text) return;
346
-
347
- this.pushUndo();
348
-
349
- this.value = this.value.slice(0, this.cursor) + text + this.value.slice(this.cursor);
350
- this.cursor += text.length;
351
- this.lastAction = "yank";
352
- }
353
-
354
- private yankPop(): void {
355
- if (this.lastAction !== "yank" || this.killRing.length <= 1) return;
356
-
357
- this.pushUndo();
358
-
359
- // Delete the previously yanked text (still at end of ring before rotation)
360
- const prevText = this.killRing.peek() || "";
361
- this.value = this.value.slice(0, this.cursor - prevText.length) + this.value.slice(this.cursor);
362
- this.cursor -= prevText.length;
363
-
364
- // Rotate and insert new entry
365
- this.killRing.rotate();
366
- const text = this.killRing.peek() || "";
367
- this.value = this.value.slice(0, this.cursor) + text + this.value.slice(this.cursor);
368
- this.cursor += text.length;
369
- this.lastAction = "yank";
370
- }
371
-
372
- private pushUndo(): void {
373
- this.undoStack.push({ value: this.value, cursor: this.cursor });
374
- }
375
-
376
- private undo(): void {
377
- const snapshot = this.undoStack.pop();
378
- if (!snapshot) return;
379
- this.value = snapshot.value;
380
- this.cursor = snapshot.cursor;
381
- this.lastAction = null;
382
- }
383
-
384
- private moveWordBackwards(): void {
385
- if (this.cursor === 0) return;
386
- this.lastAction = null;
387
- this.cursor = findWordBackward(this.value, this.cursor);
388
- }
389
-
390
- private moveWordForwards(): void {
391
- if (this.cursor >= this.value.length) return;
392
- this.lastAction = null;
393
- this.cursor = findWordForward(this.value, this.cursor);
394
- }
395
-
396
- private handlePaste(pastedText: string): void {
397
- this.lastAction = null;
398
- this.pushUndo();
399
-
400
- // Clean the pasted text - remove newlines and carriage returns
401
- const cleanText = pastedText.replace(/\r\n/g, "").replace(/\r/g, "").replace(/\n/g, "").replace(/\t/g, " ");
402
-
403
- // Insert at cursor position
404
- this.value = this.value.slice(0, this.cursor) + cleanText + this.value.slice(this.cursor);
405
- this.cursor += cleanText.length;
406
- }
407
-
408
- invalidate(): void {
409
- // No cached state to invalidate currently
410
- }
411
-
412
- render(width: number): string[] {
413
- // Calculate visible window
414
- const availableWidth = width - visibleWidth(this.prompt);
415
-
416
- if (availableWidth <= 0) {
417
- return [truncateToWidth(this.prompt, width, "")];
418
- }
419
-
420
- if (this.value.length === 0 && this.placeholder) {
421
- const placeholder = truncateToWidth(this.placeholder, availableWidth, "");
422
- const graphemes = [...segmenter.segment(placeholder)];
423
- const atCursor = graphemes[0]?.segment ?? " ";
424
- const afterCursor = placeholder.slice(atCursor.length);
425
- const marker = this.focused ? CURSOR_MARKER : "";
426
- const cursorChar = `\x1b[7m${this.placeholderStyle(atCursor)}\x1b[27m`;
427
- const textWithCursor = marker + cursorChar + this.placeholderStyle(afterCursor);
428
- const padding = " ".repeat(Math.max(0, availableWidth - visibleWidth(textWithCursor)));
429
- return [this.prompt + textWithCursor + padding];
430
- }
431
-
432
- let visibleText = "";
433
- let cursorDisplay = this.cursor;
434
- this.renderedStartColumn = 0;
435
- const totalWidth = visibleWidth(this.value);
436
-
437
- if (totalWidth < availableWidth) {
438
- // Everything fits (leave room for cursor at end)
439
- visibleText = this.value;
440
- } else {
441
- // Need horizontal scrolling
442
- // Reserve one column for cursor if it's at the end
443
- const scrollWidth = this.cursor === this.value.length ? availableWidth - 1 : availableWidth;
444
- const cursorCol = visibleWidth(this.value.slice(0, this.cursor));
445
-
446
- if (scrollWidth > 0) {
447
- const halfWidth = Math.floor(scrollWidth / 2);
448
- let startCol = 0;
449
-
450
- if (cursorCol < halfWidth) {
451
- // Cursor near start
452
- startCol = 0;
453
- } else if (cursorCol > totalWidth - halfWidth) {
454
- // Cursor near end
455
- startCol = Math.max(0, totalWidth - scrollWidth);
456
- } else {
457
- // Cursor in middle
458
- startCol = Math.max(0, cursorCol - halfWidth);
459
- }
460
-
461
- this.renderedStartColumn = startCol;
462
- visibleText = sliceByColumn(this.value, startCol, scrollWidth, true);
463
- const beforeCursor = sliceByColumn(this.value, startCol, Math.max(0, cursorCol - startCol), true);
464
- cursorDisplay = beforeCursor.length;
465
- } else {
466
- visibleText = "";
467
- cursorDisplay = 0;
468
- }
469
- }
470
-
471
- // Build line with fake cursor
472
- // Insert cursor character at cursor position
473
- const graphemes = [...segmenter.segment(visibleText.slice(cursorDisplay))];
474
- const cursorGrapheme = graphemes[0];
475
-
476
- const beforeCursor = visibleText.slice(0, cursorDisplay);
477
- const atCursor = cursorGrapheme?.segment ?? " "; // Character at cursor, or space if at end
478
- const afterCursor = visibleText.slice(cursorDisplay + atCursor.length);
479
-
480
- // Hardware cursor marker (zero-width, emitted before fake cursor for IME positioning)
481
- const marker = this.focused ? CURSOR_MARKER : "";
482
-
483
- // Use inverse video to show cursor
484
- const cursorChar = `\x1b[7m${atCursor}\x1b[27m`; // ESC[7m = reverse video, ESC[27m = normal
485
- const textWithCursor = beforeCursor + marker + cursorChar + afterCursor;
486
-
487
- // Calculate visual width
488
- const visualLength = visibleWidth(textWithCursor);
489
- const padding = " ".repeat(Math.max(0, availableWidth - visualLength));
490
- const line = this.prompt + textWithCursor + padding;
491
-
492
- return [line];
493
- }
494
- }