linecraft 0.2.1 → 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.
Files changed (110) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +3 -1
  3. package/lib/component.d.ts +34 -0
  4. package/lib/component.d.ts.map +1 -0
  5. package/lib/component.js +42 -0
  6. package/lib/component.js.map +1 -0
  7. package/lib/components/code-debug.d.ts +35 -0
  8. package/lib/components/code-debug.d.ts.map +1 -0
  9. package/lib/components/code-debug.js +294 -0
  10. package/lib/components/code-debug.js.map +1 -0
  11. package/lib/components/fill.d.ts +15 -0
  12. package/lib/components/fill.d.ts.map +1 -0
  13. package/lib/components/fill.js +37 -0
  14. package/lib/components/fill.js.map +1 -0
  15. package/lib/components/index.d.ts +2 -2
  16. package/lib/components/index.d.ts.map +1 -1
  17. package/lib/components/index.js +2 -2
  18. package/lib/components/index.js.map +1 -1
  19. package/lib/components/progress-bar-grid.d.ts +1 -1
  20. package/lib/components/progress-bar-grid.d.ts.map +1 -1
  21. package/lib/components/progress-bar-grid.js +6 -6
  22. package/lib/components/progress-bar-grid.js.map +1 -1
  23. package/lib/components/prompt.d.ts +4 -5
  24. package/lib/components/prompt.d.ts.map +1 -1
  25. package/lib/components/prompt.js +17 -69
  26. package/lib/components/prompt.js.map +1 -1
  27. package/lib/components/section.d.ts +33 -0
  28. package/lib/components/section.d.ts.map +1 -0
  29. package/lib/components/section.js +178 -0
  30. package/lib/components/section.js.map +1 -0
  31. package/lib/components/segments.d.ts +26 -0
  32. package/lib/components/segments.d.ts.map +1 -0
  33. package/lib/components/segments.js +105 -0
  34. package/lib/components/segments.js.map +1 -0
  35. package/lib/components/spinner.d.ts +18 -16
  36. package/lib/components/spinner.d.ts.map +1 -1
  37. package/lib/components/spinner.js +63 -47
  38. package/lib/components/spinner.js.map +1 -1
  39. package/lib/components/style.test.js +11 -11
  40. package/lib/components/style.test.js.map +1 -1
  41. package/lib/components/styled.d.ts +17 -0
  42. package/lib/components/styled.d.ts.map +1 -0
  43. package/lib/components/styled.js +107 -0
  44. package/lib/components/styled.js.map +1 -0
  45. package/lib/components/styled.test.d.ts +2 -0
  46. package/lib/components/styled.test.d.ts.map +1 -0
  47. package/lib/components/styled.test.js +135 -0
  48. package/lib/components/styled.test.js.map +1 -0
  49. package/lib/index.d.ts +17 -13
  50. package/lib/index.d.ts.map +1 -1
  51. package/lib/index.js +13 -13
  52. package/lib/index.js.map +1 -1
  53. package/lib/index.test.js +17 -11
  54. package/lib/index.test.js.map +1 -1
  55. package/lib/layout/grid.d.ts +31 -35
  56. package/lib/layout/grid.d.ts.map +1 -1
  57. package/lib/layout/grid.js +437 -216
  58. package/lib/layout/grid.js.map +1 -1
  59. package/lib/layout/grid.test.js +332 -36
  60. package/lib/layout/grid.test.js.map +1 -1
  61. package/lib/native/ansi.d.ts +9 -0
  62. package/lib/native/ansi.d.ts.map +1 -1
  63. package/lib/native/ansi.js +9 -0
  64. package/lib/native/ansi.js.map +1 -1
  65. package/lib/native/diff.d.ts +5 -1
  66. package/lib/native/diff.d.ts.map +1 -1
  67. package/lib/native/diff.js +25 -7
  68. package/lib/native/diff.js.map +1 -1
  69. package/lib/native/region-renderer-debug.test.d.ts +2 -0
  70. package/lib/native/region-renderer-debug.test.d.ts.map +1 -0
  71. package/lib/native/region-renderer-debug.test.js +45 -0
  72. package/lib/native/region-renderer-debug.test.js.map +1 -0
  73. package/lib/native/region-renderer.d.ts +57 -148
  74. package/lib/native/region-renderer.d.ts.map +1 -1
  75. package/lib/native/region-renderer.js +455 -1124
  76. package/lib/native/region-renderer.js.map +1 -1
  77. package/lib/native/region.test.js +2 -20
  78. package/lib/native/region.test.js.map +1 -1
  79. package/lib/region-resize.test.d.ts +2 -0
  80. package/lib/region-resize.test.d.ts.map +1 -0
  81. package/lib/region-resize.test.js +124 -0
  82. package/lib/region-resize.test.js.map +1 -0
  83. package/lib/region.d.ts +97 -9
  84. package/lib/region.d.ts.map +1 -1
  85. package/lib/region.js +591 -185
  86. package/lib/region.js.map +1 -1
  87. package/lib/region.test.js +3 -3
  88. package/lib/region.test.js.map +1 -1
  89. package/lib/types.d.ts +9 -0
  90. package/lib/types.d.ts.map +1 -1
  91. package/lib/utils/file-link.d.ts +16 -0
  92. package/lib/utils/file-link.d.ts.map +1 -0
  93. package/lib/utils/file-link.js +23 -0
  94. package/lib/utils/file-link.js.map +1 -0
  95. package/lib/utils/prompt.d.ts +15 -0
  96. package/lib/utils/prompt.d.ts.map +1 -0
  97. package/lib/utils/prompt.js +128 -0
  98. package/lib/utils/prompt.js.map +1 -0
  99. package/lib/utils/terminal-theme.d.ts +36 -0
  100. package/lib/utils/terminal-theme.d.ts.map +1 -0
  101. package/lib/utils/terminal-theme.js +61 -0
  102. package/lib/utils/terminal-theme.js.map +1 -0
  103. package/lib/utils/text.d.ts +53 -3
  104. package/lib/utils/text.d.ts.map +1 -1
  105. package/lib/utils/text.js +194 -36
  106. package/lib/utils/text.js.map +1 -1
  107. package/lib/utils/wait-for-spacebar.d.ts.map +1 -1
  108. package/lib/utils/wait-for-spacebar.js +9 -6
  109. package/lib/utils/wait-for-spacebar.js.map +1 -1
  110. package/package.json +13 -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
- import { resolveGrid } from './api/grid';
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 all lines in the region (component + waitForSpacebar + any whitespace)
19
- // This allows us to re-render the entire region correctly
20
- regionLines = [];
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
- * Set a single line (1-based line numbers)
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
- setLine(lineNumber, content) {
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
- // CRITICAL: Track this line as static content (not part of component)
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.regionLines.length < lineNumber) {
81
- this.regionLines.push({ type: 'static', lineNumber: this.regionLines.length + 1 });
331
+ while (this.explicitlyAddedLines.length < lineNumber) {
332
+ this.explicitlyAddedLines.push({ content: '', lineNumber: this.explicitlyAddedLines.length + 1 });
82
333
  }
83
- this.regionLines[lineNumber - 1] = {
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 grid descriptor
103
- if (allContent.length > 0 && typeof allContent[0] === 'object' && allContent[0] !== null && 'type' in allContent[0] && allContent[0].type === 'grid') {
104
- // Grid descriptor(s)
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 descriptor of allContent) {
112
- if (descriptor.type === 'grid') {
113
- const component = resolveGrid(this, descriptor);
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
- this.regionLines = [];
131
- for (let i = 0; i < totalHeight; i++) {
132
- this.regionLines[i] = { type: 'component', lineNumber: i + 1 };
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 descriptor of allContent) {
142
- if (descriptor.type === 'grid') {
143
- const component = resolveGrid(this, descriptor);
144
- const height = component.getHeight();
145
- component.render(0, currentLine, width);
146
- currentLine += height;
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.height = totalHeight;
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 grid descriptor
224
- if (allContent.length > 0 && typeof allContent[0] === 'object' && allContent[0] !== null && 'type' in allContent[0] && allContent[0].type === 'grid') {
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 resolvedComponents = [];
229
- for (const descriptor of allContent) {
230
- if (descriptor.type === 'grid') {
231
- const component = resolveGrid(this, descriptor);
232
- resolvedComponents.push(component);
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
- // Update regionLines
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.height = this._height;
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 resolvedComponents) {
275
- const height = component.getHeight();
276
- component.render(0, currentLine, width);
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 grid descriptor
286
- if (typeof content === 'object' && content !== null && 'type' in content && content.type === 'grid') {
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 = component.getHeight();
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
- // Update regionLines
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.height = this._height;
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
- component.render(0, startLine, width);
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: Disable auto-rendering during line setting to batch updates
336
- // setLine() will expand the region automatically, but we want to batch renders
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
- // Set all lines (setLine() will expand region automatically)
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
- this.setLine(startLine + i, lines[i]);
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 (setLine() already updated renderer.height)
345
- this._height = startLine + lines.length - 1;
346
- // Re-enable rendering and flush once
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.renderer.flush();
615
+ return new SectionReference(this, startLine, lines.length);
349
616
  }
350
617
  else if (Array.isArray(content)) {
351
- const startLine = this._height + 1;
352
- // CRITICAL: Disable auto-rendering during line setting to batch updates
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
- // Set all lines (setLine() will expand region automatically)
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
- this.setLine(startLine + i, String(content[i]));
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 (setLine() already updated renderer.height)
362
- this._height = startLine + content.length - 1;
363
- // Re-enable rendering and flush once
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.renderer.flush();
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
- renderer.logToFile(`[reRenderLastContent] CALLED: height=${this._height} lastRenderedHeight=${renderer.lastRenderedHeight}`);
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 descriptorOrArray of this.allComponentDescriptors) {
408
- const descriptors = Array.isArray(descriptorOrArray) && descriptorOrArray.length > 0 && typeof descriptorOrArray[0] === 'object' && descriptorOrArray[0] !== null && 'type' in descriptorOrArray[0]
409
- ? descriptorOrArray
410
- : [descriptorOrArray];
411
- for (const descriptor of descriptors) {
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 maxStaticLineNumber = 0;
421
- for (let i = 0; i < this.regionLines.length; i++) {
422
- const lineInfo = this.regionLines[i];
423
- if (lineInfo.type === 'static' && lineInfo.content !== undefined) {
424
- maxStaticLineNumber = Math.max(maxStaticLineNumber, lineInfo.lineNumber);
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
- // totalHeight should be at least as large as the highest static line number
428
- totalHeight = Math.max(totalHeight, maxStaticLineNumber);
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
- // This prevents overwriting or truncating content incorrectly
431
- while (renderer.pendingFrame.length < totalHeight) {
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 < totalHeight) {
759
+ while (renderer.previousFrame.length < estimatedFinalHeight) {
435
760
  renderer.previousFrame.push('');
436
761
  }
437
- // Truncate frames to exact height (in case they're too large)
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
- for (let i = 0; i < totalHeight; i++) {
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 component descriptors starting from line 1
788
+ // Now re-render ALL components starting from line 1
454
789
  let currentLine = 1;
455
- for (const descriptorOrArray of this.allComponentDescriptors) {
456
- // Each entry can be a single descriptor or an array of descriptors (for multi-line sections)
457
- const descriptors = Array.isArray(descriptorOrArray) && descriptorOrArray.length > 0 && typeof descriptorOrArray[0] === 'object' && descriptorOrArray[0] !== null && 'type' in descriptorOrArray[0]
458
- ? descriptorOrArray
459
- : [descriptorOrArray];
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 (const descriptor of descriptors) {
462
- if (descriptor.type === 'grid') {
463
- const component = resolveGrid(this, descriptor);
464
- const componentHeight = component.getHeight();
465
- // Render component at current line
466
- component.render(0, currentLine, width);
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
- // Update height to match all rendered content
473
- this._height = totalHeight;
474
- renderer.height = totalHeight;
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=${totalHeight} lastRenderedHeight=${oldLastRenderedHeight}`);
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
  }