startall 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +106 -45
  2. package/index.js +744 -129
  3. package/package.json +3 -2
  4. package/screenshot.png +0 -0
package/index.js CHANGED
@@ -10,11 +10,32 @@ import stripAnsi from 'strip-ansi';
10
10
  // Configuration
11
11
  const CONFIG_FILE = process.argv[2] || 'startall.json';
12
12
  const COUNTDOWN_SECONDS = 10;
13
- const APP_VERSION = 'v0.0.4';
13
+
14
+ // Read version from package.json
15
+ function getAppVersion() {
16
+ try {
17
+ const packagePath = new URL('./package.json', import.meta.url);
18
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
19
+ return `v${pkg.version}`;
20
+ } catch (error) {
21
+ return 'v0.0.0'; // Fallback if package.json can't be read
22
+ }
23
+ }
24
+ const APP_VERSION = getAppVersion();
14
25
 
15
26
  // Detect if running inside VS Code's integrated terminal
16
27
  const IS_VSCODE = process.env.TERM_PROGRAM === 'vscode';
17
28
 
29
+ // VSCode-specific optimizations
30
+ const VSCODE_CONFIG = {
31
+ // VSCode terminal has better mouse support
32
+ enhancedMouse: IS_VSCODE,
33
+ // VSCode can detect and linkify file paths (file:///path/to/file.js:line:col)
34
+ fileLinking: IS_VSCODE,
35
+ // Some key combinations are captured by VSCode
36
+ remapKeys: IS_VSCODE,
37
+ };
38
+
18
39
  // Pane ID generator
19
40
  let paneIdCounter = 0;
20
41
  function generatePaneId() {
@@ -297,10 +318,10 @@ function loadConfig() {
297
318
  try {
298
319
  return JSON.parse(readFileSync(CONFIG_FILE, 'utf8'));
299
320
  } catch {
300
- return { defaultSelection: [], ignore: [] };
321
+ return { defaultSelection: [], ignore: [], shortcuts: {} };
301
322
  }
302
323
  }
303
- return { defaultSelection: [], ignore: [] };
324
+ return { defaultSelection: [], ignore: [], shortcuts: {} };
304
325
  }
305
326
 
306
327
  // Save config
@@ -337,14 +358,27 @@ class ProcessManager {
337
358
  this.isFilterMode = false; // Whether in filter input mode
338
359
  this.isNamingMode = false; // Whether in pane naming input mode
339
360
  this.namingModeText = ''; // Text being typed for pane name
361
+ this.showLineNumbers = this.config.showLineNumbers !== undefined ? this.config.showLineNumbers : true; // Whether to show line numbers
362
+ this.showTimestamps = this.config.showTimestamps !== undefined ? this.config.showTimestamps : false; // Whether to show timestamps
363
+ this.isInputMode = false; // Whether in stdin input mode
364
+ this.inputModeText = ''; // Text being typed for stdin
340
365
 
341
366
  // Settings menu state
342
- this.settingsSection = 'ignore'; // 'ignore' | 'include' | 'scripts'
367
+ this.settingsSection = 'display'; // 'display' | 'ignore' | 'include' | 'scripts' | 'shortcuts'
343
368
  this.settingsIndex = 0; // Current selection index within section
344
369
  this.isAddingPattern = false; // Whether typing a new pattern
345
370
  this.newPatternText = ''; // Text being typed for new pattern
371
+ this.isAssigningShortcut = false; // Whether waiting for a key to assign as shortcut
372
+ this.shortcutScriptName = ''; // Script name being assigned a shortcut
346
373
  this.settingsContainer = null; // UI reference
347
374
  this.previousPhase = 'selection'; // Track where we came from
375
+
376
+ // Command execution overlay state
377
+ this.showCommandOverlay = false; // Whether command output overlay is visible
378
+ this.commandOverlayOutput = []; // Output lines from command
379
+ this.commandOverlayScript = ''; // Script name being executed
380
+ this.commandOverlayStatus = 'running'; // 'running' | 'exited' | 'crashed'
381
+ this.commandOverlayProcess = null; // Process reference
348
382
  this.outputBox = null; // Reference to the output container
349
383
  this.destroyed = false; // Flag to prevent operations after cleanup
350
384
  this.lastRenderedLineCount = 0; // Track how many lines we've rendered
@@ -427,23 +461,73 @@ class ProcessManager {
427
461
  clearInterval(this.countdownInterval);
428
462
  this.previousPhase = 'selection';
429
463
  this.phase = 'settings';
430
- this.settingsSection = 'ignore';
464
+ this.settingsSection = 'display';
431
465
  this.settingsIndex = 0;
432
466
  this.buildSettingsUI();
433
467
  return;
468
+ } else if (keyName >= '1' && keyName <= '9') {
469
+ // Toggle script by number (1-9)
470
+ const index = parseInt(keyName) - 1;
471
+ if (index >= 0 && index < this.scripts.length) {
472
+ const scriptName = this.scripts[index]?.name;
473
+ if (scriptName) {
474
+ if (this.selectedScripts.has(scriptName)) {
475
+ this.selectedScripts.delete(scriptName);
476
+ } else {
477
+ this.selectedScripts.add(scriptName);
478
+ }
479
+ // Reset countdown when selection changes
480
+ this.countdown = COUNTDOWN_SECONDS;
481
+ }
482
+ }
434
483
  }
435
484
  } else if (this.phase === 'settings') {
436
485
  this.handleSettingsInput(keyName, keyEvent);
437
486
  return;
438
487
  } else if (this.phase === 'running') {
488
+ // Handle command overlay
489
+ if (this.showCommandOverlay) {
490
+ if (keyName === 'escape') {
491
+ this.closeCommandOverlay();
492
+ this.buildRunningUI();
493
+ }
494
+ return;
495
+ }
496
+
439
497
  // Handle split menu
440
498
  if (this.showSplitMenu) {
441
499
  this.handleSplitMenuInput(keyName, keyEvent);
442
500
  return;
443
501
  }
444
502
 
503
+ // If in input mode (stdin), handle stdin input
504
+ if (this.isInputMode) {
505
+ const scriptName = this.scripts[this.selectedIndex]?.name;
506
+ if (keyName === 'escape') {
507
+ this.isInputMode = false;
508
+ this.inputModeText = '';
509
+ this.buildRunningUI();
510
+ } else if (keyName === 'enter' || keyName === 'return') {
511
+ // Send the input to the selected process
512
+ if (scriptName && this.inputModeText.trim()) {
513
+ this.sendInputToProcess(scriptName, this.inputModeText + '\n');
514
+ }
515
+ this.isInputMode = false;
516
+ this.inputModeText = '';
517
+ this.buildRunningUI();
518
+ } else if (keyName === 'backspace') {
519
+ this.inputModeText = this.inputModeText.slice(0, -1);
520
+ this.buildRunningUI();
521
+ } else if (keyName === 'space') {
522
+ this.inputModeText += ' ';
523
+ this.buildRunningUI();
524
+ } else if (keyName && keyName.length === 1 && !keyEvent.ctrl && !keyEvent.meta) {
525
+ this.inputModeText += keyName;
526
+ this.buildRunningUI();
527
+ }
528
+ }
445
529
  // If in naming mode, handle name input
446
- if (this.isNamingMode) {
530
+ else if (this.isNamingMode) {
447
531
  const pane = findPaneById(this.paneRoot, this.focusedPaneId);
448
532
  if (keyName === 'escape') {
449
533
  this.isNamingMode = false;
@@ -549,12 +633,15 @@ class ProcessManager {
549
633
  this.selectedIndex = Math.min(this.scripts.length - 1, this.selectedIndex + 1);
550
634
  this.buildRunningUI(); // Rebuild to show selection change
551
635
  } else if (keyName === 'left' || keyName === 'h') {
552
- // Navigate processes left
553
- this.selectedIndex = Math.max(0, this.selectedIndex - 1);
636
+ // Navigate processes left with wrapping
637
+ this.selectedIndex = this.selectedIndex - 1;
638
+ if (this.selectedIndex < 0) {
639
+ this.selectedIndex = this.scripts.length - 1;
640
+ }
554
641
  this.buildRunningUI(); // Rebuild to show selection change
555
642
  } else if (keyName === 'right' || keyName === 'l') {
556
- // Navigate processes right
557
- this.selectedIndex = Math.min(this.scripts.length - 1, this.selectedIndex + 1);
643
+ // Navigate processes right with wrapping
644
+ this.selectedIndex = (this.selectedIndex + 1) % this.scripts.length;
558
645
  this.buildRunningUI(); // Rebuild to show selection change
559
646
  } else if (keyName === 'r') {
560
647
  const scriptName = this.scripts[this.selectedIndex]?.name;
@@ -568,13 +655,13 @@ class ProcessManager {
568
655
  this.toggleProcess(scriptName);
569
656
  }
570
657
  } else if (keyName === 'o') {
571
- // Open settings (options)
572
- this.previousPhase = 'running';
573
- this.phase = 'settings';
574
- this.settingsSection = 'ignore';
575
- this.settingsIndex = 0;
576
- this.buildSettingsUI();
577
- return;
658
+ // Open settings (options)
659
+ this.previousPhase = 'running';
660
+ this.phase = 'settings';
661
+ this.settingsSection = 'display';
662
+ this.settingsIndex = 0;
663
+ this.buildSettingsUI();
664
+ return;
578
665
  } else if (keyName === 'c') {
579
666
  // Cycle color filter on focused pane
580
667
  const pane = findPaneById(this.paneRoot, this.focusedPaneId);
@@ -592,6 +679,46 @@ class ProcessManager {
592
679
  // Navigate to previous pane
593
680
  this.navigateToNextPane(-1);
594
681
  this.buildRunningUI();
682
+ } else if (keyName === 'home') {
683
+ // Scroll to top of focused pane
684
+ this.scrollFocusedPane('home');
685
+ } else if (keyName === 'end') {
686
+ // Scroll to bottom of focused pane
687
+ this.scrollFocusedPane('end');
688
+ } else if (keyName === 'pageup') {
689
+ // Scroll up one page in focused pane
690
+ this.scrollFocusedPane('pageup');
691
+ } else if (keyName === 'pagedown') {
692
+ // Scroll down one page in focused pane
693
+ this.scrollFocusedPane('pagedown');
694
+ } else if (keyName >= '1' && keyName <= '9') {
695
+ // Toggle process by number (1-9)
696
+ const index = parseInt(keyName) - 1;
697
+ if (index >= 0 && index < this.scripts.length) {
698
+ this.selectedIndex = index;
699
+ this.toggleProcessVisibility();
700
+ this.buildRunningUI();
701
+ }
702
+ } else if (keyName === 'i') {
703
+ // Enter input mode to send stdin to selected process
704
+ const scriptName = this.scripts[this.selectedIndex]?.name;
705
+ const proc = this.processes.get(scriptName);
706
+ if (scriptName && proc?.status === 'running') {
707
+ this.isInputMode = true;
708
+ this.inputModeText = '';
709
+ this.buildRunningUI();
710
+ }
711
+ } else if (keyName && keyName.length === 1 && !keyEvent.ctrl && !keyEvent.meta && !keyEvent.shift) {
712
+ // Check if this key is a custom shortcut
713
+ const shortcuts = this.config.shortcuts || {};
714
+ const scriptName = shortcuts[keyName];
715
+ if (scriptName) {
716
+ // Find the script in allScripts (since it might be ignored/filtered out)
717
+ const script = this.allScripts.find(s => s.name === scriptName);
718
+ if (script) {
719
+ this.executeCommand(scriptName);
720
+ }
721
+ }
595
722
  }
596
723
  }
597
724
  }
@@ -788,8 +915,55 @@ class ProcessManager {
788
915
  this.startProcess(scriptName);
789
916
  }
790
917
  }
918
+
919
+ sendInputToProcess(scriptName, input) {
920
+ const proc = this.processRefs.get(scriptName);
921
+ if (proc && proc.stdin && proc.stdin.writable) {
922
+ try {
923
+ proc.stdin.write(input);
924
+ // Echo the input in the output for visibility
925
+ this.addOutputLine(scriptName, `> ${input.trim()}`);
926
+ } catch (err) {
927
+ this.addOutputLine(scriptName, `Error sending input: ${err.message}`);
928
+ }
929
+ }
930
+ }
791
931
 
792
932
  handleSettingsInput(keyName, keyEvent) {
933
+ // Handle shortcut assignment mode
934
+ if (this.isAssigningShortcut) {
935
+ if (keyName === 'escape') {
936
+ this.isAssigningShortcut = false;
937
+ this.shortcutScriptName = '';
938
+ this.buildSettingsUI();
939
+ return;
940
+ } else if (keyName && keyName.length === 1 && !keyEvent.ctrl && !keyEvent.meta && !keyEvent.shift) {
941
+ // Assign this key to the script
942
+ if (!this.config.shortcuts) this.config.shortcuts = {};
943
+
944
+ // Remove any existing shortcut for this script (one key per script)
945
+ for (const [key, scriptName] of Object.entries(this.config.shortcuts)) {
946
+ if (scriptName === this.shortcutScriptName) {
947
+ delete this.config.shortcuts[key];
948
+ }
949
+ }
950
+
951
+ // Also remove this key if it's assigned to another script (one script per key)
952
+ if (this.config.shortcuts[keyName]) {
953
+ delete this.config.shortcuts[keyName];
954
+ }
955
+
956
+ // Assign the new shortcut
957
+ this.config.shortcuts[keyName] = this.shortcutScriptName;
958
+ saveConfig(this.config);
959
+ this.isAssigningShortcut = false;
960
+ this.shortcutScriptName = '';
961
+ this.buildSettingsUI();
962
+ return;
963
+ }
964
+ return;
965
+ }
966
+
793
967
  // Handle text input mode for adding patterns
794
968
  if (this.isAddingPattern) {
795
969
  if (keyName === 'escape') {
@@ -824,6 +998,14 @@ class ProcessManager {
824
998
 
825
999
  // Normal settings navigation
826
1000
  if (keyName === 'escape' || keyName === 'q') {
1001
+ // Apply filters before returning (updates this.scripts)
1002
+ this.applyFilters();
1003
+
1004
+ // Ensure selectedIndex is within bounds after filter changes
1005
+ if (this.selectedIndex >= this.scripts.length) {
1006
+ this.selectedIndex = Math.max(0, this.scripts.length - 1);
1007
+ }
1008
+
827
1009
  // Return to previous phase
828
1010
  if (this.previousPhase === 'running') {
829
1011
  this.phase = 'running';
@@ -836,46 +1018,78 @@ class ProcessManager {
836
1018
  }
837
1019
  } else if (keyName === 'tab' || keyName === 'right') {
838
1020
  // Switch section
839
- const sections = ['ignore', 'include', 'scripts'];
1021
+ const sections = ['display', 'ignore', 'include', 'shortcuts', 'scripts'];
840
1022
  const idx = sections.indexOf(this.settingsSection);
841
1023
  this.settingsSection = sections[(idx + 1) % sections.length];
842
1024
  this.settingsIndex = 0;
843
1025
  this.buildSettingsUI();
844
1026
  } else if (keyEvent.shift && keyName === 'tab') {
845
1027
  // Switch section backwards
846
- const sections = ['ignore', 'include', 'scripts'];
1028
+ const sections = ['display', 'ignore', 'include', 'shortcuts', 'scripts'];
847
1029
  const idx = sections.indexOf(this.settingsSection);
848
1030
  this.settingsSection = sections[(idx - 1 + sections.length) % sections.length];
849
1031
  this.settingsIndex = 0;
850
1032
  this.buildSettingsUI();
851
1033
  } else if (keyName === 'left') {
852
1034
  // Switch section backwards
853
- const sections = ['ignore', 'include', 'scripts'];
1035
+ const sections = ['display', 'ignore', 'include', 'shortcuts', 'scripts'];
854
1036
  const idx = sections.indexOf(this.settingsSection);
855
1037
  this.settingsSection = sections[(idx - 1 + sections.length) % sections.length];
856
1038
  this.settingsIndex = 0;
857
1039
  this.buildSettingsUI();
858
1040
  } else if (keyName === 'up') {
859
- this.settingsIndex = Math.max(0, this.settingsIndex - 1);
860
- this.buildSettingsUI();
1041
+ if (this.settingsIndex > 0) {
1042
+ this.settingsIndex--;
1043
+ this.buildSettingsUI();
1044
+ } else {
1045
+ // Move to previous section
1046
+ const sections = ['display', 'ignore', 'include', 'shortcuts', 'scripts'];
1047
+ const idx = sections.indexOf(this.settingsSection);
1048
+ if (idx > 0) {
1049
+ this.settingsSection = sections[idx - 1];
1050
+ this.settingsIndex = this.getSettingsMaxIndex();
1051
+ this.buildSettingsUI();
1052
+ }
1053
+ }
861
1054
  } else if (keyName === 'down') {
862
1055
  const maxIndex = this.getSettingsMaxIndex();
863
- this.settingsIndex = Math.min(maxIndex, this.settingsIndex + 1);
864
- this.buildSettingsUI();
865
- } else if (keyName === 'a') {
866
- // Add new pattern (only for ignore/include sections)
867
- if (this.settingsSection === 'ignore' || this.settingsSection === 'include') {
868
- this.isAddingPattern = true;
869
- this.newPatternText = '';
1056
+ if (this.settingsIndex < maxIndex) {
1057
+ this.settingsIndex++;
870
1058
  this.buildSettingsUI();
1059
+ } else {
1060
+ // Move to next section
1061
+ const sections = ['display', 'ignore', 'include', 'shortcuts', 'scripts'];
1062
+ const idx = sections.indexOf(this.settingsSection);
1063
+ if (idx < sections.length - 1) {
1064
+ this.settingsSection = sections[idx + 1];
1065
+ this.settingsIndex = 0;
1066
+ this.buildSettingsUI();
1067
+ }
871
1068
  }
1069
+ } else if (keyName === 'i') {
1070
+ // Add new ignore pattern
1071
+ this.settingsSection = 'ignore';
1072
+ this.isAddingPattern = true;
1073
+ this.newPatternText = '';
1074
+ this.buildSettingsUI();
1075
+ } else if (keyName === 'n') {
1076
+ // Add new include pattern
1077
+ this.settingsSection = 'include';
1078
+ this.isAddingPattern = true;
1079
+ this.newPatternText = '';
1080
+ this.buildSettingsUI();
872
1081
  } else if (keyName === 'd' || keyName === 'backspace') {
873
- // Delete selected pattern or toggle script ignore
1082
+ // Delete selected pattern or shortcut
874
1083
  this.deleteSelectedItem();
875
1084
  this.buildSettingsUI();
876
1085
  } else if (keyName === 'space' || keyName === 'enter' || keyName === 'return') {
877
- // Toggle for scripts section
878
- if (this.settingsSection === 'scripts') {
1086
+ // Toggle display options, script visibility, or assign shortcut
1087
+ if (this.settingsSection === 'display') {
1088
+ this.toggleDisplayOption();
1089
+ this.buildSettingsUI();
1090
+ } else if (this.settingsSection === 'shortcuts') {
1091
+ this.assignShortcut();
1092
+ } else if (this.settingsSection === 'scripts') {
879
1093
  this.toggleScriptIgnore();
880
1094
  this.buildSettingsUI();
881
1095
  }
@@ -883,16 +1097,33 @@ class ProcessManager {
883
1097
  }
884
1098
 
885
1099
  getSettingsMaxIndex() {
886
- if (this.settingsSection === 'ignore') {
887
- return Math.max(0, (this.config.ignore?.length || 0) - 1);
1100
+ if (this.settingsSection === 'display') {
1101
+ return 1; // 2 display options (line numbers, timestamps)
1102
+ } else if (this.settingsSection === 'ignore') {
1103
+ const count = this.config.ignore?.length || 0;
1104
+ return count > 0 ? count - 1 : 0;
888
1105
  } else if (this.settingsSection === 'include') {
889
- return Math.max(0, (this.config.include?.length || 0) - 1);
1106
+ const count = this.config.include?.length || 0;
1107
+ return count > 0 ? count - 1 : 0;
1108
+ } else if (this.settingsSection === 'shortcuts') {
1109
+ return Math.max(0, this.allScripts.length - 1);
890
1110
  } else if (this.settingsSection === 'scripts') {
891
1111
  return Math.max(0, this.allScripts.length - 1);
892
1112
  }
893
1113
  return 0;
894
1114
  }
895
1115
 
1116
+ toggleDisplayOption() {
1117
+ if (this.settingsIndex === 0) {
1118
+ this.showLineNumbers = !this.showLineNumbers;
1119
+ this.config.showLineNumbers = this.showLineNumbers;
1120
+ } else if (this.settingsIndex === 1) {
1121
+ this.showTimestamps = !this.showTimestamps;
1122
+ this.config.showTimestamps = this.showTimestamps;
1123
+ }
1124
+ saveConfig(this.config);
1125
+ }
1126
+
896
1127
  deleteSelectedItem() {
897
1128
  if (this.settingsSection === 'ignore' && this.config.ignore?.length > 0) {
898
1129
  this.config.ignore.splice(this.settingsIndex, 1);
@@ -906,6 +1137,27 @@ class ProcessManager {
906
1137
  saveConfig(this.config);
907
1138
  this.applyFilters();
908
1139
  this.settingsIndex = Math.max(0, Math.min(this.settingsIndex, (this.config.include?.length || 1) - 1));
1140
+ } else if (this.settingsSection === 'shortcuts') {
1141
+ const script = this.allScripts[this.settingsIndex];
1142
+ if (script && this.config.shortcuts) {
1143
+ // Find and delete any shortcut assigned to this script
1144
+ for (const [key, scriptName] of Object.entries(this.config.shortcuts)) {
1145
+ if (scriptName === script.name) {
1146
+ delete this.config.shortcuts[key];
1147
+ saveConfig(this.config);
1148
+ break;
1149
+ }
1150
+ }
1151
+ }
1152
+ }
1153
+ }
1154
+
1155
+ assignShortcut() {
1156
+ const script = this.allScripts[this.settingsIndex];
1157
+ if (script) {
1158
+ this.isAssigningShortcut = true;
1159
+ this.shortcutScriptName = script.name;
1160
+ this.buildSettingsUI();
909
1161
  }
910
1162
  }
911
1163
 
@@ -1027,6 +1279,9 @@ class ProcessManager {
1027
1279
  items.push({ label: 'Previous Pane', shortcut: 'Shift+Tab', action: () => this.navigateToNextPane(-1) });
1028
1280
  }
1029
1281
 
1282
+ items.push({ label: 'Toggle Line Numbers', shortcut: '#', action: () => { this.showLineNumbers = !this.showLineNumbers; } });
1283
+ items.push({ label: 'Toggle Timestamps', shortcut: 't', action: () => { this.showTimestamps = !this.showTimestamps; } });
1284
+
1030
1285
  return items;
1031
1286
  }
1032
1287
 
@@ -1178,6 +1433,42 @@ class ProcessManager {
1178
1433
  saveConfig(this.config);
1179
1434
  }
1180
1435
 
1436
+ // Scroll the focused pane
1437
+ scrollFocusedPane(direction) {
1438
+ if (!this.focusedPaneId) return;
1439
+
1440
+ const scrollBox = this.paneScrollBoxes.get(this.focusedPaneId);
1441
+ if (!scrollBox || !scrollBox.scrollTo) return;
1442
+
1443
+ const currentY = scrollBox.scrollTop || 0;
1444
+ const viewportHeight = scrollBox.height || 20;
1445
+ const contentHeight = scrollBox.contentHeight || 0;
1446
+
1447
+ let newY = currentY;
1448
+
1449
+ if (direction === 'home') {
1450
+ newY = 0;
1451
+ } else if (direction === 'end') {
1452
+ newY = Number.MAX_SAFE_INTEGER;
1453
+ } else if (direction === 'pageup') {
1454
+ newY = Math.max(0, currentY - viewportHeight);
1455
+ } else if (direction === 'pagedown') {
1456
+ newY = Math.min(contentHeight - viewportHeight, currentY + viewportHeight);
1457
+ }
1458
+
1459
+ scrollBox.scrollTo({ x: 0, y: newY });
1460
+
1461
+ // Save the new scroll position
1462
+ this.paneScrollPositions.set(this.focusedPaneId, { x: 0, y: newY });
1463
+
1464
+ // Auto-pause when manually scrolling (unless going to end)
1465
+ if (direction !== 'end' && !this.isPaused) {
1466
+ this.isPaused = true;
1467
+ this.updateStreamPauseState();
1468
+ this.buildRunningUI();
1469
+ }
1470
+ }
1471
+
1181
1472
  // Check if a process is visible in the focused pane
1182
1473
  isProcessVisibleInPane(scriptName, pane) {
1183
1474
  if (!pane) return true;
@@ -1302,54 +1593,115 @@ class ProcessManager {
1302
1593
  this.settingsContainer.add(inputBar);
1303
1594
  }
1304
1595
 
1305
- // Section tabs
1306
- const tabsContainer = new BoxRenderable(this.renderer, {
1307
- id: 'tabs-container',
1596
+ // Input prompt if assigning shortcut
1597
+ if (this.isAssigningShortcut) {
1598
+ const inputBar = new BoxRenderable(this.renderer, {
1599
+ id: 'input-bar',
1600
+ border: ['left'],
1601
+ borderStyle: 'single',
1602
+ borderColor: COLORS.accent,
1603
+ paddingLeft: 1,
1604
+ marginBottom: 1,
1605
+ });
1606
+ const inputText = new TextRenderable(this.renderer, {
1607
+ id: 'input-text',
1608
+ content: t`${fg(COLORS.textDim)('Press a key to assign as shortcut for')} ${fg(COLORS.accent)(this.shortcutScriptName)} ${fg(COLORS.textDim)('(esc to cancel)')}`,
1609
+ });
1610
+ inputBar.add(inputText);
1611
+ this.settingsContainer.add(inputBar);
1612
+ }
1613
+
1614
+ // Combined content panel with all sections
1615
+ const contentPanel = new BoxRenderable(this.renderer, {
1616
+ id: 'content-panel',
1308
1617
  flexDirection: 'row',
1309
- gap: 2,
1310
- marginBottom: 1,
1618
+ flexGrow: 1,
1619
+ gap: 1,
1311
1620
  });
1312
1621
 
1313
- const sections = [
1314
- { id: 'ignore', label: 'IGNORE' },
1315
- { id: 'include', label: 'INCLUDE' },
1316
- { id: 'scripts', label: 'SCRIPTS' },
1317
- ];
1622
+ // Left column - Display options, Ignore, Include
1623
+ const leftColumn = new BoxRenderable(this.renderer, {
1624
+ id: 'left-column',
1625
+ flexDirection: 'column',
1626
+ flexGrow: 1,
1627
+ gap: 1,
1628
+ });
1318
1629
 
1319
- sections.forEach(({ id, label }) => {
1320
- const isActive = this.settingsSection === id;
1321
- const tab = new TextRenderable(this.renderer, {
1322
- id: `tab-${id}`,
1323
- content: isActive
1324
- ? t`${fg(COLORS.accent)('[' + label + ']')}`
1325
- : t`${fg(COLORS.textDim)(' ' + label + ' ')}`,
1326
- });
1327
- tabsContainer.add(tab);
1630
+ // Display options section
1631
+ const displayBox = new BoxRenderable(this.renderer, {
1632
+ id: 'display-box',
1633
+ flexDirection: 'column',
1634
+ border: true,
1635
+ borderStyle: 'rounded',
1636
+ borderColor: this.settingsSection === 'display' ? COLORS.borderFocused : COLORS.border,
1637
+ title: ' Display Options ',
1638
+ titleAlignment: 'left',
1639
+ padding: 1,
1328
1640
  });
1641
+ this.buildDisplaySectionContent(displayBox);
1642
+ leftColumn.add(displayBox);
1329
1643
 
1330
- this.settingsContainer.add(tabsContainer);
1644
+ // Ignore patterns section
1645
+ const ignoreBox = new BoxRenderable(this.renderer, {
1646
+ id: 'ignore-box',
1647
+ flexDirection: 'column',
1648
+ border: true,
1649
+ borderStyle: 'rounded',
1650
+ borderColor: this.settingsSection === 'ignore' ? COLORS.borderFocused : COLORS.border,
1651
+ title: ' Ignore Patterns (i) ',
1652
+ titleAlignment: 'left',
1653
+ padding: 1,
1654
+ flexGrow: 1,
1655
+ });
1656
+ this.buildIgnoreSectionContent(ignoreBox);
1657
+ leftColumn.add(ignoreBox);
1331
1658
 
1332
- // Content panel with border
1333
- const contentPanel = new BoxRenderable(this.renderer, {
1334
- id: 'content-panel',
1659
+ // Include patterns section
1660
+ const includeBox = new BoxRenderable(this.renderer, {
1661
+ id: 'include-box',
1335
1662
  flexDirection: 'column',
1336
1663
  border: true,
1337
1664
  borderStyle: 'rounded',
1338
- borderColor: COLORS.border,
1339
- title: ` ${this.settingsSection.charAt(0).toUpperCase() + this.settingsSection.slice(1)} `,
1665
+ borderColor: this.settingsSection === 'include' ? COLORS.borderFocused : COLORS.border,
1666
+ title: ' Include Patterns (n) ',
1667
+ titleAlignment: 'left',
1668
+ padding: 1,
1669
+ flexGrow: 1,
1670
+ });
1671
+ this.buildIncludeSectionContent(includeBox);
1672
+ leftColumn.add(includeBox);
1673
+
1674
+ contentPanel.add(leftColumn);
1675
+
1676
+ // Middle column - Quick Commands
1677
+ const shortcutsBox = new BoxRenderable(this.renderer, {
1678
+ id: 'shortcuts-box',
1679
+ flexDirection: 'column',
1680
+ border: true,
1681
+ borderStyle: 'rounded',
1682
+ borderColor: this.settingsSection === 'shortcuts' ? COLORS.borderFocused : COLORS.border,
1683
+ title: ' Quick Commands ',
1340
1684
  titleAlignment: 'left',
1341
1685
  flexGrow: 1,
1342
1686
  padding: 1,
1343
1687
  });
1688
+ this.buildShortcutsSectionContent(shortcutsBox);
1689
+ contentPanel.add(shortcutsBox);
1344
1690
 
1345
- // Section content
1346
- if (this.settingsSection === 'ignore') {
1347
- this.buildIgnoreSectionContent(contentPanel);
1348
- } else if (this.settingsSection === 'include') {
1349
- this.buildIncludeSectionContent(contentPanel);
1350
- } else if (this.settingsSection === 'scripts') {
1351
- this.buildScriptsSectionContent(contentPanel);
1352
- }
1691
+ // Right column - Script List
1692
+ const scriptsBox = new BoxRenderable(this.renderer, {
1693
+ id: 'scripts-box',
1694
+ flexDirection: 'column',
1695
+ border: true,
1696
+ borderStyle: 'rounded',
1697
+ borderColor: this.settingsSection === 'scripts' ? COLORS.borderFocused : COLORS.border,
1698
+ title: ' Script List ',
1699
+ titleAlignment: 'left',
1700
+ flexGrow: 1,
1701
+ padding: 1,
1702
+ });
1703
+ this.buildScriptsSectionContent(scriptsBox);
1704
+ contentPanel.add(scriptsBox);
1353
1705
 
1354
1706
  this.settingsContainer.add(contentPanel);
1355
1707
 
@@ -1366,18 +1718,27 @@ class ProcessManager {
1366
1718
  gap: 2,
1367
1719
  });
1368
1720
 
1369
- const shortcuts = this.isAddingPattern
1370
- ? [
1371
- { key: 'enter', desc: 'save' },
1372
- { key: 'esc', desc: 'cancel' },
1373
- ]
1374
- : [
1375
- { key: 'tab', desc: 'section' },
1376
- { key: 'a', desc: 'add' },
1377
- { key: 'd', desc: 'delete' },
1378
- { key: 'space', desc: 'toggle' },
1379
- { key: 'esc', desc: 'back' },
1380
- ];
1721
+ let shortcuts;
1722
+ if (this.isAddingPattern) {
1723
+ shortcuts = [
1724
+ { key: 'enter', desc: 'save' },
1725
+ { key: 'esc', desc: 'cancel' },
1726
+ ];
1727
+ } else if (this.isAssigningShortcut) {
1728
+ shortcuts = [
1729
+ { key: 'any key', desc: 'assign' },
1730
+ { key: 'esc', desc: 'cancel' },
1731
+ ];
1732
+ } else {
1733
+ shortcuts = [
1734
+ { key: 'tab', desc: 'section' },
1735
+ { key: 'space', desc: this.settingsSection === 'shortcuts' ? 'assign' : 'toggle' },
1736
+ { key: 'i', desc: 'add ignore' },
1737
+ { key: 'n', desc: 'add include' },
1738
+ { key: 'd', desc: 'delete' },
1739
+ { key: 'esc', desc: 'back' },
1740
+ ];
1741
+ }
1381
1742
 
1382
1743
  shortcuts.forEach(({ key, desc }) => {
1383
1744
  const shortcut = new TextRenderable(this.renderer, {
@@ -1392,26 +1753,38 @@ class ProcessManager {
1392
1753
  this.renderer.root.add(this.settingsContainer);
1393
1754
  }
1394
1755
 
1395
- buildIgnoreSectionContent(container) {
1396
- const desc = new TextRenderable(this.renderer, {
1397
- id: 'ignore-desc',
1398
- content: t`${fg(COLORS.textDim)('Patterns to exclude from script list. Use * as wildcard.')}`,
1399
- });
1400
- container.add(desc);
1401
-
1402
- container.add(new TextRenderable(this.renderer, { id: 'spacer', content: '' }));
1756
+ buildDisplaySectionContent(container) {
1757
+ const options = [
1758
+ { id: 'lineNumbers', label: 'Show Line Numbers', value: this.showLineNumbers },
1759
+ { id: 'timestamps', label: 'Show Timestamps', value: this.showTimestamps },
1760
+ ];
1403
1761
 
1762
+ options.forEach((option, idx) => {
1763
+ const isFocused = this.settingsSection === 'display' && idx === this.settingsIndex;
1764
+ const indicator = isFocused ? '>' : ' ';
1765
+ const checkbox = option.value ? '[x]' : '[ ]';
1766
+ const checkColor = option.value ? COLORS.success : COLORS.textDim;
1767
+
1768
+ const line = new TextRenderable(this.renderer, {
1769
+ id: `display-option-${idx}`,
1770
+ content: t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(checkColor)(checkbox)} ${fg(COLORS.text)(option.label)}`,
1771
+ });
1772
+ container.add(line);
1773
+ });
1774
+ }
1775
+
1776
+ buildIgnoreSectionContent(container) {
1404
1777
  const patterns = this.config.ignore || [];
1405
1778
 
1406
1779
  if (patterns.length === 0) {
1407
1780
  const empty = new TextRenderable(this.renderer, {
1408
1781
  id: 'ignore-empty',
1409
- content: t`${fg(COLORS.textDim)('No ignore patterns defined. Press A to add.')}`,
1782
+ content: t`${fg(COLORS.textDim)('Press i to add')}`,
1410
1783
  });
1411
1784
  container.add(empty);
1412
1785
  } else {
1413
1786
  patterns.forEach((pattern, idx) => {
1414
- const isFocused = idx === this.settingsIndex;
1787
+ const isFocused = this.settingsSection === 'ignore' && idx === this.settingsIndex;
1415
1788
  const indicator = isFocused ? '>' : ' ';
1416
1789
 
1417
1790
  const line = new TextRenderable(this.renderer, {
@@ -1424,25 +1797,17 @@ class ProcessManager {
1424
1797
  }
1425
1798
 
1426
1799
  buildIncludeSectionContent(container) {
1427
- const desc = new TextRenderable(this.renderer, {
1428
- id: 'include-desc',
1429
- content: t`${fg(COLORS.textDim)('Only show scripts matching these patterns. Use * as wildcard.')}`,
1430
- });
1431
- container.add(desc);
1432
-
1433
- container.add(new TextRenderable(this.renderer, { id: 'spacer', content: '' }));
1434
-
1435
1800
  const patterns = this.config.include || [];
1436
1801
 
1437
1802
  if (patterns.length === 0) {
1438
1803
  const empty = new TextRenderable(this.renderer, {
1439
1804
  id: 'include-empty',
1440
- content: t`${fg(COLORS.textDim)('No include patterns (all scripts shown). Press A to add.')}`,
1805
+ content: t`${fg(COLORS.textDim)('Press n to add')}`,
1441
1806
  });
1442
1807
  container.add(empty);
1443
1808
  } else {
1444
1809
  patterns.forEach((pattern, idx) => {
1445
- const isFocused = idx === this.settingsIndex;
1810
+ const isFocused = this.settingsSection === 'include' && idx === this.settingsIndex;
1446
1811
  const indicator = isFocused ? '>' : ' ';
1447
1812
 
1448
1813
  const line = new TextRenderable(this.renderer, {
@@ -1454,29 +1819,60 @@ class ProcessManager {
1454
1819
  }
1455
1820
  }
1456
1821
 
1457
- buildScriptsSectionContent(container) {
1458
- const desc = new TextRenderable(this.renderer, {
1459
- id: 'scripts-desc',
1460
- content: t`${fg(COLORS.textDim)('Toggle individual scripts. Ignored scripts are hidden from selection.')}`,
1461
- });
1462
- container.add(desc);
1822
+ buildShortcutsSectionContent(container) {
1823
+ const shortcuts = this.config.shortcuts || {};
1463
1824
 
1464
- container.add(new TextRenderable(this.renderer, { id: 'spacer', content: '' }));
1825
+ // Helper to find shortcut key for a script
1826
+ const getShortcutKey = (scriptName) => {
1827
+ for (const [key, name] of Object.entries(shortcuts)) {
1828
+ if (name === scriptName) return key;
1829
+ }
1830
+ return null;
1831
+ };
1465
1832
 
1833
+ this.allScripts.forEach((script, idx) => {
1834
+ const isFocused = this.settingsSection === 'shortcuts' && idx === this.settingsIndex;
1835
+ const indicator = isFocused ? '>' : ' ';
1836
+ const shortcutKey = getShortcutKey(script.name);
1837
+ const processColor = this.processColors.get(script.name) || COLORS.text;
1838
+
1839
+ let content;
1840
+ if (shortcutKey) {
1841
+ content = t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(COLORS.warning)(`[${shortcutKey}]`)} ${fg(processColor)(script.displayName)}`;
1842
+ } else {
1843
+ content = t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(COLORS.textDim)('[ ]')} ${fg(processColor)(script.displayName)}`;
1844
+ }
1845
+
1846
+ const line = new TextRenderable(this.renderer, {
1847
+ id: `shortcut-item-${idx}`,
1848
+ content: content,
1849
+ });
1850
+ container.add(line);
1851
+ });
1852
+ }
1853
+
1854
+ buildScriptsSectionContent(container) {
1466
1855
  const ignorePatterns = this.config.ignore || [];
1467
1856
 
1468
1857
  this.allScripts.forEach((script, idx) => {
1469
1858
  const isIgnored = ignorePatterns.includes(script.name);
1470
- const isFocused = idx === this.settingsIndex;
1859
+ const isFocused = this.settingsSection === 'scripts' && idx === this.settingsIndex;
1471
1860
  const indicator = isFocused ? '>' : ' ';
1472
1861
  const checkbox = isIgnored ? '[x]' : '[ ]';
1473
1862
  const checkColor = isIgnored ? COLORS.error : COLORS.success;
1474
1863
  const processColor = this.processColors.get(script.name) || COLORS.text;
1475
1864
  const nameColor = isIgnored ? COLORS.textDim : processColor;
1476
1865
 
1866
+ let content;
1867
+ if (isIgnored) {
1868
+ content = t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(checkColor)(checkbox)} ${fg(nameColor)(script.displayName)} ${fg(COLORS.textDim)('(ignored)')}`;
1869
+ } else {
1870
+ content = t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(checkColor)(checkbox)} ${fg(nameColor)(script.displayName)}`;
1871
+ }
1872
+
1477
1873
  const line = new TextRenderable(this.renderer, {
1478
1874
  id: `script-toggle-${idx}`,
1479
- content: t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(checkColor)(checkbox)} ${fg(nameColor)(script.displayName)}${isIgnored ? t` ${fg(COLORS.textDim)('(ignored)')}` : ''}`,
1875
+ content: content,
1480
1876
  });
1481
1877
  container.add(line);
1482
1878
  });
@@ -1506,6 +1902,15 @@ class ProcessManager {
1506
1902
  this.countdownInterval = null;
1507
1903
  }
1508
1904
 
1905
+ // Clean up command overlay process if running
1906
+ if (this.commandOverlayProcess && this.commandOverlayProcess.pid) {
1907
+ try {
1908
+ kill(this.commandOverlayProcess.pid, 'SIGKILL');
1909
+ } catch (err) {
1910
+ // Ignore
1911
+ }
1912
+ }
1913
+
1509
1914
  for (const [scriptName, proc] of this.processRefs.entries()) {
1510
1915
  try {
1511
1916
  if (proc.pid) {
@@ -1516,6 +1921,75 @@ class ProcessManager {
1516
1921
  }
1517
1922
  }
1518
1923
  }
1924
+
1925
+ executeCommand(scriptName) {
1926
+ // Initialize overlay state
1927
+ this.showCommandOverlay = true;
1928
+ this.commandOverlayOutput = [];
1929
+ this.commandOverlayScript = scriptName;
1930
+ this.commandOverlayStatus = 'running';
1931
+
1932
+ // Spawn the process
1933
+ const proc = spawn('npm', ['run', scriptName], {
1934
+ env: {
1935
+ ...process.env,
1936
+ FORCE_COLOR: '1',
1937
+ COLORTERM: 'truecolor',
1938
+ },
1939
+ shell: true,
1940
+ });
1941
+
1942
+ this.commandOverlayProcess = proc;
1943
+
1944
+ proc.stdout.on('data', (data) => {
1945
+ const text = data.toString();
1946
+ const lines = text.split('\n');
1947
+ lines.forEach(line => {
1948
+ if (line.trim()) {
1949
+ this.commandOverlayOutput.push(line);
1950
+ this.buildRunningUI();
1951
+ }
1952
+ });
1953
+ });
1954
+
1955
+ proc.stderr.on('data', (data) => {
1956
+ const text = data.toString();
1957
+ const lines = text.split('\n');
1958
+ lines.forEach(line => {
1959
+ if (line.trim()) {
1960
+ this.commandOverlayOutput.push(line);
1961
+ this.buildRunningUI();
1962
+ }
1963
+ });
1964
+ });
1965
+
1966
+ proc.on('exit', (code) => {
1967
+ this.commandOverlayStatus = code === 0 ? 'exited' : 'crashed';
1968
+ this.commandOverlayOutput.push('');
1969
+ this.commandOverlayOutput.push(`Process exited with code ${code}`);
1970
+ this.commandOverlayProcess = null;
1971
+ this.buildRunningUI();
1972
+ });
1973
+
1974
+ this.buildRunningUI();
1975
+ }
1976
+
1977
+ closeCommandOverlay() {
1978
+ // Kill the process if still running
1979
+ if (this.commandOverlayProcess && this.commandOverlayProcess.pid) {
1980
+ try {
1981
+ kill(this.commandOverlayProcess.pid, 'SIGKILL');
1982
+ } catch (err) {
1983
+ // Ignore
1984
+ }
1985
+ }
1986
+
1987
+ this.showCommandOverlay = false;
1988
+ this.commandOverlayOutput = [];
1989
+ this.commandOverlayScript = '';
1990
+ this.commandOverlayStatus = 'running';
1991
+ this.commandOverlayProcess = null;
1992
+ }
1519
1993
 
1520
1994
  buildSelectionUI() {
1521
1995
  // Remove old containers if they exist - use destroyRecursively to clean up all children
@@ -1563,14 +2037,22 @@ class ProcessManager {
1563
2037
  this.scriptLines = this.scripts.map((script, index) => {
1564
2038
  const isSelected = this.selectedScripts.has(script.name);
1565
2039
  const isFocused = index === this.selectedIndex;
1566
- const checkIcon = isSelected ? '●' : '○';
1567
- const checkColor = isSelected ? COLORS.success : COLORS.textDim;
1568
2040
  const processColor = this.processColors.get(script.name) || COLORS.text;
1569
2041
  const nameColor = isFocused ? COLORS.text : processColor;
2042
+ const numberColor = processColor;
2043
+ const bracketColor = processColor;
1570
2044
  const bgColor = isFocused ? COLORS.bgHighlight : null;
1571
2045
 
1572
- // Build styled content - all in one template, no nesting
1573
- const content = t`${fg(checkColor)(checkIcon)} ${fg(nameColor)(script.displayName)}`;
2046
+ // Show number for first 9 scripts
2047
+ const numberLabel = index < 9 ? ` ${index + 1}` : ' ';
2048
+
2049
+ // Build checkbox with colored brackets and white x (like running screen)
2050
+ let content;
2051
+ if (isSelected) {
2052
+ content = t`${fg(numberColor)(numberLabel)} ${fg(bracketColor)('[')}${fg(COLORS.text)('x')}${fg(bracketColor)(']')} ${fg(nameColor)(script.displayName)}`;
2053
+ } else {
2054
+ content = t`${fg(numberColor)(numberLabel)} ${fg(bracketColor)('[ ]')} ${fg(nameColor)(script.displayName)}`;
2055
+ }
1574
2056
 
1575
2057
  const lineContainer = new BoxRenderable(this.renderer, {
1576
2058
  id: `script-box-${index}`,
@@ -1835,11 +2317,25 @@ class ProcessManager {
1835
2317
  const line = newLines[i];
1836
2318
  const processColor = this.processColors.get(line.process) || COLORS.text;
1837
2319
  const trimmedText = line.text.trim();
1838
- const lineNumber = String(line.lineNumber).padStart(4, ' ');
2320
+
2321
+ // Build content with proper template literal
2322
+ const lineNumber = this.showLineNumbers ? String(line.lineNumber).padStart(4, ' ') : '';
2323
+ const timestamp = this.showTimestamps ? new Date(line.timestamp).toLocaleTimeString('en-US', { hour12: false }) : '';
2324
+
2325
+ let content;
2326
+ if (this.showLineNumbers && this.showTimestamps) {
2327
+ content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
2328
+ } else if (this.showLineNumbers) {
2329
+ content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
2330
+ } else if (this.showTimestamps) {
2331
+ content = t`${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
2332
+ } else {
2333
+ content = t`${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
2334
+ }
1839
2335
 
1840
2336
  const outputLine = new TextRenderable(this.renderer, {
1841
2337
  id: `output-${pane.id}-${line.lineNumber}`,
1842
- content: t`${fg(COLORS.textDim)(lineNumber)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`,
2338
+ content: content,
1843
2339
  bg: '#000000',
1844
2340
  });
1845
2341
 
@@ -1883,10 +2379,25 @@ class ProcessManager {
1883
2379
 
1884
2380
  // Trim whitespace and let text wrap naturally - ScrollBox will handle overflow
1885
2381
  const trimmedText = line.text.trim();
1886
- const lineNumber = String(i + 1).padStart(4, ' ');
2382
+
2383
+ // Build content with proper template literal
2384
+ const lineNumber = this.showLineNumbers ? String(line.lineNumber).padStart(4, ' ') : '';
2385
+ const timestamp = this.showTimestamps ? new Date(line.timestamp).toLocaleTimeString('en-US', { hour12: false }) : '';
2386
+
2387
+ let content;
2388
+ if (this.showLineNumbers && this.showTimestamps) {
2389
+ content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
2390
+ } else if (this.showLineNumbers) {
2391
+ content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
2392
+ } else if (this.showTimestamps) {
2393
+ content = t`${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
2394
+ } else {
2395
+ content = t`${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
2396
+ }
2397
+
1887
2398
  const outputLine = new TextRenderable(this.renderer, {
1888
2399
  id: `output-${pane.id}-${i}`,
1889
- content: t`${fg(COLORS.textDim)(lineNumber)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`,
2400
+ content: content,
1890
2401
  bg: '#000000', // Black background for pane content
1891
2402
  });
1892
2403
 
@@ -2081,6 +2592,88 @@ class ProcessManager {
2081
2592
  parent.add(overlay);
2082
2593
  }
2083
2594
 
2595
+ // Build command output overlay
2596
+ buildCommandOverlay(parent) {
2597
+ const statusIcon = this.commandOverlayStatus === 'running' ? '●' :
2598
+ this.commandOverlayStatus === 'exited' ? '✓' : '✖';
2599
+ const statusColor = this.commandOverlayStatus === 'running' ? COLORS.warning :
2600
+ this.commandOverlayStatus === 'exited' ? COLORS.success : COLORS.error;
2601
+ const title = ` ${statusIcon} ${this.commandOverlayScript} `;
2602
+
2603
+ // Create centered overlay with scrollable content
2604
+ const overlay = new BoxRenderable(this.renderer, {
2605
+ id: 'command-overlay',
2606
+ position: 'absolute',
2607
+ top: '10%',
2608
+ left: '10%',
2609
+ width: '80%',
2610
+ height: '80%',
2611
+ backgroundColor: COLORS.bg,
2612
+ border: true,
2613
+ borderStyle: 'rounded',
2614
+ borderColor: statusColor,
2615
+ title: title,
2616
+ padding: 0,
2617
+ flexDirection: 'column',
2618
+ });
2619
+
2620
+ // Scrollable output content
2621
+ const outputBox = new ScrollBoxRenderable(this.renderer, {
2622
+ id: 'command-output',
2623
+ height: Math.floor(this.renderer.height * 0.8) - 4,
2624
+ scrollX: false,
2625
+ scrollY: true,
2626
+ focusable: true,
2627
+ style: {
2628
+ rootOptions: {
2629
+ flexGrow: 1,
2630
+ paddingLeft: 1,
2631
+ paddingRight: 1,
2632
+ backgroundColor: COLORS.bg,
2633
+ },
2634
+ contentOptions: {
2635
+ backgroundColor: COLORS.bg,
2636
+ width: '100%',
2637
+ },
2638
+ },
2639
+ });
2640
+
2641
+ // Add output lines
2642
+ this.commandOverlayOutput.forEach((line, idx) => {
2643
+ const outputLine = new TextRenderable(this.renderer, {
2644
+ id: `cmd-output-${idx}`,
2645
+ content: line,
2646
+ });
2647
+ outputBox.content.add(outputLine);
2648
+ });
2649
+
2650
+ // Auto-scroll to bottom
2651
+ if (outputBox.scrollTo) {
2652
+ outputBox.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER });
2653
+ }
2654
+
2655
+ overlay.add(outputBox);
2656
+
2657
+ // Footer hint
2658
+ const hintBar = new BoxRenderable(this.renderer, {
2659
+ id: 'command-hint-bar',
2660
+ border: ['top'],
2661
+ borderStyle: 'single',
2662
+ borderColor: COLORS.border,
2663
+ paddingTop: 1,
2664
+ paddingLeft: 1,
2665
+ });
2666
+
2667
+ const hint = new TextRenderable(this.renderer, {
2668
+ id: 'command-hint',
2669
+ content: t`${fg(COLORS.textDim)('Press')} ${fg(COLORS.accent)('Esc')} ${fg(COLORS.textDim)('to close')}`,
2670
+ });
2671
+ hintBar.add(hint);
2672
+ overlay.add(hintBar);
2673
+
2674
+ parent.add(overlay);
2675
+ }
2676
+
2084
2677
  buildRunningUI() {
2085
2678
  // Save scroll positions before destroying
2086
2679
  for (const [paneId, scrollBox] of this.paneScrollBoxes.entries()) {
@@ -2132,15 +2725,7 @@ class ProcessManager {
2132
2725
  paddingLeft: 1,
2133
2726
  });
2134
2727
 
2135
- // Pane count indicator
2136
- const allPanes = getAllPaneIds(this.paneRoot);
2137
- if (allPanes.length > 1) {
2138
- const paneIndicator = new TextRenderable(this.renderer, {
2139
- id: 'pane-indicator',
2140
- content: t`${fg(COLORS.cyan)(`[${allPanes.length} panes]`)} `,
2141
- });
2142
- processBar.add(paneIndicator);
2143
- }
2728
+
2144
2729
 
2145
2730
  // Add each process with checkbox showing visibility in focused pane
2146
2731
  const focusedPane = findPaneById(this.paneRoot, this.focusedPaneId);
@@ -2153,13 +2738,25 @@ class ProcessManager {
2153
2738
  const processColor = this.processColors.get(script.name) || COLORS.text;
2154
2739
  const isSelected = this.selectedIndex === index;
2155
2740
  const isVisible = this.isProcessVisibleInPane(script.name, focusedPane);
2156
- const checkbox = isVisible ? '[x]' : '[ ]';
2157
2741
  const nameColor = isSelected ? COLORS.accent : (isVisible ? processColor : COLORS.textDim);
2742
+ const numberColor = isVisible ? processColor : COLORS.textDim;
2158
2743
  const indicator = isSelected ? '>' : ' ';
2744
+ const bracketColor = isVisible ? processColor : COLORS.textDim;
2745
+
2746
+ // Show number for first 9 processes
2747
+ const numberLabel = index < 9 ? `${index + 1}` : ' ';
2748
+
2749
+ // Build content - can't nest template literals, so build entire thing at once
2750
+ let content;
2751
+ if (isVisible) {
2752
+ content = t`${fg(numberColor)(numberLabel)} ${fg(isSelected ? COLORS.accent : COLORS.textDim)(indicator)}${fg(bracketColor)('[')}${fg(COLORS.text)('x')}${fg(bracketColor)(']')} ${fg(statusColor)(statusIcon)} ${fg(nameColor)(script.displayName)}`;
2753
+ } else {
2754
+ content = t`${fg(numberColor)(numberLabel)} ${fg(isSelected ? COLORS.accent : COLORS.textDim)(indicator)}${fg(bracketColor)('[ ]')} ${fg(statusColor)(statusIcon)} ${fg(nameColor)(script.displayName)}`;
2755
+ }
2159
2756
 
2160
2757
  const processItem = new TextRenderable(this.renderer, {
2161
2758
  id: `process-item-${index}`,
2162
- content: t`${fg(isSelected ? COLORS.accent : COLORS.textDim)(indicator)}${fg(isVisible ? COLORS.text : COLORS.textDim)(checkbox)} ${fg(nameColor)(script.displayName)} ${fg(statusColor)(statusIcon)}`,
2759
+ content: content,
2163
2760
  });
2164
2761
  processBar.add(processItem);
2165
2762
  });
@@ -2230,6 +2827,17 @@ class ProcessManager {
2230
2827
  leftSide.add(filterIndicator);
2231
2828
  }
2232
2829
 
2830
+ // Input mode indicator if active
2831
+ if (this.isInputMode) {
2832
+ const scriptName = this.scripts[this.selectedIndex]?.displayName || '';
2833
+ const inputText = `[${scriptName}]> ${this.inputModeText}_`;
2834
+ const inputIndicator = new TextRenderable(this.renderer, {
2835
+ id: 'input-indicator',
2836
+ content: t`${fg(COLORS.success)(inputText)}`,
2837
+ });
2838
+ leftSide.add(inputIndicator);
2839
+ }
2840
+
2233
2841
  // Color filter indicator if active on focused pane
2234
2842
  if (focusedPane?.colorFilter) {
2235
2843
  const colorMap = {
@@ -2258,13 +2866,15 @@ class ProcessManager {
2258
2866
 
2259
2867
  const shortcuts = [
2260
2868
  { key: '\\', desc: 'panes', color: COLORS.cyan },
2261
- { key: 'spc', desc: 'toggle', color: COLORS.success },
2869
+ { key: '1-9', desc: 'toggle', color: COLORS.success },
2870
+ { key: 'i', desc: 'input', color: COLORS.success },
2262
2871
  { key: 'n', desc: 'name', color: COLORS.accent },
2263
2872
  { key: 'p', desc: 'pause', color: COLORS.warning },
2264
2873
  { key: '/', desc: 'filter', color: COLORS.cyan },
2265
2874
  { key: 'c', desc: 'color', color: COLORS.magenta },
2266
2875
  { key: 's', desc: 'stop', color: COLORS.error },
2267
2876
  { key: 'r', desc: 'restart', color: COLORS.success },
2877
+ { key: 'o', desc: 'settings', color: COLORS.magenta },
2268
2878
  { key: 'q', desc: 'quit', color: COLORS.error },
2269
2879
  ];
2270
2880
 
@@ -2291,6 +2901,11 @@ class ProcessManager {
2291
2901
  this.buildSplitMenuOverlay(mainContainer);
2292
2902
  }
2293
2903
 
2904
+ // Add command output overlay if active
2905
+ if (this.showCommandOverlay) {
2906
+ this.buildCommandOverlay(mainContainer);
2907
+ }
2908
+
2294
2909
  this.renderer.root.add(mainContainer);
2295
2910
  this.runningContainer = mainContainer;
2296
2911
  }