@pellux/goodvibes-tui 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/src/core/context-usage.ts +53 -0
  5. package/src/core/conversation.ts +4 -6
  6. package/src/core/session-recovery.ts +2 -0
  7. package/src/core/turn-event-wiring.ts +1 -0
  8. package/src/input/commands/cost-runtime.ts +49 -0
  9. package/src/input/commands/eval.ts +4 -3
  10. package/src/input/commands/operator-runtime.ts +5 -1
  11. package/src/input/commands.ts +2 -0
  12. package/src/input/handler-feed-routes.ts +60 -26
  13. package/src/input/handler-feed.ts +17 -8
  14. package/src/input/handler-picker-routes.ts +18 -13
  15. package/src/input/handler-shortcuts.ts +51 -0
  16. package/src/input/handler-ui-state.ts +4 -0
  17. package/src/input/handler.ts +43 -6
  18. package/src/input/keybindings.ts +53 -8
  19. package/src/input/model-picker.ts +5 -0
  20. package/src/input/panel-integration-actions.ts +117 -1
  21. package/src/main.ts +5 -2
  22. package/src/panels/agent-inspector-panel.ts +139 -45
  23. package/src/panels/agent-inspector-shared.ts +147 -3
  24. package/src/panels/approval-panel.ts +204 -102
  25. package/src/panels/automation-control-panel.ts +370 -103
  26. package/src/panels/base-panel.ts +26 -2
  27. package/src/panels/builtin/agent.ts +28 -37
  28. package/src/panels/builtin/development.ts +40 -32
  29. package/src/panels/builtin/knowledge.ts +15 -6
  30. package/src/panels/builtin/operations.ts +178 -117
  31. package/src/panels/builtin/session.ts +35 -9
  32. package/src/panels/builtin/shared.ts +98 -6
  33. package/src/panels/cockpit-panel.ts +253 -75
  34. package/src/panels/cockpit-read-model.ts +2 -1
  35. package/src/panels/communication-panel.ts +159 -56
  36. package/src/panels/confirm-state.ts +8 -1
  37. package/src/panels/control-plane-panel.ts +354 -95
  38. package/src/panels/cost-tracker-panel.ts +243 -57
  39. package/src/panels/debug-panel.ts +347 -159
  40. package/src/panels/diff-panel.ts +257 -82
  41. package/src/panels/docs-panel.ts +166 -64
  42. package/src/panels/eval-panel.ts +336 -109
  43. package/src/panels/expandable-list-panel.ts +189 -0
  44. package/src/panels/file-explorer-panel.ts +238 -159
  45. package/src/panels/file-preview-panel.ts +141 -59
  46. package/src/panels/git-panel.ts +360 -191
  47. package/src/panels/hooks-panel.ts +181 -19
  48. package/src/panels/incident-review-panel.ts +252 -43
  49. package/src/panels/index.ts +0 -4
  50. package/src/panels/intelligence-panel.ts +310 -103
  51. package/src/panels/knowledge-graph-panel.ts +480 -58
  52. package/src/panels/local-auth-panel.ts +251 -33
  53. package/src/panels/marketplace-panel.ts +219 -33
  54. package/src/panels/memory-panel.ts +83 -81
  55. package/src/panels/ops-control-panel.ts +211 -32
  56. package/src/panels/ops-strategy-panel.ts +131 -45
  57. package/src/panels/orchestration-panel.ts +259 -67
  58. package/src/panels/panel-list-panel.ts +184 -136
  59. package/src/panels/panel-manager.ts +120 -13
  60. package/src/panels/plan-dashboard-panel.ts +507 -74
  61. package/src/panels/plugins-panel.ts +227 -34
  62. package/src/panels/policy-panel.ts +264 -55
  63. package/src/panels/polish-core.ts +162 -0
  64. package/src/panels/polish-tables.ts +258 -0
  65. package/src/panels/polish.ts +67 -149
  66. package/src/panels/project-planning-answer-actions.ts +134 -0
  67. package/src/panels/project-planning-panel.ts +84 -113
  68. package/src/panels/provider-health-panel.ts +438 -480
  69. package/src/panels/provider-health-routes.ts +203 -0
  70. package/src/panels/provider-health-tracker.ts +194 -6
  71. package/src/panels/provider-health-views.ts +560 -0
  72. package/src/panels/qr-panel.ts +136 -38
  73. package/src/panels/remote-panel.ts +123 -38
  74. package/src/panels/routes-panel.ts +87 -24
  75. package/src/panels/sandbox-panel.ts +232 -65
  76. package/src/panels/scrollable-list-panel.ts +143 -145
  77. package/src/panels/security-panel.ts +204 -52
  78. package/src/panels/services-panel.ts +174 -76
  79. package/src/panels/session-browser-panel.ts +101 -6
  80. package/src/panels/session-maintenance.ts +4 -122
  81. package/src/panels/settings-sync-panel.ts +346 -68
  82. package/src/panels/skills-panel.ts +157 -89
  83. package/src/panels/subscription-panel.ts +105 -34
  84. package/src/panels/symbol-outline-panel.ts +287 -154
  85. package/src/panels/system-messages-panel.ts +172 -38
  86. package/src/panels/tasks-panel.ts +348 -134
  87. package/src/panels/thinking-panel.ts +66 -32
  88. package/src/panels/token-budget-panel.ts +233 -46
  89. package/src/panels/tool-inspector-panel.ts +170 -54
  90. package/src/panels/types.ts +65 -4
  91. package/src/panels/work-plan-panel.ts +393 -39
  92. package/src/panels/worktree-panel.ts +239 -61
  93. package/src/panels/wrfc-panel-format.ts +133 -0
  94. package/src/panels/wrfc-panel.ts +146 -150
  95. package/src/renderer/compaction-preview.ts +2 -1
  96. package/src/renderer/compositor.ts +46 -43
  97. package/src/renderer/conversation-overlays.ts +25 -11
  98. package/src/renderer/footer-tips.ts +41 -0
  99. package/src/renderer/fullscreen-primitives.ts +22 -16
  100. package/src/renderer/help-overlay.ts +91 -14
  101. package/src/renderer/hint-grammar.ts +52 -0
  102. package/src/renderer/layout.ts +7 -7
  103. package/src/renderer/modal-factory.ts +23 -15
  104. package/src/renderer/model-picker-overlay.ts +30 -11
  105. package/src/renderer/model-workspace.ts +2 -3
  106. package/src/renderer/overlay-box.ts +16 -10
  107. package/src/renderer/panel-composite.ts +7 -20
  108. package/src/renderer/panel-workspace-bar.ts +14 -8
  109. package/src/renderer/process-indicator.ts +3 -1
  110. package/src/renderer/progress.ts +8 -6
  111. package/src/renderer/search-overlay.ts +27 -6
  112. package/src/renderer/session-picker-modal.ts +6 -4
  113. package/src/renderer/settings-modal.ts +70 -10
  114. package/src/renderer/shell-surface.ts +41 -15
  115. package/src/renderer/status-glyphs.ts +11 -9
  116. package/src/renderer/tab-strip.ts +148 -34
  117. package/src/renderer/theme.ts +60 -3
  118. package/src/renderer/ui-factory.ts +45 -36
  119. package/src/renderer/ui-primitives.ts +23 -2
  120. package/src/runtime/bootstrap-shell.ts +35 -18
  121. package/src/runtime/diagnostics/panels/index.ts +0 -3
  122. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  123. package/src/runtime/ui/model-picker/types.ts +4 -0
  124. package/src/shell/ui-openers.ts +14 -22
  125. package/src/utils/format-duration.ts +55 -0
  126. package/src/utils/format-number.ts +71 -0
  127. package/src/utils/splash-lines.ts +44 -3
  128. package/src/version.ts +1 -1
  129. package/src/work-plans/work-plan-store.ts +13 -0
  130. package/src/panels/agent-logs-panel.ts +0 -666
  131. package/src/panels/agent-logs-shared.ts +0 -129
  132. package/src/panels/context-visualizer-panel.ts +0 -214
  133. package/src/panels/forensics-panel.ts +0 -364
  134. package/src/panels/panel-picker.ts +0 -106
  135. package/src/panels/provider-account-snapshot.ts +0 -259
  136. package/src/panels/provider-accounts-panel.ts +0 -218
  137. package/src/panels/provider-stats-panel.ts +0 -366
  138. package/src/panels/schedule-panel.ts +0 -342
  139. package/src/panels/watchers-panel.ts +0 -193
  140. package/src/renderer/panel-picker-overlay.ts +0 -202
  141. package/src/renderer/panel-tab-bar.ts +0 -69
  142. package/src/runtime/diagnostics/panels/panel-resources.ts +0 -118
  143. package/src/runtime/diagnostics/panels/policy.ts +0 -177
@@ -1,5 +1,5 @@
1
- import type { Stats } from 'node:fs';
2
- import { promises as fsPromises, readFileSync, statSync } from 'node:fs';
1
+ import type { Stats, FSWatcher } from 'node:fs';
2
+ import { promises as fsPromises, readFileSync, statSync, watch } from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import type { Line, Cell } from '../types/grid.ts';
5
5
  import { createStyledCell, createEmptyLine } from '../types/grid.ts';
@@ -8,6 +8,7 @@ import { SyntaxHighlighter, type SyntaxToken } from '../renderer/syntax-highligh
8
8
  import { getDisplayWidth } from '../utils/terminal-width.ts';
9
9
  import {
10
10
  buildEmptyState,
11
+ buildKeyboardHints,
11
12
  buildPanelLine,
12
13
  buildPanelWorkspace,
13
14
  resolveScrollablePanelSection,
@@ -19,12 +20,7 @@ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
19
20
 
20
21
  const MAX_FILE_SIZE = 100 * 1024; // 100 KB
21
22
  const BG = '#0d0d0d';
22
- const HEADER_BG = '#1e1e1e';
23
- const HEADER_FG = '#d4d4d4';
24
- const HEADER_ACCENT = '#4ec9b0';
25
23
  const LINE_NUM_FG = '238';
26
- const WARNING_FG = '#f44747';
27
- const EMPTY_FG = '244';
28
24
 
29
25
  // ─── Language Detection (from file extension) ─────────────────────────────────
30
26
 
@@ -63,8 +59,31 @@ export class FilePreviewPanel extends BasePanel {
63
59
  /** Per-file scroll position memory: path -> scrollOffset */
64
60
  private readonly scrollMemory = new Map<string, number>();
65
61
 
62
+ /**
63
+ * 1-based line number to visibly highlight, set by goToLine() (e.g. a
64
+ * symbol-outline jump). Cleared when a new file is opened.
65
+ */
66
+ private highlightedLine: number | null = null;
67
+
68
+ /**
69
+ * Monotonic counter bumped every time fileLines is replaced. Combined with
70
+ * filePath/width/highlightedLine, forms the cache key for the built
71
+ * preview lines so render() doesn't rebuild the whole file every frame.
72
+ */
73
+ private contentVersion = 0;
74
+ private cachedPreviewLines: { key: string; lines: Line[] } | null = null;
75
+
76
+ /** Set by the 'r' key; consumed by handlePanelIntegrationAction on the next
77
+ * dispatch, which awaits the reload before re-syncing the symbol outline
78
+ * (panel-integration-actions.ts owns the panelManager reference this
79
+ * panel doesn't have). */
80
+ private pendingReload = false;
81
+
82
+ /** Optional live-refresh watcher on the currently open file. */
83
+ private watcher: FSWatcher | null = null;
84
+
66
85
  constructor() {
67
- super('preview', 'Preview', 'P', 'development');
86
+ super('preview', 'Preview', '', 'development');
68
87
  }
69
88
 
70
89
  // ─── Public API ─────────────────────────────────────────────────────────────
@@ -82,6 +101,7 @@ export class FilePreviewPanel extends BasePanel {
82
101
  this.filePath = filePath;
83
102
  this.oversized = false;
84
103
  this.fenceTag = extToFenceTag(filePath);
104
+ this.highlightedLine = null;
85
105
 
86
106
  // Restore scroll position for this file, or start at top
87
107
  this.scrollOffset = this.scrollMemory.get(filePath) ?? 0;
@@ -100,10 +120,53 @@ export class FilePreviewPanel extends BasePanel {
100
120
  } catch {
101
121
  this.fileLines = [`(cannot open: ${filePath})`];
102
122
  }
123
+ this.contentVersion++;
103
124
 
125
+ this._startWatching(filePath);
104
126
  void this._loadFileAsync(filePath);
105
127
  }
106
128
 
129
+ /**
130
+ * Re-read the current file from disk (the 'r' reload key). Returns the
131
+ * pending load's promise so callers can chain follow-up work (e.g.
132
+ * re-syncing the symbol outline) after it settles, or null if there is no
133
+ * open file or no reload was queued via the 'r' key.
134
+ */
135
+ consumePendingReload(): Promise<void> | null {
136
+ if (!this.pendingReload) return null;
137
+ this.pendingReload = false;
138
+ if (this.filePath === null) return null;
139
+ return this._loadFileAsync(this.filePath);
140
+ }
141
+
142
+ /**
143
+ * Start (or restart) an optional fs.watch auto-refresh on the given file.
144
+ * Best-effort: some filesystems/platforms don't support watch, so failures
145
+ * are swallowed and auto-refresh is simply unavailable for this file — the
146
+ * explicit 'r' reload key always still works.
147
+ */
148
+ private _startWatching(filePath: string): void {
149
+ this._stopWatching();
150
+ try {
151
+ const watcher = watch(filePath, { persistent: false }, (eventType) => {
152
+ if (eventType !== 'change' && eventType !== 'rename') return;
153
+ if (this.filePath !== filePath) return; // stale watcher from a since-replaced file
154
+ void this._loadFileAsync(filePath);
155
+ });
156
+ watcher.on('error', () => this._stopWatching());
157
+ this.watcher = watcher;
158
+ } catch {
159
+ this.watcher = null;
160
+ }
161
+ }
162
+
163
+ private _stopWatching(): void {
164
+ if (this.watcher) {
165
+ try { this.watcher.close(); } catch { /* already closed */ }
166
+ this.watcher = null;
167
+ }
168
+ }
169
+
107
170
  private async _loadFileAsync(filePath: string): Promise<void> {
108
171
  try {
109
172
  await this.withLoading('Loading…', async () => {
@@ -147,6 +210,7 @@ export class FilePreviewPanel extends BasePanel {
147
210
  } catch (err) {
148
211
  this.setError(summarizeError(err));
149
212
  }
213
+ this.contentVersion++;
150
214
  this.markDirty();
151
215
  }
152
216
 
@@ -161,7 +225,9 @@ export class FilePreviewPanel extends BasePanel {
161
225
 
162
226
  goToLine(line: number): void {
163
227
  if (!Number.isFinite(line)) return;
164
- this.scrollOffset = Math.max(0, Math.min(Math.floor(line) - 1, Math.max(0, this.fileLines.length - 1)));
228
+ const target = Math.max(0, Math.min(Math.floor(line) - 1, Math.max(0, this.fileLines.length - 1)));
229
+ this.scrollOffset = target;
230
+ this.highlightedLine = target + 1;
165
231
  this.markDirty();
166
232
  }
167
233
 
@@ -182,6 +248,11 @@ export class FilePreviewPanel extends BasePanel {
182
248
  }
183
249
  }
184
250
 
251
+ override onDestroy(): void {
252
+ this._stopWatching();
253
+ super.onDestroy();
254
+ }
255
+
185
256
  // ─── Input handling ──────────────────────────────────────────────────────────
186
257
 
187
258
  handleInput(key: string): boolean {
@@ -192,6 +263,12 @@ export class FilePreviewPanel extends BasePanel {
192
263
  case 'pagedown': return this.scrollPage(1);
193
264
  case 'home': return this.scrollTo(0);
194
265
  case 'end': return this.scrollTo(Infinity);
266
+ case 'r':
267
+ if (this.filePath === null) return false;
268
+ this.pendingReload = true;
269
+ return true;
270
+ case 'd':
271
+ return this.filePath !== null;
195
272
  default: return false;
196
273
  }
197
274
  }
@@ -273,26 +350,48 @@ export class FilePreviewPanel extends BasePanel {
273
350
  ],
274
351
  } as const;
275
352
  const footerLines = [
276
- buildPanelLine(width, [[' Up/Down', DEFAULT_PANEL_PALETTE.info], [' scroll', DEFAULT_PANEL_PALETTE.dim], [' PgUp/PgDn', DEFAULT_PANEL_PALETTE.info], [' page', DEFAULT_PANEL_PALETTE.dim], [' Home/End', DEFAULT_PANEL_PALETTE.info], [' bounds', DEFAULT_PANEL_PALETTE.dim]]),
353
+ buildKeyboardHints(width, [
354
+ { keys: '↑/↓', label: 'scroll' },
355
+ { keys: 'PgUp/PgDn', label: 'page' },
356
+ { keys: 'Home/End', label: 'top/bottom' },
357
+ ], DEFAULT_PANEL_PALETTE),
277
358
  ];
278
- const fullCode = this.fileLines.join('\n');
279
- const hlLines = this.fenceTag
280
- ? this.syntaxHighlighter.highlight(fullCode, this.fenceTag)
281
- : null;
282
-
283
- const lineNumW = String(this.fileLines.length).length;
284
- const contentX = lineNumW + 2; // "NNN | "
285
- const previewLines: Line[] = [];
286
- for (let fileIdx = 0; fileIdx < this.fileLines.length; fileIdx++) {
287
-
288
- const rawLine = this.fileLines[fileIdx];
289
- const tokens: SyntaxToken[] =
290
- hlLines && fileIdx < hlLines.length && hlLines[fileIdx].length > 0
291
- ? (hlLines[fileIdx] as SyntaxToken[])
292
- : [{ text: rawLine, fg: '' }];
293
-
294
- previewLines.push(this.renderCodeLine(fileIdx, lineNumW, contentX, tokens, width));
359
+ // Line-build cache keyed on (filePath, contentVersion, width, highlightedLine):
360
+ // rebuilding every line on every frame showed up as an O(file-length)
361
+ // cost per render even when nothing changed (e.g. while idly scrolling
362
+ // within the same file). Only re-tokenize/re-render when the underlying
363
+ // content, panel width, or highlighted row actually changes.
364
+ const cacheKey = `${this.filePath ?? ''}${this.contentVersion}${width}${this.highlightedLine ?? ''}`;
365
+ let previewLines: Line[];
366
+ if (this.cachedPreviewLines && this.cachedPreviewLines.key === cacheKey) {
367
+ previewLines = this.cachedPreviewLines.lines;
368
+ } else {
369
+ const fullCode = this.fileLines.join('\n');
370
+ const hlLines = this.fenceTag
371
+ ? this.syntaxHighlighter.highlight(fullCode, this.fenceTag)
372
+ : null;
373
+
374
+ const lineNumW = String(this.fileLines.length).length;
375
+ const contentX = lineNumW + 2; // "NNN | "
376
+ const built: Line[] = [];
377
+ for (let fileIdx = 0; fileIdx < this.fileLines.length; fileIdx++) {
378
+
379
+ const rawLine = this.fileLines[fileIdx];
380
+ const tokens: SyntaxToken[] =
381
+ hlLines && fileIdx < hlLines.length && hlLines[fileIdx].length > 0
382
+ ? (hlLines[fileIdx] as SyntaxToken[])
383
+ : [{ text: rawLine, fg: '' }];
384
+
385
+ const highlighted = this.highlightedLine !== null && fileIdx === this.highlightedLine - 1;
386
+ built.push(this.renderCodeLine(fileIdx, lineNumW, contentX, tokens, width, highlighted));
387
+ }
388
+ previewLines = built;
389
+ this.cachedPreviewLines = { key: cacheKey, lines: previewLines };
295
390
  }
391
+ // Once a goToLine target is set, track it with a selectedIndex so the
392
+ // scrollable-section window keeps that row visible (with guard rows)
393
+ // the same way list-cursor panels do; plain free-scroll (no active
394
+ // highlight) keeps using the scrollOffset-only window it always has.
296
395
  const previewSection = resolveScrollablePanelSection(width, height, {
297
396
  intro,
298
397
  footerLines,
@@ -302,6 +401,7 @@ export class FilePreviewPanel extends BasePanel {
302
401
  title: 'Preview',
303
402
  scrollableLines: previewLines,
304
403
  scrollOffset: this.scrollOffset,
404
+ selectedIndex: this.highlightedLine !== null ? this.highlightedLine - 1 : undefined,
305
405
  minRows: 8,
306
406
  },
307
407
  });
@@ -319,8 +419,10 @@ export class FilePreviewPanel extends BasePanel {
319
419
  buildPanelLine(width, [
320
420
  [' Lines ', DEFAULT_PANEL_PALETTE.label],
321
421
  [String(this.fileLines.length), DEFAULT_PANEL_PALETTE.value],
322
- [' Scroll ', DEFAULT_PANEL_PALETTE.label],
422
+ [' Viewing ', DEFAULT_PANEL_PALETTE.label],
323
423
  [`${window.start + 1}-${window.end}`, DEFAULT_PANEL_PALETTE.info],
424
+ [' Lang ', DEFAULT_PANEL_PALETTE.label],
425
+ [this.fenceTag || 'text', this.fenceTag ? DEFAULT_PANEL_PALETTE.value : DEFAULT_PANEL_PALETTE.dim],
324
426
  ]),
325
427
  ],
326
428
  },
@@ -340,9 +442,14 @@ export class FilePreviewPanel extends BasePanel {
340
442
  contentX: number,
341
443
  tokens: SyntaxToken[],
342
444
  width: number,
445
+ highlighted = false,
343
446
  ): Line {
447
+ // The goToLine target row (e.g. a symbol-outline jump) renders on the
448
+ // theme's selection background instead of the plain code background so
449
+ // it's visibly distinguishable from the rest of the file.
450
+ const bg = highlighted ? DEFAULT_PANEL_PALETTE.selectBg : BG;
344
451
  const line: Cell[] = new Array(width).fill(null).map(() =>
345
- createStyledCell(' ', { bg: BG }),
452
+ createStyledCell(' ', { bg }),
346
453
  );
347
454
 
348
455
  // Line number gutter
@@ -350,12 +457,12 @@ export class FilePreviewPanel extends BasePanel {
350
457
  let cx = 0;
351
458
  for (const ch of lineNum) {
352
459
  if (cx >= lineNumW) break;
353
- line[cx++] = createStyledCell(ch, { fg: LINE_NUM_FG, bg: BG, dim: true });
460
+ line[cx++] = createStyledCell(ch, { fg: LINE_NUM_FG, bg, dim: true });
354
461
  }
355
462
  // Separator " | "
356
- line[cx++] = createStyledCell(' ', { bg: BG });
357
- line[cx++] = createStyledCell('│', { fg: LINE_NUM_FG, bg: BG, dim: true });
358
- line[cx++] = createStyledCell(' ', { bg: BG });
463
+ line[cx++] = createStyledCell(' ', { bg });
464
+ line[cx++] = createStyledCell('│', { fg: LINE_NUM_FG, bg, dim: true });
465
+ line[cx++] = createStyledCell(' ', { bg });
359
466
 
360
467
  // Syntax tokens
361
468
  for (const token of tokens) {
@@ -366,7 +473,7 @@ export class FilePreviewPanel extends BasePanel {
366
473
  const cw = getDisplayWidth(ch);
367
474
  line[cx] = createStyledCell(ch, {
368
475
  fg: token.fg || '',
369
- bg: BG,
476
+ bg,
370
477
  bold: token.bold,
371
478
  italic: token.italic,
372
479
  });
@@ -378,31 +485,6 @@ export class FilePreviewPanel extends BasePanel {
378
485
  return line;
379
486
  }
380
487
 
381
- private renderEmpty(width: number, height: number, message: string): Line[] {
382
- const lines: Line[] = [];
383
- const msgLine = createEmptyLine(width);
384
- const isWarning = message.startsWith('File too large');
385
- const fg = isWarning ? WARNING_FG : EMPTY_FG;
386
- let cx = 2;
387
- for (const ch of message) {
388
- if (cx >= width - 1) break;
389
- msgLine[cx++] = createStyledCell(ch, { fg, bg: BG });
390
- }
391
- lines.push(msgLine);
392
- for (let i = 1; i < height; i++) {
393
- lines.push(this.renderBgLine(width));
394
- }
395
- return lines;
396
- }
397
-
398
- private renderBgLine(width: number): Line {
399
- const line = createEmptyLine(width);
400
- for (let x = 0; x < width; x++) {
401
- line[x] = createStyledCell(' ', { bg: BG });
402
- }
403
- return line;
404
- }
405
-
406
488
  private scroll(delta: number): boolean {
407
489
  const before = this.scrollOffset;
408
490
  this.scrollOffset = Math.max(0, this.scrollOffset + delta);