startall 0.0.11 → 0.0.13

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/README.md CHANGED
@@ -1,6 +1,8 @@
1
- # 🚀 Start
1
+ # 🚀 startall
2
2
 
3
- > An interactive terminal UI for managing multiple npm scripts in parallel
3
+ > A powerful, interactive terminal UI for managing multiple npm scripts with tmux-style panes, filtering, and real-time control
4
+
5
+ ![startall screenshot](https://github.com/bzbetty/startall/raw/main/screenshot.png)
4
6
 
5
7
  ## The Problem
6
8
 
@@ -20,47 +22,47 @@ Traditional solutions fall short:
20
22
 
21
23
  ## The Solution
22
24
 
23
- **Start** is a lightweight, interactive TUI that gives you complete control over your development processes:
24
-
25
- ```
26
- ┌─ Starting in 7s... [Enter to start now] ─────────────┐
27
- │ [x] frontend (npm run start:frontend) │
28
- │ [x] backend (npm run start:backend) │
29
- │ [ ] worker (npm run start:worker) │
30
- │ [x] db (npm run start:db) │
31
- │ │
32
- │ ↑/↓ Navigate | Space: Toggle | Enter: Start │
33
- └───────────────────────────────────────────────────────┘
34
-
35
- After starting:
36
- ┌─ Processes ──────────────┬─ Output (filter: error) ───┐
37
- │ [f] frontend ● Running │ [backend] Error: ECONNREF │
38
- │ [b] backend ✖ Crashed │ [backend] Retrying... │
39
- │ [w] worker ⏸ Stopped │ [frontend] Started on 3000 │
40
- │ [d] db ● Running │ │
41
- │ │ │
42
- │ Space: Start/Stop │ │
43
- │ r: Restart │ │
44
- │ /: Filter output │ │
45
- └──────────────────────────┴────────────────────────────┘
46
- ```
25
+ **startall** is a sophisticated TUI that combines the power of tmux with the simplicity of npm scripts, giving you complete control over your development processes with split panes, filtering, and interactive controls.
47
26
 
48
27
  ## Features
49
28
 
50
- ### Current
51
- - **Auto-discovery**: Reads all scripts from `package.json` automatically
52
- - **Smart defaults**: Remembers your last selection
53
- - **10-second countdown**: Time to review/change selections before starting
29
+ ### 🎯 Core Features
30
+ - **Auto-discovery**: Automatically reads all scripts from `package.json`
31
+ - **Smart defaults**: Remembers your last selection in `startall.json`
32
+ - **10-second countdown**: Review selections before starting
54
33
  - **Parallel execution**: Run multiple npm scripts simultaneously
55
- - **Colored output**: Each process gets its own color prefix
56
-
57
- ### 🚧 Planned
58
- - **Live status monitoring**: See which processes are running/crashed/stopped at a glance
59
- - **Interactive controls**: Start, stop, and restart individual processes with keyboard shortcuts
60
- - **Output filtering**: Search/filter logs across all processes in real-time
34
+ - **Live status monitoring**: Real-time status indicators (● running, crashed, ○ stopped)
35
+ - **Interactive controls**: Start, stop, and restart individual processes on the fly
61
36
  - **Cross-platform**: Works identically on Windows, Linux, and macOS
62
- - **Tab view**: Switch between different process outputs
63
- - **Resource monitoring**: CPU/memory usage per process
37
+
38
+ ### 🎨 Advanced UI
39
+ - **Multi-pane layout**: tmux-inspired split panes (vertical & horizontal)
40
+ - **Flexible filtering**:
41
+ - Text search across all output (`/`)
42
+ - Filter by ANSI color (red/yellow/green/blue/cyan/magenta) (`c`)
43
+ - Per-process visibility toggles (`Space` or `1-9`)
44
+ - Per-pane filters (different views in each pane)
45
+ - **Custom pane naming**: Label panes for easier identification (`n`)
46
+ - **Persistent layouts**: Your pane configuration is saved between sessions
47
+ - **Process-specific views**: Show/hide specific processes in each pane
48
+ - **Colored output**: Each process gets unique color-coded output
49
+ - **Pause/resume**: Freeze output to review logs (`p`)
50
+ - **Scrollable history**: 1000-line buffer with mouse wheel support
51
+ - **Enhanced navigation**: Home/End/PageUp/PageDown keys
52
+
53
+ ### ⚙️ Display Options
54
+ - **Toggleable line numbers**: Show/hide line numbers (`#`)
55
+ - **Timestamps**: Show/hide timestamps for each log line (`t`)
56
+ - **Quick process toggle**: Use number keys `1-9` for instant visibility control
57
+
58
+ ### 🔧 Advanced Controls
59
+ - **Interactive input mode**: Send commands to running processes via stdin (`i`)
60
+ - Perfect for dev servers that accept commands (Vite, Rust watch, etc.)
61
+ - **Settings panel**: Configure ignore/include patterns (`o`)
62
+ - Wildcard support (`*`) for pattern matching
63
+ - Per-script visibility toggles
64
+ - **Keyboard & mouse support**: Full keyboard navigation + mouse clicking/scrolling
65
+ - **VSCode integration**: Optimized for VSCode integrated terminal
64
66
 
65
67
  ## Installation
66
68
 
@@ -90,14 +92,57 @@ That's it! The TUI will:
90
92
  - `↑`/`↓` - Navigate scripts
91
93
  - `Space` - Toggle selection
92
94
  - `Enter` - Start immediately (skip countdown)
95
+ - `o` - Open settings
93
96
  - `Ctrl+C` - Exit
94
97
 
95
- **Running Screen (planned):**
96
- - `Space` - Start/stop selected process
98
+ **Running Screen:**
99
+
100
+ *Process Control:*
101
+ - `1-9` - Quick toggle process visibility in focused pane
102
+ - `Space` - Toggle visibility of selected process
103
+ - `s` - Stop/start selected process
97
104
  - `r` - Restart selected process
98
- - `/` - Filter output
99
- - `Tab` - Switch between processes
100
- - `Ctrl+C` - Stop all and exit
105
+ - `i` - Send input to selected process (interactive mode)
106
+
107
+ *Pane Management:*
108
+ - `\` - Open command palette
109
+ - `|` - Split pane vertically (left/right)
110
+ - `_` - Split pane horizontally (top/bottom)
111
+ - `x` - Close current pane (if >1 pane exists)
112
+ - `Tab` - Next pane
113
+ - `Shift+Tab` - Previous pane
114
+ - `n` - Name current pane
115
+
116
+ *Filtering & View:*
117
+ - `/` - Enter text filter mode
118
+ - `c` - Cycle color filter (red/yellow/green/blue/cyan/magenta/none)
119
+ - `f` - Filter to selected process only
120
+ - `Esc` - Clear filters
121
+ - `p` - Pause/resume output scrolling
122
+ - `#` - Toggle line numbers
123
+ - `t` - Toggle timestamps
124
+
125
+ *Navigation:*
126
+ - `↑`/`↓` or `k`/`j` - Select process (vim-style)
127
+ - `←`/`→` or `h`/`l` - Select process (vim-style)
128
+ - `Home` - Scroll to top of pane
129
+ - `End` - Scroll to bottom of pane
130
+ - `Page Up` - Scroll up one page
131
+ - `Page Down` - Scroll down one page
132
+ - `Mouse wheel` - Scroll output
133
+
134
+ *Other:*
135
+ - `o` - Open settings
136
+ - `q` - Quit (stops all processes)
137
+ - `Ctrl+C` - Force quit
138
+
139
+ **Settings Screen:**
140
+ - `Tab`/`←`/`→` - Switch sections (Ignore/Include/Scripts)
141
+ - `↑`/`↓` - Navigate items
142
+ - `a` - Add new pattern (Ignore/Include sections)
143
+ - `d` or `Backspace` - Delete pattern
144
+ - `Space` or `Enter` - Toggle script (Scripts section)
145
+ - `Esc` or `q` - Return to previous screen
101
146
 
102
147
  ## Why Build This?
103
148
 
package/index.js CHANGED
@@ -15,6 +15,16 @@ const APP_VERSION = 'v0.0.4';
15
15
  // Detect if running inside VS Code's integrated terminal
16
16
  const IS_VSCODE = process.env.TERM_PROGRAM === 'vscode';
17
17
 
18
+ // VSCode-specific optimizations
19
+ const VSCODE_CONFIG = {
20
+ // VSCode terminal has better mouse support
21
+ enhancedMouse: IS_VSCODE,
22
+ // VSCode can detect and linkify file paths (file:///path/to/file.js:line:col)
23
+ fileLinking: IS_VSCODE,
24
+ // Some key combinations are captured by VSCode
25
+ remapKeys: IS_VSCODE,
26
+ };
27
+
18
28
  // Pane ID generator
19
29
  let paneIdCounter = 0;
20
30
  function generatePaneId() {
@@ -328,6 +338,7 @@ class ProcessManager {
328
338
  this.processes = new Map();
329
339
  this.processRefs = new Map();
330
340
  this.outputLines = [];
341
+ this.totalLinesReceived = 0; // Track total lines ever received (never resets)
331
342
  this.filter = '';
332
343
  this.maxOutputLines = 1000;
333
344
  this.maxVisibleLines = null; // Calculated dynamically based on screen height
@@ -336,9 +347,13 @@ class ProcessManager {
336
347
  this.isFilterMode = false; // Whether in filter input mode
337
348
  this.isNamingMode = false; // Whether in pane naming input mode
338
349
  this.namingModeText = ''; // Text being typed for pane name
350
+ this.showLineNumbers = this.config.showLineNumbers !== undefined ? this.config.showLineNumbers : true; // Whether to show line numbers
351
+ this.showTimestamps = this.config.showTimestamps !== undefined ? this.config.showTimestamps : false; // Whether to show timestamps
352
+ this.isInputMode = false; // Whether in stdin input mode
353
+ this.inputModeText = ''; // Text being typed for stdin
339
354
 
340
355
  // Settings menu state
341
- this.settingsSection = 'ignore'; // 'ignore' | 'include' | 'scripts'
356
+ this.settingsSection = 'display'; // 'display' | 'ignore' | 'include' | 'scripts'
342
357
  this.settingsIndex = 0; // Current selection index within section
343
358
  this.isAddingPattern = false; // Whether typing a new pattern
344
359
  this.newPatternText = ''; // Text being typed for new pattern
@@ -426,10 +441,25 @@ class ProcessManager {
426
441
  clearInterval(this.countdownInterval);
427
442
  this.previousPhase = 'selection';
428
443
  this.phase = 'settings';
429
- this.settingsSection = 'ignore';
444
+ this.settingsSection = 'display';
430
445
  this.settingsIndex = 0;
431
446
  this.buildSettingsUI();
432
447
  return;
448
+ } else if (keyName >= '1' && keyName <= '9') {
449
+ // Toggle script by number (1-9)
450
+ const index = parseInt(keyName) - 1;
451
+ if (index >= 0 && index < this.scripts.length) {
452
+ const scriptName = this.scripts[index]?.name;
453
+ if (scriptName) {
454
+ if (this.selectedScripts.has(scriptName)) {
455
+ this.selectedScripts.delete(scriptName);
456
+ } else {
457
+ this.selectedScripts.add(scriptName);
458
+ }
459
+ // Reset countdown when selection changes
460
+ this.countdown = COUNTDOWN_SECONDS;
461
+ }
462
+ }
433
463
  }
434
464
  } else if (this.phase === 'settings') {
435
465
  this.handleSettingsInput(keyName, keyEvent);
@@ -441,8 +471,34 @@ class ProcessManager {
441
471
  return;
442
472
  }
443
473
 
474
+ // If in input mode (stdin), handle stdin input
475
+ if (this.isInputMode) {
476
+ const scriptName = this.scripts[this.selectedIndex]?.name;
477
+ if (keyName === 'escape') {
478
+ this.isInputMode = false;
479
+ this.inputModeText = '';
480
+ this.buildRunningUI();
481
+ } else if (keyName === 'enter' || keyName === 'return') {
482
+ // Send the input to the selected process
483
+ if (scriptName && this.inputModeText.trim()) {
484
+ this.sendInputToProcess(scriptName, this.inputModeText + '\n');
485
+ }
486
+ this.isInputMode = false;
487
+ this.inputModeText = '';
488
+ this.buildRunningUI();
489
+ } else if (keyName === 'backspace') {
490
+ this.inputModeText = this.inputModeText.slice(0, -1);
491
+ this.buildRunningUI();
492
+ } else if (keyName === 'space') {
493
+ this.inputModeText += ' ';
494
+ this.buildRunningUI();
495
+ } else if (keyName && keyName.length === 1 && !keyEvent.ctrl && !keyEvent.meta) {
496
+ this.inputModeText += keyName;
497
+ this.buildRunningUI();
498
+ }
499
+ }
444
500
  // If in naming mode, handle name input
445
- if (this.isNamingMode) {
501
+ else if (this.isNamingMode) {
446
502
  const pane = findPaneById(this.paneRoot, this.focusedPaneId);
447
503
  if (keyName === 'escape') {
448
504
  this.isNamingMode = false;
@@ -548,12 +604,15 @@ class ProcessManager {
548
604
  this.selectedIndex = Math.min(this.scripts.length - 1, this.selectedIndex + 1);
549
605
  this.buildRunningUI(); // Rebuild to show selection change
550
606
  } else if (keyName === 'left' || keyName === 'h') {
551
- // Navigate processes left
552
- this.selectedIndex = Math.max(0, this.selectedIndex - 1);
607
+ // Navigate processes left with wrapping
608
+ this.selectedIndex = this.selectedIndex - 1;
609
+ if (this.selectedIndex < 0) {
610
+ this.selectedIndex = this.scripts.length - 1;
611
+ }
553
612
  this.buildRunningUI(); // Rebuild to show selection change
554
613
  } else if (keyName === 'right' || keyName === 'l') {
555
- // Navigate processes right
556
- this.selectedIndex = Math.min(this.scripts.length - 1, this.selectedIndex + 1);
614
+ // Navigate processes right with wrapping
615
+ this.selectedIndex = (this.selectedIndex + 1) % this.scripts.length;
557
616
  this.buildRunningUI(); // Rebuild to show selection change
558
617
  } else if (keyName === 'r') {
559
618
  const scriptName = this.scripts[this.selectedIndex]?.name;
@@ -567,13 +626,13 @@ class ProcessManager {
567
626
  this.toggleProcess(scriptName);
568
627
  }
569
628
  } else if (keyName === 'o') {
570
- // Open settings (options)
571
- this.previousPhase = 'running';
572
- this.phase = 'settings';
573
- this.settingsSection = 'ignore';
574
- this.settingsIndex = 0;
575
- this.buildSettingsUI();
576
- return;
629
+ // Open settings (options)
630
+ this.previousPhase = 'running';
631
+ this.phase = 'settings';
632
+ this.settingsSection = 'display';
633
+ this.settingsIndex = 0;
634
+ this.buildSettingsUI();
635
+ return;
577
636
  } else if (keyName === 'c') {
578
637
  // Cycle color filter on focused pane
579
638
  const pane = findPaneById(this.paneRoot, this.focusedPaneId);
@@ -591,6 +650,35 @@ class ProcessManager {
591
650
  // Navigate to previous pane
592
651
  this.navigateToNextPane(-1);
593
652
  this.buildRunningUI();
653
+ } else if (keyName === 'home') {
654
+ // Scroll to top of focused pane
655
+ this.scrollFocusedPane('home');
656
+ } else if (keyName === 'end') {
657
+ // Scroll to bottom of focused pane
658
+ this.scrollFocusedPane('end');
659
+ } else if (keyName === 'pageup') {
660
+ // Scroll up one page in focused pane
661
+ this.scrollFocusedPane('pageup');
662
+ } else if (keyName === 'pagedown') {
663
+ // Scroll down one page in focused pane
664
+ this.scrollFocusedPane('pagedown');
665
+ } else if (keyName >= '1' && keyName <= '9') {
666
+ // Toggle process by number (1-9)
667
+ const index = parseInt(keyName) - 1;
668
+ if (index >= 0 && index < this.scripts.length) {
669
+ this.selectedIndex = index;
670
+ this.toggleProcessVisibility();
671
+ this.buildRunningUI();
672
+ }
673
+ } else if (keyName === 'i') {
674
+ // Enter input mode to send stdin to selected process
675
+ const scriptName = this.scripts[this.selectedIndex]?.name;
676
+ const proc = this.processes.get(scriptName);
677
+ if (scriptName && proc?.status === 'running') {
678
+ this.isInputMode = true;
679
+ this.inputModeText = '';
680
+ this.buildRunningUI();
681
+ }
594
682
  }
595
683
  }
596
684
  }
@@ -734,6 +822,7 @@ class ProcessManager {
734
822
  process: processName,
735
823
  text,
736
824
  timestamp: Date.now(),
825
+ lineNumber: ++this.totalLinesReceived, // Track absolute line number
737
826
  });
738
827
 
739
828
  if (this.outputLines.length > this.maxOutputLines) {
@@ -748,13 +837,10 @@ class ProcessManager {
748
837
  }
749
838
 
750
839
  scheduleRender() {
751
- // Throttle renders to ~60fps to reduce CPU usage
752
- if (this.renderScheduled) return;
753
- this.renderScheduled = true;
754
- setTimeout(() => {
755
- this.renderScheduled = false;
840
+ // Update the DOM - OpenTUI's render loop will pick up changes automatically
841
+ if (!this.destroyed) {
756
842
  this.render();
757
- }, 16);
843
+ }
758
844
  }
759
845
 
760
846
  stopProcess(scriptName) {
@@ -789,6 +875,19 @@ class ProcessManager {
789
875
  this.startProcess(scriptName);
790
876
  }
791
877
  }
878
+
879
+ sendInputToProcess(scriptName, input) {
880
+ const proc = this.processRefs.get(scriptName);
881
+ if (proc && proc.stdin && proc.stdin.writable) {
882
+ try {
883
+ proc.stdin.write(input);
884
+ // Echo the input in the output for visibility
885
+ this.addOutputLine(scriptName, `> ${input.trim()}`);
886
+ } catch (err) {
887
+ this.addOutputLine(scriptName, `Error sending input: ${err.message}`);
888
+ }
889
+ }
890
+ }
792
891
 
793
892
  handleSettingsInput(keyName, keyEvent) {
794
893
  // Handle text input mode for adding patterns
@@ -825,6 +924,14 @@ class ProcessManager {
825
924
 
826
925
  // Normal settings navigation
827
926
  if (keyName === 'escape' || keyName === 'q') {
927
+ // Apply filters before returning (updates this.scripts)
928
+ this.applyFilters();
929
+
930
+ // Ensure selectedIndex is within bounds after filter changes
931
+ if (this.selectedIndex >= this.scripts.length) {
932
+ this.selectedIndex = Math.max(0, this.scripts.length - 1);
933
+ }
934
+
828
935
  // Return to previous phase
829
936
  if (this.previousPhase === 'running') {
830
937
  this.phase = 'running';
@@ -837,46 +944,76 @@ class ProcessManager {
837
944
  }
838
945
  } else if (keyName === 'tab' || keyName === 'right') {
839
946
  // Switch section
840
- const sections = ['ignore', 'include', 'scripts'];
947
+ const sections = ['display', 'ignore', 'include', 'scripts'];
841
948
  const idx = sections.indexOf(this.settingsSection);
842
949
  this.settingsSection = sections[(idx + 1) % sections.length];
843
950
  this.settingsIndex = 0;
844
951
  this.buildSettingsUI();
845
952
  } else if (keyEvent.shift && keyName === 'tab') {
846
953
  // Switch section backwards
847
- const sections = ['ignore', 'include', 'scripts'];
954
+ const sections = ['display', 'ignore', 'include', 'scripts'];
848
955
  const idx = sections.indexOf(this.settingsSection);
849
956
  this.settingsSection = sections[(idx - 1 + sections.length) % sections.length];
850
957
  this.settingsIndex = 0;
851
958
  this.buildSettingsUI();
852
959
  } else if (keyName === 'left') {
853
960
  // Switch section backwards
854
- const sections = ['ignore', 'include', 'scripts'];
961
+ const sections = ['display', 'ignore', 'include', 'scripts'];
855
962
  const idx = sections.indexOf(this.settingsSection);
856
963
  this.settingsSection = sections[(idx - 1 + sections.length) % sections.length];
857
964
  this.settingsIndex = 0;
858
965
  this.buildSettingsUI();
859
966
  } else if (keyName === 'up') {
860
- this.settingsIndex = Math.max(0, this.settingsIndex - 1);
861
- this.buildSettingsUI();
967
+ if (this.settingsIndex > 0) {
968
+ this.settingsIndex--;
969
+ this.buildSettingsUI();
970
+ } else {
971
+ // Move to previous section
972
+ const sections = ['display', 'ignore', 'include', 'scripts'];
973
+ const idx = sections.indexOf(this.settingsSection);
974
+ if (idx > 0) {
975
+ this.settingsSection = sections[idx - 1];
976
+ this.settingsIndex = this.getSettingsMaxIndex();
977
+ this.buildSettingsUI();
978
+ }
979
+ }
862
980
  } else if (keyName === 'down') {
863
981
  const maxIndex = this.getSettingsMaxIndex();
864
- this.settingsIndex = Math.min(maxIndex, this.settingsIndex + 1);
865
- this.buildSettingsUI();
866
- } else if (keyName === 'a') {
867
- // Add new pattern (only for ignore/include sections)
868
- if (this.settingsSection === 'ignore' || this.settingsSection === 'include') {
869
- this.isAddingPattern = true;
870
- this.newPatternText = '';
982
+ if (this.settingsIndex < maxIndex) {
983
+ this.settingsIndex++;
871
984
  this.buildSettingsUI();
985
+ } else {
986
+ // Move to next section
987
+ const sections = ['display', 'ignore', 'include', 'scripts'];
988
+ const idx = sections.indexOf(this.settingsSection);
989
+ if (idx < sections.length - 1) {
990
+ this.settingsSection = sections[idx + 1];
991
+ this.settingsIndex = 0;
992
+ this.buildSettingsUI();
993
+ }
872
994
  }
995
+ } else if (keyName === 'i') {
996
+ // Add new ignore pattern
997
+ this.settingsSection = 'ignore';
998
+ this.isAddingPattern = true;
999
+ this.newPatternText = '';
1000
+ this.buildSettingsUI();
1001
+ } else if (keyName === 'n') {
1002
+ // Add new include pattern
1003
+ this.settingsSection = 'include';
1004
+ this.isAddingPattern = true;
1005
+ this.newPatternText = '';
1006
+ this.buildSettingsUI();
873
1007
  } else if (keyName === 'd' || keyName === 'backspace') {
874
- // Delete selected pattern or toggle script ignore
1008
+ // Delete selected pattern
875
1009
  this.deleteSelectedItem();
876
1010
  this.buildSettingsUI();
877
1011
  } else if (keyName === 'space' || keyName === 'enter' || keyName === 'return') {
878
- // Toggle for scripts section
879
- if (this.settingsSection === 'scripts') {
1012
+ // Toggle display options, script visibility
1013
+ if (this.settingsSection === 'display') {
1014
+ this.toggleDisplayOption();
1015
+ this.buildSettingsUI();
1016
+ } else if (this.settingsSection === 'scripts') {
880
1017
  this.toggleScriptIgnore();
881
1018
  this.buildSettingsUI();
882
1019
  }
@@ -884,16 +1021,31 @@ class ProcessManager {
884
1021
  }
885
1022
 
886
1023
  getSettingsMaxIndex() {
887
- if (this.settingsSection === 'ignore') {
888
- return Math.max(0, (this.config.ignore?.length || 0) - 1);
1024
+ if (this.settingsSection === 'display') {
1025
+ return 1; // 2 display options (line numbers, timestamps)
1026
+ } else if (this.settingsSection === 'ignore') {
1027
+ const count = this.config.ignore?.length || 0;
1028
+ return count > 0 ? count - 1 : 0;
889
1029
  } else if (this.settingsSection === 'include') {
890
- return Math.max(0, (this.config.include?.length || 0) - 1);
1030
+ const count = this.config.include?.length || 0;
1031
+ return count > 0 ? count - 1 : 0;
891
1032
  } else if (this.settingsSection === 'scripts') {
892
1033
  return Math.max(0, this.allScripts.length - 1);
893
1034
  }
894
1035
  return 0;
895
1036
  }
896
1037
 
1038
+ toggleDisplayOption() {
1039
+ if (this.settingsIndex === 0) {
1040
+ this.showLineNumbers = !this.showLineNumbers;
1041
+ this.config.showLineNumbers = this.showLineNumbers;
1042
+ } else if (this.settingsIndex === 1) {
1043
+ this.showTimestamps = !this.showTimestamps;
1044
+ this.config.showTimestamps = this.showTimestamps;
1045
+ }
1046
+ saveConfig(this.config);
1047
+ }
1048
+
897
1049
  deleteSelectedItem() {
898
1050
  if (this.settingsSection === 'ignore' && this.config.ignore?.length > 0) {
899
1051
  this.config.ignore.splice(this.settingsIndex, 1);
@@ -1028,6 +1180,9 @@ class ProcessManager {
1028
1180
  items.push({ label: 'Previous Pane', shortcut: 'Shift+Tab', action: () => this.navigateToNextPane(-1) });
1029
1181
  }
1030
1182
 
1183
+ items.push({ label: 'Toggle Line Numbers', shortcut: '#', action: () => { this.showLineNumbers = !this.showLineNumbers; } });
1184
+ items.push({ label: 'Toggle Timestamps', shortcut: 't', action: () => { this.showTimestamps = !this.showTimestamps; } });
1185
+
1031
1186
  return items;
1032
1187
  }
1033
1188
 
@@ -1179,6 +1334,42 @@ class ProcessManager {
1179
1334
  saveConfig(this.config);
1180
1335
  }
1181
1336
 
1337
+ // Scroll the focused pane
1338
+ scrollFocusedPane(direction) {
1339
+ if (!this.focusedPaneId) return;
1340
+
1341
+ const scrollBox = this.paneScrollBoxes.get(this.focusedPaneId);
1342
+ if (!scrollBox || !scrollBox.scrollTo) return;
1343
+
1344
+ const currentY = scrollBox.scrollTop || 0;
1345
+ const viewportHeight = scrollBox.height || 20;
1346
+ const contentHeight = scrollBox.contentHeight || 0;
1347
+
1348
+ let newY = currentY;
1349
+
1350
+ if (direction === 'home') {
1351
+ newY = 0;
1352
+ } else if (direction === 'end') {
1353
+ newY = Number.MAX_SAFE_INTEGER;
1354
+ } else if (direction === 'pageup') {
1355
+ newY = Math.max(0, currentY - viewportHeight);
1356
+ } else if (direction === 'pagedown') {
1357
+ newY = Math.min(contentHeight - viewportHeight, currentY + viewportHeight);
1358
+ }
1359
+
1360
+ scrollBox.scrollTo({ x: 0, y: newY });
1361
+
1362
+ // Save the new scroll position
1363
+ this.paneScrollPositions.set(this.focusedPaneId, { x: 0, y: newY });
1364
+
1365
+ // Auto-pause when manually scrolling (unless going to end)
1366
+ if (direction !== 'end' && !this.isPaused) {
1367
+ this.isPaused = true;
1368
+ this.updateStreamPauseState();
1369
+ this.buildRunningUI();
1370
+ }
1371
+ }
1372
+
1182
1373
  // Check if a process is visible in the focused pane
1183
1374
  isProcessVisibleInPane(scriptName, pane) {
1184
1375
  if (!pane) return true;
@@ -1303,54 +1494,82 @@ class ProcessManager {
1303
1494
  this.settingsContainer.add(inputBar);
1304
1495
  }
1305
1496
 
1306
- // Section tabs
1307
- const tabsContainer = new BoxRenderable(this.renderer, {
1308
- id: 'tabs-container',
1497
+ // Combined content panel with all sections
1498
+ const contentPanel = new BoxRenderable(this.renderer, {
1499
+ id: 'content-panel',
1309
1500
  flexDirection: 'row',
1310
- gap: 2,
1311
- marginBottom: 1,
1501
+ flexGrow: 1,
1502
+ gap: 1,
1312
1503
  });
1313
1504
 
1314
- const sections = [
1315
- { id: 'ignore', label: 'IGNORE' },
1316
- { id: 'include', label: 'INCLUDE' },
1317
- { id: 'scripts', label: 'SCRIPTS' },
1318
- ];
1319
-
1320
- sections.forEach(({ id, label }) => {
1321
- const isActive = this.settingsSection === id;
1322
- const tab = new TextRenderable(this.renderer, {
1323
- id: `tab-${id}`,
1324
- content: isActive
1325
- ? t`${fg(COLORS.accent)('[' + label + ']')}`
1326
- : t`${fg(COLORS.textDim)(' ' + label + ' ')}`,
1327
- });
1328
- tabsContainer.add(tab);
1505
+ // Left column - Display options, Ignore, Include
1506
+ const leftColumn = new BoxRenderable(this.renderer, {
1507
+ id: 'left-column',
1508
+ flexDirection: 'column',
1509
+ flexGrow: 1,
1510
+ gap: 1,
1329
1511
  });
1330
1512
 
1331
- this.settingsContainer.add(tabsContainer);
1513
+ // Display options section
1514
+ const displayBox = new BoxRenderable(this.renderer, {
1515
+ id: 'display-box',
1516
+ flexDirection: 'column',
1517
+ border: true,
1518
+ borderStyle: 'rounded',
1519
+ borderColor: this.settingsSection === 'display' ? COLORS.borderFocused : COLORS.border,
1520
+ title: ' Display Options ',
1521
+ titleAlignment: 'left',
1522
+ padding: 1,
1523
+ });
1524
+ this.buildDisplaySectionContent(displayBox);
1525
+ leftColumn.add(displayBox);
1332
1526
 
1333
- // Content panel with border
1334
- const contentPanel = new BoxRenderable(this.renderer, {
1335
- id: 'content-panel',
1527
+ // Ignore patterns section
1528
+ const ignoreBox = new BoxRenderable(this.renderer, {
1529
+ id: 'ignore-box',
1336
1530
  flexDirection: 'column',
1337
1531
  border: true,
1338
1532
  borderStyle: 'rounded',
1339
- borderColor: COLORS.border,
1340
- title: ` ${this.settingsSection.charAt(0).toUpperCase() + this.settingsSection.slice(1)} `,
1533
+ borderColor: this.settingsSection === 'ignore' ? COLORS.borderFocused : COLORS.border,
1534
+ title: ' Ignore Patterns (i) ',
1341
1535
  titleAlignment: 'left',
1536
+ padding: 1,
1342
1537
  flexGrow: 1,
1538
+ });
1539
+ this.buildIgnoreSectionContent(ignoreBox);
1540
+ leftColumn.add(ignoreBox);
1541
+
1542
+ // Include patterns section
1543
+ const includeBox = new BoxRenderable(this.renderer, {
1544
+ id: 'include-box',
1545
+ flexDirection: 'column',
1546
+ border: true,
1547
+ borderStyle: 'rounded',
1548
+ borderColor: this.settingsSection === 'include' ? COLORS.borderFocused : COLORS.border,
1549
+ title: ' Include Patterns (n) ',
1550
+ titleAlignment: 'left',
1343
1551
  padding: 1,
1552
+ flexGrow: 1,
1344
1553
  });
1554
+ this.buildIncludeSectionContent(includeBox);
1555
+ leftColumn.add(includeBox);
1345
1556
 
1346
- // Section content
1347
- if (this.settingsSection === 'ignore') {
1348
- this.buildIgnoreSectionContent(contentPanel);
1349
- } else if (this.settingsSection === 'include') {
1350
- this.buildIncludeSectionContent(contentPanel);
1351
- } else if (this.settingsSection === 'scripts') {
1352
- this.buildScriptsSectionContent(contentPanel);
1353
- }
1557
+ contentPanel.add(leftColumn);
1558
+
1559
+ // Right column - Scripts list
1560
+ const scriptsBox = new BoxRenderable(this.renderer, {
1561
+ id: 'scripts-box',
1562
+ flexDirection: 'column',
1563
+ border: true,
1564
+ borderStyle: 'rounded',
1565
+ borderColor: this.settingsSection === 'scripts' ? COLORS.borderFocused : COLORS.border,
1566
+ title: ' Scripts ',
1567
+ titleAlignment: 'left',
1568
+ flexGrow: 2,
1569
+ padding: 1,
1570
+ });
1571
+ this.buildScriptsSectionContent(scriptsBox);
1572
+ contentPanel.add(scriptsBox);
1354
1573
 
1355
1574
  this.settingsContainer.add(contentPanel);
1356
1575
 
@@ -1374,9 +1593,10 @@ class ProcessManager {
1374
1593
  ]
1375
1594
  : [
1376
1595
  { key: 'tab', desc: 'section' },
1377
- { key: 'a', desc: 'add' },
1378
- { key: 'd', desc: 'delete' },
1379
1596
  { key: 'space', desc: 'toggle' },
1597
+ { key: 'i', desc: 'add ignore' },
1598
+ { key: 'n', desc: 'add include' },
1599
+ { key: 'd', desc: 'delete' },
1380
1600
  { key: 'esc', desc: 'back' },
1381
1601
  ];
1382
1602
 
@@ -1393,26 +1613,38 @@ class ProcessManager {
1393
1613
  this.renderer.root.add(this.settingsContainer);
1394
1614
  }
1395
1615
 
1396
- buildIgnoreSectionContent(container) {
1397
- const desc = new TextRenderable(this.renderer, {
1398
- id: 'ignore-desc',
1399
- content: t`${fg(COLORS.textDim)('Patterns to exclude from script list. Use * as wildcard.')}`,
1400
- });
1401
- container.add(desc);
1402
-
1403
- container.add(new TextRenderable(this.renderer, { id: 'spacer', content: '' }));
1616
+ buildDisplaySectionContent(container) {
1617
+ const options = [
1618
+ { id: 'lineNumbers', label: 'Show Line Numbers', value: this.showLineNumbers },
1619
+ { id: 'timestamps', label: 'Show Timestamps', value: this.showTimestamps },
1620
+ ];
1404
1621
 
1622
+ options.forEach((option, idx) => {
1623
+ const isFocused = this.settingsSection === 'display' && idx === this.settingsIndex;
1624
+ const indicator = isFocused ? '>' : ' ';
1625
+ const checkbox = option.value ? '[x]' : '[ ]';
1626
+ const checkColor = option.value ? COLORS.success : COLORS.textDim;
1627
+
1628
+ const line = new TextRenderable(this.renderer, {
1629
+ id: `display-option-${idx}`,
1630
+ content: t`${fg(isFocused ? COLORS.accent : COLORS.textDim)(indicator)} ${fg(checkColor)(checkbox)} ${fg(COLORS.text)(option.label)}`,
1631
+ });
1632
+ container.add(line);
1633
+ });
1634
+ }
1635
+
1636
+ buildIgnoreSectionContent(container) {
1405
1637
  const patterns = this.config.ignore || [];
1406
1638
 
1407
1639
  if (patterns.length === 0) {
1408
1640
  const empty = new TextRenderable(this.renderer, {
1409
1641
  id: 'ignore-empty',
1410
- content: t`${fg(COLORS.textDim)('No ignore patterns defined. Press A to add.')}`,
1642
+ content: t`${fg(COLORS.textDim)('Press i to add')}`,
1411
1643
  });
1412
1644
  container.add(empty);
1413
1645
  } else {
1414
1646
  patterns.forEach((pattern, idx) => {
1415
- const isFocused = idx === this.settingsIndex;
1647
+ const isFocused = this.settingsSection === 'ignore' && idx === this.settingsIndex;
1416
1648
  const indicator = isFocused ? '>' : ' ';
1417
1649
 
1418
1650
  const line = new TextRenderable(this.renderer, {
@@ -1425,25 +1657,17 @@ class ProcessManager {
1425
1657
  }
1426
1658
 
1427
1659
  buildIncludeSectionContent(container) {
1428
- const desc = new TextRenderable(this.renderer, {
1429
- id: 'include-desc',
1430
- content: t`${fg(COLORS.textDim)('Only show scripts matching these patterns. Use * as wildcard.')}`,
1431
- });
1432
- container.add(desc);
1433
-
1434
- container.add(new TextRenderable(this.renderer, { id: 'spacer', content: '' }));
1435
-
1436
1660
  const patterns = this.config.include || [];
1437
1661
 
1438
1662
  if (patterns.length === 0) {
1439
1663
  const empty = new TextRenderable(this.renderer, {
1440
1664
  id: 'include-empty',
1441
- content: t`${fg(COLORS.textDim)('No include patterns (all scripts shown). Press A to add.')}`,
1665
+ content: t`${fg(COLORS.textDim)('Press n to add')}`,
1442
1666
  });
1443
1667
  container.add(empty);
1444
1668
  } else {
1445
1669
  patterns.forEach((pattern, idx) => {
1446
- const isFocused = idx === this.settingsIndex;
1670
+ const isFocused = this.settingsSection === 'include' && idx === this.settingsIndex;
1447
1671
  const indicator = isFocused ? '>' : ' ';
1448
1672
 
1449
1673
  const line = new TextRenderable(this.renderer, {
@@ -1456,19 +1680,11 @@ class ProcessManager {
1456
1680
  }
1457
1681
 
1458
1682
  buildScriptsSectionContent(container) {
1459
- const desc = new TextRenderable(this.renderer, {
1460
- id: 'scripts-desc',
1461
- content: t`${fg(COLORS.textDim)('Toggle individual scripts. Ignored scripts are hidden from selection.')}`,
1462
- });
1463
- container.add(desc);
1464
-
1465
- container.add(new TextRenderable(this.renderer, { id: 'spacer', content: '' }));
1466
-
1467
1683
  const ignorePatterns = this.config.ignore || [];
1468
1684
 
1469
1685
  this.allScripts.forEach((script, idx) => {
1470
1686
  const isIgnored = ignorePatterns.includes(script.name);
1471
- const isFocused = idx === this.settingsIndex;
1687
+ const isFocused = this.settingsSection === 'scripts' && idx === this.settingsIndex;
1472
1688
  const indicator = isFocused ? '>' : ' ';
1473
1689
  const checkbox = isIgnored ? '[x]' : '[ ]';
1474
1690
  const checkColor = isIgnored ? COLORS.error : COLORS.success;
@@ -1564,14 +1780,22 @@ class ProcessManager {
1564
1780
  this.scriptLines = this.scripts.map((script, index) => {
1565
1781
  const isSelected = this.selectedScripts.has(script.name);
1566
1782
  const isFocused = index === this.selectedIndex;
1567
- const checkIcon = isSelected ? '●' : '○';
1568
- const checkColor = isSelected ? COLORS.success : COLORS.textDim;
1569
1783
  const processColor = this.processColors.get(script.name) || COLORS.text;
1570
1784
  const nameColor = isFocused ? COLORS.text : processColor;
1785
+ const numberColor = processColor;
1786
+ const bracketColor = processColor;
1571
1787
  const bgColor = isFocused ? COLORS.bgHighlight : null;
1572
1788
 
1573
- // Build styled content - all in one template, no nesting
1574
- const content = t`${fg(checkColor)(checkIcon)} ${fg(nameColor)(script.displayName)}`;
1789
+ // Show number for first 9 scripts
1790
+ const numberLabel = index < 9 ? ` ${index + 1}` : ' ';
1791
+
1792
+ // Build checkbox with colored brackets and white x (like running screen)
1793
+ let content;
1794
+ if (isSelected) {
1795
+ content = t`${fg(numberColor)(numberLabel)} ${fg(bracketColor)('[')}${fg(COLORS.text)('x')}${fg(bracketColor)(']')} ${fg(nameColor)(script.displayName)}`;
1796
+ } else {
1797
+ content = t`${fg(numberColor)(numberLabel)} ${fg(bracketColor)('[ ]')} ${fg(nameColor)(script.displayName)}`;
1798
+ }
1575
1799
 
1576
1800
  const lineContainer = new BoxRenderable(this.renderer, {
1577
1801
  id: `script-box-${index}`,
@@ -1815,87 +2039,65 @@ class ProcessManager {
1815
2039
  }
1816
2040
 
1817
2041
  updateRunningUI() {
1818
- // Update existing panes instead of rebuilding everything
2042
+ // Update existing panes incrementally, or rebuild if needed
1819
2043
  if (this.paneScrollBoxes.size > 0) {
1820
- // Update each pane's content
2044
+ // Incremental update - just append new lines to existing panes
2045
+ let hasNewContent = false;
2046
+
1821
2047
  for (const [paneId, scrollBox] of this.paneScrollBoxes.entries()) {
1822
2048
  const pane = findPaneById(this.paneRoot, paneId);
1823
2049
  if (pane && scrollBox && scrollBox.content) {
1824
- // Check if filter state changed or if paused (requires rebuild)
1825
- const currentFilterState = JSON.stringify({
1826
- filter: pane.filter || '',
1827
- hidden: pane.hidden || [],
1828
- processes: pane.processes || [],
1829
- colorFilter: pane.colorFilter || null,
1830
- });
1831
- const previousFilterState = this.paneFilterState.get(paneId);
1832
- const filterChanged = currentFilterState !== previousFilterState;
1833
- const needsRebuild = filterChanged || this.isPaused;
2050
+ const lines = this.getOutputLinesForPane(pane);
2051
+ const lastRenderedLineNumber = this.paneLineCount.get(paneId) || 0;
1834
2052
 
1835
- if (needsRebuild) {
1836
- // Filter changed - need to rebuild all content
1837
- this.paneFilterState.set(paneId, currentFilterState);
1838
-
1839
- // Remove all children
1840
- if (scrollBox.content.children) {
1841
- while (scrollBox.content.children.length > 0) {
1842
- const child = scrollBox.content.children[0];
1843
- if (child && child.id) {
1844
- scrollBox.content.remove(child.id);
1845
- } else {
1846
- break;
1847
- }
1848
- }
1849
- }
1850
-
1851
- // Rebuild all content
1852
- const height = scrollBox.height || this.renderer.height - 6;
1853
- this.buildPaneOutput(pane, scrollBox.content, height);
1854
-
1855
- // Update line count after rebuild
1856
- const lines = this.getOutputLinesForPane(pane);
1857
- this.paneLineCount.set(paneId, lines.length);
1858
-
1859
- // Auto-scroll to bottom after filter change
1860
- if (!this.isPaused) {
1861
- scrollBox.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER });
1862
- }
1863
- } else {
1864
- // No filter change - just append new lines
1865
- const lines = this.getOutputLinesForPane(pane);
1866
- const lastRenderedCount = this.paneLineCount.get(paneId) || 0;
2053
+ // Find lines that haven't been rendered yet (based on absolute line number)
2054
+ const newLines = lines.filter(line => line.lineNumber > lastRenderedLineNumber);
2055
+
2056
+ if (newLines.length > 0) {
2057
+ hasNewContent = true;
1867
2058
 
1868
- if (lines.length > lastRenderedCount) {
1869
- const newLines = lines.slice(lastRenderedCount);
2059
+ for (let i = 0; i < newLines.length; i++) {
2060
+ const line = newLines[i];
2061
+ const processColor = this.processColors.get(line.process) || COLORS.text;
2062
+ const trimmedText = line.text.trim();
2063
+
2064
+ // Build content with proper template literal
2065
+ const lineNumber = this.showLineNumbers ? String(line.lineNumber).padStart(4, ' ') : '';
2066
+ const timestamp = this.showTimestamps ? new Date(line.timestamp).toLocaleTimeString('en-US', { hour12: false }) : '';
1870
2067
 
1871
- for (let i = 0; i < newLines.length; i++) {
1872
- const line = newLines[i];
1873
- const lineIndex = lastRenderedCount + i;
1874
- const processColor = this.processColors.get(line.process) || COLORS.text;
1875
- const trimmedText = line.text.trim();
1876
-
1877
- const outputLine = new TextRenderable(this.renderer, {
1878
- id: `output-${pane.id}-${lineIndex}`,
1879
- content: t`${fg(processColor)(`[${line.process}]`)} ${trimmedText}`,
1880
- bg: '#000000',
1881
- });
1882
-
1883
- scrollBox.content.add(outputLine);
2068
+ let content;
2069
+ if (this.showLineNumbers && this.showTimestamps) {
2070
+ content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
2071
+ } else if (this.showLineNumbers) {
2072
+ content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
2073
+ } else if (this.showTimestamps) {
2074
+ content = t`${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
2075
+ } else {
2076
+ content = t`${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
1884
2077
  }
1885
2078
 
1886
- // Update line count
1887
- this.paneLineCount.set(paneId, lines.length);
2079
+ const outputLine = new TextRenderable(this.renderer, {
2080
+ id: `output-${pane.id}-${line.lineNumber}`,
2081
+ content: content,
2082
+ bg: '#000000',
2083
+ });
2084
+
2085
+ scrollBox.content.add(outputLine);
1888
2086
 
1889
- // Auto-scroll to bottom if not paused
1890
- if (!this.isPaused) {
1891
- scrollBox.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER });
2087
+ // Remove oldest line if we exceed maxOutputLines to maintain rolling window
2088
+ if (scrollBox.content.children && scrollBox.content.children.length > this.maxOutputLines) {
2089
+ const oldestChild = scrollBox.content.children[0];
2090
+ scrollBox.content.remove(oldestChild);
1892
2091
  }
1893
2092
  }
1894
- }
1895
-
1896
- // Update scrollbar visibility based on pause state
1897
- if (scrollBox.verticalScrollBar) {
1898
- scrollBox.verticalScrollBar.width = this.isPaused ? 1 : 0;
2093
+
2094
+ // Update to track the last absolute line number we rendered
2095
+ this.paneLineCount.set(paneId, newLines[newLines.length - 1].lineNumber);
2096
+
2097
+ // Auto-scroll to bottom if not paused
2098
+ if (!this.isPaused && scrollBox.scrollTo) {
2099
+ scrollBox.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER });
2100
+ }
1899
2101
  }
1900
2102
  }
1901
2103
  }
@@ -1920,9 +2122,25 @@ class ProcessManager {
1920
2122
 
1921
2123
  // Trim whitespace and let text wrap naturally - ScrollBox will handle overflow
1922
2124
  const trimmedText = line.text.trim();
2125
+
2126
+ // Build content with proper template literal
2127
+ const lineNumber = this.showLineNumbers ? String(line.lineNumber).padStart(4, ' ') : '';
2128
+ const timestamp = this.showTimestamps ? new Date(line.timestamp).toLocaleTimeString('en-US', { hour12: false }) : '';
2129
+
2130
+ let content;
2131
+ if (this.showLineNumbers && this.showTimestamps) {
2132
+ content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
2133
+ } else if (this.showLineNumbers) {
2134
+ content = t`${fg(COLORS.textDim)(lineNumber)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
2135
+ } else if (this.showTimestamps) {
2136
+ content = t`${fg(COLORS.textDim)(`[${timestamp}]`)} ${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
2137
+ } else {
2138
+ content = t`${fg(processColor)(`[${line.process}]`)} ${trimmedText}`;
2139
+ }
2140
+
1923
2141
  const outputLine = new TextRenderable(this.renderer, {
1924
2142
  id: `output-${pane.id}-${i}`,
1925
- content: t`${fg(processColor)(`[${line.process}]`)} ${trimmedText}`,
2143
+ content: content,
1926
2144
  bg: '#000000', // Black background for pane content
1927
2145
  });
1928
2146
 
@@ -2012,10 +2230,8 @@ class ProcessManager {
2012
2230
 
2013
2231
  this.buildPaneOutput(pane, outputBox.content, height);
2014
2232
 
2015
- // Restore or set scroll position
2016
- setTimeout(() => {
2017
- if (!outputBox || !outputBox.scrollTo) return;
2018
-
2233
+ // Restore or set scroll position immediately
2234
+ if (outputBox && outputBox.scrollTo) {
2019
2235
  if (this.isPaused && this.paneScrollPositions.has(pane.id)) {
2020
2236
  // Restore saved scroll position when paused
2021
2237
  const savedPos = this.paneScrollPositions.get(pane.id);
@@ -2024,7 +2240,7 @@ class ProcessManager {
2024
2240
  // Auto-scroll to bottom when not paused
2025
2241
  outputBox.scrollTo({ x: 0, y: Number.MAX_SAFE_INTEGER });
2026
2242
  }
2027
- }, 0);
2243
+ }
2028
2244
 
2029
2245
  paneContainer.add(outputBox);
2030
2246
  return paneContainer;
@@ -2170,15 +2386,7 @@ class ProcessManager {
2170
2386
  paddingLeft: 1,
2171
2387
  });
2172
2388
 
2173
- // Pane count indicator
2174
- const allPanes = getAllPaneIds(this.paneRoot);
2175
- if (allPanes.length > 1) {
2176
- const paneIndicator = new TextRenderable(this.renderer, {
2177
- id: 'pane-indicator',
2178
- content: t`${fg(COLORS.cyan)(`[${allPanes.length} panes]`)} `,
2179
- });
2180
- processBar.add(paneIndicator);
2181
- }
2389
+
2182
2390
 
2183
2391
  // Add each process with checkbox showing visibility in focused pane
2184
2392
  const focusedPane = findPaneById(this.paneRoot, this.focusedPaneId);
@@ -2191,13 +2399,25 @@ class ProcessManager {
2191
2399
  const processColor = this.processColors.get(script.name) || COLORS.text;
2192
2400
  const isSelected = this.selectedIndex === index;
2193
2401
  const isVisible = this.isProcessVisibleInPane(script.name, focusedPane);
2194
- const checkbox = isVisible ? '[x]' : '[ ]';
2195
2402
  const nameColor = isSelected ? COLORS.accent : (isVisible ? processColor : COLORS.textDim);
2403
+ const numberColor = isVisible ? processColor : COLORS.textDim;
2196
2404
  const indicator = isSelected ? '>' : ' ';
2405
+ const bracketColor = isVisible ? processColor : COLORS.textDim;
2406
+
2407
+ // Show number for first 9 processes
2408
+ const numberLabel = index < 9 ? `${index + 1}` : ' ';
2409
+
2410
+ // Build content - can't nest template literals, so build entire thing at once
2411
+ let content;
2412
+ if (isVisible) {
2413
+ 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)}`;
2414
+ } else {
2415
+ content = t`${fg(numberColor)(numberLabel)} ${fg(isSelected ? COLORS.accent : COLORS.textDim)(indicator)}${fg(bracketColor)('[ ]')} ${fg(statusColor)(statusIcon)} ${fg(nameColor)(script.displayName)}`;
2416
+ }
2197
2417
 
2198
2418
  const processItem = new TextRenderable(this.renderer, {
2199
2419
  id: `process-item-${index}`,
2200
- content: t`${fg(isSelected ? COLORS.accent : COLORS.textDim)(indicator)}${fg(isVisible ? COLORS.text : COLORS.textDim)(checkbox)} ${fg(nameColor)(script.displayName)} ${fg(statusColor)(statusIcon)}`,
2420
+ content: content,
2201
2421
  });
2202
2422
  processBar.add(processItem);
2203
2423
  });
@@ -2268,6 +2488,17 @@ class ProcessManager {
2268
2488
  leftSide.add(filterIndicator);
2269
2489
  }
2270
2490
 
2491
+ // Input mode indicator if active
2492
+ if (this.isInputMode) {
2493
+ const scriptName = this.scripts[this.selectedIndex]?.displayName || '';
2494
+ const inputText = `[${scriptName}]> ${this.inputModeText}_`;
2495
+ const inputIndicator = new TextRenderable(this.renderer, {
2496
+ id: 'input-indicator',
2497
+ content: t`${fg(COLORS.success)(inputText)}`,
2498
+ });
2499
+ leftSide.add(inputIndicator);
2500
+ }
2501
+
2271
2502
  // Color filter indicator if active on focused pane
2272
2503
  if (focusedPane?.colorFilter) {
2273
2504
  const colorMap = {
@@ -2296,13 +2527,15 @@ class ProcessManager {
2296
2527
 
2297
2528
  const shortcuts = [
2298
2529
  { key: '\\', desc: 'panes', color: COLORS.cyan },
2299
- { key: 'spc', desc: 'toggle', color: COLORS.success },
2530
+ { key: '1-9', desc: 'toggle', color: COLORS.success },
2531
+ { key: 'i', desc: 'input', color: COLORS.success },
2300
2532
  { key: 'n', desc: 'name', color: COLORS.accent },
2301
2533
  { key: 'p', desc: 'pause', color: COLORS.warning },
2302
2534
  { key: '/', desc: 'filter', color: COLORS.cyan },
2303
2535
  { key: 'c', desc: 'color', color: COLORS.magenta },
2304
2536
  { key: 's', desc: 'stop', color: COLORS.error },
2305
2537
  { key: 'r', desc: 'restart', color: COLORS.success },
2538
+ { key: 'o', desc: 'settings', color: COLORS.magenta },
2306
2539
  { key: 'q', desc: 'quit', color: COLORS.error },
2307
2540
  ];
2308
2541
 
@@ -2352,6 +2585,7 @@ async function main() {
2352
2585
  }
2353
2586
 
2354
2587
  const renderer = await createCliRenderer();
2588
+ renderer.start(); // Start the automatic render loop
2355
2589
  const manager = new ProcessManager(renderer, scripts);
2356
2590
 
2357
2591
  // Handle cleanup on exit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "startall",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -15,7 +15,8 @@
15
15
  "demo:worker2": "node -e \"setInterval(() => console.log('Processing jobs...'), 100)\"",
16
16
  "demo:errors": "node -e \"setInterval(() => { console.log('\\x1b[32mOK: All good\\x1b[0m'); console.log('\\x1b[33mWARN: Deprecated API used\\x1b[0m'); console.log('\\x1b[31mERROR: Connection failed\\x1b[0m'); console.log('Normal log message'); }, 2000)\"",
17
17
  "demo:build": "node -e \"let i=0; setInterval(() => { i++; if(i%5===0) console.log('\\x1b[31mError: Type mismatch in file.ts:42\\x1b[0m'); else if(i%3===0) console.log('\\x1b[33mWarning: Unused variable\\x1b[0m'); else console.log('\\x1b[36mCompiling module ' + i + '...\\x1b[0m'); }, 800)\"",
18
- "demo:longtext": "node -e \"setInterval(() => { console.log('\\x1b[36m[2024-01-20T12:34:56.789Z] Executing SQL query: SELECT users.id, users.name, users.email, orders.order_id, orders.total, products.name FROM users INNER JOIN orders ON users.id = orders.user_id LEFT JOIN products ON orders.product_id = products.id WHERE users.created_at > NOW() - INTERVAL 30 DAY AND orders.status = \\'completed\\' ORDER BY orders.total DESC LIMIT 100\\x1b[0m'); console.log('Fetched 42 records in 127ms'); }, 1500)\""
18
+ "demo:longtext": "node -e \"setInterval(() => { console.log('\\x1b[36m[2024-01-20T12:34:56.789Z] Executing SQL query: SELECT users.id, users.name, users.email, orders.order_id, orders.total, products.name FROM users INNER JOIN orders ON users.id = orders.user_id LEFT JOIN products ON orders.product_id = products.id WHERE users.created_at > NOW() - INTERVAL 30 DAY AND orders.status = \\'completed\\' ORDER BY orders.total DESC LIMIT 100\\x1b[0m'); console.log('Fetched 42 records in 127ms'); }, 1500)\"",
19
+ "demo:cmd": "cmd"
19
20
  },
20
21
  "keywords": [],
21
22
  "author": "",
package/screenshot.png ADDED
Binary file