startall 0.0.3 → 0.0.9

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 CHANGED
@@ -1,17 +1,20 @@
1
1
  #!/usr/bin/env bun
2
-
2
+
3
3
  import { createCliRenderer, TextRenderable, BoxRenderable, ScrollBoxRenderable, t, fg } from '@opentui/core';
4
4
  import { spawn } from 'child_process';
5
5
  import { readFileSync, writeFileSync, existsSync } from 'fs';
6
6
  import { join } from 'path';
7
7
  import kill from 'tree-kill';
8
8
  import stripAnsi from 'strip-ansi';
9
-
9
+
10
10
  // Configuration
11
11
  const CONFIG_FILE = process.argv[2] || 'startall.json';
12
12
  const COUNTDOWN_SECONDS = 10;
13
13
  const APP_VERSION = 'v0.0.4';
14
14
 
15
+ // Detect if running inside VS Code's integrated terminal
16
+ const IS_VSCODE = process.env.TERM_PROGRAM === 'vscode';
17
+
15
18
  // Pane ID generator
16
19
  let paneIdCounter = 0;
17
20
  function generatePaneId() {
@@ -23,9 +26,11 @@ function createPane(processes = []) {
23
26
  return {
24
27
  type: 'pane',
25
28
  id: generatePaneId(),
29
+ name: '', // Custom name for the pane
26
30
  processes: processes, // Array of process names shown in this pane (empty = all)
27
31
  hidden: [], // Array of process names to hide from this pane
28
32
  filter: '', // Text filter for this pane
33
+ colorFilter: null, // Color filter: 'red', 'yellow', 'green', 'blue', 'cyan', 'magenta', or null
29
34
  isPaused: false,
30
35
  scrollOffset: 0,
31
36
  };
@@ -67,6 +72,34 @@ function getAllPaneIds(node, ids = []) {
67
72
  return ids;
68
73
  }
69
74
 
75
+ // Get ANSI color codes for a color name (includes normal and bright variants)
76
+ function getAnsiColorCodes(colorName) {
77
+ const colorMap = {
78
+ red: [31, 91], // normal red, bright red
79
+ yellow: [33, 93], // normal yellow, bright yellow (warnings)
80
+ green: [32, 92], // normal green, bright green
81
+ blue: [34, 94], // normal blue, bright blue
82
+ cyan: [36, 96], // normal cyan, bright cyan
83
+ magenta: [35, 95], // normal magenta, bright magenta
84
+ };
85
+ return colorMap[colorName] || [];
86
+ }
87
+
88
+ // Check if a line contains a specific ANSI color
89
+ function lineHasColor(text, colorName) {
90
+ const codes = getAnsiColorCodes(colorName);
91
+ // Match ANSI escape sequences like \x1b[31m, \x1b[91m, \x1b[1;31m, etc.
92
+ for (const code of codes) {
93
+ // Check for direct color code: \x1b[31m
94
+ if (text.includes(`\x1b[${code}m`)) return true;
95
+ // Check for color with modifiers: \x1b[1;31m, \x1b[0;31m, etc.
96
+ if (text.includes(`;${code}m`)) return true;
97
+ // Check for color at start of sequence: \x1b[31;1m
98
+ if (text.includes(`\x1b[${code};`)) return true;
99
+ }
100
+ return false;
101
+ }
102
+
70
103
  // Find parent of a node
71
104
  function findParent(root, targetId, parent = null) {
72
105
  if (!root) return null;
@@ -170,8 +203,11 @@ function serializePaneTree(node) {
170
203
  if (node.type === 'pane') {
171
204
  return {
172
205
  type: 'pane',
206
+ name: node.name || '',
173
207
  processes: node.processes || [],
174
208
  hidden: node.hidden || [],
209
+ filter: node.filter || '',
210
+ colorFilter: node.colorFilter || null,
175
211
  };
176
212
  }
177
213
 
@@ -189,7 +225,10 @@ function deserializePaneTree(data) {
189
225
 
190
226
  if (data.type === 'pane') {
191
227
  const pane = createPane(data.processes || []);
228
+ pane.name = data.name || '';
192
229
  pane.hidden = data.hidden || [];
230
+ pane.filter = data.filter || '';
231
+ pane.colorFilter = data.colorFilter || null;
193
232
  return pane;
194
233
  }
195
234
 
@@ -232,26 +271,26 @@ function isIncluded(name, includePatterns) {
232
271
  function isIgnored(name, ignorePatterns) {
233
272
  return ignorePatterns.some(pattern => matchesPattern(name, pattern));
234
273
  }
235
-
236
- // Parse npm scripts from package.json
237
- function parseNpmScripts(packageJsonPath) {
238
- try {
239
- const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
240
- const scripts = pkg.scripts || {};
241
-
242
- return Object.entries(scripts)
243
- .filter(([name]) => !name.startsWith('pre') && !name.startsWith('post') && name !== 'start')
244
- .map(([name, command]) => ({
245
- name,
246
- command: `npm run ${name}`,
247
- displayName: name,
248
- }));
249
- } catch (error) {
250
- console.error('Error reading package.json:', error.message);
251
- process.exit(1);
252
- }
253
- }
254
-
274
+
275
+ // Parse npm scripts from package.json
276
+ function parseNpmScripts(packageJsonPath) {
277
+ try {
278
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
279
+ const scripts = pkg.scripts || {};
280
+
281
+ return Object.entries(scripts)
282
+ .filter(([name]) => !name.startsWith('pre') && !name.startsWith('post') && name !== 'start')
283
+ .map(([name, command]) => ({
284
+ name,
285
+ command: `npm run ${name}`,
286
+ displayName: name,
287
+ }));
288
+ } catch (error) {
289
+ console.error('Error reading package.json:', error.message);
290
+ process.exit(1);
291
+ }
292
+ }
293
+
255
294
  // Load config
256
295
  function loadConfig() {
257
296
  if (existsSync(CONFIG_FILE)) {
@@ -272,7 +311,7 @@ function saveConfig(config) {
272
311
  console.error('Error saving config:', error.message);
273
312
  }
274
313
  }
275
-
314
+
276
315
  // Process Manager
277
316
  class ProcessManager {
278
317
  constructor(renderer, scripts) {
@@ -295,6 +334,8 @@ class ProcessManager {
295
334
  this.isPaused = false; // Whether output scrolling is paused
296
335
  this.wasPaused = false; // Track previous pause state to detect changes
297
336
  this.isFilterMode = false; // Whether in filter input mode
337
+ this.isNamingMode = false; // Whether in pane naming input mode
338
+ this.namingModeText = ''; // Text being typed for pane name
298
339
 
299
340
  // Settings menu state
300
341
  this.settingsSection = 'ignore'; // 'ignore' | 'include' | 'scripts'
@@ -305,7 +346,7 @@ class ProcessManager {
305
346
  this.previousPhase = 'selection'; // Track where we came from
306
347
  this.outputBox = null; // Reference to the output container
307
348
  this.destroyed = false; // Flag to prevent operations after cleanup
308
- this.lastRenderedLineCount = 0; // Track how many lines we've rendered
349
+ this.lastRenderedLineCount = 0; // Track how many lines we've rendered
309
350
  this.headerRenderable = null; // Reference to header text in running UI
310
351
  this.processListRenderable = null; // Reference to process list text in running UI
311
352
  this.renderScheduled = false; // Throttle renders for CPU efficiency
@@ -318,44 +359,44 @@ class ProcessManager {
318
359
  this.splitMenuIndex = 0; // Selected item in split menu
319
360
 
320
361
  // Assign colors to each script
321
- this.processColors = new Map();
322
- const colors = ['#7aa2f7', '#bb9af7', '#9ece6a', '#f7768e', '#e0af68', '#73daca'];
323
- scripts.forEach((script, index) => {
324
- this.processColors.set(script.name, colors[index % colors.length]);
325
- });
326
-
327
- // UI references
328
- this.headerText = null;
329
- this.scriptLines = [];
330
- this.scriptLinePositions = []; // Track Y positions of script lines for mouse clicks
331
- this.selectionContainer = null;
332
- this.runningContainer = null;
333
-
334
- this.setupKeyboardHandlers();
335
- this.setupMouseHandlers();
336
- this.buildSelectionUI();
337
- this.startCountdown();
338
- }
339
-
340
- setupKeyboardHandlers() {
341
- this.renderer.keyInput.on('keypress', (key) => {
342
- // Handle Ctrl+C (if exitOnCtrlC is false)
343
- if (key.ctrl && key.name === 'c') {
344
- this.cleanup();
345
- this.renderer.destroy();
346
- return;
347
- }
348
-
349
- this.handleInput(key.name, key);
350
- this.render();
351
- });
352
- }
353
-
354
- setupMouseHandlers() {
355
- // Mouse events are handled via BoxRenderable properties, not a global handler
356
- // We'll add onMouseDown to individual script lines in buildSelectionUI
357
- }
358
-
362
+ this.processColors = new Map();
363
+ const colors = ['#7aa2f7', '#bb9af7', '#9ece6a', '#f7768e', '#e0af68', '#73daca'];
364
+ scripts.forEach((script, index) => {
365
+ this.processColors.set(script.name, colors[index % colors.length]);
366
+ });
367
+
368
+ // UI references
369
+ this.headerText = null;
370
+ this.scriptLines = [];
371
+ this.scriptLinePositions = []; // Track Y positions of script lines for mouse clicks
372
+ this.selectionContainer = null;
373
+ this.runningContainer = null;
374
+
375
+ this.setupKeyboardHandlers();
376
+ this.setupMouseHandlers();
377
+ this.buildSelectionUI();
378
+ this.startCountdown();
379
+ }
380
+
381
+ setupKeyboardHandlers() {
382
+ this.renderer.keyInput.on('keypress', (key) => {
383
+ // Handle Ctrl+C (if exitOnCtrlC is false)
384
+ if (key.ctrl && key.name === 'c') {
385
+ this.cleanup();
386
+ this.renderer.destroy();
387
+ return;
388
+ }
389
+
390
+ this.handleInput(key.name, key);
391
+ this.render();
392
+ });
393
+ }
394
+
395
+ setupMouseHandlers() {
396
+ // Mouse events are handled via BoxRenderable properties, not a global handler
397
+ // We'll add onMouseDown to individual script lines in buildSelectionUI
398
+ }
399
+
359
400
  handleInput(keyName, keyEvent) {
360
401
  if (this.phase === 'selection') {
361
402
  if (keyName === 'enter' || keyName === 'return') {
@@ -376,8 +417,8 @@ class ProcessManager {
376
417
  // Reset countdown when selection changes
377
418
  this.countdown = COUNTDOWN_SECONDS;
378
419
  }
379
- } else if (keyName === 'c') {
380
- // Open settings menu
420
+ } else if (keyName === 'o') {
421
+ // Open settings menu (options)
381
422
  clearInterval(this.countdownInterval);
382
423
  this.previousPhase = 'selection';
383
424
  this.phase = 'settings';
@@ -396,8 +437,31 @@ class ProcessManager {
396
437
  return;
397
438
  }
398
439
 
440
+ // If in naming mode, handle name input
441
+ if (this.isNamingMode) {
442
+ const pane = findPaneById(this.paneRoot, this.focusedPaneId);
443
+ if (keyName === 'escape') {
444
+ this.isNamingMode = false;
445
+ this.namingModeText = '';
446
+ this.buildRunningUI(); // Rebuild to cancel naming
447
+ } else if (keyName === 'enter' || keyName === 'return') {
448
+ if (pane) {
449
+ pane.name = this.namingModeText;
450
+ this.savePaneLayout(); // Save the new name
451
+ }
452
+ this.isNamingMode = false;
453
+ this.namingModeText = '';
454
+ this.buildRunningUI(); // Rebuild with new name
455
+ } else if (keyName === 'backspace') {
456
+ this.namingModeText = this.namingModeText.slice(0, -1);
457
+ this.buildRunningUI(); // Update UI to show name change
458
+ } else if (keyName && keyName.length === 1 && !keyEvent.ctrl && !keyEvent.meta) {
459
+ this.namingModeText += keyName;
460
+ this.buildRunningUI(); // Update UI to show name change
461
+ }
462
+ }
399
463
  // If in filter mode, handle filter input
400
- if (this.isFilterMode) {
464
+ else if (this.isFilterMode) {
401
465
  const pane = findPaneById(this.paneRoot, this.focusedPaneId);
402
466
  if (keyName === 'escape') {
403
467
  this.isFilterMode = false;
@@ -416,6 +480,7 @@ class ProcessManager {
416
480
  } else {
417
481
  // Normal mode - handle commands
418
482
  if (keyName === 'q') {
483
+ this.savePaneLayout();
419
484
  this.cleanup();
420
485
  this.renderer.destroy();
421
486
  } else if (keyName === '\\') {
@@ -457,6 +522,12 @@ class ProcessManager {
457
522
  this.isFilterMode = true;
458
523
  const pane = findPaneById(this.paneRoot, this.focusedPaneId);
459
524
  if (pane) pane.filter = '';
525
+ } else if (keyName === 'n') {
526
+ // Enter naming mode for focused pane
527
+ this.isNamingMode = true;
528
+ const pane = findPaneById(this.paneRoot, this.focusedPaneId);
529
+ this.namingModeText = pane?.name || '';
530
+ this.buildRunningUI(); // Rebuild to show naming input
460
531
  } else if (keyName === 'escape') {
461
532
  // Clear filter on focused pane
462
533
  const pane = findPaneById(this.paneRoot, this.focusedPaneId);
@@ -491,14 +562,23 @@ class ProcessManager {
491
562
  if (scriptName) {
492
563
  this.toggleProcess(scriptName);
493
564
  }
494
- } else if (keyName === 'c') {
495
- // Open settings
565
+ } else if (keyName === 'o') {
566
+ // Open settings (options)
496
567
  this.previousPhase = 'running';
497
568
  this.phase = 'settings';
498
569
  this.settingsSection = 'ignore';
499
570
  this.settingsIndex = 0;
500
571
  this.buildSettingsUI();
501
572
  return;
573
+ } else if (keyName === 'c') {
574
+ // Cycle color filter on focused pane
575
+ const pane = findPaneById(this.paneRoot, this.focusedPaneId);
576
+ if (pane) {
577
+ const colors = [null, 'red', 'yellow', 'green', 'blue', 'cyan', 'magenta'];
578
+ const currentIndex = colors.indexOf(pane.colorFilter);
579
+ pane.colorFilter = colors[(currentIndex + 1) % colors.length];
580
+ this.buildRunningUI();
581
+ }
502
582
  } else if (keyName === 'tab') {
503
583
  // Navigate to next pane
504
584
  this.navigateToNextPane(1);
@@ -511,14 +591,14 @@ class ProcessManager {
511
591
  }
512
592
  }
513
593
  }
514
-
515
- handleMouse(mouse) {
516
- if (this.phase === 'selection') {
517
- // Left click or scroll wheel click
518
- if (mouse.type === 'mousedown' && (mouse.button === 'left' || mouse.button === 'middle')) {
519
- // Check if click is on a script line
520
- const clickedIndex = this.scriptLinePositions.findIndex(pos => pos === mouse.y);
521
-
594
+
595
+ handleMouse(mouse) {
596
+ if (this.phase === 'selection') {
597
+ // Left click or scroll wheel click
598
+ if (mouse.type === 'mousedown' && (mouse.button === 'left' || mouse.button === 'middle')) {
599
+ // Check if click is on a script line
600
+ const clickedIndex = this.scriptLinePositions.findIndex(pos => pos === mouse.y);
601
+
522
602
  if (clickedIndex !== -1) {
523
603
  const scriptName = this.scripts[clickedIndex]?.name;
524
604
  if (scriptName) {
@@ -535,44 +615,44 @@ class ProcessManager {
535
615
  this.render();
536
616
  }
537
617
  }
538
- } else if (mouse.type === 'wheeldown') {
539
- this.selectedIndex = Math.min(this.scripts.length - 1, this.selectedIndex + 1);
540
- this.render();
541
- } else if (mouse.type === 'wheelup') {
542
- this.selectedIndex = Math.max(0, this.selectedIndex - 1);
543
- this.render();
544
- }
545
- } else if (this.phase === 'running') {
546
- // Mouse support for running phase
547
- if (mouse.type === 'mousedown' && mouse.button === 'left') {
548
- const clickedIndex = this.scriptLinePositions.findIndex(pos => pos === mouse.y);
549
-
550
- if (clickedIndex !== -1) {
551
- this.selectedIndex = clickedIndex;
552
- this.render();
553
- }
554
- } else if (mouse.type === 'wheeldown') {
555
- this.selectedIndex = Math.min(this.scripts.length - 1, this.selectedIndex + 1);
556
- this.render();
557
- } else if (mouse.type === 'wheelup') {
558
- this.selectedIndex = Math.max(0, this.selectedIndex - 1);
559
- this.render();
560
- }
561
- }
562
- }
563
-
564
- startCountdown() {
565
- this.countdownInterval = setInterval(() => {
566
- this.countdown--;
567
- this.render();
568
-
569
- if (this.countdown <= 0) {
570
- clearInterval(this.countdownInterval);
571
- this.startProcesses();
572
- }
573
- }, 1000);
574
- }
575
-
618
+ } else if (mouse.type === 'wheeldown') {
619
+ this.selectedIndex = Math.min(this.scripts.length - 1, this.selectedIndex + 1);
620
+ this.render();
621
+ } else if (mouse.type === 'wheelup') {
622
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
623
+ this.render();
624
+ }
625
+ } else if (this.phase === 'running') {
626
+ // Mouse support for running phase
627
+ if (mouse.type === 'mousedown' && mouse.button === 'left') {
628
+ const clickedIndex = this.scriptLinePositions.findIndex(pos => pos === mouse.y);
629
+
630
+ if (clickedIndex !== -1) {
631
+ this.selectedIndex = clickedIndex;
632
+ this.render();
633
+ }
634
+ } else if (mouse.type === 'wheeldown') {
635
+ this.selectedIndex = Math.min(this.scripts.length - 1, this.selectedIndex + 1);
636
+ this.render();
637
+ } else if (mouse.type === 'wheelup') {
638
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
639
+ this.render();
640
+ }
641
+ }
642
+ }
643
+
644
+ startCountdown() {
645
+ this.countdownInterval = setInterval(() => {
646
+ this.countdown--;
647
+ this.render();
648
+
649
+ if (this.countdown <= 0) {
650
+ clearInterval(this.countdownInterval);
651
+ this.startProcesses();
652
+ }
653
+ }, 1000);
654
+ }
655
+
576
656
  startProcesses() {
577
657
  const selected = Array.from(this.selectedScripts);
578
658
 
@@ -600,20 +680,20 @@ class ProcessManager {
600
680
 
601
681
  this.render();
602
682
  }
603
-
604
- startProcess(scriptName) {
605
- const script = this.scripts.find(s => s.name === scriptName);
606
- if (!script) return;
607
-
608
- const proc = spawn('npm', ['run', scriptName], {
609
- env: {
610
- ...process.env,
611
- FORCE_COLOR: '1',
612
- COLORTERM: 'truecolor',
613
- },
614
- shell: true,
615
- });
616
-
683
+
684
+ startProcess(scriptName) {
685
+ const script = this.scripts.find(s => s.name === scriptName);
686
+ if (!script) return;
687
+
688
+ const proc = spawn('npm', ['run', scriptName], {
689
+ env: {
690
+ ...process.env,
691
+ FORCE_COLOR: '1',
692
+ COLORTERM: 'truecolor',
693
+ },
694
+ shell: true,
695
+ });
696
+
617
697
  proc.stdout.on('data', (data) => {
618
698
  const text = data.toString();
619
699
  const lines = text.split('\n');
@@ -639,11 +719,11 @@ class ProcessManager {
639
719
  this.processes.set(scriptName, { status, exitCode: code });
640
720
  this.addOutputLine(scriptName, `Process exited with code ${code}`);
641
721
  });
642
-
643
- this.processRefs.set(scriptName, proc);
644
- this.processes.set(scriptName, { status: 'running', pid: proc.pid });
645
- }
646
-
722
+
723
+ this.processRefs.set(scriptName, proc);
724
+ this.processes.set(scriptName, { status: 'running', pid: proc.pid });
725
+ }
726
+
647
727
  addOutputLine(processName, text) {
648
728
  // Always store the output line, even when paused
649
729
  this.outputLines.push({
@@ -672,31 +752,31 @@ class ProcessManager {
672
752
  this.render();
673
753
  }, 16);
674
754
  }
675
-
676
- stopProcess(scriptName) {
677
- const proc = this.processRefs.get(scriptName);
678
- if (proc && proc.pid) {
679
- // Use tree-kill to kill the entire process tree
680
- kill(proc.pid, 'SIGTERM', (err) => {
681
- if (err) {
682
- // If SIGTERM fails, try SIGKILL
683
- kill(proc.pid, 'SIGKILL');
684
- }
685
- });
686
- this.processRefs.delete(scriptName);
687
- this.processes.set(scriptName, { status: 'stopped' });
688
- this.addOutputLine(scriptName, 'Process stopped');
689
- }
690
- }
691
-
692
- restartProcess(scriptName) {
693
- this.stopProcess(scriptName);
694
- setTimeout(() => {
695
- this.startProcess(scriptName);
696
- this.render();
697
- }, 100);
698
- }
699
-
755
+
756
+ stopProcess(scriptName) {
757
+ const proc = this.processRefs.get(scriptName);
758
+ if (proc && proc.pid) {
759
+ // Use tree-kill to kill the entire process tree
760
+ kill(proc.pid, 'SIGTERM', (err) => {
761
+ if (err) {
762
+ // If SIGTERM fails, try SIGKILL
763
+ kill(proc.pid, 'SIGKILL');
764
+ }
765
+ });
766
+ this.processRefs.delete(scriptName);
767
+ this.processes.set(scriptName, { status: 'stopped' });
768
+ this.addOutputLine(scriptName, 'Process stopped');
769
+ }
770
+ }
771
+
772
+ restartProcess(scriptName) {
773
+ this.stopProcess(scriptName);
774
+ setTimeout(() => {
775
+ this.startProcess(scriptName);
776
+ this.render();
777
+ }, 100);
778
+ }
779
+
700
780
  toggleProcess(scriptName) {
701
781
  const proc = this.processes.get(scriptName);
702
782
  if (proc?.status === 'running') {
@@ -1135,6 +1215,11 @@ class ProcessManager {
1135
1215
  );
1136
1216
  }
1137
1217
 
1218
+ // Apply color filter (filter lines containing specific ANSI color codes)
1219
+ if (pane.colorFilter) {
1220
+ lines = lines.filter(line => lineHasColor(line.text, pane.colorFilter));
1221
+ }
1222
+
1138
1223
  return lines;
1139
1224
  }
1140
1225
 
@@ -1408,7 +1493,7 @@ class ProcessManager {
1408
1493
  }
1409
1494
  }
1410
1495
  }
1411
-
1496
+
1412
1497
  cleanup() {
1413
1498
  this.destroyed = true;
1414
1499
 
@@ -1428,7 +1513,7 @@ class ProcessManager {
1428
1513
  }
1429
1514
  }
1430
1515
  }
1431
-
1516
+
1432
1517
  buildSelectionUI() {
1433
1518
  // Remove old containers if they exist - use destroyRecursively to clean up all children
1434
1519
  if (this.selectionContainer) {
@@ -1529,6 +1614,15 @@ class ProcessManager {
1529
1614
  leftSide.add(titleText);
1530
1615
  this.headerText = titleText; // Save reference for countdown updates
1531
1616
 
1617
+ // VS Code hint
1618
+ if (IS_VSCODE) {
1619
+ const vscodeHint = new TextRenderable(this.renderer, {
1620
+ id: 'vscode-hint',
1621
+ content: t`${fg(COLORS.textDim)('(vscode)')}`,
1622
+ });
1623
+ leftSide.add(vscodeHint);
1624
+ }
1625
+
1532
1626
  footerBar.add(leftSide);
1533
1627
 
1534
1628
  // Right side: shortcuts
@@ -1541,7 +1635,7 @@ class ProcessManager {
1541
1635
  const shortcuts = [
1542
1636
  { key: 'spc', desc: 'sel', color: COLORS.success },
1543
1637
  { key: 'ret', desc: 'go', color: COLORS.accent },
1544
- { key: 'c', desc: 'cfg', color: COLORS.magenta },
1638
+ { key: 'o', desc: 'cfg', color: COLORS.magenta },
1545
1639
  ];
1546
1640
 
1547
1641
  shortcuts.forEach(({ key, desc, color }) => {
@@ -1557,32 +1651,32 @@ class ProcessManager {
1557
1651
 
1558
1652
  this.renderer.root.add(this.selectionContainer);
1559
1653
  }
1560
-
1654
+
1561
1655
  getHeaderText() {
1562
1656
  return `Starting in ${this.countdown}s...`;
1563
1657
  }
1564
-
1565
- getScriptLineText(script, index) {
1566
- const isSelected = this.selectedScripts.has(script.name);
1567
- const isFocused = index === this.selectedIndex;
1568
- const prefix = isFocused ? '▶' : ' ';
1569
- const checkbox = isSelected ? '✓' : ' ';
1570
- const processColor = this.processColors.get(script.name) || '#FFFFFF';
1571
-
1572
- // Use colored text for script name
1573
- return t`${prefix} [${checkbox}] ${fg(processColor)(script.displayName)}`;
1574
- }
1575
-
1576
- getScriptLineColor(index) {
1577
- // Return base color for the line (prefix will be cyan when focused)
1578
- return index === this.selectedIndex ? '#00FFFF' : '#FFFFFF';
1579
- }
1580
-
1658
+
1659
+ getScriptLineText(script, index) {
1660
+ const isSelected = this.selectedScripts.has(script.name);
1661
+ const isFocused = index === this.selectedIndex;
1662
+ const prefix = isFocused ? '▶' : ' ';
1663
+ const checkbox = isSelected ? '✓' : ' ';
1664
+ const processColor = this.processColors.get(script.name) || '#FFFFFF';
1665
+
1666
+ // Use colored text for script name
1667
+ return t`${prefix} [${checkbox}] ${fg(processColor)(script.displayName)}`;
1668
+ }
1669
+
1670
+ getScriptLineColor(index) {
1671
+ // Return base color for the line (prefix will be cyan when focused)
1672
+ return index === this.selectedIndex ? '#00FFFF' : '#FFFFFF';
1673
+ }
1674
+
1581
1675
  updateSelectionUI() {
1582
1676
  // Rebuild UI each time - simpler and more reliable with the new structure
1583
1677
  this.buildSelectionUI();
1584
1678
  }
1585
-
1679
+
1586
1680
  render() {
1587
1681
  // Don't render if destroyed
1588
1682
  if (this.destroyed) return;
@@ -1598,130 +1692,130 @@ class ProcessManager {
1598
1692
  this.updateRunningUI();
1599
1693
  }
1600
1694
  }
1601
-
1602
- getProcessListContent() {
1603
- // Build process list content dynamically for any number of processes
1604
- let contentString = '';
1605
-
1606
- this.scripts.forEach((script, index) => {
1607
- const proc = this.processes.get(script.name);
1608
- const status = proc?.status || 'stopped';
1609
- const icon = status === 'running' ? '●' : status === 'crashed' ? '✖' : '○';
1610
- const statusColor = status === 'running' ? '#00FF00' : status === 'crashed' ? '#FF0000' : '#666666';
1611
- const processColor = this.processColors.get(script.name) || '#FFFFFF';
1612
- const prefix = this.selectedIndex === index ? '▶' : '';
1613
-
1614
- // Build the colored string for this process
1615
- if (index > 0) contentString += ' ';
1616
- contentString += prefix + script.displayName + ' ' + icon;
1617
- });
1618
-
1619
- return contentString;
1620
- }
1621
-
1622
- updateRunningHeader() {
1623
- // Update only the header and process list without rebuilding everything
1624
- if (!this.headerRenderable || !this.processListRenderable || !this.runningContainer) {
1625
- return;
1626
- }
1627
-
1628
- // Update header (plain text works)
1629
- const selectedScript = this.scripts[this.selectedIndex];
1630
- const selectedName = selectedScript ? selectedScript.displayName : '';
1631
- const pauseIndicator = this.isPaused ? ' [PAUSED]' : '';
1632
- const filterIndicator = this.isFilterMode ? ` [FILTER: ${this.filter}_]` : (this.filter ? ` [FILTER: ${this.filter}]` : '');
1633
- const headerText = `[←→: Navigate | Space: Pause | S: Stop | R: Restart | F: Filter Selected | /: Filter Text | Q: Quit] ${selectedName}${pauseIndicator}${filterIndicator}`;
1634
-
1635
- if (this.headerRenderable.setContent) {
1636
- this.headerRenderable.setContent(headerText);
1637
- }
1638
-
1639
- // For process list with styled text, we need to recreate it
1640
- // Remove old one
1641
- this.runningContainer.remove(this.processListRenderable);
1642
- this.processListRenderable.destroy();
1643
-
1644
- // Create new process list with current selection
1645
- let processContent;
1646
- if (this.scripts.length === 1) {
1647
- const script = this.scripts[0];
1648
- const proc = this.processes.get(script.name);
1649
- const status = proc?.status || 'stopped';
1650
- const statusIcon = status === 'running' ? '●' : status === 'crashed' ? '✖' : '○';
1651
- const statusColor = status === 'running' ? '#00FF00' : status === 'crashed' ? '#FF0000' : '#666666';
1652
- const processColor = this.processColors.get(script.name) || '#FFFFFF';
1653
- processContent = t`▶${fg(processColor)(script.displayName)} ${fg(statusColor)(statusIcon)}`;
1654
- } else if (this.scripts.length === 2) {
1655
- const s0 = this.scripts[0];
1656
- const s1 = this.scripts[1];
1657
- const proc0 = this.processes.get(s0.name);
1658
- const proc1 = this.processes.get(s1.name);
1659
- const status0 = proc0?.status || 'stopped';
1660
- const status1 = proc1?.status || 'stopped';
1661
- const icon0 = status0 === 'running' ? '●' : status0 === 'crashed' ? '✖' : '○';
1662
- const icon1 = status1 === 'running' ? '●' : status1 === 'crashed' ? '✖' : '○';
1663
- const color0 = status0 === 'running' ? '#00FF00' : status0 === 'crashed' ? '#FF0000' : '#666666';
1664
- const color1 = status1 === 'running' ? '#00FF00' : status1 === 'crashed' ? '#FF0000' : '#666666';
1665
- const pcolor0 = this.processColors.get(s0.name) || '#FFFFFF';
1666
- const pcolor1 = this.processColors.get(s1.name) || '#FFFFFF';
1667
- const prefix0 = this.selectedIndex === 0 ? '▶' : '';
1668
- const prefix1 = this.selectedIndex === 1 ? '▶' : '';
1669
- processContent = t`${prefix0}${fg(pcolor0)(s0.displayName)} ${fg(color0)(icon0)} ${prefix1}${fg(pcolor1)(s1.displayName)} ${fg(color1)(icon1)}`;
1670
- } else if (this.scripts.length === 3) {
1671
- const s0 = this.scripts[0];
1672
- const s1 = this.scripts[1];
1673
- const s2 = this.scripts[2];
1674
- const proc0 = this.processes.get(s0.name);
1675
- const proc1 = this.processes.get(s1.name);
1676
- const proc2 = this.processes.get(s2.name);
1677
- const status0 = proc0?.status || 'stopped';
1678
- const status1 = proc1?.status || 'stopped';
1679
- const status2 = proc2?.status || 'stopped';
1680
- const icon0 = status0 === 'running' ? '●' : status0 === 'crashed' ? '✖' : '○';
1681
- const icon1 = status1 === 'running' ? '●' : status1 === 'crashed' ? '✖' : '○';
1682
- const icon2 = status2 === 'running' ? '●' : status2 === 'crashed' ? '✖' : '○';
1683
- const color0 = status0 === 'running' ? '#00FF00' : status0 === 'crashed' ? '#FF0000' : '#666666';
1684
- const color1 = status1 === 'running' ? '#00FF00' : status1 === 'crashed' ? '#FF0000' : '#666666';
1685
- const color2 = status2 === 'running' ? '#00FF00' : status2 === 'crashed' ? '#FF0000' : '#666666';
1686
- const pcolor0 = this.processColors.get(s0.name) || '#FFFFFF';
1687
- const pcolor1 = this.processColors.get(s1.name) || '#FFFFFF';
1688
- const pcolor2 = this.processColors.get(s2.name) || '#FFFFFF';
1689
- const prefix0 = this.selectedIndex === 0 ? '▶' : '';
1690
- const prefix1 = this.selectedIndex === 1 ? '▶' : '';
1691
- const prefix2 = this.selectedIndex === 2 ? '▶' : '';
1692
- processContent = t`${prefix0}${fg(pcolor0)(s0.displayName)} ${fg(color0)(icon0)} ${prefix1}${fg(pcolor1)(s1.displayName)} ${fg(color1)(icon1)} ${prefix2}${fg(pcolor2)(s2.displayName)} ${fg(color2)(icon2)}`;
1693
- } else {
1694
- // 4+ processes - for now hardcode to 4, but should be dynamic
1695
- const parts = this.scripts.slice(0, 4).map((script, idx) => {
1696
- const proc = this.processes.get(script.name);
1697
- const status = proc?.status || 'stopped';
1698
- const icon = status === 'running' ? '●' : status === 'crashed' ? '✖' : '○';
1699
- const color = status === 'running' ? '#00FF00' : status === 'crashed' ? '#FF0000' : '#666666';
1700
- const pcolor = this.processColors.get(script.name) || '#FFFFFF';
1701
- const prefix = this.selectedIndex === idx ? '▶' : '';
1702
- return { prefix, name: script.displayName, icon, color, pcolor };
1703
- });
1704
- processContent = t`${parts[0].prefix}${fg(parts[0].pcolor)(parts[0].name)} ${fg(parts[0].color)(parts[0].icon)} ${parts[1].prefix}${fg(parts[1].pcolor)(parts[1].name)} ${fg(parts[1].color)(parts[1].icon)} ${parts[2].prefix}${fg(parts[2].pcolor)(parts[2].name)} ${fg(parts[2].color)(parts[2].icon)} ${parts[3].prefix}${fg(parts[3].pcolor)(parts[3].name)} ${fg(parts[3].color)(parts[3].icon)}`;
1705
- }
1706
-
1707
- // Create new process list renderable
1708
- this.processListRenderable = new TextRenderable(this.renderer, {
1709
- id: 'process-list',
1710
- content: processContent,
1711
- });
1712
-
1713
- // Insert it back in the right position (after header and spacer)
1714
- // This is tricky - we need to insert at position 2
1715
- // For now, just rebuild the whole UI since we can't easily insert
1716
- this.buildRunningUI();
1717
- }
1718
-
1719
- updateRunningUI() {
1720
- // Just rebuild the entire UI - simpler and more reliable
1721
- // OpenTUI doesn't have great incremental update support anyway
1722
- this.buildRunningUI();
1723
- }
1724
-
1695
+
1696
+ getProcessListContent() {
1697
+ // Build process list content dynamically for any number of processes
1698
+ let contentString = '';
1699
+
1700
+ this.scripts.forEach((script, index) => {
1701
+ const proc = this.processes.get(script.name);
1702
+ const status = proc?.status || 'stopped';
1703
+ const icon = status === 'running' ? '●' : status === 'crashed' ? '✖' : '○';
1704
+ const statusColor = status === 'running' ? '#00FF00' : status === 'crashed' ? '#FF0000' : '#666666';
1705
+ const processColor = this.processColors.get(script.name) || '#FFFFFF';
1706
+ const prefix = this.selectedIndex === index ? '▶' : '';
1707
+
1708
+ // Build the colored string for this process
1709
+ if (index > 0) contentString += ' ';
1710
+ contentString += prefix + script.displayName + ' ' + icon;
1711
+ });
1712
+
1713
+ return contentString;
1714
+ }
1715
+
1716
+ updateRunningHeader() {
1717
+ // Update only the header and process list without rebuilding everything
1718
+ if (!this.headerRenderable || !this.processListRenderable || !this.runningContainer) {
1719
+ return;
1720
+ }
1721
+
1722
+ // Update header (plain text works)
1723
+ const selectedScript = this.scripts[this.selectedIndex];
1724
+ const selectedName = selectedScript ? selectedScript.displayName : '';
1725
+ const pauseIndicator = this.isPaused ? ' [PAUSED]' : '';
1726
+ const filterIndicator = this.isFilterMode ? ` [FILTER: ${this.filter}_]` : (this.filter ? ` [FILTER: ${this.filter}]` : '');
1727
+ const headerText = `[←→: Navigate | Space: Pause | S: Stop | R: Restart | F: Filter Selected | /: Filter Text | Q: Quit] ${selectedName}${pauseIndicator}${filterIndicator}`;
1728
+
1729
+ if (this.headerRenderable.setContent) {
1730
+ this.headerRenderable.setContent(headerText);
1731
+ }
1732
+
1733
+ // For process list with styled text, we need to recreate it
1734
+ // Remove old one
1735
+ this.runningContainer.remove(this.processListRenderable);
1736
+ this.processListRenderable.destroy();
1737
+
1738
+ // Create new process list with current selection
1739
+ let processContent;
1740
+ if (this.scripts.length === 1) {
1741
+ const script = this.scripts[0];
1742
+ const proc = this.processes.get(script.name);
1743
+ const status = proc?.status || 'stopped';
1744
+ const statusIcon = status === 'running' ? '●' : status === 'crashed' ? '✖' : '○';
1745
+ const statusColor = status === 'running' ? '#00FF00' : status === 'crashed' ? '#FF0000' : '#666666';
1746
+ const processColor = this.processColors.get(script.name) || '#FFFFFF';
1747
+ processContent = t`▶${fg(processColor)(script.displayName)} ${fg(statusColor)(statusIcon)}`;
1748
+ } else if (this.scripts.length === 2) {
1749
+ const s0 = this.scripts[0];
1750
+ const s1 = this.scripts[1];
1751
+ const proc0 = this.processes.get(s0.name);
1752
+ const proc1 = this.processes.get(s1.name);
1753
+ const status0 = proc0?.status || 'stopped';
1754
+ const status1 = proc1?.status || 'stopped';
1755
+ const icon0 = status0 === 'running' ? '●' : status0 === 'crashed' ? '✖' : '○';
1756
+ const icon1 = status1 === 'running' ? '●' : status1 === 'crashed' ? '✖' : '○';
1757
+ const color0 = status0 === 'running' ? '#00FF00' : status0 === 'crashed' ? '#FF0000' : '#666666';
1758
+ const color1 = status1 === 'running' ? '#00FF00' : status1 === 'crashed' ? '#FF0000' : '#666666';
1759
+ const pcolor0 = this.processColors.get(s0.name) || '#FFFFFF';
1760
+ const pcolor1 = this.processColors.get(s1.name) || '#FFFFFF';
1761
+ const prefix0 = this.selectedIndex === 0 ? '▶' : '';
1762
+ const prefix1 = this.selectedIndex === 1 ? '▶' : '';
1763
+ processContent = t`${prefix0}${fg(pcolor0)(s0.displayName)} ${fg(color0)(icon0)} ${prefix1}${fg(pcolor1)(s1.displayName)} ${fg(color1)(icon1)}`;
1764
+ } else if (this.scripts.length === 3) {
1765
+ const s0 = this.scripts[0];
1766
+ const s1 = this.scripts[1];
1767
+ const s2 = this.scripts[2];
1768
+ const proc0 = this.processes.get(s0.name);
1769
+ const proc1 = this.processes.get(s1.name);
1770
+ const proc2 = this.processes.get(s2.name);
1771
+ const status0 = proc0?.status || 'stopped';
1772
+ const status1 = proc1?.status || 'stopped';
1773
+ const status2 = proc2?.status || 'stopped';
1774
+ const icon0 = status0 === 'running' ? '●' : status0 === 'crashed' ? '✖' : '○';
1775
+ const icon1 = status1 === 'running' ? '●' : status1 === 'crashed' ? '✖' : '○';
1776
+ const icon2 = status2 === 'running' ? '●' : status2 === 'crashed' ? '✖' : '○';
1777
+ const color0 = status0 === 'running' ? '#00FF00' : status0 === 'crashed' ? '#FF0000' : '#666666';
1778
+ const color1 = status1 === 'running' ? '#00FF00' : status1 === 'crashed' ? '#FF0000' : '#666666';
1779
+ const color2 = status2 === 'running' ? '#00FF00' : status2 === 'crashed' ? '#FF0000' : '#666666';
1780
+ const pcolor0 = this.processColors.get(s0.name) || '#FFFFFF';
1781
+ const pcolor1 = this.processColors.get(s1.name) || '#FFFFFF';
1782
+ const pcolor2 = this.processColors.get(s2.name) || '#FFFFFF';
1783
+ const prefix0 = this.selectedIndex === 0 ? '▶' : '';
1784
+ const prefix1 = this.selectedIndex === 1 ? '▶' : '';
1785
+ const prefix2 = this.selectedIndex === 2 ? '▶' : '';
1786
+ processContent = t`${prefix0}${fg(pcolor0)(s0.displayName)} ${fg(color0)(icon0)} ${prefix1}${fg(pcolor1)(s1.displayName)} ${fg(color1)(icon1)} ${prefix2}${fg(pcolor2)(s2.displayName)} ${fg(color2)(icon2)}`;
1787
+ } else {
1788
+ // 4+ processes - for now hardcode to 4, but should be dynamic
1789
+ const parts = this.scripts.slice(0, 4).map((script, idx) => {
1790
+ const proc = this.processes.get(script.name);
1791
+ const status = proc?.status || 'stopped';
1792
+ const icon = status === 'running' ? '●' : status === 'crashed' ? '✖' : '○';
1793
+ const color = status === 'running' ? '#00FF00' : status === 'crashed' ? '#FF0000' : '#666666';
1794
+ const pcolor = this.processColors.get(script.name) || '#FFFFFF';
1795
+ const prefix = this.selectedIndex === idx ? '▶' : '';
1796
+ return { prefix, name: script.displayName, icon, color, pcolor };
1797
+ });
1798
+ processContent = t`${parts[0].prefix}${fg(parts[0].pcolor)(parts[0].name)} ${fg(parts[0].color)(parts[0].icon)} ${parts[1].prefix}${fg(parts[1].pcolor)(parts[1].name)} ${fg(parts[1].color)(parts[1].icon)} ${parts[2].prefix}${fg(parts[2].pcolor)(parts[2].name)} ${fg(parts[2].color)(parts[2].icon)} ${parts[3].prefix}${fg(parts[3].pcolor)(parts[3].name)} ${fg(parts[3].color)(parts[3].icon)}`;
1799
+ }
1800
+
1801
+ // Create new process list renderable
1802
+ this.processListRenderable = new TextRenderable(this.renderer, {
1803
+ id: 'process-list',
1804
+ content: processContent,
1805
+ });
1806
+
1807
+ // Insert it back in the right position (after header and spacer)
1808
+ // This is tricky - we need to insert at position 2
1809
+ // For now, just rebuild the whole UI since we can't easily insert
1810
+ this.buildRunningUI();
1811
+ }
1812
+
1813
+ updateRunningUI() {
1814
+ // Just rebuild the entire UI - simpler and more reliable
1815
+ // OpenTUI doesn't have great incremental update support anyway
1816
+ this.buildRunningUI();
1817
+ }
1818
+
1725
1819
  // Build a single pane's output area
1726
1820
  buildPaneOutput(pane, container, height) {
1727
1821
  const isFocused = pane.id === this.focusedPaneId;
@@ -1731,6 +1825,10 @@ class ProcessManager {
1731
1825
  const outputHeight = Math.max(3, height - 2);
1732
1826
  let linesToShow = this.isPaused ? lines : lines.slice(-outputHeight);
1733
1827
 
1828
+ // Use full terminal width for padding - simpler and ensures complete clearing
1829
+ // Each pane will pad to full width, excess will be clipped by container
1830
+ const approxPaneWidth = this.renderer.width;
1831
+
1734
1832
  // Add lines in reverse order (newest first)
1735
1833
  for (let i = linesToShow.length - 1; i >= 0; i--) {
1736
1834
  const line = linesToShow[i];
@@ -1768,12 +1866,47 @@ class ProcessManager {
1768
1866
  truncatedText = result + '\x1b[0m...';
1769
1867
  }
1770
1868
 
1869
+ // Get visible length of current line and pad to fill width
1870
+ const linePrefix = `[${line.process}] `;
1871
+ const currentLength = linePrefix.length + stripAnsi(truncatedText).length;
1872
+
1873
+ // Pad with spaces to fill the width
1874
+ const paddingNeeded = Math.max(0, approxPaneWidth - currentLength);
1875
+ const padding = ' '.repeat(paddingNeeded);
1876
+
1771
1877
  const outputLine = new TextRenderable(this.renderer, {
1772
1878
  id: `output-${pane.id}-${i}`,
1773
- content: t`${fg(processColor)(`[${line.process}]`)} ${truncatedText}`,
1879
+ content: t`${fg(processColor)(`[${line.process}]`)} ${truncatedText}${padding}`,
1880
+ bg: '#000000', // Black background for pane content
1774
1881
  });
1882
+
1775
1883
  container.add(outputLine);
1776
1884
  }
1885
+
1886
+ // Fill remaining vertical space with blank lines
1887
+ const emptyLinesNeeded = Math.max(0, outputHeight - linesToShow.length);
1888
+ for (let j = 0; j < emptyLinesNeeded; j++) {
1889
+ const emptyLine = new TextRenderable(this.renderer, {
1890
+ id: `empty-${pane.id}-${j}`,
1891
+ content: ' '.repeat(approxPaneWidth), // Fill entire width with spaces
1892
+ bg: '#000000', // Black background for empty lines
1893
+ });
1894
+
1895
+ container.add(emptyLine);
1896
+ }
1897
+ }
1898
+
1899
+ // Count how many vertical panes exist (for width calculation)
1900
+ countVerticalPanes(node) {
1901
+ if (!node) return 1;
1902
+ if (node.type === 'pane') return 1;
1903
+ if (node.direction === 'vertical') {
1904
+ // Vertical split means panes side by side
1905
+ return node.children.reduce((sum, child) => sum + this.countVerticalPanes(child), 0);
1906
+ } else {
1907
+ // Horizontal split means panes stacked, count the max
1908
+ return Math.max(...node.children.map(child => this.countVerticalPanes(child)));
1909
+ }
1777
1910
  }
1778
1911
 
1779
1912
  // Build a pane panel with title bar
@@ -1781,21 +1914,25 @@ class ProcessManager {
1781
1914
  const isFocused = pane.id === this.focusedPaneId;
1782
1915
  const borderColor = isFocused ? COLORS.borderFocused : COLORS.border;
1783
1916
 
1784
- // Title shows assigned processes or "All", plus filter and hidden count
1785
- const processLabel = pane.processes.length > 0
1917
+ // Title shows custom name (if set), or assigned processes or "All", plus filter and hidden count
1918
+ const customName = pane.name ? pane.name : null;
1919
+ const processLabel = customName || (pane.processes.length > 0
1786
1920
  ? pane.processes.join(', ')
1787
- : 'All';
1921
+ : 'All');
1788
1922
  const focusLabel = isFocused ? '*' : '';
1789
1923
  const hiddenCount = pane.hidden?.length || 0;
1790
1924
  const hiddenLabel = hiddenCount > 0 ? ` -${hiddenCount}` : '';
1791
1925
  const filterLabel = pane.filter ? ` /${pane.filter}` : '';
1926
+ const namingInputLabel = (isFocused && this.isNamingMode) ? `Name: ${this.namingModeText}_` : '';
1792
1927
  const filterInputLabel = (isFocused && this.isFilterMode) ? `/${pane.filter || ''}_` : '';
1793
- const title = ` ${focusLabel}${processLabel}${hiddenLabel}${filterInputLabel || filterLabel} `;
1928
+ const title = ` ${focusLabel}${namingInputLabel || processLabel}${hiddenLabel}${filterInputLabel || filterLabel} `;
1794
1929
 
1795
1930
  const paneContainer = new BoxRenderable(this.renderer, {
1796
1931
  id: `pane-${pane.id}`,
1797
1932
  flexDirection: 'column',
1798
1933
  flexGrow: flexGrow,
1934
+ flexShrink: 0, // Prevent shrinking - maintain 50/50 split
1935
+ flexBasis: 0, // Use flexGrow ratio for sizing, not content size
1799
1936
  border: true,
1800
1937
  borderStyle: 'rounded',
1801
1938
  borderColor: borderColor,
@@ -1803,19 +1940,24 @@ class ProcessManager {
1803
1940
  titleAlignment: 'left',
1804
1941
  padding: 0,
1805
1942
  overflow: 'hidden',
1943
+ backgroundColor: '#000000', // Black background for pane container
1806
1944
  });
1807
1945
 
1808
- // Output content - always use BoxRenderable for consistent sizing
1946
+ // Output content - use BoxRenderable that fills remaining space
1947
+ // Use passed height or calculate default for line count calculation
1948
+ const height = availableHeight ? Math.max(5, availableHeight - 2) : Math.max(5, this.renderer.height - 6);
1949
+
1809
1950
  const outputBox = new BoxRenderable(this.renderer, {
1810
1951
  id: `pane-output-${pane.id}`,
1811
1952
  flexDirection: 'column',
1812
1953
  flexGrow: 1,
1954
+ flexShrink: 1,
1955
+ flexBasis: 0,
1813
1956
  overflow: 'hidden',
1814
1957
  paddingLeft: 1,
1958
+ backgroundColor: '#000000', // Black background for pane
1815
1959
  });
1816
1960
 
1817
- // Use passed height or calculate default
1818
- const height = availableHeight ? Math.max(5, availableHeight - 2) : Math.max(5, this.renderer.height - 6);
1819
1961
  this.buildPaneOutput(pane, outputBox, height);
1820
1962
 
1821
1963
  paneContainer.add(outputBox);
@@ -1840,6 +1982,8 @@ class ProcessManager {
1840
1982
  id: `split-${node.direction}`,
1841
1983
  flexDirection: node.direction === 'vertical' ? 'row' : 'column',
1842
1984
  flexGrow: flexGrow,
1985
+ flexShrink: 0,
1986
+ flexBasis: 0,
1843
1987
  gap: 0,
1844
1988
  });
1845
1989
 
@@ -1931,13 +2075,13 @@ class ProcessManager {
1931
2075
  // Clear outputBox reference since it was destroyed with runningContainer
1932
2076
  this.outputBox = null;
1933
2077
 
1934
- // Create main container - full screen with dark background
2078
+ // Create main container - full screen with black background
1935
2079
  const mainContainer = new BoxRenderable(this.renderer, {
1936
2080
  id: 'running-container',
1937
2081
  flexDirection: 'column',
1938
2082
  width: '100%',
1939
2083
  height: '100%',
1940
- backgroundColor: COLORS.bg,
2084
+ backgroundColor: '#000000',
1941
2085
  });
1942
2086
 
1943
2087
  // Process tabs at top
@@ -1989,7 +2133,9 @@ class ProcessManager {
1989
2133
  id: 'pane-area',
1990
2134
  flexDirection: 'column',
1991
2135
  flexGrow: 1,
1992
- backgroundColor: COLORS.bg,
2136
+ flexShrink: 0,
2137
+ flexBasis: 0,
2138
+ backgroundColor: '#000000',
1993
2139
  });
1994
2140
 
1995
2141
  const paneLayout = this.buildPaneLayout(this.paneRoot);
@@ -2026,6 +2172,15 @@ class ProcessManager {
2026
2172
  });
2027
2173
  leftSide.add(statusIndicator);
2028
2174
 
2175
+ // VS Code hint
2176
+ if (IS_VSCODE) {
2177
+ const vscodeHint = new TextRenderable(this.renderer, {
2178
+ id: 'vscode-hint',
2179
+ content: t`${fg(COLORS.textDim)('(vscode)')}`,
2180
+ });
2181
+ leftSide.add(vscodeHint);
2182
+ }
2183
+
2029
2184
  // Filter indicator if active
2030
2185
  if (this.filter || this.isFilterMode) {
2031
2186
  const filterText = this.isFilterMode ? `/${this.filter}_` : `/${this.filter}`;
@@ -2036,6 +2191,23 @@ class ProcessManager {
2036
2191
  leftSide.add(filterIndicator);
2037
2192
  }
2038
2193
 
2194
+ // Color filter indicator if active on focused pane
2195
+ if (focusedPane?.colorFilter) {
2196
+ const colorMap = {
2197
+ red: COLORS.error,
2198
+ yellow: COLORS.warning,
2199
+ green: COLORS.success,
2200
+ blue: COLORS.accent,
2201
+ cyan: COLORS.cyan,
2202
+ magenta: COLORS.magenta,
2203
+ };
2204
+ const colorIndicator = new TextRenderable(this.renderer, {
2205
+ id: 'color-filter-indicator',
2206
+ content: t`${fg(colorMap[focusedPane.colorFilter] || COLORS.text)(`[${focusedPane.colorFilter}]`)}`,
2207
+ });
2208
+ leftSide.add(colorIndicator);
2209
+ }
2210
+
2039
2211
  footerBar.add(leftSide);
2040
2212
 
2041
2213
  // Right side: shortcuts and title
@@ -2048,8 +2220,10 @@ class ProcessManager {
2048
2220
  const shortcuts = [
2049
2221
  { key: '\\', desc: 'panes', color: COLORS.cyan },
2050
2222
  { key: 'spc', desc: 'toggle', color: COLORS.success },
2223
+ { key: 'n', desc: 'name', color: COLORS.accent },
2051
2224
  { key: 'p', desc: 'pause', color: COLORS.warning },
2052
2225
  { key: '/', desc: 'filter', color: COLORS.cyan },
2226
+ { key: 'c', desc: 'color', color: COLORS.magenta },
2053
2227
  { key: 's', desc: 'stop', color: COLORS.error },
2054
2228
  { key: 'r', desc: 'restart', color: COLORS.success },
2055
2229
  { key: 'q', desc: 'quit', color: COLORS.error },
@@ -2082,27 +2256,27 @@ class ProcessManager {
2082
2256
  this.runningContainer = mainContainer;
2083
2257
  }
2084
2258
  }
2085
-
2086
- // Main
2087
- async function main() {
2088
- const cwd = process.cwd();
2089
- const packageJsonPath = join(cwd, 'package.json');
2090
-
2091
- if (!existsSync(packageJsonPath)) {
2092
- console.error(`Error: No package.json found in ${cwd}`);
2093
- process.exit(1);
2094
- }
2095
-
2096
- const scripts = parseNpmScripts(packageJsonPath);
2097
-
2098
- if (scripts.length === 0) {
2099
- console.error('No npm scripts found in package.json');
2100
- process.exit(1);
2101
- }
2102
-
2103
- const renderer = await createCliRenderer();
2104
- const manager = new ProcessManager(renderer, scripts);
2105
-
2259
+
2260
+ // Main
2261
+ async function main() {
2262
+ const cwd = process.cwd();
2263
+ const packageJsonPath = join(cwd, 'package.json');
2264
+
2265
+ if (!existsSync(packageJsonPath)) {
2266
+ console.error(`Error: No package.json found in ${cwd}`);
2267
+ process.exit(1);
2268
+ }
2269
+
2270
+ const scripts = parseNpmScripts(packageJsonPath);
2271
+
2272
+ if (scripts.length === 0) {
2273
+ console.error('No npm scripts found in package.json');
2274
+ process.exit(1);
2275
+ }
2276
+
2277
+ const renderer = await createCliRenderer();
2278
+ const manager = new ProcessManager(renderer, scripts);
2279
+
2106
2280
  // Handle cleanup on exit
2107
2281
  const handleExit = () => {
2108
2282
  manager.cleanup();
@@ -2112,8 +2286,8 @@ async function main() {
2112
2286
  process.on('SIGINT', handleExit);
2113
2287
  process.on('SIGTERM', handleExit);
2114
2288
  }
2115
-
2116
- main().catch(err => {
2117
- console.error('Error:', err);
2118
- process.exit(1);
2119
- });
2289
+
2290
+ main().catch(err => {
2291
+ console.error('Error:', err);
2292
+ process.exit(1);
2293
+ });