pyre-cli 10.1.0 → 10.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +211 -494
  2. package/dist/{checkCmd-RKW6QGDR.js → checkCmd-WQAHAUWO.js} +1 -1
  3. package/dist/{chunk-GHRDW646.js → chunk-23YWCI3O.js} +375 -135
  4. package/dist/{service-Q74BBILO.js → chunk-KFPTD266.js} +43 -3
  5. package/dist/{doctor-GYHFTNZQ.js → chunk-KVUMOA6C.js} +7 -6
  6. package/dist/{chunk-53O2RGSL.js → chunk-QM5L43EK.js} +19 -2
  7. package/dist/{chunk-6UHZP4QP.js → chunk-XTOOEKLM.js} +24 -12
  8. package/dist/{collectors-H43PN4PS.js → collectors-JDN5Y7EV.js} +1 -1
  9. package/dist/{completions-JKYEJNGR.js → completions-ZVTNE52B.js} +15 -11
  10. package/dist/{config-GYGJXU2Q.js → config-J4YMADEF.js} +3 -1
  11. package/dist/doctor-LHFW4UJP.js +10 -0
  12. package/dist/firebaseSync-ON3RZ6KX.js +91 -0
  13. package/dist/index.js +4245 -736
  14. package/dist/monitors/platform/windows-ui.ps1 +42 -0
  15. package/dist/{netUsageCmd-WKBTW2YG.js → netUsageCmd-67IBZZYN.js} +2 -2
  16. package/dist/{pipeCmd-7REDBLBU.js → pipeCmd-2N4JPKYE.js} +1 -1
  17. package/dist/{prometheusCmd-PL64BLN5.js → prometheusCmd-LEUM5WCZ.js} +1 -1
  18. package/dist/serveCmd-L7LVJOZN.js +353 -0
  19. package/dist/service-7GIX3YNC.js +16 -0
  20. package/dist/syncCmd-42IRF633.js +246 -0
  21. package/dist/{topoCmd-DU2HHZNH.js → topoCmd-DZ3YS5IL.js} +1 -1
  22. package/dist/{watchdogCmd-UF53JPZB.js → watchdogCmd-6GR4UN6B.js} +7 -7
  23. package/dist/{wizard-TD7BGX6O.js → wizard-JQMQWFAW.js} +1 -1
  24. package/package.json +4 -3
  25. package/parity.md +2 -0
  26. package/src/completions.ts +15 -11
  27. package/src/easy_features.test.ts +149 -0
  28. package/src/firebaseSync.ts +157 -0
  29. package/src/formatters/html_template.ts +3086 -598
  30. package/src/formatters/render.ts +25 -12
  31. package/src/index.ts +518 -24
  32. package/src/live/input.ts +501 -148
  33. package/src/live/render.ts +163 -18
  34. package/src/live/state.ts +52 -2
  35. package/src/live/types.ts +1 -1
  36. package/src/monitors/collectors.ts +3 -10
  37. package/src/monitors/platform/windows-ui.ps1 +42 -0
  38. package/src/monitors/platform/windows.ts +442 -147
  39. package/src/monitors/types.ts +8 -0
  40. package/src/serveCmd.ts +17 -2
  41. package/src/service.ts +49 -0
  42. package/src/state/config.ts +48 -0
  43. package/src/syncCmd.ts +159 -11
  44. package/src/web/dashboard.ts +205 -0
  45. package/dist/serveCmd-2H5VLF3P.js +0 -153
  46. package/dist/syncCmd-A2W3QB3S.js +0 -126
package/src/live/input.ts CHANGED
@@ -15,9 +15,10 @@ import { exportSnapshot, startLogging, stopLogging, toggleLogging, writeLogRow }
15
15
  import { render, footerLine, checkAlerts, invalidateTableCache, invalidateFrame } from './render.js';
16
16
  import type { LiveOptions, ExportFormat, InputMode, SortMode, GraphMode, ActivePanel } from './types.js';
17
17
  import { startP2PServer } from '../p2p/index.js';
18
- import { writeConfig } from '../state/config.js';
18
+ import { writeConfig, readConfig } from '../state/config.js';
19
19
  import { AVAILABLE_AI_MODELS } from '../ai/models.js';
20
20
  import { loadPlugins, executePlugins } from '../plugins/index.js';
21
+ import { PALETTE_COMMANDS } from './state.js';
21
22
 
22
23
  function persistConfig() {
23
24
  writeConfig({
@@ -43,6 +44,9 @@ import { loadPlugins, executePlugins } from '../plugins/index.js';
43
44
  dockerModeConfirmed: state.dockerModeConfirmed,
44
45
  aiModel: state.aiModel,
45
46
  aiBackend: state.aiBackend,
47
+ bookmarks: state.bookmarks.map(bm => ({ ...bm })),
48
+ graphZoomLevel: state.graphZoomLevel,
49
+ panelLayout: [...state.panelLayout],
46
50
  });
47
51
  }
48
52
 
@@ -59,7 +63,7 @@ function isEscKey(key: readline.Key, str?: string): boolean {
59
63
  return key.name === 'escape' || key.name === 'esc' || key.sequence === '\x1b' || str === '\x1b';
60
64
  }
61
65
 
62
- function handleInputModeKey(str: string, key: readline.Key) {
66
+ function handleInputModeKey(str: string, key: readline.Key) {
63
67
  if (isEscKey(key, str)) {
64
68
  state.inputMode = null;
65
69
  state.inputBuffer = '';
@@ -67,6 +71,59 @@ function handleInputModeKey(str: string, key: readline.Key) {
67
71
  return;
68
72
  }
69
73
 
74
+ if (state.inputMode === 'quick-ref') {
75
+ render();
76
+ return;
77
+ }
78
+
79
+ if (state.inputMode === 'bookmark-save') {
80
+ if (key.name === 'backspace') {
81
+ state.inputBuffer = state.inputBuffer.slice(0, -1);
82
+ render();
83
+ return;
84
+ }
85
+ if (isEnterKey(key, str)) {
86
+ saveBookmark();
87
+ return;
88
+ }
89
+ if (str && str.length === 1 && !key.ctrl && !key.meta) {
90
+ state.inputBuffer += str;
91
+ render();
92
+ }
93
+ return;
94
+ }
95
+
96
+ if (state.inputMode === 'command-palette') {
97
+ if (isUpKey(key, str) || key.name === 'k') {
98
+ state.paletteIndex = Math.max(0, state.paletteIndex - 1);
99
+ render();
100
+ return;
101
+ }
102
+ if (isDownKey(key, str) || key.name === 'j') {
103
+ state.paletteIndex = Math.min(state.paletteFilteredCommands.length - 1, state.paletteIndex + 1);
104
+ render();
105
+ return;
106
+ }
107
+ if (isEnterKey(key, str)) {
108
+ executePaletteCommand();
109
+ return;
110
+ }
111
+ if (key.name === 'backspace') {
112
+ state.inputBuffer = state.inputBuffer.slice(0, -1);
113
+ updatePaletteFilter();
114
+ state.paletteIndex = 0;
115
+ render();
116
+ return;
117
+ }
118
+ if (str && str.length === 1 && !key.ctrl && !key.meta) {
119
+ state.inputBuffer += str;
120
+ updatePaletteFilter();
121
+ state.paletteIndex = 0;
122
+ render();
123
+ }
124
+ return;
125
+ }
126
+
70
127
  if (state.inputMode === 'customizer') {
71
128
  const themesList = Object.keys(THEMES) as ThemeName[];
72
129
 
@@ -526,21 +583,27 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
526
583
  * rendering interval. Idempotent — calling while already
527
584
  * running is a no-op.
528
585
  */
529
- export async function startLive(opts: LiveOptions, splashPromise?: Promise<void>) {
530
- if (state.running) return;
531
- state.running = true;
532
- state.paused = false;
533
- state.detailed = !!opts.detailed;
534
- if (opts.theme) state.currentTheme = opts.theme;
535
- state.interval = opts.interval;
536
- if (opts.exportDir) state.exportDir = opts.exportDir;
537
- if (opts.alertCpu !== undefined) state.CPU_ALERT_PCT = opts.alertCpu;
538
- if (opts.alertTemp !== undefined) state.TEMP_ALERT_C = opts.alertTemp;
539
- if (opts.tempUnit) state.tempUnit = opts.tempUnit;
540
- state.termWidth = process.stdout.columns || 80;
541
- state.termHeight = process.stdout.rows || 24;
542
- state.history.reset();
543
- state.history.setMaxLen(Math.max(20, Math.min(200, state.termWidth - 30)));
586
+ export async function startLive(opts: LiveOptions, splashPromise?: Promise<void>) {
587
+ if (state.running) return;
588
+ state.running = true;
589
+ state.paused = false;
590
+ state.detailed = !!opts.detailed;
591
+ if (opts.theme) state.currentTheme = opts.theme;
592
+ state.interval = opts.interval;
593
+ if (opts.exportDir) state.exportDir = opts.exportDir;
594
+ if (opts.alertCpu !== undefined) state.CPU_ALERT_PCT = opts.alertCpu;
595
+ if (opts.alertTemp !== undefined) state.TEMP_ALERT_C = opts.alertTemp;
596
+ if (opts.tempUnit) state.tempUnit = opts.tempUnit;
597
+ state.termWidth = process.stdout.columns || 80;
598
+ state.termHeight = process.stdout.rows || 24;
599
+ state.history.reset();
600
+ state.history.setMaxLen(Math.max(20, 40 * (readConfig().graphZoomLevel || 1)));
601
+ state.bookmarks = readConfig().bookmarks || [];
602
+ state.bookmarkRecallIndex = 0;
603
+ state.graphZoomLevel = readConfig().graphZoomLevel || 1;
604
+ state.panelHistory = [];
605
+ state.paletteIndex = 0;
606
+ state.paletteFilteredCommands = [...PALETTE_COMMANDS];
544
607
 
545
608
  await loadPlugins().catch(() => {});
546
609
  const warmupPromise = doWarmup();
@@ -621,6 +684,29 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
621
684
  state.inputBuffer = state.processFilter;
622
685
  render();
623
686
  return;
687
+ case '?':
688
+ state.inputMode = state.inputMode === 'quick-ref' ? null : 'quick-ref';
689
+ render();
690
+ return;
691
+ case ':':
692
+ state.inputMode = state.inputMode === 'command-palette' ? null : 'command-palette';
693
+ state.inputBuffer = '';
694
+ state.paletteIndex = 0;
695
+ state.paletteFilteredCommands = [...PALETTE_COMMANDS];
696
+ render();
697
+ return;
698
+ case '[': {
699
+ state.graphZoomLevel = Math.max(1, state.graphZoomLevel - 1);
700
+ state.history.setMaxLen(Math.max(20, 40 * state.graphZoomLevel));
701
+ setStatus(`History zoom: ${state.graphZoomLevel}x (${state.history.maxLen} points)`);
702
+ return;
703
+ }
704
+ case ']': {
705
+ state.graphZoomLevel = Math.min(5, state.graphZoomLevel + 1);
706
+ state.history.setMaxLen(Math.max(20, 40 * state.graphZoomLevel));
707
+ setStatus(`History zoom: ${state.graphZoomLevel}x (${state.history.maxLen} points)`);
708
+ return;
709
+ }
624
710
  case '0': case '1': case '2': case '3': case '4':
625
711
  case '5': case '6': case '7': case '8': case '9': {
626
712
  const tabMap: Record<string, ActivePanel> = {
@@ -629,6 +715,7 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
629
715
  };
630
716
  const panelId = tabMap[key.sequence];
631
717
  if (panelId) {
718
+ pushPanelHistory(panelId);
632
719
  state.activePanel = state.activePanel === panelId ? 'grid' : panelId;
633
720
  setStatus(state.activePanel === 'grid' ? 'Grid view' : `${panelId} panel`);
634
721
  render();
@@ -636,18 +723,21 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
636
723
  return;
637
724
  }
638
725
  case 'P': {
726
+ pushPanelHistory('process');
639
727
  state.activePanel = state.activePanel === 'process' ? 'grid' : 'process';
640
728
  setStatus(state.activePanel === 'grid' ? 'Grid view' : 'Process panel');
641
729
  render();
642
730
  return;
643
731
  }
644
732
  case 'C': {
733
+ pushPanelHistory('containers');
645
734
  state.activePanel = state.activePanel === 'containers' ? 'grid' : 'containers';
646
735
  setStatus(state.activePanel === 'grid' ? 'Grid view' : 'Containers panel');
647
736
  render();
648
737
  return;
649
738
  }
650
739
  case 'B': {
740
+ pushPanelHistory('blender');
651
741
  state.activePanel = state.activePanel === 'blender' ? 'grid' : 'blender';
652
742
  setStatus(state.activePanel === 'grid' ? 'Grid view' : 'Blender panel');
653
743
  render();
@@ -682,10 +772,22 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
682
772
  render();
683
773
  break;
684
774
  case 'm':
685
- state.inputMode = 'ai-model';
686
- state.modelSelectionIndex = Math.max(0, AVAILABLE_AI_MODELS.findIndex(m => m.id === state.aiModel));
775
+ if (key.shift) {
776
+ // Shift+M: save bookmark
777
+ state.inputMode = 'bookmark-save';
778
+ state.inputBuffer = '';
779
+ render();
780
+ } else {
781
+ state.inputMode = 'ai-model';
782
+ state.modelSelectionIndex = Math.max(0, AVAILABLE_AI_MODELS.findIndex(m => m.id === state.aiModel));
783
+ render();
784
+ }
785
+ return;
786
+ case 'M':
787
+ // M (no shift): recall next bookmark
788
+ recallNextBookmark();
687
789
  render();
688
- break;
790
+ return;
689
791
  case 'p':
690
792
  state.paused = !state.paused;
691
793
  setStatus(state.paused ? 'Paused' : 'Resumed');
@@ -701,132 +803,176 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
701
803
  render();
702
804
  break;
703
805
  case 'd':
704
-
705
806
  state.detailed = !state.detailed;
706
807
  setStatus(`Detailed sensor mode: ${state.detailed ? 'on' : 'off'}`);
707
808
  break;
708
- case 'T':
709
- state.tempUnit = state.tempUnit === 'c' ? 'f' : 'c';
710
- setStatus(`Temperature unit: ${state.tempUnit.toUpperCase()}`);
711
- invalidateTableCache();
712
- render();
713
- break;
714
- case 's':
715
- const sortCycle: SortMode[] = ['cpu', 'mem', 'pid', 'user', 'command', 'state', 'threads', 'runtime'];
716
- const curIdx = sortCycle.indexOf(state.sortMode);
717
- state.sortMode = sortCycle[(curIdx + 1) % sortCycle.length];
718
- setStatus(`Sorting by ${state.sortMode}`);
719
- render();
720
- break;
721
- case 'k':
722
- state.inputMode = 'kill';
723
- state.inputBuffer = '';
724
- render();
725
- break;
726
- case 'S':
727
- state.inputMode = 'signal';
728
- state.inputBuffer = 'SIGTERM';
729
- render();
730
- break;
731
- case 't':
732
- state.treeView = !state.treeView;
733
- setStatus(state.treeView ? 'Tree view enabled' : 'Flat view enabled');
734
- render();
735
- break;
736
- case 'e':
737
- exportSnapshot();
738
- render();
739
- break;
740
- case 'l':
741
- toggleLogging();
742
- render();
743
- break;
744
- case 'f':
745
- const fmtCycle: ExportFormat[] = ['json', 'csv', 'tsv', 'html', 'md'];
746
- const curFmt = fmtCycle.indexOf(state.exportFormat);
747
- state.exportFormat = fmtCycle[(curFmt + 1) % fmtCycle.length];
748
- setStatus(`Export format: ${state.exportFormat}`);
749
- render();
750
- break;
751
- case 'tab':
752
- cycleTab(1);
753
- break;
754
- case 'up':
755
- if (state.lastData?.processes?.length) {
756
- state.trackedPid = null;
757
- state.processSelectionIndex = Math.max(0, state.processSelectionIndex - 1);
758
- render();
759
- }
760
- break;
761
- case 'down':
762
- if (state.lastData?.processes?.length) {
763
- state.trackedPid = null;
764
- const count = state.lastData.processes.length;
765
- state.processSelectionIndex = Math.min(count - 1, state.processSelectionIndex + 1);
766
- render();
767
- }
768
- break;
769
- case 'space':
770
- case ' ':
771
- if (state.lastData?.processes?.length && state.processSelectionIndex >= 0) {
772
- const sorted = state.lastData.processes;
773
- const proc = sorted[state.processSelectionIndex];
774
- if (proc) {
775
- if (state.trackedPid === proc.pid) {
776
- state.trackedPid = null;
777
- setStatus(`Stopped following PID ${proc.pid}`);
778
- } else {
779
- state.trackedPid = proc.pid;
780
- setStatus(`Following PID ${proc.pid} (${proc.command})`);
781
- }
782
- render();
783
- }
784
- }
785
- break;
786
- case 'return':
787
- case 'enter':
788
- if (state.inspectingProcess) {
789
- state.inspectingProcess = null;
790
- render();
791
- } else if (state.lastData?.processes?.length && state.processSelectionIndex >= 0) {
792
- const sorted = state.lastData.processes;
793
- const proc = sorted[state.processSelectionIndex];
794
- if (proc) {
795
- state.inspectingProcess = proc;
796
- state.trackedPid = proc.pid;
797
- setStatus(`Inspecting & following PID ${proc.pid}`);
798
- render();
799
- }
800
- }
801
- break;
802
- case 'escape':
803
- case 'esc':
804
- if (state.inspectingProcess) {
805
- state.inspectingProcess = null;
806
- render();
807
- } else if (state.trackedPid !== null) {
808
- state.trackedPid = null;
809
- setStatus('Stopped following process');
810
- render();
811
- } else if (state.activePanel !== 'grid') {
812
- state.activePanel = 'grid';
813
- setStatus('Grid view');
814
- render();
815
- } else {
816
- state.inputMode = 'menu';
817
- state.menuSelectionIndex = 0;
818
- render();
819
- }
820
- break;
821
- default:
822
- if (str && str.length === 1) {
823
- const tabId = tabKeyToId(str);
824
- if (tabId) {
825
- state.activePanel = tabId as ActivePanel;
826
- setStatus(`Panel: ${tabId.toUpperCase()}`);
827
- render();
828
- }
829
- }
809
+ case 'T':
810
+ state.tempUnit = state.tempUnit === 'c' ? 'f' : 'c';
811
+ setStatus(`Temperature unit: ${state.tempUnit.toUpperCase()}`);
812
+ invalidateTableCache();
813
+ render();
814
+ break;
815
+ case 's':
816
+ const sortCycle: SortMode[] = ['cpu', 'mem', 'pid', 'user', 'command', 'state', 'threads', 'runtime'];
817
+ const curIdx = sortCycle.indexOf(state.sortMode);
818
+ state.sortMode = sortCycle[(curIdx + 1) % sortCycle.length];
819
+ setStatus(`Sorting by ${state.sortMode}`);
820
+ render();
821
+ break;
822
+ case 'k':
823
+ state.inputMode = 'kill';
824
+ state.inputBuffer = '';
825
+ render();
826
+ break;
827
+ case 'S':
828
+ state.inputMode = 'signal';
829
+ state.inputBuffer = 'SIGTERM';
830
+ render();
831
+ break;
832
+ case 't':
833
+ state.treeView = !state.treeView;
834
+ setStatus(state.treeView ? 'Tree view enabled' : 'Flat view enabled');
835
+ render();
836
+ break;
837
+ case 'e':
838
+ exportSnapshot();
839
+ render();
840
+ break;
841
+ case 'l':
842
+ toggleLogging();
843
+ render();
844
+ break;
845
+ case 'f':
846
+ const fmtCycle: ExportFormat[] = ['json', 'csv', 'tsv', 'html', 'md'];
847
+ const curFmt = fmtCycle.indexOf(state.exportFormat);
848
+ state.exportFormat = fmtCycle[(curFmt + 1) % fmtCycle.length];
849
+ setStatus(`Export format: ${state.exportFormat}`);
850
+ render();
851
+ break;
852
+ case 'tab':
853
+ if (key.shift) {
854
+ // Shift+Tab: go back in panel history
855
+ if (state.panelHistory.length > 0) {
856
+ const prev = state.panelHistory.pop()!;
857
+ state.activePanel = prev as any;
858
+ setStatus(`Panel: ${prev.toUpperCase()} (back)`);
859
+ } else {
860
+ cycleTab(-1);
861
+ }
862
+ } else {
863
+ pushPanelHistory(state.activePanel === 'grid' ? 'cpu' : state.activePanel);
864
+ cycleTab(1);
865
+ }
866
+ break;
867
+ case 'up':
868
+ if (state.inputMode === 'command-palette') {
869
+ state.paletteIndex = Math.max(0, state.paletteIndex - 1);
870
+ render();
871
+ return;
872
+ }
873
+ if (state.lastData?.processes?.length) {
874
+ state.trackedPid = null;
875
+ state.processSelectionIndex = Math.max(0, state.processSelectionIndex - 1);
876
+ render();
877
+ }
878
+ break;
879
+ case 'down':
880
+ if (state.inputMode === 'command-palette') {
881
+ state.paletteIndex = Math.min(state.paletteFilteredCommands.length - 1, state.paletteIndex + 1);
882
+ render();
883
+ return;
884
+ }
885
+ if (state.lastData?.processes?.length) {
886
+ state.trackedPid = null;
887
+ const count = state.lastData.processes.length;
888
+ state.processSelectionIndex = Math.min(count - 1, state.processSelectionIndex + 1);
889
+ render();
890
+ }
891
+ break;
892
+ case 'space':
893
+ case ' ':
894
+ if (state.lastData?.processes?.length && state.processSelectionIndex >= 0) {
895
+ const sorted = state.lastData.processes;
896
+ const proc = sorted[state.processSelectionIndex];
897
+ if (proc) {
898
+ if (state.trackedPid === proc.pid) {
899
+ state.trackedPid = null;
900
+ setStatus(`Stopped following PID ${proc.pid}`);
901
+ } else {
902
+ state.trackedPid = proc.pid;
903
+ setStatus(`Following PID ${proc.pid} (${proc.command})`);
904
+ }
905
+ render();
906
+ }
907
+ }
908
+ break;
909
+ case 'return':
910
+ case 'enter':
911
+ if (state.inputMode === 'command-palette') {
912
+ executePaletteCommand();
913
+ return;
914
+ }
915
+ if (state.inputMode === 'bookmark-save') {
916
+ saveBookmark();
917
+ return;
918
+ }
919
+ if (state.inspectingProcess) {
920
+ state.inspectingProcess = null;
921
+ render();
922
+ } else if (state.lastData?.processes?.length && state.processSelectionIndex >= 0) {
923
+ const sorted = state.lastData.processes;
924
+ const proc = sorted[state.processSelectionIndex];
925
+ if (proc) {
926
+ state.inspectingProcess = proc;
927
+ state.trackedPid = proc.pid;
928
+ setStatus(`Inspecting & following PID ${proc.pid}`);
929
+ render();
930
+ }
931
+ }
932
+ break;
933
+ case 'escape':
934
+ case 'esc':
935
+ if (state.inputMode === 'quick-ref' || state.inputMode === 'command-palette' || state.inputMode === 'bookmark-save') {
936
+ state.inputMode = null;
937
+ state.inputBuffer = '';
938
+ render();
939
+ return;
940
+ }
941
+ if (state.inspectingProcess) {
942
+ state.inspectingProcess = null;
943
+ render();
944
+ } else if (state.trackedPid !== null) {
945
+ state.trackedPid = null;
946
+ setStatus('Stopped following process');
947
+ render();
948
+ } else if (state.activePanel !== 'grid') {
949
+ state.activePanel = 'grid';
950
+ setStatus('Grid view');
951
+ render();
952
+ } else {
953
+ state.inputMode = 'menu';
954
+ state.menuSelectionIndex = 0;
955
+ render();
956
+ }
957
+ break;
958
+ default:
959
+ if (str && str.length === 1) {
960
+ if (state.inputMode === 'command-palette' || state.inputMode === 'bookmark-save') {
961
+ state.inputBuffer += str;
962
+ if (state.inputMode === 'command-palette') {
963
+ updatePaletteFilter();
964
+ }
965
+ render();
966
+ return;
967
+ }
968
+ const tabId = tabKeyToId(str);
969
+ if (tabId) {
970
+ pushPanelHistory(tabId);
971
+ state.activePanel = tabId as ActivePanel;
972
+ setStatus(`Panel: ${tabId.toUpperCase()}`);
973
+ render();
974
+ }
975
+ }
830
976
  }
831
977
  };
832
978
 
@@ -839,15 +985,222 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
839
985
  return map[str] ?? null;
840
986
  }
841
987
 
988
+ function saveBookmark() {
989
+ const name = state.inputBuffer.trim() || `view-${state.bookmarks.length + 1}`;
990
+ const bookmark = {
991
+ name,
992
+ activePanel: state.activePanel,
993
+ sortMode: state.sortMode,
994
+ processFilter: state.processFilter,
995
+ treeView: state.treeView,
996
+ showGraphs: state.showGraphs,
997
+ graphMode: state.graphMode,
998
+ trackedPid: state.trackedPid,
999
+ currentTheme: state.currentTheme,
1000
+ panelLayout: [...state.panelLayout],
1001
+ detailed: state.detailed,
1002
+ };
1003
+ state.bookmarks.push(bookmark);
1004
+ state.inputMode = null;
1005
+ state.inputBuffer = '';
1006
+ setStatus(`Bookmark saved: "${name}" (${state.bookmarks.length} total)`);
1007
+ invalidateTableCache();
1008
+ render();
1009
+ }
1010
+
1011
+ function recallNextBookmark() {
1012
+ if (state.bookmarks.length === 0) {
1013
+ setStatus('No bookmarks saved. Press Shift+M to save current view.');
1014
+ return;
1015
+ }
1016
+ const idx = state.bookmarkRecallIndex % state.bookmarks.length;
1017
+ const bm = state.bookmarks[idx];
1018
+ state.activePanel = bm.activePanel as typeof state.activePanel;
1019
+ state.sortMode = bm.sortMode as typeof state.sortMode;
1020
+ state.processFilter = bm.processFilter;
1021
+ state.treeView = bm.treeView;
1022
+ state.showGraphs = bm.showGraphs;
1023
+ state.graphMode = bm.graphMode as typeof state.graphMode;
1024
+ state.trackedPid = bm.trackedPid;
1025
+ state.currentTheme = bm.currentTheme as typeof state.currentTheme;
1026
+ state.panelLayout = [...bm.panelLayout];
1027
+ state.detailed = bm.detailed;
1028
+ state.bookmarkRecallIndex = (idx + 1) % state.bookmarks.length;
1029
+ setStatus(`Recalled bookmark: "${bm.name}"`);
1030
+ invalidateTableCache();
1031
+ render();
1032
+ }
1033
+
1034
+ function executePaletteCommand() {
1035
+ const cmds = state.paletteFilteredCommands;
1036
+ if (cmds.length === 0 || state.paletteIndex >= cmds.length) {
1037
+ state.inputMode = null;
1038
+ state.inputBuffer = '';
1039
+ state.paletteIndex = 0;
1040
+ render();
1041
+ return;
1042
+ }
1043
+ const cmd = cmds[state.paletteIndex];
1044
+ state.inputMode = null;
1045
+ state.inputBuffer = '';
1046
+ state.paletteIndex = 0;
1047
+
1048
+ // Execute the command
1049
+ switch (cmd.id) {
1050
+ case 'cpu': case 'mem': case 'gpu': case 'power': case 'battery':
1051
+ case 'thermal': case 'network': case 'packets': case 'tasks': case 'disk':
1052
+ case 'process': case 'containers': case 'p2p': case 'anomalies': case 'blender':
1053
+ state.activePanel = state.activePanel === cmd.id ? 'grid' : cmd.id as any;
1054
+ setStatus(state.activePanel === 'grid' ? 'Grid view' : `Panel: ${cmd.id.toUpperCase()}`);
1055
+ break;
1056
+ case 'grid':
1057
+ state.activePanel = 'grid';
1058
+ setStatus('Grid view');
1059
+ break;
1060
+ case 'graph-toggle':
1061
+ state.showGraphs = !state.showGraphs;
1062
+ setStatus(state.showGraphs ? 'Graphs shown' : 'Graphs hidden');
1063
+ break;
1064
+ case 'graph-mode':
1065
+ state.graphMode = state.graphMode === 'spark' ? 'bar' : 'spark';
1066
+ setStatus(`Graph mode: ${state.graphMode}`);
1067
+ break;
1068
+ case 'pause':
1069
+ state.paused = !state.paused;
1070
+ setStatus(state.paused ? 'Paused' : 'Resumed');
1071
+ break;
1072
+ case 'detailed':
1073
+ state.detailed = !state.detailed;
1074
+ setStatus(`Detailed sensor mode: ${state.detailed ? 'on' : 'off'}`);
1075
+ break;
1076
+ case 'tree':
1077
+ state.treeView = !state.treeView;
1078
+ setStatus(state.treeView ? 'Tree view' : 'Flat view');
1079
+ break;
1080
+ case 'temp-unit':
1081
+ state.tempUnit = state.tempUnit === 'c' ? 'f' : 'c';
1082
+ setStatus(`Temperature unit: ${state.tempUnit.toUpperCase()}`);
1083
+ invalidateTableCache();
1084
+ break;
1085
+ case 'filter':
1086
+ state.inputMode = 'filter';
1087
+ state.inputBuffer = state.processFilter;
1088
+ render();
1089
+ return;
1090
+ case 'sort':
1091
+ const sortCycle: SortMode[] = ['cpu', 'mem', 'pid', 'user', 'command', 'state', 'threads', 'runtime'];
1092
+ state.sortMode = sortCycle[(sortCycle.indexOf(state.sortMode) + 1) % sortCycle.length];
1093
+ setStatus(`Sorting by ${state.sortMode}`);
1094
+ break;
1095
+ case 'kill':
1096
+ state.inputMode = 'kill';
1097
+ state.inputBuffer = '';
1098
+ render();
1099
+ return;
1100
+ case 'signal':
1101
+ state.inputMode = 'signal';
1102
+ state.inputBuffer = 'SIGTERM';
1103
+ render();
1104
+ return;
1105
+ case 'export':
1106
+ exportSnapshot();
1107
+ render();
1108
+ return;
1109
+ case 'log':
1110
+ toggleLogging();
1111
+ render();
1112
+ return;
1113
+ case 'format-cycle': {
1114
+ const fmtCycle: ExportFormat[] = ['json', 'csv', 'tsv', 'html', 'md'];
1115
+ state.exportFormat = fmtCycle[(fmtCycle.indexOf(state.exportFormat) + 1) % fmtCycle.length];
1116
+ setStatus(`Export format: ${state.exportFormat}`);
1117
+ break;
1118
+ }
1119
+ case 'interval-inc':
1120
+ state.interval += 1;
1121
+ restartTicker();
1122
+ setStatus(`Interval set to ${state.interval}s`);
1123
+ break;
1124
+ case 'interval-dec':
1125
+ state.interval = Math.max(1, state.interval - 1);
1126
+ restartTicker();
1127
+ setStatus(`Interval set to ${state.interval}s`);
1128
+ break;
1129
+ case 'customize':
1130
+ state.inputMode = 'customizer';
1131
+ state.customizerIndex = 0;
1132
+ render();
1133
+ return;
1134
+ case 'ai-model':
1135
+ state.inputMode = 'ai-model';
1136
+ state.modelSelectionIndex = Math.max(0, AVAILABLE_AI_MODELS.findIndex(m => m.id === state.aiModel));
1137
+ render();
1138
+ return;
1139
+ case 'bookmark-save':
1140
+ state.inputMode = 'bookmark-save';
1141
+ state.inputBuffer = '';
1142
+ render();
1143
+ return;
1144
+ case 'bookmark-recall':
1145
+ recallNextBookmark();
1146
+ render();
1147
+ return;
1148
+ case 'zoom-in':
1149
+ state.graphZoomLevel = Math.max(1, state.graphZoomLevel - 1);
1150
+ state.history.setMaxLen(Math.max(20, 40 * state.graphZoomLevel));
1151
+ setStatus(`History zoom: ${state.graphZoomLevel}x (${state.history.maxLen} points)`);
1152
+ break;
1153
+ case 'zoom-out':
1154
+ state.graphZoomLevel = Math.min(5, state.graphZoomLevel + 1);
1155
+ state.history.setMaxLen(Math.max(20, 40 * state.graphZoomLevel));
1156
+ setStatus(`History zoom: ${state.graphZoomLevel}x (${state.history.maxLen} points)`);
1157
+ break;
1158
+ case 'quick-ref':
1159
+ state.inputMode = state.inputMode === 'quick-ref' ? null : 'quick-ref';
1160
+ render();
1161
+ return;
1162
+ }
1163
+ invalidateTableCache();
1164
+ render();
1165
+ }
1166
+
1167
+ function updatePaletteFilter() {
1168
+ const q = state.inputBuffer.toLowerCase().trim();
1169
+ if (!q) {
1170
+ state.paletteFilteredCommands = [...PALETTE_COMMANDS];
1171
+ } else {
1172
+ state.paletteFilteredCommands = PALETTE_COMMANDS.filter(cmd =>
1173
+ cmd.label.toLowerCase().includes(q) ||
1174
+ cmd.keys.toLowerCase().includes(q) ||
1175
+ cmd.category.toLowerCase().includes(q) ||
1176
+ cmd.id.toLowerCase().includes(q)
1177
+ );
1178
+ }
1179
+ state.paletteIndex = Math.max(0, Math.min(state.paletteIndex, state.paletteFilteredCommands.length - 1));
1180
+ }
1181
+
842
1182
  function cycleTab(direction: number) {
843
1183
  const tabs = state.PANEL_TABS.map(t => t.id) as string[];
844
- const current = state.activePanel === 'grid' ? -1 : tabs.indexOf(state.activePanel);
845
- const next = (current + direction + tabs.length) % tabs.length;
846
- state.activePanel = tabs[next] as typeof state.activePanel;
1184
+ const visibleTabs = tabs.filter(id => state.visiblePanels[id as keyof VisibleItems] !== false);
1185
+ const current = state.activePanel === 'grid' ? -1 : visibleTabs.indexOf(state.activePanel);
1186
+ const next = current === -1 ? 0 : (current + direction + visibleTabs.length) % visibleTabs.length;
1187
+ const prev = state.activePanel === 'grid' ? null : state.activePanel;
1188
+ state.activePanel = visibleTabs[next] as typeof state.activePanel;
1189
+ if (prev && prev !== state.activePanel) {
1190
+ pushPanelHistory(prev);
1191
+ }
847
1192
  setStatus(`Panel: ${state.activePanel.toUpperCase()}`);
848
1193
  render();
849
1194
  }
850
1195
 
1196
+ function pushPanelHistory(panelId: string) {
1197
+ const last = state.panelHistory.length > 0 ? state.panelHistory[state.panelHistory.length - 1] : null;
1198
+ if (panelId !== last) {
1199
+ state.panelHistory.push(panelId);
1200
+ if (state.panelHistory.length > 20) state.panelHistory.shift();
1201
+ }
1202
+ }
1203
+
851
1204
  function handleNormalMouse(seq: string) {
852
1205
  if (seq.length < 6) return;
853
1206
  const cb = seq.charCodeAt(3) - 32;
@@ -890,8 +1243,8 @@ function killProcess(pidStr: string, signal: string = 'SIGTERM') {
890
1243
 
891
1244
  function handleMouseClick(y: number, x: number) {
892
1245
  if (y !== TAB_BAR_ROW) return;
893
- const hitboxes = getTabHitboxes();
894
- const hit = hitboxes.find(h => x >= h.start && x < h.end);
1246
+ const hitboxes = getTabHitboxes(state.visiblePanels);
1247
+ const hit = hitboxes.find(h => x >= h.start && h.end > x);
895
1248
  if (hit) {
896
1249
  state.activePanel = state.activePanel === hit.id ? 'grid' : (hit.id as typeof state.activePanel);
897
1250
  setStatus(state.activePanel === 'grid' ? 'Grid view' : `Panel: ${hit.id.toUpperCase()}`);