git-watchtower 1.3.0 → 1.5.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.
- package/bin/git-watchtower.js +489 -1394
- package/package.json +1 -1
package/bin/git-watchtower.js
CHANGED
|
@@ -86,6 +86,10 @@ const { isGitAvailable: checkGitAvailable } = require('../src/git/commands');
|
|
|
86
86
|
const { getDefaultConfig, migrateConfig } = require('../src/config/schema');
|
|
87
87
|
const { getConfigPath, loadConfig: loadConfigFile, saveConfig: saveConfigFile, CONFIG_FILE_NAME } = require('../src/config/loader');
|
|
88
88
|
|
|
89
|
+
// Centralized state store
|
|
90
|
+
const { Store } = require('../src/state/store');
|
|
91
|
+
const store = new Store();
|
|
92
|
+
|
|
89
93
|
const PROJECT_ROOT = process.cwd();
|
|
90
94
|
|
|
91
95
|
function loadConfig() {
|
|
@@ -363,19 +367,8 @@ let AUTO_PULL = true;
|
|
|
363
367
|
const MAX_LOG_ENTRIES = 10;
|
|
364
368
|
const MAX_SERVER_LOG_LINES = 500;
|
|
365
369
|
|
|
366
|
-
// Dynamic settings
|
|
367
|
-
let visibleBranchCount = 7;
|
|
368
|
-
let soundEnabled = true;
|
|
369
|
-
let casinoModeEnabled = false;
|
|
370
|
-
|
|
371
370
|
// Server process management (for command mode)
|
|
372
371
|
let serverProcess = null;
|
|
373
|
-
let serverLogBuffer = []; // In-memory log buffer
|
|
374
|
-
let serverRunning = false;
|
|
375
|
-
let serverCrashed = false;
|
|
376
|
-
let logViewMode = false; // Viewing logs modal
|
|
377
|
-
let logViewTab = 'server'; // 'activity' or 'server'
|
|
378
|
-
let logScrollOffset = 0; // Scroll position in log view
|
|
379
372
|
|
|
380
373
|
function applyConfig(config) {
|
|
381
374
|
// Server settings
|
|
@@ -391,28 +384,35 @@ function applyConfig(config) {
|
|
|
391
384
|
AUTO_PULL = config.autoPull !== false;
|
|
392
385
|
GIT_POLL_INTERVAL = config.gitPollInterval || parseInt(process.env.GIT_POLL_INTERVAL, 10) || 5000;
|
|
393
386
|
|
|
394
|
-
// UI settings
|
|
395
|
-
|
|
396
|
-
|
|
387
|
+
// UI settings via store
|
|
388
|
+
const casinoEnabled = config.casinoMode === true;
|
|
389
|
+
store.setState({
|
|
390
|
+
visibleBranchCount: config.visibleBranches || 7,
|
|
391
|
+
soundEnabled: config.soundEnabled !== false,
|
|
392
|
+
casinoModeEnabled: casinoEnabled,
|
|
393
|
+
serverMode: SERVER_MODE,
|
|
394
|
+
noServer: NO_SERVER,
|
|
395
|
+
port: PORT,
|
|
396
|
+
maxLogEntries: MAX_LOG_ENTRIES,
|
|
397
|
+
projectName: path.basename(PROJECT_ROOT),
|
|
398
|
+
adaptivePollInterval: GIT_POLL_INTERVAL,
|
|
399
|
+
});
|
|
397
400
|
|
|
398
401
|
// Casino mode
|
|
399
|
-
|
|
400
|
-
if (casinoModeEnabled) {
|
|
402
|
+
if (casinoEnabled) {
|
|
401
403
|
casino.enable();
|
|
402
404
|
}
|
|
403
405
|
}
|
|
404
406
|
|
|
405
407
|
// Server log management
|
|
406
408
|
function addServerLog(line, isError = false) {
|
|
407
|
-
const
|
|
408
|
-
serverLogBuffer.
|
|
409
|
-
|
|
410
|
-
serverLogBuffer.shift();
|
|
411
|
-
}
|
|
409
|
+
const entry = { timestamp: new Date().toLocaleTimeString(), line, isError };
|
|
410
|
+
const serverLogBuffer = [...store.get('serverLogBuffer'), entry].slice(-MAX_SERVER_LOG_LINES);
|
|
411
|
+
store.setState({ serverLogBuffer });
|
|
412
412
|
}
|
|
413
413
|
|
|
414
414
|
function clearServerLog() {
|
|
415
|
-
serverLogBuffer
|
|
415
|
+
store.setState({ serverLogBuffer: [] });
|
|
416
416
|
}
|
|
417
417
|
|
|
418
418
|
// openInBrowser imported from src/utils/browser.js
|
|
@@ -609,8 +609,7 @@ function startServerProcess() {
|
|
|
609
609
|
}
|
|
610
610
|
|
|
611
611
|
clearServerLog();
|
|
612
|
-
serverCrashed
|
|
613
|
-
serverRunning = false;
|
|
612
|
+
store.setState({ serverCrashed: false, serverRunning: false });
|
|
614
613
|
|
|
615
614
|
addLog(`Starting: ${SERVER_COMMAND}`, 'update');
|
|
616
615
|
addServerLog(`$ ${SERVER_COMMAND}`);
|
|
@@ -631,7 +630,7 @@ function startServerProcess() {
|
|
|
631
630
|
|
|
632
631
|
try {
|
|
633
632
|
serverProcess = spawn(cmd, args, spawnOptions);
|
|
634
|
-
serverRunning
|
|
633
|
+
store.setState({ serverRunning: true });
|
|
635
634
|
|
|
636
635
|
serverProcess.stdout.on('data', (data) => {
|
|
637
636
|
const lines = data.toString().split('\n').filter(Boolean);
|
|
@@ -644,17 +643,16 @@ function startServerProcess() {
|
|
|
644
643
|
});
|
|
645
644
|
|
|
646
645
|
serverProcess.on('error', (err) => {
|
|
647
|
-
serverRunning
|
|
648
|
-
serverCrashed = true;
|
|
646
|
+
store.setState({ serverRunning: false, serverCrashed: true });
|
|
649
647
|
addServerLog(`Error: ${err.message}`, true);
|
|
650
648
|
addLog(`Server error: ${err.message}`, 'error');
|
|
651
649
|
render();
|
|
652
650
|
});
|
|
653
651
|
|
|
654
652
|
serverProcess.on('close', (code) => {
|
|
655
|
-
serverRunning
|
|
653
|
+
store.setState({ serverRunning: false });
|
|
656
654
|
if (code !== 0 && code !== null) {
|
|
657
|
-
serverCrashed
|
|
655
|
+
store.setState({ serverCrashed: true });
|
|
658
656
|
addServerLog(`Process exited with code ${code}`, true);
|
|
659
657
|
addLog(`Server exited with code ${code}`, 'error');
|
|
660
658
|
} else {
|
|
@@ -667,7 +665,7 @@ function startServerProcess() {
|
|
|
667
665
|
|
|
668
666
|
addLog(`Server started (pid: ${serverProcess.pid})`, 'success');
|
|
669
667
|
} catch (err) {
|
|
670
|
-
serverCrashed
|
|
668
|
+
store.setState({ serverCrashed: true });
|
|
671
669
|
addServerLog(`Failed to start: ${err.message}`, true);
|
|
672
670
|
addLog(`Failed to start server: ${err.message}`, 'error');
|
|
673
671
|
}
|
|
@@ -692,7 +690,7 @@ function stopServerProcess() {
|
|
|
692
690
|
}
|
|
693
691
|
|
|
694
692
|
serverProcess = null;
|
|
695
|
-
serverRunning
|
|
693
|
+
store.setState({ serverRunning: false });
|
|
696
694
|
}
|
|
697
695
|
|
|
698
696
|
function restartServerProcess() {
|
|
@@ -705,63 +703,41 @@ function restartServerProcess() {
|
|
|
705
703
|
}
|
|
706
704
|
|
|
707
705
|
// Network and polling state
|
|
708
|
-
let consecutiveNetworkFailures = 0;
|
|
709
|
-
let isOffline = false;
|
|
710
|
-
let lastFetchDuration = 0;
|
|
711
706
|
let slowFetchWarningShown = false;
|
|
712
707
|
let verySlowFetchWarningShown = false;
|
|
713
|
-
let adaptivePollInterval = GIT_POLL_INTERVAL;
|
|
714
708
|
let pollIntervalId = null;
|
|
715
709
|
|
|
716
|
-
// Git state
|
|
717
|
-
let isDetachedHead = false;
|
|
718
|
-
let hasMergeConflict = false;
|
|
719
|
-
|
|
720
710
|
// ANSI escape codes and box drawing imported from src/ui/ansi.js
|
|
721
|
-
const { ansi, box, truncate, sparkline: uiSparkline, visibleLength, stripAnsi } = require('../src/ui/ansi');
|
|
711
|
+
const { ansi, box, truncate, sparkline: uiSparkline, visibleLength, stripAnsi, padRight, padLeft, getMaxBranchesForScreen: calcMaxBranches, drawBox: renderBox, clearArea: renderClearArea } = require('../src/ui/ansi');
|
|
722
712
|
|
|
723
713
|
// Error detection utilities imported from src/utils/errors.js
|
|
724
|
-
const { ErrorHandler } = require('../src/utils/errors');
|
|
714
|
+
const { ErrorHandler, isAuthError, isMergeConflict, isNetworkError } = require('../src/utils/errors');
|
|
725
715
|
|
|
726
|
-
//
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
716
|
+
// Keyboard handling utilities imported from src/ui/keybindings.js
|
|
717
|
+
const { filterBranches } = require('../src/ui/keybindings');
|
|
718
|
+
|
|
719
|
+
// Extracted renderer and action handlers
|
|
720
|
+
const renderer = require('../src/ui/renderer');
|
|
721
|
+
const actions = require('../src/ui/actions');
|
|
722
|
+
|
|
723
|
+
// Diff stats parsing and stash imported from src/git/commands.js
|
|
724
|
+
const { parseDiffStats, stash: gitStash, stashPop: gitStashPop } = require('../src/git/commands');
|
|
725
|
+
|
|
726
|
+
// State (non-store globals)
|
|
731
727
|
let previousBranchStates = new Map(); // branch name -> commit hash
|
|
732
728
|
let knownBranchNames = new Set(); // Track known branches to detect NEW ones
|
|
733
|
-
let isPolling = false;
|
|
734
|
-
let pollingStatus = 'idle';
|
|
735
|
-
let terminalWidth = process.stdout.columns || 80;
|
|
736
|
-
let terminalHeight = process.stdout.rows || 24;
|
|
737
729
|
|
|
738
730
|
// SSE clients for live reload
|
|
739
731
|
const clients = new Set();
|
|
740
732
|
|
|
741
|
-
//
|
|
742
|
-
const activityLog = [];
|
|
743
|
-
|
|
744
|
-
// Flash state
|
|
745
|
-
let flashMessage = null;
|
|
733
|
+
// Flash/error toast timers
|
|
746
734
|
let flashTimeout = null;
|
|
747
|
-
|
|
748
|
-
// Error toast state (more prominent than activity log)
|
|
749
|
-
let errorToast = null;
|
|
750
735
|
let errorToastTimeout = null;
|
|
751
736
|
|
|
752
|
-
//
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
// Search/filter state
|
|
757
|
-
let searchMode = false;
|
|
758
|
-
let searchQuery = '';
|
|
759
|
-
let filteredBranches = null;
|
|
760
|
-
|
|
761
|
-
// Branch action modal state
|
|
762
|
-
let actionMode = false;
|
|
763
|
-
let actionData = null; // { branch, sessionUrl, prInfo, hasGh, hasGlab, webUrl, isClaudeBranch, ... }
|
|
764
|
-
let actionLoading = false; // true while PR info is being fetched asynchronously
|
|
737
|
+
// Tracks the operation that failed due to dirty working directory.
|
|
738
|
+
// When set, pressing 'S' will stash changes and retry the operation.
|
|
739
|
+
// Shape: { type: 'switch', branch: string } | { type: 'pull' } | null
|
|
740
|
+
let pendingDirtyOperation = null;
|
|
765
741
|
|
|
766
742
|
// Cached environment info (populated once at startup, doesn't change during session)
|
|
767
743
|
let cachedEnv = null; // { hasGh, hasGlab, ghAuthed, glabAuthed, webUrlBase, platform }
|
|
@@ -770,21 +746,15 @@ let cachedEnv = null; // { hasGh, hasGlab, ghAuthed, glabAuthed, webUrlBase, pla
|
|
|
770
746
|
// Invalidated when the branch's commit hash changes
|
|
771
747
|
const prInfoCache = new Map();
|
|
772
748
|
|
|
773
|
-
// Bulk PR status map: Map<branchName, { state: 'OPEN'|'MERGED'|'CLOSED', number, title }>
|
|
774
|
-
// Updated in background every PR_STATUS_POLL_INTERVAL ms
|
|
775
|
-
let branchPrStatusMap = new Map();
|
|
776
749
|
let lastPrStatusFetch = 0;
|
|
777
750
|
const PR_STATUS_POLL_INTERVAL = 60 * 1000; // 60 seconds
|
|
778
751
|
let prStatusFetchInFlight = false;
|
|
779
752
|
|
|
780
753
|
// BASE_BRANCH_RE and isBaseBranch imported from src/git/pr.js
|
|
781
754
|
|
|
782
|
-
// Session history for undo
|
|
783
|
-
const switchHistory = [];
|
|
784
755
|
const MAX_HISTORY = 20;
|
|
785
756
|
|
|
786
|
-
// Sparkline
|
|
787
|
-
const sparklineCache = new Map(); // branch name -> sparkline string
|
|
757
|
+
// Sparkline timing
|
|
788
758
|
let lastSparklineUpdate = 0;
|
|
789
759
|
const SPARKLINE_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
790
760
|
|
|
@@ -847,18 +817,7 @@ function execAsync(command, options = {}) {
|
|
|
847
817
|
async function getDiffStats(fromCommit, toCommit = 'HEAD') {
|
|
848
818
|
try {
|
|
849
819
|
const { stdout } = await execAsync(`git diff --stat ${fromCommit}..${toCommit}`);
|
|
850
|
-
|
|
851
|
-
const match = stdout.match(/(\d+) insertions?\(\+\).*?(\d+) deletions?\(-\)/);
|
|
852
|
-
if (match) {
|
|
853
|
-
return { added: parseInt(match[1], 10), deleted: parseInt(match[2], 10) };
|
|
854
|
-
}
|
|
855
|
-
// Try to match just insertions or just deletions
|
|
856
|
-
const insertMatch = stdout.match(/(\d+) insertions?\(\+\)/);
|
|
857
|
-
const deleteMatch = stdout.match(/(\d+) deletions?\(-\)/);
|
|
858
|
-
return {
|
|
859
|
-
added: insertMatch ? parseInt(insertMatch[1], 10) : 0,
|
|
860
|
-
deleted: deleteMatch ? parseInt(deleteMatch[1], 10) : 0,
|
|
861
|
-
};
|
|
820
|
+
return parseDiffStats(stdout);
|
|
862
821
|
} catch (e) {
|
|
863
822
|
return { added: 0, deleted: 0 };
|
|
864
823
|
}
|
|
@@ -868,21 +827,10 @@ async function getDiffStats(fromCommit, toCommit = 'HEAD') {
|
|
|
868
827
|
|
|
869
828
|
// truncate imported from src/ui/ansi.js
|
|
870
829
|
|
|
871
|
-
|
|
872
|
-
if (str.length >= len) return str.substring(0, len);
|
|
873
|
-
return str + ' '.repeat(len - str.length);
|
|
874
|
-
}
|
|
830
|
+
// padRight, padLeft imported from src/ui/ansi.js
|
|
875
831
|
|
|
876
832
|
function getMaxBranchesForScreen() {
|
|
877
|
-
|
|
878
|
-
// Each branch takes 2 rows, plus 4 for box borders
|
|
879
|
-
const availableHeight = terminalHeight - 2 - MAX_LOG_ENTRIES - 5 - 2;
|
|
880
|
-
return Math.max(1, Math.floor(availableHeight / 2));
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
function padLeft(str, len) {
|
|
884
|
-
if (str.length >= len) return str.substring(0, len);
|
|
885
|
-
return ' '.repeat(len - str.length) + str;
|
|
833
|
+
return calcMaxBranches(store.get('terminalHeight'), MAX_LOG_ENTRIES);
|
|
886
834
|
}
|
|
887
835
|
|
|
888
836
|
// Casino mode funny messages
|
|
@@ -927,12 +875,16 @@ function getCasinoMessage(type) {
|
|
|
927
875
|
}
|
|
928
876
|
|
|
929
877
|
function addLog(message, type = 'info') {
|
|
930
|
-
const timestamp = new Date().toLocaleTimeString();
|
|
931
878
|
const icons = { info: '○', success: '✓', warning: '●', error: '✗', update: '⟳' };
|
|
932
879
|
const colors = { info: 'white', success: 'green', warning: 'yellow', error: 'red', update: 'cyan' };
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
880
|
+
const entry = {
|
|
881
|
+
message, type,
|
|
882
|
+
timestamp: new Date().toLocaleTimeString(),
|
|
883
|
+
icon: icons[type] || '○',
|
|
884
|
+
color: colors[type] || 'white',
|
|
885
|
+
};
|
|
886
|
+
const activityLog = [entry, ...store.get('activityLog')].slice(0, MAX_LOG_ENTRIES);
|
|
887
|
+
store.setState({ activityLog });
|
|
936
888
|
}
|
|
937
889
|
|
|
938
890
|
// generateSparkline uses uiSparkline from src/ui/ansi.js
|
|
@@ -943,7 +895,7 @@ function generateSparkline(commitCounts) {
|
|
|
943
895
|
|
|
944
896
|
async function getBranchSparkline(branchName) {
|
|
945
897
|
// Check cache first
|
|
946
|
-
const cached = sparklineCache.get(branchName);
|
|
898
|
+
const cached = store.get('sparklineCache').get(branchName);
|
|
947
899
|
if (cached && (Date.now() - lastSparklineUpdate) < SPARKLINE_CACHE_TTL) {
|
|
948
900
|
return cached;
|
|
949
901
|
}
|
|
@@ -957,7 +909,8 @@ async function refreshAllSparklines() {
|
|
|
957
909
|
}
|
|
958
910
|
|
|
959
911
|
try {
|
|
960
|
-
|
|
912
|
+
const currentBranches = store.get('branches');
|
|
913
|
+
for (const branch of currentBranches.slice(0, 20)) { // Limit to top 20
|
|
961
914
|
if (branch.isDeleted) continue;
|
|
962
915
|
|
|
963
916
|
// Get commit counts for last 7 days
|
|
@@ -983,7 +936,7 @@ async function refreshAllSparklines() {
|
|
|
983
936
|
}
|
|
984
937
|
|
|
985
938
|
const counts = Array.from(dayCounts.values());
|
|
986
|
-
sparklineCache.set(branch.name, generateSparkline(counts));
|
|
939
|
+
store.get('sparklineCache').set(branch.name, generateSparkline(counts));
|
|
987
940
|
}
|
|
988
941
|
lastSparklineUpdate = now;
|
|
989
942
|
} catch (e) {
|
|
@@ -1022,7 +975,7 @@ async function getPreviewData(branchName) {
|
|
|
1022
975
|
|
|
1023
976
|
// playSound delegates to extracted src/utils/sound.js
|
|
1024
977
|
function playSound() {
|
|
1025
|
-
if (!soundEnabled) return;
|
|
978
|
+
if (!store.get('soundEnabled')) return;
|
|
1026
979
|
playSoundEffect({ cwd: PROJECT_ROOT });
|
|
1027
980
|
}
|
|
1028
981
|
|
|
@@ -1047,335 +1000,32 @@ function restoreTerminalTitle() {
|
|
|
1047
1000
|
}
|
|
1048
1001
|
|
|
1049
1002
|
function updateTerminalSize() {
|
|
1050
|
-
|
|
1051
|
-
|
|
1003
|
+
store.setState({
|
|
1004
|
+
terminalWidth: process.stdout.columns || 80,
|
|
1005
|
+
terminalHeight: process.stdout.rows || 24,
|
|
1006
|
+
});
|
|
1052
1007
|
}
|
|
1053
1008
|
|
|
1054
1009
|
function drawBox(row, col, width, height, title = '', titleColor = ansi.cyan) {
|
|
1055
|
-
|
|
1056
|
-
write(ansi.moveTo(row, col));
|
|
1057
|
-
write(ansi.gray + box.topLeft + box.horizontal.repeat(width - 2) + box.topRight + ansi.reset);
|
|
1058
|
-
|
|
1059
|
-
// Title
|
|
1060
|
-
if (title) {
|
|
1061
|
-
write(ansi.moveTo(row, col + 2));
|
|
1062
|
-
write(ansi.gray + ' ' + titleColor + title + ansi.gray + ' ' + ansi.reset);
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
// Sides
|
|
1066
|
-
for (let i = 1; i < height - 1; i++) {
|
|
1067
|
-
write(ansi.moveTo(row + i, col));
|
|
1068
|
-
write(ansi.gray + box.vertical + ansi.reset);
|
|
1069
|
-
write(ansi.moveTo(row + i, col + width - 1));
|
|
1070
|
-
write(ansi.gray + box.vertical + ansi.reset);
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
// Bottom border
|
|
1074
|
-
write(ansi.moveTo(row + height - 1, col));
|
|
1075
|
-
write(ansi.gray + box.bottomLeft + box.horizontal.repeat(width - 2) + box.bottomRight + ansi.reset);
|
|
1010
|
+
write(renderBox(row, col, width, height, title, titleColor));
|
|
1076
1011
|
}
|
|
1077
1012
|
|
|
1078
1013
|
function clearArea(row, col, width, height) {
|
|
1079
|
-
|
|
1080
|
-
write(ansi.moveTo(row + i, col));
|
|
1081
|
-
write(' '.repeat(width));
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
function renderHeader() {
|
|
1086
|
-
const width = terminalWidth;
|
|
1087
|
-
// Header row: 1 normally, 2 when casino mode (row 1 is marquee)
|
|
1088
|
-
const headerRow = casinoModeEnabled ? 2 : 1;
|
|
1089
|
-
|
|
1090
|
-
let statusIcon = { idle: ansi.green + '●', fetching: ansi.yellow + '⟳', error: ansi.red + '●' }[pollingStatus];
|
|
1091
|
-
|
|
1092
|
-
// Override status for special states
|
|
1093
|
-
if (isOffline) {
|
|
1094
|
-
statusIcon = ansi.red + '⊘';
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
const soundIcon = soundEnabled ? ansi.green + '🔔' : ansi.gray + '🔕';
|
|
1098
|
-
const projectName = path.basename(PROJECT_ROOT);
|
|
1099
|
-
|
|
1100
|
-
write(ansi.moveTo(headerRow, 1));
|
|
1101
|
-
write(ansi.bgBlue + ansi.white + ansi.bold);
|
|
1102
|
-
|
|
1103
|
-
// Left side: Title + separator + project name
|
|
1104
|
-
const leftContent = ` 🏰 Git Watchtower ${ansi.dim}│${ansi.bold} ${projectName}`;
|
|
1105
|
-
const leftVisibleLen = 21 + projectName.length; // " 🏰 Git Watchtower │ " + projectName
|
|
1106
|
-
|
|
1107
|
-
write(leftContent);
|
|
1108
|
-
|
|
1109
|
-
// Warning badges (center area)
|
|
1110
|
-
let badges = '';
|
|
1111
|
-
let badgesVisibleLen = 0;
|
|
1112
|
-
|
|
1113
|
-
// Casino mode slot display moved to its own row below header (row 3)
|
|
1114
|
-
|
|
1115
|
-
if (SERVER_MODE === 'command' && serverCrashed) {
|
|
1116
|
-
const label = ' CRASHED ';
|
|
1117
|
-
badges += ' ' + ansi.bgRed + ansi.white + label + ansi.bgBlue + ansi.white;
|
|
1118
|
-
badgesVisibleLen += 1 + label.length;
|
|
1119
|
-
}
|
|
1120
|
-
if (isOffline) {
|
|
1121
|
-
const label = ' OFFLINE ';
|
|
1122
|
-
badges += ' ' + ansi.bgRed + ansi.white + label + ansi.bgBlue + ansi.white;
|
|
1123
|
-
badgesVisibleLen += 1 + label.length;
|
|
1124
|
-
}
|
|
1125
|
-
if (isDetachedHead) {
|
|
1126
|
-
const label = ' DETACHED HEAD ';
|
|
1127
|
-
badges += ' ' + ansi.bgYellow + ansi.black + label + ansi.bgBlue + ansi.white;
|
|
1128
|
-
badgesVisibleLen += 1 + label.length;
|
|
1129
|
-
}
|
|
1130
|
-
if (hasMergeConflict) {
|
|
1131
|
-
const label = ' MERGE CONFLICT ';
|
|
1132
|
-
badges += ' ' + ansi.bgRed + ansi.white + label + ansi.bgBlue + ansi.white;
|
|
1133
|
-
badgesVisibleLen += 1 + label.length;
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
write(badges);
|
|
1137
|
-
|
|
1138
|
-
// Right side: Server mode + URL + status icons
|
|
1139
|
-
let modeLabel = '';
|
|
1140
|
-
let modeBadge = '';
|
|
1141
|
-
if (SERVER_MODE === 'static') {
|
|
1142
|
-
modeLabel = ' STATIC ';
|
|
1143
|
-
modeBadge = ansi.bgCyan + ansi.black + modeLabel + ansi.bgBlue + ansi.white;
|
|
1144
|
-
} else if (SERVER_MODE === 'command') {
|
|
1145
|
-
modeLabel = ' COMMAND ';
|
|
1146
|
-
modeBadge = ansi.bgGreen + ansi.black + modeLabel + ansi.bgBlue + ansi.white;
|
|
1147
|
-
} else {
|
|
1148
|
-
modeLabel = ' MONITOR ';
|
|
1149
|
-
modeBadge = ansi.bgMagenta + ansi.white + modeLabel + ansi.bgBlue + ansi.white;
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
let serverInfo = '';
|
|
1153
|
-
let serverInfoVisible = '';
|
|
1154
|
-
if (SERVER_MODE === 'none') {
|
|
1155
|
-
serverInfoVisible = '';
|
|
1156
|
-
} else {
|
|
1157
|
-
const statusDot = serverRunning ? ansi.green + '●' : (serverCrashed ? ansi.red + '●' : ansi.gray + '○');
|
|
1158
|
-
serverInfoVisible = `localhost:${PORT} `;
|
|
1159
|
-
serverInfo = statusDot + ansi.white + ` localhost:${PORT} `;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
const rightContent = `${modeBadge} ${serverInfo}${statusIcon}${ansi.bgBlue} ${soundIcon}${ansi.bgBlue} `;
|
|
1163
|
-
const rightVisibleLen = modeLabel.length + 1 + serverInfoVisible.length + 5; // mode + space + serverInfo + "● 🔔 "
|
|
1164
|
-
|
|
1165
|
-
// Calculate padding to fill full width
|
|
1166
|
-
const usedSpace = leftVisibleLen + badgesVisibleLen + rightVisibleLen;
|
|
1167
|
-
const padding = Math.max(1, width - usedSpace);
|
|
1168
|
-
write(' '.repeat(padding));
|
|
1169
|
-
write(rightContent);
|
|
1170
|
-
write(ansi.reset);
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
function renderBranchList() {
|
|
1174
|
-
// Start row: 3 normally, 4 when casino mode (row 1 is marquee, row 2 is header)
|
|
1175
|
-
const startRow = casinoModeEnabled ? 4 : 3;
|
|
1176
|
-
const boxWidth = terminalWidth;
|
|
1177
|
-
const contentWidth = boxWidth - 4; // Space between borders
|
|
1178
|
-
const height = Math.min(visibleBranchCount * 2 + 4, Math.floor(terminalHeight * 0.5));
|
|
1179
|
-
|
|
1180
|
-
// Determine which branches to show (filtered or all)
|
|
1181
|
-
const displayBranches = filteredBranches !== null ? filteredBranches : branches;
|
|
1182
|
-
const boxTitle = searchMode
|
|
1183
|
-
? `BRANCHES (/${searchQuery}_)`
|
|
1184
|
-
: 'ACTIVE BRANCHES';
|
|
1185
|
-
|
|
1186
|
-
drawBox(startRow, 1, boxWidth, height, boxTitle, ansi.cyan);
|
|
1187
|
-
|
|
1188
|
-
// Clear content area first (fixes border gaps)
|
|
1189
|
-
for (let i = 1; i < height - 1; i++) {
|
|
1190
|
-
write(ansi.moveTo(startRow + i, 2));
|
|
1191
|
-
write(' '.repeat(contentWidth + 2));
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
// Header line
|
|
1195
|
-
write(ansi.moveTo(startRow + 1, 2));
|
|
1196
|
-
write(ansi.gray + '─'.repeat(contentWidth + 2) + ansi.reset);
|
|
1197
|
-
|
|
1198
|
-
if (displayBranches.length === 0) {
|
|
1199
|
-
write(ansi.moveTo(startRow + 3, 4));
|
|
1200
|
-
if (searchMode && searchQuery) {
|
|
1201
|
-
write(ansi.gray + `No branches matching "${searchQuery}"` + ansi.reset);
|
|
1202
|
-
} else {
|
|
1203
|
-
write(ansi.gray + "No branches found. Press 'f' to fetch." + ansi.reset);
|
|
1204
|
-
}
|
|
1205
|
-
return startRow + height;
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
let row = startRow + 2;
|
|
1209
|
-
for (let i = 0; i < displayBranches.length && i < visibleBranchCount; i++) {
|
|
1210
|
-
const branch = displayBranches[i];
|
|
1211
|
-
const isSelected = i === selectedIndex;
|
|
1212
|
-
const isCurrent = branch.name === currentBranch;
|
|
1213
|
-
const timeAgo = formatTimeAgo(branch.date);
|
|
1214
|
-
const sparkline = sparklineCache.get(branch.name) || ' ';
|
|
1215
|
-
const prStatus = branchPrStatusMap.get(branch.name); // { state, number, title } or undefined
|
|
1216
|
-
// Never treat default/base branches as "merged" — they're merge targets, not sources
|
|
1217
|
-
const isBranchBase = isBaseBranch(branch.name);
|
|
1218
|
-
const isMerged = !isBranchBase && prStatus && prStatus.state === 'MERGED';
|
|
1219
|
-
const hasOpenPr = prStatus && prStatus.state === 'OPEN';
|
|
1220
|
-
|
|
1221
|
-
// Branch name line
|
|
1222
|
-
write(ansi.moveTo(row, 2));
|
|
1223
|
-
|
|
1224
|
-
// Cursor indicator
|
|
1225
|
-
const cursor = isSelected ? ' ▶ ' : ' ';
|
|
1226
|
-
|
|
1227
|
-
// Branch name - adjust for sparkline
|
|
1228
|
-
const maxNameLen = contentWidth - 38; // Extra space for sparkline
|
|
1229
|
-
const displayName = truncate(branch.name, maxNameLen);
|
|
1230
|
-
|
|
1231
|
-
// Padding after name
|
|
1232
|
-
const namePadding = Math.max(1, maxNameLen - displayName.length + 2);
|
|
1233
|
-
|
|
1234
|
-
// Write the line
|
|
1235
|
-
if (isSelected) write(ansi.inverse);
|
|
1236
|
-
write(cursor);
|
|
1237
|
-
|
|
1238
|
-
if (branch.isDeleted) {
|
|
1239
|
-
write(ansi.gray + ansi.dim + displayName + ansi.reset);
|
|
1240
|
-
if (isSelected) write(ansi.inverse);
|
|
1241
|
-
} else if (isMerged && !isCurrent) {
|
|
1242
|
-
// Merged branches get dimmed styling (like deleted, but in magenta tint)
|
|
1243
|
-
write(ansi.dim + ansi.fg256(103) + displayName + ansi.reset);
|
|
1244
|
-
if (isSelected) write(ansi.inverse);
|
|
1245
|
-
} else if (isCurrent) {
|
|
1246
|
-
write(ansi.green + ansi.bold + displayName + ansi.reset);
|
|
1247
|
-
if (isSelected) write(ansi.inverse);
|
|
1248
|
-
} else if (branch.justUpdated) {
|
|
1249
|
-
write(ansi.yellow + displayName + ansi.reset);
|
|
1250
|
-
if (isSelected) write(ansi.inverse);
|
|
1251
|
-
branch.justUpdated = false;
|
|
1252
|
-
} else {
|
|
1253
|
-
write(displayName);
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
write(' '.repeat(namePadding));
|
|
1257
|
-
|
|
1258
|
-
// Sparkline (7 chars)
|
|
1259
|
-
if (isSelected) write(ansi.reset);
|
|
1260
|
-
if (isMerged && !isCurrent) {
|
|
1261
|
-
write(ansi.dim + ansi.fg256(60) + sparkline + ansi.reset); // Dimmed sparkline for merged
|
|
1262
|
-
} else {
|
|
1263
|
-
write(ansi.fg256(39) + sparkline + ansi.reset); // Nice blue color
|
|
1264
|
-
}
|
|
1265
|
-
if (isSelected) write(ansi.inverse);
|
|
1266
|
-
|
|
1267
|
-
// PR status dot indicator (1 char)
|
|
1268
|
-
if (isSelected) write(ansi.reset);
|
|
1269
|
-
if (isMerged) {
|
|
1270
|
-
write(ansi.dim + ansi.magenta + '●' + ansi.reset);
|
|
1271
|
-
} else if (hasOpenPr) {
|
|
1272
|
-
write(ansi.brightGreen + '●' + ansi.reset);
|
|
1273
|
-
} else {
|
|
1274
|
-
write(' ');
|
|
1275
|
-
}
|
|
1276
|
-
if (isSelected) write(ansi.inverse);
|
|
1277
|
-
|
|
1278
|
-
// Status badge
|
|
1279
|
-
if (branch.isDeleted) {
|
|
1280
|
-
if (isSelected) write(ansi.reset);
|
|
1281
|
-
write(ansi.red + ansi.dim + '✗ DELETED' + ansi.reset);
|
|
1282
|
-
if (isSelected) write(ansi.inverse);
|
|
1283
|
-
} else if (isMerged && !isCurrent && !branch.isNew && !branch.hasUpdates) {
|
|
1284
|
-
if (isSelected) write(ansi.reset);
|
|
1285
|
-
write(ansi.dim + ansi.magenta + '✓ MERGED ' + ansi.reset);
|
|
1286
|
-
if (isSelected) write(ansi.inverse);
|
|
1287
|
-
} else if (isCurrent) {
|
|
1288
|
-
if (isSelected) write(ansi.reset);
|
|
1289
|
-
write(ansi.green + '★ CURRENT' + ansi.reset);
|
|
1290
|
-
if (isSelected) write(ansi.inverse);
|
|
1291
|
-
} else if (branch.isNew) {
|
|
1292
|
-
if (isSelected) write(ansi.reset);
|
|
1293
|
-
write(ansi.magenta + '✦ NEW ' + ansi.reset);
|
|
1294
|
-
if (isSelected) write(ansi.inverse);
|
|
1295
|
-
} else if (branch.hasUpdates) {
|
|
1296
|
-
if (isSelected) write(ansi.reset);
|
|
1297
|
-
write(ansi.yellow + '↓ UPDATES' + ansi.reset);
|
|
1298
|
-
if (isSelected) write(ansi.inverse);
|
|
1299
|
-
} else {
|
|
1300
|
-
write(' ');
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
// Time ago
|
|
1304
|
-
write(' ');
|
|
1305
|
-
if (isSelected) write(ansi.reset);
|
|
1306
|
-
write(ansi.gray + padLeft(timeAgo, 10) + ansi.reset);
|
|
1307
|
-
|
|
1308
|
-
if (isSelected) write(ansi.reset);
|
|
1309
|
-
|
|
1310
|
-
row++;
|
|
1311
|
-
|
|
1312
|
-
// Commit info line
|
|
1313
|
-
write(ansi.moveTo(row, 2));
|
|
1314
|
-
if (isMerged && !isCurrent) {
|
|
1315
|
-
// Dimmed commit line for merged branches, with PR number
|
|
1316
|
-
write(ansi.dim + ' └─ ' + ansi.reset);
|
|
1317
|
-
write(ansi.dim + ansi.cyan + (branch.commit || '???????') + ansi.reset);
|
|
1318
|
-
write(ansi.dim + ' • ' + ansi.reset);
|
|
1319
|
-
const prTag = ansi.dim + ansi.magenta + '#' + prStatus.number + ansi.reset + ansi.dim + ' ';
|
|
1320
|
-
write(prTag + ansi.gray + ansi.dim + truncate(branch.subject || 'No commit message', contentWidth - 28) + ansi.reset);
|
|
1321
|
-
} else {
|
|
1322
|
-
write(' └─ ');
|
|
1323
|
-
write(ansi.cyan + (branch.commit || '???????') + ansi.reset);
|
|
1324
|
-
write(' • ');
|
|
1325
|
-
if (hasOpenPr) {
|
|
1326
|
-
// Show PR number inline for open PRs
|
|
1327
|
-
const prTag = ansi.brightGreen + '#' + prStatus.number + ansi.reset + ' ';
|
|
1328
|
-
write(prTag + ansi.gray + truncate(branch.subject || 'No commit message', contentWidth - 28) + ansi.reset);
|
|
1329
|
-
} else {
|
|
1330
|
-
write(ansi.gray + truncate(branch.subject || 'No commit message', contentWidth - 22) + ansi.reset);
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
row++;
|
|
1335
|
-
}
|
|
1336
|
-
|
|
1337
|
-
return startRow + height;
|
|
1014
|
+
write(renderClearArea(row, col, width, height));
|
|
1338
1015
|
}
|
|
1339
1016
|
|
|
1340
|
-
|
|
1341
|
-
const boxWidth = terminalWidth;
|
|
1342
|
-
const contentWidth = boxWidth - 4;
|
|
1343
|
-
const height = Math.min(MAX_LOG_ENTRIES + 3, terminalHeight - startRow - 4);
|
|
1344
|
-
|
|
1345
|
-
drawBox(startRow, 1, boxWidth, height, 'ACTIVITY LOG', ansi.gray);
|
|
1346
|
-
|
|
1347
|
-
// Clear content area first (fixes border gaps)
|
|
1348
|
-
for (let i = 1; i < height - 1; i++) {
|
|
1349
|
-
write(ansi.moveTo(startRow + i, 2));
|
|
1350
|
-
write(' '.repeat(contentWidth + 2));
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
let row = startRow + 1;
|
|
1354
|
-
for (let i = 0; i < activityLog.length && i < height - 2; i++) {
|
|
1355
|
-
const entry = activityLog[i];
|
|
1356
|
-
write(ansi.moveTo(row, 3));
|
|
1357
|
-
write(ansi.gray + `[${entry.timestamp}]` + ansi.reset + ' ');
|
|
1358
|
-
write(ansi[entry.color] + entry.icon + ansi.reset + ' ');
|
|
1359
|
-
write(truncate(entry.message, contentWidth - 16));
|
|
1360
|
-
row++;
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
if (activityLog.length === 0) {
|
|
1364
|
-
write(ansi.moveTo(startRow + 1, 3));
|
|
1365
|
-
write(ansi.gray + 'No activity yet...' + ansi.reset);
|
|
1366
|
-
}
|
|
1017
|
+
// renderHeader - now delegated to renderer.renderHeader()
|
|
1367
1018
|
|
|
1368
|
-
|
|
1369
|
-
}
|
|
1019
|
+
// renderBranchList, renderActivityLog — now delegated to renderer module (src/ui/renderer.js)
|
|
1370
1020
|
|
|
1371
1021
|
function renderCasinoStats(startRow) {
|
|
1372
|
-
if (!casinoModeEnabled) return startRow;
|
|
1022
|
+
if (!store.get('casinoModeEnabled')) return startRow;
|
|
1373
1023
|
|
|
1374
|
-
const boxWidth = terminalWidth;
|
|
1024
|
+
const boxWidth = store.get('terminalWidth');
|
|
1375
1025
|
const height = 6; // Box with two content lines
|
|
1376
1026
|
|
|
1377
1027
|
// Don't draw if not enough space
|
|
1378
|
-
if (startRow + height > terminalHeight - 3) return startRow;
|
|
1028
|
+
if (startRow + height > store.get('terminalHeight') - 3) return startRow;
|
|
1379
1029
|
|
|
1380
1030
|
drawBox(startRow, 1, boxWidth, height, '🎰 CASINO WINNINGS 🎰', ansi.brightMagenta);
|
|
1381
1031
|
|
|
@@ -1411,667 +1061,17 @@ function renderCasinoStats(startRow) {
|
|
|
1411
1061
|
return startRow + height;
|
|
1412
1062
|
}
|
|
1413
1063
|
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
write(ansi.moveTo(row, 1));
|
|
1418
|
-
write(ansi.bgBlack + ansi.white);
|
|
1419
|
-
write(' ');
|
|
1420
|
-
write(ansi.gray + '[↑↓]' + ansi.reset + ansi.bgBlack + ' Nav ');
|
|
1421
|
-
write(ansi.gray + '[/]' + ansi.reset + ansi.bgBlack + ' Search ');
|
|
1422
|
-
write(ansi.gray + '[v]' + ansi.reset + ansi.bgBlack + ' Preview ');
|
|
1423
|
-
write(ansi.gray + '[Enter]' + ansi.reset + ansi.bgBlack + ' Switch ');
|
|
1424
|
-
write(ansi.gray + '[h]' + ansi.reset + ansi.bgBlack + ' History ');
|
|
1425
|
-
write(ansi.gray + '[i]' + ansi.reset + ansi.bgBlack + ' Info ');
|
|
1426
|
-
write(ansi.gray + '[b]' + ansi.reset + ansi.bgBlack + ' Actions ');
|
|
1427
|
-
|
|
1428
|
-
// Mode-specific keys
|
|
1429
|
-
if (!NO_SERVER) {
|
|
1430
|
-
write(ansi.gray + '[l]' + ansi.reset + ansi.bgBlack + ' Logs ');
|
|
1431
|
-
write(ansi.gray + '[o]' + ansi.reset + ansi.bgBlack + ' Open ');
|
|
1432
|
-
}
|
|
1433
|
-
if (SERVER_MODE === 'static') {
|
|
1434
|
-
write(ansi.gray + '[r]' + ansi.reset + ansi.bgBlack + ' Reload ');
|
|
1435
|
-
} else if (SERVER_MODE === 'command') {
|
|
1436
|
-
write(ansi.gray + '[R]' + ansi.reset + ansi.bgBlack + ' Restart ');
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
write(ansi.gray + '[±]' + ansi.reset + ansi.bgBlack + ' List:' + ansi.cyan + visibleBranchCount + ansi.reset + ansi.bgBlack + ' ');
|
|
1440
|
-
|
|
1441
|
-
// Casino mode toggle indicator
|
|
1442
|
-
if (casinoModeEnabled) {
|
|
1443
|
-
write(ansi.brightMagenta + '[c]' + ansi.reset + ansi.bgBlack + ' 🎰 ');
|
|
1444
|
-
} else {
|
|
1445
|
-
write(ansi.gray + '[c]' + ansi.reset + ansi.bgBlack + ' Casino ');
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
|
-
write(ansi.gray + '[q]' + ansi.reset + ansi.bgBlack + ' Quit ');
|
|
1449
|
-
write(ansi.reset);
|
|
1450
|
-
}
|
|
1451
|
-
|
|
1452
|
-
function renderFlash() {
|
|
1453
|
-
if (!flashMessage) return;
|
|
1454
|
-
|
|
1455
|
-
const width = 50;
|
|
1456
|
-
const height = 5;
|
|
1457
|
-
const col = Math.floor((terminalWidth - width) / 2);
|
|
1458
|
-
const row = Math.floor((terminalHeight - height) / 2);
|
|
1459
|
-
|
|
1460
|
-
// Draw double-line box
|
|
1461
|
-
write(ansi.moveTo(row, col));
|
|
1462
|
-
write(ansi.yellow + ansi.bold);
|
|
1463
|
-
write(box.dTopLeft + box.dHorizontal.repeat(width - 2) + box.dTopRight);
|
|
1464
|
-
|
|
1465
|
-
for (let i = 1; i < height - 1; i++) {
|
|
1466
|
-
write(ansi.moveTo(row + i, col));
|
|
1467
|
-
write(box.dVertical + ' '.repeat(width - 2) + box.dVertical);
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
write(ansi.moveTo(row + height - 1, col));
|
|
1471
|
-
write(box.dBottomLeft + box.dHorizontal.repeat(width - 2) + box.dBottomRight);
|
|
1472
|
-
write(ansi.reset);
|
|
1473
|
-
|
|
1474
|
-
// Content
|
|
1475
|
-
write(ansi.moveTo(row + 1, col + Math.floor((width - 16) / 2)));
|
|
1476
|
-
write(ansi.yellow + ansi.bold + '⚡ NEW UPDATE ⚡' + ansi.reset);
|
|
1477
|
-
|
|
1478
|
-
write(ansi.moveTo(row + 2, col + 2));
|
|
1479
|
-
const truncMsg = truncate(flashMessage, width - 4);
|
|
1480
|
-
write(ansi.white + truncMsg + ansi.reset);
|
|
1481
|
-
|
|
1482
|
-
write(ansi.moveTo(row + 3, col + Math.floor((width - 22) / 2)));
|
|
1483
|
-
write(ansi.gray + 'Press any key to dismiss' + ansi.reset);
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
function renderErrorToast() {
|
|
1487
|
-
if (!errorToast) return;
|
|
1488
|
-
|
|
1489
|
-
const width = Math.min(60, terminalWidth - 4);
|
|
1490
|
-
const col = Math.floor((terminalWidth - width) / 2);
|
|
1491
|
-
const row = 2; // Near the top, below header
|
|
1492
|
-
|
|
1493
|
-
// Calculate height based on content
|
|
1494
|
-
const lines = [];
|
|
1495
|
-
lines.push(errorToast.title || 'Git Error');
|
|
1496
|
-
lines.push('');
|
|
1497
|
-
|
|
1498
|
-
// Word wrap the message
|
|
1499
|
-
const msgWords = errorToast.message.split(' ');
|
|
1500
|
-
let currentLine = '';
|
|
1501
|
-
for (const word of msgWords) {
|
|
1502
|
-
if ((currentLine + ' ' + word).length > width - 6) {
|
|
1503
|
-
lines.push(currentLine.trim());
|
|
1504
|
-
currentLine = word;
|
|
1505
|
-
} else {
|
|
1506
|
-
currentLine += (currentLine ? ' ' : '') + word;
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
if (currentLine) lines.push(currentLine.trim());
|
|
1510
|
-
|
|
1511
|
-
if (errorToast.hint) {
|
|
1512
|
-
lines.push('');
|
|
1513
|
-
lines.push(errorToast.hint);
|
|
1514
|
-
}
|
|
1515
|
-
lines.push('');
|
|
1516
|
-
lines.push('Press any key to dismiss');
|
|
1517
|
-
|
|
1518
|
-
const height = lines.length + 2;
|
|
1519
|
-
|
|
1520
|
-
// Draw red error box
|
|
1521
|
-
write(ansi.moveTo(row, col));
|
|
1522
|
-
write(ansi.red + ansi.bold);
|
|
1523
|
-
write(box.dTopLeft + box.dHorizontal.repeat(width - 2) + box.dTopRight);
|
|
1524
|
-
|
|
1525
|
-
for (let i = 1; i < height - 1; i++) {
|
|
1526
|
-
write(ansi.moveTo(row + i, col));
|
|
1527
|
-
write(ansi.red + box.dVertical + ansi.reset + ansi.bgRed + ansi.white + ' '.repeat(width - 2) + ansi.reset + ansi.red + box.dVertical + ansi.reset);
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
write(ansi.moveTo(row + height - 1, col));
|
|
1531
|
-
write(ansi.red + box.dBottomLeft + box.dHorizontal.repeat(width - 2) + box.dBottomRight);
|
|
1532
|
-
write(ansi.reset);
|
|
1533
|
-
|
|
1534
|
-
// Render content
|
|
1535
|
-
let contentRow = row + 1;
|
|
1536
|
-
for (let i = 0; i < lines.length; i++) {
|
|
1537
|
-
const line = lines[i];
|
|
1538
|
-
write(ansi.moveTo(contentRow, col + 2));
|
|
1539
|
-
write(ansi.bgRed + ansi.white);
|
|
1540
|
-
|
|
1541
|
-
if (i === 0) {
|
|
1542
|
-
// Title line - centered and bold
|
|
1543
|
-
const titlePadding = Math.floor((width - 4 - line.length) / 2);
|
|
1544
|
-
write(' '.repeat(titlePadding) + ansi.bold + line + ansi.reset + ansi.bgRed + ansi.white + ' '.repeat(width - 4 - titlePadding - line.length));
|
|
1545
|
-
} else if (line === 'Press any key to dismiss') {
|
|
1546
|
-
// Instruction line - centered and dimmer
|
|
1547
|
-
const padding = Math.floor((width - 4 - line.length) / 2);
|
|
1548
|
-
write(ansi.reset + ansi.bgRed + ansi.gray + ' '.repeat(padding) + line + ' '.repeat(width - 4 - padding - line.length));
|
|
1549
|
-
} else if (errorToast.hint && line === errorToast.hint) {
|
|
1550
|
-
// Hint line - yellow on red
|
|
1551
|
-
const padding = Math.floor((width - 4 - line.length) / 2);
|
|
1552
|
-
write(ansi.reset + ansi.bgRed + ansi.yellow + ' '.repeat(padding) + line + ' '.repeat(width - 4 - padding - line.length));
|
|
1553
|
-
} else {
|
|
1554
|
-
// Regular content
|
|
1555
|
-
write(padRight(line, width - 4));
|
|
1556
|
-
}
|
|
1557
|
-
write(ansi.reset);
|
|
1558
|
-
contentRow++;
|
|
1559
|
-
}
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
function renderPreview() {
|
|
1563
|
-
if (!previewMode || !previewData) return;
|
|
1564
|
-
|
|
1565
|
-
const width = Math.min(60, terminalWidth - 4);
|
|
1566
|
-
const height = 16;
|
|
1567
|
-
const col = Math.floor((terminalWidth - width) / 2);
|
|
1568
|
-
const row = Math.floor((terminalHeight - height) / 2);
|
|
1569
|
-
|
|
1570
|
-
const displayBranches = filteredBranches !== null ? filteredBranches : branches;
|
|
1571
|
-
const branch = displayBranches[selectedIndex];
|
|
1572
|
-
if (!branch) return;
|
|
1573
|
-
|
|
1574
|
-
// Draw box
|
|
1575
|
-
write(ansi.moveTo(row, col));
|
|
1576
|
-
write(ansi.cyan + ansi.bold);
|
|
1577
|
-
write(box.dTopLeft + box.dHorizontal.repeat(width - 2) + box.dTopRight);
|
|
1578
|
-
|
|
1579
|
-
for (let i = 1; i < height - 1; i++) {
|
|
1580
|
-
write(ansi.moveTo(row + i, col));
|
|
1581
|
-
write(ansi.cyan + box.dVertical + ansi.reset + ' '.repeat(width - 2) + ansi.cyan + box.dVertical + ansi.reset);
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
write(ansi.moveTo(row + height - 1, col));
|
|
1585
|
-
write(ansi.cyan + box.dBottomLeft + box.dHorizontal.repeat(width - 2) + box.dBottomRight);
|
|
1586
|
-
write(ansi.reset);
|
|
1587
|
-
|
|
1588
|
-
// Title
|
|
1589
|
-
const title = ` Preview: ${truncate(branch.name, width - 14)} `;
|
|
1590
|
-
write(ansi.moveTo(row, col + 2));
|
|
1591
|
-
write(ansi.cyan + ansi.bold + title + ansi.reset);
|
|
1592
|
-
|
|
1593
|
-
// Commits section
|
|
1594
|
-
write(ansi.moveTo(row + 2, col + 2));
|
|
1595
|
-
write(ansi.white + ansi.bold + 'Recent Commits:' + ansi.reset);
|
|
1596
|
-
|
|
1597
|
-
let contentRow = row + 3;
|
|
1598
|
-
if (previewData.commits.length === 0) {
|
|
1599
|
-
write(ansi.moveTo(contentRow, col + 3));
|
|
1600
|
-
write(ansi.gray + '(no commits)' + ansi.reset);
|
|
1601
|
-
contentRow++;
|
|
1602
|
-
} else {
|
|
1603
|
-
for (const commit of previewData.commits.slice(0, 5)) {
|
|
1604
|
-
write(ansi.moveTo(contentRow, col + 3));
|
|
1605
|
-
write(ansi.yellow + commit.hash + ansi.reset + ' ');
|
|
1606
|
-
write(ansi.gray + truncate(commit.message, width - 14) + ansi.reset);
|
|
1607
|
-
contentRow++;
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
// Files section
|
|
1612
|
-
contentRow++;
|
|
1613
|
-
write(ansi.moveTo(contentRow, col + 2));
|
|
1614
|
-
write(ansi.white + ansi.bold + 'Files Changed vs HEAD:' + ansi.reset);
|
|
1615
|
-
contentRow++;
|
|
1616
|
-
|
|
1617
|
-
if (previewData.filesChanged.length === 0) {
|
|
1618
|
-
write(ansi.moveTo(contentRow, col + 3));
|
|
1619
|
-
write(ansi.gray + '(no changes or same as current)' + ansi.reset);
|
|
1620
|
-
} else {
|
|
1621
|
-
for (const file of previewData.filesChanged.slice(0, 5)) {
|
|
1622
|
-
write(ansi.moveTo(contentRow, col + 3));
|
|
1623
|
-
write(ansi.green + '• ' + ansi.reset + truncate(file, width - 8));
|
|
1624
|
-
contentRow++;
|
|
1625
|
-
}
|
|
1626
|
-
if (previewData.filesChanged.length > 5) {
|
|
1627
|
-
write(ansi.moveTo(contentRow, col + 3));
|
|
1628
|
-
write(ansi.gray + `... and ${previewData.filesChanged.length - 5} more` + ansi.reset);
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
// Instructions
|
|
1633
|
-
write(ansi.moveTo(row + height - 2, col + Math.floor((width - 26) / 2)));
|
|
1634
|
-
write(ansi.gray + 'Press [v] or [Esc] to close' + ansi.reset);
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
function renderHistory() {
|
|
1638
|
-
const width = Math.min(50, terminalWidth - 4);
|
|
1639
|
-
const height = Math.min(switchHistory.length + 5, 15);
|
|
1640
|
-
const col = Math.floor((terminalWidth - width) / 2);
|
|
1641
|
-
const row = Math.floor((terminalHeight - height) / 2);
|
|
1642
|
-
|
|
1643
|
-
// Draw box
|
|
1644
|
-
write(ansi.moveTo(row, col));
|
|
1645
|
-
write(ansi.magenta + ansi.bold);
|
|
1646
|
-
write(box.dTopLeft + box.dHorizontal.repeat(width - 2) + box.dTopRight);
|
|
1647
|
-
|
|
1648
|
-
for (let i = 1; i < height - 1; i++) {
|
|
1649
|
-
write(ansi.moveTo(row + i, col));
|
|
1650
|
-
write(ansi.magenta + box.dVertical + ansi.reset + ' '.repeat(width - 2) + ansi.magenta + box.dVertical + ansi.reset);
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
write(ansi.moveTo(row + height - 1, col));
|
|
1654
|
-
write(ansi.magenta + box.dBottomLeft + box.dHorizontal.repeat(width - 2) + box.dBottomRight);
|
|
1655
|
-
write(ansi.reset);
|
|
1064
|
+
// renderFooter, renderFlash, renderErrorToast, renderPreview, renderHistory
|
|
1065
|
+
// — now delegated to renderer module (src/ui/renderer.js)
|
|
1656
1066
|
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
write(ansi.magenta + ansi.bold + ' Switch History ' + ansi.reset);
|
|
1067
|
+
// renderLogView, renderInfo, renderActionModal
|
|
1068
|
+
// — now delegated to renderer module (src/ui/renderer.js)
|
|
1660
1069
|
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
let contentRow = row + 2;
|
|
1667
|
-
for (let i = 0; i < Math.min(switchHistory.length, height - 4); i++) {
|
|
1668
|
-
const entry = switchHistory[i];
|
|
1669
|
-
write(ansi.moveTo(contentRow, col + 3));
|
|
1670
|
-
if (i === 0) {
|
|
1671
|
-
write(ansi.yellow + '→ ' + ansi.reset); // Most recent
|
|
1672
|
-
} else {
|
|
1673
|
-
write(ansi.gray + ' ' + ansi.reset);
|
|
1674
|
-
}
|
|
1675
|
-
write(truncate(entry.from, 15) + ansi.gray + ' → ' + ansi.reset);
|
|
1676
|
-
write(ansi.cyan + truncate(entry.to, 15) + ansi.reset);
|
|
1677
|
-
contentRow++;
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
// Instructions
|
|
1682
|
-
write(ansi.moveTo(row + height - 2, col + 2));
|
|
1683
|
-
write(ansi.gray + '[u] Undo last [h]/[Esc] Close' + ansi.reset);
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
let historyMode = false;
|
|
1687
|
-
let infoMode = false;
|
|
1688
|
-
|
|
1689
|
-
function renderLogView() {
|
|
1690
|
-
if (!logViewMode) return;
|
|
1691
|
-
|
|
1692
|
-
const width = Math.min(terminalWidth - 4, 100);
|
|
1693
|
-
const height = Math.min(terminalHeight - 4, 30);
|
|
1694
|
-
const col = Math.floor((terminalWidth - width) / 2);
|
|
1695
|
-
const row = Math.floor((terminalHeight - height) / 2);
|
|
1696
|
-
|
|
1697
|
-
// Determine which log to display
|
|
1698
|
-
const isServerTab = logViewTab === 'server';
|
|
1699
|
-
const logData = isServerTab ? serverLogBuffer : activityLog;
|
|
1700
|
-
|
|
1701
|
-
// Draw box
|
|
1702
|
-
write(ansi.moveTo(row, col));
|
|
1703
|
-
write(ansi.yellow + ansi.bold);
|
|
1704
|
-
write(box.dTopLeft + box.dHorizontal.repeat(width - 2) + box.dTopRight);
|
|
1705
|
-
|
|
1706
|
-
for (let i = 1; i < height - 1; i++) {
|
|
1707
|
-
write(ansi.moveTo(row + i, col));
|
|
1708
|
-
write(ansi.yellow + box.dVertical + ansi.reset + ' '.repeat(width - 2) + ansi.yellow + box.dVertical + ansi.reset);
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
write(ansi.moveTo(row + height - 1, col));
|
|
1712
|
-
write(ansi.yellow + box.dBottomLeft + box.dHorizontal.repeat(width - 2) + box.dBottomRight);
|
|
1713
|
-
write(ansi.reset);
|
|
1714
|
-
|
|
1715
|
-
// Title with tabs
|
|
1716
|
-
const activityTab = logViewTab === 'activity'
|
|
1717
|
-
? ansi.bgWhite + ansi.black + ' 1:Activity ' + ansi.reset + ansi.yellow
|
|
1718
|
-
: ansi.gray + ' 1:Activity ' + ansi.yellow;
|
|
1719
|
-
const serverTab = logViewTab === 'server'
|
|
1720
|
-
? ansi.bgWhite + ansi.black + ' 2:Server ' + ansi.reset + ansi.yellow
|
|
1721
|
-
: ansi.gray + ' 2:Server ' + ansi.yellow;
|
|
1722
|
-
|
|
1723
|
-
// Server status (only show on server tab)
|
|
1724
|
-
let statusIndicator = '';
|
|
1725
|
-
if (isServerTab && SERVER_MODE === 'command') {
|
|
1726
|
-
const statusText = serverRunning ? ansi.green + 'RUNNING' : (serverCrashed ? ansi.red + 'CRASHED' : ansi.gray + 'STOPPED');
|
|
1727
|
-
statusIndicator = ` [${statusText}${ansi.yellow}]`;
|
|
1728
|
-
} else if (isServerTab && SERVER_MODE === 'static') {
|
|
1729
|
-
statusIndicator = ansi.green + ' [STATIC]' + ansi.yellow;
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
write(ansi.moveTo(row, col + 2));
|
|
1733
|
-
write(ansi.yellow + ansi.bold + ' ' + activityTab + ' ' + serverTab + statusIndicator + ' ' + ansi.reset);
|
|
1734
|
-
|
|
1735
|
-
// Content
|
|
1736
|
-
const contentHeight = height - 4;
|
|
1737
|
-
const maxScroll = Math.max(0, logData.length - contentHeight);
|
|
1738
|
-
logScrollOffset = Math.min(logScrollOffset, maxScroll);
|
|
1739
|
-
logScrollOffset = Math.max(0, logScrollOffset);
|
|
1740
|
-
|
|
1741
|
-
let contentRow = row + 2;
|
|
1742
|
-
|
|
1743
|
-
if (logData.length === 0) {
|
|
1744
|
-
write(ansi.moveTo(contentRow, col + 2));
|
|
1745
|
-
write(ansi.gray + (isServerTab ? 'No server output yet...' : 'No activity yet...') + ansi.reset);
|
|
1746
|
-
} else if (isServerTab) {
|
|
1747
|
-
// Server log: newest at bottom, scroll from bottom
|
|
1748
|
-
const startIndex = Math.max(0, serverLogBuffer.length - contentHeight - logScrollOffset);
|
|
1749
|
-
const endIndex = Math.min(serverLogBuffer.length, startIndex + contentHeight);
|
|
1750
|
-
|
|
1751
|
-
for (let i = startIndex; i < endIndex; i++) {
|
|
1752
|
-
const entry = serverLogBuffer[i];
|
|
1753
|
-
write(ansi.moveTo(contentRow, col + 2));
|
|
1754
|
-
const lineText = truncate(entry.line, width - 4);
|
|
1755
|
-
if (entry.isError) {
|
|
1756
|
-
write(ansi.red + lineText + ansi.reset);
|
|
1757
|
-
} else {
|
|
1758
|
-
write(lineText);
|
|
1759
|
-
}
|
|
1760
|
-
contentRow++;
|
|
1761
|
-
}
|
|
1762
|
-
} else {
|
|
1763
|
-
// Activity log: newest first, scroll from top
|
|
1764
|
-
const startIndex = logScrollOffset;
|
|
1765
|
-
const endIndex = Math.min(activityLog.length, startIndex + contentHeight);
|
|
1766
|
-
|
|
1767
|
-
for (let i = startIndex; i < endIndex; i++) {
|
|
1768
|
-
const entry = activityLog[i];
|
|
1769
|
-
write(ansi.moveTo(contentRow, col + 2));
|
|
1770
|
-
write(ansi.gray + `[${entry.timestamp}]` + ansi.reset + ' ');
|
|
1771
|
-
write(ansi[entry.color] + entry.icon + ansi.reset + ' ');
|
|
1772
|
-
write(truncate(entry.message, width - 18));
|
|
1773
|
-
contentRow++;
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
// Scroll indicator
|
|
1778
|
-
if (logData.length > contentHeight) {
|
|
1779
|
-
const scrollPercent = isServerTab
|
|
1780
|
-
? Math.round((1 - logScrollOffset / maxScroll) * 100)
|
|
1781
|
-
: Math.round((logScrollOffset / maxScroll) * 100);
|
|
1782
|
-
write(ansi.moveTo(row, col + width - 10));
|
|
1783
|
-
write(ansi.gray + ` ${scrollPercent}% ` + ansi.reset);
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
// Instructions
|
|
1787
|
-
write(ansi.moveTo(row + height - 2, col + 2));
|
|
1788
|
-
const restartHint = SERVER_MODE === 'command' ? '[R] Restart ' : '';
|
|
1789
|
-
write(ansi.gray + '[1/2] Switch Tab [↑↓] Scroll ' + restartHint + '[l]/[Esc] Close' + ansi.reset);
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
function renderInfo() {
|
|
1793
|
-
const width = Math.min(50, terminalWidth - 4);
|
|
1794
|
-
const height = NO_SERVER ? 9 : 12;
|
|
1795
|
-
const col = Math.floor((terminalWidth - width) / 2);
|
|
1796
|
-
const row = Math.floor((terminalHeight - height) / 2);
|
|
1797
|
-
|
|
1798
|
-
// Draw box
|
|
1799
|
-
write(ansi.moveTo(row, col));
|
|
1800
|
-
write(ansi.cyan + ansi.bold);
|
|
1801
|
-
write(box.dTopLeft + box.dHorizontal.repeat(width - 2) + box.dTopRight);
|
|
1802
|
-
|
|
1803
|
-
for (let i = 1; i < height - 1; i++) {
|
|
1804
|
-
write(ansi.moveTo(row + i, col));
|
|
1805
|
-
write(ansi.cyan + box.dVertical + ansi.reset + ' '.repeat(width - 2) + ansi.cyan + box.dVertical + ansi.reset);
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
write(ansi.moveTo(row + height - 1, col));
|
|
1809
|
-
write(ansi.cyan + box.dBottomLeft + box.dHorizontal.repeat(width - 2) + box.dBottomRight);
|
|
1810
|
-
write(ansi.reset);
|
|
1811
|
-
|
|
1812
|
-
// Title
|
|
1813
|
-
write(ansi.moveTo(row, col + 2));
|
|
1814
|
-
write(ansi.cyan + ansi.bold + (NO_SERVER ? ' Status Info ' : ' Server Info ') + ansi.reset);
|
|
1815
|
-
|
|
1816
|
-
// Content
|
|
1817
|
-
let contentRow = row + 2;
|
|
1818
|
-
|
|
1819
|
-
if (!NO_SERVER) {
|
|
1820
|
-
write(ansi.moveTo(contentRow, col + 3));
|
|
1821
|
-
write(ansi.white + ansi.bold + 'Dev Server' + ansi.reset);
|
|
1822
|
-
contentRow++;
|
|
1823
|
-
|
|
1824
|
-
write(ansi.moveTo(contentRow, col + 3));
|
|
1825
|
-
write(ansi.gray + 'URL: ' + ansi.reset + ansi.green + `http://localhost:${PORT}` + ansi.reset);
|
|
1826
|
-
contentRow++;
|
|
1827
|
-
|
|
1828
|
-
write(ansi.moveTo(contentRow, col + 3));
|
|
1829
|
-
write(ansi.gray + 'Port: ' + ansi.reset + ansi.yellow + PORT + ansi.reset);
|
|
1830
|
-
contentRow++;
|
|
1831
|
-
|
|
1832
|
-
write(ansi.moveTo(contentRow, col + 3));
|
|
1833
|
-
write(ansi.gray + 'Connected browsers: ' + ansi.reset + ansi.cyan + clients.size + ansi.reset);
|
|
1834
|
-
contentRow++;
|
|
1835
|
-
|
|
1836
|
-
contentRow++;
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
write(ansi.moveTo(contentRow, col + 3));
|
|
1840
|
-
write(ansi.white + ansi.bold + 'Git Polling' + ansi.reset);
|
|
1841
|
-
contentRow++;
|
|
1842
|
-
|
|
1843
|
-
write(ansi.moveTo(contentRow, col + 3));
|
|
1844
|
-
write(ansi.gray + 'Interval: ' + ansi.reset + `${adaptivePollInterval / 1000}s`);
|
|
1845
|
-
contentRow++;
|
|
1846
|
-
|
|
1847
|
-
write(ansi.moveTo(contentRow, col + 3));
|
|
1848
|
-
write(ansi.gray + 'Status: ' + ansi.reset + (isOffline ? ansi.red + 'Offline' : ansi.green + 'Online') + ansi.reset);
|
|
1849
|
-
contentRow++;
|
|
1850
|
-
|
|
1851
|
-
if (NO_SERVER) {
|
|
1852
|
-
write(ansi.moveTo(contentRow, col + 3));
|
|
1853
|
-
write(ansi.gray + 'Mode: ' + ansi.reset + ansi.magenta + 'No-Server (branch monitor only)' + ansi.reset);
|
|
1854
|
-
}
|
|
1855
|
-
|
|
1856
|
-
// Instructions
|
|
1857
|
-
write(ansi.moveTo(row + height - 2, col + Math.floor((width - 20) / 2)));
|
|
1858
|
-
write(ansi.gray + 'Press [i] or [Esc] to close' + ansi.reset);
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
function renderActionModal() {
|
|
1862
|
-
if (!actionMode || !actionData) return;
|
|
1863
|
-
|
|
1864
|
-
const { branch, sessionUrl, prInfo, hasGh, hasGlab, ghAuthed, glabAuthed, webUrl, isClaudeBranch, platform, prLoaded } = actionData;
|
|
1865
|
-
|
|
1866
|
-
const width = Math.min(64, terminalWidth - 4);
|
|
1867
|
-
const innerW = width - 6;
|
|
1868
|
-
|
|
1869
|
-
const platformLabel = platform === 'gitlab' ? 'GitLab' : platform === 'bitbucket' ? 'Bitbucket' : platform === 'azure' ? 'Azure DevOps' : 'GitHub';
|
|
1870
|
-
const prLabel = platform === 'gitlab' ? 'MR' : 'PR';
|
|
1871
|
-
const cliTool = platform === 'gitlab' ? 'glab' : 'gh';
|
|
1872
|
-
const hasCli = platform === 'gitlab' ? hasGlab : hasGh;
|
|
1873
|
-
const cliAuthed = platform === 'gitlab' ? glabAuthed : ghAuthed;
|
|
1874
|
-
const cliReady = hasCli && cliAuthed;
|
|
1875
|
-
const loading = actionLoading; // PR info still loading
|
|
1876
|
-
|
|
1877
|
-
// Build actions list — ALL actions always shown, grayed out with reasons when unavailable
|
|
1878
|
-
// { key, label, available, reason, loading }
|
|
1879
|
-
const actions = [];
|
|
1880
|
-
|
|
1881
|
-
// Open on web
|
|
1882
|
-
actions.push({
|
|
1883
|
-
key: 'b', label: `Open branch on ${platformLabel}`,
|
|
1884
|
-
available: !!webUrl, reason: !webUrl ? 'Could not parse remote URL' : null,
|
|
1885
|
-
});
|
|
1886
|
-
|
|
1887
|
-
// Claude session — always shown so users know it exists
|
|
1888
|
-
actions.push({
|
|
1889
|
-
key: 'c', label: 'Open Claude Code session',
|
|
1890
|
-
available: !!sessionUrl,
|
|
1891
|
-
reason: !isClaudeBranch ? 'Not a Claude branch' : !sessionUrl && !loading ? 'No session URL in commits' : null,
|
|
1892
|
-
loading: isClaudeBranch && !sessionUrl && loading,
|
|
1893
|
-
});
|
|
1894
|
-
|
|
1895
|
-
// PR: create or view depending on state
|
|
1896
|
-
const prIsMerged = prInfo && (prInfo.state === 'MERGED' || prInfo.state === 'merged');
|
|
1897
|
-
const prIsOpen = prInfo && (prInfo.state === 'OPEN' || prInfo.state === 'open');
|
|
1898
|
-
if (prInfo) {
|
|
1899
|
-
actions.push({ key: 'p', label: `View ${prLabel} #${prInfo.number}`, available: !!webUrl, reason: null });
|
|
1900
|
-
} else {
|
|
1901
|
-
actions.push({
|
|
1902
|
-
key: 'p', label: `Create ${prLabel}`,
|
|
1903
|
-
available: cliReady && prLoaded,
|
|
1904
|
-
reason: !hasCli ? `Requires ${cliTool} CLI` : !cliAuthed ? `Run: ${cliTool} auth login` : null,
|
|
1905
|
-
loading: cliReady && !prLoaded,
|
|
1906
|
-
});
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
// Diff — opens on web, just needs a PR and webUrl
|
|
1910
|
-
actions.push({
|
|
1911
|
-
key: 'd', label: `View ${prLabel} diff on ${platformLabel}`,
|
|
1912
|
-
available: !!prInfo && !!webUrl,
|
|
1913
|
-
reason: !prInfo && prLoaded ? `No ${prLabel}` : !webUrl ? 'Could not parse remote URL' : null,
|
|
1914
|
-
loading: !prLoaded && (cliReady || !!webUrl),
|
|
1915
|
-
});
|
|
1916
|
-
|
|
1917
|
-
// Approve — disabled for merged PRs
|
|
1918
|
-
actions.push({
|
|
1919
|
-
key: 'a', label: `Approve ${prLabel}`,
|
|
1920
|
-
available: !!prInfo && prIsOpen && cliReady,
|
|
1921
|
-
reason: prIsMerged ? `${prLabel} already merged` : !hasCli ? `Requires ${cliTool} CLI` : !cliAuthed ? `Run: ${cliTool} auth login` : !prInfo && prLoaded ? `No open ${prLabel}` : null,
|
|
1922
|
-
loading: cliReady && !prLoaded,
|
|
1923
|
-
});
|
|
1924
|
-
|
|
1925
|
-
// Merge — disabled for already-merged PRs
|
|
1926
|
-
actions.push({
|
|
1927
|
-
key: 'm', label: `Merge ${prLabel} (squash)`,
|
|
1928
|
-
available: !!prInfo && prIsOpen && cliReady,
|
|
1929
|
-
reason: prIsMerged ? `${prLabel} already merged` : !hasCli ? `Requires ${cliTool} CLI` : !cliAuthed ? `Run: ${cliTool} auth login` : !prInfo && prLoaded ? `No open ${prLabel}` : null,
|
|
1930
|
-
loading: cliReady && !prLoaded,
|
|
1931
|
-
});
|
|
1932
|
-
|
|
1933
|
-
// CI
|
|
1934
|
-
actions.push({
|
|
1935
|
-
key: 'i', label: 'Check CI status',
|
|
1936
|
-
available: cliReady && (!!prInfo || platform === 'gitlab'),
|
|
1937
|
-
reason: !hasCli ? `Requires ${cliTool} CLI` : !cliAuthed ? `Run: ${cliTool} auth login` : !prInfo && prLoaded && platform !== 'gitlab' ? `No open ${prLabel}` : null,
|
|
1938
|
-
loading: cliReady && !prLoaded && platform !== 'gitlab',
|
|
1939
|
-
});
|
|
1940
|
-
|
|
1941
|
-
// Calculate height
|
|
1942
|
-
let contentLines = 0;
|
|
1943
|
-
contentLines += 2; // spacing + branch name
|
|
1944
|
-
contentLines += 1; // separator
|
|
1945
|
-
contentLines += actions.length;
|
|
1946
|
-
contentLines += 1; // separator
|
|
1947
|
-
|
|
1948
|
-
// Status info
|
|
1949
|
-
const statusInfoLines = [];
|
|
1950
|
-
if (prInfo) {
|
|
1951
|
-
let prStatus = `${prLabel} #${prInfo.number}: ${truncate(prInfo.title, innerW - 20)}`;
|
|
1952
|
-
const badges = [];
|
|
1953
|
-
if (prIsMerged) badges.push('merged');
|
|
1954
|
-
if (prInfo.approved) badges.push('approved');
|
|
1955
|
-
if (prInfo.checksPass) badges.push('checks pass');
|
|
1956
|
-
if (prInfo.checksFail) badges.push('checks fail');
|
|
1957
|
-
if (badges.length) prStatus += ` [${badges.join(', ')}]`;
|
|
1958
|
-
statusInfoLines.push({ color: prIsMerged ? 'magenta' : 'green', text: prStatus });
|
|
1959
|
-
} else if (loading) {
|
|
1960
|
-
statusInfoLines.push({ color: 'gray', text: `Loading ${prLabel} info...` });
|
|
1961
|
-
} else if (cliReady) {
|
|
1962
|
-
statusInfoLines.push({ color: 'gray', text: `No ${prLabel} for this branch` });
|
|
1963
|
-
}
|
|
1964
|
-
|
|
1965
|
-
if (isClaudeBranch) {
|
|
1966
|
-
if (sessionUrl) {
|
|
1967
|
-
const shortSession = sessionUrl.replace('https://claude.ai/code/', '');
|
|
1968
|
-
statusInfoLines.push({ color: 'magenta', text: `Session: ${truncate(shortSession, innerW - 10)}` });
|
|
1969
|
-
} else if (!loading) {
|
|
1970
|
-
statusInfoLines.push({ color: 'gray', text: 'Claude branch (no session URL in commits)' });
|
|
1971
|
-
}
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
contentLines += statusInfoLines.length;
|
|
1975
|
-
|
|
1976
|
-
// Setup hints
|
|
1977
|
-
const hints = [];
|
|
1978
|
-
if (!hasCli) {
|
|
1979
|
-
if (platform === 'gitlab') {
|
|
1980
|
-
hints.push(`Install glab: https://gitlab.com/gitlab-org/cli`);
|
|
1981
|
-
hints.push(`Then run: glab auth login`);
|
|
1982
|
-
} else {
|
|
1983
|
-
hints.push(`Install gh: https://cli.github.com`);
|
|
1984
|
-
hints.push(`Then run: gh auth login`);
|
|
1985
|
-
}
|
|
1986
|
-
} else if (!cliAuthed) {
|
|
1987
|
-
hints.push(`${cliTool} is installed but not authenticated`);
|
|
1988
|
-
hints.push(`Run: ${cliTool} auth login`);
|
|
1989
|
-
}
|
|
1990
|
-
|
|
1991
|
-
if (hints.length > 0) {
|
|
1992
|
-
contentLines += 1;
|
|
1993
|
-
contentLines += hints.length;
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
contentLines += 2; // blank + close instructions
|
|
1997
|
-
|
|
1998
|
-
const height = contentLines + 3;
|
|
1999
|
-
const col = Math.floor((terminalWidth - width) / 2);
|
|
2000
|
-
const row = Math.floor((terminalHeight - height) / 2);
|
|
2001
|
-
|
|
2002
|
-
// Draw box
|
|
2003
|
-
const borderColor = ansi.brightCyan;
|
|
2004
|
-
write(ansi.moveTo(row, col));
|
|
2005
|
-
write(borderColor + ansi.bold);
|
|
2006
|
-
write(box.dTopLeft + box.dHorizontal.repeat(width - 2) + box.dTopRight);
|
|
2007
|
-
|
|
2008
|
-
for (let i = 1; i < height - 1; i++) {
|
|
2009
|
-
write(ansi.moveTo(row + i, col));
|
|
2010
|
-
write(borderColor + box.dVertical + ansi.reset + ' '.repeat(width - 2) + borderColor + box.dVertical + ansi.reset);
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
write(ansi.moveTo(row + height - 1, col));
|
|
2014
|
-
write(borderColor + box.dBottomLeft + box.dHorizontal.repeat(width - 2) + box.dBottomRight);
|
|
2015
|
-
write(ansi.reset);
|
|
2016
|
-
|
|
2017
|
-
// Title
|
|
2018
|
-
const title = ' Branch Actions ';
|
|
2019
|
-
write(ansi.moveTo(row, col + 2));
|
|
2020
|
-
write(borderColor + ansi.bold + title + ansi.reset);
|
|
2021
|
-
|
|
2022
|
-
let r = row + 2;
|
|
2023
|
-
|
|
2024
|
-
// Branch name with type indicator
|
|
2025
|
-
write(ansi.moveTo(r, col + 3));
|
|
2026
|
-
write(ansi.white + ansi.bold + truncate(branch.name, innerW - 10) + ansi.reset);
|
|
2027
|
-
if (isClaudeBranch) {
|
|
2028
|
-
write(ansi.magenta + ' [Claude]' + ansi.reset);
|
|
2029
|
-
}
|
|
2030
|
-
r++;
|
|
2031
|
-
|
|
2032
|
-
// Separator
|
|
2033
|
-
r++;
|
|
2034
|
-
|
|
2035
|
-
// Actions list — all always visible
|
|
2036
|
-
for (const action of actions) {
|
|
2037
|
-
write(ansi.moveTo(r, col + 3));
|
|
2038
|
-
if (action.loading) {
|
|
2039
|
-
write(ansi.gray + '[' + action.key + '] ' + action.label + ' ' + ansi.dim + ansi.cyan + 'loading...' + ansi.reset);
|
|
2040
|
-
} else if (action.available) {
|
|
2041
|
-
write(ansi.brightCyan + '[' + action.key + ']' + ansi.reset + ' ' + action.label);
|
|
2042
|
-
} else {
|
|
2043
|
-
write(ansi.gray + '[' + action.key + '] ' + action.label);
|
|
2044
|
-
if (action.reason) {
|
|
2045
|
-
write(' ' + ansi.dim + ansi.yellow + action.reason + ansi.reset);
|
|
2046
|
-
}
|
|
2047
|
-
write(ansi.reset);
|
|
2048
|
-
}
|
|
2049
|
-
r++;
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
// Separator
|
|
2053
|
-
r++;
|
|
2054
|
-
|
|
2055
|
-
// Status info
|
|
2056
|
-
for (const info of statusInfoLines) {
|
|
2057
|
-
write(ansi.moveTo(r, col + 3));
|
|
2058
|
-
write(ansi[info.color] + truncate(info.text, innerW) + ansi.reset);
|
|
2059
|
-
r++;
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
// Setup hints
|
|
2063
|
-
if (hints.length > 0) {
|
|
2064
|
-
r++;
|
|
2065
|
-
for (const hint of hints) {
|
|
2066
|
-
write(ansi.moveTo(r, col + 3));
|
|
2067
|
-
write(ansi.yellow + truncate(hint, innerW) + ansi.reset);
|
|
2068
|
-
r++;
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
|
-
// Close instructions
|
|
2073
|
-
write(ansi.moveTo(row + height - 2, col + Math.floor((width - 18) / 2)));
|
|
2074
|
-
write(ansi.gray + 'Press [Esc] to close' + ansi.reset);
|
|
1070
|
+
// Build a state snapshot from the current globals for the renderer
|
|
1071
|
+
function getRenderState() {
|
|
1072
|
+
const s = store.getState();
|
|
1073
|
+
s.clientCount = clients.size;
|
|
1074
|
+
return s;
|
|
2075
1075
|
}
|
|
2076
1076
|
|
|
2077
1077
|
function render() {
|
|
@@ -2081,17 +1081,21 @@ function render() {
|
|
|
2081
1081
|
write(ansi.moveToTop);
|
|
2082
1082
|
write(ansi.clearScreen);
|
|
2083
1083
|
|
|
1084
|
+
const state = getRenderState();
|
|
1085
|
+
const { casinoModeEnabled, terminalWidth, terminalHeight } = state;
|
|
1086
|
+
|
|
2084
1087
|
// Casino mode: top marquee border
|
|
2085
1088
|
if (casinoModeEnabled) {
|
|
2086
1089
|
write(ansi.moveTo(1, 1));
|
|
2087
1090
|
write(casino.renderMarqueeLine(terminalWidth, 'top'));
|
|
2088
1091
|
}
|
|
2089
1092
|
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
const
|
|
1093
|
+
// Delegate to extracted renderer module
|
|
1094
|
+
renderer.renderHeader(state, write);
|
|
1095
|
+
const logStart = renderer.renderBranchList(state, write);
|
|
1096
|
+
const statsStart = renderer.renderActivityLog(state, write, logStart);
|
|
2093
1097
|
renderCasinoStats(statsStart);
|
|
2094
|
-
renderFooter();
|
|
1098
|
+
renderer.renderFooter(state, write);
|
|
2095
1099
|
|
|
2096
1100
|
// Casino mode: full border (top, bottom, left, right)
|
|
2097
1101
|
if (casinoModeEnabled) {
|
|
@@ -2114,7 +1118,6 @@ function render() {
|
|
|
2114
1118
|
if (casinoModeEnabled && casino.isSlotsActive()) {
|
|
2115
1119
|
const slotDisplay = casino.getSlotReelDisplay();
|
|
2116
1120
|
if (slotDisplay) {
|
|
2117
|
-
// Row 3: below header (row 1 is marquee, row 2 is header)
|
|
2118
1121
|
const resultLabel = casino.getSlotResultLabel();
|
|
2119
1122
|
let leftLabel, rightLabel;
|
|
2120
1123
|
|
|
@@ -2123,7 +1126,6 @@ function render() {
|
|
|
2123
1126
|
rightLabel = '';
|
|
2124
1127
|
} else if (resultLabel) {
|
|
2125
1128
|
leftLabel = ansi.bgBrightGreen + ansi.black + ansi.bold + ' RESULT ' + ansi.reset;
|
|
2126
|
-
// Flash effect for jackpots, use result color for text
|
|
2127
1129
|
const flash = resultLabel.isJackpot && (Math.floor(Date.now() / 150) % 2 === 0);
|
|
2128
1130
|
const bgColor = flash ? ansi.bgBrightYellow : ansi.bgWhite;
|
|
2129
1131
|
rightLabel = ' ' + bgColor + resultLabel.color + ansi.bold + ' ' + resultLabel.text + ' ' + ansi.reset;
|
|
@@ -2133,7 +1135,7 @@ function render() {
|
|
|
2133
1135
|
}
|
|
2134
1136
|
|
|
2135
1137
|
const fullDisplay = leftLabel + ' ' + slotDisplay + rightLabel;
|
|
2136
|
-
const col = Math.floor((terminalWidth - 70) / 2);
|
|
1138
|
+
const col = Math.floor((terminalWidth - 70) / 2);
|
|
2137
1139
|
write(ansi.moveTo(3, Math.max(2, col)));
|
|
2138
1140
|
write(fullDisplay);
|
|
2139
1141
|
}
|
|
@@ -2159,44 +1161,50 @@ function render() {
|
|
|
2159
1161
|
}
|
|
2160
1162
|
}
|
|
2161
1163
|
|
|
2162
|
-
|
|
2163
|
-
|
|
1164
|
+
// Delegate modal/overlay rendering to extracted renderer
|
|
1165
|
+
if (state.flashMessage) {
|
|
1166
|
+
renderer.renderFlash(state, write);
|
|
2164
1167
|
}
|
|
2165
1168
|
|
|
2166
|
-
if (previewMode && previewData) {
|
|
2167
|
-
renderPreview();
|
|
1169
|
+
if (state.previewMode && state.previewData) {
|
|
1170
|
+
renderer.renderPreview(state, write);
|
|
2168
1171
|
}
|
|
2169
1172
|
|
|
2170
|
-
if (historyMode) {
|
|
2171
|
-
renderHistory();
|
|
1173
|
+
if (state.historyMode) {
|
|
1174
|
+
renderer.renderHistory(state, write);
|
|
2172
1175
|
}
|
|
2173
1176
|
|
|
2174
|
-
if (infoMode) {
|
|
2175
|
-
renderInfo();
|
|
1177
|
+
if (state.infoMode) {
|
|
1178
|
+
renderer.renderInfo(state, write);
|
|
2176
1179
|
}
|
|
2177
1180
|
|
|
2178
|
-
if (logViewMode) {
|
|
2179
|
-
renderLogView();
|
|
1181
|
+
if (state.logViewMode) {
|
|
1182
|
+
renderer.renderLogView(state, write);
|
|
2180
1183
|
}
|
|
2181
1184
|
|
|
2182
|
-
if (actionMode) {
|
|
2183
|
-
renderActionModal();
|
|
1185
|
+
if (state.actionMode) {
|
|
1186
|
+
renderer.renderActionModal(state, write);
|
|
2184
1187
|
}
|
|
2185
1188
|
|
|
2186
1189
|
// Error toast renders on top of everything for maximum visibility
|
|
2187
|
-
if (errorToast) {
|
|
2188
|
-
renderErrorToast();
|
|
1190
|
+
if (state.errorToast) {
|
|
1191
|
+
renderer.renderErrorToast(state, write);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Stash confirmation dialog renders on top of everything
|
|
1195
|
+
if (state.stashConfirmMode) {
|
|
1196
|
+
renderer.renderStashConfirm(state, write);
|
|
2189
1197
|
}
|
|
2190
1198
|
}
|
|
2191
1199
|
|
|
2192
1200
|
function showFlash(message) {
|
|
2193
1201
|
if (flashTimeout) clearTimeout(flashTimeout);
|
|
2194
1202
|
|
|
2195
|
-
flashMessage
|
|
1203
|
+
store.setState({ flashMessage: message });
|
|
2196
1204
|
render();
|
|
2197
1205
|
|
|
2198
1206
|
flashTimeout = setTimeout(() => {
|
|
2199
|
-
flashMessage
|
|
1207
|
+
store.setState({ flashMessage: null });
|
|
2200
1208
|
render();
|
|
2201
1209
|
}, 3000);
|
|
2202
1210
|
}
|
|
@@ -2206,8 +1214,8 @@ function hideFlash() {
|
|
|
2206
1214
|
clearTimeout(flashTimeout);
|
|
2207
1215
|
flashTimeout = null;
|
|
2208
1216
|
}
|
|
2209
|
-
if (flashMessage) {
|
|
2210
|
-
flashMessage
|
|
1217
|
+
if (store.get('flashMessage')) {
|
|
1218
|
+
store.setState({ flashMessage: null });
|
|
2211
1219
|
render();
|
|
2212
1220
|
}
|
|
2213
1221
|
}
|
|
@@ -2215,12 +1223,12 @@ function hideFlash() {
|
|
|
2215
1223
|
function showErrorToast(title, message, hint = null, duration = 8000) {
|
|
2216
1224
|
if (errorToastTimeout) clearTimeout(errorToastTimeout);
|
|
2217
1225
|
|
|
2218
|
-
errorToast
|
|
1226
|
+
store.setState({ errorToast: { title, message, hint } });
|
|
2219
1227
|
playSound(); // Alert sound for errors
|
|
2220
1228
|
render();
|
|
2221
1229
|
|
|
2222
1230
|
errorToastTimeout = setTimeout(() => {
|
|
2223
|
-
errorToast
|
|
1231
|
+
store.setState({ errorToast: null });
|
|
2224
1232
|
render();
|
|
2225
1233
|
}, duration);
|
|
2226
1234
|
}
|
|
@@ -2230,8 +1238,28 @@ function hideErrorToast() {
|
|
|
2230
1238
|
clearTimeout(errorToastTimeout);
|
|
2231
1239
|
errorToastTimeout = null;
|
|
2232
1240
|
}
|
|
2233
|
-
if (errorToast) {
|
|
2234
|
-
errorToast
|
|
1241
|
+
if (store.get('errorToast')) {
|
|
1242
|
+
store.setState({ errorToast: null });
|
|
1243
|
+
render();
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function showStashConfirm(operationLabel) {
|
|
1248
|
+
store.setState({
|
|
1249
|
+
stashConfirmMode: true,
|
|
1250
|
+
stashConfirmSelectedIndex: 0,
|
|
1251
|
+
pendingDirtyOperationLabel: operationLabel,
|
|
1252
|
+
});
|
|
1253
|
+
render();
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function hideStashConfirm() {
|
|
1257
|
+
if (store.get('stashConfirmMode')) {
|
|
1258
|
+
store.setState({
|
|
1259
|
+
stashConfirmMode: false,
|
|
1260
|
+
stashConfirmSelectedIndex: 0,
|
|
1261
|
+
pendingDirtyOperationLabel: null,
|
|
1262
|
+
});
|
|
2235
1263
|
render();
|
|
2236
1264
|
}
|
|
2237
1265
|
}
|
|
@@ -2245,12 +1273,12 @@ async function getCurrentBranch() {
|
|
|
2245
1273
|
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD');
|
|
2246
1274
|
// Check for detached HEAD state
|
|
2247
1275
|
if (stdout === 'HEAD') {
|
|
2248
|
-
isDetachedHead
|
|
1276
|
+
store.setState({ isDetachedHead: true });
|
|
2249
1277
|
// Get the short commit hash instead
|
|
2250
1278
|
const { stdout: commitHash } = await execAsync('git rev-parse --short HEAD');
|
|
2251
1279
|
return `HEAD@${commitHash}`;
|
|
2252
1280
|
}
|
|
2253
|
-
isDetachedHead
|
|
1281
|
+
store.setState({ isDetachedHead: false });
|
|
2254
1282
|
return stdout;
|
|
2255
1283
|
} catch (e) {
|
|
2256
1284
|
return null;
|
|
@@ -2276,46 +1304,7 @@ async function hasUncommittedChanges() {
|
|
|
2276
1304
|
}
|
|
2277
1305
|
}
|
|
2278
1306
|
|
|
2279
|
-
|
|
2280
|
-
const authErrors = [
|
|
2281
|
-
'Authentication failed',
|
|
2282
|
-
'could not read Username',
|
|
2283
|
-
'could not read Password',
|
|
2284
|
-
'Permission denied',
|
|
2285
|
-
'invalid credentials',
|
|
2286
|
-
'authorization failed',
|
|
2287
|
-
'fatal: Authentication',
|
|
2288
|
-
'HTTP 401',
|
|
2289
|
-
'HTTP 403',
|
|
2290
|
-
];
|
|
2291
|
-
const msg = (errorMessage || '').toLowerCase();
|
|
2292
|
-
return authErrors.some(err => msg.includes(err.toLowerCase()));
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
function isMergeConflict(errorMessage) {
|
|
2296
|
-
const conflictIndicators = [
|
|
2297
|
-
'CONFLICT',
|
|
2298
|
-
'Automatic merge failed',
|
|
2299
|
-
'fix conflicts',
|
|
2300
|
-
'Merge conflict',
|
|
2301
|
-
];
|
|
2302
|
-
return conflictIndicators.some(ind => (errorMessage || '').includes(ind));
|
|
2303
|
-
}
|
|
2304
|
-
|
|
2305
|
-
function isNetworkError(errorMessage) {
|
|
2306
|
-
const networkErrors = [
|
|
2307
|
-
'Could not resolve host',
|
|
2308
|
-
'unable to access',
|
|
2309
|
-
'Connection refused',
|
|
2310
|
-
'Network is unreachable',
|
|
2311
|
-
'Connection timed out',
|
|
2312
|
-
'Failed to connect',
|
|
2313
|
-
'no route to host',
|
|
2314
|
-
'Temporary failure in name resolution',
|
|
2315
|
-
];
|
|
2316
|
-
const msg = (errorMessage || '').toLowerCase();
|
|
2317
|
-
return networkErrors.some(err => msg.includes(err.toLowerCase()));
|
|
2318
|
-
}
|
|
1307
|
+
// isAuthError, isMergeConflict, isNetworkError imported from src/utils/errors.js
|
|
2319
1308
|
|
|
2320
1309
|
async function getAllBranches() {
|
|
2321
1310
|
try {
|
|
@@ -2400,16 +1389,12 @@ async function switchToBranch(branchName, recordHistory = true) {
|
|
|
2400
1389
|
const isDirty = await hasUncommittedChanges();
|
|
2401
1390
|
if (isDirty) {
|
|
2402
1391
|
addLog(`Cannot switch: uncommitted changes in working directory`, 'error');
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
'Cannot Switch Branch',
|
|
2406
|
-
'You have uncommitted changes in your working directory that would be lost.',
|
|
2407
|
-
'Run: git stash or git commit'
|
|
2408
|
-
);
|
|
1392
|
+
pendingDirtyOperation = { type: 'switch', branch: branchName };
|
|
1393
|
+
showStashConfirm(`switch to ${branchName}`);
|
|
2409
1394
|
return { success: false, reason: 'dirty' };
|
|
2410
1395
|
}
|
|
2411
1396
|
|
|
2412
|
-
const previousBranch = currentBranch;
|
|
1397
|
+
const previousBranch = store.get('currentBranch');
|
|
2413
1398
|
|
|
2414
1399
|
addLog(`Switching to ${safeBranchName}...`, 'update');
|
|
2415
1400
|
render();
|
|
@@ -2423,22 +1408,22 @@ async function switchToBranch(branchName, recordHistory = true) {
|
|
|
2423
1408
|
await execAsync(`git checkout -b "${safeBranchName}" "${REMOTE_NAME}/${safeBranchName}"`);
|
|
2424
1409
|
}
|
|
2425
1410
|
|
|
2426
|
-
currentBranch
|
|
2427
|
-
isDetachedHead = false; // Successfully switched to branch
|
|
1411
|
+
store.setState({ currentBranch: safeBranchName, isDetachedHead: false });
|
|
2428
1412
|
|
|
2429
1413
|
// Clear NEW flag when branch becomes current
|
|
2430
|
-
const branchInfo = branches.find(b => b.name === safeBranchName);
|
|
1414
|
+
const branchInfo = store.get('branches').find(b => b.name === safeBranchName);
|
|
2431
1415
|
if (branchInfo && branchInfo.isNew) {
|
|
2432
1416
|
branchInfo.isNew = false;
|
|
2433
1417
|
}
|
|
2434
1418
|
|
|
2435
1419
|
// Record in history (for undo)
|
|
2436
1420
|
if (recordHistory && previousBranch && previousBranch !== safeBranchName) {
|
|
2437
|
-
switchHistory
|
|
2438
|
-
|
|
1421
|
+
const switchHistory = [{ from: previousBranch, to: safeBranchName, timestamp: Date.now() }, ...store.get('switchHistory')].slice(0, MAX_HISTORY);
|
|
1422
|
+
store.setState({ switchHistory });
|
|
2439
1423
|
}
|
|
2440
1424
|
|
|
2441
1425
|
addLog(`Switched to ${safeBranchName}`, 'success');
|
|
1426
|
+
pendingDirtyOperation = null;
|
|
2442
1427
|
|
|
2443
1428
|
// Restart server if configured (command mode)
|
|
2444
1429
|
if (SERVER_MODE === 'command' && RESTART_ON_SWITCH && serverProcess) {
|
|
@@ -2458,12 +1443,8 @@ async function switchToBranch(branchName, recordHistory = true) {
|
|
|
2458
1443
|
);
|
|
2459
1444
|
} else if (errMsg.includes('local changes') || errMsg.includes('overwritten')) {
|
|
2460
1445
|
addLog(`Cannot switch: local changes would be overwritten`, 'error');
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
'Cannot Switch Branch',
|
|
2464
|
-
'Your local changes would be overwritten by checkout.',
|
|
2465
|
-
'Run: git stash or git commit'
|
|
2466
|
-
);
|
|
1446
|
+
pendingDirtyOperation = { type: 'switch', branch: branchName };
|
|
1447
|
+
showStashConfirm(`switch to ${branchName}`);
|
|
2467
1448
|
} else {
|
|
2468
1449
|
addLog(`Failed to switch: ${errMsg}`, 'error');
|
|
2469
1450
|
showErrorToast(
|
|
@@ -2477,17 +1458,18 @@ async function switchToBranch(branchName, recordHistory = true) {
|
|
|
2477
1458
|
}
|
|
2478
1459
|
|
|
2479
1460
|
async function undoLastSwitch() {
|
|
2480
|
-
|
|
1461
|
+
const currentHistory = store.get('switchHistory');
|
|
1462
|
+
if (currentHistory.length === 0) {
|
|
2481
1463
|
addLog('No switch history to undo', 'warning');
|
|
2482
1464
|
return { success: false };
|
|
2483
1465
|
}
|
|
2484
1466
|
|
|
2485
|
-
const lastSwitch =
|
|
1467
|
+
const lastSwitch = currentHistory[0];
|
|
2486
1468
|
addLog(`Undoing: going back to ${lastSwitch.from}`, 'update');
|
|
2487
1469
|
|
|
2488
1470
|
const result = await switchToBranch(lastSwitch.from, false);
|
|
2489
1471
|
if (result.success) {
|
|
2490
|
-
switchHistory.
|
|
1472
|
+
store.setState({ switchHistory: store.get('switchHistory').slice(1) });
|
|
2491
1473
|
addLog(`Undone: back on ${lastSwitch.from}`, 'success');
|
|
2492
1474
|
}
|
|
2493
1475
|
return result;
|
|
@@ -2514,14 +1496,18 @@ async function pullCurrentBranch() {
|
|
|
2514
1496
|
|
|
2515
1497
|
await execAsync(`git pull "${REMOTE_NAME}" "${branch}"`);
|
|
2516
1498
|
addLog('Pulled successfully', 'success');
|
|
1499
|
+
pendingDirtyOperation = null;
|
|
2517
1500
|
notifyClients();
|
|
2518
1501
|
return { success: true };
|
|
2519
1502
|
} catch (e) {
|
|
2520
1503
|
const errMsg = e.stderr || e.message || String(e);
|
|
2521
1504
|
addLog(`Pull failed: ${errMsg}`, 'error');
|
|
2522
1505
|
|
|
2523
|
-
if (
|
|
2524
|
-
|
|
1506
|
+
if (errMsg.includes('local changes') || errMsg.includes('overwritten') || errMsg.includes('uncommitted changes')) {
|
|
1507
|
+
pendingDirtyOperation = { type: 'pull' };
|
|
1508
|
+
showStashConfirm('pull');
|
|
1509
|
+
} else if (isMergeConflict(errMsg)) {
|
|
1510
|
+
store.setState({ hasMergeConflict: true });
|
|
2525
1511
|
showErrorToast(
|
|
2526
1512
|
'Merge Conflict!',
|
|
2527
1513
|
'Git pull resulted in merge conflicts that need manual resolution.',
|
|
@@ -2550,17 +1536,78 @@ async function pullCurrentBranch() {
|
|
|
2550
1536
|
}
|
|
2551
1537
|
}
|
|
2552
1538
|
|
|
1539
|
+
async function stashAndRetry() {
|
|
1540
|
+
const operation = pendingDirtyOperation;
|
|
1541
|
+
if (!operation) {
|
|
1542
|
+
addLog('No pending operation to retry', 'warning');
|
|
1543
|
+
render();
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
pendingDirtyOperation = null;
|
|
1548
|
+
hideErrorToast();
|
|
1549
|
+
hideStashConfirm();
|
|
1550
|
+
|
|
1551
|
+
addLog('Stashing uncommitted changes...', 'update');
|
|
1552
|
+
render();
|
|
1553
|
+
|
|
1554
|
+
const stashResult = await gitStash({ message: 'git-watchtower: auto-stash before ' + (operation.type === 'switch' ? `switching to ${operation.branch}` : 'pull') });
|
|
1555
|
+
if (!stashResult.success) {
|
|
1556
|
+
addLog(`Stash failed: ${stashResult.error ? stashResult.error.message : 'unknown error'}`, 'error');
|
|
1557
|
+
showErrorToast('Stash Failed', stashResult.error ? stashResult.error.message : 'Could not stash changes.');
|
|
1558
|
+
render();
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
addLog('Changes stashed successfully', 'success');
|
|
1563
|
+
|
|
1564
|
+
if (operation.type === 'switch') {
|
|
1565
|
+
const switchResult = await switchToBranch(operation.branch);
|
|
1566
|
+
if (!switchResult.success) {
|
|
1567
|
+
addLog('Branch switch failed after stash — restoring stashed changes...', 'warning');
|
|
1568
|
+
const popResult = await gitStashPop();
|
|
1569
|
+
if (popResult.success) {
|
|
1570
|
+
addLog('Stashed changes restored', 'info');
|
|
1571
|
+
showFlash('Stashed changes restored (switch failed)');
|
|
1572
|
+
} else {
|
|
1573
|
+
addLog('Warning: could not restore stashed changes. Run: git stash pop', 'error');
|
|
1574
|
+
showErrorToast('Stash Pop Failed', 'Could not restore stashed changes.', 'Run: git stash pop');
|
|
1575
|
+
}
|
|
1576
|
+
} else {
|
|
1577
|
+
showFlash(`Stashed & switched to ${operation.branch}`);
|
|
1578
|
+
}
|
|
1579
|
+
await pollGitChanges();
|
|
1580
|
+
} else if (operation.type === 'pull') {
|
|
1581
|
+
const pullResult = await pullCurrentBranch();
|
|
1582
|
+
if (!pullResult.success) {
|
|
1583
|
+
addLog('Pull failed after stash — restoring stashed changes...', 'warning');
|
|
1584
|
+
const popResult = await gitStashPop();
|
|
1585
|
+
if (popResult.success) {
|
|
1586
|
+
addLog('Stashed changes restored', 'info');
|
|
1587
|
+
showFlash('Stashed changes restored (pull failed)');
|
|
1588
|
+
} else {
|
|
1589
|
+
addLog('Warning: could not restore stashed changes. Run: git stash pop', 'error');
|
|
1590
|
+
showErrorToast('Stash Pop Failed', 'Could not restore stashed changes.', 'Run: git stash pop');
|
|
1591
|
+
}
|
|
1592
|
+
} else {
|
|
1593
|
+
showFlash('Stashed & pulled successfully');
|
|
1594
|
+
}
|
|
1595
|
+
await pollGitChanges();
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
render();
|
|
1599
|
+
}
|
|
1600
|
+
|
|
2553
1601
|
// ============================================================================
|
|
2554
1602
|
// Polling
|
|
2555
1603
|
// ============================================================================
|
|
2556
1604
|
|
|
2557
1605
|
async function pollGitChanges() {
|
|
2558
|
-
if (isPolling) return;
|
|
2559
|
-
isPolling
|
|
2560
|
-
pollingStatus = 'fetching';
|
|
1606
|
+
if (store.get('isPolling')) return;
|
|
1607
|
+
store.setState({ isPolling: true, pollingStatus: 'fetching' });
|
|
2561
1608
|
|
|
2562
1609
|
// Casino mode: start slot reels spinning (no sound - too annoying)
|
|
2563
|
-
if (casinoModeEnabled) {
|
|
1610
|
+
if (store.get('casinoModeEnabled')) {
|
|
2564
1611
|
casino.startSlotReels(render);
|
|
2565
1612
|
}
|
|
2566
1613
|
|
|
@@ -2570,25 +1617,28 @@ async function pollGitChanges() {
|
|
|
2570
1617
|
|
|
2571
1618
|
try {
|
|
2572
1619
|
const newCurrentBranch = await getCurrentBranch();
|
|
1620
|
+
const prevCurrentBranch = store.get('currentBranch');
|
|
2573
1621
|
|
|
2574
|
-
if (
|
|
2575
|
-
addLog(`Branch switched externally: ${
|
|
1622
|
+
if (prevCurrentBranch && newCurrentBranch !== prevCurrentBranch) {
|
|
1623
|
+
addLog(`Branch switched externally: ${prevCurrentBranch} → ${newCurrentBranch}`, 'warning');
|
|
2576
1624
|
notifyClients();
|
|
2577
1625
|
}
|
|
2578
|
-
currentBranch
|
|
1626
|
+
store.setState({ currentBranch: newCurrentBranch });
|
|
2579
1627
|
|
|
2580
1628
|
const allBranches = await getAllBranches();
|
|
2581
1629
|
|
|
2582
1630
|
// Track fetch duration
|
|
2583
|
-
lastFetchDuration = Date.now() - fetchStartTime;
|
|
1631
|
+
const lastFetchDuration = Date.now() - fetchStartTime;
|
|
1632
|
+
store.setState({ lastFetchDuration });
|
|
2584
1633
|
|
|
2585
1634
|
// Check for slow fetches
|
|
2586
1635
|
if (lastFetchDuration > 30000 && !verySlowFetchWarningShown) {
|
|
2587
1636
|
addLog(`⚠ Fetches taking ${Math.round(lastFetchDuration / 1000)}s - network may be slow`, 'warning');
|
|
2588
1637
|
verySlowFetchWarningShown = true;
|
|
2589
1638
|
// Slow down polling
|
|
2590
|
-
|
|
2591
|
-
|
|
1639
|
+
const newInterval = Math.min(store.get('adaptivePollInterval') * 2, 60000);
|
|
1640
|
+
store.setState({ adaptivePollInterval: newInterval });
|
|
1641
|
+
addLog(`Polling interval increased to ${newInterval / 1000}s`, 'info');
|
|
2592
1642
|
restartPolling();
|
|
2593
1643
|
} else if (lastFetchDuration > 15000 && !slowFetchWarningShown) {
|
|
2594
1644
|
addLog(`Fetches taking ${Math.round(lastFetchDuration / 1000)}s`, 'warning');
|
|
@@ -2597,21 +1647,22 @@ async function pollGitChanges() {
|
|
|
2597
1647
|
// Reset warnings if fetches are fast again
|
|
2598
1648
|
slowFetchWarningShown = false;
|
|
2599
1649
|
verySlowFetchWarningShown = false;
|
|
2600
|
-
if (adaptivePollInterval > GIT_POLL_INTERVAL) {
|
|
2601
|
-
adaptivePollInterval
|
|
2602
|
-
addLog(`Polling interval restored to ${
|
|
1650
|
+
if (store.get('adaptivePollInterval') > GIT_POLL_INTERVAL) {
|
|
1651
|
+
store.setState({ adaptivePollInterval: GIT_POLL_INTERVAL });
|
|
1652
|
+
addLog(`Polling interval restored to ${GIT_POLL_INTERVAL / 1000}s`, 'info');
|
|
2603
1653
|
restartPolling();
|
|
2604
1654
|
}
|
|
2605
1655
|
}
|
|
2606
1656
|
|
|
2607
1657
|
// Network success - reset failure counter
|
|
2608
|
-
|
|
2609
|
-
if (isOffline) {
|
|
2610
|
-
isOffline = false;
|
|
1658
|
+
if (store.get('isOffline')) {
|
|
2611
1659
|
addLog('Connection restored', 'success');
|
|
2612
1660
|
}
|
|
1661
|
+
store.setState({ consecutiveNetworkFailures: 0, isOffline: false });
|
|
1662
|
+
|
|
2613
1663
|
const fetchedBranchNames = new Set(allBranches.map(b => b.name));
|
|
2614
1664
|
const now = Date.now();
|
|
1665
|
+
const currentBranches = store.get('branches');
|
|
2615
1666
|
|
|
2616
1667
|
// Detect NEW branches (not seen before)
|
|
2617
1668
|
const newBranchList = [];
|
|
@@ -2623,7 +1674,7 @@ async function pollGitChanges() {
|
|
|
2623
1674
|
newBranchList.push(branch);
|
|
2624
1675
|
} else {
|
|
2625
1676
|
// Preserve isNew flag from previous poll cycle for branches not yet switched to
|
|
2626
|
-
const prevBranch =
|
|
1677
|
+
const prevBranch = currentBranches.find(b => b.name === branch.name);
|
|
2627
1678
|
if (prevBranch && prevBranch.isNew) {
|
|
2628
1679
|
branch.isNew = true;
|
|
2629
1680
|
branch.newAt = prevBranch.newAt;
|
|
@@ -2636,7 +1687,7 @@ async function pollGitChanges() {
|
|
|
2636
1687
|
for (const knownName of knownBranchNames) {
|
|
2637
1688
|
if (!fetchedBranchNames.has(knownName)) {
|
|
2638
1689
|
// This branch was deleted from remote
|
|
2639
|
-
const existingInList =
|
|
1690
|
+
const existingInList = currentBranches.find(b => b.name === knownName);
|
|
2640
1691
|
if (existingInList && !existingInList.isDeleted) {
|
|
2641
1692
|
existingInList.isDeleted = true;
|
|
2642
1693
|
existingInList.deletedAt = now;
|
|
@@ -2651,14 +1702,15 @@ async function pollGitChanges() {
|
|
|
2651
1702
|
// Note: isNew flag is only cleared when branch becomes current (see below)
|
|
2652
1703
|
|
|
2653
1704
|
// Keep deleted branches in the list (don't remove them)
|
|
2654
|
-
const
|
|
1705
|
+
const pollFilteredBranches = allBranches;
|
|
2655
1706
|
|
|
2656
1707
|
// Detect updates on other branches (for flash notification)
|
|
2657
1708
|
const updatedBranches = [];
|
|
2658
|
-
|
|
1709
|
+
const currentBranchName = store.get('currentBranch');
|
|
1710
|
+
for (const branch of pollFilteredBranches) {
|
|
2659
1711
|
if (branch.isDeleted) continue;
|
|
2660
1712
|
const prevCommit = previousBranchStates.get(branch.name);
|
|
2661
|
-
if (prevCommit && prevCommit !== branch.commit && branch.name !==
|
|
1713
|
+
if (prevCommit && prevCommit !== branch.commit && branch.name !== currentBranchName) {
|
|
2662
1714
|
updatedBranches.push(branch);
|
|
2663
1715
|
branch.justUpdated = true;
|
|
2664
1716
|
}
|
|
@@ -2666,6 +1718,7 @@ async function pollGitChanges() {
|
|
|
2666
1718
|
}
|
|
2667
1719
|
|
|
2668
1720
|
// Flash and sound for updates or new branches
|
|
1721
|
+
const casinoOn = store.get('casinoModeEnabled');
|
|
2669
1722
|
const notifyBranches = [...updatedBranches, ...newBranchList];
|
|
2670
1723
|
if (notifyBranches.length > 0) {
|
|
2671
1724
|
for (const branch of updatedBranches) {
|
|
@@ -2673,7 +1726,7 @@ async function pollGitChanges() {
|
|
|
2673
1726
|
}
|
|
2674
1727
|
|
|
2675
1728
|
// Casino mode: add funny commentary
|
|
2676
|
-
if (
|
|
1729
|
+
if (casinoOn) {
|
|
2677
1730
|
addLog(`🎰 ${getCasinoMessage('win')}`, 'success');
|
|
2678
1731
|
}
|
|
2679
1732
|
|
|
@@ -2682,7 +1735,7 @@ async function pollGitChanges() {
|
|
|
2682
1735
|
playSound();
|
|
2683
1736
|
|
|
2684
1737
|
// Casino mode: trigger win effect based on number of updated branches
|
|
2685
|
-
if (
|
|
1738
|
+
if (casinoOn) {
|
|
2686
1739
|
// Estimate line changes: more branches = bigger "win"
|
|
2687
1740
|
// Each branch update counts as ~100 lines (placeholder until we calculate actual diff)
|
|
2688
1741
|
const estimatedLines = notifyBranches.length * 100;
|
|
@@ -2694,21 +1747,23 @@ async function pollGitChanges() {
|
|
|
2694
1747
|
}
|
|
2695
1748
|
casino.recordPoll(true);
|
|
2696
1749
|
}
|
|
2697
|
-
} else if (
|
|
1750
|
+
} else if (casinoOn) {
|
|
2698
1751
|
// No updates - stop reels and show result briefly
|
|
2699
1752
|
casino.stopSlotReels(false, render);
|
|
2700
1753
|
casino.recordPoll(false);
|
|
2701
1754
|
}
|
|
2702
1755
|
|
|
2703
1756
|
// Remember which branch was selected before updating the list
|
|
2704
|
-
const
|
|
1757
|
+
const { selectedBranchName: prevSelName, selectedIndex: prevSelIdx } = store.getState();
|
|
1758
|
+
const previouslySelectedName = prevSelName || (currentBranches[prevSelIdx] ? currentBranches[prevSelIdx].name : null);
|
|
2705
1759
|
|
|
2706
1760
|
// Sort: new branches first, then by date, merged branches near bottom, deleted at bottom
|
|
2707
|
-
|
|
1761
|
+
const prStatusMap = store.get('branchPrStatusMap');
|
|
1762
|
+
pollFilteredBranches.sort((a, b) => {
|
|
2708
1763
|
const aIsBase = isBaseBranch(a.name);
|
|
2709
1764
|
const bIsBase = isBaseBranch(b.name);
|
|
2710
|
-
const aMerged = !aIsBase &&
|
|
2711
|
-
const bMerged = !bIsBase &&
|
|
1765
|
+
const aMerged = !aIsBase && prStatusMap.has(a.name) && prStatusMap.get(a.name).state === 'MERGED';
|
|
1766
|
+
const bMerged = !bIsBase && prStatusMap.has(b.name) && prStatusMap.get(b.name).state === 'MERGED';
|
|
2712
1767
|
if (a.isDeleted && !b.isDeleted) return 1;
|
|
2713
1768
|
if (!a.isDeleted && b.isDeleted) return -1;
|
|
2714
1769
|
if (aMerged && !bMerged && !b.isDeleted) return 1;
|
|
@@ -2719,23 +1774,24 @@ async function pollGitChanges() {
|
|
|
2719
1774
|
});
|
|
2720
1775
|
|
|
2721
1776
|
// Store all branches (no limit) - visibleBranchCount controls display
|
|
2722
|
-
branches = filteredBranches;
|
|
2723
|
-
|
|
2724
1777
|
// Restore selection to the same branch (by name) after reordering
|
|
1778
|
+
let newSelectedIndex = prevSelIdx;
|
|
1779
|
+
let newSelectedName = prevSelName;
|
|
2725
1780
|
if (previouslySelectedName) {
|
|
2726
|
-
const
|
|
2727
|
-
if (
|
|
2728
|
-
|
|
2729
|
-
|
|
1781
|
+
const foundIdx = pollFilteredBranches.findIndex(b => b.name === previouslySelectedName);
|
|
1782
|
+
if (foundIdx >= 0) {
|
|
1783
|
+
newSelectedIndex = foundIdx;
|
|
1784
|
+
newSelectedName = previouslySelectedName;
|
|
2730
1785
|
} else {
|
|
2731
1786
|
// Branch fell off the list, keep index at bottom or clamp
|
|
2732
|
-
|
|
2733
|
-
|
|
1787
|
+
newSelectedIndex = Math.min(prevSelIdx, Math.max(0, pollFilteredBranches.length - 1));
|
|
1788
|
+
newSelectedName = pollFilteredBranches[newSelectedIndex] ? pollFilteredBranches[newSelectedIndex].name : null;
|
|
2734
1789
|
}
|
|
2735
|
-
} else if (
|
|
2736
|
-
|
|
2737
|
-
|
|
1790
|
+
} else if (prevSelIdx >= pollFilteredBranches.length) {
|
|
1791
|
+
newSelectedIndex = Math.max(0, pollFilteredBranches.length - 1);
|
|
1792
|
+
newSelectedName = pollFilteredBranches[newSelectedIndex] ? pollFilteredBranches[newSelectedIndex].name : null;
|
|
2738
1793
|
}
|
|
1794
|
+
store.setState({ branches: pollFilteredBranches, selectedIndex: newSelectedIndex, selectedBranchName: newSelectedName });
|
|
2739
1795
|
|
|
2740
1796
|
// Background PR status fetch (throttled to every PR_STATUS_POLL_INTERVAL)
|
|
2741
1797
|
const now2 = Date.now();
|
|
@@ -2743,7 +1799,7 @@ async function pollGitChanges() {
|
|
|
2743
1799
|
prStatusFetchInFlight = true;
|
|
2744
1800
|
fetchAllPrStatuses().then(map => {
|
|
2745
1801
|
if (map) {
|
|
2746
|
-
branchPrStatusMap
|
|
1802
|
+
store.setState({ branchPrStatusMap: map });
|
|
2747
1803
|
render(); // re-render to show updated PR indicators
|
|
2748
1804
|
}
|
|
2749
1805
|
lastPrStatusFetch = Date.now();
|
|
@@ -2754,28 +1810,29 @@ async function pollGitChanges() {
|
|
|
2754
1810
|
}
|
|
2755
1811
|
|
|
2756
1812
|
// AUTO-PULL: If current branch has remote updates, pull automatically (if enabled)
|
|
2757
|
-
const
|
|
2758
|
-
|
|
2759
|
-
|
|
1813
|
+
const autoPullBranchName = store.get('currentBranch');
|
|
1814
|
+
const currentInfo = store.get('branches').find(b => b.name === autoPullBranchName);
|
|
1815
|
+
if (AUTO_PULL && currentInfo && currentInfo.hasUpdates && !store.get('hasMergeConflict')) {
|
|
1816
|
+
addLog(`Auto-pulling changes for ${autoPullBranchName}...`, 'update');
|
|
2760
1817
|
render();
|
|
2761
1818
|
|
|
2762
1819
|
// Save the old commit for diff calculation (casino mode)
|
|
2763
1820
|
const oldCommit = currentInfo.commit;
|
|
2764
1821
|
|
|
2765
1822
|
try {
|
|
2766
|
-
await execAsync(`git pull "${REMOTE_NAME}" "${
|
|
2767
|
-
addLog(`Pulled successfully from ${
|
|
1823
|
+
await execAsync(`git pull "${REMOTE_NAME}" "${autoPullBranchName}"`);
|
|
1824
|
+
addLog(`Pulled successfully from ${autoPullBranchName}`, 'success');
|
|
2768
1825
|
currentInfo.hasUpdates = false;
|
|
2769
|
-
hasMergeConflict
|
|
1826
|
+
store.setState({ hasMergeConflict: false });
|
|
2770
1827
|
// Update the stored commit to the new one
|
|
2771
1828
|
const newCommit = await execAsync('git rev-parse --short HEAD');
|
|
2772
1829
|
currentInfo.commit = newCommit.stdout.trim();
|
|
2773
|
-
previousBranchStates.set(
|
|
1830
|
+
previousBranchStates.set(autoPullBranchName, newCommit.stdout.trim());
|
|
2774
1831
|
// Reload browsers
|
|
2775
1832
|
notifyClients();
|
|
2776
1833
|
|
|
2777
1834
|
// Casino mode: calculate actual diff and trigger win effect
|
|
2778
|
-
if (casinoModeEnabled && oldCommit) {
|
|
1835
|
+
if (store.get('casinoModeEnabled') && oldCommit) {
|
|
2779
1836
|
const diffStats = await getDiffStats(oldCommit, 'HEAD');
|
|
2780
1837
|
const totalLines = diffStats.added + diffStats.deleted;
|
|
2781
1838
|
if (totalLines > 0) {
|
|
@@ -2790,7 +1847,7 @@ async function pollGitChanges() {
|
|
|
2790
1847
|
} catch (e) {
|
|
2791
1848
|
const errMsg = e.stderr || e.stdout || e.message || String(e);
|
|
2792
1849
|
if (isMergeConflict(errMsg)) {
|
|
2793
|
-
hasMergeConflict
|
|
1850
|
+
store.setState({ hasMergeConflict: true });
|
|
2794
1851
|
addLog(`MERGE CONFLICT detected!`, 'error');
|
|
2795
1852
|
addLog(`Resolve conflicts manually, then commit`, 'warning');
|
|
2796
1853
|
showErrorToast(
|
|
@@ -2799,7 +1856,7 @@ async function pollGitChanges() {
|
|
|
2799
1856
|
'Run: git status to see conflicts'
|
|
2800
1857
|
);
|
|
2801
1858
|
// Casino mode: trigger loss effect
|
|
2802
|
-
if (casinoModeEnabled) {
|
|
1859
|
+
if (store.get('casinoModeEnabled')) {
|
|
2803
1860
|
casino.triggerLoss('MERGE CONFLICT!', render);
|
|
2804
1861
|
casinoSounds.playLoss();
|
|
2805
1862
|
addLog(`💀 ${getCasinoMessage('loss')}`, 'error');
|
|
@@ -2823,16 +1880,16 @@ async function pollGitChanges() {
|
|
|
2823
1880
|
}
|
|
2824
1881
|
}
|
|
2825
1882
|
|
|
2826
|
-
pollingStatus
|
|
1883
|
+
store.setState({ pollingStatus: 'idle' });
|
|
2827
1884
|
// Casino mode: stop slot reels if still spinning (already handled above, just cleanup)
|
|
2828
|
-
if (casinoModeEnabled && casino.isSlotSpinning()) {
|
|
1885
|
+
if (store.get('casinoModeEnabled') && casino.isSlotSpinning()) {
|
|
2829
1886
|
casino.stopSlotReels(false, render);
|
|
2830
1887
|
}
|
|
2831
1888
|
} catch (err) {
|
|
2832
1889
|
const errMsg = err.stderr || err.message || String(err);
|
|
2833
1890
|
|
|
2834
1891
|
// Casino mode: stop slot reels and show loss on error
|
|
2835
|
-
if (casinoModeEnabled) {
|
|
1892
|
+
if (store.get('casinoModeEnabled')) {
|
|
2836
1893
|
casino.stopSlotReels(false, render);
|
|
2837
1894
|
casino.triggerLoss('BUST!', render);
|
|
2838
1895
|
casinoSounds.playLoss();
|
|
@@ -2840,17 +1897,18 @@ async function pollGitChanges() {
|
|
|
2840
1897
|
|
|
2841
1898
|
// Handle different error types
|
|
2842
1899
|
if (isNetworkError(errMsg)) {
|
|
2843
|
-
consecutiveNetworkFailures
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
1900
|
+
const failures = store.get('consecutiveNetworkFailures') + 1;
|
|
1901
|
+
store.setState({ consecutiveNetworkFailures: failures });
|
|
1902
|
+
if (failures >= 3 && !store.get('isOffline')) {
|
|
1903
|
+
store.setState({ isOffline: true });
|
|
1904
|
+
addLog(`Network unavailable (${failures} failures)`, 'error');
|
|
2847
1905
|
showErrorToast(
|
|
2848
1906
|
'Network Unavailable',
|
|
2849
1907
|
'Cannot connect to the remote repository. Git operations will fail until connection is restored.',
|
|
2850
1908
|
'Check your internet connection'
|
|
2851
1909
|
);
|
|
2852
1910
|
}
|
|
2853
|
-
pollingStatus
|
|
1911
|
+
store.setState({ pollingStatus: 'error' });
|
|
2854
1912
|
} else if (isAuthError(errMsg)) {
|
|
2855
1913
|
addLog(`Authentication error - check credentials`, 'error');
|
|
2856
1914
|
addLog(`Try: git config credential.helper store`, 'warning');
|
|
@@ -2859,13 +1917,13 @@ async function pollGitChanges() {
|
|
|
2859
1917
|
'Failed to authenticate with the remote repository.',
|
|
2860
1918
|
'Run: git config credential.helper store'
|
|
2861
1919
|
);
|
|
2862
|
-
pollingStatus
|
|
1920
|
+
store.setState({ pollingStatus: 'error' });
|
|
2863
1921
|
} else {
|
|
2864
|
-
pollingStatus
|
|
1922
|
+
store.setState({ pollingStatus: 'error' });
|
|
2865
1923
|
addLog(`Polling error: ${errMsg}`, 'error');
|
|
2866
1924
|
}
|
|
2867
1925
|
} finally {
|
|
2868
|
-
isPolling
|
|
1926
|
+
store.setState({ isPolling: false });
|
|
2869
1927
|
render();
|
|
2870
1928
|
}
|
|
2871
1929
|
}
|
|
@@ -2874,7 +1932,7 @@ function restartPolling() {
|
|
|
2874
1932
|
if (pollIntervalId) {
|
|
2875
1933
|
clearInterval(pollIntervalId);
|
|
2876
1934
|
}
|
|
2877
|
-
pollIntervalId = setInterval(pollGitChanges, adaptivePollInterval);
|
|
1935
|
+
pollIntervalId = setInterval(pollGitChanges, store.get('adaptivePollInterval'));
|
|
2878
1936
|
}
|
|
2879
1937
|
|
|
2880
1938
|
// ============================================================================
|
|
@@ -3011,17 +2069,18 @@ function setupFileWatcher() {
|
|
|
3011
2069
|
// Keyboard Input
|
|
3012
2070
|
// ============================================================================
|
|
3013
2071
|
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
2072
|
+
// applySearchFilter — replaced by filterBranches import (src/ui/renderer.js)
|
|
2073
|
+
|
|
2074
|
+
// Apply state updates from action handlers to store
|
|
2075
|
+
function applyUpdates(updates) {
|
|
2076
|
+
if (!updates) return false;
|
|
2077
|
+
store.setState(updates);
|
|
2078
|
+
return true;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
// Build current state snapshot for action handlers
|
|
2082
|
+
function getActionState() {
|
|
2083
|
+
return store.getState();
|
|
3025
2084
|
}
|
|
3026
2085
|
|
|
3027
2086
|
function setupKeyboardInput() {
|
|
@@ -3032,25 +2091,11 @@ function setupKeyboardInput() {
|
|
|
3032
2091
|
process.stdin.setEncoding('utf8');
|
|
3033
2092
|
|
|
3034
2093
|
process.stdin.on('data', async (key) => {
|
|
3035
|
-
// Handle search mode input
|
|
3036
|
-
if (searchMode) {
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
// Escape clears search
|
|
3041
|
-
searchQuery = '';
|
|
3042
|
-
filteredBranches = null;
|
|
3043
|
-
}
|
|
3044
|
-
render();
|
|
3045
|
-
return;
|
|
3046
|
-
} else if (key === '\u007f' || key === '\b') { // Backspace
|
|
3047
|
-
searchQuery = searchQuery.slice(0, -1);
|
|
3048
|
-
applySearchFilter();
|
|
3049
|
-
render();
|
|
3050
|
-
return;
|
|
3051
|
-
} else if (key.length === 1 && key >= ' ' && key <= '~') { // Printable chars
|
|
3052
|
-
searchQuery += key;
|
|
3053
|
-
applySearchFilter();
|
|
2094
|
+
// Handle search mode input via actions module
|
|
2095
|
+
if (store.get('searchMode')) {
|
|
2096
|
+
const searchResult = actions.handleSearchInput(getActionState(), key);
|
|
2097
|
+
if (searchResult) {
|
|
2098
|
+
applyUpdates(searchResult);
|
|
3054
2099
|
render();
|
|
3055
2100
|
return;
|
|
3056
2101
|
}
|
|
@@ -3061,24 +2106,23 @@ function setupKeyboardInput() {
|
|
|
3061
2106
|
}
|
|
3062
2107
|
|
|
3063
2108
|
// Handle modal modes
|
|
3064
|
-
if (previewMode) {
|
|
2109
|
+
if (store.get('previewMode')) {
|
|
3065
2110
|
if (key === 'v' || key === '\u001b' || key === '\r' || key === '\n') {
|
|
3066
|
-
|
|
3067
|
-
previewData = null;
|
|
2111
|
+
applyUpdates(actions.togglePreview(getActionState()));
|
|
3068
2112
|
render();
|
|
3069
2113
|
return;
|
|
3070
2114
|
}
|
|
3071
2115
|
return; // Ignore other keys in preview mode
|
|
3072
2116
|
}
|
|
3073
2117
|
|
|
3074
|
-
if (historyMode) {
|
|
2118
|
+
if (store.get('historyMode')) {
|
|
3075
2119
|
if (key === 'h' || key === '\u001b') {
|
|
3076
|
-
|
|
2120
|
+
applyUpdates(actions.toggleHistory(getActionState()));
|
|
3077
2121
|
render();
|
|
3078
2122
|
return;
|
|
3079
2123
|
}
|
|
3080
2124
|
if (key === 'u') {
|
|
3081
|
-
historyMode
|
|
2125
|
+
store.setState({ historyMode: false });
|
|
3082
2126
|
await undoLastSwitch();
|
|
3083
2127
|
await pollGitChanges();
|
|
3084
2128
|
return;
|
|
@@ -3086,44 +2130,38 @@ function setupKeyboardInput() {
|
|
|
3086
2130
|
return; // Ignore other keys in history mode
|
|
3087
2131
|
}
|
|
3088
2132
|
|
|
3089
|
-
if (infoMode) {
|
|
2133
|
+
if (store.get('infoMode')) {
|
|
3090
2134
|
if (key === 'i' || key === '\u001b') {
|
|
3091
|
-
|
|
2135
|
+
applyUpdates(actions.toggleInfo(getActionState()));
|
|
3092
2136
|
render();
|
|
3093
2137
|
return;
|
|
3094
2138
|
}
|
|
3095
2139
|
return; // Ignore other keys in info mode
|
|
3096
2140
|
}
|
|
3097
2141
|
|
|
3098
|
-
if (logViewMode) {
|
|
2142
|
+
if (store.get('logViewMode')) {
|
|
3099
2143
|
if (key === 'l' || key === '\u001b') {
|
|
3100
|
-
|
|
3101
|
-
logScrollOffset = 0;
|
|
2144
|
+
applyUpdates(actions.toggleLogView(getActionState()));
|
|
3102
2145
|
render();
|
|
3103
2146
|
return;
|
|
3104
2147
|
}
|
|
3105
2148
|
if (key === '1') { // Switch to activity tab
|
|
3106
|
-
|
|
3107
|
-
logScrollOffset = 0;
|
|
2149
|
+
applyUpdates(actions.switchLogTab(getActionState(), 'activity'));
|
|
3108
2150
|
render();
|
|
3109
2151
|
return;
|
|
3110
2152
|
}
|
|
3111
2153
|
if (key === '2') { // Switch to server tab
|
|
3112
|
-
|
|
3113
|
-
logScrollOffset = 0;
|
|
2154
|
+
applyUpdates(actions.switchLogTab(getActionState(), 'server'));
|
|
3114
2155
|
render();
|
|
3115
2156
|
return;
|
|
3116
2157
|
}
|
|
3117
|
-
// Get current log data for scroll bounds
|
|
3118
|
-
const currentLogData = logViewTab === 'server' ? serverLogBuffer : activityLog;
|
|
3119
|
-
const maxScroll = Math.max(0, currentLogData.length - 10);
|
|
3120
2158
|
if (key === '\u001b[A' || key === 'k') { // Up - scroll
|
|
3121
|
-
|
|
2159
|
+
applyUpdates(actions.scrollLog(getActionState(), 'up'));
|
|
3122
2160
|
render();
|
|
3123
2161
|
return;
|
|
3124
2162
|
}
|
|
3125
2163
|
if (key === '\u001b[B' || key === 'j') { // Down - scroll
|
|
3126
|
-
|
|
2164
|
+
applyUpdates(actions.scrollLog(getActionState(), 'down'));
|
|
3127
2165
|
render();
|
|
3128
2166
|
return;
|
|
3129
2167
|
}
|
|
@@ -3135,16 +2173,15 @@ function setupKeyboardInput() {
|
|
|
3135
2173
|
return; // Ignore other keys in log view mode
|
|
3136
2174
|
}
|
|
3137
2175
|
|
|
3138
|
-
if (actionMode) {
|
|
2176
|
+
if (store.get('actionMode')) {
|
|
3139
2177
|
if (key === '\u001b') { // Escape to close
|
|
3140
|
-
|
|
3141
|
-
actionData = null;
|
|
3142
|
-
actionLoading = false;
|
|
2178
|
+
applyUpdates(actions.closeActionModal(getActionState()));
|
|
3143
2179
|
render();
|
|
3144
2180
|
return;
|
|
3145
2181
|
}
|
|
3146
|
-
|
|
3147
|
-
|
|
2182
|
+
const currentActionData = store.get('actionData');
|
|
2183
|
+
if (!currentActionData) return;
|
|
2184
|
+
const { branch: aBranch, sessionUrl, prInfo, hasGh, hasGlab, ghAuthed, glabAuthed, webUrl, platform, prLoaded } = currentActionData;
|
|
3148
2185
|
const cliReady = (platform === 'gitlab') ? (hasGlab && glabAuthed) : (hasGh && ghAuthed);
|
|
3149
2186
|
const prLabel = platform === 'gitlab' ? 'MR' : 'PR';
|
|
3150
2187
|
|
|
@@ -3185,13 +2222,12 @@ function setupKeyboardInput() {
|
|
|
3185
2222
|
addLog(`${prLabel} created: ${(result.stdout || '').trim().split('\n').pop()}`, 'success');
|
|
3186
2223
|
// Invalidate cache and refresh modal data
|
|
3187
2224
|
prInfoCache.delete(aBranch.name);
|
|
3188
|
-
|
|
3189
|
-
actionLoading
|
|
2225
|
+
const refreshedData = gatherLocalActionData(aBranch);
|
|
2226
|
+
store.setState({ actionData: refreshedData, actionLoading: true });
|
|
3190
2227
|
render();
|
|
3191
|
-
loadAsyncActionData(aBranch,
|
|
3192
|
-
if (actionMode && actionData && actionData.branch.name === aBranch.name) {
|
|
3193
|
-
actionData
|
|
3194
|
-
actionLoading = false;
|
|
2228
|
+
loadAsyncActionData(aBranch, refreshedData).then((fullData) => {
|
|
2229
|
+
if (store.get('actionMode') && store.get('actionData') && store.get('actionData').branch.name === aBranch.name) {
|
|
2230
|
+
store.setState({ actionData: fullData, actionLoading: false });
|
|
3195
2231
|
render();
|
|
3196
2232
|
}
|
|
3197
2233
|
}).catch(() => {});
|
|
@@ -3243,9 +2279,7 @@ function setupKeyboardInput() {
|
|
|
3243
2279
|
await execAsync(`gh pr merge ${prInfo.number} --squash --delete-branch 2>&1`);
|
|
3244
2280
|
}
|
|
3245
2281
|
addLog(`${prLabel} #${prInfo.number} merged`, 'success');
|
|
3246
|
-
actionMode
|
|
3247
|
-
actionData = null;
|
|
3248
|
-
actionLoading = false;
|
|
2282
|
+
store.setState({ actionMode: false, actionData: null, actionLoading: false });
|
|
3249
2283
|
prInfoCache.delete(aBranch.name);
|
|
3250
2284
|
// Force-refresh bulk PR statuses so inline indicators update immediately
|
|
3251
2285
|
lastPrStatusFetch = 0;
|
|
@@ -3295,55 +2329,101 @@ function setupKeyboardInput() {
|
|
|
3295
2329
|
return; // Ignore other keys in action mode
|
|
3296
2330
|
}
|
|
3297
2331
|
|
|
2332
|
+
// Handle stash confirmation dialog
|
|
2333
|
+
if (store.get('stashConfirmMode')) {
|
|
2334
|
+
if (key === '\u001b[A' || key === 'k') { // Up
|
|
2335
|
+
const idx = store.get('stashConfirmSelectedIndex');
|
|
2336
|
+
if (idx > 0) {
|
|
2337
|
+
store.setState({ stashConfirmSelectedIndex: idx - 1 });
|
|
2338
|
+
render();
|
|
2339
|
+
}
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
if (key === '\u001b[B' || key === 'j') { // Down
|
|
2343
|
+
const idx = store.get('stashConfirmSelectedIndex');
|
|
2344
|
+
if (idx < 1) {
|
|
2345
|
+
store.setState({ stashConfirmSelectedIndex: idx + 1 });
|
|
2346
|
+
render();
|
|
2347
|
+
}
|
|
2348
|
+
return;
|
|
2349
|
+
}
|
|
2350
|
+
if (key === '\r' || key === '\n') { // Enter — execute selected option
|
|
2351
|
+
const idx = store.get('stashConfirmSelectedIndex');
|
|
2352
|
+
hideStashConfirm();
|
|
2353
|
+
if (idx === 0 && pendingDirtyOperation) {
|
|
2354
|
+
await stashAndRetry();
|
|
2355
|
+
} else {
|
|
2356
|
+
addLog('Stash cancelled — handle changes manually', 'info');
|
|
2357
|
+
pendingDirtyOperation = null;
|
|
2358
|
+
}
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
if (key === 'S') { // S shortcut — stash directly
|
|
2362
|
+
hideStashConfirm();
|
|
2363
|
+
if (pendingDirtyOperation) {
|
|
2364
|
+
await stashAndRetry();
|
|
2365
|
+
}
|
|
2366
|
+
return;
|
|
2367
|
+
}
|
|
2368
|
+
if (key === '\u001b') { // Escape — cancel
|
|
2369
|
+
hideStashConfirm();
|
|
2370
|
+
addLog('Stash cancelled — handle changes manually', 'info');
|
|
2371
|
+
pendingDirtyOperation = null;
|
|
2372
|
+
render();
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
return; // Ignore other keys in stash confirm mode
|
|
2376
|
+
}
|
|
2377
|
+
|
|
3298
2378
|
// Dismiss flash on any key
|
|
3299
|
-
if (flashMessage) {
|
|
2379
|
+
if (store.get('flashMessage')) {
|
|
3300
2380
|
hideFlash();
|
|
3301
2381
|
if (key !== '\u001b[A' && key !== '\u001b[B' && key !== '\r' && key !== 'q') {
|
|
3302
2382
|
return;
|
|
3303
2383
|
}
|
|
3304
2384
|
}
|
|
3305
2385
|
|
|
3306
|
-
// Dismiss error toast on any key
|
|
3307
|
-
if (errorToast) {
|
|
2386
|
+
// Dismiss error toast on any key (S triggers stash if pending)
|
|
2387
|
+
if (store.get('errorToast')) {
|
|
2388
|
+
if (key === 'S' && pendingDirtyOperation) {
|
|
2389
|
+
await stashAndRetry();
|
|
2390
|
+
return;
|
|
2391
|
+
}
|
|
3308
2392
|
hideErrorToast();
|
|
3309
2393
|
if (key !== '\u001b[A' && key !== '\u001b[B' && key !== '\r' && key !== 'q') {
|
|
3310
2394
|
return;
|
|
3311
2395
|
}
|
|
3312
2396
|
}
|
|
3313
2397
|
|
|
3314
|
-
const
|
|
2398
|
+
const { filteredBranches: currentFiltered, branches: currentBranchList, selectedIndex: curSelIdx } = store.getState();
|
|
2399
|
+
const displayBranches = currentFiltered !== null ? currentFiltered : currentBranchList;
|
|
2400
|
+
const actionState = getActionState();
|
|
3315
2401
|
|
|
3316
2402
|
switch (key) {
|
|
3317
2403
|
case '\u001b[A': // Up arrow
|
|
3318
|
-
case 'k':
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
selectedBranchName = displayBranches[selectedIndex] ? displayBranches[selectedIndex].name : null;
|
|
3322
|
-
render();
|
|
3323
|
-
}
|
|
2404
|
+
case 'k': {
|
|
2405
|
+
const result = actions.moveUp(actionState);
|
|
2406
|
+
if (result) { applyUpdates(result); render(); }
|
|
3324
2407
|
break;
|
|
2408
|
+
}
|
|
3325
2409
|
|
|
3326
2410
|
case '\u001b[B': // Down arrow
|
|
3327
|
-
case 'j':
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
selectedBranchName = displayBranches[selectedIndex] ? displayBranches[selectedIndex].name : null;
|
|
3331
|
-
render();
|
|
3332
|
-
}
|
|
2411
|
+
case 'j': {
|
|
2412
|
+
const result = actions.moveDown(actionState);
|
|
2413
|
+
if (result) { applyUpdates(result); render(); }
|
|
3333
2414
|
break;
|
|
2415
|
+
}
|
|
3334
2416
|
|
|
3335
2417
|
case '\r': // Enter
|
|
3336
2418
|
case '\n':
|
|
3337
|
-
if (displayBranches.length > 0 &&
|
|
3338
|
-
const branch = displayBranches[
|
|
2419
|
+
if (displayBranches.length > 0 && curSelIdx < displayBranches.length) {
|
|
2420
|
+
const branch = displayBranches[curSelIdx];
|
|
3339
2421
|
if (branch.isDeleted) {
|
|
3340
2422
|
addLog(`Cannot switch to deleted branch: ${branch.name}`, 'error');
|
|
3341
2423
|
render();
|
|
3342
|
-
} else if (branch.name !== currentBranch) {
|
|
2424
|
+
} else if (branch.name !== store.get('currentBranch')) {
|
|
3343
2425
|
// Clear search when switching
|
|
3344
|
-
searchQuery
|
|
3345
|
-
filteredBranches = null;
|
|
3346
|
-
searchMode = false;
|
|
2426
|
+
store.setState({ searchQuery: '', filteredBranches: null, searchMode: false });
|
|
3347
2427
|
await switchToBranch(branch.name);
|
|
3348
2428
|
await pollGitChanges();
|
|
3349
2429
|
}
|
|
@@ -3351,30 +2431,28 @@ function setupKeyboardInput() {
|
|
|
3351
2431
|
break;
|
|
3352
2432
|
|
|
3353
2433
|
case 'v': // Preview pane
|
|
3354
|
-
if (displayBranches.length > 0 &&
|
|
3355
|
-
const branch = displayBranches[
|
|
2434
|
+
if (displayBranches.length > 0 && curSelIdx < displayBranches.length) {
|
|
2435
|
+
const branch = displayBranches[curSelIdx];
|
|
3356
2436
|
addLog(`Loading preview for ${branch.name}...`, 'info');
|
|
3357
2437
|
render();
|
|
3358
|
-
|
|
3359
|
-
previewMode
|
|
2438
|
+
const pvData = await getPreviewData(branch.name);
|
|
2439
|
+
store.setState({ previewData: pvData, previewMode: true });
|
|
3360
2440
|
render();
|
|
3361
2441
|
}
|
|
3362
2442
|
break;
|
|
3363
2443
|
|
|
3364
2444
|
case '/': // Search mode
|
|
3365
|
-
|
|
3366
|
-
searchQuery = '';
|
|
3367
|
-
selectedIndex = 0;
|
|
2445
|
+
applyUpdates(actions.enterSearchMode(actionState));
|
|
3368
2446
|
render();
|
|
3369
2447
|
break;
|
|
3370
2448
|
|
|
3371
2449
|
case 'h': // History
|
|
3372
|
-
|
|
2450
|
+
applyUpdates(actions.toggleHistory(actionState));
|
|
3373
2451
|
render();
|
|
3374
2452
|
break;
|
|
3375
2453
|
|
|
3376
2454
|
case 'i': // Server info
|
|
3377
|
-
|
|
2455
|
+
applyUpdates(actions.toggleInfo(actionState));
|
|
3378
2456
|
render();
|
|
3379
2457
|
break;
|
|
3380
2458
|
|
|
@@ -3403,13 +2481,11 @@ function setupKeyboardInput() {
|
|
|
3403
2481
|
}
|
|
3404
2482
|
break;
|
|
3405
2483
|
|
|
3406
|
-
case 'l': // View server logs
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
logScrollOffset = 0;
|
|
3410
|
-
render();
|
|
3411
|
-
}
|
|
2484
|
+
case 'l': { // View server logs
|
|
2485
|
+
const logResult = actions.toggleLogView(actionState);
|
|
2486
|
+
if (logResult) { applyUpdates(logResult); render(); }
|
|
3412
2487
|
break;
|
|
2488
|
+
}
|
|
3413
2489
|
|
|
3414
2490
|
case 'o': // Open live server in browser
|
|
3415
2491
|
if (!NO_SERVER) {
|
|
@@ -3421,26 +2497,24 @@ function setupKeyboardInput() {
|
|
|
3421
2497
|
break;
|
|
3422
2498
|
|
|
3423
2499
|
case 'b': { // Branch action modal
|
|
3424
|
-
const branch = displayBranches.length > 0 &&
|
|
3425
|
-
? displayBranches[
|
|
2500
|
+
const branch = displayBranches.length > 0 && curSelIdx < displayBranches.length
|
|
2501
|
+
? displayBranches[curSelIdx] : null;
|
|
3426
2502
|
if (branch) {
|
|
3427
2503
|
// Phase 1: Open modal instantly with local/cached data
|
|
3428
|
-
|
|
3429
|
-
actionMode
|
|
3430
|
-
actionLoading = !actionData.prLoaded;
|
|
2504
|
+
const localData = gatherLocalActionData(branch);
|
|
2505
|
+
store.setState({ actionData: localData, actionMode: true, actionLoading: !localData.prLoaded });
|
|
3431
2506
|
render();
|
|
3432
2507
|
|
|
3433
2508
|
// Phase 2: Load async data (session URL, PR info) in background
|
|
3434
|
-
loadAsyncActionData(branch,
|
|
2509
|
+
loadAsyncActionData(branch, localData).then((fullData) => {
|
|
3435
2510
|
// Only update if modal is still open for the same branch
|
|
3436
|
-
if (actionMode && actionData && actionData.branch.name === branch.name) {
|
|
3437
|
-
actionData
|
|
3438
|
-
actionLoading = false;
|
|
2511
|
+
if (store.get('actionMode') && store.get('actionData') && store.get('actionData').branch.name === branch.name) {
|
|
2512
|
+
store.setState({ actionData: fullData, actionLoading: false });
|
|
3439
2513
|
render();
|
|
3440
2514
|
}
|
|
3441
2515
|
}).catch(() => {
|
|
3442
|
-
if (actionMode && actionData && actionData.branch.name === branch.name) {
|
|
3443
|
-
actionLoading
|
|
2516
|
+
if (store.get('actionMode') && store.get('actionData') && store.get('actionData').branch.name === branch.name) {
|
|
2517
|
+
store.setState({ actionLoading: false });
|
|
3444
2518
|
render();
|
|
3445
2519
|
}
|
|
3446
2520
|
});
|
|
@@ -3458,71 +2532,90 @@ function setupKeyboardInput() {
|
|
|
3458
2532
|
render();
|
|
3459
2533
|
break;
|
|
3460
2534
|
|
|
3461
|
-
case 's':
|
|
3462
|
-
|
|
3463
|
-
addLog(`Sound notifications ${soundEnabled ? 'enabled' : 'disabled'}`, 'info');
|
|
3464
|
-
if (soundEnabled) playSound();
|
|
2535
|
+
case 's': {
|
|
2536
|
+
applyUpdates(actions.toggleSound(actionState));
|
|
2537
|
+
addLog(`Sound notifications ${store.get('soundEnabled') ? 'enabled' : 'disabled'}`, 'info');
|
|
2538
|
+
if (store.get('soundEnabled')) playSound();
|
|
3465
2539
|
render();
|
|
3466
2540
|
break;
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
case 'S': // Stash changes — open confirm dialog or show hint
|
|
2544
|
+
if (pendingDirtyOperation) {
|
|
2545
|
+
const label = pendingDirtyOperation.type === 'switch'
|
|
2546
|
+
? `switch to ${pendingDirtyOperation.branch}`
|
|
2547
|
+
: 'pull';
|
|
2548
|
+
showStashConfirm(label);
|
|
2549
|
+
} else {
|
|
2550
|
+
showFlash('No pending operation — stash with S after a failed switch or pull');
|
|
2551
|
+
}
|
|
2552
|
+
break;
|
|
3467
2553
|
|
|
3468
|
-
case 'c': // Toggle casino mode
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
2554
|
+
case 'c': { // Toggle casino mode
|
|
2555
|
+
const newCasinoState = casino.toggle();
|
|
2556
|
+
store.setState({ casinoModeEnabled: newCasinoState });
|
|
2557
|
+
addLog(`Casino mode ${newCasinoState ? '🎰 ENABLED' : 'disabled'}`, newCasinoState ? 'success' : 'info');
|
|
2558
|
+
if (newCasinoState) {
|
|
3472
2559
|
addLog(`Have you noticed this game has that 'variable rewards' thing going on? 🤔😉`, 'info');
|
|
3473
|
-
if (soundEnabled) {
|
|
2560
|
+
if (store.get('soundEnabled')) {
|
|
3474
2561
|
casinoSounds.playJackpot();
|
|
3475
2562
|
}
|
|
3476
2563
|
}
|
|
3477
2564
|
render();
|
|
3478
2565
|
break;
|
|
2566
|
+
}
|
|
3479
2567
|
|
|
3480
2568
|
// Number keys to set visible branch count
|
|
3481
2569
|
case '1': case '2': case '3': case '4': case '5':
|
|
3482
2570
|
case '6': case '7': case '8': case '9':
|
|
3483
|
-
|
|
3484
|
-
addLog(`Showing ${visibleBranchCount} branches`, 'info');
|
|
2571
|
+
applyUpdates(actions.setVisibleBranchCount(actionState, parseInt(key, 10)));
|
|
2572
|
+
addLog(`Showing ${store.get('visibleBranchCount')} branches`, 'info');
|
|
3485
2573
|
render();
|
|
3486
2574
|
break;
|
|
3487
2575
|
|
|
3488
2576
|
case '0': // 0 = 10 branches
|
|
3489
|
-
|
|
3490
|
-
addLog(`Showing ${visibleBranchCount} branches`, 'info');
|
|
2577
|
+
applyUpdates(actions.setVisibleBranchCount(actionState, 10));
|
|
2578
|
+
addLog(`Showing ${store.get('visibleBranchCount')} branches`, 'info');
|
|
3491
2579
|
render();
|
|
3492
2580
|
break;
|
|
3493
2581
|
|
|
3494
2582
|
case '+':
|
|
3495
|
-
case '=': // = key (same key as + without shift)
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
2583
|
+
case '=': { // = key (same key as + without shift)
|
|
2584
|
+
const incResult = actions.increaseVisibleBranches(actionState, getMaxBranchesForScreen());
|
|
2585
|
+
if (incResult) {
|
|
2586
|
+
applyUpdates(incResult);
|
|
2587
|
+
addLog(`Showing ${store.get('visibleBranchCount')} branches`, 'info');
|
|
3499
2588
|
render();
|
|
3500
2589
|
}
|
|
3501
2590
|
break;
|
|
2591
|
+
}
|
|
3502
2592
|
|
|
3503
2593
|
case '-':
|
|
3504
|
-
case '_': // _ key (same key as - with shift)
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
2594
|
+
case '_': { // _ key (same key as - with shift)
|
|
2595
|
+
const decResult = actions.decreaseVisibleBranches(actionState);
|
|
2596
|
+
if (decResult) {
|
|
2597
|
+
applyUpdates(decResult);
|
|
2598
|
+
addLog(`Showing ${store.get('visibleBranchCount')} branches`, 'info');
|
|
3508
2599
|
render();
|
|
3509
2600
|
}
|
|
3510
2601
|
break;
|
|
2602
|
+
}
|
|
3511
2603
|
|
|
3512
2604
|
case 'q':
|
|
3513
2605
|
case '\u0003': // Ctrl+C
|
|
3514
2606
|
await shutdown();
|
|
3515
2607
|
break;
|
|
3516
2608
|
|
|
3517
|
-
case '\u001b': // Escape - clear search if active, otherwise quit
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
filteredBranches = null;
|
|
3521
|
-
render();
|
|
3522
|
-
} else {
|
|
2609
|
+
case '\u001b': { // Escape - clear search if active, otherwise quit
|
|
2610
|
+
const escResult = actions.handleEscape(actionState);
|
|
2611
|
+
if (escResult && escResult._quit) {
|
|
3523
2612
|
await shutdown();
|
|
2613
|
+
} else if (escResult) {
|
|
2614
|
+
applyUpdates(escResult);
|
|
2615
|
+
render();
|
|
3524
2616
|
}
|
|
3525
2617
|
break;
|
|
2618
|
+
}
|
|
3526
2619
|
}
|
|
3527
2620
|
});
|
|
3528
2621
|
}
|
|
@@ -3621,27 +2714,28 @@ async function start() {
|
|
|
3621
2714
|
}
|
|
3622
2715
|
|
|
3623
2716
|
// Get initial state
|
|
3624
|
-
|
|
2717
|
+
const initBranch = await getCurrentBranch();
|
|
2718
|
+
store.setState({ currentBranch: initBranch });
|
|
3625
2719
|
|
|
3626
2720
|
// Warn if in detached HEAD state
|
|
3627
|
-
if (isDetachedHead) {
|
|
2721
|
+
if (store.get('isDetachedHead')) {
|
|
3628
2722
|
addLog(`Warning: In detached HEAD state`, 'warning');
|
|
3629
2723
|
}
|
|
3630
|
-
|
|
2724
|
+
const initBranches = await getAllBranches();
|
|
2725
|
+
store.setState({ branches: initBranches });
|
|
3631
2726
|
|
|
3632
2727
|
// Initialize previous states and known branches
|
|
3633
|
-
for (const branch of
|
|
2728
|
+
for (const branch of initBranches) {
|
|
3634
2729
|
previousBranchStates.set(branch.name, branch.commit);
|
|
3635
2730
|
knownBranchNames.add(branch.name);
|
|
3636
2731
|
}
|
|
3637
2732
|
|
|
3638
2733
|
// Find current branch in list and select it
|
|
3639
|
-
const currentIndex =
|
|
2734
|
+
const currentIndex = initBranches.findIndex(b => b.name === initBranch);
|
|
3640
2735
|
if (currentIndex >= 0) {
|
|
3641
|
-
selectedIndex
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
selectedBranchName = branches[0].name;
|
|
2736
|
+
store.setState({ selectedIndex: currentIndex, selectedBranchName: initBranch });
|
|
2737
|
+
} else if (initBranches.length > 0) {
|
|
2738
|
+
store.setState({ selectedBranchName: initBranches[0].name });
|
|
3645
2739
|
}
|
|
3646
2740
|
|
|
3647
2741
|
// Load sparklines and action cache in background
|
|
@@ -3650,7 +2744,7 @@ async function start() {
|
|
|
3650
2744
|
// Once env is known, kick off initial PR status fetch
|
|
3651
2745
|
fetchAllPrStatuses().then(map => {
|
|
3652
2746
|
if (map) {
|
|
3653
|
-
branchPrStatusMap
|
|
2747
|
+
store.setState({ branchPrStatusMap: map });
|
|
3654
2748
|
lastPrStatusFetch = Date.now();
|
|
3655
2749
|
render();
|
|
3656
2750
|
}
|
|
@@ -3658,13 +2752,14 @@ async function start() {
|
|
|
3658
2752
|
}).catch(() => {});
|
|
3659
2753
|
|
|
3660
2754
|
// Start server based on mode
|
|
2755
|
+
const startBranchName = store.get('currentBranch');
|
|
3661
2756
|
if (SERVER_MODE === 'none') {
|
|
3662
2757
|
addLog(`Running in no-server mode (branch monitoring only)`, 'info');
|
|
3663
|
-
addLog(`Current branch: ${
|
|
2758
|
+
addLog(`Current branch: ${startBranchName}`, 'info');
|
|
3664
2759
|
render();
|
|
3665
2760
|
} else if (SERVER_MODE === 'command') {
|
|
3666
2761
|
addLog(`Command mode: ${SERVER_COMMAND}`, 'info');
|
|
3667
|
-
addLog(`Current branch: ${
|
|
2762
|
+
addLog(`Current branch: ${startBranchName}`, 'info');
|
|
3668
2763
|
render();
|
|
3669
2764
|
// Start the user's dev server
|
|
3670
2765
|
startServerProcess();
|
|
@@ -3673,7 +2768,7 @@ async function start() {
|
|
|
3673
2768
|
server.listen(PORT, () => {
|
|
3674
2769
|
addLog(`Server started on http://localhost:${PORT}`, 'success');
|
|
3675
2770
|
addLog(`Serving ${STATIC_DIR.replace(PROJECT_ROOT, '.')}`, 'info');
|
|
3676
|
-
addLog(`Current branch: ${currentBranch}`, 'info');
|
|
2771
|
+
addLog(`Current branch: ${store.get('currentBranch')}`, 'info');
|
|
3677
2772
|
// Add server log entries for static server
|
|
3678
2773
|
addServerLog(`Static server started on http://localhost:${PORT}`);
|
|
3679
2774
|
addServerLog(`Serving files from: ${STATIC_DIR.replace(PROJECT_ROOT, '.')}`);
|
|
@@ -3707,7 +2802,7 @@ async function start() {
|
|
|
3707
2802
|
});
|
|
3708
2803
|
|
|
3709
2804
|
// Start polling with adaptive interval
|
|
3710
|
-
pollIntervalId = setInterval(pollGitChanges, adaptivePollInterval);
|
|
2805
|
+
pollIntervalId = setInterval(pollGitChanges, store.get('adaptivePollInterval'));
|
|
3711
2806
|
|
|
3712
2807
|
// Initial render
|
|
3713
2808
|
render();
|