startall 0.0.23 → 0.0.25
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/index.js +690 -342
- package/package.json +4 -4
package/index.js
CHANGED
|
@@ -1,11 +1,183 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import { createCliRenderer, TextRenderable, BoxRenderable, ScrollBoxRenderable, ASCIIFontRenderable, t, fg, bold, dim, RGBA } from '@opentui/core';
|
|
3
|
+
import { createCliRenderer, TextRenderable, BoxRenderable, ScrollBoxRenderable, ASCIIFontRenderable, TextareaRenderable, SyntaxStyle, parseColor, t, fg, bg, bold, dim, RGBA, StyledText, OptimizedBuffer } from '@opentui/core';
|
|
4
4
|
import { spawn, execSync, spawnSync } from 'child_process';
|
|
5
5
|
import { readFileSync, writeFileSync, writeSync, existsSync } from 'fs';
|
|
6
6
|
import { join } from 'path';
|
|
7
7
|
import kill from 'tree-kill';
|
|
8
8
|
import stripAnsi from 'strip-ansi';
|
|
9
|
+
import { Database } from 'bun:sqlite';
|
|
10
|
+
|
|
11
|
+
// SQLite-backed output line storage
|
|
12
|
+
class OutputDatabase {
|
|
13
|
+
constructor(maxLines = 1000) {
|
|
14
|
+
this.db = new Database(':memory:');
|
|
15
|
+
this.maxLines = maxLines;
|
|
16
|
+
|
|
17
|
+
// Enable WAL mode for better concurrent read/write performance
|
|
18
|
+
this.db.exec('PRAGMA journal_mode = WAL');
|
|
19
|
+
|
|
20
|
+
// Create the output_lines table with colors bitmask for efficient filtering
|
|
21
|
+
this.db.exec(`
|
|
22
|
+
CREATE TABLE output_lines (
|
|
23
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
24
|
+
line_number INTEGER NOT NULL,
|
|
25
|
+
process TEXT NOT NULL,
|
|
26
|
+
process_lower TEXT NOT NULL,
|
|
27
|
+
text TEXT NOT NULL,
|
|
28
|
+
text_lower TEXT NOT NULL,
|
|
29
|
+
timestamp INTEGER NOT NULL,
|
|
30
|
+
colors INTEGER NOT NULL DEFAULT 0
|
|
31
|
+
)
|
|
32
|
+
`);
|
|
33
|
+
this.db.exec('CREATE INDEX idx_line_number ON output_lines(line_number)');
|
|
34
|
+
this.db.exec('CREATE INDEX idx_process ON output_lines(process)');
|
|
35
|
+
this.db.exec('CREATE INDEX idx_colors ON output_lines(colors)');
|
|
36
|
+
|
|
37
|
+
// Column list with aliases to match existing JS property names
|
|
38
|
+
this._columns = 'id, line_number AS lineNumber, process, process_lower AS processLower, text, text_lower AS textLower, timestamp, colors';
|
|
39
|
+
|
|
40
|
+
// Prepare reusable statements for performance
|
|
41
|
+
this._insertStmt = this.db.prepare(
|
|
42
|
+
'INSERT INTO output_lines (line_number, process, process_lower, text, text_lower, timestamp, colors) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
|
43
|
+
);
|
|
44
|
+
this._countStmt = this.db.prepare('SELECT COUNT(*) AS cnt FROM output_lines');
|
|
45
|
+
this._deleteOldStmt = this.db.prepare(
|
|
46
|
+
'DELETE FROM output_lines WHERE id NOT IN (SELECT id FROM output_lines ORDER BY id DESC LIMIT ?)'
|
|
47
|
+
);
|
|
48
|
+
this._selectAllStmt = this.db.prepare(`SELECT ${this._columns} FROM output_lines ORDER BY id ASC`);
|
|
49
|
+
this._clearStmt = this.db.prepare('DELETE FROM output_lines');
|
|
50
|
+
// Fast path for queryVisible with no filters
|
|
51
|
+
this._queryVisibleNoFilterStmt = this.db.prepare(`SELECT ${this._columns} FROM (
|
|
52
|
+
SELECT * FROM output_lines ORDER BY id DESC LIMIT ?
|
|
53
|
+
) ORDER BY id ASC`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
insert(lineNumber, process, text, timestamp, colorBitmask) {
|
|
57
|
+
this._insertStmt.run(lineNumber, process, process.toLowerCase(), text, text.toLowerCase(), timestamp, colorBitmask);
|
|
58
|
+
|
|
59
|
+
// Enforce max lines limit
|
|
60
|
+
const { cnt } = this._countStmt.get();
|
|
61
|
+
if (cnt > this.maxLines) {
|
|
62
|
+
this._deleteOldStmt.run(this.maxLines);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getAll() {
|
|
67
|
+
return this._selectAllStmt.all();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
count() {
|
|
71
|
+
return this._countStmt.get().cnt;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
clear() {
|
|
75
|
+
this._clearStmt.run();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Build SQL conditions and params for pane filters
|
|
79
|
+
_buildPaneFilters(pane) {
|
|
80
|
+
const conditions = [];
|
|
81
|
+
const params = [];
|
|
82
|
+
|
|
83
|
+
if (pane.processes && pane.processes.length > 0) {
|
|
84
|
+
const placeholders = pane.processes.map(() => '?').join(', ');
|
|
85
|
+
conditions.push(`process IN (${placeholders})`);
|
|
86
|
+
params.push(...pane.processes);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (pane.hidden && pane.hidden.length > 0) {
|
|
90
|
+
const placeholders = pane.hidden.map(() => '?').join(', ');
|
|
91
|
+
conditions.push(`process NOT IN (${placeholders})`);
|
|
92
|
+
params.push(...pane.hidden);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (pane.filter) {
|
|
96
|
+
const filterLower = pane.filter.toLowerCase();
|
|
97
|
+
conditions.push('(process_lower LIKE ? OR text_lower LIKE ?)');
|
|
98
|
+
params.push(`%${filterLower}%`, `%${filterLower}%`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (pane.colorFilter) {
|
|
102
|
+
// Use bitmask check: (colors & bit) != 0
|
|
103
|
+
const colorBit = this._getColorBit(pane.colorFilter);
|
|
104
|
+
if (colorBit > 0) {
|
|
105
|
+
conditions.push('(colors & ?) != 0');
|
|
106
|
+
params.push(colorBit);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { conditions, params };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Get color bitmask value for a color name
|
|
114
|
+
_getColorBit(colorName) {
|
|
115
|
+
const bits = { red: 1, yellow: 2, green: 4, blue: 8, cyan: 16, magenta: 32 };
|
|
116
|
+
return bits[colorName] || 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Query with pane filters applied via SQL
|
|
120
|
+
queryForPane(pane) {
|
|
121
|
+
const { conditions, params } = this._buildPaneFilters(pane);
|
|
122
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
123
|
+
const sql = `SELECT ${this._columns} FROM output_lines ${where} ORDER BY id ASC`;
|
|
124
|
+
return this.db.prepare(sql).all(...params);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Query lines newer than a given line_number, with pane filters
|
|
128
|
+
queryNewLines(afterLineNumber, pane, limit) {
|
|
129
|
+
const { conditions, params } = this._buildPaneFilters(pane);
|
|
130
|
+
conditions.unshift('line_number > ?');
|
|
131
|
+
params.unshift(afterLineNumber);
|
|
132
|
+
|
|
133
|
+
const where = `WHERE ${conditions.join(' AND ')}`;
|
|
134
|
+
// Get newest lines up to limit, but return them in ascending order
|
|
135
|
+
const sql = `SELECT ${this._columns} FROM (SELECT * FROM output_lines ${where} ORDER BY id DESC LIMIT ?) ORDER BY id ASC`;
|
|
136
|
+
params.push(limit);
|
|
137
|
+
|
|
138
|
+
return this.db.prepare(sql).all(...params);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Count lines matching pane filters (for calculating max scroll)
|
|
142
|
+
countForPane(pane) {
|
|
143
|
+
const { conditions, params } = this._buildPaneFilters(pane);
|
|
144
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
145
|
+
const sql = `SELECT COUNT(*) AS cnt FROM output_lines ${where}`;
|
|
146
|
+
return this.db.prepare(sql).get(...params).cnt;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Query visible lines for virtual scrolling: get `limit` lines starting from `offset` from the end
|
|
150
|
+
// offset=0 means the most recent lines, offset=10 means 10 lines back from the end
|
|
151
|
+
queryVisible(pane, limit, offsetFromEnd) {
|
|
152
|
+
const { conditions, params } = this._buildPaneFilters(pane);
|
|
153
|
+
const totalToFetch = limit + offsetFromEnd;
|
|
154
|
+
|
|
155
|
+
let rows;
|
|
156
|
+
if (conditions.length === 0) {
|
|
157
|
+
// Fast path: no filters, use prepared statement
|
|
158
|
+
rows = this._queryVisibleNoFilterStmt.all(totalToFetch);
|
|
159
|
+
} else {
|
|
160
|
+
// Slow path: build query with filters
|
|
161
|
+
const where = `WHERE ${conditions.join(' AND ')}`;
|
|
162
|
+
const sql = `SELECT ${this._columns} FROM (
|
|
163
|
+
SELECT * FROM output_lines ${where} ORDER BY id DESC LIMIT ?
|
|
164
|
+
) ORDER BY id ASC`;
|
|
165
|
+
params.push(totalToFetch);
|
|
166
|
+
rows = this.db.prepare(sql).all(...params);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// If offset > 0, trim the newest lines
|
|
170
|
+
if (offsetFromEnd > 0 && rows.length > limit) {
|
|
171
|
+
rows = rows.slice(0, limit);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return rows;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
close() {
|
|
178
|
+
this.db.close();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
9
181
|
|
|
10
182
|
// Configuration
|
|
11
183
|
const CONFIG_FILE = process.argv[2] || 'startall.json';
|
|
@@ -93,34 +265,65 @@ function getAllPaneIds(node, ids = []) {
|
|
|
93
265
|
return ids;
|
|
94
266
|
}
|
|
95
267
|
|
|
268
|
+
// Color bitmask values for efficient SQL filtering
|
|
269
|
+
const COLOR_BITS = {
|
|
270
|
+
red: 1, // 0b000001
|
|
271
|
+
yellow: 2, // 0b000010
|
|
272
|
+
green: 4, // 0b000100
|
|
273
|
+
blue: 8, // 0b001000
|
|
274
|
+
cyan: 16, // 0b010000
|
|
275
|
+
magenta: 32, // 0b100000
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// ANSI color codes (normal and bright variants)
|
|
279
|
+
const ANSI_COLOR_CODES = {
|
|
280
|
+
red: [31, 91],
|
|
281
|
+
yellow: [33, 93],
|
|
282
|
+
green: [32, 92],
|
|
283
|
+
blue: [34, 94],
|
|
284
|
+
cyan: [36, 96],
|
|
285
|
+
magenta: [35, 95],
|
|
286
|
+
};
|
|
287
|
+
|
|
96
288
|
// Get ANSI color codes for a color name (includes normal and bright variants)
|
|
97
289
|
function getAnsiColorCodes(colorName) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
290
|
+
return ANSI_COLOR_CODES[colorName] || [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Check if text contains a specific ANSI color code
|
|
294
|
+
function textHasColorCode(text, code) {
|
|
295
|
+
// Check for direct color code: \x1b[31m
|
|
296
|
+
if (text.includes(`\x1b[${code}m`)) return true;
|
|
297
|
+
// Check for color with modifiers: \x1b[1;31m, \x1b[0;31m, etc.
|
|
298
|
+
if (text.includes(`;${code}m`)) return true;
|
|
299
|
+
// Check for color at start of sequence: \x1b[31;1m
|
|
300
|
+
if (text.includes(`\x1b[${code};`)) return true;
|
|
301
|
+
return false;
|
|
107
302
|
}
|
|
108
303
|
|
|
109
304
|
// Check if a line contains a specific ANSI color
|
|
110
305
|
function lineHasColor(text, colorName) {
|
|
111
306
|
const codes = getAnsiColorCodes(colorName);
|
|
112
|
-
// Match ANSI escape sequences like \x1b[31m, \x1b[91m, \x1b[1;31m, etc.
|
|
113
307
|
for (const code of codes) {
|
|
114
|
-
|
|
115
|
-
if (text.includes(`\x1b[${code}m`)) return true;
|
|
116
|
-
// Check for color with modifiers: \x1b[1;31m, \x1b[0;31m, etc.
|
|
117
|
-
if (text.includes(`;${code}m`)) return true;
|
|
118
|
-
// Check for color at start of sequence: \x1b[31;1m
|
|
119
|
-
if (text.includes(`\x1b[${code};`)) return true;
|
|
308
|
+
if (textHasColorCode(text, code)) return true;
|
|
120
309
|
}
|
|
121
310
|
return false;
|
|
122
311
|
}
|
|
123
312
|
|
|
313
|
+
// Detect all colors in text and return bitmask
|
|
314
|
+
function detectColorBitmask(text) {
|
|
315
|
+
let bitmask = 0;
|
|
316
|
+
for (const [colorName, codes] of Object.entries(ANSI_COLOR_CODES)) {
|
|
317
|
+
for (const code of codes) {
|
|
318
|
+
if (textHasColorCode(text, code)) {
|
|
319
|
+
bitmask |= COLOR_BITS[colorName];
|
|
320
|
+
break; // Found this color, no need to check other codes for it
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return bitmask;
|
|
325
|
+
}
|
|
326
|
+
|
|
124
327
|
// Find parent of a node
|
|
125
328
|
function findParent(root, targetId, parent = null) {
|
|
126
329
|
if (!root) return null;
|
|
@@ -429,6 +632,15 @@ function gitPush() {
|
|
|
429
632
|
}
|
|
430
633
|
}
|
|
431
634
|
|
|
635
|
+
function gitPull() {
|
|
636
|
+
try {
|
|
637
|
+
const output = execSync('git pull', { stdio: 'pipe', windowsHide: true, timeout: 30000 }).toString().trim();
|
|
638
|
+
return { success: true, output: output || 'Pulled successfully' };
|
|
639
|
+
} catch (err) {
|
|
640
|
+
return { success: false, error: err.stderr?.toString() || err.message };
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
432
644
|
// Detect git repo once at startup
|
|
433
645
|
const IS_GIT_REPO = isGitRepo();
|
|
434
646
|
|
|
@@ -541,10 +753,10 @@ class ProcessManager {
|
|
|
541
753
|
this.selectedIndex = 0;
|
|
542
754
|
this.processes = new Map();
|
|
543
755
|
this.processRefs = new Map();
|
|
544
|
-
this.
|
|
756
|
+
this.maxOutputLines = 1000; // Lines kept in database
|
|
757
|
+
this.outputDb = new OutputDatabase(this.maxOutputLines);
|
|
545
758
|
this.totalLinesReceived = 0; // Track total lines ever received (never resets)
|
|
546
759
|
this.filter = '';
|
|
547
|
-
this.maxOutputLines = 1000; // Lines kept in memory
|
|
548
760
|
this.maxDomLines = 150; // Lines kept in DOM (buffer for varying heights)
|
|
549
761
|
this.lineRenderables = new Map(); // Reusable TextRenderables per pane
|
|
550
762
|
this.maxVisibleLines = null; // Calculated dynamically based on screen height
|
|
@@ -581,11 +793,12 @@ class ProcessManager {
|
|
|
581
793
|
this.outputBox = null; // Reference to the output container
|
|
582
794
|
this.destroyed = false; // Flag to prevent operations after cleanup
|
|
583
795
|
this.lastRenderedLineCount = 0; // Track how many lines we've rendered
|
|
796
|
+
this.hasNewLines = false; // Flag set when new lines are added, cleared after render
|
|
584
797
|
this.headerRenderable = null; // Reference to header text in running UI
|
|
585
798
|
this.processListRenderable = null; // Reference to process list text in running UI
|
|
586
799
|
this.renderScheduled = false; // Throttle renders for CPU efficiency
|
|
587
800
|
this.lastRenderTime = 0; // Timestamp of last render
|
|
588
|
-
this.minRenderInterval =
|
|
801
|
+
this.minRenderInterval = 1; // Minimum ms between renders (~60fps cap)
|
|
589
802
|
|
|
590
803
|
// Performance metrics
|
|
591
804
|
this.showPerformanceMetrics = this.config.showPerformanceMetrics || false;
|
|
@@ -600,16 +813,28 @@ class ProcessManager {
|
|
|
600
813
|
this.splitMode = false; // Whether waiting for split command after Ctrl+b
|
|
601
814
|
this.showSplitMenu = false; // Whether to show the command palette
|
|
602
815
|
this.splitMenuIndex = 0; // Selected item in split menu
|
|
603
|
-
this.paneScrollPositions = new Map(); // Store scroll positions per pane ID
|
|
604
|
-
this.paneScrollBoxes = new Map(); // Store ScrollBox references per pane ID
|
|
816
|
+
this.paneScrollPositions = new Map(); // Store scroll positions per pane ID (legacy)
|
|
817
|
+
this.paneScrollBoxes = new Map(); // Store ScrollBox references per pane ID (legacy)
|
|
605
818
|
this.paneFilterState = new Map(); // Track filter state per pane to detect changes
|
|
606
819
|
this.paneLineCount = new Map(); // Track how many lines we've rendered per pane
|
|
607
820
|
this.uiJustRebuilt = false; // Flag to skip redundant render after buildRunningUI
|
|
608
821
|
|
|
822
|
+
// Virtual scrolling state
|
|
823
|
+
this.paneScrollOffsets = new Map(); // Scroll offset from end per pane (0 = at bottom)
|
|
824
|
+
this.paneVisibleHeight = new Map(); // Visible height per pane for scroll calculations
|
|
825
|
+
this.paneOutputBoxes = new Map(); // Store output BoxRenderable references per pane
|
|
826
|
+
|
|
609
827
|
// Column view state (one pane per script, side by side)
|
|
610
828
|
this.isColumnView = false; // Whether column view is active
|
|
611
829
|
this.savedPaneRoot = null; // Saved pane tree to restore when toggling back
|
|
612
830
|
|
|
831
|
+
// Line format cache (keyed by line ID + settings hash)
|
|
832
|
+
this.lineFormatCache = new Map();
|
|
833
|
+
this.lineFormatCacheKey = ''; // Cache key based on display settings
|
|
834
|
+
|
|
835
|
+
// Pane content cache (keyed by pane ID -> {lastLineId, styledText})
|
|
836
|
+
this.paneContentCache = new Map();
|
|
837
|
+
|
|
613
838
|
// Copy mode state (select text to copy)
|
|
614
839
|
this.isCopyMode = false; // Whether in copy/select mode
|
|
615
840
|
this.copyModeCursor = 0; // Current cursor line index within visible lines
|
|
@@ -664,13 +889,27 @@ class ProcessManager {
|
|
|
664
889
|
return;
|
|
665
890
|
}
|
|
666
891
|
|
|
667
|
-
// Handle Ctrl+L - clear screen
|
|
892
|
+
// Handle Ctrl+L - clear screen and redraw (kills artifacts)
|
|
668
893
|
if (key.ctrl && key.name === 'l') {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
this.
|
|
672
|
-
this.buildRunningUI();
|
|
894
|
+
// Debounce - cancel any pending wipe and restart
|
|
895
|
+
if (this.wipeTimeout) {
|
|
896
|
+
clearTimeout(this.wipeTimeout);
|
|
673
897
|
}
|
|
898
|
+
|
|
899
|
+
// Save current phase and show wipe screen
|
|
900
|
+
const savedPhase = this.phase;
|
|
901
|
+
this.buildWipeScreen();
|
|
902
|
+
|
|
903
|
+
this.wipeTimeout = setTimeout(() => {
|
|
904
|
+
this.wipeTimeout = null;
|
|
905
|
+
if (savedPhase === 'running') {
|
|
906
|
+
this.buildRunningUI();
|
|
907
|
+
} else if (savedPhase === 'selection') {
|
|
908
|
+
this.buildSelectionUI();
|
|
909
|
+
} else if (savedPhase === 'settings') {
|
|
910
|
+
this.buildSettingsUI();
|
|
911
|
+
}
|
|
912
|
+
}, 50);
|
|
674
913
|
return;
|
|
675
914
|
}
|
|
676
915
|
|
|
@@ -865,13 +1104,13 @@ class ProcessManager {
|
|
|
865
1104
|
this.buildRunningUI();
|
|
866
1105
|
} else if (keyName === 'p') {
|
|
867
1106
|
// Toggle pause output scrolling globally
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1107
|
+
this.isPaused = !this.isPaused;
|
|
1108
|
+
// Reset scroll offset to bottom when unpausing
|
|
1109
|
+
if (!this.isPaused) {
|
|
1110
|
+
for (const paneId of getAllPaneIds(this.paneRoot)) {
|
|
1111
|
+
this.paneScrollOffsets.set(paneId, 0);
|
|
872
1112
|
}
|
|
873
1113
|
}
|
|
874
|
-
this.isPaused = !this.isPaused;
|
|
875
1114
|
this.updateStreamPauseState();
|
|
876
1115
|
this.buildRunningUI();
|
|
877
1116
|
} else if (keyName === 'f') {
|
|
@@ -1094,13 +1333,16 @@ class ProcessManager {
|
|
|
1094
1333
|
} else {
|
|
1095
1334
|
this.paneRoot = createPane([]); // Empty array means show all processes
|
|
1096
1335
|
}
|
|
1097
|
-
|
|
1336
|
+
// Focus the first actual pane (paneRoot might be a split node)
|
|
1337
|
+
const allPanes = getAllPaneIds(this.paneRoot);
|
|
1338
|
+
this.focusedPaneId = allPanes.length > 0 ? allPanes[0] : this.paneRoot.id;
|
|
1098
1339
|
|
|
1099
1340
|
selected.forEach(scriptName => {
|
|
1100
1341
|
this.startProcess(scriptName);
|
|
1101
1342
|
});
|
|
1102
1343
|
|
|
1103
|
-
|
|
1344
|
+
// Build the UI immediately (don't rely on render() since hasNewLines may be false)
|
|
1345
|
+
this.buildRunningUI();
|
|
1104
1346
|
}
|
|
1105
1347
|
|
|
1106
1348
|
startProcess(scriptName) {
|
|
@@ -1147,22 +1389,25 @@ class ProcessManager {
|
|
|
1147
1389
|
}
|
|
1148
1390
|
|
|
1149
1391
|
addOutputLine(processName, text) {
|
|
1150
|
-
//
|
|
1151
|
-
|
|
1152
|
-
this.outputLines.push({
|
|
1153
|
-
process: processName,
|
|
1154
|
-
processLower: processName.toLowerCase(),
|
|
1155
|
-
text,
|
|
1156
|
-
textLower: text.toLowerCase(),
|
|
1157
|
-
timestamp: Date.now(),
|
|
1158
|
-
lineNumber: ++this.totalLinesReceived, // Track absolute line number
|
|
1159
|
-
});
|
|
1392
|
+
// Don't write if database is closed
|
|
1393
|
+
if (this.destroyed) return;
|
|
1160
1394
|
|
|
1161
|
-
//
|
|
1162
|
-
|
|
1163
|
-
this.outputLines.shift();
|
|
1164
|
-
}
|
|
1395
|
+
// Detect colors in the text for efficient SQL filtering
|
|
1396
|
+
const colorBitmask = detectColorBitmask(text);
|
|
1165
1397
|
|
|
1398
|
+
// Always store the output line, even when paused
|
|
1399
|
+
// Insert into SQLite database
|
|
1400
|
+
this.outputDb.insert(
|
|
1401
|
+
++this.totalLinesReceived,
|
|
1402
|
+
processName,
|
|
1403
|
+
text,
|
|
1404
|
+
Date.now(),
|
|
1405
|
+
colorBitmask
|
|
1406
|
+
);
|
|
1407
|
+
|
|
1408
|
+
// Mark that new lines are available
|
|
1409
|
+
this.hasNewLines = true;
|
|
1410
|
+
|
|
1166
1411
|
// Only render if not paused - this prevents new output from appearing
|
|
1167
1412
|
// when the user is reviewing history
|
|
1168
1413
|
if (!this.isPaused) {
|
|
@@ -1811,19 +2056,38 @@ class ProcessManager {
|
|
|
1811
2056
|
// Toggle visibility of selected process in focused pane
|
|
1812
2057
|
toggleProcessVisibility() {
|
|
1813
2058
|
const scriptName = this.scripts[this.selectedIndex]?.name;
|
|
1814
|
-
if (!scriptName || !this.focusedPaneId)
|
|
2059
|
+
if (!scriptName || !this.focusedPaneId) {
|
|
2060
|
+
return;
|
|
2061
|
+
}
|
|
1815
2062
|
|
|
1816
2063
|
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
1817
|
-
if (!pane)
|
|
2064
|
+
if (!pane) {
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
1818
2067
|
|
|
1819
|
-
// Initialize
|
|
2068
|
+
// Initialize arrays if needed
|
|
1820
2069
|
if (!pane.hidden) pane.hidden = [];
|
|
2070
|
+
if (!pane.processes) pane.processes = [];
|
|
1821
2071
|
|
|
1822
|
-
//
|
|
1823
|
-
|
|
1824
|
-
|
|
2072
|
+
// Check current visibility
|
|
2073
|
+
const isCurrentlyVisible = this.isProcessVisibleInPane(scriptName, pane);
|
|
2074
|
+
|
|
2075
|
+
if (isCurrentlyVisible) {
|
|
2076
|
+
// Hide it - add to hidden list
|
|
2077
|
+
if (!pane.hidden.includes(scriptName)) {
|
|
2078
|
+
pane.hidden.push(scriptName);
|
|
2079
|
+
}
|
|
2080
|
+
// Also remove from processes if it's there
|
|
2081
|
+
if (pane.processes.includes(scriptName)) {
|
|
2082
|
+
pane.processes = pane.processes.filter(p => p !== scriptName);
|
|
2083
|
+
}
|
|
1825
2084
|
} else {
|
|
1826
|
-
|
|
2085
|
+
// Show it - remove from hidden list
|
|
2086
|
+
pane.hidden = pane.hidden.filter(p => p !== scriptName);
|
|
2087
|
+
// If pane has a process filter, add this process to it
|
|
2088
|
+
if (pane.processes.length > 0 && !pane.processes.includes(scriptName)) {
|
|
2089
|
+
pane.processes.push(scriptName);
|
|
2090
|
+
}
|
|
1827
2091
|
}
|
|
1828
2092
|
|
|
1829
2093
|
this.savePaneLayout();
|
|
@@ -1837,38 +2101,46 @@ class ProcessManager {
|
|
|
1837
2101
|
debouncedSaveConfig(this.config);
|
|
1838
2102
|
}
|
|
1839
2103
|
|
|
1840
|
-
// Scroll the focused pane
|
|
2104
|
+
// Scroll the focused pane (virtual scrolling - adjusts offset into database)
|
|
1841
2105
|
scrollFocusedPane(direction) {
|
|
1842
2106
|
if (!this.focusedPaneId) return;
|
|
1843
2107
|
|
|
1844
|
-
const
|
|
1845
|
-
if (!
|
|
2108
|
+
const pane = findPaneById(this.paneRoot, this.focusedPaneId);
|
|
2109
|
+
if (!pane) return;
|
|
1846
2110
|
|
|
1847
|
-
const
|
|
1848
|
-
const
|
|
1849
|
-
const
|
|
2111
|
+
const totalLines = this.outputDb.countForPane(pane);
|
|
2112
|
+
const visibleHeight = this.paneVisibleHeight.get(this.focusedPaneId) || 20;
|
|
2113
|
+
const currentOffset = this.paneScrollOffsets.get(this.focusedPaneId) || 0;
|
|
2114
|
+
const maxOffset = Math.max(0, totalLines - visibleHeight);
|
|
1850
2115
|
|
|
1851
|
-
let
|
|
2116
|
+
let newOffset = currentOffset;
|
|
1852
2117
|
|
|
1853
2118
|
if (direction === 'home') {
|
|
1854
|
-
|
|
2119
|
+
// Scroll to top (maximum offset from end)
|
|
2120
|
+
newOffset = maxOffset;
|
|
1855
2121
|
} else if (direction === 'end') {
|
|
1856
|
-
|
|
2122
|
+
// Scroll to bottom (offset 0 = most recent)
|
|
2123
|
+
newOffset = 0;
|
|
1857
2124
|
} else if (direction === 'pageup') {
|
|
1858
|
-
|
|
2125
|
+
newOffset = Math.min(maxOffset, currentOffset + visibleHeight);
|
|
1859
2126
|
} else if (direction === 'pagedown') {
|
|
1860
|
-
|
|
2127
|
+
newOffset = Math.max(0, currentOffset - visibleHeight);
|
|
2128
|
+
} else if (direction === 'up') {
|
|
2129
|
+
newOffset = Math.min(maxOffset, currentOffset + 1);
|
|
2130
|
+
} else if (direction === 'down') {
|
|
2131
|
+
newOffset = Math.max(0, currentOffset - 1);
|
|
1861
2132
|
}
|
|
1862
2133
|
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
2134
|
+
// Only update if changed
|
|
2135
|
+
if (newOffset !== currentOffset) {
|
|
2136
|
+
this.paneScrollOffsets.set(this.focusedPaneId, newOffset);
|
|
2137
|
+
|
|
2138
|
+
// Auto-pause when manually scrolling (unless going to end)
|
|
2139
|
+
if (direction !== 'end' && !this.isPaused) {
|
|
2140
|
+
this.isPaused = true;
|
|
2141
|
+
this.updateStreamPauseState();
|
|
2142
|
+
}
|
|
2143
|
+
|
|
1872
2144
|
this.buildRunningUI();
|
|
1873
2145
|
}
|
|
1874
2146
|
}
|
|
@@ -2089,51 +2361,18 @@ class ProcessManager {
|
|
|
2089
2361
|
}
|
|
2090
2362
|
|
|
2091
2363
|
// Count horizontal splits (which reduce available height per pane)
|
|
2092
|
-
// Get output lines for a specific pane -
|
|
2364
|
+
// Get output lines for a specific pane - queries SQLite with filters
|
|
2093
2365
|
getOutputLinesForPane(pane) {
|
|
2094
|
-
|
|
2095
|
-
const hasProcessFilter = pane.processes.length > 0;
|
|
2096
|
-
const hasHiddenFilter = pane.hidden && pane.hidden.length > 0;
|
|
2097
|
-
const hasTextFilter = !!pane.filter;
|
|
2098
|
-
const hasColorFilter = !!pane.colorFilter;
|
|
2099
|
-
|
|
2100
|
-
if (!hasProcessFilter && !hasHiddenFilter && !hasTextFilter && !hasColorFilter) {
|
|
2101
|
-
return this.outputLines;
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
// Build Sets for O(1) lookups
|
|
2105
|
-
const processSet = hasProcessFilter ? new Set(pane.processes) : null;
|
|
2106
|
-
const hiddenSet = hasHiddenFilter ? new Set(pane.hidden) : null;
|
|
2107
|
-
const filterLower = hasTextFilter ? pane.filter.toLowerCase() : null;
|
|
2108
|
-
|
|
2109
|
-
// Single pass through lines
|
|
2110
|
-
return this.outputLines.filter(line => {
|
|
2111
|
-
// Check process filter
|
|
2112
|
-
if (processSet && !processSet.has(line.process)) return false;
|
|
2113
|
-
|
|
2114
|
-
// Check hidden filter
|
|
2115
|
-
if (hiddenSet && hiddenSet.has(line.process)) return false;
|
|
2116
|
-
|
|
2117
|
-
// Check text filter (use cached lowercase from line if available)
|
|
2118
|
-
if (filterLower) {
|
|
2119
|
-
const processLower = line.processLower || line.process.toLowerCase();
|
|
2120
|
-
const textLower = line.textLower || line.text.toLowerCase();
|
|
2121
|
-
if (!processLower.includes(filterLower) && !textLower.includes(filterLower)) {
|
|
2122
|
-
return false;
|
|
2123
|
-
}
|
|
2124
|
-
}
|
|
2125
|
-
|
|
2126
|
-
// Check color filter
|
|
2127
|
-
if (hasColorFilter && !lineHasColor(line.text, pane.colorFilter)) {
|
|
2128
|
-
return false;
|
|
2129
|
-
}
|
|
2130
|
-
|
|
2131
|
-
return true;
|
|
2132
|
-
});
|
|
2366
|
+
return this.outputDb.queryForPane(pane);
|
|
2133
2367
|
}
|
|
2134
2368
|
|
|
2135
2369
|
buildSettingsUI() {
|
|
2136
2370
|
// Remove old containers - use destroyRecursively to clean up all children
|
|
2371
|
+
if (this.wipeContainer) {
|
|
2372
|
+
this.renderer.root.remove(this.wipeContainer);
|
|
2373
|
+
this.wipeContainer.destroyRecursively();
|
|
2374
|
+
this.wipeContainer = null;
|
|
2375
|
+
}
|
|
2137
2376
|
if (this.selectionContainer) {
|
|
2138
2377
|
this.renderer.root.remove(this.selectionContainer);
|
|
2139
2378
|
this.selectionContainer.destroyRecursively();
|
|
@@ -2530,6 +2769,11 @@ class ProcessManager {
|
|
|
2530
2769
|
// Ignore
|
|
2531
2770
|
}
|
|
2532
2771
|
}
|
|
2772
|
+
|
|
2773
|
+
// Close the SQLite database
|
|
2774
|
+
if (this.outputDb) {
|
|
2775
|
+
this.outputDb.close();
|
|
2776
|
+
}
|
|
2533
2777
|
}
|
|
2534
2778
|
|
|
2535
2779
|
executeCommand(scriptName) {
|
|
@@ -2665,6 +2909,22 @@ class ProcessManager {
|
|
|
2665
2909
|
this.refreshGitStatus();
|
|
2666
2910
|
this.buildRunningUI();
|
|
2667
2911
|
}, 10);
|
|
2912
|
+
} else if (keyName === 'l') {
|
|
2913
|
+
// Pull
|
|
2914
|
+
this.gitModalPhase = 'pulling';
|
|
2915
|
+
this.gitModalOutput = ['Pulling...'];
|
|
2916
|
+
this.buildRunningUI();
|
|
2917
|
+
setTimeout(() => {
|
|
2918
|
+
const result = gitPull();
|
|
2919
|
+
if (result.success) {
|
|
2920
|
+
this.gitModalOutput = [result.output];
|
|
2921
|
+
} else {
|
|
2922
|
+
this.gitModalOutput = [`Pull failed: ${result.error}`];
|
|
2923
|
+
}
|
|
2924
|
+
this.gitModalPhase = 'result';
|
|
2925
|
+
this.refreshGitStatus();
|
|
2926
|
+
this.buildRunningUI();
|
|
2927
|
+
}, 10);
|
|
2668
2928
|
} else if (keyName === 'r') {
|
|
2669
2929
|
// Refresh status
|
|
2670
2930
|
this.refreshGitStatus();
|
|
@@ -2743,6 +3003,22 @@ class ProcessManager {
|
|
|
2743
3003
|
this.refreshGitStatus();
|
|
2744
3004
|
this.buildRunningUI();
|
|
2745
3005
|
}, 10);
|
|
3006
|
+
} else if (keyName === 'l') {
|
|
3007
|
+
// Allow pulling from result phase
|
|
3008
|
+
this.gitModalPhase = 'pulling';
|
|
3009
|
+
this.gitModalOutput = ['Pulling...'];
|
|
3010
|
+
this.buildRunningUI();
|
|
3011
|
+
setTimeout(() => {
|
|
3012
|
+
const result = gitPull();
|
|
3013
|
+
if (result.success) {
|
|
3014
|
+
this.gitModalOutput = [result.output];
|
|
3015
|
+
} else {
|
|
3016
|
+
this.gitModalOutput = [`Pull failed: ${result.error}`];
|
|
3017
|
+
}
|
|
3018
|
+
this.gitModalPhase = 'result';
|
|
3019
|
+
this.refreshGitStatus();
|
|
3020
|
+
this.buildRunningUI();
|
|
3021
|
+
}, 10);
|
|
2746
3022
|
} else {
|
|
2747
3023
|
this.gitModalPhase = 'status';
|
|
2748
3024
|
this.refreshGitStatus();
|
|
@@ -2752,8 +3028,56 @@ class ProcessManager {
|
|
|
2752
3028
|
// 'committing' and 'pushing' phases ignore input (busy)
|
|
2753
3029
|
}
|
|
2754
3030
|
|
|
3031
|
+
// Full screen wipe to clear artifacts (Ctrl+L)
|
|
3032
|
+
buildWipeScreen() {
|
|
3033
|
+
// Remove old containers
|
|
3034
|
+
if (this.selectionContainer) {
|
|
3035
|
+
this.renderer.root.remove(this.selectionContainer);
|
|
3036
|
+
this.selectionContainer.destroyRecursively();
|
|
3037
|
+
this.selectionContainer = null;
|
|
3038
|
+
}
|
|
3039
|
+
if (this.settingsContainer) {
|
|
3040
|
+
this.renderer.root.remove(this.settingsContainer);
|
|
3041
|
+
this.settingsContainer.destroyRecursively();
|
|
3042
|
+
this.settingsContainer = null;
|
|
3043
|
+
}
|
|
3044
|
+
if (this.runningContainer) {
|
|
3045
|
+
this.renderer.root.remove(this.runningContainer);
|
|
3046
|
+
this.runningContainer.destroyRecursively();
|
|
3047
|
+
this.runningContainer = null;
|
|
3048
|
+
this.outputBox = null;
|
|
3049
|
+
this.paneScrollBoxes.clear();
|
|
3050
|
+
this.paneOutputBoxes.clear();
|
|
3051
|
+
this.lineRenderables.clear();
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
// Create full-screen wipe with different background color
|
|
3055
|
+
const wipeContainer = new BoxRenderable(this.renderer, {
|
|
3056
|
+
id: 'wipe-container',
|
|
3057
|
+
width: '100%',
|
|
3058
|
+
height: '100%',
|
|
3059
|
+
backgroundColor: COLORS.bg,
|
|
3060
|
+
justifyContent: 'center',
|
|
3061
|
+
alignItems: 'center',
|
|
3062
|
+
});
|
|
3063
|
+
|
|
3064
|
+
const wipeText = new TextRenderable(this.renderer, {
|
|
3065
|
+
id: 'wipe-text',
|
|
3066
|
+
content: t`${fg(COLORS.textDim)('Clearing...')}`,
|
|
3067
|
+
});
|
|
3068
|
+
wipeContainer.add(wipeText);
|
|
3069
|
+
|
|
3070
|
+
this.renderer.root.add(wipeContainer);
|
|
3071
|
+
this.wipeContainer = wipeContainer;
|
|
3072
|
+
}
|
|
3073
|
+
|
|
2755
3074
|
buildSelectionUI() {
|
|
2756
3075
|
// Remove old containers if they exist - use destroyRecursively to clean up all children
|
|
3076
|
+
if (this.wipeContainer) {
|
|
3077
|
+
this.renderer.root.remove(this.wipeContainer);
|
|
3078
|
+
this.wipeContainer.destroyRecursively();
|
|
3079
|
+
this.wipeContainer = null;
|
|
3080
|
+
}
|
|
2757
3081
|
if (this.selectionContainer) {
|
|
2758
3082
|
this.renderer.root.remove(this.selectionContainer);
|
|
2759
3083
|
this.selectionContainer.destroyRecursively();
|
|
@@ -3039,7 +3363,9 @@ class ProcessManager {
|
|
|
3039
3363
|
|
|
3040
3364
|
getPerformanceString() {
|
|
3041
3365
|
const metrics = this.getPerformanceMetrics();
|
|
3042
|
-
|
|
3366
|
+
const rendererStats = this.renderer.getStats ? this.renderer.getStats() : null;
|
|
3367
|
+
const rendererFps = rendererStats ? rendererStats.fps : '?';
|
|
3368
|
+
return `${metrics.fps}fps ${metrics.avgRenderTime}ms r:${rendererFps}fps db:${this.outputDb.count()}`;
|
|
3043
3369
|
}
|
|
3044
3370
|
|
|
3045
3371
|
getProcessListContent() {
|
|
@@ -3178,109 +3504,79 @@ class ProcessManager {
|
|
|
3178
3504
|
}
|
|
3179
3505
|
}
|
|
3180
3506
|
|
|
3181
|
-
//
|
|
3182
|
-
if (this.
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
for (const [paneId, scrollBox] of this.paneScrollBoxes.entries()) {
|
|
3507
|
+
// Skip update if paused or no new lines
|
|
3508
|
+
if (this.isPaused || !this.hasNewLines) return;
|
|
3509
|
+
|
|
3510
|
+
// Clear the flag
|
|
3511
|
+
this.hasNewLines = false;
|
|
3512
|
+
|
|
3513
|
+
// Virtual scrolling update - only update visible lines from database
|
|
3514
|
+
if (this.paneOutputBoxes.size > 0) {
|
|
3515
|
+
for (const [paneId, outputBox] of this.paneOutputBoxes.entries()) {
|
|
3191
3516
|
const pane = findPaneById(this.paneRoot, paneId);
|
|
3192
|
-
if (!pane || !
|
|
3193
|
-
// ScrollBox invalid, need full rebuild
|
|
3517
|
+
if (!pane || !outputBox) {
|
|
3194
3518
|
this.buildRunningUI();
|
|
3195
3519
|
return;
|
|
3196
3520
|
}
|
|
3197
3521
|
|
|
3198
|
-
// Only update
|
|
3199
|
-
const
|
|
3200
|
-
if (
|
|
3201
|
-
const lastUpdate = this.paneLastUpdate?.get(paneId) || 0;
|
|
3202
|
-
if (now - lastUpdate < 200) continue; // Update non-focused panes every 200ms
|
|
3203
|
-
if (!this.paneLastUpdate) this.paneLastUpdate = new Map();
|
|
3204
|
-
this.paneLastUpdate.set(paneId, now);
|
|
3205
|
-
}
|
|
3522
|
+
// Only update if at the bottom (offset=0)
|
|
3523
|
+
const scrollOffset = this.paneScrollOffsets.get(paneId) || 0;
|
|
3524
|
+
if (scrollOffset !== 0) continue;
|
|
3206
3525
|
|
|
3207
|
-
const
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
}
|
|
3236
|
-
const renderables = this.lineRenderables.get(paneId);
|
|
3237
|
-
|
|
3238
|
-
// Add new lines - reuse existing renderables or create new ones
|
|
3239
|
-
for (const line of newLines) {
|
|
3240
|
-
const processColor = this.processColors.get(line.process) || COLORS.text;
|
|
3241
|
-
|
|
3242
|
-
// Build content
|
|
3243
|
-
const lineNumber = this.showLineNumbers ? String(line.lineNumber).padStart(4, ' ') : '';
|
|
3244
|
-
const timestamp = this.showTimestamps ? (line.timeString || (line.timeString = new Date(line.timestamp).toLocaleTimeString('en-US', { hour12: false }))) : '';
|
|
3245
|
-
|
|
3246
|
-
let content;
|
|
3247
|
-
if (this.showLineNumbers && this.showTimestamps) {
|
|
3248
|
-
content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3249
|
-
} else if (this.showLineNumbers) {
|
|
3250
|
-
content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3251
|
-
} else if (this.showTimestamps) {
|
|
3252
|
-
content = t`${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3253
|
-
} else {
|
|
3254
|
-
content = t`${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3255
|
-
}
|
|
3256
|
-
|
|
3257
|
-
// Create new renderable and add to pool
|
|
3258
|
-
const outputLine = new TextRenderable(this.renderer, {
|
|
3259
|
-
id: `output-${pane.id}-${line.lineNumber}`,
|
|
3260
|
-
content: content,
|
|
3261
|
-
bg: '#000000',
|
|
3262
|
-
});
|
|
3263
|
-
|
|
3264
|
-
scrollBox.content.add(outputLine);
|
|
3265
|
-
renderables.push(outputLine);
|
|
3266
|
-
}
|
|
3267
|
-
|
|
3268
|
-
// Remove excess old lines - keep DOM small for performance
|
|
3269
|
-
while (renderables.length > maxDomLinesPerPane) {
|
|
3270
|
-
const oldLine = renderables.shift();
|
|
3271
|
-
scrollBox.content.remove(oldLine);
|
|
3272
|
-
oldLine.destroy();
|
|
3273
|
-
}
|
|
3274
|
-
|
|
3275
|
-
// Update to track the last absolute line number we rendered
|
|
3276
|
-
this.paneLineCount.set(paneId, newLines[newLines.length - 1].lineNumber);
|
|
3277
|
-
|
|
3278
|
-
// Auto-scroll to bottom if not paused
|
|
3279
|
-
if (!this.isPaused && scrollBox.scrollTo) {
|
|
3280
|
-
scrollBox.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER });
|
|
3526
|
+
const visibleHeight = this.paneVisibleHeight.get(paneId) || 20;
|
|
3527
|
+
const lines = this.outputDb.queryVisible(pane, visibleHeight, 0);
|
|
3528
|
+
|
|
3529
|
+
// Skip if no lines or last line hasn't changed
|
|
3530
|
+
if (lines.length === 0) continue;
|
|
3531
|
+
const lastLineNum = lines[lines.length - 1].lineNumber;
|
|
3532
|
+
const prevLastLine = this.paneLineCount.get(paneId) || 0;
|
|
3533
|
+
if (lastLineNum === prevLastLine) continue;
|
|
3534
|
+
|
|
3535
|
+
// Check pane content cache - if first line ID matches, we can append
|
|
3536
|
+
const firstLineId = lines[0].id;
|
|
3537
|
+
const cache = this.paneContentCache.get(paneId);
|
|
3538
|
+
|
|
3539
|
+
let styledText;
|
|
3540
|
+
if (cache && cache.firstLineId === firstLineId && cache.lineCount === lines.length - 1) {
|
|
3541
|
+
// Only 1 new line at the end - append to cached chunks
|
|
3542
|
+
const newLine = lines[lines.length - 1];
|
|
3543
|
+
const newLineContent = this.formatOutputLine(newLine, lines.length - 1, false, -1, -1);
|
|
3544
|
+
const chunks = cache.chunks.concat(newLineContent.chunks);
|
|
3545
|
+
styledText = new StyledText(chunks);
|
|
3546
|
+
this.paneContentCache.set(paneId, { firstLineId, lineCount: lines.length, chunks });
|
|
3547
|
+
} else {
|
|
3548
|
+
// Full rebuild
|
|
3549
|
+
const chunks = [];
|
|
3550
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3551
|
+
const lineContent = this.formatOutputLine(lines[i], i, false, -1, -1);
|
|
3552
|
+
for (let j = 0; j < lineContent.chunks.length; j++) {
|
|
3553
|
+
chunks.push(lineContent.chunks[j]);
|
|
3281
3554
|
}
|
|
3282
3555
|
}
|
|
3556
|
+
styledText = new StyledText(chunks);
|
|
3557
|
+
this.paneContentCache.set(paneId, { firstLineId, lineCount: lines.length, chunks });
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
// Destroy old renderable and create new one to avoid artifacts
|
|
3561
|
+
const renderables = this.lineRenderables.get(paneId);
|
|
3562
|
+
if (renderables && renderables.length > 0) {
|
|
3563
|
+
outputBox.remove(renderables[0]);
|
|
3564
|
+
renderables[0].destroy();
|
|
3283
3565
|
}
|
|
3566
|
+
|
|
3567
|
+
// Create new TextRenderable
|
|
3568
|
+
const outputText = new TextRenderable(this.renderer, {
|
|
3569
|
+
id: `output-${paneId}`,
|
|
3570
|
+
content: styledText,
|
|
3571
|
+
bg: '#000000',
|
|
3572
|
+
width: '100%',
|
|
3573
|
+
});
|
|
3574
|
+
outputBox.add(outputText);
|
|
3575
|
+
this.lineRenderables.set(paneId, [outputText]);
|
|
3576
|
+
|
|
3577
|
+
// Track last rendered line
|
|
3578
|
+
this.paneLineCount.set(paneId, lastLineNum);
|
|
3579
|
+
}
|
|
3284
3580
|
} else {
|
|
3285
3581
|
// First time or no panes exist - do full rebuild
|
|
3286
3582
|
this.buildRunningUI();
|
|
@@ -3295,9 +3591,8 @@ class ProcessManager {
|
|
|
3295
3591
|
}
|
|
3296
3592
|
|
|
3297
3593
|
// Remove old process bar items
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
this.processBarContainer.remove(child);
|
|
3594
|
+
for (const child of this.processBarContainer.getChildren()) {
|
|
3595
|
+
this.processBarContainer.remove(child.id);
|
|
3301
3596
|
child.destroy();
|
|
3302
3597
|
}
|
|
3303
3598
|
|
|
@@ -3314,7 +3609,7 @@ class ProcessManager {
|
|
|
3314
3609
|
const isVisible = this.isProcessVisibleInPane(script.name, focusedPane);
|
|
3315
3610
|
const nameColor = isSelected ? COLORS.accent : (isVisible ? processColor : COLORS.textDim);
|
|
3316
3611
|
const numberColor = isVisible ? processColor : COLORS.textDim;
|
|
3317
|
-
const indicator = isSelected ? '
|
|
3612
|
+
const indicator = isSelected ? '❯' : ' ';
|
|
3318
3613
|
const bracketColor = isVisible ? processColor : COLORS.textDim;
|
|
3319
3614
|
|
|
3320
3615
|
const numberLabel = index < 9 ? `${index + 1}` : ' ';
|
|
@@ -3334,7 +3629,83 @@ class ProcessManager {
|
|
|
3334
3629
|
});
|
|
3335
3630
|
}
|
|
3336
3631
|
|
|
3337
|
-
//
|
|
3632
|
+
// Get cache key for current display settings
|
|
3633
|
+
getFormatCacheKey() {
|
|
3634
|
+
return `${this.showLineNumbers}-${this.showTimestamps}-${this.renderer.width}`;
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
// Format a single line for display (returns styled text chunks)
|
|
3638
|
+
formatOutputLine(line, index, inCopyMode, copySelStart, copySelEnd) {
|
|
3639
|
+
// Copy mode can't use cache (dynamic highlighting)
|
|
3640
|
+
if (inCopyMode) {
|
|
3641
|
+
return this.formatOutputLineCopyMode(line, index, copySelStart, copySelEnd);
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
// Can't cache if we're padding to width (width can vary)
|
|
3645
|
+
// Check cache only if no padding needed
|
|
3646
|
+
const cacheKey = this.getFormatCacheKey();
|
|
3647
|
+
if (cacheKey !== this.lineFormatCacheKey) {
|
|
3648
|
+
// Settings changed, clear cache
|
|
3649
|
+
this.lineFormatCache.clear();
|
|
3650
|
+
this.lineFormatCacheKey = cacheKey;
|
|
3651
|
+
}
|
|
3652
|
+
|
|
3653
|
+
// Format line
|
|
3654
|
+
const processColor = this.processColors.get(line.process) || COLORS.text;
|
|
3655
|
+
const lineNumber = this.showLineNumbers ? String(line.lineNumber).padStart(4, ' ') : '';
|
|
3656
|
+
const timestamp = this.showTimestamps ? (line.timeString || (line.timeString = new Date(line.timestamp).toLocaleTimeString('en-US', { hour12: false }))) : '';
|
|
3657
|
+
|
|
3658
|
+
let result;
|
|
3659
|
+
if (this.showLineNumbers && this.showTimestamps) {
|
|
3660
|
+
result = t`${fg(COLORS.textDim)(lineNumber)} ${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${line.text}\n`;
|
|
3661
|
+
} else if (this.showLineNumbers) {
|
|
3662
|
+
result = t`${fg(COLORS.textDim)(lineNumber)} ${fg(processColor)(`[${line.process}]`)} ${line.text}\n`;
|
|
3663
|
+
} else if (this.showTimestamps) {
|
|
3664
|
+
result = t`${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${line.text}\n`;
|
|
3665
|
+
} else {
|
|
3666
|
+
result = t`${fg(processColor)(`[${line.process}]`)} ${line.text}\n`;
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
return result;
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
// Format line in copy mode (no caching - dynamic highlighting)
|
|
3673
|
+
formatOutputLineCopyMode(line, index, copySelStart, copySelEnd) {
|
|
3674
|
+
const processColor = this.processColors.get(line.process) || COLORS.text;
|
|
3675
|
+
const lineNumber = this.showLineNumbers ? String(line.lineNumber).padStart(4, ' ') : '';
|
|
3676
|
+
const timestamp = this.showTimestamps ? (line.timeString || (line.timeString = new Date(line.timestamp).toLocaleTimeString('en-US', { hour12: false }))) : '';
|
|
3677
|
+
|
|
3678
|
+
const isCursorLine = index === this.copyModeCursor;
|
|
3679
|
+
const isSelectedLine = index >= copySelStart && index <= copySelEnd;
|
|
3680
|
+
|
|
3681
|
+
const marker = isCursorLine ? fg(COLORS.copyCursorText)('\u25b6 ') : ' ';
|
|
3682
|
+
let textColor, procColor, dimColor;
|
|
3683
|
+
if (isCursorLine) {
|
|
3684
|
+
textColor = COLORS.copyCursorText;
|
|
3685
|
+
procColor = COLORS.copyCursorText;
|
|
3686
|
+
dimColor = COLORS.copySelectText;
|
|
3687
|
+
} else if (isSelectedLine) {
|
|
3688
|
+
textColor = COLORS.copySelectText;
|
|
3689
|
+
procColor = processColor;
|
|
3690
|
+
dimColor = COLORS.textDim;
|
|
3691
|
+
} else {
|
|
3692
|
+
textColor = COLORS.textDim;
|
|
3693
|
+
procColor = COLORS.textDim;
|
|
3694
|
+
dimColor = COLORS.textDim;
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
if (this.showLineNumbers && this.showTimestamps) {
|
|
3698
|
+
return t`${marker}${fg(dimColor)(lineNumber)} ${fg(dimColor)(`[${timestamp}]`)} ${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}\n`;
|
|
3699
|
+
} else if (this.showLineNumbers) {
|
|
3700
|
+
return t`${marker}${fg(dimColor)(lineNumber)} ${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}\n`;
|
|
3701
|
+
} else if (this.showTimestamps) {
|
|
3702
|
+
return t`${marker}${fg(dimColor)(`[${timestamp}]`)} ${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}\n`;
|
|
3703
|
+
} else {
|
|
3704
|
+
return t`${marker}${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}\n`;
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
|
|
3708
|
+
// Build a single pane's output area - uses single TextRenderable for efficiency
|
|
3338
3709
|
buildPaneOutput(pane, container, height) {
|
|
3339
3710
|
const isFocused = pane.id === this.focusedPaneId;
|
|
3340
3711
|
const lines = this.getOutputLinesForPane(pane);
|
|
@@ -3344,90 +3715,34 @@ class ProcessManager {
|
|
|
3344
3715
|
const maxLines = this.isPaused ? lines.length : Math.min(lines.length, height || 50);
|
|
3345
3716
|
const linesToShow = lines.slice(-maxLines);
|
|
3346
3717
|
|
|
3347
|
-
// Initialize renderable pool for this pane
|
|
3348
|
-
const renderables = [];
|
|
3349
|
-
this.lineRenderables.set(pane.id, renderables);
|
|
3350
|
-
|
|
3351
3718
|
// Determine copy mode selection range for this pane
|
|
3352
3719
|
const inCopyMode = this.isCopyMode && isFocused;
|
|
3353
3720
|
let copySelStart = -1;
|
|
3354
3721
|
let copySelEnd = -1;
|
|
3355
|
-
if (inCopyMode) {
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
copySelEnd = Math.max(this.copyModeAnchor, this.copyModeCursor);
|
|
3359
|
-
}
|
|
3722
|
+
if (inCopyMode && this.copyModeAnchor !== null) {
|
|
3723
|
+
copySelStart = Math.min(this.copyModeAnchor, this.copyModeCursor);
|
|
3724
|
+
copySelEnd = Math.max(this.copyModeAnchor, this.copyModeCursor);
|
|
3360
3725
|
}
|
|
3361
3726
|
|
|
3362
|
-
//
|
|
3727
|
+
// Build all lines as a single styled text (much more efficient than one renderable per line)
|
|
3728
|
+
const chunks = [];
|
|
3363
3729
|
for (let i = 0; i < linesToShow.length; i++) {
|
|
3364
|
-
const
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
// Build content with proper template literal
|
|
3368
|
-
const lineNumber = this.showLineNumbers ? String(line.lineNumber).padStart(4, ' ') : '';
|
|
3369
|
-
const timestamp = this.showTimestamps ? new Date(line.timestamp).toLocaleTimeString('en-US', { hour12: false }) : '';
|
|
3370
|
-
|
|
3371
|
-
// Determine copy mode highlighting for this line
|
|
3372
|
-
const isCursorLine = inCopyMode && i === this.copyModeCursor;
|
|
3373
|
-
const isSelectedLine = inCopyMode && i >= copySelStart && i <= copySelEnd;
|
|
3374
|
-
|
|
3375
|
-
let content;
|
|
3376
|
-
if (inCopyMode) {
|
|
3377
|
-
// In copy mode: cursor line gets bright marker + white text on blue bg
|
|
3378
|
-
// Selected lines get lighter text on darker blue bg
|
|
3379
|
-
// Unselected lines are dimmed to make selection stand out
|
|
3380
|
-
const marker = isCursorLine ? fg(COLORS.copyCursorText)('\u25b6 ') : ' ';
|
|
3381
|
-
|
|
3382
|
-
let textColor, procColor, dimColor;
|
|
3383
|
-
if (isCursorLine) {
|
|
3384
|
-
textColor = COLORS.copyCursorText;
|
|
3385
|
-
procColor = COLORS.copyCursorText;
|
|
3386
|
-
dimColor = COLORS.copySelectText;
|
|
3387
|
-
} else if (isSelectedLine) {
|
|
3388
|
-
textColor = COLORS.copySelectText;
|
|
3389
|
-
procColor = processColor;
|
|
3390
|
-
dimColor = COLORS.textDim;
|
|
3391
|
-
} else {
|
|
3392
|
-
textColor = COLORS.textDim;
|
|
3393
|
-
procColor = COLORS.textDim;
|
|
3394
|
-
dimColor = COLORS.textDim;
|
|
3395
|
-
}
|
|
3396
|
-
|
|
3397
|
-
if (this.showLineNumbers && this.showTimestamps) {
|
|
3398
|
-
content = t`${marker}${fg(dimColor)(lineNumber)} ${fg(dimColor)(`[${timestamp}]`)} ${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}`;
|
|
3399
|
-
} else if (this.showLineNumbers) {
|
|
3400
|
-
content = t`${marker}${fg(dimColor)(lineNumber)} ${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}`;
|
|
3401
|
-
} else if (this.showTimestamps) {
|
|
3402
|
-
content = t`${marker}${fg(dimColor)(`[${timestamp}]`)} ${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}`;
|
|
3403
|
-
} else {
|
|
3404
|
-
content = t`${marker}${fg(procColor)(`[${line.process}]`)} ${fg(textColor)(line.text)}`;
|
|
3405
|
-
}
|
|
3406
|
-
} else {
|
|
3407
|
-
if (this.showLineNumbers && this.showTimestamps) {
|
|
3408
|
-
content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3409
|
-
} else if (this.showLineNumbers) {
|
|
3410
|
-
content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3411
|
-
} else if (this.showTimestamps) {
|
|
3412
|
-
content = t`${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3413
|
-
} else {
|
|
3414
|
-
content = t`${fg(processColor)(`[${line.process}]`)} ${line.text}`;
|
|
3415
|
-
}
|
|
3416
|
-
}
|
|
3417
|
-
|
|
3418
|
-
// High-contrast background: bright blue for cursor, medium blue for selected, black for rest
|
|
3419
|
-
const bgColor = isCursorLine ? COLORS.copyCursorBg : (isSelectedLine ? COLORS.copySelectBg : '#000000');
|
|
3420
|
-
|
|
3421
|
-
const outputLine = new TextRenderable(this.renderer, {
|
|
3422
|
-
id: `output-${pane.id}-${line.lineNumber}`,
|
|
3423
|
-
content: content,
|
|
3424
|
-
bg: bgColor,
|
|
3425
|
-
});
|
|
3426
|
-
|
|
3427
|
-
container.add(outputLine);
|
|
3428
|
-
renderables.push(outputLine);
|
|
3730
|
+
const lineContent = this.formatOutputLine(linesToShow[i], i, inCopyMode, copySelStart, copySelEnd);
|
|
3731
|
+
chunks.push(...lineContent.chunks);
|
|
3429
3732
|
}
|
|
3430
3733
|
|
|
3734
|
+
// Create single TextRenderable with all content
|
|
3735
|
+
const outputText = new TextRenderable(this.renderer, {
|
|
3736
|
+
id: `output-${pane.id}`,
|
|
3737
|
+
content: new StyledText(chunks),
|
|
3738
|
+
bg: '#000000',
|
|
3739
|
+
});
|
|
3740
|
+
|
|
3741
|
+
container.add(outputText);
|
|
3742
|
+
|
|
3743
|
+
// Store reference for incremental updates
|
|
3744
|
+
this.lineRenderables.set(pane.id, [outputText]);
|
|
3745
|
+
|
|
3431
3746
|
// Track last rendered line number
|
|
3432
3747
|
if (linesToShow.length > 0) {
|
|
3433
3748
|
this.paneLineCount.set(pane.id, linesToShow[linesToShow.length - 1].lineNumber);
|
|
@@ -3447,7 +3762,7 @@ class ProcessManager {
|
|
|
3447
3762
|
}
|
|
3448
3763
|
}
|
|
3449
3764
|
|
|
3450
|
-
// Build a pane panel with title bar
|
|
3765
|
+
// Build a pane panel with title bar - uses virtual scrolling (no ScrollBox)
|
|
3451
3766
|
buildPanePanel(pane, flexGrow = 1, availableHeight = null) {
|
|
3452
3767
|
const isFocused = pane.id === this.focusedPaneId;
|
|
3453
3768
|
const borderColor = isFocused ? COLORS.borderFocused : COLORS.border;
|
|
@@ -3463,14 +3778,20 @@ class ProcessManager {
|
|
|
3463
3778
|
const filterLabel = pane.filter ? ` /${pane.filter}` : '';
|
|
3464
3779
|
const namingInputLabel = (isFocused && this.isNamingMode) ? `Name: ${this.namingModeText}_` : '';
|
|
3465
3780
|
const filterInputLabel = (isFocused && this.isFilterMode) ? `/${pane.filter || ''}_` : '';
|
|
3466
|
-
|
|
3781
|
+
|
|
3782
|
+
// Show scroll position indicator when paused
|
|
3783
|
+
const scrollOffset = this.paneScrollOffsets.get(pane.id) || 0;
|
|
3784
|
+
const totalLines = this.outputDb.countForPane(pane);
|
|
3785
|
+
const scrollIndicator = (this.isPaused && scrollOffset > 0) ? ` [${scrollOffset}↑]` : '';
|
|
3786
|
+
|
|
3787
|
+
const title = ` ${focusLabel}${namingInputLabel || processLabel}${hiddenLabel}${filterInputLabel || filterLabel}${scrollIndicator} `;
|
|
3467
3788
|
|
|
3468
3789
|
const paneContainer = new BoxRenderable(this.renderer, {
|
|
3469
3790
|
id: `pane-${pane.id}`,
|
|
3470
3791
|
flexDirection: 'column',
|
|
3471
3792
|
flexGrow: flexGrow,
|
|
3472
|
-
flexShrink: 0,
|
|
3473
|
-
flexBasis: 0,
|
|
3793
|
+
flexShrink: 0,
|
|
3794
|
+
flexBasis: 0,
|
|
3474
3795
|
border: true,
|
|
3475
3796
|
borderStyle: 'rounded',
|
|
3476
3797
|
borderColor: borderColor,
|
|
@@ -3478,63 +3799,81 @@ class ProcessManager {
|
|
|
3478
3799
|
titleAlignment: 'left',
|
|
3479
3800
|
padding: 0,
|
|
3480
3801
|
overflow: 'hidden',
|
|
3481
|
-
backgroundColor: '#000000',
|
|
3802
|
+
backgroundColor: '#000000',
|
|
3482
3803
|
});
|
|
3483
3804
|
|
|
3484
|
-
//
|
|
3485
|
-
const
|
|
3805
|
+
// Calculate visible height (minus border)
|
|
3806
|
+
const visibleHeight = availableHeight ? Math.max(5, availableHeight - 2) : Math.max(5, this.renderer.height - 6);
|
|
3486
3807
|
|
|
3487
|
-
|
|
3808
|
+
// Store visible height for scroll calculations
|
|
3809
|
+
this.paneVisibleHeight.set(pane.id, visibleHeight);
|
|
3810
|
+
|
|
3811
|
+
// Create output container (no ScrollBox - we handle scrolling virtually)
|
|
3812
|
+
const outputBox = new BoxRenderable(this.renderer, {
|
|
3488
3813
|
id: `pane-output-${pane.id}`,
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
flexGrow: 1,
|
|
3496
|
-
flexShrink: 1,
|
|
3497
|
-
flexBasis: 0,
|
|
3498
|
-
paddingLeft: 1,
|
|
3499
|
-
backgroundColor: '#000000',
|
|
3500
|
-
},
|
|
3501
|
-
contentOptions: {
|
|
3502
|
-
backgroundColor: '#000000',
|
|
3503
|
-
width: '100%',
|
|
3504
|
-
},
|
|
3505
|
-
},
|
|
3814
|
+
flexDirection: 'column',
|
|
3815
|
+
flexGrow: 1,
|
|
3816
|
+
paddingLeft: 1,
|
|
3817
|
+
backgroundColor: '#000000',
|
|
3818
|
+
overflow: 'hidden',
|
|
3819
|
+
shouldFill: true,
|
|
3506
3820
|
});
|
|
3507
3821
|
|
|
3508
|
-
//
|
|
3509
|
-
|
|
3510
|
-
|
|
3822
|
+
// Query only the visible lines from database
|
|
3823
|
+
const lines = this.outputDb.queryVisible(pane, visibleHeight, scrollOffset);
|
|
3824
|
+
|
|
3825
|
+
// Build content for visible lines
|
|
3826
|
+
this.buildPaneOutputVirtual(pane, outputBox, lines, visibleHeight);
|
|
3827
|
+
|
|
3828
|
+
// Store reference to output box for updates
|
|
3829
|
+
this.paneOutputBoxes.set(pane.id, outputBox);
|
|
3830
|
+
|
|
3831
|
+
// Track the last line number for incremental updates
|
|
3832
|
+
if (lines.length > 0) {
|
|
3833
|
+
this.paneLineCount.set(pane.id, lines[lines.length - 1].lineNumber);
|
|
3511
3834
|
}
|
|
3512
3835
|
|
|
3513
|
-
|
|
3514
|
-
|
|
3836
|
+
paneContainer.add(outputBox);
|
|
3837
|
+
return paneContainer;
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
// Build pane output with virtual scrolling - only renders visible lines
|
|
3841
|
+
buildPaneOutputVirtual(pane, container, lines, visibleHeight) {
|
|
3842
|
+
const isFocused = pane.id === this.focusedPaneId;
|
|
3515
3843
|
|
|
3516
|
-
|
|
3844
|
+
// Determine copy mode selection range
|
|
3845
|
+
const inCopyMode = this.isCopyMode && isFocused;
|
|
3846
|
+
let copySelStart = -1;
|
|
3847
|
+
let copySelEnd = -1;
|
|
3848
|
+
if (inCopyMode && this.copyModeAnchor !== null) {
|
|
3849
|
+
copySelStart = Math.min(this.copyModeAnchor, this.copyModeCursor);
|
|
3850
|
+
copySelEnd = Math.max(this.copyModeAnchor, this.copyModeCursor);
|
|
3851
|
+
}
|
|
3517
3852
|
|
|
3518
|
-
//
|
|
3519
|
-
const
|
|
3520
|
-
|
|
3521
|
-
this.
|
|
3853
|
+
// Build all visible lines as single styled text
|
|
3854
|
+
const chunks = [];
|
|
3855
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3856
|
+
const lineContent = this.formatOutputLine(lines[i], i, inCopyMode, copySelStart, copySelEnd);
|
|
3857
|
+
chunks.push(...lineContent.chunks);
|
|
3522
3858
|
}
|
|
3523
3859
|
|
|
3524
|
-
//
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
// Restore saved scroll position when paused
|
|
3528
|
-
const savedPos = this.paneScrollPositions.get(pane.id);
|
|
3529
|
-
outputBox.scrollTo(savedPos);
|
|
3530
|
-
} else if (!this.isPaused) {
|
|
3531
|
-
// Auto-scroll to bottom when not paused
|
|
3532
|
-
outputBox.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER });
|
|
3533
|
-
}
|
|
3860
|
+
// Fill remaining visible area with blank lines to clear artifacts
|
|
3861
|
+
for (let i = lines.length; i < visibleHeight; i++) {
|
|
3862
|
+
chunks.push({ text: '\n' });
|
|
3534
3863
|
}
|
|
3535
3864
|
|
|
3536
|
-
|
|
3537
|
-
|
|
3865
|
+
// Create single TextRenderable with all visible content
|
|
3866
|
+
const outputText = new TextRenderable(this.renderer, {
|
|
3867
|
+
id: `output-${pane.id}`,
|
|
3868
|
+
content: new StyledText(chunks),
|
|
3869
|
+
bg: '#000000',
|
|
3870
|
+
width: '100%',
|
|
3871
|
+
});
|
|
3872
|
+
|
|
3873
|
+
container.add(outputText);
|
|
3874
|
+
|
|
3875
|
+
// Store reference for updates
|
|
3876
|
+
this.lineRenderables.set(pane.id, [outputText]);
|
|
3538
3877
|
}
|
|
3539
3878
|
|
|
3540
3879
|
// Recursively build the pane layout, passing available height down
|
|
@@ -3835,6 +4174,7 @@ class ProcessManager {
|
|
|
3835
4174
|
let titleIcon = '';
|
|
3836
4175
|
if (this.gitModalPhase === 'committing') titleIcon = '...';
|
|
3837
4176
|
else if (this.gitModalPhase === 'pushing') titleIcon = '...';
|
|
4177
|
+
else if (this.gitModalPhase === 'pulling') titleIcon = '...';
|
|
3838
4178
|
else titleIcon = '';
|
|
3839
4179
|
const title = ` Git: ${branch} ${titleIcon}`;
|
|
3840
4180
|
|
|
@@ -3924,9 +4264,9 @@ class ProcessManager {
|
|
|
3924
4264
|
content: t`${fg(COLORS.textDim)('All changes will be staged and committed.')}`,
|
|
3925
4265
|
});
|
|
3926
4266
|
overlay.add(commitHint);
|
|
3927
|
-
} else if (this.gitModalPhase === 'committing' || this.gitModalPhase === 'pushing') {
|
|
4267
|
+
} else if (this.gitModalPhase === 'committing' || this.gitModalPhase === 'pushing' || this.gitModalPhase === 'pulling') {
|
|
3928
4268
|
// Show busy indicator
|
|
3929
|
-
const busyText = this.gitModalPhase === 'committing' ? 'Committing...' : 'Pushing...';
|
|
4269
|
+
const busyText = this.gitModalPhase === 'committing' ? 'Committing...' : this.gitModalPhase === 'pushing' ? 'Pushing...' : 'Pulling...';
|
|
3930
4270
|
const busyLine = new TextRenderable(this.renderer, {
|
|
3931
4271
|
id: 'git-busy',
|
|
3932
4272
|
content: t`${fg(COLORS.warning)(busyText)}`,
|
|
@@ -4080,7 +4420,7 @@ class ProcessManager {
|
|
|
4080
4420
|
});
|
|
4081
4421
|
hintBar.add(hint);
|
|
4082
4422
|
});
|
|
4083
|
-
} else if (this.gitModalPhase === 'committing' || this.gitModalPhase === 'pushing') {
|
|
4423
|
+
} else if (this.gitModalPhase === 'committing' || this.gitModalPhase === 'pushing' || this.gitModalPhase === 'pulling') {
|
|
4084
4424
|
const hint = new TextRenderable(this.renderer, {
|
|
4085
4425
|
id: 'git-hint-busy',
|
|
4086
4426
|
content: t`${fg(COLORS.warning)('Please wait...')}`,
|
|
@@ -4091,6 +4431,7 @@ class ProcessManager {
|
|
|
4091
4431
|
{ key: 'c', desc: 'commit', color: COLORS.success },
|
|
4092
4432
|
{ key: 'a', desc: 'stage all', color: COLORS.warning },
|
|
4093
4433
|
{ key: 'p', desc: 'push', color: COLORS.cyan },
|
|
4434
|
+
{ key: 'l', desc: 'pull', color: COLORS.cyan },
|
|
4094
4435
|
{ key: 'r', desc: 'refresh', color: COLORS.magenta },
|
|
4095
4436
|
{ key: 'esc', desc: 'close', color: COLORS.error },
|
|
4096
4437
|
];
|
|
@@ -4119,6 +4460,11 @@ class ProcessManager {
|
|
|
4119
4460
|
}
|
|
4120
4461
|
|
|
4121
4462
|
// Remove old containers if they exist - use destroyRecursively to clean up all children
|
|
4463
|
+
if (this.wipeContainer) {
|
|
4464
|
+
this.renderer.root.remove(this.wipeContainer);
|
|
4465
|
+
this.wipeContainer.destroyRecursively();
|
|
4466
|
+
this.wipeContainer = null;
|
|
4467
|
+
}
|
|
4122
4468
|
if (this.selectionContainer) {
|
|
4123
4469
|
this.renderer.root.remove(this.selectionContainer);
|
|
4124
4470
|
this.selectionContainer.destroyRecursively();
|
|
@@ -4178,7 +4524,7 @@ class ProcessManager {
|
|
|
4178
4524
|
const isVisible = this.isProcessVisibleInPane(script.name, focusedPane);
|
|
4179
4525
|
const nameColor = isSelected ? COLORS.accent : (isVisible ? processColor : COLORS.textDim);
|
|
4180
4526
|
const numberColor = isVisible ? processColor : COLORS.textDim;
|
|
4181
|
-
const indicator = isSelected ? '
|
|
4527
|
+
const indicator = isSelected ? '❯' : ' ';
|
|
4182
4528
|
const bracketColor = isVisible ? processColor : COLORS.textDim;
|
|
4183
4529
|
|
|
4184
4530
|
// Show number for first 9 processes
|
|
@@ -4489,7 +4835,9 @@ async function main() {
|
|
|
4489
4835
|
process.exit(1);
|
|
4490
4836
|
}
|
|
4491
4837
|
|
|
4492
|
-
const renderer = await createCliRenderer(
|
|
4838
|
+
const renderer = await createCliRenderer({
|
|
4839
|
+
backgroundColor: '#000000',
|
|
4840
|
+
});
|
|
4493
4841
|
renderer.start(); // Start the automatic render loop
|
|
4494
4842
|
const manager = new ProcessManager(renderer, scripts);
|
|
4495
4843
|
|