git-watchtower 1.10.5 → 1.10.7
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/package.json +1 -1
- package/src/server/web-ui/js.js +199 -216
package/package.json
CHANGED
package/src/server/web-ui/js.js
CHANGED
|
@@ -33,26 +33,32 @@ function getDashboardJs() {
|
|
|
33
33
|
'use strict';
|
|
34
34
|
|
|
35
35
|
// ── State ──────────────────────────────────────────────────────
|
|
36
|
-
let state = null;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
36
|
+
let state = null; // server-pushed state (branches, config, etc.)
|
|
37
|
+
|
|
38
|
+
// Client-side UI state — consolidated into a single object for
|
|
39
|
+
// easier debugging (inspect ui in console) and clearer separation
|
|
40
|
+
// from the server-pushed 'state' above.
|
|
41
|
+
const ui = {
|
|
42
|
+
prevBranches: null,
|
|
43
|
+
selectedIndex: 0,
|
|
44
|
+
searchMode: false,
|
|
45
|
+
searchQuery: '',
|
|
46
|
+
confirmMode: false,
|
|
47
|
+
confirmCallback: null,
|
|
48
|
+
connected: false,
|
|
49
|
+
flashTimer: null,
|
|
50
|
+
activeTabId: null,
|
|
51
|
+
logViewerMode: false,
|
|
52
|
+
logViewerTab: 'server',
|
|
53
|
+
branchActionMode: false,
|
|
54
|
+
infoMode: false,
|
|
55
|
+
cleanupMode: false,
|
|
56
|
+
updateMode: false,
|
|
57
|
+
stashMode: false,
|
|
58
|
+
pendingStashBranch: null,
|
|
59
|
+
updateNotificationShown: false,
|
|
60
|
+
remoteTabPollTimer: null,
|
|
61
|
+
};
|
|
56
62
|
|
|
57
63
|
// ── Persistent Preferences (localStorage) ─────────────────────
|
|
58
64
|
const PREFS_KEY = 'git-watchtower-prefs';
|
|
@@ -191,27 +197,27 @@ function getDashboardJs() {
|
|
|
191
197
|
evtSource = new EventSource('/api/events');
|
|
192
198
|
|
|
193
199
|
evtSource.onopen = () => {
|
|
194
|
-
connected = true;
|
|
200
|
+
ui.connected = true;
|
|
195
201
|
updateConnectionStatus();
|
|
196
202
|
};
|
|
197
203
|
|
|
198
204
|
evtSource.addEventListener('state', (e) => {
|
|
199
205
|
try {
|
|
200
206
|
const newState = JSON.parse(e.data);
|
|
201
|
-
if (!activeTabId && newState.activeProjectId) {
|
|
202
|
-
activeTabId = newState.activeProjectId;
|
|
207
|
+
if (!ui.activeTabId && newState.activeProjectId) {
|
|
208
|
+
ui.activeTabId = newState.activeProjectId;
|
|
203
209
|
}
|
|
204
210
|
// SSE always pushes the local project's state. When the user
|
|
205
211
|
// is viewing a different tab we must NOT overwrite the per-project
|
|
206
212
|
// data (branches, PRs, activity, etc.) — only update global
|
|
207
213
|
// metadata so the tab bar, connection status, and version info
|
|
208
214
|
// stay current.
|
|
209
|
-
const viewingLocalProject = !activeTabId || activeTabId === newState.activeProjectId;
|
|
215
|
+
const viewingLocalProject = !ui.activeTabId || ui.activeTabId === newState.activeProjectId;
|
|
210
216
|
if (viewingLocalProject) {
|
|
211
217
|
if (state && state.branches) {
|
|
212
218
|
diffBranchesForNotifications(state.branches, newState.branches || []);
|
|
213
219
|
}
|
|
214
|
-
prevBranches = state ? state.branches : null;
|
|
220
|
+
ui.prevBranches = state ? state.branches : null;
|
|
215
221
|
state = newState;
|
|
216
222
|
} else {
|
|
217
223
|
if (state) {
|
|
@@ -240,7 +246,7 @@ function getDashboardJs() {
|
|
|
240
246
|
try {
|
|
241
247
|
const data = JSON.parse(e.data);
|
|
242
248
|
if (!data.success && data.message && data.message.indexOf('uncommitted') !== -1) {
|
|
243
|
-
pendingStashBranch = data.branch || null;
|
|
249
|
+
ui.pendingStashBranch = data.branch || null;
|
|
244
250
|
showErrorToastWithHint(data.message, 'Press S to stash');
|
|
245
251
|
} else {
|
|
246
252
|
showToast(data.message, data.success ? 'success' : 'error');
|
|
@@ -249,7 +255,7 @@ function getDashboardJs() {
|
|
|
249
255
|
});
|
|
250
256
|
|
|
251
257
|
evtSource.onerror = () => {
|
|
252
|
-
connected = false;
|
|
258
|
+
ui.connected = false;
|
|
253
259
|
updateConnectionStatus();
|
|
254
260
|
};
|
|
255
261
|
}
|
|
@@ -257,8 +263,8 @@ function getDashboardJs() {
|
|
|
257
263
|
function updateConnectionStatus() {
|
|
258
264
|
const dot = document.getElementById('connection-dot');
|
|
259
265
|
const badge = document.getElementById('status-badge');
|
|
260
|
-
if (connected) {
|
|
261
|
-
dot.className = 'connection-dot connected';
|
|
266
|
+
if (ui.connected) {
|
|
267
|
+
dot.className = 'connection-dot ui.connected';
|
|
262
268
|
badge.className = 'badge badge-online';
|
|
263
269
|
badge.textContent = 'live';
|
|
264
270
|
} else {
|
|
@@ -274,7 +280,7 @@ function getDashboardJs() {
|
|
|
274
280
|
xhr.open('POST', '/api/action');
|
|
275
281
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
276
282
|
const data = { action, payload: payload || {} };
|
|
277
|
-
if (activeTabId) data.projectId = activeTabId;
|
|
283
|
+
if (ui.activeTabId) data.projectId = ui.activeTabId;
|
|
278
284
|
xhr.send(JSON.stringify(data));
|
|
279
285
|
}
|
|
280
286
|
|
|
@@ -283,8 +289,8 @@ function getDashboardJs() {
|
|
|
283
289
|
const el = document.getElementById('flash');
|
|
284
290
|
el.textContent = text;
|
|
285
291
|
el.className = 'flash visible ' + (type || 'info');
|
|
286
|
-
clearTimeout(flashTimer);
|
|
287
|
-
flashTimer = setTimeout(() => { el.className = 'flash'; }, 3000);
|
|
292
|
+
clearTimeout(ui.flashTimer);
|
|
293
|
+
ui.flashTimer = setTimeout(() => { el.className = 'flash'; }, 3000);
|
|
288
294
|
}
|
|
289
295
|
|
|
290
296
|
// ── Toast Notifications ────────────────────────────────────────
|
|
@@ -337,7 +343,7 @@ function getDashboardJs() {
|
|
|
337
343
|
};
|
|
338
344
|
|
|
339
345
|
function anyModalOpen() {
|
|
340
|
-
return _openModals.length > 0 || confirmMode;
|
|
346
|
+
return _openModals.length > 0 || ui.confirmMode;
|
|
341
347
|
}
|
|
342
348
|
|
|
343
349
|
// Create modal instances
|
|
@@ -349,18 +355,18 @@ function getDashboardJs() {
|
|
|
349
355
|
const updateModal = new Modal('update-overlay', 'update-close');
|
|
350
356
|
|
|
351
357
|
// Per-modal hide callbacks for state cleanup
|
|
352
|
-
logViewerModal.onHide = () => { logViewerMode = false; };
|
|
353
|
-
branchActionModal.onHide = () => { branchActionMode = false; };
|
|
354
|
-
infoModal.onHide = () => { infoMode = false; };
|
|
355
|
-
stashModal.onHide = () => { stashMode = false; pendingStashBranch = null; };
|
|
356
|
-
cleanupModal.onHide = () => { cleanupMode = false; };
|
|
357
|
-
updateModal.onHide = () => { updateMode = false; };
|
|
358
|
+
logViewerModal.onHide = () => { ui.logViewerMode = false; };
|
|
359
|
+
branchActionModal.onHide = () => { ui.branchActionMode = false; };
|
|
360
|
+
infoModal.onHide = () => { ui.infoMode = false; };
|
|
361
|
+
stashModal.onHide = () => { ui.stashMode = false; ui.pendingStashBranch = null; };
|
|
362
|
+
cleanupModal.onHide = () => { ui.cleanupMode = false; };
|
|
363
|
+
updateModal.onHide = () => { ui.updateMode = false; };
|
|
358
364
|
|
|
359
365
|
// ── Confirm Dialog ─────────────────────────────────────────────
|
|
360
366
|
function showConfirm(title, message, onConfirm, opts) {
|
|
361
367
|
opts = opts || {};
|
|
362
|
-
confirmMode = true;
|
|
363
|
-
confirmCallback = onConfirm;
|
|
368
|
+
ui.confirmMode = true;
|
|
369
|
+
ui.confirmCallback = onConfirm;
|
|
364
370
|
const box = document.getElementById('confirm-box');
|
|
365
371
|
box.innerHTML =
|
|
366
372
|
'<div class="confirm-title">' + escHtml(title) + '</div>' +
|
|
@@ -375,13 +381,13 @@ function getDashboardJs() {
|
|
|
375
381
|
document.getElementById('confirm-cancel').onclick = hideConfirm;
|
|
376
382
|
document.getElementById('confirm-ok').onclick = () => {
|
|
377
383
|
hideConfirm();
|
|
378
|
-
if (confirmCallback) confirmCallback();
|
|
384
|
+
if (ui.confirmCallback) ui.confirmCallback();
|
|
379
385
|
};
|
|
380
386
|
}
|
|
381
387
|
|
|
382
388
|
function hideConfirm() {
|
|
383
|
-
confirmMode = false;
|
|
384
|
-
confirmCallback = null;
|
|
389
|
+
ui.confirmMode = false;
|
|
390
|
+
ui.confirmCallback = null;
|
|
385
391
|
document.getElementById('confirm-overlay').className = 'confirm-overlay';
|
|
386
392
|
}
|
|
387
393
|
|
|
@@ -398,7 +404,7 @@ function getDashboardJs() {
|
|
|
398
404
|
let html = '';
|
|
399
405
|
for (let i = 0; i < projects.length; i++) {
|
|
400
406
|
const p = projects[i];
|
|
401
|
-
const isActive = p.id === activeTabId;
|
|
407
|
+
const isActive = p.id === ui.activeTabId;
|
|
402
408
|
html += '<div class="tab' + (isActive ? ' active' : '') + '" data-project-id="' + escHtml(p.id) + '">';
|
|
403
409
|
html += '<span class="tab-dot"></span>';
|
|
404
410
|
html += escHtml(p.name);
|
|
@@ -412,7 +418,7 @@ function getDashboardJs() {
|
|
|
412
418
|
const xhr = new XMLHttpRequest();
|
|
413
419
|
xhr.open('GET', '/api/projects/' + projectId + '/state');
|
|
414
420
|
xhr.onload = () => {
|
|
415
|
-
if (xhr.status === 200 && activeTabId === projectId) {
|
|
421
|
+
if (xhr.status === 200 && ui.activeTabId === projectId) {
|
|
416
422
|
try {
|
|
417
423
|
const pState = JSON.parse(xhr.responseText);
|
|
418
424
|
state.branches = pState.branches || [];
|
|
@@ -435,20 +441,20 @@ function getDashboardJs() {
|
|
|
435
441
|
}
|
|
436
442
|
|
|
437
443
|
function switchTab(projectId) {
|
|
438
|
-
if (projectId === activeTabId) return;
|
|
439
|
-
activeTabId = projectId;
|
|
440
|
-
selectedIndex = 0;
|
|
441
|
-
searchQuery = '';
|
|
442
|
-
searchMode = false;
|
|
444
|
+
if (projectId === ui.activeTabId) return;
|
|
445
|
+
ui.activeTabId = projectId;
|
|
446
|
+
ui.selectedIndex = 0;
|
|
447
|
+
ui.searchQuery = '';
|
|
448
|
+
ui.searchMode = false;
|
|
443
449
|
document.getElementById('search-bar').className = 'search-bar';
|
|
444
450
|
document.getElementById('search-input').value = '';
|
|
445
451
|
renderTabs();
|
|
446
452
|
fetchAndApplyProjectState(projectId);
|
|
447
453
|
|
|
448
|
-
clearInterval(remoteTabPollTimer);
|
|
449
|
-
remoteTabPollTimer = null;
|
|
454
|
+
clearInterval(ui.remoteTabPollTimer);
|
|
455
|
+
ui.remoteTabPollTimer = null;
|
|
450
456
|
if (state && projectId !== state.activeProjectId) {
|
|
451
|
-
remoteTabPollTimer = setInterval(() => {
|
|
457
|
+
ui.remoteTabPollTimer = setInterval(() => {
|
|
452
458
|
fetchAndApplyProjectState(projectId);
|
|
453
459
|
}, 2000);
|
|
454
460
|
}
|
|
@@ -464,7 +470,7 @@ ${pureFnBlock}
|
|
|
464
470
|
getDisplayBranches = function() {
|
|
465
471
|
if (!state || !state.branches) return [];
|
|
466
472
|
return _pureGetDisplayBranches(state.branches, {
|
|
467
|
-
searchQuery: searchQuery,
|
|
473
|
+
searchQuery: ui.searchQuery,
|
|
468
474
|
pinnedBranches: pinnedBranches,
|
|
469
475
|
sortOrder: sortOrder,
|
|
470
476
|
});
|
|
@@ -487,7 +493,7 @@ ${pureFnBlock}
|
|
|
487
493
|
if (state.version) versionEl.textContent = 'v' + state.version;
|
|
488
494
|
|
|
489
495
|
// Status badge
|
|
490
|
-
if (connected) {
|
|
496
|
+
if (ui.connected) {
|
|
491
497
|
const badge = document.getElementById('status-badge');
|
|
492
498
|
if (state.isOffline) {
|
|
493
499
|
badge.className = 'badge badge-offline';
|
|
@@ -507,13 +513,13 @@ ${pureFnBlock}
|
|
|
507
513
|
renderPrefsBar();
|
|
508
514
|
|
|
509
515
|
// Auto-show update notification (once per session)
|
|
510
|
-
if (state.updateAvailable && !updateNotificationShown && !anyModalOpen()) {
|
|
511
|
-
updateNotificationShown = true;
|
|
516
|
+
if (state.updateAvailable && !ui.updateNotificationShown && !anyModalOpen()) {
|
|
517
|
+
ui.updateNotificationShown = true;
|
|
512
518
|
showUpdateModal();
|
|
513
519
|
}
|
|
514
520
|
|
|
515
521
|
// Update log viewer if open
|
|
516
|
-
if (logViewerMode) renderLogViewer();
|
|
522
|
+
if (ui.logViewerMode) renderLogViewer();
|
|
517
523
|
}
|
|
518
524
|
|
|
519
525
|
function renderBranches() {
|
|
@@ -522,14 +528,14 @@ ${pureFnBlock}
|
|
|
522
528
|
const countEl = document.getElementById('branch-count');
|
|
523
529
|
countEl.textContent = branches.length;
|
|
524
530
|
|
|
525
|
-
if (selectedIndex >= branches.length) {
|
|
526
|
-
selectedIndex = Math.max(0, branches.length - 1);
|
|
531
|
+
if (ui.selectedIndex >= branches.length) {
|
|
532
|
+
ui.selectedIndex = Math.max(0, branches.length - 1);
|
|
527
533
|
}
|
|
528
534
|
|
|
529
535
|
if (branches.length === 0) {
|
|
530
536
|
container.innerHTML = '<div class="empty-state">' +
|
|
531
537
|
'<div class="empty-state-icon">🌿</div>' +
|
|
532
|
-
(searchQuery ? 'No branches matching "' + escHtml(searchQuery) + '"' : 'No branches found') +
|
|
538
|
+
(ui.searchQuery ? 'No branches matching "' + escHtml(ui.searchQuery) + '"' : 'No branches found') +
|
|
533
539
|
'</div>';
|
|
534
540
|
return;
|
|
535
541
|
}
|
|
@@ -537,7 +543,7 @@ ${pureFnBlock}
|
|
|
537
543
|
let html = '';
|
|
538
544
|
for (let i = 0; i < branches.length; i++) {
|
|
539
545
|
const b = branches[i];
|
|
540
|
-
const isSelected = i === selectedIndex;
|
|
546
|
+
const isSelected = i === ui.selectedIndex;
|
|
541
547
|
const isCurrent = b.name === state.currentBranch;
|
|
542
548
|
|
|
543
549
|
// Sparkline
|
|
@@ -668,8 +674,8 @@ ${pureFnBlock}
|
|
|
668
674
|
|
|
669
675
|
// ── Log Viewer ─────────────────────────────────────────────────
|
|
670
676
|
function showLogViewer() {
|
|
671
|
-
logViewerMode = true;
|
|
672
|
-
logViewerTab = 'server';
|
|
677
|
+
ui.logViewerMode = true;
|
|
678
|
+
ui.logViewerTab = 'server';
|
|
673
679
|
renderLogViewer();
|
|
674
680
|
logViewerModal.show();
|
|
675
681
|
}
|
|
@@ -682,11 +688,11 @@ ${pureFnBlock}
|
|
|
682
688
|
// Update tab active state
|
|
683
689
|
const tabs = document.querySelectorAll('.log-viewer-tab');
|
|
684
690
|
for (let t = 0; t < tabs.length; t++) {
|
|
685
|
-
tabs[t].className = 'log-viewer-tab' + (tabs[t].getAttribute('data-tab') === logViewerTab ? ' active' : '');
|
|
691
|
+
tabs[t].className = 'log-viewer-tab' + (tabs[t].getAttribute('data-tab') === ui.logViewerTab ? ' active' : '');
|
|
686
692
|
}
|
|
687
693
|
|
|
688
694
|
let html = '';
|
|
689
|
-
if (logViewerTab === 'server') {
|
|
695
|
+
if (ui.logViewerTab === 'server') {
|
|
690
696
|
const logs = state.serverLogBuffer || [];
|
|
691
697
|
if (logs.length === 0) {
|
|
692
698
|
html = '<div style="color:var(--text-muted);padding:20px;text-align:center;">No server logs</div>';
|
|
@@ -721,16 +727,16 @@ ${pureFnBlock}
|
|
|
721
727
|
document.getElementById('log-viewer-tabs').addEventListener('click', (e) => {
|
|
722
728
|
const tab = e.target.closest('.log-viewer-tab');
|
|
723
729
|
if (!tab) return;
|
|
724
|
-
logViewerTab = tab.getAttribute('data-tab');
|
|
730
|
+
ui.logViewerTab = tab.getAttribute('data-tab');
|
|
725
731
|
renderLogViewer();
|
|
726
732
|
});
|
|
727
733
|
|
|
728
734
|
// ── Branch Action Modal ────────────────────────────────────────
|
|
729
735
|
function showBranchActions() {
|
|
730
736
|
const branches = getDisplayBranches();
|
|
731
|
-
if (!branches.length || selectedIndex >= branches.length) return;
|
|
732
|
-
const branch = branches[selectedIndex];
|
|
733
|
-
branchActionMode = true;
|
|
737
|
+
if (!branches.length || ui.selectedIndex >= branches.length) return;
|
|
738
|
+
const branch = branches[ui.selectedIndex];
|
|
739
|
+
ui.branchActionMode = true;
|
|
734
740
|
branchActionModal.show();
|
|
735
741
|
document.getElementById('branch-action-title').textContent = 'Actions: ' + branch.name;
|
|
736
742
|
|
|
@@ -836,7 +842,7 @@ ${pureFnBlock}
|
|
|
836
842
|
// ── Info Panel ─────────────────────────────────────────────────
|
|
837
843
|
function showInfo() {
|
|
838
844
|
if (!state) return;
|
|
839
|
-
infoMode = true;
|
|
845
|
+
ui.infoMode = true;
|
|
840
846
|
const grid = document.getElementById('info-grid');
|
|
841
847
|
const rows = [
|
|
842
848
|
['Project', state.projectName || '-'],
|
|
@@ -863,8 +869,8 @@ ${pureFnBlock}
|
|
|
863
869
|
|
|
864
870
|
// ── Stash Management ───────────────────────────────────────────
|
|
865
871
|
function showStashDialog(pendingBranch) {
|
|
866
|
-
stashMode = true;
|
|
867
|
-
pendingStashBranch = pendingBranch || null;
|
|
872
|
+
ui.stashMode = true;
|
|
873
|
+
ui.pendingStashBranch = pendingBranch || null;
|
|
868
874
|
const msg = pendingBranch
|
|
869
875
|
? 'You have uncommitted changes. Stash them before switching to <strong>' + escHtml(pendingBranch) + '</strong>?'
|
|
870
876
|
: 'Stash all uncommitted changes in the working directory?';
|
|
@@ -877,7 +883,7 @@ ${pureFnBlock}
|
|
|
877
883
|
stashModal.show();
|
|
878
884
|
document.getElementById('stash-cancel').onclick = hideStash;
|
|
879
885
|
document.getElementById('stash-confirm').onclick = () => {
|
|
880
|
-
sendAction('stash', { pendingBranch: pendingStashBranch });
|
|
886
|
+
sendAction('stash', { pendingBranch: ui.pendingStashBranch });
|
|
881
887
|
showToast('Stashing changes...', 'info');
|
|
882
888
|
hideStash();
|
|
883
889
|
};
|
|
@@ -887,7 +893,7 @@ ${pureFnBlock}
|
|
|
887
893
|
|
|
888
894
|
// ── Branch Cleanup ─────────────────────────────────────────────
|
|
889
895
|
function showCleanup() {
|
|
890
|
-
cleanupMode = true;
|
|
896
|
+
ui.cleanupMode = true;
|
|
891
897
|
const html = '<div style="color:var(--text-dim);font-size:13px;margin-bottom:12px;">Scanning for branches with deleted remotes...</div>';
|
|
892
898
|
document.getElementById('cleanup-content').innerHTML = html;
|
|
893
899
|
cleanupModal.show();
|
|
@@ -950,7 +956,7 @@ ${pureFnBlock}
|
|
|
950
956
|
// ── Update Notification ────────────────────────────────────────
|
|
951
957
|
function showUpdateModal() {
|
|
952
958
|
if (!state || !state.updateAvailable) return;
|
|
953
|
-
updateMode = true;
|
|
959
|
+
ui.updateMode = true;
|
|
954
960
|
const html = '<div class="update-versions">';
|
|
955
961
|
html += '<span class="old-version">v' + escHtml(state.version || '?') + '</span>';
|
|
956
962
|
html += '<span class="arrow">→</span>';
|
|
@@ -1029,7 +1035,7 @@ ${pureFnBlock}
|
|
|
1029
1035
|
hintEl.addEventListener('click', (e) => {
|
|
1030
1036
|
const h = e.currentTarget.getAttribute('data-hint');
|
|
1031
1037
|
if (h === 'Press S to stash') {
|
|
1032
|
-
showStashDialog(pendingStashBranch);
|
|
1038
|
+
showStashDialog(ui.pendingStashBranch);
|
|
1033
1039
|
}
|
|
1034
1040
|
toast.classList.remove('visible');
|
|
1035
1041
|
setTimeout(() => { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 300);
|
|
@@ -1043,6 +1049,99 @@ ${pureFnBlock}
|
|
|
1043
1049
|
}
|
|
1044
1050
|
|
|
1045
1051
|
// ── Keyboard ───────────────────────────────────────────────────
|
|
1052
|
+
|
|
1053
|
+
// Key-to-action mapping for normal mode.
|
|
1054
|
+
// Declarative — easy to test, extend, and share with TUI.
|
|
1055
|
+
const KEY_MAP = {
|
|
1056
|
+
'j': 'moveDown',
|
|
1057
|
+
'ArrowDown': 'moveDown',
|
|
1058
|
+
'k': 'moveUp',
|
|
1059
|
+
'ArrowUp': 'moveUp',
|
|
1060
|
+
'Enter': 'selectBranch',
|
|
1061
|
+
'/': 'search',
|
|
1062
|
+
'p': 'pull',
|
|
1063
|
+
'f': 'fetch',
|
|
1064
|
+
'r': 'reloadBrowsers',
|
|
1065
|
+
'R': 'restartServer',
|
|
1066
|
+
'c': 'toggleCasino',
|
|
1067
|
+
'o': 'openBrowser',
|
|
1068
|
+
'h': 'showHistory',
|
|
1069
|
+
'u': 'undo',
|
|
1070
|
+
's': 'toggleSound',
|
|
1071
|
+
'b': 'branchActions',
|
|
1072
|
+
'i': 'info',
|
|
1073
|
+
'l': 'logViewer',
|
|
1074
|
+
'S': 'stash',
|
|
1075
|
+
'd': 'cleanup',
|
|
1076
|
+
'Escape': 'escape',
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
// Action handlers for normal mode.
|
|
1080
|
+
// Each receives the KeyboardEvent for cases that need it.
|
|
1081
|
+
const KEY_ACTIONS = {
|
|
1082
|
+
moveDown() { moveSelection(1); },
|
|
1083
|
+
moveUp() { moveSelection(-1); },
|
|
1084
|
+
selectBranch() {
|
|
1085
|
+
const branches = getDisplayBranches();
|
|
1086
|
+
if (branches.length > 0 && ui.selectedIndex < branches.length) {
|
|
1087
|
+
const b = branches[ui.selectedIndex];
|
|
1088
|
+
if (b.isDeleted) {
|
|
1089
|
+
showToast('Cannot switch to a deleted branch', 'error');
|
|
1090
|
+
} else if (b.name === state.currentBranch) {
|
|
1091
|
+
showToast('Already on ' + b.name, 'info');
|
|
1092
|
+
} else {
|
|
1093
|
+
sendAction('switchBranch', { branch: b.name });
|
|
1094
|
+
showToast('Switching to ' + b.name + '...', 'info');
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
search() {
|
|
1099
|
+
ui.searchMode = true;
|
|
1100
|
+
ui.searchQuery = '';
|
|
1101
|
+
ui.selectedIndex = 0;
|
|
1102
|
+
document.getElementById('search-bar').className = 'search-bar active';
|
|
1103
|
+
const input = document.getElementById('search-input');
|
|
1104
|
+
input.value = '';
|
|
1105
|
+
input.focus();
|
|
1106
|
+
},
|
|
1107
|
+
pull() { sendAction('pull'); showToast('Pulling current branch...', 'info'); },
|
|
1108
|
+
fetch() { sendAction('fetch'); showToast('Fetching all branches...', 'info'); },
|
|
1109
|
+
reloadBrowsers() {
|
|
1110
|
+
if (state && state.serverMode === 'static') {
|
|
1111
|
+
sendAction('reloadBrowsers');
|
|
1112
|
+
showToast('Reloading browsers...', 'info');
|
|
1113
|
+
}
|
|
1114
|
+
},
|
|
1115
|
+
restartServer() {
|
|
1116
|
+
if (state && state.serverMode === 'command') {
|
|
1117
|
+
showConfirm('Restart Server', 'Restart the dev server process?', () => {
|
|
1118
|
+
sendAction('restartServer');
|
|
1119
|
+
showToast('Restarting server...', 'info');
|
|
1120
|
+
}, { label: 'Restart' });
|
|
1121
|
+
}
|
|
1122
|
+
},
|
|
1123
|
+
toggleCasino() { sendAction('toggleCasino'); },
|
|
1124
|
+
openBrowser() { sendAction('openBrowser'); showToast('Opening in browser...', 'info'); },
|
|
1125
|
+
showHistory() {
|
|
1126
|
+
if (state && state.switchHistory && state.switchHistory.length > 0) {
|
|
1127
|
+
const last = state.switchHistory[0];
|
|
1128
|
+
let histMsg = 'Last: ' + last.from + ' \\u2192 ' + last.to;
|
|
1129
|
+
if (state.switchHistory.length > 1) histMsg += ' (+' + (state.switchHistory.length - 1) + ' more)';
|
|
1130
|
+
showToast(histMsg, 'info');
|
|
1131
|
+
} else {
|
|
1132
|
+
showToast('No switch history yet', 'info');
|
|
1133
|
+
}
|
|
1134
|
+
},
|
|
1135
|
+
undo() { sendAction('undo'); showToast('Undoing last switch...', 'info'); },
|
|
1136
|
+
toggleSound() { sendAction('toggleSound'); showToast(state && state.soundEnabled ? 'Sound off' : 'Sound on', 'info'); },
|
|
1137
|
+
branchActions() { showBranchActions(); },
|
|
1138
|
+
info() { showInfo(); },
|
|
1139
|
+
logViewer() { showLogViewer(); },
|
|
1140
|
+
stash() { showStashDialog(null); },
|
|
1141
|
+
cleanup() { showCleanup(); },
|
|
1142
|
+
escape() { /* no-op in normal mode */ },
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1046
1145
|
document.addEventListener('keydown', (e) => {
|
|
1047
1146
|
// Ignore when typing in input fields (other than search)
|
|
1048
1147
|
if (e.target.tagName === 'INPUT' && e.target.id !== 'search-input') return;
|
|
@@ -1056,10 +1155,10 @@ ${pureFnBlock}
|
|
|
1056
1155
|
}
|
|
1057
1156
|
|
|
1058
1157
|
// Log viewer tab switching
|
|
1059
|
-
if (logViewerMode) {
|
|
1158
|
+
if (ui.logViewerMode) {
|
|
1060
1159
|
if (e.key === 'Tab' || e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
|
1061
1160
|
e.preventDefault();
|
|
1062
|
-
logViewerTab = logViewerTab === 'server' ? 'activity' : 'server';
|
|
1161
|
+
ui.logViewerTab = ui.logViewerTab === 'server' ? 'activity' : 'server';
|
|
1063
1162
|
renderLogViewer();
|
|
1064
1163
|
}
|
|
1065
1164
|
return;
|
|
@@ -1069,11 +1168,11 @@ ${pureFnBlock}
|
|
|
1069
1168
|
if (_openModals.length > 0) return;
|
|
1070
1169
|
|
|
1071
1170
|
// Confirm dialog mode — Escape to cancel, Enter to confirm
|
|
1072
|
-
if (confirmMode) {
|
|
1171
|
+
if (ui.confirmMode) {
|
|
1073
1172
|
if (e.key === 'Escape') { e.preventDefault(); hideConfirm(); }
|
|
1074
1173
|
if (e.key === 'Enter') {
|
|
1075
1174
|
e.preventDefault();
|
|
1076
|
-
const cb = confirmCallback;
|
|
1175
|
+
const cb = ui.confirmCallback;
|
|
1077
1176
|
hideConfirm();
|
|
1078
1177
|
if (cb) cb();
|
|
1079
1178
|
}
|
|
@@ -1081,20 +1180,20 @@ ${pureFnBlock}
|
|
|
1081
1180
|
}
|
|
1082
1181
|
|
|
1083
1182
|
// Search mode
|
|
1084
|
-
if (searchMode) {
|
|
1183
|
+
if (ui.searchMode) {
|
|
1085
1184
|
if (e.key === 'Escape') {
|
|
1086
1185
|
e.preventDefault();
|
|
1087
|
-
searchMode = false;
|
|
1088
|
-
searchQuery = '';
|
|
1186
|
+
ui.searchMode = false;
|
|
1187
|
+
ui.searchQuery = '';
|
|
1089
1188
|
document.getElementById('search-bar').className = 'search-bar';
|
|
1090
1189
|
document.getElementById('search-input').value = '';
|
|
1091
|
-
selectedIndex = 0;
|
|
1190
|
+
ui.selectedIndex = 0;
|
|
1092
1191
|
renderBranches();
|
|
1093
1192
|
return;
|
|
1094
1193
|
}
|
|
1095
1194
|
if (e.key === 'Enter') {
|
|
1096
1195
|
e.preventDefault();
|
|
1097
|
-
searchMode = false;
|
|
1196
|
+
ui.searchMode = false;
|
|
1098
1197
|
document.getElementById('search-bar').className = 'search-bar';
|
|
1099
1198
|
return;
|
|
1100
1199
|
}
|
|
@@ -1125,7 +1224,7 @@ ${pureFnBlock}
|
|
|
1125
1224
|
// Tab cycling with Tab key
|
|
1126
1225
|
if (e.key === 'Tab' && projects.length > 1) {
|
|
1127
1226
|
e.preventDefault();
|
|
1128
|
-
const curIdx = projects.findIndex((p) => p.id === activeTabId);
|
|
1227
|
+
const curIdx = projects.findIndex((p) => p.id === ui.activeTabId);
|
|
1129
1228
|
const nextIdx = e.shiftKey
|
|
1130
1229
|
? (curIdx - 1 + projects.length) % projects.length
|
|
1131
1230
|
: (curIdx + 1) % projects.length;
|
|
@@ -1133,142 +1232,26 @@ ${pureFnBlock}
|
|
|
1133
1232
|
return;
|
|
1134
1233
|
}
|
|
1135
1234
|
|
|
1136
|
-
// Normal mode
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
moveSelection(1);
|
|
1142
|
-
break;
|
|
1143
|
-
case 'k':
|
|
1144
|
-
case 'ArrowUp':
|
|
1145
|
-
e.preventDefault();
|
|
1146
|
-
moveSelection(-1);
|
|
1147
|
-
break;
|
|
1148
|
-
case 'Enter':
|
|
1149
|
-
e.preventDefault();
|
|
1150
|
-
const branches = getDisplayBranches();
|
|
1151
|
-
if (branches.length > 0 && selectedIndex < branches.length) {
|
|
1152
|
-
const b = branches[selectedIndex];
|
|
1153
|
-
if (b.isDeleted) {
|
|
1154
|
-
showToast('Cannot switch to a deleted branch', 'error');
|
|
1155
|
-
} else if (b.name === state.currentBranch) {
|
|
1156
|
-
showToast('Already on ' + b.name, 'info');
|
|
1157
|
-
} else {
|
|
1158
|
-
sendAction('switchBranch', { branch: b.name });
|
|
1159
|
-
showToast('Switching to ' + b.name + '...', 'info');
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
break;
|
|
1163
|
-
case '/':
|
|
1164
|
-
e.preventDefault();
|
|
1165
|
-
searchMode = true;
|
|
1166
|
-
searchQuery = '';
|
|
1167
|
-
selectedIndex = 0;
|
|
1168
|
-
document.getElementById('search-bar').className = 'search-bar active';
|
|
1169
|
-
const input = document.getElementById('search-input');
|
|
1170
|
-
input.value = '';
|
|
1171
|
-
input.focus();
|
|
1172
|
-
break;
|
|
1173
|
-
case 'p':
|
|
1174
|
-
e.preventDefault();
|
|
1175
|
-
sendAction('pull');
|
|
1176
|
-
showToast('Pulling current branch...', 'info');
|
|
1177
|
-
break;
|
|
1178
|
-
case 'f':
|
|
1179
|
-
e.preventDefault();
|
|
1180
|
-
sendAction('fetch');
|
|
1181
|
-
showToast('Fetching all branches...', 'info');
|
|
1182
|
-
break;
|
|
1183
|
-
case 'r':
|
|
1184
|
-
e.preventDefault();
|
|
1185
|
-
if (state && state.serverMode === 'static') {
|
|
1186
|
-
sendAction('reloadBrowsers');
|
|
1187
|
-
showToast('Reloading browsers...', 'info');
|
|
1188
|
-
}
|
|
1189
|
-
break;
|
|
1190
|
-
case 'R':
|
|
1191
|
-
e.preventDefault();
|
|
1192
|
-
if (state && state.serverMode === 'command') {
|
|
1193
|
-
showConfirm(
|
|
1194
|
-
'Restart Server',
|
|
1195
|
-
'Restart the dev server process?',
|
|
1196
|
-
() => {
|
|
1197
|
-
sendAction('restartServer');
|
|
1198
|
-
showToast('Restarting server...', 'info');
|
|
1199
|
-
},
|
|
1200
|
-
{ label: 'Restart' }
|
|
1201
|
-
);
|
|
1202
|
-
}
|
|
1203
|
-
break;
|
|
1204
|
-
case 'c':
|
|
1205
|
-
e.preventDefault();
|
|
1206
|
-
sendAction('toggleCasino');
|
|
1207
|
-
break;
|
|
1208
|
-
case 'o':
|
|
1209
|
-
e.preventDefault();
|
|
1210
|
-
sendAction('openBrowser');
|
|
1211
|
-
showToast('Opening in browser...', 'info');
|
|
1212
|
-
break;
|
|
1213
|
-
case 'h':
|
|
1214
|
-
e.preventDefault();
|
|
1215
|
-
if (state && state.switchHistory && state.switchHistory.length > 0) {
|
|
1216
|
-
const last = state.switchHistory[0];
|
|
1217
|
-
const histMsg = 'Last: ' + last.from + ' \\u2192 ' + last.to;
|
|
1218
|
-
if (state.switchHistory.length > 1) histMsg += ' (+' + (state.switchHistory.length - 1) + ' more)';
|
|
1219
|
-
showToast(histMsg, 'info');
|
|
1220
|
-
} else {
|
|
1221
|
-
showToast('No switch history yet', 'info');
|
|
1222
|
-
}
|
|
1223
|
-
break;
|
|
1224
|
-
case 'u':
|
|
1225
|
-
e.preventDefault();
|
|
1226
|
-
sendAction('undo');
|
|
1227
|
-
showToast('Undoing last switch...', 'info');
|
|
1228
|
-
break;
|
|
1229
|
-
case 's':
|
|
1230
|
-
e.preventDefault();
|
|
1231
|
-
sendAction('toggleSound');
|
|
1232
|
-
showToast(state && state.soundEnabled ? 'Sound off' : 'Sound on', 'info');
|
|
1233
|
-
break;
|
|
1234
|
-
case 'b':
|
|
1235
|
-
e.preventDefault();
|
|
1236
|
-
showBranchActions();
|
|
1237
|
-
break;
|
|
1238
|
-
case 'i':
|
|
1239
|
-
e.preventDefault();
|
|
1240
|
-
showInfo();
|
|
1241
|
-
break;
|
|
1242
|
-
case 'l':
|
|
1243
|
-
e.preventDefault();
|
|
1244
|
-
showLogViewer();
|
|
1245
|
-
break;
|
|
1246
|
-
case 'S':
|
|
1247
|
-
e.preventDefault();
|
|
1248
|
-
showStashDialog(null);
|
|
1249
|
-
break;
|
|
1250
|
-
case 'd':
|
|
1251
|
-
e.preventDefault();
|
|
1252
|
-
showCleanup();
|
|
1253
|
-
break;
|
|
1254
|
-
case 'Escape':
|
|
1255
|
-
e.preventDefault();
|
|
1256
|
-
break;
|
|
1235
|
+
// Normal mode — look up action from key map
|
|
1236
|
+
const action = KEY_MAP[e.key];
|
|
1237
|
+
if (action && KEY_ACTIONS[action]) {
|
|
1238
|
+
e.preventDefault();
|
|
1239
|
+
KEY_ACTIONS[action](e);
|
|
1257
1240
|
}
|
|
1258
1241
|
});
|
|
1259
1242
|
|
|
1260
1243
|
// Search input handler
|
|
1261
1244
|
document.getElementById('search-input').addEventListener('input', (e) => {
|
|
1262
|
-
searchQuery = e.target.value;
|
|
1263
|
-
selectedIndex = 0;
|
|
1245
|
+
ui.searchQuery = e.target.value;
|
|
1246
|
+
ui.selectedIndex = 0;
|
|
1264
1247
|
renderBranches();
|
|
1265
1248
|
});
|
|
1266
1249
|
|
|
1267
1250
|
function moveSelection(delta) {
|
|
1268
1251
|
const branches = getDisplayBranches();
|
|
1269
|
-
const newIndex = selectedIndex + delta;
|
|
1252
|
+
const newIndex = ui.selectedIndex + delta;
|
|
1270
1253
|
if (newIndex >= 0 && newIndex < branches.length) {
|
|
1271
|
-
selectedIndex = newIndex;
|
|
1254
|
+
ui.selectedIndex = newIndex;
|
|
1272
1255
|
renderBranches();
|
|
1273
1256
|
}
|
|
1274
1257
|
}
|
|
@@ -1279,7 +1262,7 @@ ${pureFnBlock}
|
|
|
1279
1262
|
if (!item) return;
|
|
1280
1263
|
const idx = parseInt(item.getAttribute('data-index'), 10);
|
|
1281
1264
|
if (isNaN(idx)) return;
|
|
1282
|
-
selectedIndex = idx;
|
|
1265
|
+
ui.selectedIndex = idx;
|
|
1283
1266
|
renderBranches();
|
|
1284
1267
|
|
|
1285
1268
|
// Double-click to switch with confirmation
|
|
@@ -1339,8 +1322,8 @@ ${pureFnBlock}
|
|
|
1339
1322
|
}
|
|
1340
1323
|
if (e.target.id === 'pin-selected-btn') {
|
|
1341
1324
|
const branches = getDisplayBranches();
|
|
1342
|
-
if (branches.length > 0 && selectedIndex < branches.length) {
|
|
1343
|
-
const bn = branches[selectedIndex].name;
|
|
1325
|
+
if (branches.length > 0 && ui.selectedIndex < branches.length) {
|
|
1326
|
+
const bn = branches[ui.selectedIndex].name;
|
|
1344
1327
|
const idx = pinnedBranches.indexOf(bn);
|
|
1345
1328
|
if (idx === -1) {
|
|
1346
1329
|
pinnedBranches.push(bn);
|