@dungle-scrubs/tallow 0.8.21 → 0.8.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/dist/cli.js +35 -4
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config.d.ts +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/interactive-mode-patch.d.ts +2 -0
  6. package/dist/interactive-mode-patch.d.ts.map +1 -1
  7. package/dist/interactive-mode-patch.js +82 -0
  8. package/dist/interactive-mode-patch.js.map +1 -1
  9. package/dist/sdk.d.ts +17 -0
  10. package/dist/sdk.d.ts.map +1 -1
  11. package/dist/sdk.js +68 -1
  12. package/dist/sdk.js.map +1 -1
  13. package/dist/workspace-transition-relay.d.ts +40 -7
  14. package/dist/workspace-transition-relay.d.ts.map +1 -1
  15. package/dist/workspace-transition-relay.js +81 -16
  16. package/dist/workspace-transition-relay.js.map +1 -1
  17. package/extensions/__integration__/background-task-widget-ownership.test.ts +216 -0
  18. package/extensions/__integration__/claude-hooks-compat.test.ts +156 -0
  19. package/extensions/__integration__/slash-command-bridge.test.ts +169 -23
  20. package/extensions/_shared/atomic-write.ts +1 -1
  21. package/extensions/_shared/bordered-box.ts +102 -0
  22. package/extensions/_shared/interop-events.ts +5 -0
  23. package/extensions/_shared/pid-registry.ts +1 -1
  24. package/extensions/agent-commands-tool/index.ts +4 -1
  25. package/extensions/background-task-tool/__tests__/lifecycle.test.ts +50 -25
  26. package/extensions/background-task-tool/index.ts +139 -221
  27. package/extensions/bash-tool-enhanced/index.ts +1 -75
  28. package/extensions/cd-tool/index.ts +2 -2
  29. package/extensions/context-fork/spawn.ts +4 -1
  30. package/extensions/health/index.ts +6 -6
  31. package/extensions/hooks/__tests__/claude-compat.test.ts +35 -0
  32. package/extensions/hooks/__tests__/subprocess-hardening.test.ts +73 -0
  33. package/extensions/hooks/index.ts +27 -4
  34. package/extensions/loop/__tests__/loop.test.ts +168 -4
  35. package/extensions/loop/extension.json +6 -5
  36. package/extensions/loop/index.ts +242 -31
  37. package/extensions/plan-mode-tool/__tests__/agent-end-execution.test.ts +373 -0
  38. package/extensions/plan-mode-tool/index.ts +103 -41
  39. package/extensions/prompt-suggestions/__tests__/editor-compatibility.test.ts +42 -0
  40. package/extensions/prompt-suggestions/index.ts +41 -6
  41. package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +267 -671
  42. package/extensions/slash-command-bridge/extension.json +1 -1
  43. package/extensions/slash-command-bridge/index.ts +230 -116
  44. package/extensions/subagent-tool/index.ts +2 -2
  45. package/extensions/subagent-tool/process.ts +4 -5
  46. package/extensions/tasks/commands/register-tasks-extension.ts +41 -0
  47. package/extensions/teams-tool/__tests__/peer-messaging.test.ts +29 -24
  48. package/extensions/teams-tool/dashboard.ts +3 -5
  49. package/extensions/teams-tool/dispatch/auto-dispatch.ts +18 -1
  50. package/extensions/teams-tool/tools/teammate-tools.ts +9 -6
  51. package/extensions/wezterm-pane-control/__tests__/index.test.ts +88 -4
  52. package/extensions/wezterm-pane-control/index.ts +113 -8
  53. package/package.json +6 -4
  54. package/packages/tallow-tui/README.md +51 -0
  55. package/packages/tallow-tui/dist/autocomplete.d.ts +48 -0
  56. package/packages/tallow-tui/dist/autocomplete.d.ts.map +1 -0
  57. package/packages/tallow-tui/dist/autocomplete.js +564 -0
  58. package/packages/tallow-tui/dist/autocomplete.js.map +1 -0
  59. package/packages/tallow-tui/dist/border-styles.d.ts +32 -0
  60. package/packages/tallow-tui/dist/border-styles.d.ts.map +1 -0
  61. package/packages/tallow-tui/dist/border-styles.js +46 -0
  62. package/packages/tallow-tui/dist/border-styles.js.map +1 -0
  63. package/packages/tallow-tui/dist/components/bordered-box.d.ts +52 -0
  64. package/packages/tallow-tui/dist/components/bordered-box.d.ts.map +1 -0
  65. package/packages/tallow-tui/dist/components/bordered-box.js +89 -0
  66. package/packages/tallow-tui/dist/components/bordered-box.js.map +1 -0
  67. package/packages/tallow-tui/dist/components/box.d.ts +22 -0
  68. package/packages/tallow-tui/dist/components/box.d.ts.map +1 -0
  69. package/packages/tallow-tui/dist/components/box.js +104 -0
  70. package/packages/tallow-tui/dist/components/box.js.map +1 -0
  71. package/packages/tallow-tui/dist/components/cancellable-loader.d.ts +22 -0
  72. package/packages/tallow-tui/dist/components/cancellable-loader.d.ts.map +1 -0
  73. package/packages/tallow-tui/dist/components/cancellable-loader.js +35 -0
  74. package/packages/tallow-tui/dist/components/cancellable-loader.js.map +1 -0
  75. package/packages/tallow-tui/dist/components/editor.d.ts +240 -0
  76. package/packages/tallow-tui/dist/components/editor.d.ts.map +1 -0
  77. package/packages/tallow-tui/dist/components/editor.js +1766 -0
  78. package/packages/tallow-tui/dist/components/editor.js.map +1 -0
  79. package/packages/tallow-tui/dist/components/image.d.ts +126 -0
  80. package/packages/tallow-tui/dist/components/image.d.ts.map +1 -0
  81. package/packages/tallow-tui/dist/components/image.js +245 -0
  82. package/packages/tallow-tui/dist/components/image.js.map +1 -0
  83. package/packages/tallow-tui/dist/components/input.d.ts +37 -0
  84. package/packages/tallow-tui/dist/components/input.d.ts.map +1 -0
  85. package/packages/tallow-tui/dist/components/input.js +439 -0
  86. package/packages/tallow-tui/dist/components/input.js.map +1 -0
  87. package/packages/tallow-tui/dist/components/loader.d.ts +88 -0
  88. package/packages/tallow-tui/dist/components/loader.d.ts.map +1 -0
  89. package/packages/tallow-tui/dist/components/loader.js +146 -0
  90. package/packages/tallow-tui/dist/components/loader.js.map +1 -0
  91. package/packages/tallow-tui/dist/components/markdown.d.ts +95 -0
  92. package/packages/tallow-tui/dist/components/markdown.d.ts.map +1 -0
  93. package/packages/tallow-tui/dist/components/markdown.js +633 -0
  94. package/packages/tallow-tui/dist/components/markdown.js.map +1 -0
  95. package/packages/tallow-tui/dist/components/select-list.d.ts +32 -0
  96. package/packages/tallow-tui/dist/components/select-list.d.ts.map +1 -0
  97. package/packages/tallow-tui/dist/components/select-list.js +156 -0
  98. package/packages/tallow-tui/dist/components/select-list.js.map +1 -0
  99. package/packages/tallow-tui/dist/components/settings-list.d.ts +50 -0
  100. package/packages/tallow-tui/dist/components/settings-list.d.ts.map +1 -0
  101. package/packages/tallow-tui/dist/components/settings-list.js +189 -0
  102. package/packages/tallow-tui/dist/components/settings-list.js.map +1 -0
  103. package/packages/tallow-tui/dist/components/spacer.d.ts +12 -0
  104. package/packages/tallow-tui/dist/components/spacer.d.ts.map +1 -0
  105. package/packages/tallow-tui/dist/components/spacer.js +23 -0
  106. package/packages/tallow-tui/dist/components/spacer.js.map +1 -0
  107. package/packages/tallow-tui/dist/components/text.d.ts +19 -0
  108. package/packages/tallow-tui/dist/components/text.d.ts.map +1 -0
  109. package/packages/tallow-tui/dist/components/text.js +91 -0
  110. package/packages/tallow-tui/dist/components/text.js.map +1 -0
  111. package/packages/tallow-tui/dist/components/truncated-text.d.ts +13 -0
  112. package/packages/tallow-tui/dist/components/truncated-text.d.ts.map +1 -0
  113. package/packages/tallow-tui/dist/components/truncated-text.js +51 -0
  114. package/packages/tallow-tui/dist/components/truncated-text.js.map +1 -0
  115. package/packages/tallow-tui/dist/editor-component.d.ts +50 -0
  116. package/packages/tallow-tui/dist/editor-component.d.ts.map +1 -0
  117. package/packages/tallow-tui/dist/editor-component.js +2 -0
  118. package/packages/tallow-tui/dist/editor-component.js.map +1 -0
  119. package/packages/tallow-tui/dist/fuzzy.d.ts +16 -0
  120. package/packages/tallow-tui/dist/fuzzy.d.ts.map +1 -0
  121. package/packages/tallow-tui/dist/fuzzy.js +107 -0
  122. package/packages/tallow-tui/dist/fuzzy.js.map +1 -0
  123. package/packages/tallow-tui/dist/index.d.ts +25 -0
  124. package/packages/tallow-tui/dist/index.d.ts.map +1 -0
  125. package/packages/tallow-tui/dist/index.js +35 -0
  126. package/packages/tallow-tui/dist/index.js.map +1 -0
  127. package/packages/tallow-tui/dist/keybindings.d.ts +39 -0
  128. package/packages/tallow-tui/dist/keybindings.d.ts.map +1 -0
  129. package/packages/tallow-tui/dist/keybindings.js +114 -0
  130. package/packages/tallow-tui/dist/keybindings.js.map +1 -0
  131. package/packages/tallow-tui/dist/keys.d.ts +168 -0
  132. package/packages/tallow-tui/dist/keys.d.ts.map +1 -0
  133. package/packages/tallow-tui/dist/keys.js +971 -0
  134. package/packages/tallow-tui/dist/keys.js.map +1 -0
  135. package/packages/tallow-tui/dist/kill-ring.d.ts +28 -0
  136. package/packages/tallow-tui/dist/kill-ring.d.ts.map +1 -0
  137. package/packages/tallow-tui/dist/kill-ring.js +44 -0
  138. package/packages/tallow-tui/dist/kill-ring.js.map +1 -0
  139. package/packages/tallow-tui/dist/stdin-buffer.d.ts +48 -0
  140. package/packages/tallow-tui/dist/stdin-buffer.d.ts.map +1 -0
  141. package/packages/tallow-tui/dist/stdin-buffer.js +317 -0
  142. package/packages/tallow-tui/dist/stdin-buffer.js.map +1 -0
  143. package/packages/tallow-tui/dist/terminal-image.d.ts +161 -0
  144. package/packages/tallow-tui/dist/terminal-image.d.ts.map +1 -0
  145. package/packages/tallow-tui/dist/terminal-image.js +460 -0
  146. package/packages/tallow-tui/dist/terminal-image.js.map +1 -0
  147. package/packages/tallow-tui/dist/terminal.d.ts +102 -0
  148. package/packages/tallow-tui/dist/terminal.d.ts.map +1 -0
  149. package/packages/tallow-tui/dist/terminal.js +263 -0
  150. package/packages/tallow-tui/dist/terminal.js.map +1 -0
  151. package/packages/tallow-tui/dist/test-utils/capability-env.d.ts +14 -0
  152. package/packages/tallow-tui/dist/test-utils/capability-env.d.ts.map +1 -0
  153. package/packages/tallow-tui/dist/test-utils/capability-env.js +55 -0
  154. package/packages/tallow-tui/dist/test-utils/capability-env.js.map +1 -0
  155. package/packages/tallow-tui/dist/tui.d.ts +239 -0
  156. package/packages/tallow-tui/dist/tui.d.ts.map +1 -0
  157. package/packages/tallow-tui/dist/tui.js +1058 -0
  158. package/packages/tallow-tui/dist/tui.js.map +1 -0
  159. package/packages/tallow-tui/dist/undo-stack.d.ts +17 -0
  160. package/packages/tallow-tui/dist/undo-stack.d.ts.map +1 -0
  161. package/packages/tallow-tui/dist/undo-stack.js +25 -0
  162. package/packages/tallow-tui/dist/undo-stack.js.map +1 -0
  163. package/packages/tallow-tui/dist/utils.d.ts +96 -0
  164. package/packages/tallow-tui/dist/utils.d.ts.map +1 -0
  165. package/packages/tallow-tui/dist/utils.js +843 -0
  166. package/packages/tallow-tui/dist/utils.js.map +1 -0
  167. package/packages/tallow-tui/package.json +24 -0
  168. package/packages/tallow-tui/src/__tests__/__snapshots__/render.test.ts.snap +121 -0
  169. package/packages/tallow-tui/src/__tests__/editor-border.test.ts +72 -0
  170. package/packages/tallow-tui/src/__tests__/editor-change-listener.test.ts +121 -0
  171. package/packages/tallow-tui/src/__tests__/editor-ghost-text.test.ts +112 -0
  172. package/packages/tallow-tui/src/__tests__/fuzzy.test.ts +91 -0
  173. package/packages/tallow-tui/src/__tests__/image-component.test.ts +113 -0
  174. package/packages/tallow-tui/src/__tests__/keys.test.ts +141 -0
  175. package/packages/tallow-tui/src/__tests__/render.test.ts +179 -0
  176. package/packages/tallow-tui/src/__tests__/stdin-buffer.test.ts +82 -0
  177. package/packages/tallow-tui/src/__tests__/terminal-image.test.ts +363 -0
  178. package/packages/tallow-tui/src/__tests__/tui-diff-regression.test.ts +454 -0
  179. package/packages/tallow-tui/src/__tests__/tui-render-scheduling.test.ts +256 -0
  180. package/packages/tallow-tui/src/__tests__/utils.test.ts +259 -0
  181. package/packages/tallow-tui/src/autocomplete.ts +716 -0
  182. package/packages/tallow-tui/src/border-styles.ts +60 -0
  183. package/packages/tallow-tui/src/components/bordered-box.ts +113 -0
  184. package/packages/tallow-tui/src/components/box.ts +137 -0
  185. package/packages/tallow-tui/src/components/cancellable-loader.ts +40 -0
  186. package/packages/tallow-tui/src/components/editor.ts +2143 -0
  187. package/packages/tallow-tui/src/components/image.ts +315 -0
  188. package/packages/tallow-tui/src/components/input.ts +522 -0
  189. package/packages/tallow-tui/src/components/loader.ts +187 -0
  190. package/packages/tallow-tui/src/components/markdown.ts +780 -0
  191. package/packages/tallow-tui/src/components/select-list.ts +197 -0
  192. package/packages/tallow-tui/src/components/settings-list.ts +264 -0
  193. package/packages/tallow-tui/src/components/spacer.ts +28 -0
  194. package/packages/tallow-tui/src/components/text.ts +113 -0
  195. package/packages/tallow-tui/src/components/truncated-text.ts +65 -0
  196. package/packages/tallow-tui/src/editor-component.ts +92 -0
  197. package/packages/tallow-tui/src/fuzzy.ts +133 -0
  198. package/packages/tallow-tui/src/index.ts +118 -0
  199. package/packages/tallow-tui/src/keybindings.ts +183 -0
  200. package/packages/tallow-tui/src/keys.ts +1189 -0
  201. package/packages/tallow-tui/src/kill-ring.ts +46 -0
  202. package/packages/tallow-tui/src/stdin-buffer.ts +386 -0
  203. package/packages/tallow-tui/src/terminal-image.ts +619 -0
  204. package/packages/tallow-tui/src/terminal.ts +350 -0
  205. package/packages/tallow-tui/src/test-utils/capability-env.ts +56 -0
  206. package/packages/tallow-tui/src/tui.ts +1336 -0
  207. package/packages/tallow-tui/src/undo-stack.ts +28 -0
  208. package/packages/tallow-tui/src/utils.ts +948 -0
  209. package/packages/tallow-tui/tsconfig.build.json +21 -0
  210. package/runtime/agent-runner.ts +20 -0
  211. package/runtime/atomic-write.ts +8 -0
  212. package/runtime/otel.ts +12 -0
  213. package/runtime/resolve-module.ts +23 -0
  214. package/runtime/runtime-path-provider.ts +12 -0
  215. package/runtime/runtime-provenance.ts +17 -0
  216. package/runtime/workspace-transition-relay.ts +21 -0
  217. package/runtime/workspace-transition.ts +29 -0
@@ -0,0 +1,113 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { Image, type ImageTheme } from "../components/image.js";
3
+ import { withCapabilityEnv } from "../test-utils/capability-env.js";
4
+
5
+ const IDENTITY_THEME: ImageTheme = {
6
+ fallbackColor: (text) => text,
7
+ };
8
+
9
+ /**
10
+ * Extract Kitty `c=<columns>` param from rendered image lines.
11
+ *
12
+ * @param lines - Rendered TUI lines from Image.render
13
+ * @returns Parsed Kitty column count, or null if sequence not present
14
+ */
15
+ function getKittyColumns(lines: readonly string[]): number | null {
16
+ for (const line of lines) {
17
+ const start = line.indexOf("\x1b_G");
18
+ if (start < 0) {
19
+ continue;
20
+ }
21
+ const match = line.slice(start).match(/c=(\d+)/);
22
+ if (match) {
23
+ return Number(match[1]);
24
+ }
25
+ }
26
+ return null;
27
+ }
28
+
29
+ describe("Image component narrow-pane rendering", () => {
30
+ it("applies one safe width contract for border and no-border rendering", () => {
31
+ withCapabilityEnv({ TERM_PROGRAM: "kitty" }, () => {
32
+ const dimensions = { widthPx: 4000, heightPx: 2500 };
33
+ const cases = [
34
+ { border: false, expectedColumns: 1, width: 1 },
35
+ { border: false, expectedColumns: 6, width: 8 },
36
+ { border: false, expectedColumns: 18, width: 20 },
37
+ { border: true, expectedColumns: 1, width: 1 },
38
+ { border: true, expectedColumns: 2, width: 8 },
39
+ { border: true, expectedColumns: 14, width: 20 },
40
+ ] as const;
41
+
42
+ for (const testCase of cases) {
43
+ const image = new Image(
44
+ "AA==",
45
+ "image/png",
46
+ IDENTITY_THEME,
47
+ { border: testCase.border, maxHeightCells: 200 },
48
+ dimensions
49
+ );
50
+ const lines = image.render(testCase.width);
51
+ expect(lines.length).toBeGreaterThan(0);
52
+ expect(getKittyColumns(lines)).toBe(testCase.expectedColumns);
53
+ }
54
+ });
55
+ });
56
+
57
+ it("renders portrait images at narrow widths with valid positive geometry", () => {
58
+ withCapabilityEnv({ TERM_PROGRAM: "kitty" }, () => {
59
+ const image = new Image(
60
+ "AA==",
61
+ "image/png",
62
+ IDENTITY_THEME,
63
+ { border: false, maxHeightCells: 200 },
64
+ { widthPx: 1179, heightPx: 2556 }
65
+ );
66
+
67
+ for (const width of [8, 12, 20, 30]) {
68
+ const lines = image.render(width);
69
+ expect(lines.some((line) => line.includes("\x1b_G"))).toBe(true);
70
+ const columns = getKittyColumns(lines);
71
+ expect(columns).not.toBeNull();
72
+ expect(columns).toBeGreaterThanOrEqual(1);
73
+ }
74
+ });
75
+ });
76
+
77
+ it("renders landscape images in narrow panes for both border modes", () => {
78
+ withCapabilityEnv({ TERM_PROGRAM: "kitty" }, () => {
79
+ const dimensions = { widthPx: 2400, heightPx: 1200 };
80
+ for (const border of [false, true]) {
81
+ const image = new Image(
82
+ "AA==",
83
+ "image/png",
84
+ IDENTITY_THEME,
85
+ { border, maxHeightCells: 200 },
86
+ dimensions
87
+ );
88
+ const lines = image.render(12);
89
+ expect(lines.length).toBeGreaterThan(0);
90
+ expect(getKittyColumns(lines)).toBeGreaterThanOrEqual(1);
91
+ }
92
+ });
93
+ });
94
+
95
+ it("degrades safely for ultra-narrow pane widths", () => {
96
+ withCapabilityEnv({ TERM_PROGRAM: "kitty" }, () => {
97
+ for (const border of [false, true]) {
98
+ const image = new Image(
99
+ "AA==",
100
+ "image/png",
101
+ IDENTITY_THEME,
102
+ { border, maxHeightCells: 200 },
103
+ { widthPx: 1200, heightPx: 1800 }
104
+ );
105
+
106
+ expect(() => image.render(0)).not.toThrow();
107
+ expect(() => image.render(1)).not.toThrow();
108
+ expect(getKittyColumns(image.render(0))).toBe(1);
109
+ expect(getKittyColumns(image.render(1))).toBe(1);
110
+ }
111
+ });
112
+ });
113
+ });
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Tests for tallow-tui key parsing: parseKey, matchesKey, isKeyRelease, isKeyRepeat.
3
+ */
4
+ import { describe, expect, it } from "bun:test";
5
+ import { isKeyRelease, isKeyRepeat, matchesKey, parseKey } from "../keys.js";
6
+
7
+ // ── parseKey ─────────────────────────────────────────────────────────────────
8
+
9
+ describe("parseKey", () => {
10
+ it("parses printable ASCII letter", () => {
11
+ expect(parseKey("a")).toBe("a");
12
+ });
13
+
14
+ it("parses enter (carriage return)", () => {
15
+ expect(parseKey("\r")).toBe("enter");
16
+ });
17
+
18
+ it("parses escape", () => {
19
+ expect(parseKey("\x1b")).toBe("escape");
20
+ });
21
+
22
+ it("parses tab", () => {
23
+ expect(parseKey("\t")).toBe("tab");
24
+ });
25
+
26
+ it("parses space", () => {
27
+ expect(parseKey(" ")).toBe("space");
28
+ });
29
+
30
+ it("parses arrow up", () => {
31
+ expect(parseKey("\x1b[A")).toBe("up");
32
+ });
33
+
34
+ it("parses arrow down", () => {
35
+ expect(parseKey("\x1b[B")).toBe("down");
36
+ });
37
+
38
+ it("parses arrow right", () => {
39
+ expect(parseKey("\x1b[C")).toBe("right");
40
+ });
41
+
42
+ it("parses arrow left", () => {
43
+ expect(parseKey("\x1b[D")).toBe("left");
44
+ });
45
+
46
+ it("parses ctrl+c", () => {
47
+ expect(parseKey("\x03")).toBe("ctrl+c");
48
+ });
49
+
50
+ it("parses backspace (DEL)", () => {
51
+ expect(parseKey("\x7f")).toBe("backspace");
52
+ });
53
+
54
+ it("parses backspace (BS)", () => {
55
+ expect(parseKey("\x08")).toBe("backspace");
56
+ });
57
+
58
+ it("parses home", () => {
59
+ expect(parseKey("\x1b[H")).toBe("home");
60
+ });
61
+
62
+ it("parses end", () => {
63
+ expect(parseKey("\x1b[F")).toBe("end");
64
+ });
65
+
66
+ it("parses delete", () => {
67
+ expect(parseKey("\x1b[3~")).toBe("delete");
68
+ });
69
+
70
+ it("parses shift+tab", () => {
71
+ expect(parseKey("\x1b[Z")).toBe("shift+tab");
72
+ });
73
+
74
+ it("parses ctrl+space", () => {
75
+ expect(parseKey("\x00")).toBe("ctrl+space");
76
+ });
77
+
78
+ it("returns undefined for unrecognized sequences", () => {
79
+ expect(parseKey("\x1b[999z")).toBeUndefined();
80
+ });
81
+ });
82
+
83
+ // ── matchesKey ───────────────────────────────────────────────────────────────
84
+
85
+ describe("matchesKey", () => {
86
+ it("matches arrow up", () => {
87
+ expect(matchesKey("\x1b[A", "up")).toBe(true);
88
+ });
89
+
90
+ it("matches enter", () => {
91
+ expect(matchesKey("\r", "enter")).toBe(true);
92
+ });
93
+
94
+ it("matches escape", () => {
95
+ expect(matchesKey("\x1b", "escape")).toBe(true);
96
+ });
97
+
98
+ it("rejects non-matching key", () => {
99
+ expect(matchesKey("\x1b[A", "down")).toBe(false);
100
+ });
101
+
102
+ it("matches tab", () => {
103
+ expect(matchesKey("\t", "tab")).toBe(true);
104
+ });
105
+
106
+ it("matches backspace", () => {
107
+ expect(matchesKey("\x7f", "backspace")).toBe(true);
108
+ });
109
+ });
110
+
111
+ // ── isKeyRelease ─────────────────────────────────────────────────────────────
112
+
113
+ describe("isKeyRelease", () => {
114
+ it("returns false for regular ASCII input", () => {
115
+ expect(isKeyRelease("a")).toBe(false);
116
+ });
117
+
118
+ it("returns false for legacy arrow sequences", () => {
119
+ expect(isKeyRelease("\x1b[A")).toBe(false);
120
+ });
121
+
122
+ it("returns false for empty string", () => {
123
+ expect(isKeyRelease("")).toBe(false);
124
+ });
125
+ });
126
+
127
+ // ── isKeyRepeat ──────────────────────────────────────────────────────────────
128
+
129
+ describe("isKeyRepeat", () => {
130
+ it("returns false for regular ASCII input", () => {
131
+ expect(isKeyRepeat("a")).toBe(false);
132
+ });
133
+
134
+ it("returns false for legacy arrow sequences", () => {
135
+ expect(isKeyRepeat("\x1b[A")).toBe(false);
136
+ });
137
+
138
+ it("returns false for empty string", () => {
139
+ expect(isKeyRepeat("")).toBe(false);
140
+ });
141
+ });
@@ -0,0 +1,179 @@
1
+ /**
2
+ * TUI component snapshot tests.
3
+ *
4
+ * Renders components to plaintext and compares against snapshots.
5
+ * Run `bun test --update-snapshots` to regenerate after intentional changes.
6
+ */
7
+ import { describe, expect, it } from "bun:test";
8
+ import { renderSnapshot } from "../../../../test-utils/virtual-terminal.js";
9
+ import { BorderedBox } from "../components/bordered-box.js";
10
+ import { Markdown, type MarkdownTheme } from "../components/markdown.js";
11
+ import { Text } from "../components/text.js";
12
+ import { TruncatedText } from "../components/truncated-text.js";
13
+
14
+ // ── Helpers ──────────────────────────────────────────────────────────────────
15
+
16
+ /** Identity theme — all styling functions return input unchanged. */
17
+ const identityTheme: MarkdownTheme = {
18
+ heading: (t) => t,
19
+ link: (t) => t,
20
+ linkUrl: (t) => t,
21
+ code: (t) => t,
22
+ codeBlock: (t) => t,
23
+ codeBlockBorder: (t) => t,
24
+ quote: (t) => t,
25
+ quoteBorder: (t) => t,
26
+ hr: (t) => t,
27
+ listBullet: (t) => t,
28
+ bold: (t) => t,
29
+ italic: (t) => t,
30
+ strikethrough: (t) => t,
31
+ underline: (t) => t,
32
+ };
33
+
34
+ // ════════════════════════════════════════════════════════════════
35
+ // Text
36
+ // ════════════════════════════════════════════════════════════════
37
+
38
+ describe("Text", () => {
39
+ it("renders plain text at width 40", () => {
40
+ const text = new Text("Hello, world!", 0, 0);
41
+ expect(renderSnapshot(text, 40)).toMatchSnapshot();
42
+ });
43
+
44
+ it("wraps long text at narrow width", () => {
45
+ const text = new Text("This is a longer piece of text that should wrap at narrow widths", 0, 0);
46
+ expect(renderSnapshot(text, 20)).toMatchSnapshot();
47
+ });
48
+
49
+ it("renders with padding", () => {
50
+ const text = new Text("Padded text", 2, 1);
51
+ expect(renderSnapshot(text, 40)).toMatchSnapshot();
52
+ });
53
+
54
+ it("handles empty text", () => {
55
+ const text = new Text("", 0, 0);
56
+ expect(renderSnapshot(text, 40)).toMatchSnapshot();
57
+ });
58
+
59
+ it("handles multiline text", () => {
60
+ const text = new Text("Line one\nLine two\nLine three", 0, 0);
61
+ expect(renderSnapshot(text, 40)).toMatchSnapshot();
62
+ });
63
+ });
64
+
65
+ // ════════════════════════════════════════════════════════════════
66
+ // Markdown
67
+ // ════════════════════════════════════════════════════════════════
68
+
69
+ /** Create a Markdown component with identity theme and no padding. */
70
+ function md(text: string): Markdown {
71
+ return new Markdown(text, 0, 0, identityTheme);
72
+ }
73
+
74
+ describe("Markdown", () => {
75
+ it("renders heading", () => {
76
+ expect(renderSnapshot(md("# Hello World"), 40)).toMatchSnapshot();
77
+ });
78
+
79
+ it("renders paragraph text", () => {
80
+ expect(
81
+ renderSnapshot(md("Some paragraph text that should be rendered normally."), 40)
82
+ ).toMatchSnapshot();
83
+ });
84
+
85
+ it("renders code block", () => {
86
+ expect(renderSnapshot(md("```ts\nconst x = 1;\nconst y = 2;\n```"), 40)).toMatchSnapshot();
87
+ });
88
+
89
+ it("renders unordered list", () => {
90
+ expect(renderSnapshot(md("- Item one\n- Item two\n- Item three"), 40)).toMatchSnapshot();
91
+ });
92
+
93
+ it("renders ordered list", () => {
94
+ expect(renderSnapshot(md("1. First\n2. Second\n3. Third"), 40)).toMatchSnapshot();
95
+ });
96
+
97
+ it("renders blockquote", () => {
98
+ expect(renderSnapshot(md("> This is a quote"), 40)).toMatchSnapshot();
99
+ });
100
+
101
+ it("renders at narrow width (20)", () => {
102
+ expect(
103
+ renderSnapshot(
104
+ md("# Hello World\n\nSome paragraph text that should wrap at this narrow width."),
105
+ 20
106
+ )
107
+ ).toMatchSnapshot();
108
+ });
109
+
110
+ it("renders inline formatting", () => {
111
+ expect(renderSnapshot(md("Text with **bold** and *italic* and `code`."), 60)).toMatchSnapshot();
112
+ });
113
+
114
+ it("renders horizontal rule", () => {
115
+ expect(renderSnapshot(md("Above\n\n---\n\nBelow"), 40)).toMatchSnapshot();
116
+ });
117
+
118
+ it("handles empty content", () => {
119
+ expect(renderSnapshot(md(""), 40)).toMatchSnapshot();
120
+ });
121
+ });
122
+
123
+ // ════════════════════════════════════════════════════════════════
124
+ // BorderedBox
125
+ // ════════════════════════════════════════════════════════════════
126
+
127
+ describe("BorderedBox", () => {
128
+ it("renders content with default border", () => {
129
+ const box = new BorderedBox(["Hello", "World"]);
130
+ expect(renderSnapshot(box, 30)).toMatchSnapshot();
131
+ });
132
+
133
+ it("renders with title", () => {
134
+ const box = new BorderedBox(["Content line"], { title: "My Box" });
135
+ expect(renderSnapshot(box, 30)).toMatchSnapshot();
136
+ });
137
+
138
+ it("renders with custom padding", () => {
139
+ const box = new BorderedBox(["Padded"], { paddingX: 3 });
140
+ expect(renderSnapshot(box, 30)).toMatchSnapshot();
141
+ });
142
+
143
+ it("handles empty content", () => {
144
+ const box = new BorderedBox([]);
145
+ expect(renderSnapshot(box, 30)).toMatchSnapshot();
146
+ });
147
+
148
+ it("handles very narrow width", () => {
149
+ const box = new BorderedBox(["Some content"]);
150
+ // At width 5, inner width = 5 - 2 - 2 = 1
151
+ expect(renderSnapshot(box, 5)).toMatchSnapshot();
152
+ });
153
+ });
154
+
155
+ // ════════════════════════════════════════════════════════════════
156
+ // TruncatedText
157
+ // ════════════════════════════════════════════════════════════════
158
+
159
+ describe("TruncatedText", () => {
160
+ it("renders short text without truncation", () => {
161
+ const tt = new TruncatedText("Short");
162
+ expect(renderSnapshot(tt, 40)).toMatchSnapshot();
163
+ });
164
+
165
+ it("truncates long text", () => {
166
+ const tt = new TruncatedText("This text is much too long to fit in a narrow terminal");
167
+ expect(renderSnapshot(tt, 20)).toMatchSnapshot();
168
+ });
169
+
170
+ it("renders with padding", () => {
171
+ const tt = new TruncatedText("Padded", 2, 1);
172
+ expect(renderSnapshot(tt, 30)).toMatchSnapshot();
173
+ });
174
+
175
+ it("handles multiline text (takes first line)", () => {
176
+ const tt = new TruncatedText("First line\nSecond line");
177
+ expect(renderSnapshot(tt, 40)).toMatchSnapshot();
178
+ });
179
+ });
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Tests for tallow-tui StdinBuffer: sequence extraction and event emission.
3
+ * Tests the public StdinBuffer class interface since extractCompleteSequences is private.
4
+ */
5
+ import { describe, expect, it } from "bun:test";
6
+ import { StdinBuffer } from "../stdin-buffer.js";
7
+
8
+ /**
9
+ * Collects all 'data' events emitted by a StdinBuffer during a synchronous process() call.
10
+ * @param input - Raw input string to feed
11
+ * @returns Array of emitted sequence strings
12
+ */
13
+ function collectSequences(input: string): string[] {
14
+ const sequences: string[] = [];
15
+ const buffer = new StdinBuffer({ timeout: 0 });
16
+ buffer.on("data", (seq: string) => sequences.push(seq));
17
+ buffer.process(input);
18
+ return sequences;
19
+ }
20
+
21
+ // ── StdinBuffer sequence extraction ──────────────────────────────────────────
22
+
23
+ describe("StdinBuffer", () => {
24
+ it("emits single ASCII character", () => {
25
+ const seqs = collectSequences("a");
26
+ expect(seqs).toContain("a");
27
+ });
28
+
29
+ it("emits each character separately for plain text", () => {
30
+ const seqs = collectSequences("abc");
31
+ expect(seqs).toEqual(["a", "b", "c"]);
32
+ });
33
+
34
+ it("emits complete CSI arrow sequence as one unit", () => {
35
+ const seqs = collectSequences("\x1b[A");
36
+ expect(seqs).toEqual(["\x1b[A"]);
37
+ });
38
+
39
+ it("emits complete SGR sequence as one unit", () => {
40
+ const seqs = collectSequences("\x1b[31m");
41
+ expect(seqs).toEqual(["\x1b[31m"]);
42
+ });
43
+
44
+ it("handles escape followed by printable as separate events", () => {
45
+ // In legacy mode, ESC + letter can be alt+letter
46
+ const seqs = collectSequences("\x1ba");
47
+ expect(seqs.length).toBeGreaterThanOrEqual(1);
48
+ });
49
+
50
+ it("handles multiple CSI sequences in one chunk", () => {
51
+ const seqs = collectSequences("\x1b[A\x1b[B");
52
+ expect(seqs).toEqual(["\x1b[A", "\x1b[B"]);
53
+ });
54
+
55
+ it("handles mixed ASCII and escape sequences", () => {
56
+ const seqs = collectSequences("a\x1b[Ab");
57
+ expect(seqs).toEqual(["a", "\x1b[A", "b"]);
58
+ });
59
+
60
+ it("handles bracketed paste via paste event", () => {
61
+ const pastes: string[] = [];
62
+ const buffer = new StdinBuffer({ timeout: 0 });
63
+ buffer.on("paste", (content: string) => pastes.push(content));
64
+ buffer.process("\x1b[200~hello world\x1b[201~");
65
+ expect(pastes).toEqual(["hello world"]);
66
+ });
67
+
68
+ it("handles delete key sequence", () => {
69
+ const seqs = collectSequences("\x1b[3~");
70
+ expect(seqs).toEqual(["\x1b[3~"]);
71
+ });
72
+
73
+ it("emits empty string for empty input", () => {
74
+ const seqs = collectSequences("");
75
+ expect(seqs).toEqual([""]);
76
+ });
77
+
78
+ it("handles ctrl characters", () => {
79
+ const seqs = collectSequences("\x03"); // ctrl+c
80
+ expect(seqs).toEqual(["\x03"]);
81
+ });
82
+ });