claude-remote-cli 1.1.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/index.js +110 -0
- package/package.json +1 -1
- package/public/app.js +179 -0
- package/public/index.html +27 -0
- package/public/style.css +159 -0
package/dist/server/index.js
CHANGED
|
@@ -3,6 +3,8 @@ import http from 'node:http';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import readline from 'node:readline';
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
6
8
|
import express from 'express';
|
|
7
9
|
import cookieParser from 'cookie-parser';
|
|
8
10
|
import { loadConfig, saveConfig, DEFAULTS } from './config.js';
|
|
@@ -10,11 +12,49 @@ import * as auth from './auth.js';
|
|
|
10
12
|
import * as sessions from './sessions.js';
|
|
11
13
|
import { setupWebSocket } from './ws.js';
|
|
12
14
|
import { WorktreeWatcher } from './watcher.js';
|
|
15
|
+
import { isInstalled as serviceIsInstalled } from './service.js';
|
|
13
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
17
|
const __dirname = path.dirname(__filename);
|
|
18
|
+
const execFileAsync = promisify(execFile);
|
|
15
19
|
// When run via CLI bin, config lives in ~/.config/claude-remote-cli/
|
|
16
20
|
// When run directly (development), fall back to local config.json
|
|
17
21
|
const CONFIG_PATH = process.env.CLAUDE_REMOTE_CONFIG || path.join(__dirname, '..', '..', 'config.json');
|
|
22
|
+
const VERSION_CACHE_TTL = 5 * 60 * 1000;
|
|
23
|
+
let versionCache = null;
|
|
24
|
+
function getCurrentVersion() {
|
|
25
|
+
const pkgPath = path.join(__dirname, '..', '..', 'package.json');
|
|
26
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
27
|
+
return pkg.version;
|
|
28
|
+
}
|
|
29
|
+
function semverLessThan(a, b) {
|
|
30
|
+
const parse = (v) => v.split('.').map(Number);
|
|
31
|
+
const [aMaj = 0, aMin = 0, aPat = 0] = parse(a);
|
|
32
|
+
const [bMaj = 0, bMin = 0, bPat = 0] = parse(b);
|
|
33
|
+
if (aMaj !== bMaj)
|
|
34
|
+
return aMaj < bMaj;
|
|
35
|
+
if (aMin !== bMin)
|
|
36
|
+
return aMin < bMin;
|
|
37
|
+
return aPat < bPat;
|
|
38
|
+
}
|
|
39
|
+
async function getLatestVersion() {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
if (versionCache && now - versionCache.fetchedAt < VERSION_CACHE_TTL) {
|
|
42
|
+
return versionCache.latest;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetch('https://registry.npmjs.org/claude-remote-cli/latest');
|
|
46
|
+
if (!res.ok)
|
|
47
|
+
return null;
|
|
48
|
+
const data = await res.json();
|
|
49
|
+
if (!data.version)
|
|
50
|
+
return null;
|
|
51
|
+
versionCache = { latest: data.version, fetchedAt: now };
|
|
52
|
+
return data.version;
|
|
53
|
+
}
|
|
54
|
+
catch (_) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
18
58
|
function parseTTL(ttl) {
|
|
19
59
|
if (typeof ttl !== 'string')
|
|
20
60
|
return 24 * 60 * 60 * 1000;
|
|
@@ -221,6 +261,54 @@ async function main() {
|
|
|
221
261
|
broadcastEvent('worktrees-changed');
|
|
222
262
|
res.json(config.rootDirs);
|
|
223
263
|
});
|
|
264
|
+
// DELETE /worktrees — remove a worktree, prune, and delete its branch
|
|
265
|
+
app.delete('/worktrees', requireAuth, async (req, res) => {
|
|
266
|
+
const { worktreePath, repoPath } = req.body;
|
|
267
|
+
if (!worktreePath || !repoPath) {
|
|
268
|
+
res.status(400).json({ error: 'worktreePath and repoPath are required' });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// Validate the path is inside a .claude/worktrees/ directory
|
|
272
|
+
if (!worktreePath.includes(path.sep + '.claude' + path.sep + 'worktrees' + path.sep)) {
|
|
273
|
+
res.status(400).json({ error: 'Path is not inside a .claude/worktrees/ directory' });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
// Check no active session is using this worktree
|
|
277
|
+
const activeSessions = sessions.list();
|
|
278
|
+
const conflict = activeSessions.find(function (s) { return s.repoPath === worktreePath; });
|
|
279
|
+
if (conflict) {
|
|
280
|
+
res.status(409).json({ error: 'Close the active session first' });
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
// Derive branch name from worktree directory name
|
|
284
|
+
const branchName = worktreePath.split('/').pop() || '';
|
|
285
|
+
try {
|
|
286
|
+
// Remove the worktree (will fail if uncommitted changes — no --force)
|
|
287
|
+
await execFileAsync('git', ['worktree', 'remove', worktreePath], { cwd: repoPath });
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
const message = err instanceof Error ? err.message : 'Failed to remove worktree';
|
|
291
|
+
res.status(500).json({ error: message });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
// Prune stale worktree refs
|
|
296
|
+
await execFileAsync('git', ['worktree', 'prune'], { cwd: repoPath });
|
|
297
|
+
}
|
|
298
|
+
catch (_) {
|
|
299
|
+
// Non-fatal: prune failure doesn't block success
|
|
300
|
+
}
|
|
301
|
+
if (branchName) {
|
|
302
|
+
try {
|
|
303
|
+
// Delete the branch
|
|
304
|
+
await execFileAsync('git', ['branch', '-D', branchName], { cwd: repoPath });
|
|
305
|
+
}
|
|
306
|
+
catch (_) {
|
|
307
|
+
// Non-fatal: branch may not exist or may be checked out elsewhere
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
res.json({ ok: true });
|
|
311
|
+
});
|
|
224
312
|
// POST /sessions
|
|
225
313
|
app.post('/sessions', requireAuth, (req, res) => {
|
|
226
314
|
const { repoPath, repoName, worktreePath, claudeArgs } = req.body;
|
|
@@ -289,6 +377,28 @@ async function main() {
|
|
|
289
377
|
res.status(404).json({ error: 'Session not found' });
|
|
290
378
|
}
|
|
291
379
|
});
|
|
380
|
+
// GET /version — check current vs latest
|
|
381
|
+
app.get('/version', requireAuth, async (_req, res) => {
|
|
382
|
+
const current = getCurrentVersion();
|
|
383
|
+
const latest = await getLatestVersion();
|
|
384
|
+
const updateAvailable = latest !== null && semverLessThan(current, latest);
|
|
385
|
+
res.json({ current, latest, updateAvailable });
|
|
386
|
+
});
|
|
387
|
+
// POST /update — install latest version from npm
|
|
388
|
+
app.post('/update', requireAuth, async (_req, res) => {
|
|
389
|
+
try {
|
|
390
|
+
await execFileAsync('npm', ['install', '-g', 'claude-remote-cli@latest']);
|
|
391
|
+
const restarting = serviceIsInstalled();
|
|
392
|
+
res.json({ ok: true, restarting });
|
|
393
|
+
if (restarting) {
|
|
394
|
+
setTimeout(() => process.exit(0), 1000);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
const message = err instanceof Error ? err.message : 'Update failed';
|
|
399
|
+
res.status(500).json({ ok: false, error: message });
|
|
400
|
+
}
|
|
401
|
+
});
|
|
292
402
|
server.listen(config.port, config.host, () => {
|
|
293
403
|
console.log(`claude-remote-cli listening on ${config.host}:${config.port}`);
|
|
294
404
|
});
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -34,6 +34,41 @@
|
|
|
34
34
|
var sidebarRepoFilter = document.getElementById('sidebar-repo-filter');
|
|
35
35
|
var dialogRootSelect = document.getElementById('dialog-root-select');
|
|
36
36
|
var dialogRepoSelect = document.getElementById('dialog-repo-select');
|
|
37
|
+
var contextMenu = document.getElementById('context-menu');
|
|
38
|
+
var ctxDeleteWorktree = document.getElementById('ctx-delete-worktree');
|
|
39
|
+
var deleteWtDialog = document.getElementById('delete-worktree-dialog');
|
|
40
|
+
var deleteWtName = document.getElementById('delete-wt-name');
|
|
41
|
+
var deleteWtCancel = document.getElementById('delete-wt-cancel');
|
|
42
|
+
var deleteWtConfirm = document.getElementById('delete-wt-confirm');
|
|
43
|
+
var updateToast = document.getElementById('update-toast');
|
|
44
|
+
var updateToastText = document.getElementById('update-toast-text');
|
|
45
|
+
var updateToastBtn = document.getElementById('update-toast-btn');
|
|
46
|
+
var updateToastDismiss = document.getElementById('update-toast-dismiss');
|
|
47
|
+
|
|
48
|
+
// Context menu state
|
|
49
|
+
var contextMenuTarget = null; // stores { worktreePath, repoPath, name }
|
|
50
|
+
var longPressTimer = null;
|
|
51
|
+
var longPressFired = false;
|
|
52
|
+
|
|
53
|
+
function showContextMenu(x, y, wt) {
|
|
54
|
+
contextMenuTarget = { worktreePath: wt.path, repoPath: wt.repoPath, name: wt.name };
|
|
55
|
+
contextMenu.style.left = Math.min(x, window.innerWidth - 180) + 'px';
|
|
56
|
+
contextMenu.style.top = Math.min(y, window.innerHeight - 60) + 'px';
|
|
57
|
+
contextMenu.hidden = false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function hideContextMenu() {
|
|
61
|
+
contextMenu.hidden = true;
|
|
62
|
+
contextMenuTarget = null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
document.addEventListener('click', function () {
|
|
66
|
+
hideContextMenu();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
document.addEventListener('keydown', function (e) {
|
|
70
|
+
if (e.key === 'Escape') hideContextMenu();
|
|
71
|
+
});
|
|
37
72
|
|
|
38
73
|
// Session / worktree / repo state
|
|
39
74
|
var cachedSessions = [];
|
|
@@ -86,6 +121,7 @@
|
|
|
86
121
|
loadRepos();
|
|
87
122
|
refreshAll();
|
|
88
123
|
connectEventSocket();
|
|
124
|
+
checkForUpdates();
|
|
89
125
|
}
|
|
90
126
|
|
|
91
127
|
// ── Terminal ────────────────────────────────────────────────────────────────
|
|
@@ -409,10 +445,44 @@
|
|
|
409
445
|
|
|
410
446
|
li.appendChild(infoDiv);
|
|
411
447
|
|
|
448
|
+
// Click to resume (but not if context menu just opened or long-press fired)
|
|
412
449
|
li.addEventListener('click', function () {
|
|
450
|
+
if (longPressFired || !contextMenu.hidden) return;
|
|
413
451
|
startSession(wt.repoPath, wt.path);
|
|
414
452
|
});
|
|
415
453
|
|
|
454
|
+
// Right-click context menu (desktop)
|
|
455
|
+
li.addEventListener('contextmenu', function (e) {
|
|
456
|
+
e.preventDefault();
|
|
457
|
+
e.stopPropagation();
|
|
458
|
+
showContextMenu(e.clientX, e.clientY, wt);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Long-press context menu (mobile)
|
|
462
|
+
li.addEventListener('touchstart', function (e) {
|
|
463
|
+
longPressFired = false;
|
|
464
|
+
longPressTimer = setTimeout(function () {
|
|
465
|
+
longPressTimer = null;
|
|
466
|
+
longPressFired = true;
|
|
467
|
+
var touch = e.touches[0];
|
|
468
|
+
showContextMenu(touch.clientX, touch.clientY, wt);
|
|
469
|
+
}, 500);
|
|
470
|
+
}, { passive: true });
|
|
471
|
+
|
|
472
|
+
li.addEventListener('touchend', function () {
|
|
473
|
+
if (longPressTimer) {
|
|
474
|
+
clearTimeout(longPressTimer);
|
|
475
|
+
longPressTimer = null;
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
li.addEventListener('touchmove', function () {
|
|
480
|
+
if (longPressTimer) {
|
|
481
|
+
clearTimeout(longPressTimer);
|
|
482
|
+
longPressTimer = null;
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
|
|
416
486
|
return li;
|
|
417
487
|
}
|
|
418
488
|
|
|
@@ -480,6 +550,48 @@
|
|
|
480
550
|
.catch(function () {});
|
|
481
551
|
}
|
|
482
552
|
|
|
553
|
+
// ── Delete Worktree ────────────────────────────────────────────────────────
|
|
554
|
+
|
|
555
|
+
ctxDeleteWorktree.addEventListener('click', function (e) {
|
|
556
|
+
e.stopPropagation();
|
|
557
|
+
hideContextMenu();
|
|
558
|
+
if (!contextMenuTarget) return;
|
|
559
|
+
deleteWtName.textContent = contextMenuTarget.name;
|
|
560
|
+
deleteWtDialog.showModal();
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
deleteWtCancel.addEventListener('click', function () {
|
|
564
|
+
deleteWtDialog.close();
|
|
565
|
+
contextMenuTarget = null;
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
deleteWtConfirm.addEventListener('click', function () {
|
|
569
|
+
if (!contextMenuTarget) return;
|
|
570
|
+
var target = contextMenuTarget;
|
|
571
|
+
deleteWtDialog.close();
|
|
572
|
+
contextMenuTarget = null;
|
|
573
|
+
|
|
574
|
+
fetch('/worktrees', {
|
|
575
|
+
method: 'DELETE',
|
|
576
|
+
headers: { 'Content-Type': 'application/json' },
|
|
577
|
+
body: JSON.stringify({
|
|
578
|
+
worktreePath: target.worktreePath,
|
|
579
|
+
repoPath: target.repoPath,
|
|
580
|
+
}),
|
|
581
|
+
})
|
|
582
|
+
.then(function (res) {
|
|
583
|
+
if (!res.ok) {
|
|
584
|
+
return res.json().then(function (data) {
|
|
585
|
+
alert(data.error || 'Failed to delete worktree');
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
// UI will auto-update via worktrees-changed WebSocket event
|
|
589
|
+
})
|
|
590
|
+
.catch(function () {
|
|
591
|
+
alert('Failed to delete worktree');
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
483
595
|
function highlightActiveSession() {
|
|
484
596
|
var items = sessionList.querySelectorAll('li');
|
|
485
597
|
items.forEach(function (li) {
|
|
@@ -744,6 +856,73 @@
|
|
|
744
856
|
vv.addEventListener('scroll', onViewportResize);
|
|
745
857
|
})();
|
|
746
858
|
|
|
859
|
+
// ── Update Toast ─────────────────────────────────────────────────────────────
|
|
860
|
+
|
|
861
|
+
function checkForUpdates() {
|
|
862
|
+
fetch('/version')
|
|
863
|
+
.then(function (res) {
|
|
864
|
+
if (!res.ok) return;
|
|
865
|
+
return res.json();
|
|
866
|
+
})
|
|
867
|
+
.then(function (data) {
|
|
868
|
+
if (data && data.updateAvailable) {
|
|
869
|
+
showUpdateToast(data.current, data.latest);
|
|
870
|
+
}
|
|
871
|
+
})
|
|
872
|
+
.catch(function () {
|
|
873
|
+
// Silently ignore version check errors
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
function showUpdateToast(current, latest) {
|
|
878
|
+
updateToastText.textContent = 'Update available: v' + current + ' \u2192 v' + latest;
|
|
879
|
+
updateToast.hidden = false;
|
|
880
|
+
updateToastBtn.disabled = false;
|
|
881
|
+
updateToastBtn.textContent = 'Update Now';
|
|
882
|
+
|
|
883
|
+
updateToastBtn.onclick = function () {
|
|
884
|
+
triggerUpdate(latest);
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function triggerUpdate(latest) {
|
|
889
|
+
updateToastBtn.disabled = true;
|
|
890
|
+
updateToastBtn.textContent = 'Updating\u2026';
|
|
891
|
+
|
|
892
|
+
fetch('/update', { method: 'POST' })
|
|
893
|
+
.then(function (res) {
|
|
894
|
+
return res.json().then(function (data) {
|
|
895
|
+
return { ok: res.ok, data: data };
|
|
896
|
+
});
|
|
897
|
+
})
|
|
898
|
+
.then(function (result) {
|
|
899
|
+
if (result.ok && result.data.restarting) {
|
|
900
|
+
updateToastText.textContent = 'Updated! Restarting server\u2026';
|
|
901
|
+
updateToastBtn.hidden = true;
|
|
902
|
+
updateToastDismiss.hidden = true;
|
|
903
|
+
setTimeout(function () {
|
|
904
|
+
location.reload();
|
|
905
|
+
}, 5000);
|
|
906
|
+
} else if (result.ok) {
|
|
907
|
+
updateToastText.textContent = 'Updated! Please restart the server manually.';
|
|
908
|
+
updateToastBtn.hidden = true;
|
|
909
|
+
} else {
|
|
910
|
+
updateToastText.textContent = 'Update failed: ' + (result.data.error || 'Unknown error');
|
|
911
|
+
updateToastBtn.disabled = false;
|
|
912
|
+
updateToastBtn.textContent = 'Retry';
|
|
913
|
+
}
|
|
914
|
+
})
|
|
915
|
+
.catch(function () {
|
|
916
|
+
updateToastText.textContent = 'Update failed. Please try again.';
|
|
917
|
+
updateToastBtn.disabled = false;
|
|
918
|
+
updateToastBtn.textContent = 'Retry';
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
updateToastDismiss.addEventListener('click', function () {
|
|
923
|
+
updateToast.hidden = true;
|
|
924
|
+
});
|
|
925
|
+
|
|
747
926
|
// ── Auto-auth Check ─────────────────────────────────────────────────────────
|
|
748
927
|
|
|
749
928
|
fetch('/sessions')
|
package/public/index.html
CHANGED
|
@@ -78,6 +78,17 @@
|
|
|
78
78
|
</div>
|
|
79
79
|
</div>
|
|
80
80
|
|
|
81
|
+
<!-- Update Toast -->
|
|
82
|
+
<div id="update-toast" hidden>
|
|
83
|
+
<div id="update-toast-content">
|
|
84
|
+
<span id="update-toast-text"></span>
|
|
85
|
+
<div id="update-toast-actions">
|
|
86
|
+
<button id="update-toast-btn" class="btn-accent">Update Now</button>
|
|
87
|
+
<button id="update-toast-dismiss" aria-label="Dismiss">×</button>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
81
92
|
</div>
|
|
82
93
|
|
|
83
94
|
<!-- New Session Dialog -->
|
|
@@ -122,6 +133,22 @@
|
|
|
122
133
|
</div>
|
|
123
134
|
</dialog>
|
|
124
135
|
|
|
136
|
+
<!-- Context Menu -->
|
|
137
|
+
<div id="context-menu" class="context-menu" hidden>
|
|
138
|
+
<button id="ctx-delete-worktree" class="context-menu-item">Delete worktree</button>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<!-- Delete Worktree Confirmation Dialog -->
|
|
142
|
+
<dialog id="delete-worktree-dialog">
|
|
143
|
+
<h2>Delete worktree?</h2>
|
|
144
|
+
<p class="delete-wt-warning">This will remove the worktree directory and delete its branch. This cannot be undone.</p>
|
|
145
|
+
<p class="delete-wt-name" id="delete-wt-name"></p>
|
|
146
|
+
<div class="dialog-actions">
|
|
147
|
+
<button id="delete-wt-cancel">Cancel</button>
|
|
148
|
+
<button id="delete-wt-confirm" class="btn-danger">Delete</button>
|
|
149
|
+
</div>
|
|
150
|
+
</dialog>
|
|
151
|
+
|
|
125
152
|
<script src="/vendor/xterm.js"></script>
|
|
126
153
|
<script src="/vendor/addon-fit.js"></script>
|
|
127
154
|
<script src="/app.js"></script>
|
package/public/style.css
CHANGED
|
@@ -737,6 +737,165 @@ dialog#settings-dialog h2 {
|
|
|
737
737
|
opacity: 0.85;
|
|
738
738
|
}
|
|
739
739
|
|
|
740
|
+
/* ===== Context Menu ===== */
|
|
741
|
+
.context-menu {
|
|
742
|
+
position: fixed;
|
|
743
|
+
z-index: 200;
|
|
744
|
+
background: var(--surface);
|
|
745
|
+
border: 1px solid var(--border);
|
|
746
|
+
border-radius: 8px;
|
|
747
|
+
padding: 4px;
|
|
748
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
|
749
|
+
min-width: 160px;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
.context-menu-item {
|
|
753
|
+
display: block;
|
|
754
|
+
width: 100%;
|
|
755
|
+
padding: 10px 14px;
|
|
756
|
+
background: none;
|
|
757
|
+
border: none;
|
|
758
|
+
border-radius: 6px;
|
|
759
|
+
color: var(--text);
|
|
760
|
+
font-size: 0.85rem;
|
|
761
|
+
text-align: left;
|
|
762
|
+
cursor: pointer;
|
|
763
|
+
touch-action: manipulation;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.context-menu-item:hover,
|
|
767
|
+
.context-menu-item:active {
|
|
768
|
+
background: var(--bg);
|
|
769
|
+
color: var(--accent);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/* ===== Delete Worktree Dialog ===== */
|
|
773
|
+
dialog#delete-worktree-dialog {
|
|
774
|
+
background: var(--surface);
|
|
775
|
+
border: 1px solid var(--border);
|
|
776
|
+
border-radius: 12px;
|
|
777
|
+
color: var(--text);
|
|
778
|
+
padding: 1.5rem;
|
|
779
|
+
width: 90%;
|
|
780
|
+
max-width: 400px;
|
|
781
|
+
margin: auto;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
dialog#delete-worktree-dialog::backdrop {
|
|
785
|
+
background: rgba(0, 0, 0, 0.7);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
dialog#delete-worktree-dialog h2 {
|
|
789
|
+
font-size: 1.1rem;
|
|
790
|
+
margin-bottom: 0.75rem;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.delete-wt-warning {
|
|
794
|
+
font-size: 0.85rem;
|
|
795
|
+
color: var(--text-muted);
|
|
796
|
+
margin-bottom: 0.75rem;
|
|
797
|
+
line-height: 1.4;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
.delete-wt-name {
|
|
801
|
+
font-size: 0.85rem;
|
|
802
|
+
font-family: monospace;
|
|
803
|
+
color: var(--accent);
|
|
804
|
+
padding: 8px 10px;
|
|
805
|
+
background: var(--bg);
|
|
806
|
+
border-radius: 6px;
|
|
807
|
+
margin-bottom: 1rem;
|
|
808
|
+
word-break: break-all;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.btn-danger {
|
|
812
|
+
background: #c0392b !important;
|
|
813
|
+
border-color: #c0392b !important;
|
|
814
|
+
color: #fff !important;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
.btn-danger:active {
|
|
818
|
+
opacity: 0.85;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/* ===== Update Toast ===== */
|
|
822
|
+
|
|
823
|
+
#update-toast {
|
|
824
|
+
position: fixed;
|
|
825
|
+
bottom: 0;
|
|
826
|
+
left: 0;
|
|
827
|
+
right: 0;
|
|
828
|
+
z-index: 150;
|
|
829
|
+
display: flex;
|
|
830
|
+
justify-content: center;
|
|
831
|
+
padding: 12px 12px calc(12px + env(safe-area-inset-bottom));
|
|
832
|
+
pointer-events: none;
|
|
833
|
+
animation: toast-slide-up 0.25s ease-out;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
@keyframes toast-slide-up {
|
|
837
|
+
from {
|
|
838
|
+
transform: translateY(100%);
|
|
839
|
+
opacity: 0;
|
|
840
|
+
}
|
|
841
|
+
to {
|
|
842
|
+
transform: translateY(0);
|
|
843
|
+
opacity: 1;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
#update-toast-content {
|
|
848
|
+
display: flex;
|
|
849
|
+
flex-direction: row;
|
|
850
|
+
align-items: center;
|
|
851
|
+
gap: 12px;
|
|
852
|
+
background: var(--surface);
|
|
853
|
+
border: 1px solid var(--border);
|
|
854
|
+
border-radius: 10px;
|
|
855
|
+
padding: 12px 16px;
|
|
856
|
+
max-width: 500px;
|
|
857
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
|
|
858
|
+
pointer-events: auto;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
#update-toast-text {
|
|
862
|
+
flex: 1;
|
|
863
|
+
font-size: 0.85rem;
|
|
864
|
+
color: var(--text);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
#update-toast-actions {
|
|
868
|
+
display: flex;
|
|
869
|
+
gap: 8px;
|
|
870
|
+
flex-shrink: 0;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
#update-toast-btn {
|
|
874
|
+
padding: 8px 14px;
|
|
875
|
+
border-radius: 6px;
|
|
876
|
+
font-size: 0.8rem;
|
|
877
|
+
border: none;
|
|
878
|
+
white-space: nowrap;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
#update-toast-btn:disabled {
|
|
882
|
+
opacity: 0.6;
|
|
883
|
+
cursor: not-allowed;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
#update-toast-dismiss {
|
|
887
|
+
background: none;
|
|
888
|
+
border: none;
|
|
889
|
+
color: var(--text-muted);
|
|
890
|
+
font-size: 1.2rem;
|
|
891
|
+
padding: 4px 6px;
|
|
892
|
+
cursor: pointer;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
#update-toast-dismiss:hover {
|
|
896
|
+
color: var(--text);
|
|
897
|
+
}
|
|
898
|
+
|
|
740
899
|
/* ===== Mobile Responsive ===== */
|
|
741
900
|
@media (max-width: 600px) {
|
|
742
901
|
#mobile-header {
|