git-watchtower 1.3.0 → 1.4.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 +411 -1386
- 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);
|
|
1014
|
+
write(renderClearArea(row, col, width, height));
|
|
1171
1015
|
}
|
|
1172
1016
|
|
|
1173
|
-
|
|
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));
|
|
1017
|
+
// renderHeader - now delegated to renderer.renderHeader()
|
|
1257
1018
|
|
|
1258
|
-
|
|
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;
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
function renderActivityLog(startRow) {
|
|
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
|
-
}
|
|
1367
|
-
|
|
1368
|
-
return startRow + height;
|
|
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);
|
|
1064
|
+
// renderFooter, renderFlash, renderErrorToast, renderPreview, renderHistory
|
|
1065
|
+
// — now delegated to renderer module (src/ui/renderer.js)
|
|
1459
1066
|
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
write(ansi.yellow + ansi.bold);
|
|
1463
|
-
write(box.dTopLeft + box.dHorizontal.repeat(width - 2) + box.dTopRight);
|
|
1067
|
+
// renderLogView, renderInfo, renderActionModal
|
|
1068
|
+
// — now delegated to renderer module (src/ui/renderer.js)
|
|
1464
1069
|
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
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);
|
|
1656
|
-
|
|
1657
|
-
// Title
|
|
1658
|
-
write(ansi.moveTo(row, col + 2));
|
|
1659
|
-
write(ansi.magenta + ansi.bold + ' Switch History ' + ansi.reset);
|
|
1660
|
-
|
|
1661
|
-
// Content
|
|
1662
|
-
if (switchHistory.length === 0) {
|
|
1663
|
-
write(ansi.moveTo(row + 2, col + 3));
|
|
1664
|
-
write(ansi.gray + 'No branch switches yet' + ansi.reset);
|
|
1665
|
-
} else {
|
|
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,45 @@ 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);
|
|
2189
1192
|
}
|
|
2190
1193
|
}
|
|
2191
1194
|
|
|
2192
1195
|
function showFlash(message) {
|
|
2193
1196
|
if (flashTimeout) clearTimeout(flashTimeout);
|
|
2194
1197
|
|
|
2195
|
-
flashMessage
|
|
1198
|
+
store.setState({ flashMessage: message });
|
|
2196
1199
|
render();
|
|
2197
1200
|
|
|
2198
1201
|
flashTimeout = setTimeout(() => {
|
|
2199
|
-
flashMessage
|
|
1202
|
+
store.setState({ flashMessage: null });
|
|
2200
1203
|
render();
|
|
2201
1204
|
}, 3000);
|
|
2202
1205
|
}
|
|
@@ -2206,8 +1209,8 @@ function hideFlash() {
|
|
|
2206
1209
|
clearTimeout(flashTimeout);
|
|
2207
1210
|
flashTimeout = null;
|
|
2208
1211
|
}
|
|
2209
|
-
if (flashMessage) {
|
|
2210
|
-
flashMessage
|
|
1212
|
+
if (store.get('flashMessage')) {
|
|
1213
|
+
store.setState({ flashMessage: null });
|
|
2211
1214
|
render();
|
|
2212
1215
|
}
|
|
2213
1216
|
}
|
|
@@ -2215,12 +1218,13 @@ function hideFlash() {
|
|
|
2215
1218
|
function showErrorToast(title, message, hint = null, duration = 8000) {
|
|
2216
1219
|
if (errorToastTimeout) clearTimeout(errorToastTimeout);
|
|
2217
1220
|
|
|
2218
|
-
errorToast
|
|
1221
|
+
store.setState({ errorToast: { title, message, hint } });
|
|
2219
1222
|
playSound(); // Alert sound for errors
|
|
2220
1223
|
render();
|
|
2221
1224
|
|
|
2222
1225
|
errorToastTimeout = setTimeout(() => {
|
|
2223
|
-
errorToast
|
|
1226
|
+
store.setState({ errorToast: null });
|
|
1227
|
+
pendingDirtyOperation = null;
|
|
2224
1228
|
render();
|
|
2225
1229
|
}, duration);
|
|
2226
1230
|
}
|
|
@@ -2230,8 +1234,8 @@ function hideErrorToast() {
|
|
|
2230
1234
|
clearTimeout(errorToastTimeout);
|
|
2231
1235
|
errorToastTimeout = null;
|
|
2232
1236
|
}
|
|
2233
|
-
if (errorToast) {
|
|
2234
|
-
errorToast
|
|
1237
|
+
if (store.get('errorToast')) {
|
|
1238
|
+
store.setState({ errorToast: null });
|
|
2235
1239
|
render();
|
|
2236
1240
|
}
|
|
2237
1241
|
}
|
|
@@ -2245,12 +1249,12 @@ async function getCurrentBranch() {
|
|
|
2245
1249
|
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD');
|
|
2246
1250
|
// Check for detached HEAD state
|
|
2247
1251
|
if (stdout === 'HEAD') {
|
|
2248
|
-
isDetachedHead
|
|
1252
|
+
store.setState({ isDetachedHead: true });
|
|
2249
1253
|
// Get the short commit hash instead
|
|
2250
1254
|
const { stdout: commitHash } = await execAsync('git rev-parse --short HEAD');
|
|
2251
1255
|
return `HEAD@${commitHash}`;
|
|
2252
1256
|
}
|
|
2253
|
-
isDetachedHead
|
|
1257
|
+
store.setState({ isDetachedHead: false });
|
|
2254
1258
|
return stdout;
|
|
2255
1259
|
} catch (e) {
|
|
2256
1260
|
return null;
|
|
@@ -2276,46 +1280,7 @@ async function hasUncommittedChanges() {
|
|
|
2276
1280
|
}
|
|
2277
1281
|
}
|
|
2278
1282
|
|
|
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
|
-
}
|
|
1283
|
+
// isAuthError, isMergeConflict, isNetworkError imported from src/utils/errors.js
|
|
2319
1284
|
|
|
2320
1285
|
async function getAllBranches() {
|
|
2321
1286
|
try {
|
|
@@ -2400,16 +1365,17 @@ async function switchToBranch(branchName, recordHistory = true) {
|
|
|
2400
1365
|
const isDirty = await hasUncommittedChanges();
|
|
2401
1366
|
if (isDirty) {
|
|
2402
1367
|
addLog(`Cannot switch: uncommitted changes in working directory`, 'error');
|
|
2403
|
-
addLog(`
|
|
1368
|
+
addLog(`Press S to stash changes, or commit manually`, 'warning');
|
|
1369
|
+
pendingDirtyOperation = { type: 'switch', branch: branchName };
|
|
2404
1370
|
showErrorToast(
|
|
2405
1371
|
'Cannot Switch Branch',
|
|
2406
1372
|
'You have uncommitted changes in your working directory that would be lost.',
|
|
2407
|
-
'
|
|
1373
|
+
'Press S to stash changes'
|
|
2408
1374
|
);
|
|
2409
1375
|
return { success: false, reason: 'dirty' };
|
|
2410
1376
|
}
|
|
2411
1377
|
|
|
2412
|
-
const previousBranch = currentBranch;
|
|
1378
|
+
const previousBranch = store.get('currentBranch');
|
|
2413
1379
|
|
|
2414
1380
|
addLog(`Switching to ${safeBranchName}...`, 'update');
|
|
2415
1381
|
render();
|
|
@@ -2423,19 +1389,18 @@ async function switchToBranch(branchName, recordHistory = true) {
|
|
|
2423
1389
|
await execAsync(`git checkout -b "${safeBranchName}" "${REMOTE_NAME}/${safeBranchName}"`);
|
|
2424
1390
|
}
|
|
2425
1391
|
|
|
2426
|
-
currentBranch
|
|
2427
|
-
isDetachedHead = false; // Successfully switched to branch
|
|
1392
|
+
store.setState({ currentBranch: safeBranchName, isDetachedHead: false });
|
|
2428
1393
|
|
|
2429
1394
|
// Clear NEW flag when branch becomes current
|
|
2430
|
-
const branchInfo = branches.find(b => b.name === safeBranchName);
|
|
1395
|
+
const branchInfo = store.get('branches').find(b => b.name === safeBranchName);
|
|
2431
1396
|
if (branchInfo && branchInfo.isNew) {
|
|
2432
1397
|
branchInfo.isNew = false;
|
|
2433
1398
|
}
|
|
2434
1399
|
|
|
2435
1400
|
// Record in history (for undo)
|
|
2436
1401
|
if (recordHistory && previousBranch && previousBranch !== safeBranchName) {
|
|
2437
|
-
switchHistory
|
|
2438
|
-
|
|
1402
|
+
const switchHistory = [{ from: previousBranch, to: safeBranchName, timestamp: Date.now() }, ...store.get('switchHistory')].slice(0, MAX_HISTORY);
|
|
1403
|
+
store.setState({ switchHistory });
|
|
2439
1404
|
}
|
|
2440
1405
|
|
|
2441
1406
|
addLog(`Switched to ${safeBranchName}`, 'success');
|
|
@@ -2458,11 +1423,12 @@ async function switchToBranch(branchName, recordHistory = true) {
|
|
|
2458
1423
|
);
|
|
2459
1424
|
} else if (errMsg.includes('local changes') || errMsg.includes('overwritten')) {
|
|
2460
1425
|
addLog(`Cannot switch: local changes would be overwritten`, 'error');
|
|
2461
|
-
addLog(`
|
|
1426
|
+
addLog(`Press S to stash changes, or commit manually`, 'warning');
|
|
1427
|
+
pendingDirtyOperation = { type: 'switch', branch: branchName };
|
|
2462
1428
|
showErrorToast(
|
|
2463
1429
|
'Cannot Switch Branch',
|
|
2464
1430
|
'Your local changes would be overwritten by checkout.',
|
|
2465
|
-
'
|
|
1431
|
+
'Press S to stash changes'
|
|
2466
1432
|
);
|
|
2467
1433
|
} else {
|
|
2468
1434
|
addLog(`Failed to switch: ${errMsg}`, 'error');
|
|
@@ -2477,17 +1443,18 @@ async function switchToBranch(branchName, recordHistory = true) {
|
|
|
2477
1443
|
}
|
|
2478
1444
|
|
|
2479
1445
|
async function undoLastSwitch() {
|
|
2480
|
-
|
|
1446
|
+
const currentHistory = store.get('switchHistory');
|
|
1447
|
+
if (currentHistory.length === 0) {
|
|
2481
1448
|
addLog('No switch history to undo', 'warning');
|
|
2482
1449
|
return { success: false };
|
|
2483
1450
|
}
|
|
2484
1451
|
|
|
2485
|
-
const lastSwitch =
|
|
1452
|
+
const lastSwitch = currentHistory[0];
|
|
2486
1453
|
addLog(`Undoing: going back to ${lastSwitch.from}`, 'update');
|
|
2487
1454
|
|
|
2488
1455
|
const result = await switchToBranch(lastSwitch.from, false);
|
|
2489
1456
|
if (result.success) {
|
|
2490
|
-
switchHistory.
|
|
1457
|
+
store.setState({ switchHistory: store.get('switchHistory').slice(1) });
|
|
2491
1458
|
addLog(`Undone: back on ${lastSwitch.from}`, 'success');
|
|
2492
1459
|
}
|
|
2493
1460
|
return result;
|
|
@@ -2520,8 +1487,16 @@ async function pullCurrentBranch() {
|
|
|
2520
1487
|
const errMsg = e.stderr || e.message || String(e);
|
|
2521
1488
|
addLog(`Pull failed: ${errMsg}`, 'error');
|
|
2522
1489
|
|
|
2523
|
-
if (
|
|
2524
|
-
|
|
1490
|
+
if (errMsg.includes('local changes') || errMsg.includes('overwritten') || errMsg.includes('uncommitted changes')) {
|
|
1491
|
+
addLog(`Press S to stash changes, or commit manually`, 'warning');
|
|
1492
|
+
pendingDirtyOperation = { type: 'pull' };
|
|
1493
|
+
showErrorToast(
|
|
1494
|
+
'Pull Failed',
|
|
1495
|
+
'Your local changes would be overwritten by pull.',
|
|
1496
|
+
'Press S to stash changes'
|
|
1497
|
+
);
|
|
1498
|
+
} else if (isMergeConflict(errMsg)) {
|
|
1499
|
+
store.setState({ hasMergeConflict: true });
|
|
2525
1500
|
showErrorToast(
|
|
2526
1501
|
'Merge Conflict!',
|
|
2527
1502
|
'Git pull resulted in merge conflicts that need manual resolution.',
|
|
@@ -2550,17 +1525,69 @@ async function pullCurrentBranch() {
|
|
|
2550
1525
|
}
|
|
2551
1526
|
}
|
|
2552
1527
|
|
|
1528
|
+
async function stashAndRetry() {
|
|
1529
|
+
const operation = pendingDirtyOperation;
|
|
1530
|
+
if (!operation) {
|
|
1531
|
+
addLog('No pending operation to retry', 'warning');
|
|
1532
|
+
render();
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
pendingDirtyOperation = null;
|
|
1537
|
+
hideErrorToast();
|
|
1538
|
+
|
|
1539
|
+
addLog('Stashing uncommitted changes...', 'update');
|
|
1540
|
+
render();
|
|
1541
|
+
|
|
1542
|
+
const stashResult = await gitStash({ message: 'git-watchtower: auto-stash before ' + (operation.type === 'switch' ? `switching to ${operation.branch}` : 'pull') });
|
|
1543
|
+
if (!stashResult.success) {
|
|
1544
|
+
addLog(`Stash failed: ${stashResult.error ? stashResult.error.message : 'unknown error'}`, 'error');
|
|
1545
|
+
showErrorToast('Stash Failed', stashResult.error ? stashResult.error.message : 'Could not stash changes.');
|
|
1546
|
+
render();
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
addLog('Changes stashed successfully', 'success');
|
|
1551
|
+
|
|
1552
|
+
if (operation.type === 'switch') {
|
|
1553
|
+
const switchResult = await switchToBranch(operation.branch);
|
|
1554
|
+
if (!switchResult.success) {
|
|
1555
|
+
addLog('Branch switch failed after stash — restoring stashed changes...', 'warning');
|
|
1556
|
+
const popResult = await gitStashPop();
|
|
1557
|
+
if (popResult.success) {
|
|
1558
|
+
addLog('Stashed changes restored', 'info');
|
|
1559
|
+
} else {
|
|
1560
|
+
addLog('Warning: could not restore stashed changes. Run: git stash pop', 'error');
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
await pollGitChanges();
|
|
1564
|
+
} else if (operation.type === 'pull') {
|
|
1565
|
+
const pullResult = await pullCurrentBranch();
|
|
1566
|
+
if (!pullResult.success) {
|
|
1567
|
+
addLog('Pull failed after stash — restoring stashed changes...', 'warning');
|
|
1568
|
+
const popResult = await gitStashPop();
|
|
1569
|
+
if (popResult.success) {
|
|
1570
|
+
addLog('Stashed changes restored', 'info');
|
|
1571
|
+
} else {
|
|
1572
|
+
addLog('Warning: could not restore stashed changes. Run: git stash pop', 'error');
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
await pollGitChanges();
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
render();
|
|
1579
|
+
}
|
|
1580
|
+
|
|
2553
1581
|
// ============================================================================
|
|
2554
1582
|
// Polling
|
|
2555
1583
|
// ============================================================================
|
|
2556
1584
|
|
|
2557
1585
|
async function pollGitChanges() {
|
|
2558
|
-
if (isPolling) return;
|
|
2559
|
-
isPolling
|
|
2560
|
-
pollingStatus = 'fetching';
|
|
1586
|
+
if (store.get('isPolling')) return;
|
|
1587
|
+
store.setState({ isPolling: true, pollingStatus: 'fetching' });
|
|
2561
1588
|
|
|
2562
1589
|
// Casino mode: start slot reels spinning (no sound - too annoying)
|
|
2563
|
-
if (casinoModeEnabled) {
|
|
1590
|
+
if (store.get('casinoModeEnabled')) {
|
|
2564
1591
|
casino.startSlotReels(render);
|
|
2565
1592
|
}
|
|
2566
1593
|
|
|
@@ -2570,25 +1597,28 @@ async function pollGitChanges() {
|
|
|
2570
1597
|
|
|
2571
1598
|
try {
|
|
2572
1599
|
const newCurrentBranch = await getCurrentBranch();
|
|
1600
|
+
const prevCurrentBranch = store.get('currentBranch');
|
|
2573
1601
|
|
|
2574
|
-
if (
|
|
2575
|
-
addLog(`Branch switched externally: ${
|
|
1602
|
+
if (prevCurrentBranch && newCurrentBranch !== prevCurrentBranch) {
|
|
1603
|
+
addLog(`Branch switched externally: ${prevCurrentBranch} → ${newCurrentBranch}`, 'warning');
|
|
2576
1604
|
notifyClients();
|
|
2577
1605
|
}
|
|
2578
|
-
currentBranch
|
|
1606
|
+
store.setState({ currentBranch: newCurrentBranch });
|
|
2579
1607
|
|
|
2580
1608
|
const allBranches = await getAllBranches();
|
|
2581
1609
|
|
|
2582
1610
|
// Track fetch duration
|
|
2583
|
-
lastFetchDuration = Date.now() - fetchStartTime;
|
|
1611
|
+
const lastFetchDuration = Date.now() - fetchStartTime;
|
|
1612
|
+
store.setState({ lastFetchDuration });
|
|
2584
1613
|
|
|
2585
1614
|
// Check for slow fetches
|
|
2586
1615
|
if (lastFetchDuration > 30000 && !verySlowFetchWarningShown) {
|
|
2587
1616
|
addLog(`⚠ Fetches taking ${Math.round(lastFetchDuration / 1000)}s - network may be slow`, 'warning');
|
|
2588
1617
|
verySlowFetchWarningShown = true;
|
|
2589
1618
|
// Slow down polling
|
|
2590
|
-
|
|
2591
|
-
|
|
1619
|
+
const newInterval = Math.min(store.get('adaptivePollInterval') * 2, 60000);
|
|
1620
|
+
store.setState({ adaptivePollInterval: newInterval });
|
|
1621
|
+
addLog(`Polling interval increased to ${newInterval / 1000}s`, 'info');
|
|
2592
1622
|
restartPolling();
|
|
2593
1623
|
} else if (lastFetchDuration > 15000 && !slowFetchWarningShown) {
|
|
2594
1624
|
addLog(`Fetches taking ${Math.round(lastFetchDuration / 1000)}s`, 'warning');
|
|
@@ -2597,21 +1627,22 @@ async function pollGitChanges() {
|
|
|
2597
1627
|
// Reset warnings if fetches are fast again
|
|
2598
1628
|
slowFetchWarningShown = false;
|
|
2599
1629
|
verySlowFetchWarningShown = false;
|
|
2600
|
-
if (adaptivePollInterval > GIT_POLL_INTERVAL) {
|
|
2601
|
-
adaptivePollInterval
|
|
2602
|
-
addLog(`Polling interval restored to ${
|
|
1630
|
+
if (store.get('adaptivePollInterval') > GIT_POLL_INTERVAL) {
|
|
1631
|
+
store.setState({ adaptivePollInterval: GIT_POLL_INTERVAL });
|
|
1632
|
+
addLog(`Polling interval restored to ${GIT_POLL_INTERVAL / 1000}s`, 'info');
|
|
2603
1633
|
restartPolling();
|
|
2604
1634
|
}
|
|
2605
1635
|
}
|
|
2606
1636
|
|
|
2607
1637
|
// Network success - reset failure counter
|
|
2608
|
-
|
|
2609
|
-
if (isOffline) {
|
|
2610
|
-
isOffline = false;
|
|
1638
|
+
if (store.get('isOffline')) {
|
|
2611
1639
|
addLog('Connection restored', 'success');
|
|
2612
1640
|
}
|
|
1641
|
+
store.setState({ consecutiveNetworkFailures: 0, isOffline: false });
|
|
1642
|
+
|
|
2613
1643
|
const fetchedBranchNames = new Set(allBranches.map(b => b.name));
|
|
2614
1644
|
const now = Date.now();
|
|
1645
|
+
const currentBranches = store.get('branches');
|
|
2615
1646
|
|
|
2616
1647
|
// Detect NEW branches (not seen before)
|
|
2617
1648
|
const newBranchList = [];
|
|
@@ -2623,7 +1654,7 @@ async function pollGitChanges() {
|
|
|
2623
1654
|
newBranchList.push(branch);
|
|
2624
1655
|
} else {
|
|
2625
1656
|
// Preserve isNew flag from previous poll cycle for branches not yet switched to
|
|
2626
|
-
const prevBranch =
|
|
1657
|
+
const prevBranch = currentBranches.find(b => b.name === branch.name);
|
|
2627
1658
|
if (prevBranch && prevBranch.isNew) {
|
|
2628
1659
|
branch.isNew = true;
|
|
2629
1660
|
branch.newAt = prevBranch.newAt;
|
|
@@ -2636,7 +1667,7 @@ async function pollGitChanges() {
|
|
|
2636
1667
|
for (const knownName of knownBranchNames) {
|
|
2637
1668
|
if (!fetchedBranchNames.has(knownName)) {
|
|
2638
1669
|
// This branch was deleted from remote
|
|
2639
|
-
const existingInList =
|
|
1670
|
+
const existingInList = currentBranches.find(b => b.name === knownName);
|
|
2640
1671
|
if (existingInList && !existingInList.isDeleted) {
|
|
2641
1672
|
existingInList.isDeleted = true;
|
|
2642
1673
|
existingInList.deletedAt = now;
|
|
@@ -2651,14 +1682,15 @@ async function pollGitChanges() {
|
|
|
2651
1682
|
// Note: isNew flag is only cleared when branch becomes current (see below)
|
|
2652
1683
|
|
|
2653
1684
|
// Keep deleted branches in the list (don't remove them)
|
|
2654
|
-
const
|
|
1685
|
+
const pollFilteredBranches = allBranches;
|
|
2655
1686
|
|
|
2656
1687
|
// Detect updates on other branches (for flash notification)
|
|
2657
1688
|
const updatedBranches = [];
|
|
2658
|
-
|
|
1689
|
+
const currentBranchName = store.get('currentBranch');
|
|
1690
|
+
for (const branch of pollFilteredBranches) {
|
|
2659
1691
|
if (branch.isDeleted) continue;
|
|
2660
1692
|
const prevCommit = previousBranchStates.get(branch.name);
|
|
2661
|
-
if (prevCommit && prevCommit !== branch.commit && branch.name !==
|
|
1693
|
+
if (prevCommit && prevCommit !== branch.commit && branch.name !== currentBranchName) {
|
|
2662
1694
|
updatedBranches.push(branch);
|
|
2663
1695
|
branch.justUpdated = true;
|
|
2664
1696
|
}
|
|
@@ -2666,6 +1698,7 @@ async function pollGitChanges() {
|
|
|
2666
1698
|
}
|
|
2667
1699
|
|
|
2668
1700
|
// Flash and sound for updates or new branches
|
|
1701
|
+
const casinoOn = store.get('casinoModeEnabled');
|
|
2669
1702
|
const notifyBranches = [...updatedBranches, ...newBranchList];
|
|
2670
1703
|
if (notifyBranches.length > 0) {
|
|
2671
1704
|
for (const branch of updatedBranches) {
|
|
@@ -2673,7 +1706,7 @@ async function pollGitChanges() {
|
|
|
2673
1706
|
}
|
|
2674
1707
|
|
|
2675
1708
|
// Casino mode: add funny commentary
|
|
2676
|
-
if (
|
|
1709
|
+
if (casinoOn) {
|
|
2677
1710
|
addLog(`🎰 ${getCasinoMessage('win')}`, 'success');
|
|
2678
1711
|
}
|
|
2679
1712
|
|
|
@@ -2682,7 +1715,7 @@ async function pollGitChanges() {
|
|
|
2682
1715
|
playSound();
|
|
2683
1716
|
|
|
2684
1717
|
// Casino mode: trigger win effect based on number of updated branches
|
|
2685
|
-
if (
|
|
1718
|
+
if (casinoOn) {
|
|
2686
1719
|
// Estimate line changes: more branches = bigger "win"
|
|
2687
1720
|
// Each branch update counts as ~100 lines (placeholder until we calculate actual diff)
|
|
2688
1721
|
const estimatedLines = notifyBranches.length * 100;
|
|
@@ -2694,21 +1727,23 @@ async function pollGitChanges() {
|
|
|
2694
1727
|
}
|
|
2695
1728
|
casino.recordPoll(true);
|
|
2696
1729
|
}
|
|
2697
|
-
} else if (
|
|
1730
|
+
} else if (casinoOn) {
|
|
2698
1731
|
// No updates - stop reels and show result briefly
|
|
2699
1732
|
casino.stopSlotReels(false, render);
|
|
2700
1733
|
casino.recordPoll(false);
|
|
2701
1734
|
}
|
|
2702
1735
|
|
|
2703
1736
|
// Remember which branch was selected before updating the list
|
|
2704
|
-
const
|
|
1737
|
+
const { selectedBranchName: prevSelName, selectedIndex: prevSelIdx } = store.getState();
|
|
1738
|
+
const previouslySelectedName = prevSelName || (currentBranches[prevSelIdx] ? currentBranches[prevSelIdx].name : null);
|
|
2705
1739
|
|
|
2706
1740
|
// Sort: new branches first, then by date, merged branches near bottom, deleted at bottom
|
|
2707
|
-
|
|
1741
|
+
const prStatusMap = store.get('branchPrStatusMap');
|
|
1742
|
+
pollFilteredBranches.sort((a, b) => {
|
|
2708
1743
|
const aIsBase = isBaseBranch(a.name);
|
|
2709
1744
|
const bIsBase = isBaseBranch(b.name);
|
|
2710
|
-
const aMerged = !aIsBase &&
|
|
2711
|
-
const bMerged = !bIsBase &&
|
|
1745
|
+
const aMerged = !aIsBase && prStatusMap.has(a.name) && prStatusMap.get(a.name).state === 'MERGED';
|
|
1746
|
+
const bMerged = !bIsBase && prStatusMap.has(b.name) && prStatusMap.get(b.name).state === 'MERGED';
|
|
2712
1747
|
if (a.isDeleted && !b.isDeleted) return 1;
|
|
2713
1748
|
if (!a.isDeleted && b.isDeleted) return -1;
|
|
2714
1749
|
if (aMerged && !bMerged && !b.isDeleted) return 1;
|
|
@@ -2719,23 +1754,24 @@ async function pollGitChanges() {
|
|
|
2719
1754
|
});
|
|
2720
1755
|
|
|
2721
1756
|
// Store all branches (no limit) - visibleBranchCount controls display
|
|
2722
|
-
branches = filteredBranches;
|
|
2723
|
-
|
|
2724
1757
|
// Restore selection to the same branch (by name) after reordering
|
|
1758
|
+
let newSelectedIndex = prevSelIdx;
|
|
1759
|
+
let newSelectedName = prevSelName;
|
|
2725
1760
|
if (previouslySelectedName) {
|
|
2726
|
-
const
|
|
2727
|
-
if (
|
|
2728
|
-
|
|
2729
|
-
|
|
1761
|
+
const foundIdx = pollFilteredBranches.findIndex(b => b.name === previouslySelectedName);
|
|
1762
|
+
if (foundIdx >= 0) {
|
|
1763
|
+
newSelectedIndex = foundIdx;
|
|
1764
|
+
newSelectedName = previouslySelectedName;
|
|
2730
1765
|
} else {
|
|
2731
1766
|
// Branch fell off the list, keep index at bottom or clamp
|
|
2732
|
-
|
|
2733
|
-
|
|
1767
|
+
newSelectedIndex = Math.min(prevSelIdx, Math.max(0, pollFilteredBranches.length - 1));
|
|
1768
|
+
newSelectedName = pollFilteredBranches[newSelectedIndex] ? pollFilteredBranches[newSelectedIndex].name : null;
|
|
2734
1769
|
}
|
|
2735
|
-
} else if (
|
|
2736
|
-
|
|
2737
|
-
|
|
1770
|
+
} else if (prevSelIdx >= pollFilteredBranches.length) {
|
|
1771
|
+
newSelectedIndex = Math.max(0, pollFilteredBranches.length - 1);
|
|
1772
|
+
newSelectedName = pollFilteredBranches[newSelectedIndex] ? pollFilteredBranches[newSelectedIndex].name : null;
|
|
2738
1773
|
}
|
|
1774
|
+
store.setState({ branches: pollFilteredBranches, selectedIndex: newSelectedIndex, selectedBranchName: newSelectedName });
|
|
2739
1775
|
|
|
2740
1776
|
// Background PR status fetch (throttled to every PR_STATUS_POLL_INTERVAL)
|
|
2741
1777
|
const now2 = Date.now();
|
|
@@ -2743,7 +1779,7 @@ async function pollGitChanges() {
|
|
|
2743
1779
|
prStatusFetchInFlight = true;
|
|
2744
1780
|
fetchAllPrStatuses().then(map => {
|
|
2745
1781
|
if (map) {
|
|
2746
|
-
branchPrStatusMap
|
|
1782
|
+
store.setState({ branchPrStatusMap: map });
|
|
2747
1783
|
render(); // re-render to show updated PR indicators
|
|
2748
1784
|
}
|
|
2749
1785
|
lastPrStatusFetch = Date.now();
|
|
@@ -2754,28 +1790,29 @@ async function pollGitChanges() {
|
|
|
2754
1790
|
}
|
|
2755
1791
|
|
|
2756
1792
|
// AUTO-PULL: If current branch has remote updates, pull automatically (if enabled)
|
|
2757
|
-
const
|
|
2758
|
-
|
|
2759
|
-
|
|
1793
|
+
const autoPullBranchName = store.get('currentBranch');
|
|
1794
|
+
const currentInfo = store.get('branches').find(b => b.name === autoPullBranchName);
|
|
1795
|
+
if (AUTO_PULL && currentInfo && currentInfo.hasUpdates && !store.get('hasMergeConflict')) {
|
|
1796
|
+
addLog(`Auto-pulling changes for ${autoPullBranchName}...`, 'update');
|
|
2760
1797
|
render();
|
|
2761
1798
|
|
|
2762
1799
|
// Save the old commit for diff calculation (casino mode)
|
|
2763
1800
|
const oldCommit = currentInfo.commit;
|
|
2764
1801
|
|
|
2765
1802
|
try {
|
|
2766
|
-
await execAsync(`git pull "${REMOTE_NAME}" "${
|
|
2767
|
-
addLog(`Pulled successfully from ${
|
|
1803
|
+
await execAsync(`git pull "${REMOTE_NAME}" "${autoPullBranchName}"`);
|
|
1804
|
+
addLog(`Pulled successfully from ${autoPullBranchName}`, 'success');
|
|
2768
1805
|
currentInfo.hasUpdates = false;
|
|
2769
|
-
hasMergeConflict
|
|
1806
|
+
store.setState({ hasMergeConflict: false });
|
|
2770
1807
|
// Update the stored commit to the new one
|
|
2771
1808
|
const newCommit = await execAsync('git rev-parse --short HEAD');
|
|
2772
1809
|
currentInfo.commit = newCommit.stdout.trim();
|
|
2773
|
-
previousBranchStates.set(
|
|
1810
|
+
previousBranchStates.set(autoPullBranchName, newCommit.stdout.trim());
|
|
2774
1811
|
// Reload browsers
|
|
2775
1812
|
notifyClients();
|
|
2776
1813
|
|
|
2777
1814
|
// Casino mode: calculate actual diff and trigger win effect
|
|
2778
|
-
if (casinoModeEnabled && oldCommit) {
|
|
1815
|
+
if (store.get('casinoModeEnabled') && oldCommit) {
|
|
2779
1816
|
const diffStats = await getDiffStats(oldCommit, 'HEAD');
|
|
2780
1817
|
const totalLines = diffStats.added + diffStats.deleted;
|
|
2781
1818
|
if (totalLines > 0) {
|
|
@@ -2790,7 +1827,7 @@ async function pollGitChanges() {
|
|
|
2790
1827
|
} catch (e) {
|
|
2791
1828
|
const errMsg = e.stderr || e.stdout || e.message || String(e);
|
|
2792
1829
|
if (isMergeConflict(errMsg)) {
|
|
2793
|
-
hasMergeConflict
|
|
1830
|
+
store.setState({ hasMergeConflict: true });
|
|
2794
1831
|
addLog(`MERGE CONFLICT detected!`, 'error');
|
|
2795
1832
|
addLog(`Resolve conflicts manually, then commit`, 'warning');
|
|
2796
1833
|
showErrorToast(
|
|
@@ -2799,7 +1836,7 @@ async function pollGitChanges() {
|
|
|
2799
1836
|
'Run: git status to see conflicts'
|
|
2800
1837
|
);
|
|
2801
1838
|
// Casino mode: trigger loss effect
|
|
2802
|
-
if (casinoModeEnabled) {
|
|
1839
|
+
if (store.get('casinoModeEnabled')) {
|
|
2803
1840
|
casino.triggerLoss('MERGE CONFLICT!', render);
|
|
2804
1841
|
casinoSounds.playLoss();
|
|
2805
1842
|
addLog(`💀 ${getCasinoMessage('loss')}`, 'error');
|
|
@@ -2823,16 +1860,16 @@ async function pollGitChanges() {
|
|
|
2823
1860
|
}
|
|
2824
1861
|
}
|
|
2825
1862
|
|
|
2826
|
-
pollingStatus
|
|
1863
|
+
store.setState({ pollingStatus: 'idle' });
|
|
2827
1864
|
// Casino mode: stop slot reels if still spinning (already handled above, just cleanup)
|
|
2828
|
-
if (casinoModeEnabled && casino.isSlotSpinning()) {
|
|
1865
|
+
if (store.get('casinoModeEnabled') && casino.isSlotSpinning()) {
|
|
2829
1866
|
casino.stopSlotReels(false, render);
|
|
2830
1867
|
}
|
|
2831
1868
|
} catch (err) {
|
|
2832
1869
|
const errMsg = err.stderr || err.message || String(err);
|
|
2833
1870
|
|
|
2834
1871
|
// Casino mode: stop slot reels and show loss on error
|
|
2835
|
-
if (casinoModeEnabled) {
|
|
1872
|
+
if (store.get('casinoModeEnabled')) {
|
|
2836
1873
|
casino.stopSlotReels(false, render);
|
|
2837
1874
|
casino.triggerLoss('BUST!', render);
|
|
2838
1875
|
casinoSounds.playLoss();
|
|
@@ -2840,17 +1877,18 @@ async function pollGitChanges() {
|
|
|
2840
1877
|
|
|
2841
1878
|
// Handle different error types
|
|
2842
1879
|
if (isNetworkError(errMsg)) {
|
|
2843
|
-
consecutiveNetworkFailures
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
1880
|
+
const failures = store.get('consecutiveNetworkFailures') + 1;
|
|
1881
|
+
store.setState({ consecutiveNetworkFailures: failures });
|
|
1882
|
+
if (failures >= 3 && !store.get('isOffline')) {
|
|
1883
|
+
store.setState({ isOffline: true });
|
|
1884
|
+
addLog(`Network unavailable (${failures} failures)`, 'error');
|
|
2847
1885
|
showErrorToast(
|
|
2848
1886
|
'Network Unavailable',
|
|
2849
1887
|
'Cannot connect to the remote repository. Git operations will fail until connection is restored.',
|
|
2850
1888
|
'Check your internet connection'
|
|
2851
1889
|
);
|
|
2852
1890
|
}
|
|
2853
|
-
pollingStatus
|
|
1891
|
+
store.setState({ pollingStatus: 'error' });
|
|
2854
1892
|
} else if (isAuthError(errMsg)) {
|
|
2855
1893
|
addLog(`Authentication error - check credentials`, 'error');
|
|
2856
1894
|
addLog(`Try: git config credential.helper store`, 'warning');
|
|
@@ -2859,13 +1897,13 @@ async function pollGitChanges() {
|
|
|
2859
1897
|
'Failed to authenticate with the remote repository.',
|
|
2860
1898
|
'Run: git config credential.helper store'
|
|
2861
1899
|
);
|
|
2862
|
-
pollingStatus
|
|
1900
|
+
store.setState({ pollingStatus: 'error' });
|
|
2863
1901
|
} else {
|
|
2864
|
-
pollingStatus
|
|
1902
|
+
store.setState({ pollingStatus: 'error' });
|
|
2865
1903
|
addLog(`Polling error: ${errMsg}`, 'error');
|
|
2866
1904
|
}
|
|
2867
1905
|
} finally {
|
|
2868
|
-
isPolling
|
|
1906
|
+
store.setState({ isPolling: false });
|
|
2869
1907
|
render();
|
|
2870
1908
|
}
|
|
2871
1909
|
}
|
|
@@ -2874,7 +1912,7 @@ function restartPolling() {
|
|
|
2874
1912
|
if (pollIntervalId) {
|
|
2875
1913
|
clearInterval(pollIntervalId);
|
|
2876
1914
|
}
|
|
2877
|
-
pollIntervalId = setInterval(pollGitChanges, adaptivePollInterval);
|
|
1915
|
+
pollIntervalId = setInterval(pollGitChanges, store.get('adaptivePollInterval'));
|
|
2878
1916
|
}
|
|
2879
1917
|
|
|
2880
1918
|
// ============================================================================
|
|
@@ -3011,17 +2049,18 @@ function setupFileWatcher() {
|
|
|
3011
2049
|
// Keyboard Input
|
|
3012
2050
|
// ============================================================================
|
|
3013
2051
|
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
2052
|
+
// applySearchFilter — replaced by filterBranches import (src/ui/renderer.js)
|
|
2053
|
+
|
|
2054
|
+
// Apply state updates from action handlers to store
|
|
2055
|
+
function applyUpdates(updates) {
|
|
2056
|
+
if (!updates) return false;
|
|
2057
|
+
store.setState(updates);
|
|
2058
|
+
return true;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
// Build current state snapshot for action handlers
|
|
2062
|
+
function getActionState() {
|
|
2063
|
+
return store.getState();
|
|
3025
2064
|
}
|
|
3026
2065
|
|
|
3027
2066
|
function setupKeyboardInput() {
|
|
@@ -3032,25 +2071,11 @@ function setupKeyboardInput() {
|
|
|
3032
2071
|
process.stdin.setEncoding('utf8');
|
|
3033
2072
|
|
|
3034
2073
|
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();
|
|
2074
|
+
// Handle search mode input via actions module
|
|
2075
|
+
if (store.get('searchMode')) {
|
|
2076
|
+
const searchResult = actions.handleSearchInput(getActionState(), key);
|
|
2077
|
+
if (searchResult) {
|
|
2078
|
+
applyUpdates(searchResult);
|
|
3054
2079
|
render();
|
|
3055
2080
|
return;
|
|
3056
2081
|
}
|
|
@@ -3061,24 +2086,23 @@ function setupKeyboardInput() {
|
|
|
3061
2086
|
}
|
|
3062
2087
|
|
|
3063
2088
|
// Handle modal modes
|
|
3064
|
-
if (previewMode) {
|
|
2089
|
+
if (store.get('previewMode')) {
|
|
3065
2090
|
if (key === 'v' || key === '\u001b' || key === '\r' || key === '\n') {
|
|
3066
|
-
|
|
3067
|
-
previewData = null;
|
|
2091
|
+
applyUpdates(actions.togglePreview(getActionState()));
|
|
3068
2092
|
render();
|
|
3069
2093
|
return;
|
|
3070
2094
|
}
|
|
3071
2095
|
return; // Ignore other keys in preview mode
|
|
3072
2096
|
}
|
|
3073
2097
|
|
|
3074
|
-
if (historyMode) {
|
|
2098
|
+
if (store.get('historyMode')) {
|
|
3075
2099
|
if (key === 'h' || key === '\u001b') {
|
|
3076
|
-
|
|
2100
|
+
applyUpdates(actions.toggleHistory(getActionState()));
|
|
3077
2101
|
render();
|
|
3078
2102
|
return;
|
|
3079
2103
|
}
|
|
3080
2104
|
if (key === 'u') {
|
|
3081
|
-
historyMode
|
|
2105
|
+
store.setState({ historyMode: false });
|
|
3082
2106
|
await undoLastSwitch();
|
|
3083
2107
|
await pollGitChanges();
|
|
3084
2108
|
return;
|
|
@@ -3086,44 +2110,38 @@ function setupKeyboardInput() {
|
|
|
3086
2110
|
return; // Ignore other keys in history mode
|
|
3087
2111
|
}
|
|
3088
2112
|
|
|
3089
|
-
if (infoMode) {
|
|
2113
|
+
if (store.get('infoMode')) {
|
|
3090
2114
|
if (key === 'i' || key === '\u001b') {
|
|
3091
|
-
|
|
2115
|
+
applyUpdates(actions.toggleInfo(getActionState()));
|
|
3092
2116
|
render();
|
|
3093
2117
|
return;
|
|
3094
2118
|
}
|
|
3095
2119
|
return; // Ignore other keys in info mode
|
|
3096
2120
|
}
|
|
3097
2121
|
|
|
3098
|
-
if (logViewMode) {
|
|
2122
|
+
if (store.get('logViewMode')) {
|
|
3099
2123
|
if (key === 'l' || key === '\u001b') {
|
|
3100
|
-
|
|
3101
|
-
logScrollOffset = 0;
|
|
2124
|
+
applyUpdates(actions.toggleLogView(getActionState()));
|
|
3102
2125
|
render();
|
|
3103
2126
|
return;
|
|
3104
2127
|
}
|
|
3105
2128
|
if (key === '1') { // Switch to activity tab
|
|
3106
|
-
|
|
3107
|
-
logScrollOffset = 0;
|
|
2129
|
+
applyUpdates(actions.switchLogTab(getActionState(), 'activity'));
|
|
3108
2130
|
render();
|
|
3109
2131
|
return;
|
|
3110
2132
|
}
|
|
3111
2133
|
if (key === '2') { // Switch to server tab
|
|
3112
|
-
|
|
3113
|
-
logScrollOffset = 0;
|
|
2134
|
+
applyUpdates(actions.switchLogTab(getActionState(), 'server'));
|
|
3114
2135
|
render();
|
|
3115
2136
|
return;
|
|
3116
2137
|
}
|
|
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
2138
|
if (key === '\u001b[A' || key === 'k') { // Up - scroll
|
|
3121
|
-
|
|
2139
|
+
applyUpdates(actions.scrollLog(getActionState(), 'up'));
|
|
3122
2140
|
render();
|
|
3123
2141
|
return;
|
|
3124
2142
|
}
|
|
3125
2143
|
if (key === '\u001b[B' || key === 'j') { // Down - scroll
|
|
3126
|
-
|
|
2144
|
+
applyUpdates(actions.scrollLog(getActionState(), 'down'));
|
|
3127
2145
|
render();
|
|
3128
2146
|
return;
|
|
3129
2147
|
}
|
|
@@ -3135,16 +2153,15 @@ function setupKeyboardInput() {
|
|
|
3135
2153
|
return; // Ignore other keys in log view mode
|
|
3136
2154
|
}
|
|
3137
2155
|
|
|
3138
|
-
if (actionMode) {
|
|
2156
|
+
if (store.get('actionMode')) {
|
|
3139
2157
|
if (key === '\u001b') { // Escape to close
|
|
3140
|
-
|
|
3141
|
-
actionData = null;
|
|
3142
|
-
actionLoading = false;
|
|
2158
|
+
applyUpdates(actions.closeActionModal(getActionState()));
|
|
3143
2159
|
render();
|
|
3144
2160
|
return;
|
|
3145
2161
|
}
|
|
3146
|
-
|
|
3147
|
-
|
|
2162
|
+
const currentActionData = store.get('actionData');
|
|
2163
|
+
if (!currentActionData) return;
|
|
2164
|
+
const { branch: aBranch, sessionUrl, prInfo, hasGh, hasGlab, ghAuthed, glabAuthed, webUrl, platform, prLoaded } = currentActionData;
|
|
3148
2165
|
const cliReady = (platform === 'gitlab') ? (hasGlab && glabAuthed) : (hasGh && ghAuthed);
|
|
3149
2166
|
const prLabel = platform === 'gitlab' ? 'MR' : 'PR';
|
|
3150
2167
|
|
|
@@ -3185,13 +2202,12 @@ function setupKeyboardInput() {
|
|
|
3185
2202
|
addLog(`${prLabel} created: ${(result.stdout || '').trim().split('\n').pop()}`, 'success');
|
|
3186
2203
|
// Invalidate cache and refresh modal data
|
|
3187
2204
|
prInfoCache.delete(aBranch.name);
|
|
3188
|
-
|
|
3189
|
-
actionLoading
|
|
2205
|
+
const refreshedData = gatherLocalActionData(aBranch);
|
|
2206
|
+
store.setState({ actionData: refreshedData, actionLoading: true });
|
|
3190
2207
|
render();
|
|
3191
|
-
loadAsyncActionData(aBranch,
|
|
3192
|
-
if (actionMode && actionData && actionData.branch.name === aBranch.name) {
|
|
3193
|
-
actionData
|
|
3194
|
-
actionLoading = false;
|
|
2208
|
+
loadAsyncActionData(aBranch, refreshedData).then((fullData) => {
|
|
2209
|
+
if (store.get('actionMode') && store.get('actionData') && store.get('actionData').branch.name === aBranch.name) {
|
|
2210
|
+
store.setState({ actionData: fullData, actionLoading: false });
|
|
3195
2211
|
render();
|
|
3196
2212
|
}
|
|
3197
2213
|
}).catch(() => {});
|
|
@@ -3243,9 +2259,7 @@ function setupKeyboardInput() {
|
|
|
3243
2259
|
await execAsync(`gh pr merge ${prInfo.number} --squash --delete-branch 2>&1`);
|
|
3244
2260
|
}
|
|
3245
2261
|
addLog(`${prLabel} #${prInfo.number} merged`, 'success');
|
|
3246
|
-
actionMode
|
|
3247
|
-
actionData = null;
|
|
3248
|
-
actionLoading = false;
|
|
2262
|
+
store.setState({ actionMode: false, actionData: null, actionLoading: false });
|
|
3249
2263
|
prInfoCache.delete(aBranch.name);
|
|
3250
2264
|
// Force-refresh bulk PR statuses so inline indicators update immediately
|
|
3251
2265
|
lastPrStatusFetch = 0;
|
|
@@ -3296,54 +2310,55 @@ function setupKeyboardInput() {
|
|
|
3296
2310
|
}
|
|
3297
2311
|
|
|
3298
2312
|
// Dismiss flash on any key
|
|
3299
|
-
if (flashMessage) {
|
|
2313
|
+
if (store.get('flashMessage')) {
|
|
3300
2314
|
hideFlash();
|
|
3301
2315
|
if (key !== '\u001b[A' && key !== '\u001b[B' && key !== '\r' && key !== 'q') {
|
|
3302
2316
|
return;
|
|
3303
2317
|
}
|
|
3304
2318
|
}
|
|
3305
2319
|
|
|
3306
|
-
// Dismiss error toast on any key
|
|
3307
|
-
if (errorToast) {
|
|
2320
|
+
// Dismiss error toast on any key (S triggers stash if pending)
|
|
2321
|
+
if (store.get('errorToast')) {
|
|
2322
|
+
if (key === 'S' && pendingDirtyOperation) {
|
|
2323
|
+
await stashAndRetry();
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
3308
2326
|
hideErrorToast();
|
|
2327
|
+
pendingDirtyOperation = null;
|
|
3309
2328
|
if (key !== '\u001b[A' && key !== '\u001b[B' && key !== '\r' && key !== 'q') {
|
|
3310
2329
|
return;
|
|
3311
2330
|
}
|
|
3312
2331
|
}
|
|
3313
2332
|
|
|
3314
|
-
const
|
|
2333
|
+
const { filteredBranches: currentFiltered, branches: currentBranchList, selectedIndex: curSelIdx } = store.getState();
|
|
2334
|
+
const displayBranches = currentFiltered !== null ? currentFiltered : currentBranchList;
|
|
2335
|
+
const actionState = getActionState();
|
|
3315
2336
|
|
|
3316
2337
|
switch (key) {
|
|
3317
2338
|
case '\u001b[A': // Up arrow
|
|
3318
|
-
case 'k':
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
selectedBranchName = displayBranches[selectedIndex] ? displayBranches[selectedIndex].name : null;
|
|
3322
|
-
render();
|
|
3323
|
-
}
|
|
2339
|
+
case 'k': {
|
|
2340
|
+
const result = actions.moveUp(actionState);
|
|
2341
|
+
if (result) { applyUpdates(result); render(); }
|
|
3324
2342
|
break;
|
|
2343
|
+
}
|
|
3325
2344
|
|
|
3326
2345
|
case '\u001b[B': // Down arrow
|
|
3327
|
-
case 'j':
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
selectedBranchName = displayBranches[selectedIndex] ? displayBranches[selectedIndex].name : null;
|
|
3331
|
-
render();
|
|
3332
|
-
}
|
|
2346
|
+
case 'j': {
|
|
2347
|
+
const result = actions.moveDown(actionState);
|
|
2348
|
+
if (result) { applyUpdates(result); render(); }
|
|
3333
2349
|
break;
|
|
2350
|
+
}
|
|
3334
2351
|
|
|
3335
2352
|
case '\r': // Enter
|
|
3336
2353
|
case '\n':
|
|
3337
|
-
if (displayBranches.length > 0 &&
|
|
3338
|
-
const branch = displayBranches[
|
|
2354
|
+
if (displayBranches.length > 0 && curSelIdx < displayBranches.length) {
|
|
2355
|
+
const branch = displayBranches[curSelIdx];
|
|
3339
2356
|
if (branch.isDeleted) {
|
|
3340
2357
|
addLog(`Cannot switch to deleted branch: ${branch.name}`, 'error');
|
|
3341
2358
|
render();
|
|
3342
|
-
} else if (branch.name !== currentBranch) {
|
|
2359
|
+
} else if (branch.name !== store.get('currentBranch')) {
|
|
3343
2360
|
// Clear search when switching
|
|
3344
|
-
searchQuery
|
|
3345
|
-
filteredBranches = null;
|
|
3346
|
-
searchMode = false;
|
|
2361
|
+
store.setState({ searchQuery: '', filteredBranches: null, searchMode: false });
|
|
3347
2362
|
await switchToBranch(branch.name);
|
|
3348
2363
|
await pollGitChanges();
|
|
3349
2364
|
}
|
|
@@ -3351,30 +2366,28 @@ function setupKeyboardInput() {
|
|
|
3351
2366
|
break;
|
|
3352
2367
|
|
|
3353
2368
|
case 'v': // Preview pane
|
|
3354
|
-
if (displayBranches.length > 0 &&
|
|
3355
|
-
const branch = displayBranches[
|
|
2369
|
+
if (displayBranches.length > 0 && curSelIdx < displayBranches.length) {
|
|
2370
|
+
const branch = displayBranches[curSelIdx];
|
|
3356
2371
|
addLog(`Loading preview for ${branch.name}...`, 'info');
|
|
3357
2372
|
render();
|
|
3358
|
-
|
|
3359
|
-
previewMode
|
|
2373
|
+
const pvData = await getPreviewData(branch.name);
|
|
2374
|
+
store.setState({ previewData: pvData, previewMode: true });
|
|
3360
2375
|
render();
|
|
3361
2376
|
}
|
|
3362
2377
|
break;
|
|
3363
2378
|
|
|
3364
2379
|
case '/': // Search mode
|
|
3365
|
-
|
|
3366
|
-
searchQuery = '';
|
|
3367
|
-
selectedIndex = 0;
|
|
2380
|
+
applyUpdates(actions.enterSearchMode(actionState));
|
|
3368
2381
|
render();
|
|
3369
2382
|
break;
|
|
3370
2383
|
|
|
3371
2384
|
case 'h': // History
|
|
3372
|
-
|
|
2385
|
+
applyUpdates(actions.toggleHistory(actionState));
|
|
3373
2386
|
render();
|
|
3374
2387
|
break;
|
|
3375
2388
|
|
|
3376
2389
|
case 'i': // Server info
|
|
3377
|
-
|
|
2390
|
+
applyUpdates(actions.toggleInfo(actionState));
|
|
3378
2391
|
render();
|
|
3379
2392
|
break;
|
|
3380
2393
|
|
|
@@ -3403,13 +2416,11 @@ function setupKeyboardInput() {
|
|
|
3403
2416
|
}
|
|
3404
2417
|
break;
|
|
3405
2418
|
|
|
3406
|
-
case 'l': // View server logs
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
logScrollOffset = 0;
|
|
3410
|
-
render();
|
|
3411
|
-
}
|
|
2419
|
+
case 'l': { // View server logs
|
|
2420
|
+
const logResult = actions.toggleLogView(actionState);
|
|
2421
|
+
if (logResult) { applyUpdates(logResult); render(); }
|
|
3412
2422
|
break;
|
|
2423
|
+
}
|
|
3413
2424
|
|
|
3414
2425
|
case 'o': // Open live server in browser
|
|
3415
2426
|
if (!NO_SERVER) {
|
|
@@ -3421,26 +2432,24 @@ function setupKeyboardInput() {
|
|
|
3421
2432
|
break;
|
|
3422
2433
|
|
|
3423
2434
|
case 'b': { // Branch action modal
|
|
3424
|
-
const branch = displayBranches.length > 0 &&
|
|
3425
|
-
? displayBranches[
|
|
2435
|
+
const branch = displayBranches.length > 0 && curSelIdx < displayBranches.length
|
|
2436
|
+
? displayBranches[curSelIdx] : null;
|
|
3426
2437
|
if (branch) {
|
|
3427
2438
|
// Phase 1: Open modal instantly with local/cached data
|
|
3428
|
-
|
|
3429
|
-
actionMode
|
|
3430
|
-
actionLoading = !actionData.prLoaded;
|
|
2439
|
+
const localData = gatherLocalActionData(branch);
|
|
2440
|
+
store.setState({ actionData: localData, actionMode: true, actionLoading: !localData.prLoaded });
|
|
3431
2441
|
render();
|
|
3432
2442
|
|
|
3433
2443
|
// Phase 2: Load async data (session URL, PR info) in background
|
|
3434
|
-
loadAsyncActionData(branch,
|
|
2444
|
+
loadAsyncActionData(branch, localData).then((fullData) => {
|
|
3435
2445
|
// 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;
|
|
2446
|
+
if (store.get('actionMode') && store.get('actionData') && store.get('actionData').branch.name === branch.name) {
|
|
2447
|
+
store.setState({ actionData: fullData, actionLoading: false });
|
|
3439
2448
|
render();
|
|
3440
2449
|
}
|
|
3441
2450
|
}).catch(() => {
|
|
3442
|
-
if (actionMode && actionData && actionData.branch.name === branch.name) {
|
|
3443
|
-
actionLoading
|
|
2451
|
+
if (store.get('actionMode') && store.get('actionData') && store.get('actionData').branch.name === branch.name) {
|
|
2452
|
+
store.setState({ actionLoading: false });
|
|
3444
2453
|
render();
|
|
3445
2454
|
}
|
|
3446
2455
|
});
|
|
@@ -3458,71 +2467,85 @@ function setupKeyboardInput() {
|
|
|
3458
2467
|
render();
|
|
3459
2468
|
break;
|
|
3460
2469
|
|
|
3461
|
-
case 's':
|
|
3462
|
-
|
|
3463
|
-
addLog(`Sound notifications ${soundEnabled ? 'enabled' : 'disabled'}`, 'info');
|
|
3464
|
-
if (soundEnabled) playSound();
|
|
2470
|
+
case 's': {
|
|
2471
|
+
applyUpdates(actions.toggleSound(actionState));
|
|
2472
|
+
addLog(`Sound notifications ${store.get('soundEnabled') ? 'enabled' : 'disabled'}`, 'info');
|
|
2473
|
+
if (store.get('soundEnabled')) playSound();
|
|
3465
2474
|
render();
|
|
3466
2475
|
break;
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
case 'S': // Stash changes (only active with pending dirty operation)
|
|
2479
|
+
if (pendingDirtyOperation) {
|
|
2480
|
+
await stashAndRetry();
|
|
2481
|
+
}
|
|
2482
|
+
break;
|
|
3467
2483
|
|
|
3468
|
-
case 'c': // Toggle casino mode
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
2484
|
+
case 'c': { // Toggle casino mode
|
|
2485
|
+
const newCasinoState = casino.toggle();
|
|
2486
|
+
store.setState({ casinoModeEnabled: newCasinoState });
|
|
2487
|
+
addLog(`Casino mode ${newCasinoState ? '🎰 ENABLED' : 'disabled'}`, newCasinoState ? 'success' : 'info');
|
|
2488
|
+
if (newCasinoState) {
|
|
3472
2489
|
addLog(`Have you noticed this game has that 'variable rewards' thing going on? 🤔😉`, 'info');
|
|
3473
|
-
if (soundEnabled) {
|
|
2490
|
+
if (store.get('soundEnabled')) {
|
|
3474
2491
|
casinoSounds.playJackpot();
|
|
3475
2492
|
}
|
|
3476
2493
|
}
|
|
3477
2494
|
render();
|
|
3478
2495
|
break;
|
|
2496
|
+
}
|
|
3479
2497
|
|
|
3480
2498
|
// Number keys to set visible branch count
|
|
3481
2499
|
case '1': case '2': case '3': case '4': case '5':
|
|
3482
2500
|
case '6': case '7': case '8': case '9':
|
|
3483
|
-
|
|
3484
|
-
addLog(`Showing ${visibleBranchCount} branches`, 'info');
|
|
2501
|
+
applyUpdates(actions.setVisibleBranchCount(actionState, parseInt(key, 10)));
|
|
2502
|
+
addLog(`Showing ${store.get('visibleBranchCount')} branches`, 'info');
|
|
3485
2503
|
render();
|
|
3486
2504
|
break;
|
|
3487
2505
|
|
|
3488
2506
|
case '0': // 0 = 10 branches
|
|
3489
|
-
|
|
3490
|
-
addLog(`Showing ${visibleBranchCount} branches`, 'info');
|
|
2507
|
+
applyUpdates(actions.setVisibleBranchCount(actionState, 10));
|
|
2508
|
+
addLog(`Showing ${store.get('visibleBranchCount')} branches`, 'info');
|
|
3491
2509
|
render();
|
|
3492
2510
|
break;
|
|
3493
2511
|
|
|
3494
2512
|
case '+':
|
|
3495
|
-
case '=': // = key (same key as + without shift)
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
2513
|
+
case '=': { // = key (same key as + without shift)
|
|
2514
|
+
const incResult = actions.increaseVisibleBranches(actionState, getMaxBranchesForScreen());
|
|
2515
|
+
if (incResult) {
|
|
2516
|
+
applyUpdates(incResult);
|
|
2517
|
+
addLog(`Showing ${store.get('visibleBranchCount')} branches`, 'info');
|
|
3499
2518
|
render();
|
|
3500
2519
|
}
|
|
3501
2520
|
break;
|
|
2521
|
+
}
|
|
3502
2522
|
|
|
3503
2523
|
case '-':
|
|
3504
|
-
case '_': // _ key (same key as - with shift)
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
2524
|
+
case '_': { // _ key (same key as - with shift)
|
|
2525
|
+
const decResult = actions.decreaseVisibleBranches(actionState);
|
|
2526
|
+
if (decResult) {
|
|
2527
|
+
applyUpdates(decResult);
|
|
2528
|
+
addLog(`Showing ${store.get('visibleBranchCount')} branches`, 'info');
|
|
3508
2529
|
render();
|
|
3509
2530
|
}
|
|
3510
2531
|
break;
|
|
2532
|
+
}
|
|
3511
2533
|
|
|
3512
2534
|
case 'q':
|
|
3513
2535
|
case '\u0003': // Ctrl+C
|
|
3514
2536
|
await shutdown();
|
|
3515
2537
|
break;
|
|
3516
2538
|
|
|
3517
|
-
case '\u001b': // Escape - clear search if active, otherwise quit
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
filteredBranches = null;
|
|
3521
|
-
render();
|
|
3522
|
-
} else {
|
|
2539
|
+
case '\u001b': { // Escape - clear search if active, otherwise quit
|
|
2540
|
+
const escResult = actions.handleEscape(actionState);
|
|
2541
|
+
if (escResult && escResult._quit) {
|
|
3523
2542
|
await shutdown();
|
|
2543
|
+
} else if (escResult) {
|
|
2544
|
+
applyUpdates(escResult);
|
|
2545
|
+
render();
|
|
3524
2546
|
}
|
|
3525
2547
|
break;
|
|
2548
|
+
}
|
|
3526
2549
|
}
|
|
3527
2550
|
});
|
|
3528
2551
|
}
|
|
@@ -3621,27 +2644,28 @@ async function start() {
|
|
|
3621
2644
|
}
|
|
3622
2645
|
|
|
3623
2646
|
// Get initial state
|
|
3624
|
-
|
|
2647
|
+
const initBranch = await getCurrentBranch();
|
|
2648
|
+
store.setState({ currentBranch: initBranch });
|
|
3625
2649
|
|
|
3626
2650
|
// Warn if in detached HEAD state
|
|
3627
|
-
if (isDetachedHead) {
|
|
2651
|
+
if (store.get('isDetachedHead')) {
|
|
3628
2652
|
addLog(`Warning: In detached HEAD state`, 'warning');
|
|
3629
2653
|
}
|
|
3630
|
-
|
|
2654
|
+
const initBranches = await getAllBranches();
|
|
2655
|
+
store.setState({ branches: initBranches });
|
|
3631
2656
|
|
|
3632
2657
|
// Initialize previous states and known branches
|
|
3633
|
-
for (const branch of
|
|
2658
|
+
for (const branch of initBranches) {
|
|
3634
2659
|
previousBranchStates.set(branch.name, branch.commit);
|
|
3635
2660
|
knownBranchNames.add(branch.name);
|
|
3636
2661
|
}
|
|
3637
2662
|
|
|
3638
2663
|
// Find current branch in list and select it
|
|
3639
|
-
const currentIndex =
|
|
2664
|
+
const currentIndex = initBranches.findIndex(b => b.name === initBranch);
|
|
3640
2665
|
if (currentIndex >= 0) {
|
|
3641
|
-
selectedIndex
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
selectedBranchName = branches[0].name;
|
|
2666
|
+
store.setState({ selectedIndex: currentIndex, selectedBranchName: initBranch });
|
|
2667
|
+
} else if (initBranches.length > 0) {
|
|
2668
|
+
store.setState({ selectedBranchName: initBranches[0].name });
|
|
3645
2669
|
}
|
|
3646
2670
|
|
|
3647
2671
|
// Load sparklines and action cache in background
|
|
@@ -3650,7 +2674,7 @@ async function start() {
|
|
|
3650
2674
|
// Once env is known, kick off initial PR status fetch
|
|
3651
2675
|
fetchAllPrStatuses().then(map => {
|
|
3652
2676
|
if (map) {
|
|
3653
|
-
branchPrStatusMap
|
|
2677
|
+
store.setState({ branchPrStatusMap: map });
|
|
3654
2678
|
lastPrStatusFetch = Date.now();
|
|
3655
2679
|
render();
|
|
3656
2680
|
}
|
|
@@ -3658,13 +2682,14 @@ async function start() {
|
|
|
3658
2682
|
}).catch(() => {});
|
|
3659
2683
|
|
|
3660
2684
|
// Start server based on mode
|
|
2685
|
+
const startBranchName = store.get('currentBranch');
|
|
3661
2686
|
if (SERVER_MODE === 'none') {
|
|
3662
2687
|
addLog(`Running in no-server mode (branch monitoring only)`, 'info');
|
|
3663
|
-
addLog(`Current branch: ${
|
|
2688
|
+
addLog(`Current branch: ${startBranchName}`, 'info');
|
|
3664
2689
|
render();
|
|
3665
2690
|
} else if (SERVER_MODE === 'command') {
|
|
3666
2691
|
addLog(`Command mode: ${SERVER_COMMAND}`, 'info');
|
|
3667
|
-
addLog(`Current branch: ${
|
|
2692
|
+
addLog(`Current branch: ${startBranchName}`, 'info');
|
|
3668
2693
|
render();
|
|
3669
2694
|
// Start the user's dev server
|
|
3670
2695
|
startServerProcess();
|
|
@@ -3673,7 +2698,7 @@ async function start() {
|
|
|
3673
2698
|
server.listen(PORT, () => {
|
|
3674
2699
|
addLog(`Server started on http://localhost:${PORT}`, 'success');
|
|
3675
2700
|
addLog(`Serving ${STATIC_DIR.replace(PROJECT_ROOT, '.')}`, 'info');
|
|
3676
|
-
addLog(`Current branch: ${currentBranch}`, 'info');
|
|
2701
|
+
addLog(`Current branch: ${store.get('currentBranch')}`, 'info');
|
|
3677
2702
|
// Add server log entries for static server
|
|
3678
2703
|
addServerLog(`Static server started on http://localhost:${PORT}`);
|
|
3679
2704
|
addServerLog(`Serving files from: ${STATIC_DIR.replace(PROJECT_ROOT, '.')}`);
|
|
@@ -3707,7 +2732,7 @@ async function start() {
|
|
|
3707
2732
|
});
|
|
3708
2733
|
|
|
3709
2734
|
// Start polling with adaptive interval
|
|
3710
|
-
pollIntervalId = setInterval(pollGitChanges, adaptivePollInterval);
|
|
2735
|
+
pollIntervalId = setInterval(pollGitChanges, store.get('adaptivePollInterval'));
|
|
3711
2736
|
|
|
3712
2737
|
// Initial render
|
|
3713
2738
|
render();
|