@dungle-scrubs/tallow 0.9.4 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/dist/cli.js +7 -4
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config.d.ts +1 -1
  4. package/dist/config.js +1 -1
  5. package/dist/interactive-mode-patch.d.ts +24 -12
  6. package/dist/interactive-mode-patch.d.ts.map +1 -1
  7. package/dist/interactive-mode-patch.js +229 -146
  8. package/dist/interactive-mode-patch.js.map +1 -1
  9. package/dist/interactive-reset.d.ts +49 -0
  10. package/dist/interactive-reset.d.ts.map +1 -0
  11. package/dist/interactive-reset.js +40 -0
  12. package/dist/interactive-reset.js.map +1 -0
  13. package/dist/pi-tui-editor-patch.d.ts +10 -0
  14. package/dist/pi-tui-editor-patch.d.ts.map +1 -0
  15. package/dist/pi-tui-editor-patch.js +159 -0
  16. package/dist/pi-tui-editor-patch.js.map +1 -0
  17. package/dist/pi-tui-patch.d.ts +2 -0
  18. package/dist/pi-tui-patch.d.ts.map +1 -0
  19. package/dist/pi-tui-patch.js +563 -0
  20. package/dist/pi-tui-patch.js.map +1 -0
  21. package/dist/pi-tui-settings-list-patch.d.ts +11 -0
  22. package/dist/pi-tui-settings-list-patch.d.ts.map +1 -0
  23. package/dist/pi-tui-settings-list-patch.js +38 -0
  24. package/dist/pi-tui-settings-list-patch.js.map +1 -0
  25. package/dist/reset-diagnostics.d.ts +69 -0
  26. package/dist/reset-diagnostics.d.ts.map +1 -0
  27. package/dist/reset-diagnostics.js +41 -0
  28. package/dist/reset-diagnostics.js.map +1 -0
  29. package/dist/sdk.d.ts +5 -21
  30. package/dist/sdk.d.ts.map +1 -1
  31. package/dist/sdk.js +180 -149
  32. package/dist/sdk.js.map +1 -1
  33. package/dist/workspace-transition-interactive.d.ts +1 -0
  34. package/dist/workspace-transition-interactive.d.ts.map +1 -1
  35. package/dist/workspace-transition-interactive.js +7 -17
  36. package/dist/workspace-transition-interactive.js.map +1 -1
  37. package/extensions/__integration__/audit-findings.test.ts +4 -5
  38. package/extensions/_icons/index.ts +2 -4
  39. package/extensions/_shared/__tests__/image-metadata.test.ts +33 -0
  40. package/extensions/_shared/__tests__/terminal-links.test.ts +18 -0
  41. package/extensions/_shared/image-metadata.ts +99 -0
  42. package/extensions/_shared/inline-preview.ts +1 -1
  43. package/extensions/_shared/terminal-links.ts +22 -0
  44. package/extensions/ask-user-question-tool/index.ts +0 -3
  45. package/extensions/clear/__tests__/clear.test.ts +269 -2
  46. package/extensions/command-expansion/index.ts +1 -1
  47. package/extensions/context-files/index.ts +5 -1
  48. package/extensions/context-fork/__tests__/context-fork.test.ts +94 -1
  49. package/extensions/context-fork/extension.json +1 -1
  50. package/extensions/context-fork/index.ts +32 -0
  51. package/extensions/edit-tool-enhanced/index.ts +2 -1
  52. package/extensions/hooks/index.ts +33 -11
  53. package/extensions/loop/index.ts +14 -1
  54. package/extensions/lsp/index.ts +64 -13
  55. package/extensions/lsp/package.json +2 -2
  56. package/extensions/random-spinner/index.ts +7 -642
  57. package/extensions/read-tool-enhanced/index.ts +6 -8
  58. package/extensions/render-stabilizer/__tests__/render-stabilizer.test.ts +2 -3
  59. package/extensions/render-stabilizer/index.ts +6 -6
  60. package/extensions/slash-command-bridge/__tests__/slash-command-bridge.test.ts +26 -0
  61. package/extensions/slash-command-bridge/index.ts +14 -2
  62. package/extensions/subagent-tool/model-resolver.ts +274 -7
  63. package/extensions/tasks/commands/register-tasks-extension.ts +9 -9
  64. package/extensions/teams-tool/tools/register-extension.ts +1 -3
  65. package/extensions/web-search-tool/index.ts +2 -1
  66. package/extensions/write-tool-enhanced/index.ts +2 -1
  67. package/node_modules/@mariozechner/pi-tui/README.md +56 -34
  68. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts +18 -13
  69. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.d.ts.map +1 -1
  70. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js +182 -113
  71. package/node_modules/@mariozechner/pi-tui/dist/autocomplete.js.map +1 -1
  72. package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js +3 -3
  73. package/node_modules/@mariozechner/pi-tui/dist/components/cancellable-loader.js.map +1 -1
  74. package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts +45 -36
  75. package/node_modules/@mariozechner/pi-tui/dist/components/editor.d.ts.map +1 -1
  76. package/node_modules/@mariozechner/pi-tui/dist/components/editor.js +489 -325
  77. package/node_modules/@mariozechner/pi-tui/dist/components/editor.js.map +1 -1
  78. package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts +1 -99
  79. package/node_modules/@mariozechner/pi-tui/dist/components/image.d.ts.map +1 -1
  80. package/node_modules/@mariozechner/pi-tui/dist/components/image.js +17 -192
  81. package/node_modules/@mariozechner/pi-tui/dist/components/image.js.map +1 -1
  82. package/node_modules/@mariozechner/pi-tui/dist/components/input.d.ts.map +1 -1
  83. package/node_modules/@mariozechner/pi-tui/dist/components/input.js +57 -60
  84. package/node_modules/@mariozechner/pi-tui/dist/components/input.js.map +1 -1
  85. package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts +2 -69
  86. package/node_modules/@mariozechner/pi-tui/dist/components/loader.d.ts.map +1 -1
  87. package/node_modules/@mariozechner/pi-tui/dist/components/loader.js +5 -102
  88. package/node_modules/@mariozechner/pi-tui/dist/components/loader.js.map +1 -1
  89. package/node_modules/@mariozechner/pi-tui/dist/components/markdown.d.ts.map +1 -1
  90. package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js +111 -53
  91. package/node_modules/@mariozechner/pi-tui/dist/components/markdown.js.map +1 -1
  92. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts +19 -1
  93. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.d.ts.map +1 -1
  94. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js +78 -67
  95. package/node_modules/@mariozechner/pi-tui/dist/components/select-list.js.map +1 -1
  96. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts +1 -25
  97. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.d.ts.map +1 -1
  98. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js +13 -50
  99. package/node_modules/@mariozechner/pi-tui/dist/components/settings-list.js.map +1 -1
  100. package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +8 -10
  101. package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
  102. package/node_modules/@mariozechner/pi-tui/dist/index.js +6 -9
  103. package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
  104. package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +108 -238
  105. package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
  106. package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +108 -365
  107. package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
  108. package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +33 -48
  109. package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
  110. package/node_modules/@mariozechner/pi-tui/dist/keys.js +239 -155
  111. package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
  112. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts +14 -94
  113. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.d.ts.map +1 -1
  114. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js +44 -186
  115. package/node_modules/@mariozechner/pi-tui/dist/terminal-image.js.map +1 -1
  116. package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +13 -58
  117. package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
  118. package/node_modules/@mariozechner/pi-tui/dist/terminal.js +78 -111
  119. package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
  120. package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +24 -110
  121. package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
  122. package/node_modules/@mariozechner/pi-tui/dist/tui.js +188 -435
  123. package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
  124. package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts +0 -18
  125. package/node_modules/@mariozechner/pi-tui/dist/utils.d.ts.map +1 -1
  126. package/node_modules/@mariozechner/pi-tui/dist/utils.js +251 -119
  127. package/node_modules/@mariozechner/pi-tui/dist/utils.js.map +1 -1
  128. package/node_modules/@mariozechner/pi-tui/package.json +6 -6
  129. package/node_modules/@mariozechner/pi-tui/src/__tests__/__snapshots__/render.test.ts.snap +3 -40
  130. package/node_modules/@mariozechner/pi-tui/src/__tests__/image-component.test.ts +71 -81
  131. package/node_modules/@mariozechner/pi-tui/src/__tests__/render.test.ts +0 -33
  132. package/node_modules/@mariozechner/pi-tui/src/__tests__/terminal-image.test.ts +93 -334
  133. package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +1 -1
  134. package/node_modules/@mariozechner/pi-tui/src/__tests__/utils.test.ts +11 -196
  135. package/node_modules/@mariozechner/pi-tui/src/autocomplete.ts +228 -142
  136. package/node_modules/@mariozechner/pi-tui/src/components/cancellable-loader.ts +3 -3
  137. package/node_modules/@mariozechner/pi-tui/src/components/editor.ts +624 -390
  138. package/node_modules/@mariozechner/pi-tui/src/components/image.ts +17 -227
  139. package/node_modules/@mariozechner/pi-tui/src/components/input.ts +71 -63
  140. package/node_modules/@mariozechner/pi-tui/src/components/loader.ts +5 -137
  141. package/node_modules/@mariozechner/pi-tui/src/components/markdown.ts +143 -52
  142. package/node_modules/@mariozechner/pi-tui/src/components/select-list.ts +136 -70
  143. package/node_modules/@mariozechner/pi-tui/src/components/settings-list.ts +12 -51
  144. package/node_modules/@mariozechner/pi-tui/src/index.ts +17 -36
  145. package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +148 -421
  146. package/node_modules/@mariozechner/pi-tui/src/keys.ts +253 -181
  147. package/node_modules/@mariozechner/pi-tui/src/terminal-image.ts +51 -252
  148. package/node_modules/@mariozechner/pi-tui/src/terminal.ts +78 -133
  149. package/node_modules/@mariozechner/pi-tui/src/tui.ts +202 -478
  150. package/node_modules/@mariozechner/pi-tui/src/utils.ts +289 -125
  151. package/node_modules/@mariozechner/pi-tui/tsconfig.build.json +1 -0
  152. package/package.json +13 -13
  153. package/packages/tallow-tui/node_modules/@types/mime-types/README.md +8 -2
  154. package/packages/tallow-tui/node_modules/@types/mime-types/index.d.ts +6 -0
  155. package/packages/tallow-tui/node_modules/@types/mime-types/package.json +9 -3
  156. package/packages/tallow-tui/node_modules/get-east-asian-width/lookup-data.js +18 -0
  157. package/packages/tallow-tui/node_modules/get-east-asian-width/lookup.js +116 -384
  158. package/packages/tallow-tui/node_modules/get-east-asian-width/package.json +5 -4
  159. package/packages/tallow-tui/node_modules/get-east-asian-width/utilities.js +24 -0
  160. package/packages/tallow-tui/node_modules/marked/README.md +5 -4
  161. package/packages/tallow-tui/node_modules/marked/bin/main.js +10 -8
  162. package/packages/tallow-tui/node_modules/marked/bin/marked.js +2 -1
  163. package/packages/tallow-tui/node_modules/marked/lib/marked.d.ts +156 -125
  164. package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js +67 -2179
  165. package/packages/tallow-tui/node_modules/marked/lib/marked.esm.js.map +3 -3
  166. package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js +67 -2201
  167. package/packages/tallow-tui/node_modules/marked/lib/marked.umd.js.map +3 -3
  168. package/packages/tallow-tui/node_modules/marked/man/marked.1 +4 -2
  169. package/packages/tallow-tui/node_modules/marked/man/marked.1.md +2 -1
  170. package/packages/tallow-tui/node_modules/marked/package.json +26 -34
  171. package/skills/tallow-expert/SKILL.md +1 -3
  172. package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts +0 -32
  173. package/node_modules/@mariozechner/pi-tui/dist/border-styles.d.ts.map +0 -1
  174. package/node_modules/@mariozechner/pi-tui/dist/border-styles.js +0 -46
  175. package/node_modules/@mariozechner/pi-tui/dist/border-styles.js.map +0 -1
  176. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts +0 -52
  177. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.d.ts.map +0 -1
  178. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js +0 -89
  179. package/node_modules/@mariozechner/pi-tui/dist/components/bordered-box.js.map +0 -1
  180. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts +0 -14
  181. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.d.ts.map +0 -1
  182. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js +0 -55
  183. package/node_modules/@mariozechner/pi-tui/dist/test-utils/capability-env.js.map +0 -1
  184. package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-change-listener.test.ts +0 -121
  185. package/node_modules/@mariozechner/pi-tui/src/__tests__/editor-ghost-text.test.ts +0 -112
  186. package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +0 -134
  187. package/node_modules/@mariozechner/pi-tui/src/__tests__/settings-list.test.ts +0 -81
  188. package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +0 -555
  189. package/node_modules/@mariozechner/pi-tui/src/border-styles.ts +0 -60
  190. package/node_modules/@mariozechner/pi-tui/src/components/bordered-box.ts +0 -113
  191. package/node_modules/@mariozechner/pi-tui/src/test-utils/capability-env.ts +0 -56
  192. package/packages/tallow-tui/node_modules/marked/lib/marked.cjs +0 -2211
  193. package/packages/tallow-tui/node_modules/marked/lib/marked.cjs.map +0 -7
  194. package/packages/tallow-tui/node_modules/marked/lib/marked.d.cts +0 -728
  195. package/packages/tallow-tui/node_modules/marked/marked.min.js +0 -69
@@ -1,555 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { stripAnsi } from "../../../../test-utils/virtual-terminal.js";
3
- import type { Terminal } from "../terminal.js";
4
- import { type Component, TUI } from "../tui.js";
5
-
6
- /** Terminal test double that records all writes for assertion. */
7
- class MockTerminal implements Terminal {
8
- private readonly width: number;
9
- private readonly height: number;
10
- public readonly writes: string[] = [];
11
-
12
- constructor(width: number, height: number) {
13
- this.width = width;
14
- this.height = height;
15
- }
16
-
17
- start(_onInput: (data: string) => void, _onResize: () => void): void {}
18
-
19
- stop(): void {}
20
-
21
- async drainInput(): Promise<void> {}
22
-
23
- write(data: string): void {
24
- this.writes.push(data);
25
- }
26
-
27
- get columns(): number {
28
- return this.width;
29
- }
30
-
31
- get rows(): number {
32
- return this.height;
33
- }
34
-
35
- get kittyProtocolActive(): boolean {
36
- return false;
37
- }
38
-
39
- moveBy(_lines: number): void {}
40
-
41
- hideCursor(): void {}
42
-
43
- showCursor(): void {}
44
-
45
- clearLine(): void {}
46
-
47
- clearFromCursor(): void {}
48
-
49
- clearScreen(): void {}
50
-
51
- enableMouse(): void {}
52
- disableMouse(): void {}
53
- enterAlternateScreen(): void {}
54
-
55
- leaveAlternateScreen(): void {}
56
-
57
- setTitle(_title: string): void {}
58
-
59
- setProgress(_percent: number): void {}
60
-
61
- clearProgress(): void {}
62
- }
63
-
64
- /** Mutable component that lets tests drive exact rendered line sequences. */
65
- class MutableLinesComponent implements Component {
66
- private lines: string[];
67
-
68
- constructor(lines: string[]) {
69
- this.lines = lines;
70
- }
71
-
72
- setLines(lines: string[]): void {
73
- this.lines = lines;
74
- }
75
-
76
- render(_width: number): string[] {
77
- return this.lines;
78
- }
79
-
80
- invalidate(): void {}
81
- }
82
-
83
- interface ScenarioResult {
84
- border: string;
85
- finalWrite: string;
86
- redrawsBeforeUpdate: number;
87
- redrawsAfterUpdate: number;
88
- }
89
-
90
- /**
91
- * Invoke TUI's internal render synchronously for deterministic testing.
92
- *
93
- * @param tui - TUI instance under test
94
- */
95
- function renderNow(tui: TUI): void {
96
- const renderer = tui as unknown as { doRender: () => void };
97
- renderer.doRender();
98
- }
99
-
100
- /**
101
- * Build a frame with stable lines, editor-like borders, and optional trailing lines.
102
- *
103
- * @param stableLines - Number of unchanged lines before editor
104
- * @param inputText - Editor content line between borders
105
- * @param trailingLines - Number of transient lines after editor
106
- * @param width - Terminal width in columns
107
- * @returns Frame lines for the component render output
108
- */
109
- function createFrame(
110
- stableLines: number,
111
- inputText: string,
112
- trailingLines: number,
113
- width: number
114
- ): string[] {
115
- const stable = Array.from({ length: stableLines }, (_, index) => `stable ${index}`);
116
- const border = "─".repeat(width);
117
- const inputLine = inputText.padEnd(width, " ").slice(0, width);
118
- const trailing = Array.from({ length: trailingLines }, (_, index) => `tail ${index}`);
119
- return [...stable, border, inputLine, border, ...trailing];
120
- }
121
-
122
- /**
123
- * Reproduce grow -> shrink -> update sequence that previously risked row drift.
124
- *
125
- * @returns Scenario outputs used by assertions
126
- */
127
- function runGrowShrinkUpdateScenario(): ScenarioResult {
128
- const width = 32;
129
- const height = 10;
130
- const stableLines = 18;
131
- const border = "─".repeat(width);
132
- const terminal = new MockTerminal(width, height);
133
- const tui = new TUI(terminal);
134
- const component = new MutableLinesComponent(createFrame(stableLines, "input A", 9, width));
135
- tui.addChild(component);
136
-
137
- // Grow to establish a large working area.
138
- renderNow(tui);
139
-
140
- // Shrink without changing earlier viewport lines. This keeps maxLinesRendered > previousLines.length.
141
- component.setLines(createFrame(stableLines, "input A", 0, width));
142
- renderNow(tui);
143
-
144
- const redrawsBeforeUpdate = tui.fullRedraws;
145
-
146
- // Trigger a regular update in the editor band after shrink.
147
- component.setLines(createFrame(stableLines, "input B", 0, width));
148
- renderNow(tui);
149
-
150
- return {
151
- border,
152
- finalWrite: terminal.writes[terminal.writes.length - 1] ?? "",
153
- redrawsBeforeUpdate,
154
- redrawsAfterUpdate: tui.fullRedraws,
155
- };
156
- }
157
-
158
- /**
159
- * Reproduce grow -> shrink -> grow -> update cycle that simulates agent turn
160
- * content fluctuations (loader appears, content shrinks, new content arrives).
161
- *
162
- * @returns All terminal writes and final redraw count
163
- */
164
- function runHeightFluctuationScenario(): { allWrites: string[]; fullRedraws: number } {
165
- const width = 32;
166
- const height = 10;
167
- const stableLines = 18;
168
- const terminal = new MockTerminal(width, height);
169
- const tui = new TUI(terminal);
170
- const component = new MutableLinesComponent(createFrame(stableLines, "input A", 9, width));
171
- tui.addChild(component);
172
-
173
- // Phase 1: Grow to establish a large working area (simulates loader + streaming).
174
- renderNow(tui);
175
-
176
- // Phase 2: Shrink (simulates loader stopping or tool result replacing progress).
177
- component.setLines(createFrame(stableLines, "input A", 0, width));
178
- renderNow(tui);
179
-
180
- // Phase 3: Grow again (simulates new streaming content arriving).
181
- component.setLines(createFrame(stableLines, "input A", 5, width));
182
- renderNow(tui);
183
-
184
- // Phase 4: Update within content (simulates editor input change).
185
- component.setLines(createFrame(stableLines, "input B", 5, width));
186
- renderNow(tui);
187
-
188
- return {
189
- allWrites: [...terminal.writes],
190
- fullRedraws: tui.fullRedraws,
191
- };
192
- }
193
-
194
- describe("TUI differential rendering shrink regression", () => {
195
- test("realigns viewport basis on drift instead of full redraw", () => {
196
- const result = runGrowShrinkUpdateScenario();
197
-
198
- // Viewport basis drift is now handled by realignment, not full redraw.
199
- // The update render should use a partial redraw (no increase in fullRedraws).
200
- expect(result.redrawsAfterUpdate).toBe(result.redrawsBeforeUpdate);
201
- });
202
-
203
- test("keeps editor content correct after grow->shrink->update", () => {
204
- const result = runGrowShrinkUpdateScenario();
205
- const plain = stripAnsi(result.finalWrite);
206
-
207
- // The partial redraw only writes the changed line, not the full content.
208
- // Borders are already on screen from the prior render and don't need
209
- // to be redrawn — their absence in the final write proves partial redraw worked.
210
- expect(plain).toContain("input B");
211
- });
212
-
213
- test("never clears scrollback during content height fluctuation", () => {
214
- const result = runHeightFluctuationScenario();
215
-
216
- // No write should ever contain \x1b[3J (clear scrollback).
217
- // This sequence destroys the user's scroll position and reading context.
218
- for (const write of result.allWrites) {
219
- expect(write).not.toContain("\x1b[3J");
220
- }
221
- });
222
-
223
- test("never clears scrollback during grow->shrink->update", () => {
224
- const width = 32;
225
- const height = 10;
226
- const stableLines = 18;
227
- const terminal = new MockTerminal(width, height);
228
- const tui = new TUI(terminal);
229
- const component = new MutableLinesComponent(createFrame(stableLines, "input A", 9, width));
230
- tui.addChild(component);
231
-
232
- renderNow(tui);
233
-
234
- component.setLines(createFrame(stableLines, "input A", 0, width));
235
- renderNow(tui);
236
-
237
- component.setLines(createFrame(stableLines, "input B", 0, width));
238
- renderNow(tui);
239
-
240
- for (const write of terminal.writes) {
241
- expect(write).not.toContain("\x1b[3J");
242
- }
243
- });
244
-
245
- test("extraLines > height in diff path triggers safety full redraw", () => {
246
- const width = 40;
247
- const height = 10;
248
- const terminal = new MockTerminal(width, height);
249
- const tui = new TUI(terminal);
250
- // Start with 30 lines (extraLines on shrink = 25, which exceeds height = 10)
251
- const component = new MutableLinesComponent(Array.from({ length: 30 }, (_, i) => `line ${i}`));
252
- tui.addChild(component);
253
- renderNow(tui);
254
-
255
- // Shrink to 5 lines with changed content so we don't hit the "all deleted" path
256
- component.setLines(Array.from({ length: 5 }, (_, i) => `changed ${i}`));
257
- const redrawsBefore = tui.fullRedraws;
258
- renderNow(tui);
259
-
260
- // Should have triggered safety full redraw (extraLines = 25 > height = 10)
261
- expect(tui.fullRedraws).toBe(redrawsBefore + 1);
262
- });
263
-
264
- test("maxLinesRendered decays after shrink without overlays", () => {
265
- const width = 40;
266
- const height = 20;
267
- const terminal = new MockTerminal(width, height);
268
- const tui = new TUI(terminal);
269
- const component = new MutableLinesComponent(Array.from({ length: 15 }, (_, i) => `line ${i}`));
270
- tui.addChild(component);
271
- renderNow(tui);
272
-
273
- component.setLines(Array.from({ length: 8 }, (_, i) => `short ${i}`));
274
- renderNow(tui);
275
-
276
- // maxLinesRendered should have decayed to actual content size
277
- const tuiInternal = tui as unknown as { maxLinesRendered: number };
278
- expect(tuiInternal.maxLinesRendered).toBe(8);
279
- });
280
-
281
- test("grow-shrink-grow does not leave ghost gap", () => {
282
- const width = 40;
283
- const height = 10;
284
- const terminal = new MockTerminal(width, height);
285
- const tui = new TUI(terminal);
286
- const component = new MutableLinesComponent(Array.from({ length: 25 }, (_, i) => `line ${i}`));
287
- tui.addChild(component);
288
- renderNow(tui); // 25 lines
289
-
290
- component.setLines(Array.from({ length: 8 }, (_, i) => `shrunk ${i}`));
291
- renderNow(tui); // Shrink to 8
292
-
293
- component.setLines(Array.from({ length: 12 }, (_, i) => `grown ${i}`));
294
- renderNow(tui); // Grow to 12
295
-
296
- // maxLinesRendered should track actual content, not stale high-water mark
297
- const tuiInternal = tui as unknown as { maxLinesRendered: number };
298
- expect(tuiInternal.maxLinesRendered).toBe(12);
299
-
300
- // Verify no blank-line ghost: writes should not contain scrollback destruction
301
- const lastWrite = terminal.writes[terminal.writes.length - 1] ?? "";
302
- expect(lastWrite).not.toContain("\x1b[3J");
303
- });
304
-
305
- test("viewport drift corrected on same render as shrink", () => {
306
- const width = 40;
307
- const height = 10;
308
- const terminal = new MockTerminal(width, height);
309
- const tui = new TUI(terminal);
310
- // Start with 80 lines to establish large working area
311
- const component = new MutableLinesComponent(Array.from({ length: 80 }, (_, i) => `line ${i}`));
312
- tui.addChild(component);
313
- renderNow(tui); // maxLinesRendered = 80
314
-
315
- // Shrink to 30 — drift should be corrected immediately (same cycle)
316
- component.setLines(Array.from({ length: 30 }, (_, i) => `shrunk ${i}`));
317
- renderNow(tui);
318
-
319
- const tuiInternal = tui as unknown as {
320
- maxLinesRendered: number;
321
- previousViewportTop: number;
322
- };
323
-
324
- // maxLinesRendered should have decayed to 30 (not stuck at 80)
325
- expect(tuiInternal.maxLinesRendered).toBe(30);
326
- // previousViewportTop should be consistent with the decayed maxLinesRendered
327
- expect(tuiInternal.previousViewportTop).toBe(Math.max(0, 30 - height));
328
-
329
- // Grow to 40 — should NOT require full redraw since drift was already corrected
330
- component.setLines(Array.from({ length: 40 }, (_, i) => `grown ${i}`));
331
- renderNow(tui);
332
-
333
- // The key assertion is maxLinesRendered is correct
334
- expect(tuiInternal.maxLinesRendered).toBe(40);
335
- });
336
-
337
- test("large shrink (>5 lines) triggers full redraw to prevent ghosting", () => {
338
- const width = 40;
339
- const height = 20;
340
- const terminal = new MockTerminal(width, height);
341
- const tui = new TUI(terminal);
342
- // Start with 18 lines
343
- const component = new MutableLinesComponent(Array.from({ length: 18 }, (_, i) => `line ${i}`));
344
- tui.addChild(component);
345
- renderNow(tui); // First render (fullRedraw #1)
346
-
347
- // Shrink by 6 lines (above the 5-line threshold) — should trigger full redraw
348
- component.setLines(Array.from({ length: 12 }, (_, i) => `shrunk ${i}`));
349
- const redrawsBefore = tui.fullRedraws;
350
- renderNow(tui);
351
-
352
- expect(tui.fullRedraws).toBe(redrawsBefore + 1);
353
- const tuiInternal = tui as unknown as { maxLinesRendered: number };
354
- expect(tuiInternal.maxLinesRendered).toBe(12);
355
- });
356
-
357
- test("small shrink (<=5 lines) uses partial diff, not full redraw", () => {
358
- const width = 40;
359
- const height = 20;
360
- const terminal = new MockTerminal(width, height);
361
- const tui = new TUI(terminal);
362
- // Start with 12 lines
363
- const component = new MutableLinesComponent(Array.from({ length: 12 }, (_, i) => `line ${i}`));
364
- tui.addChild(component);
365
- renderNow(tui); // First render (fullRedraw #1)
366
-
367
- // Shrink by exactly 5 lines (at threshold, NOT over) — should NOT trigger
368
- // the large-shrink full redraw path
369
- component.setLines(Array.from({ length: 7 }, (_, i) => `shrunk ${i}`));
370
- const redrawsBefore = tui.fullRedraws;
371
- renderNow(tui);
372
-
373
- // Should use partial diff (no increase in fullRedraws)
374
- expect(tui.fullRedraws).toBe(redrawsBefore);
375
- const tuiInternal = tui as unknown as { maxLinesRendered: number };
376
- expect(tuiInternal.maxLinesRendered).toBe(7);
377
- });
378
-
379
- test("rapid grow-shrink-grow cycles don't accumulate ghost state", () => {
380
- const width = 40;
381
- const height = 15;
382
- const terminal = new MockTerminal(width, height);
383
- const tui = new TUI(terminal);
384
- const component = new MutableLinesComponent(
385
- Array.from({ length: 10 }, (_, i) => `initial ${i}`)
386
- );
387
- tui.addChild(component);
388
- renderNow(tui);
389
-
390
- const tuiInternal = tui as unknown as {
391
- maxLinesRendered: number;
392
- previousViewportTop: number;
393
- };
394
-
395
- // Simulate 5 rapid grow-shrink cycles (like streaming tool output + collapse)
396
- for (let cycle = 0; cycle < 5; cycle++) {
397
- // Grow: simulate streaming output
398
- component.setLines(Array.from({ length: 40 }, (_, i) => `stream-${cycle}-${i}`));
399
- renderNow(tui);
400
-
401
- // Shrink: simulate tool result collapse
402
- component.setLines(Array.from({ length: 8 }, (_, i) => `result-${cycle}-${i}`));
403
- renderNow(tui);
404
- }
405
-
406
- // After all cycles, maxLinesRendered should match actual content (8), not
407
- // any stale high-water mark from the streaming phases
408
- expect(tuiInternal.maxLinesRendered).toBe(8);
409
- expect(tuiInternal.previousViewportTop).toBe(0); // 8 < height(15), so 0
410
-
411
- // A subsequent update should work correctly via partial diff
412
- component.setLines(Array.from({ length: 8 }, (_, i) => `final ${i}`));
413
- const redrawsBefore = tui.fullRedraws;
414
- renderNow(tui);
415
-
416
- // No full redraw needed — content didn't shrink
417
- expect(tui.fullRedraws).toBe(redrawsBefore);
418
- });
419
-
420
- test("drift correction uses newLines.length, not previousLines.length", () => {
421
- const width = 40;
422
- const height = 10;
423
- const terminal = new MockTerminal(width, height);
424
- const tui = new TUI(terminal);
425
-
426
- // Use an overlay to keep maxLinesRendered inflated (overlay padding).
427
- // With overlays, maxLinesRendered = Math.max(old, new) — only grows.
428
- const component = new MutableLinesComponent(Array.from({ length: 50 }, (_, i) => `line ${i}`));
429
- tui.addChild(component);
430
-
431
- const overlayComponent: Component = {
432
- render: () => ["overlay line"],
433
- invalidate: () => {},
434
- };
435
- tui.showOverlay(overlayComponent);
436
- renderNow(tui); // maxLinesRendered >= 50 (padded by overlay)
437
-
438
- // Remove overlay and shrink content simultaneously — maxLinesRendered is
439
- // still inflated from overlay padding, but newLines.length will be much
440
- // smaller. The large-shrink guard (>5 lines) triggers a full redraw,
441
- // which correctly resets maxLinesRendered to newLines.length.
442
- tui.hideOverlay();
443
- component.setLines(Array.from({ length: 20 }, (_, i) => `shrunk ${i}`));
444
- renderNow(tui);
445
-
446
- const tuiInternal = tui as unknown as {
447
- maxLinesRendered: number;
448
- previousViewportTop: number;
449
- };
450
-
451
- // maxLinesRendered should be 20 (newLines.length), not whatever
452
- // previousLines.length was (which includes overlay padding)
453
- expect(tuiInternal.maxLinesRendered).toBe(20);
454
- expect(tuiInternal.previousViewportTop).toBe(Math.max(0, 20 - height));
455
- });
456
-
457
- test("requestScrollbackClear includes \\x1b[3J only on next full render", () => {
458
- const width = 40;
459
- const height = 10;
460
- const terminal = new MockTerminal(width, height);
461
- const tui = new TUI(terminal);
462
- const component = new MutableLinesComponent(Array.from({ length: 20 }, (_, i) => `line ${i}`));
463
- tui.addChild(component);
464
-
465
- // Initial render — no scrollback clear
466
- renderNow(tui);
467
- expect(terminal.writes.some((w) => w.includes("\x1b[3J"))).toBe(false);
468
-
469
- // Request scrollback clear, then shrink content to trigger fullRender(true)
470
- tui.requestScrollbackClear();
471
- component.setLines(Array.from({ length: 5 }, (_, i) => `new ${i}`));
472
- renderNow(tui);
473
-
474
- // The large shrink (20→5) triggers fullRender(true) which should include \x1b[3J
475
- const clearWrite = terminal.writes.find((w) => w.includes("\x1b[3J"));
476
- expect(clearWrite).toBeDefined();
477
-
478
- // Subsequent renders should NOT include \x1b[3J (flag consumed)
479
- terminal.writes.length = 0;
480
- component.setLines(Array.from({ length: 3 }, (_, i) => `final ${i}`));
481
- renderNow(tui);
482
- expect(terminal.writes.some((w) => w.includes("\x1b[3J"))).toBe(false);
483
- });
484
-
485
- test("gradual shrink across multiple frames triggers full redraw", () => {
486
- const width = 40;
487
- const height = 10;
488
- const terminal = new MockTerminal(width, height);
489
- const tui = new TUI(terminal);
490
- const component = new MutableLinesComponent(Array.from({ length: 20 }, (_, i) => `line ${i}`));
491
- tui.addChild(component);
492
- renderNow(tui); // Establish peak at 20 lines
493
-
494
- const redraws = () => tui.fullRedraws;
495
-
496
- // Shrink by 2 lines per frame — each frame delta is ≤5 (not caught by
497
- // single-frame guard). The rolling peak should catch the accumulated drop.
498
- const base = redraws();
499
- component.setLines(Array.from({ length: 18 }, (_, i) => `line ${i}`)); // -2
500
- renderNow(tui);
501
- component.setLines(Array.from({ length: 16 }, (_, i) => `line ${i}`)); // -4 total
502
- renderNow(tui);
503
- // Still within threshold — partial redraws only
504
- expect(redraws() - base).toBe(0);
505
-
506
- component.setLines(Array.from({ length: 14 }, (_, i) => `line ${i}`)); // -6 total from peak
507
- renderNow(tui);
508
- // Accumulated shrink exceeds threshold — should have triggered full redraw
509
- expect(redraws() - base).toBeGreaterThanOrEqual(1);
510
- });
511
-
512
- test("rolling shrink peak resets after full redraw", () => {
513
- const width = 40;
514
- const height = 10;
515
- const terminal = new MockTerminal(width, height);
516
- const tui = new TUI(terminal);
517
- const component = new MutableLinesComponent(Array.from({ length: 20 }, (_, i) => `line ${i}`));
518
- tui.addChild(component);
519
- renderNow(tui); // Peak = 20
520
-
521
- // Trigger rolling shrink detection
522
- component.setLines(Array.from({ length: 13 }, (_, i) => `line ${i}`)); // -7
523
- renderNow(tui);
524
- const afterFirstRedraw = tui.fullRedraws;
525
-
526
- // Now shrink by only 2 from the new peak (13) — should NOT trigger
527
- component.setLines(Array.from({ length: 11 }, (_, i) => `line ${i}`)); // -2 from reset peak
528
- renderNow(tui);
529
- expect(tui.fullRedraws).toBe(afterFirstRedraw);
530
- });
531
-
532
- test("requestScrollbackClear has no effect on partial (differential) renders", () => {
533
- const width = 40;
534
- const height = 10;
535
- const terminal = new MockTerminal(width, height);
536
- const tui = new TUI(terminal);
537
- const component = new MutableLinesComponent(Array.from({ length: 8 }, (_, i) => `line ${i}`));
538
- tui.addChild(component);
539
- renderNow(tui);
540
-
541
- // Request scrollback clear but only change one line (small diff, no fullRender)
542
- tui.requestScrollbackClear();
543
- component.setLines(Array.from({ length: 8 }, (_, i) => (i === 3 ? "CHANGED" : `line ${i}`)));
544
- renderNow(tui);
545
-
546
- // Partial render path — no \x1b[3J emitted, flag stays pending
547
- expect(terminal.writes.some((w) => w.includes("\x1b[3J"))).toBe(false);
548
-
549
- // Now trigger a large shrink to fire fullRender(true) — flag should still be pending
550
- terminal.writes.length = 0;
551
- component.setLines(["single line"]);
552
- renderNow(tui);
553
- expect(terminal.writes.some((w) => w.includes("\x1b[3J"))).toBe(true);
554
- });
555
- });
@@ -1,60 +0,0 @@
1
- /**
2
- * Border character sets for box-drawing.
3
- *
4
- * @module
5
- */
6
-
7
- /** A set of box-drawing characters for rendering borders. */
8
- export interface BorderStyle {
9
- topLeft: string;
10
- topRight: string;
11
- bottomLeft: string;
12
- bottomRight: string;
13
- horizontal: string;
14
- vertical: string;
15
- }
16
-
17
- /** Sharp corners — standard box-drawing (┌┐└┘). */
18
- export const SHARP: BorderStyle = {
19
- topLeft: "┌",
20
- topRight: "┐",
21
- bottomLeft: "└",
22
- bottomRight: "┘",
23
- horizontal: "─",
24
- vertical: "│",
25
- };
26
-
27
- /** Rounded corners — Unicode arc box-drawing (╭╮╰╯). */
28
- export const ROUNDED: BorderStyle = {
29
- topLeft: "╭",
30
- topRight: "╮",
31
- bottomLeft: "╰",
32
- bottomRight: "╯",
33
- horizontal: "─",
34
- vertical: "│",
35
- };
36
-
37
- /** Flat — horizontal rules only, no corners or verticals. */
38
- export const FLAT: BorderStyle = {
39
- topLeft: "─",
40
- topRight: "─",
41
- bottomLeft: "─",
42
- bottomRight: "─",
43
- horizontal: "─",
44
- vertical: " ",
45
- };
46
-
47
- /**
48
- * Global default border style — set once, applies to all new BorderedBox instances.
49
- * Extensions can set this at session_start to override.
50
- */
51
- export let defaultBorderStyle: BorderStyle = SHARP;
52
-
53
- /**
54
- * Set the global default border style.
55
- *
56
- * @param style - Border style to use as default
57
- */
58
- export function setDefaultBorderStyle(style: BorderStyle): void {
59
- defaultBorderStyle = style;
60
- }