linecraft 0.2.0 → 0.2.2
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.
- package/LICENSE +0 -1
- package/README.md +34 -8
- package/lib/component.d.ts +34 -0
- package/lib/component.d.ts.map +1 -0
- package/lib/component.js +42 -0
- package/lib/component.js.map +1 -0
- package/lib/components/code-debug.d.ts +35 -0
- package/lib/components/code-debug.d.ts.map +1 -0
- package/lib/components/code-debug.js +294 -0
- package/lib/components/code-debug.js.map +1 -0
- package/lib/components/fill.d.ts +15 -0
- package/lib/components/fill.d.ts.map +1 -0
- package/lib/components/fill.js +37 -0
- package/lib/components/fill.js.map +1 -0
- package/lib/components/index.d.ts +2 -2
- package/lib/components/index.d.ts.map +1 -1
- package/lib/components/index.js +2 -2
- package/lib/components/index.js.map +1 -1
- package/lib/components/progress-bar-grid.d.ts +1 -1
- package/lib/components/progress-bar-grid.d.ts.map +1 -1
- package/lib/components/progress-bar-grid.js +6 -6
- package/lib/components/progress-bar-grid.js.map +1 -1
- package/lib/components/prompt.d.ts +4 -5
- package/lib/components/prompt.d.ts.map +1 -1
- package/lib/components/prompt.js +17 -69
- package/lib/components/prompt.js.map +1 -1
- package/lib/components/section.d.ts +33 -0
- package/lib/components/section.d.ts.map +1 -0
- package/lib/components/section.js +178 -0
- package/lib/components/section.js.map +1 -0
- package/lib/components/segments.d.ts +26 -0
- package/lib/components/segments.d.ts.map +1 -0
- package/lib/components/segments.js +105 -0
- package/lib/components/segments.js.map +1 -0
- package/lib/components/spinner.d.ts +18 -16
- package/lib/components/spinner.d.ts.map +1 -1
- package/lib/components/spinner.js +63 -47
- package/lib/components/spinner.js.map +1 -1
- package/lib/components/style.test.js +11 -11
- package/lib/components/style.test.js.map +1 -1
- package/lib/components/styled.d.ts +17 -0
- package/lib/components/styled.d.ts.map +1 -0
- package/lib/components/styled.js +107 -0
- package/lib/components/styled.js.map +1 -0
- package/lib/components/styled.test.d.ts +2 -0
- package/lib/components/styled.test.d.ts.map +1 -0
- package/lib/components/styled.test.js +135 -0
- package/lib/components/styled.test.js.map +1 -0
- package/lib/index.d.ts +17 -13
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +13 -13
- package/lib/index.js.map +1 -1
- package/lib/index.test.js +17 -11
- package/lib/index.test.js.map +1 -1
- package/lib/layout/grid.d.ts +31 -35
- package/lib/layout/grid.d.ts.map +1 -1
- package/lib/layout/grid.js +437 -216
- package/lib/layout/grid.js.map +1 -1
- package/lib/layout/grid.test.js +332 -36
- package/lib/layout/grid.test.js.map +1 -1
- package/lib/native/ansi.d.ts +9 -0
- package/lib/native/ansi.d.ts.map +1 -1
- package/lib/native/ansi.js +9 -0
- package/lib/native/ansi.js.map +1 -1
- package/lib/native/diff.d.ts +5 -1
- package/lib/native/diff.d.ts.map +1 -1
- package/lib/native/diff.js +25 -7
- package/lib/native/diff.js.map +1 -1
- package/lib/native/region-renderer-debug.test.d.ts +2 -0
- package/lib/native/region-renderer-debug.test.d.ts.map +1 -0
- package/lib/native/region-renderer-debug.test.js +45 -0
- package/lib/native/region-renderer-debug.test.js.map +1 -0
- package/lib/native/region-renderer.d.ts +57 -148
- package/lib/native/region-renderer.d.ts.map +1 -1
- package/lib/native/region-renderer.js +455 -1124
- package/lib/native/region-renderer.js.map +1 -1
- package/lib/native/region.test.js +2 -20
- package/lib/native/region.test.js.map +1 -1
- package/lib/region-resize.test.d.ts +2 -0
- package/lib/region-resize.test.d.ts.map +1 -0
- package/lib/region-resize.test.js +124 -0
- package/lib/region-resize.test.js.map +1 -0
- package/lib/region.d.ts +97 -9
- package/lib/region.d.ts.map +1 -1
- package/lib/region.js +591 -185
- package/lib/region.js.map +1 -1
- package/lib/region.test.js +3 -3
- package/lib/region.test.js.map +1 -1
- package/lib/types.d.ts +9 -0
- package/lib/types.d.ts.map +1 -1
- package/lib/utils/file-link.d.ts +16 -0
- package/lib/utils/file-link.d.ts.map +1 -0
- package/lib/utils/file-link.js +23 -0
- package/lib/utils/file-link.js.map +1 -0
- package/lib/utils/prompt.d.ts +15 -0
- package/lib/utils/prompt.d.ts.map +1 -0
- package/lib/utils/prompt.js +128 -0
- package/lib/utils/prompt.js.map +1 -0
- package/lib/utils/terminal-theme.d.ts +36 -0
- package/lib/utils/terminal-theme.d.ts.map +1 -0
- package/lib/utils/terminal-theme.js +61 -0
- package/lib/utils/terminal-theme.js.map +1 -0
- package/lib/utils/text.d.ts +53 -3
- package/lib/utils/text.d.ts.map +1 -1
- package/lib/utils/text.js +194 -36
- package/lib/utils/text.js.map +1 -1
- package/lib/utils/wait-for-spacebar.d.ts.map +1 -1
- package/lib/utils/wait-for-spacebar.js +9 -6
- package/lib/utils/wait-for-spacebar.js.map +1 -1
- package/package.json +17 -13
package/lib/region.js
CHANGED
|
@@ -1,7 +1,180 @@
|
|
|
1
1
|
import { RegionRenderer } from './native/region-renderer';
|
|
2
2
|
import { applyStyle } from './utils/colors';
|
|
3
3
|
import { getTerminalWidth } from './utils/terminal';
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Type guard to check if an item is a Component
|
|
6
|
+
*/
|
|
7
|
+
function isComponent(item) {
|
|
8
|
+
return item !== null && (typeof item === 'function' ||
|
|
9
|
+
(typeof item === 'object' && 'render' in item && typeof item.render === 'function'));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Get the height of a component by calling it with a context
|
|
13
|
+
*/
|
|
14
|
+
function getComponentHeight(component, region, width) {
|
|
15
|
+
const ctx = {
|
|
16
|
+
availableWidth: width,
|
|
17
|
+
region: region,
|
|
18
|
+
columnIndex: 0,
|
|
19
|
+
rowIndex: 0,
|
|
20
|
+
};
|
|
21
|
+
const result = typeof component === 'function' ? component(ctx) : component.render(ctx);
|
|
22
|
+
if (result === null)
|
|
23
|
+
return 0;
|
|
24
|
+
return Array.isArray(result) ? result.length : 1;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Render a component and return its content (pure function)
|
|
28
|
+
* The caller is responsible for writing the content to the appropriate position
|
|
29
|
+
*/
|
|
30
|
+
function renderComponent(component, region, width) {
|
|
31
|
+
// Track cleanup callback for this component
|
|
32
|
+
let cleanupCallback;
|
|
33
|
+
const ctx = {
|
|
34
|
+
availableWidth: width,
|
|
35
|
+
region: region,
|
|
36
|
+
columnIndex: 0,
|
|
37
|
+
rowIndex: 0, // Components don't know their position - caller decides
|
|
38
|
+
// Provide onUpdate callback for animated components (like Spinner)
|
|
39
|
+
onUpdate: () => {
|
|
40
|
+
// Re-render the last content to update animated components
|
|
41
|
+
region.reRenderLastContent();
|
|
42
|
+
},
|
|
43
|
+
// Provide onCleanup callback for components to register cleanup
|
|
44
|
+
onCleanup: (callback) => {
|
|
45
|
+
cleanupCallback = callback;
|
|
46
|
+
region.registerComponentCleanup(callback);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
const result = typeof component === 'function' ? component(ctx) : component.render(ctx);
|
|
50
|
+
// If component registered a cleanup callback, it's already stored in region
|
|
51
|
+
// (cleanupCallback is just for reference, the region already has it)
|
|
52
|
+
if (result === null)
|
|
53
|
+
return [];
|
|
54
|
+
// Components return their content - caller writes it to the correct position
|
|
55
|
+
if (Array.isArray(result)) {
|
|
56
|
+
return result.map(line => typeof line === 'string'
|
|
57
|
+
? line
|
|
58
|
+
: applyStyle(line.text, line.style));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const styled = typeof result === 'string'
|
|
62
|
+
? result
|
|
63
|
+
: applyStyle(result.text, result.style);
|
|
64
|
+
return [styled];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Reference to a component in the region that can be removed
|
|
69
|
+
*/
|
|
70
|
+
export class ComponentReference {
|
|
71
|
+
region;
|
|
72
|
+
componentIndex;
|
|
73
|
+
height;
|
|
74
|
+
constructor(region, componentIndex, height) {
|
|
75
|
+
this.region = region;
|
|
76
|
+
this.componentIndex = componentIndex;
|
|
77
|
+
this.height = height;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Delete this component - removes it from the region
|
|
81
|
+
*/
|
|
82
|
+
delete() {
|
|
83
|
+
const renderer = this.region.getRenderer();
|
|
84
|
+
const wasRenderingDisabled = renderer.disableRendering;
|
|
85
|
+
renderer.disableRendering = true;
|
|
86
|
+
// Remove component from allComponentDescriptors
|
|
87
|
+
this.region.removeComponent(this.componentIndex);
|
|
88
|
+
// Reduce region height
|
|
89
|
+
this.region.decreaseHeight(this.height);
|
|
90
|
+
renderer.setHeight(this.region.getInternalHeight());
|
|
91
|
+
// Re-render to update the display
|
|
92
|
+
this.region.reRenderLastContent();
|
|
93
|
+
renderer.disableRendering = wasRenderingDisabled;
|
|
94
|
+
// Force a render
|
|
95
|
+
if (!wasRenderingDisabled) {
|
|
96
|
+
void renderer.flush();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Reference to a section of lines in the region that can be updated
|
|
102
|
+
*/
|
|
103
|
+
export class SectionReference {
|
|
104
|
+
region;
|
|
105
|
+
startLine;
|
|
106
|
+
height;
|
|
107
|
+
constructor(region, startLine, height) {
|
|
108
|
+
this.region = region;
|
|
109
|
+
this.startLine = startLine;
|
|
110
|
+
this.height = height;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Delete this section - removes the lines from the region
|
|
114
|
+
*/
|
|
115
|
+
delete() {
|
|
116
|
+
const renderer = this.region.getRenderer();
|
|
117
|
+
const wasRenderingDisabled = renderer.disableRendering;
|
|
118
|
+
renderer.disableRendering = true;
|
|
119
|
+
// Remove lines from explicitlyAddedLines
|
|
120
|
+
this.region.removeRegionLines(this.startLine - 1, this.height);
|
|
121
|
+
// Reduce region height
|
|
122
|
+
this.region.decreaseHeight(this.height);
|
|
123
|
+
renderer.setHeight(this.region.getInternalHeight());
|
|
124
|
+
// Shrink the frame arrays (this also clears previousViewportFrame for recalculation)
|
|
125
|
+
renderer.shrinkFrame(this.startLine - 1, this.height);
|
|
126
|
+
renderer.disableRendering = wasRenderingDisabled;
|
|
127
|
+
// Force a render - the viewport frame will be recalculated and show correct content
|
|
128
|
+
if (!wasRenderingDisabled) {
|
|
129
|
+
void renderer.flush();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Update the content of this section
|
|
134
|
+
*/
|
|
135
|
+
update(content) {
|
|
136
|
+
const renderer = this.region.getRenderer();
|
|
137
|
+
const wasRenderingDisabled = renderer.disableRendering;
|
|
138
|
+
renderer.disableRendering = true;
|
|
139
|
+
let lines = [];
|
|
140
|
+
if (typeof content === 'string') {
|
|
141
|
+
lines = content.split('\n');
|
|
142
|
+
}
|
|
143
|
+
else if (Array.isArray(content)) {
|
|
144
|
+
lines = content.map(item => {
|
|
145
|
+
if (typeof item === 'string') {
|
|
146
|
+
return item;
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
const text = item.text;
|
|
150
|
+
return applyStyle(text, item.style);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// Update all lines in the section (use provided lines, or empty strings for remaining lines)
|
|
155
|
+
const linesToUpdate = this.height;
|
|
156
|
+
// Prepare updates for the renderer
|
|
157
|
+
const updates = [];
|
|
158
|
+
for (let i = 0; i < linesToUpdate; i++) {
|
|
159
|
+
const lineNumber = this.startLine + i;
|
|
160
|
+
// Use provided line if available, otherwise empty string to clear
|
|
161
|
+
const styled = i < lines.length ? lines[i] : '';
|
|
162
|
+
updates.push({ lineNumber, content: styled });
|
|
163
|
+
// Track in explicitlyAddedLines
|
|
164
|
+
this.region.ensureExplicitlyAddedLine(this.startLine + i - 1);
|
|
165
|
+
this.region.updateExplicitlyAddedLine(this.startLine + i - 1, {
|
|
166
|
+
content: styled,
|
|
167
|
+
lineNumber: this.startLine + i
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
renderer.disableRendering = wasRenderingDisabled;
|
|
171
|
+
// Use renderer's method to update lines and schedule render
|
|
172
|
+
// The renderer manages when to actually render (throttling, batching, etc.)
|
|
173
|
+
if (linesToUpdate > 0 && !wasRenderingDisabled) {
|
|
174
|
+
renderer.updateLines(updates);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
5
178
|
/**
|
|
6
179
|
* TerminalRegion - High-level API for terminal region management
|
|
7
180
|
*
|
|
@@ -15,29 +188,27 @@ export class TerminalRegion {
|
|
|
15
188
|
// Track ALL component descriptors that have been set/added, so we can re-render them
|
|
16
189
|
// This is an array of descriptors (or arrays of descriptors for multi-line sections)
|
|
17
190
|
allComponentDescriptors = [];
|
|
18
|
-
// Track
|
|
19
|
-
|
|
20
|
-
|
|
191
|
+
// Track cleanup callbacks registered by components (called when components are removed/replaced)
|
|
192
|
+
componentCleanupCallbacks = [];
|
|
193
|
+
// Track explicitly added content (prompts, whitespace added via add() or setLine())
|
|
194
|
+
// Components are re-rendered from allComponentDescriptors, so they don't need to be tracked here
|
|
195
|
+
// TODO: With signals, we'll track signal dependencies and re-render only what changed
|
|
196
|
+
explicitlyAddedLines = [];
|
|
21
197
|
// Prevent concurrent re-renders (e.g., multiple resize events firing rapidly)
|
|
22
198
|
isReRendering = false;
|
|
23
199
|
constructor(options = {}) {
|
|
24
200
|
this._width = options.width ?? getTerminalWidth();
|
|
25
201
|
this._height = options.height ?? 1;
|
|
26
202
|
// Create the low-level renderer
|
|
27
|
-
// Only pass width if it was explicitly set by the user (to allow auto-resize)
|
|
28
203
|
const rendererOptions = {
|
|
29
|
-
height: this._height,
|
|
30
204
|
stdout: options.stdout,
|
|
31
205
|
disableRendering: options.disableRendering,
|
|
206
|
+
debugLog: options.debugLog,
|
|
32
207
|
// CRITICAL: Pass callback to re-render last content during keep-alive and resize
|
|
33
208
|
// This ensures ALL components (grid) are re-rendered with current width
|
|
34
209
|
// The region orchestrates this - components don't manage their own re-rendering
|
|
35
210
|
onKeepAlive: () => this.reRenderLastContent(),
|
|
36
211
|
};
|
|
37
|
-
// Only set width if user explicitly provided it (allows auto-resize to work)
|
|
38
|
-
if (options.width !== undefined) {
|
|
39
|
-
rendererOptions.width = options.width;
|
|
40
|
-
}
|
|
41
212
|
this.renderer = new RegionRenderer(rendererOptions);
|
|
42
213
|
}
|
|
43
214
|
get width() {
|
|
@@ -51,17 +222,97 @@ export class TerminalRegion {
|
|
|
51
222
|
getLine(lineNumber) {
|
|
52
223
|
return this.renderer.getLine(lineNumber);
|
|
53
224
|
}
|
|
225
|
+
async getStartRow() {
|
|
226
|
+
return this.renderer.getStartRow();
|
|
227
|
+
}
|
|
228
|
+
showCursorAt(lineNumber, column) {
|
|
229
|
+
this.renderer.showCursorAt(lineNumber, column);
|
|
230
|
+
}
|
|
231
|
+
hideCursor() {
|
|
232
|
+
this.renderer.hideCursor();
|
|
233
|
+
}
|
|
54
234
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* Note: With auto-wrap disabled globally, we manage all wrapping ourselves.
|
|
58
|
-
* This method sets a single line - if content needs to wrap, it should
|
|
59
|
-
* be handled by the component layer (grid, style, etc.) before calling this.
|
|
60
|
-
* Content that exceeds the region width will be truncated by the terminal.
|
|
61
|
-
*
|
|
62
|
-
* The region will automatically expand if lineNumber exceeds current height.
|
|
235
|
+
* Get the underlying renderer (for internal use by SectionReference)
|
|
236
|
+
* @internal
|
|
63
237
|
*/
|
|
64
|
-
|
|
238
|
+
getRenderer() {
|
|
239
|
+
return this.renderer;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Get the internal height (for internal use by SectionReference)
|
|
243
|
+
* @internal
|
|
244
|
+
*/
|
|
245
|
+
getInternalHeight() {
|
|
246
|
+
return this._height;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Decrease the region height (for internal use by SectionReference)
|
|
250
|
+
* @internal
|
|
251
|
+
*/
|
|
252
|
+
decreaseHeight(amount) {
|
|
253
|
+
this._height = Math.max(0, this._height - amount);
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Remove lines from explicitlyAddedLines (for internal use by SectionReference)
|
|
257
|
+
* @internal
|
|
258
|
+
*/
|
|
259
|
+
removeRegionLines(startIndex, count) {
|
|
260
|
+
this.explicitlyAddedLines.splice(startIndex, count);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Register a cleanup callback for a component (called when component is removed/replaced)
|
|
264
|
+
* @internal
|
|
265
|
+
*/
|
|
266
|
+
registerComponentCleanup(callback) {
|
|
267
|
+
this.componentCleanupCallbacks.push(callback);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Call all registered cleanup callbacks and clear them
|
|
271
|
+
* @internal
|
|
272
|
+
*/
|
|
273
|
+
cleanupAllComponents() {
|
|
274
|
+
for (const callback of this.componentCleanupCallbacks) {
|
|
275
|
+
try {
|
|
276
|
+
callback();
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
// Ignore errors in cleanup callbacks
|
|
280
|
+
console.error('Error in component cleanup callback:', err);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
this.componentCleanupCallbacks = [];
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Remove a component from allComponentDescriptors (for internal use by ComponentReference)
|
|
287
|
+
* @internal
|
|
288
|
+
*/
|
|
289
|
+
removeComponent(componentIndex) {
|
|
290
|
+
if (componentIndex >= 0 && componentIndex < this.allComponentDescriptors.length) {
|
|
291
|
+
// Cleanup is handled by cleanupAllComponents when components are replaced
|
|
292
|
+
this.allComponentDescriptors.splice(componentIndex, 1);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Ensure a region line exists at the given index (for internal use by SectionReference)
|
|
297
|
+
* @internal
|
|
298
|
+
*/
|
|
299
|
+
ensureExplicitlyAddedLine(index) {
|
|
300
|
+
while (this.explicitlyAddedLines.length <= index) {
|
|
301
|
+
this.explicitlyAddedLines.push({ content: '', lineNumber: this.explicitlyAddedLines.length + 1 });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Update a region line (for internal use by SectionReference)
|
|
306
|
+
* @internal
|
|
307
|
+
*/
|
|
308
|
+
updateExplicitlyAddedLine(index, lineInfo) {
|
|
309
|
+
this.explicitlyAddedLines[index] = lineInfo;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Internal method to set a single line (used by components and SectionReference)
|
|
313
|
+
* @internal
|
|
314
|
+
*/
|
|
315
|
+
setLineInternal(lineNumber, content) {
|
|
65
316
|
if (lineNumber < 1) {
|
|
66
317
|
throw new Error('Line numbers start at 1');
|
|
67
318
|
}
|
|
@@ -75,18 +326,30 @@ export class TerminalRegion {
|
|
|
75
326
|
// Extract text and apply styling
|
|
76
327
|
const text = typeof content === 'string' ? content : content.text;
|
|
77
328
|
const styled = applyStyle(text, typeof content === 'object' ? content.style : undefined);
|
|
78
|
-
//
|
|
329
|
+
// Track this line as explicitly added content (via setLine/add)
|
|
79
330
|
// This allows us to preserve it during re-renders
|
|
80
|
-
while (this.
|
|
81
|
-
this.
|
|
331
|
+
while (this.explicitlyAddedLines.length < lineNumber) {
|
|
332
|
+
this.explicitlyAddedLines.push({ content: '', lineNumber: this.explicitlyAddedLines.length + 1 });
|
|
82
333
|
}
|
|
83
|
-
this.
|
|
84
|
-
type: 'static',
|
|
334
|
+
this.explicitlyAddedLines[lineNumber - 1] = {
|
|
85
335
|
content: styled,
|
|
86
336
|
lineNumber
|
|
87
337
|
};
|
|
88
338
|
// Update the renderer (this will expand the renderer if needed)
|
|
89
339
|
this.renderer.setLine(lineNumber, styled);
|
|
340
|
+
// CRITICAL: Sync region height with renderer height after setLine
|
|
341
|
+
// This ensures _height matches renderer.height, preventing height mismatches
|
|
342
|
+
const renderer = this.renderer;
|
|
343
|
+
if (renderer.height > this._height) {
|
|
344
|
+
this._height = renderer.height;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* @deprecated Use add() which returns a LineReference with update() method
|
|
349
|
+
* Set a single line (1-based line numbers)
|
|
350
|
+
*/
|
|
351
|
+
setLine(lineNumber, content) {
|
|
352
|
+
this.setLineInternal(lineNumber, content);
|
|
90
353
|
}
|
|
91
354
|
/**
|
|
92
355
|
* Set content in the region, replacing all existing content.
|
|
@@ -99,19 +362,20 @@ export class TerminalRegion {
|
|
|
99
362
|
const allContent = additionalLines.length > 0
|
|
100
363
|
? [content, ...additionalLines]
|
|
101
364
|
: [content];
|
|
102
|
-
// Check if first item is a
|
|
103
|
-
if (allContent.length > 0 &&
|
|
104
|
-
//
|
|
365
|
+
// Check if first item is a Component (object with render method or function)
|
|
366
|
+
if (allContent.length > 0 && isComponent(allContent[0])) {
|
|
367
|
+
// Cleanup: Call cleanup callbacks from old components before replacing
|
|
368
|
+
this.cleanupAllComponents();
|
|
369
|
+
// Component(s)
|
|
105
370
|
this.allComponentDescriptors = [allContent.length > 1 ? allContent : allContent[0]];
|
|
106
371
|
const width = this.width;
|
|
107
372
|
let totalHeight = 0;
|
|
108
373
|
const renderer = this.renderer;
|
|
109
374
|
const oldHeight = this._height;
|
|
110
375
|
// Calculate total height first
|
|
111
|
-
for (const
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
totalHeight += component.getHeight();
|
|
376
|
+
for (const component of allContent) {
|
|
377
|
+
if (isComponent(component)) {
|
|
378
|
+
totalHeight += getComponentHeight(component, this, width);
|
|
115
379
|
}
|
|
116
380
|
}
|
|
117
381
|
// FULL REPLACE: Truncate both frames to exact new height
|
|
@@ -127,26 +391,36 @@ export class TerminalRegion {
|
|
|
127
391
|
renderer.pendingFrame[i] = '';
|
|
128
392
|
}
|
|
129
393
|
this._height = totalHeight;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
394
|
+
// CRITICAL: set() does a FULL REPLACE, so clear ALL explicitlyAddedLines
|
|
395
|
+
// Components are tracked in allComponentDescriptors and re-rendered from there
|
|
396
|
+
// Any old explicitlyAddedLines entries are stale and should be removed
|
|
397
|
+
this.explicitlyAddedLines = [];
|
|
398
|
+
// Don't track component lines in explicitlyAddedLines - they're re-rendered from allComponentDescriptors
|
|
134
399
|
// CRITICAL: Disable auto-rendering during component rendering
|
|
135
400
|
// Components call setLine() which triggers scheduleRender(), but we want
|
|
136
401
|
// to batch all updates and render once at the end
|
|
137
402
|
const wasRenderingDisabled = renderer.disableRendering;
|
|
138
403
|
renderer.disableRendering = true;
|
|
139
404
|
// Render each component on its line
|
|
405
|
+
// CRITICAL: Write directly to pendingFrame, don't use setLine() which adds to explicitlyAddedLines
|
|
140
406
|
let currentLine = 1;
|
|
141
|
-
for (const
|
|
142
|
-
if (
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
407
|
+
for (const component of allContent) {
|
|
408
|
+
if (isComponent(component)) {
|
|
409
|
+
const componentLines = renderComponent(component, this, width);
|
|
410
|
+
// Write component content directly to pendingFrame (don't use setLine which tracks in explicitlyAddedLines)
|
|
411
|
+
for (let i = 0; i < componentLines.length; i++) {
|
|
412
|
+
const lineNumber = currentLine + i;
|
|
413
|
+
const index = lineNumber - 1;
|
|
414
|
+
renderer.ensureFrameSize(index + 1);
|
|
415
|
+
renderer.pendingFrame[index] = componentLines[i];
|
|
416
|
+
if (lineNumber > renderer.height) {
|
|
417
|
+
renderer.height = lineNumber;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
currentLine += componentLines.length;
|
|
147
421
|
}
|
|
148
422
|
}
|
|
149
|
-
renderer.
|
|
423
|
+
renderer.setHeight(totalHeight);
|
|
150
424
|
if (totalHeight > oldHeight) {
|
|
151
425
|
renderer.expandTo(totalHeight);
|
|
152
426
|
}
|
|
@@ -156,47 +430,6 @@ export class TerminalRegion {
|
|
|
156
430
|
void this.renderer.flush();
|
|
157
431
|
return;
|
|
158
432
|
}
|
|
159
|
-
// Single grid descriptor
|
|
160
|
-
if (typeof content === 'object' && content !== null && 'type' in content && content.type === 'grid') {
|
|
161
|
-
this.allComponentDescriptors = [content];
|
|
162
|
-
const component = resolveGrid(this, content);
|
|
163
|
-
const width = this.width;
|
|
164
|
-
const height = component.getHeight();
|
|
165
|
-
const renderer = this.renderer;
|
|
166
|
-
const oldHeight = this._height;
|
|
167
|
-
renderer.pendingFrame = renderer.pendingFrame.slice(0, height);
|
|
168
|
-
renderer.previousFrame = renderer.previousFrame.slice(0, height);
|
|
169
|
-
while (renderer.pendingFrame.length < height) {
|
|
170
|
-
renderer.pendingFrame.push('');
|
|
171
|
-
}
|
|
172
|
-
while (renderer.previousFrame.length < height) {
|
|
173
|
-
renderer.previousFrame.push('');
|
|
174
|
-
}
|
|
175
|
-
for (let i = 0; i < height; i++) {
|
|
176
|
-
renderer.pendingFrame[i] = '';
|
|
177
|
-
}
|
|
178
|
-
this._height = height;
|
|
179
|
-
this.regionLines = [];
|
|
180
|
-
for (let i = 0; i < height; i++) {
|
|
181
|
-
this.regionLines[i] = { type: 'component', lineNumber: i + 1 };
|
|
182
|
-
}
|
|
183
|
-
renderer.height = height;
|
|
184
|
-
if (height > oldHeight) {
|
|
185
|
-
renderer.expandTo(height);
|
|
186
|
-
}
|
|
187
|
-
// CRITICAL: Disable auto-rendering during component rendering
|
|
188
|
-
const wasRenderingDisabled = renderer.disableRendering;
|
|
189
|
-
renderer.disableRendering = true;
|
|
190
|
-
component.render(0, 1, width);
|
|
191
|
-
if (renderer.pendingFrame.length > height) {
|
|
192
|
-
renderer.pendingFrame = renderer.pendingFrame.slice(0, height);
|
|
193
|
-
}
|
|
194
|
-
// Re-enable rendering and flush once
|
|
195
|
-
renderer.disableRendering = wasRenderingDisabled;
|
|
196
|
-
// Note: flush() is async but we don't await here - caller should await if needed
|
|
197
|
-
void this.renderer.flush();
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
433
|
// Original string/array handling
|
|
201
434
|
if (typeof content === 'string') {
|
|
202
435
|
// Single string with \n line breaks
|
|
@@ -214,23 +447,23 @@ export class TerminalRegion {
|
|
|
214
447
|
/**
|
|
215
448
|
* Add content to the region, appending it after existing content.
|
|
216
449
|
* This is useful for adding multiple sections without overwriting previous content.
|
|
450
|
+
* Returns a SectionReference or ComponentReference that can be used to update/delete the content later.
|
|
217
451
|
*/
|
|
218
452
|
add(content, ...additionalLines) {
|
|
219
453
|
// Check if we have multiple grid descriptors
|
|
220
454
|
const allContent = additionalLines.length > 0
|
|
221
455
|
? [content, ...additionalLines]
|
|
222
456
|
: [content];
|
|
223
|
-
// Check if first item is a
|
|
224
|
-
if (allContent.length > 0 &&
|
|
457
|
+
// Check if first item is a Component
|
|
458
|
+
if (allContent.length > 0 && isComponent(allContent[0])) {
|
|
225
459
|
const width = this.width;
|
|
226
460
|
let totalHeight = 0;
|
|
227
461
|
// Calculate total height of new content
|
|
228
|
-
const
|
|
229
|
-
for (const
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
totalHeight += component.getHeight();
|
|
462
|
+
const components = [];
|
|
463
|
+
for (const item of allContent) {
|
|
464
|
+
if (isComponent(item)) {
|
|
465
|
+
components.push(item);
|
|
466
|
+
totalHeight += getComponentHeight(item, this, width);
|
|
234
467
|
}
|
|
235
468
|
}
|
|
236
469
|
const renderer = this.renderer;
|
|
@@ -252,16 +485,14 @@ export class TerminalRegion {
|
|
|
252
485
|
for (let i = 0; i < totalHeight; i++) {
|
|
253
486
|
renderer.pendingFrame[this._height + i] = '';
|
|
254
487
|
}
|
|
255
|
-
//
|
|
256
|
-
for (let i = 0; i < totalHeight; i++) {
|
|
257
|
-
this.regionLines.push({ type: 'component', lineNumber: this._height + i + 1 });
|
|
258
|
-
}
|
|
488
|
+
// Don't track component lines in regionLines - they're re-rendered from allComponentDescriptors
|
|
259
489
|
// Track this new content
|
|
260
490
|
const descriptorsToAdd = allContent.length > 1 ? allContent : allContent[0];
|
|
491
|
+
const componentIndex = this.allComponentDescriptors.length;
|
|
261
492
|
this.allComponentDescriptors.push(descriptorsToAdd);
|
|
262
493
|
// Update region height BEFORE rendering
|
|
263
494
|
this._height += totalHeight;
|
|
264
|
-
renderer.
|
|
495
|
+
renderer.setHeight(this._height);
|
|
265
496
|
// Expand region to accommodate new content
|
|
266
497
|
renderer.expandTo(this._height);
|
|
267
498
|
// CRITICAL: Disable auto-rendering during component rendering
|
|
@@ -271,22 +502,31 @@ export class TerminalRegion {
|
|
|
271
502
|
renderer.disableRendering = true;
|
|
272
503
|
// Render each component starting from startLine
|
|
273
504
|
let currentLine = startLine;
|
|
274
|
-
for (const component of
|
|
275
|
-
const height = component
|
|
276
|
-
component
|
|
505
|
+
for (const component of components) {
|
|
506
|
+
const height = getComponentHeight(component, this, width);
|
|
507
|
+
const componentLines = renderComponent(component, this, width);
|
|
508
|
+
// Write component content to pendingFrame at the correct position
|
|
509
|
+
for (let i = 0; i < componentLines.length; i++) {
|
|
510
|
+
const lineNumber = currentLine + i;
|
|
511
|
+
const index = lineNumber - 1;
|
|
512
|
+
renderer.ensureFrameSize(index + 1);
|
|
513
|
+
renderer.pendingFrame[index] = componentLines[i];
|
|
514
|
+
if (lineNumber > renderer.height) {
|
|
515
|
+
renderer.height = lineNumber;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
277
518
|
currentLine += height;
|
|
278
519
|
}
|
|
279
520
|
// Re-enable rendering and flush once
|
|
280
521
|
renderer.disableRendering = wasRenderingDisabled;
|
|
281
522
|
// Note: flush() is async but we don't await here - caller should await if needed
|
|
282
523
|
void this.renderer.flush();
|
|
283
|
-
return;
|
|
524
|
+
return new ComponentReference(this, componentIndex, totalHeight);
|
|
284
525
|
}
|
|
285
|
-
// Single
|
|
286
|
-
if (
|
|
287
|
-
const component = resolveGrid(this, content);
|
|
526
|
+
// Single Component (object with render method or function)
|
|
527
|
+
if (isComponent(content)) {
|
|
288
528
|
const width = this.width;
|
|
289
|
-
const height =
|
|
529
|
+
const height = getComponentHeight(content, this, width);
|
|
290
530
|
const startLine = this._height + 1;
|
|
291
531
|
const renderer = this.renderer;
|
|
292
532
|
// Extend frames
|
|
@@ -306,64 +546,121 @@ export class TerminalRegion {
|
|
|
306
546
|
for (let i = 0; i < height; i++) {
|
|
307
547
|
renderer.pendingFrame[this._height + i] = '';
|
|
308
548
|
}
|
|
309
|
-
//
|
|
310
|
-
for (let i = 0; i < height; i++) {
|
|
311
|
-
this.regionLines.push({ type: 'component', lineNumber: this._height + i + 1 });
|
|
312
|
-
}
|
|
549
|
+
// Don't track component lines in regionLines - they're re-rendered from allComponentDescriptors
|
|
313
550
|
// Track this new content
|
|
551
|
+
const componentIndex = this.allComponentDescriptors.length;
|
|
314
552
|
this.allComponentDescriptors.push(content);
|
|
315
553
|
// Update region height BEFORE rendering
|
|
316
554
|
this._height += height;
|
|
317
|
-
renderer.
|
|
555
|
+
renderer.setHeight(this._height);
|
|
318
556
|
// Expand region to accommodate new content
|
|
319
557
|
renderer.expandTo(this._height);
|
|
320
558
|
// CRITICAL: Disable auto-rendering during component rendering
|
|
321
559
|
const wasRenderingDisabled = renderer.disableRendering;
|
|
322
560
|
renderer.disableRendering = true;
|
|
323
|
-
// Render component
|
|
324
|
-
|
|
561
|
+
// Render component (renderComponent will set up onUpdate callback)
|
|
562
|
+
const componentLines = renderComponent(content, this, width);
|
|
563
|
+
// Write component content to pendingFrame at the correct position
|
|
564
|
+
for (let i = 0; i < componentLines.length; i++) {
|
|
565
|
+
const lineNumber = startLine + i;
|
|
566
|
+
const index = lineNumber - 1;
|
|
567
|
+
renderer.ensureFrameSize(index + 1);
|
|
568
|
+
renderer.pendingFrame[index] = componentLines[i];
|
|
569
|
+
if (lineNumber > renderer.height) {
|
|
570
|
+
renderer.height = lineNumber;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
325
573
|
// Re-enable rendering and flush once
|
|
326
574
|
renderer.disableRendering = wasRenderingDisabled;
|
|
327
575
|
// Note: flush() is async but we don't await here - caller should await if needed
|
|
328
576
|
void this.renderer.flush();
|
|
329
|
-
return;
|
|
577
|
+
return new ComponentReference(this, componentIndex, height);
|
|
330
578
|
}
|
|
331
579
|
// String/array handling - append after existing content
|
|
332
580
|
if (typeof content === 'string') {
|
|
333
581
|
const lines = content.split('\n');
|
|
334
582
|
const startLine = this._height + 1;
|
|
335
|
-
// CRITICAL:
|
|
336
|
-
//
|
|
583
|
+
// CRITICAL: Directly update pendingFrame without scheduling renders
|
|
584
|
+
// We don't want add() to trigger any rendering - only update() should render
|
|
337
585
|
const renderer = this.renderer;
|
|
338
586
|
const wasRenderingDisabled = renderer.disableRendering;
|
|
339
587
|
renderer.disableRendering = true;
|
|
340
|
-
//
|
|
588
|
+
// Expand renderer if needed
|
|
589
|
+
const endLine = startLine + lines.length - 1;
|
|
590
|
+
if (endLine > renderer.height) {
|
|
591
|
+
renderer.expandTo(endLine);
|
|
592
|
+
renderer.height = endLine;
|
|
593
|
+
}
|
|
594
|
+
// Directly update pendingFrame without going through setLine() (which schedules renders)
|
|
341
595
|
for (let i = 0; i < lines.length; i++) {
|
|
342
|
-
|
|
596
|
+
const lineIndex = startLine + i - 1;
|
|
597
|
+
while (renderer.pendingFrame.length <= lineIndex) {
|
|
598
|
+
renderer.pendingFrame.push('');
|
|
599
|
+
}
|
|
600
|
+
renderer.pendingFrame[lineIndex] = lines[i];
|
|
601
|
+
// Track in explicitlyAddedLines
|
|
602
|
+
while (this.regionLines.length < startLine + i) {
|
|
603
|
+
this.regionLines.push({ content: '', lineNumber: this.regionLines.length + 1 });
|
|
604
|
+
}
|
|
605
|
+
this.regionLines[startLine + i - 1] = {
|
|
606
|
+
content: lines[i],
|
|
607
|
+
lineNumber: startLine + i
|
|
608
|
+
};
|
|
343
609
|
}
|
|
344
|
-
// Update region height tracking
|
|
345
|
-
this._height =
|
|
346
|
-
// Re-enable rendering
|
|
610
|
+
// Update region height tracking
|
|
611
|
+
this._height = endLine;
|
|
612
|
+
// Re-enable rendering (but don't flush - let caller decide when to render)
|
|
613
|
+
// The update() method will handle flushing when ready
|
|
347
614
|
renderer.disableRendering = wasRenderingDisabled;
|
|
348
|
-
this.
|
|
615
|
+
return new SectionReference(this, startLine, lines.length);
|
|
349
616
|
}
|
|
350
617
|
else if (Array.isArray(content)) {
|
|
351
|
-
|
|
352
|
-
//
|
|
353
|
-
// setLine() will expand the region automatically, but we want to batch renders
|
|
618
|
+
// CRITICAL: Check if region is effectively empty (no content has been set yet)
|
|
619
|
+
// If so, start from line 1 instead of appending after height
|
|
354
620
|
const renderer = this.renderer;
|
|
621
|
+
// Region is empty if:
|
|
622
|
+
// 1. Height is 0, OR
|
|
623
|
+
// 2. All lines in pendingFrame are empty strings (no content set yet)
|
|
624
|
+
const isEmpty = this._height === 0 ||
|
|
625
|
+
(renderer.pendingFrame && renderer.pendingFrame.length > 0 &&
|
|
626
|
+
renderer.pendingFrame.every((line) => !line || line.trim() === ''));
|
|
627
|
+
const startLine = isEmpty ? 1 : this._height + 1;
|
|
628
|
+
// CRITICAL: Directly update pendingFrame without scheduling renders
|
|
629
|
+
// We don't want add() to trigger any rendering - only update() should render
|
|
355
630
|
const wasRenderingDisabled = renderer.disableRendering;
|
|
356
631
|
renderer.disableRendering = true;
|
|
357
|
-
//
|
|
632
|
+
// Expand renderer if needed
|
|
633
|
+
const endLine = startLine + content.length - 1;
|
|
634
|
+
if (endLine > renderer.height) {
|
|
635
|
+
renderer.expandTo(endLine);
|
|
636
|
+
renderer.height = endLine;
|
|
637
|
+
}
|
|
638
|
+
// Directly update pendingFrame without going through setLine() (which schedules renders)
|
|
358
639
|
for (let i = 0; i < content.length; i++) {
|
|
359
|
-
|
|
640
|
+
const lineIndex = startLine + i - 1;
|
|
641
|
+
const lineContent = String(content[i]);
|
|
642
|
+
while (renderer.pendingFrame.length <= lineIndex) {
|
|
643
|
+
renderer.pendingFrame.push('');
|
|
644
|
+
}
|
|
645
|
+
renderer.pendingFrame[lineIndex] = lineContent;
|
|
646
|
+
// Track in explicitlyAddedLines
|
|
647
|
+
while (this.explicitlyAddedLines.length < startLine + i) {
|
|
648
|
+
this.explicitlyAddedLines.push({ content: '', lineNumber: this.explicitlyAddedLines.length + 1 });
|
|
649
|
+
}
|
|
650
|
+
this.explicitlyAddedLines[startLine + i - 1] = {
|
|
651
|
+
content: lineContent,
|
|
652
|
+
lineNumber: startLine + i
|
|
653
|
+
};
|
|
360
654
|
}
|
|
361
|
-
// Update region height tracking
|
|
362
|
-
this._height =
|
|
363
|
-
// Re-enable rendering
|
|
655
|
+
// Update region height tracking
|
|
656
|
+
this._height = Math.max(this._height, endLine);
|
|
657
|
+
// Re-enable rendering (but don't flush - let caller decide when to render)
|
|
658
|
+
// The update() method will handle flushing when ready
|
|
364
659
|
renderer.disableRendering = wasRenderingDisabled;
|
|
365
|
-
this.
|
|
660
|
+
return new SectionReference(this, startLine, content.length);
|
|
366
661
|
}
|
|
662
|
+
// Fallback: return empty section reference (shouldn't happen)
|
|
663
|
+
return new SectionReference(this, this._height + 1, 0);
|
|
367
664
|
}
|
|
368
665
|
clearLine(lineNumber) {
|
|
369
666
|
if (lineNumber < 1) {
|
|
@@ -374,9 +671,12 @@ export class TerminalRegion {
|
|
|
374
671
|
clear() {
|
|
375
672
|
this.renderer.clear();
|
|
376
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* Force immediate render of any pending updates (bypasses throttle)
|
|
676
|
+
* Returns a promise that resolves when rendering is complete
|
|
677
|
+
* @internal - Internal method, not part of public API
|
|
678
|
+
*/
|
|
377
679
|
async flush() {
|
|
378
|
-
// Force immediate render of any pending updates (bypasses throttle)
|
|
379
|
-
// Returns a promise that resolves when rendering is complete
|
|
380
680
|
await this.renderer.flush();
|
|
381
681
|
}
|
|
382
682
|
/**
|
|
@@ -394,104 +694,208 @@ export class TerminalRegion {
|
|
|
394
694
|
// CRITICAL: Prevent concurrent re-renders
|
|
395
695
|
// If we're already re-rendering, skip this call to prevent duplicates
|
|
396
696
|
if (this.isReRendering) {
|
|
697
|
+
const renderer = this.renderer;
|
|
698
|
+
renderer.logToFile(`[reRenderLastContent] SKIPPED: already re-rendering`);
|
|
397
699
|
return;
|
|
398
700
|
}
|
|
399
701
|
if (this.allComponentDescriptors.length > 0) {
|
|
400
702
|
this.isReRendering = true;
|
|
401
703
|
try {
|
|
402
704
|
const renderer = this.renderer;
|
|
403
|
-
|
|
705
|
+
// CRITICAL: Read width AFTER setting isReRendering to ensure we get the latest value
|
|
706
|
+
// The width getter syncs with renderer.getWidth() which should have the updated width from resize
|
|
404
707
|
const width = this.width;
|
|
708
|
+
renderer.logToFile(`[reRenderLastContent] CALLED: height=${this._height} width=${width} lastRenderedHeight=${renderer.lastRenderedHeight}`);
|
|
405
709
|
// First, calculate total height of ALL content (components + static lines)
|
|
406
710
|
let totalHeight = 0;
|
|
407
|
-
for (const
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (descriptor.type === 'grid') {
|
|
413
|
-
const component = resolveGrid(this, descriptor);
|
|
414
|
-
totalHeight += component.getHeight();
|
|
711
|
+
for (const itemOrArray of this.allComponentDescriptors) {
|
|
712
|
+
const items = Array.isArray(itemOrArray) ? itemOrArray : [itemOrArray];
|
|
713
|
+
for (const item of items) {
|
|
714
|
+
if (isComponent(item)) {
|
|
715
|
+
totalHeight += getComponentHeight(item, this, width);
|
|
415
716
|
}
|
|
416
717
|
}
|
|
417
718
|
}
|
|
418
719
|
// CRITICAL: Include static lines (waitForSpacebar, whitespace) in total height
|
|
419
720
|
// Count how many static lines exist beyond the component height
|
|
420
|
-
let
|
|
421
|
-
for (let i = 0; i < this.
|
|
422
|
-
const lineInfo = this.
|
|
423
|
-
if (lineInfo.
|
|
424
|
-
|
|
721
|
+
let maxExplicitLineNumber = 0;
|
|
722
|
+
for (let i = 0; i < this.explicitlyAddedLines.length; i++) {
|
|
723
|
+
const lineInfo = this.explicitlyAddedLines[i];
|
|
724
|
+
if (lineInfo.content) {
|
|
725
|
+
maxExplicitLineNumber = Math.max(maxExplicitLineNumber, lineInfo.lineNumber);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
// totalHeight should be at least as large as the highest explicitly added line number
|
|
729
|
+
totalHeight = Math.max(totalHeight, maxExplicitLineNumber);
|
|
730
|
+
// CRITICAL: Collect explicitly added lines BEFORE sizing frames, so we know the final height
|
|
731
|
+
// explicitlyAddedLines ONLY contains explicitly added content (prompts, etc.)
|
|
732
|
+
// Components are re-rendered from allComponentDescriptors, so they're not tracked here
|
|
733
|
+
// TODO: With signals, we'll track signal dependencies and re-render only what changed
|
|
734
|
+
const explicitlyAddedLines = [];
|
|
735
|
+
// CRITICAL: Collect ALL explicitly added lines that have content
|
|
736
|
+
// We'll render them AFTER all components, regardless of their original line numbers
|
|
737
|
+
// The original line numbers are just for preserving order, not for filtering
|
|
738
|
+
// This ensures prompts and other explicitly added content always appear at the end
|
|
739
|
+
for (let i = 0; i < this.explicitlyAddedLines.length; i++) {
|
|
740
|
+
const lineInfo = this.explicitlyAddedLines[i];
|
|
741
|
+
if (lineInfo.content && lineInfo.content.trim() !== '') {
|
|
742
|
+
explicitlyAddedLines.push({
|
|
743
|
+
originalLineNumber: lineInfo.lineNumber,
|
|
744
|
+
content: lineInfo.content,
|
|
745
|
+
});
|
|
746
|
+
renderer.logToFile(`[reRenderLastContent] Found explicitly added line at index ${i} (original line ${lineInfo.lineNumber}): "${lineInfo.content.substring(0, 40)}"`);
|
|
425
747
|
}
|
|
426
748
|
}
|
|
427
|
-
//
|
|
428
|
-
|
|
749
|
+
// Sort explicitly added lines by original line number to preserve order
|
|
750
|
+
explicitlyAddedLines.sort((a, b) => a.originalLineNumber - b.originalLineNumber);
|
|
751
|
+
renderer.logToFile(`[reRenderLastContent] Collected ${explicitlyAddedLines.length} explicitly added lines (will render after components)`);
|
|
752
|
+
// Calculate final height: components + explicitly added lines
|
|
753
|
+
const estimatedFinalHeight = totalHeight + explicitlyAddedLines.length;
|
|
429
754
|
// CRITICAL: Ensure frames are the correct size BEFORE re-rendering
|
|
430
|
-
//
|
|
431
|
-
while (renderer.pendingFrame.length <
|
|
755
|
+
// Use estimated final height to avoid truncating static lines
|
|
756
|
+
while (renderer.pendingFrame.length < estimatedFinalHeight) {
|
|
432
757
|
renderer.pendingFrame.push('');
|
|
433
758
|
}
|
|
434
|
-
while (renderer.previousFrame.length <
|
|
759
|
+
while (renderer.previousFrame.length < estimatedFinalHeight) {
|
|
435
760
|
renderer.previousFrame.push('');
|
|
436
761
|
}
|
|
437
|
-
//
|
|
438
|
-
renderer.pendingFrame = renderer.pendingFrame.slice(0, totalHeight);
|
|
439
|
-
renderer.previousFrame = renderer.previousFrame.slice(0, totalHeight);
|
|
762
|
+
// Don't truncate frames - we'll expand as needed when rendering static lines
|
|
440
763
|
// CRITICAL: Don't clear previousFrame on resize/re-render
|
|
441
764
|
// We need to preserve previousFrame so renderNow() can detect if content actually changed
|
|
442
765
|
// If we clear it, renderNow() will always think content changed and re-render unnecessarily
|
|
443
766
|
// The previousFrame will be updated after renderNow() completes
|
|
444
|
-
// Clear all lines before re-rendering
|
|
445
|
-
|
|
767
|
+
// CRITICAL: Clear all lines before re-rendering
|
|
768
|
+
// We need to clear the ENTIRE pendingFrame, not just up to totalHeight,
|
|
769
|
+
// because old content might be beyond totalHeight and cause corruption
|
|
770
|
+
renderer.logToFile(`[reRenderLastContent] BEFORE CLEAR: pendingFrame.length=${renderer.pendingFrame.length}, first 5 lines:`);
|
|
771
|
+
for (let i = 0; i < Math.min(5, renderer.pendingFrame.length); i++) {
|
|
772
|
+
renderer.logToFile(`[reRenderLastContent] [${i}]: "${renderer.pendingFrame[i].substring(0, 50)}"`);
|
|
773
|
+
}
|
|
774
|
+
for (let i = 0; i < renderer.pendingFrame.length; i++) {
|
|
446
775
|
renderer.pendingFrame[i] = '';
|
|
447
776
|
}
|
|
777
|
+
// Also ensure pendingFrame is exactly the right size
|
|
778
|
+
renderer.pendingFrame = renderer.pendingFrame.slice(0, estimatedFinalHeight);
|
|
779
|
+
while (renderer.pendingFrame.length < estimatedFinalHeight) {
|
|
780
|
+
renderer.pendingFrame.push('');
|
|
781
|
+
}
|
|
782
|
+
renderer.logToFile(`[reRenderLastContent] AFTER CLEAR: pendingFrame.length=${renderer.pendingFrame.length}, estimatedFinalHeight=${estimatedFinalHeight}`);
|
|
448
783
|
// CRITICAL: Disable auto-rendering during component rendering
|
|
449
784
|
// Components call setLine() which triggers scheduleRender(), but we want
|
|
450
785
|
// to batch all updates and render once at the end
|
|
451
786
|
const wasRenderingDisabled = renderer.disableRendering;
|
|
452
787
|
renderer.disableRendering = true;
|
|
453
|
-
// Now re-render ALL
|
|
788
|
+
// Now re-render ALL components starting from line 1
|
|
454
789
|
let currentLine = 1;
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
790
|
+
renderer.logToFile(`[reRenderLastContent] Re-rendering ${this.allComponentDescriptors.length} component descriptor(s)`);
|
|
791
|
+
for (let descIdx = 0; descIdx < this.allComponentDescriptors.length; descIdx++) {
|
|
792
|
+
const itemOrArray = this.allComponentDescriptors[descIdx];
|
|
793
|
+
// Each entry can be a single component or an array of components (for multi-line sections)
|
|
794
|
+
const items = Array.isArray(itemOrArray) ? itemOrArray : [itemOrArray];
|
|
795
|
+
renderer.logToFile(`[reRenderLastContent] Descriptor ${descIdx}: ${items.length} item(s), starting at line ${currentLine}`);
|
|
460
796
|
// Re-render each component in this section
|
|
461
|
-
for (
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const componentHeight =
|
|
465
|
-
|
|
466
|
-
component
|
|
797
|
+
for (let itemIdx = 0; itemIdx < items.length; itemIdx++) {
|
|
798
|
+
const item = items[itemIdx];
|
|
799
|
+
if (isComponent(item)) {
|
|
800
|
+
const componentHeight = getComponentHeight(item, this, width);
|
|
801
|
+
renderer.logToFile(`[reRenderLastContent] Rendering component ${itemIdx} at line ${currentLine}, height=${componentHeight}`);
|
|
802
|
+
// Render component (pure - just returns content)
|
|
803
|
+
const componentLines = renderComponent(item, this, width);
|
|
804
|
+
// Write component content to pendingFrame at the correct position
|
|
805
|
+
for (let i = 0; i < componentLines.length; i++) {
|
|
806
|
+
const lineNumber = currentLine + i;
|
|
807
|
+
const index = lineNumber - 1;
|
|
808
|
+
renderer.ensureFrameSize(index + 1);
|
|
809
|
+
// DEBUG: Log what we're writing and where
|
|
810
|
+
const oldContent = renderer.pendingFrame[index] ? renderer.pendingFrame[index].substring(0, 40) : '(empty)';
|
|
811
|
+
const newContent = componentLines[i].substring(0, 40);
|
|
812
|
+
if (oldContent !== newContent && oldContent !== '(empty)') {
|
|
813
|
+
renderer.logToFile(`[reRenderLastContent] WARNING: Overwriting component line ${lineNumber} (index ${index}) - old: "${oldContent}", new: "${newContent}"`);
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
renderer.logToFile(`[reRenderLastContent] Writing component line ${lineNumber} (index ${index}): "${newContent}"`);
|
|
817
|
+
}
|
|
818
|
+
renderer.pendingFrame[index] = componentLines[i];
|
|
819
|
+
if (lineNumber > renderer.height) {
|
|
820
|
+
renderer.height = lineNumber;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
467
823
|
// Move to next line
|
|
468
824
|
currentLine += componentHeight;
|
|
469
825
|
}
|
|
470
826
|
}
|
|
471
827
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
renderer.
|
|
828
|
+
renderer.logToFile(`[reRenderLastContent] Finished rendering components, currentLine=${currentLine}`);
|
|
829
|
+
// DEBUG: Log pendingFrame state after component rendering
|
|
830
|
+
renderer.logToFile(`[reRenderLastContent] AFTER COMPONENT RENDER: pendingFrame.length=${renderer.pendingFrame.length}`);
|
|
831
|
+
for (let i = 0; i < Math.min(10, renderer.pendingFrame.length); i++) {
|
|
832
|
+
const content = renderer.pendingFrame[i] ? renderer.pendingFrame[i].substring(0, 50) : '(empty)';
|
|
833
|
+
renderer.logToFile(`[reRenderLastContent] [${i}] (line ${i + 1}): "${content}"`);
|
|
834
|
+
}
|
|
835
|
+
// After re-rendering components, re-render explicitly added lines
|
|
836
|
+
// Explicitly added lines should come AFTER all components
|
|
837
|
+
// (explicitlyAddedLines was already collected above)
|
|
838
|
+
// Re-render explicitly added lines starting after all components
|
|
839
|
+
// currentLine is where the last component ended, so explicitly added lines start there
|
|
840
|
+
let explicitLinePosition = currentLine;
|
|
841
|
+
renderer.logToFile(`[reRenderLastContent] Re-rendering ${explicitlyAddedLines.length} explicitly added lines starting at line ${explicitLinePosition}`);
|
|
842
|
+
for (const explicitLine of explicitlyAddedLines) {
|
|
843
|
+
// Ensure pendingFrame has enough lines
|
|
844
|
+
while (renderer.pendingFrame.length < explicitLinePosition) {
|
|
845
|
+
renderer.pendingFrame.push('');
|
|
846
|
+
}
|
|
847
|
+
const index = explicitLinePosition - 1;
|
|
848
|
+
const oldContent = renderer.pendingFrame[index] ? renderer.pendingFrame[index].substring(0, 40) : '(empty)';
|
|
849
|
+
const newContent = explicitLine.content.substring(0, 40);
|
|
850
|
+
if (oldContent !== newContent && oldContent !== '(empty)') {
|
|
851
|
+
renderer.logToFile(`[reRenderLastContent] WARNING: Overwriting explicitly added line ${explicitLinePosition} (index ${index}, original ${explicitLine.originalLineNumber}) - old: "${oldContent}", new: "${newContent}"`);
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
renderer.logToFile(`[reRenderLastContent] Writing explicitly added line ${explicitLinePosition} (index ${index}, original ${explicitLine.originalLineNumber}): "${newContent}"`);
|
|
855
|
+
}
|
|
856
|
+
renderer.pendingFrame[index] = explicitLine.content;
|
|
857
|
+
explicitLinePosition++;
|
|
858
|
+
}
|
|
859
|
+
// DEBUG: Log pendingFrame state after explicitly added line rendering
|
|
860
|
+
renderer.logToFile(`[reRenderLastContent] AFTER EXPLICITLY ADDED LINES: pendingFrame.length=${renderer.pendingFrame.length}`);
|
|
861
|
+
for (let i = 0; i < Math.min(10, renderer.pendingFrame.length); i++) {
|
|
862
|
+
const content = renderer.pendingFrame[i] ? renderer.pendingFrame[i].substring(0, 50) : '(empty)';
|
|
863
|
+
renderer.logToFile(`[reRenderLastContent] [${i}] (line ${i + 1}): "${content}"`);
|
|
864
|
+
}
|
|
865
|
+
// Update total height to include explicitly added lines
|
|
866
|
+
const finalHeight = Math.max(totalHeight, explicitLinePosition - 1);
|
|
867
|
+
// Update explicitlyAddedLines to reflect new positions
|
|
868
|
+
// Only track explicitly added lines - components are re-rendered from allComponentDescriptors
|
|
869
|
+
this.explicitlyAddedLines = [];
|
|
870
|
+
// Add explicitly added lines at their new positions (after all components)
|
|
871
|
+
let newExplicitLinePosition = currentLine;
|
|
872
|
+
for (const explicitLine of explicitlyAddedLines) {
|
|
873
|
+
this.explicitlyAddedLines.push({
|
|
874
|
+
content: explicitLine.content,
|
|
875
|
+
lineNumber: newExplicitLinePosition,
|
|
876
|
+
});
|
|
877
|
+
newExplicitLinePosition++;
|
|
878
|
+
}
|
|
879
|
+
// Update height to match all rendered content (components + static lines)
|
|
880
|
+
const oldHeight = this._height;
|
|
881
|
+
this._height = finalHeight;
|
|
882
|
+
renderer.setHeight(finalHeight);
|
|
883
|
+
// CRITICAL: If content height changed significantly, reset previousViewportFrame
|
|
884
|
+
// This prevents the diff algorithm from comparing mismatched viewport positions
|
|
885
|
+
// When content wraps/unwraps, the viewport shows different logical lines,
|
|
886
|
+
// so we need a fresh diff comparison
|
|
887
|
+
if (Math.abs(finalHeight - oldHeight) > 0) {
|
|
888
|
+
const rendererInternal = renderer;
|
|
889
|
+
// Reset previousViewportFrame to force full redraw with correct viewport positioning
|
|
890
|
+
rendererInternal.previousViewportFrame = [];
|
|
891
|
+
rendererInternal.lastRenderedHeight = 0;
|
|
892
|
+
}
|
|
475
893
|
// CRITICAL: DON'T update lastRenderedHeight before flushing
|
|
476
894
|
// This would change the state and make renderNow() think height hasn't increased
|
|
477
895
|
// when it actually has. Let renderNow() update lastRenderedHeight after rendering.
|
|
478
896
|
// This keeps the state consistent with the normal set() path
|
|
479
897
|
const oldLastRenderedHeight = renderer.lastRenderedHeight;
|
|
480
|
-
renderer.logToFile(`[reRenderLastContent] BEFORE flush: height=${
|
|
481
|
-
// CRITICAL: Re-render static lines (waitForSpacebar, whitespace)
|
|
482
|
-
// These are tracked in regionLines with type 'static'
|
|
483
|
-
for (let i = 0; i < this.regionLines.length; i++) {
|
|
484
|
-
const lineInfo = this.regionLines[i];
|
|
485
|
-
if (lineInfo.type === 'static' && lineInfo.content !== undefined) {
|
|
486
|
-
// Re-render this static line
|
|
487
|
-
const lineNumber = i + 1;
|
|
488
|
-
// Ensure pendingFrame has enough lines
|
|
489
|
-
while (renderer.pendingFrame.length < lineNumber) {
|
|
490
|
-
renderer.pendingFrame.push('');
|
|
491
|
-
}
|
|
492
|
-
renderer.pendingFrame[lineNumber - 1] = lineInfo.content;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
898
|
+
renderer.logToFile(`[reRenderLastContent] BEFORE flush: height=${finalHeight} oldHeight=${oldHeight} lastRenderedHeight=${oldLastRenderedHeight} explicitlyAddedLines=${explicitlyAddedLines.length}`);
|
|
495
899
|
// Re-enable rendering and flush once
|
|
496
900
|
renderer.disableRendering = wasRenderingDisabled;
|
|
497
901
|
this.renderer.flush();
|
|
@@ -506,6 +910,8 @@ export class TerminalRegion {
|
|
|
506
910
|
// this.region.setThrottleFps(fps);
|
|
507
911
|
}
|
|
508
912
|
async destroy(clearFirst = false) {
|
|
913
|
+
// Cleanup: Call all component cleanup callbacks before destroying
|
|
914
|
+
this.cleanupAllComponents();
|
|
509
915
|
await this.renderer.destroy(clearFirst);
|
|
510
916
|
}
|
|
511
917
|
}
|