claude-remote-cli 1.9.6 → 2.0.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/README.md +4 -3
- package/dist/server/index.js +116 -18
- package/dist/server/watcher.js +3 -5
- package/package.json +1 -1
- package/public/app.js +100 -4
- package/public/index.html +9 -0
- package/public/style.css +56 -0
package/README.md
CHANGED
|
@@ -142,9 +142,10 @@ The PIN hash is stored in config under `pinHash`. To reset:
|
|
|
142
142
|
## Features
|
|
143
143
|
|
|
144
144
|
- **PIN-protected access** with rate limiting
|
|
145
|
-
- **
|
|
145
|
+
- **Branch-aware sessions** — create worktrees from new or existing branches with a type-to-search branch picker
|
|
146
|
+
- **Worktree isolation** — each session runs in its own git worktree under `.worktrees/`
|
|
146
147
|
- **Resume sessions** — click inactive worktrees to reconnect with `--continue`
|
|
147
|
-
- **Persistent session names** — display names and timestamps survive server restarts
|
|
148
|
+
- **Persistent session names** — display names, branch names, and timestamps survive server restarts
|
|
148
149
|
- **Clipboard image paste** — paste screenshots directly into remote terminal sessions (macOS clipboard + xclip on Linux)
|
|
149
150
|
- **Yolo mode** — skip permission prompts with `--dangerously-skip-permissions` (per-session checkbox or context menu)
|
|
150
151
|
- **Worktree cleanup** — delete inactive worktrees from the context menu (removes worktree, prunes refs, deletes branch)
|
|
@@ -169,7 +170,7 @@ claude-remote-cli/
|
|
|
169
170
|
│ ├── index.ts # Express server, REST API routes
|
|
170
171
|
│ ├── sessions.ts # PTY session manager (node-pty)
|
|
171
172
|
│ ├── ws.ts # WebSocket relay (PTY ↔ browser)
|
|
172
|
-
│ ├── watcher.ts # File watcher for .
|
|
173
|
+
│ ├── watcher.ts # File watcher for .worktrees/ changes
|
|
173
174
|
│ ├── auth.ts # PIN hashing, verification, rate limiting
|
|
174
175
|
│ ├── config.ts # Config loading/saving, worktree metadata
|
|
175
176
|
│ ├── clipboard.ts # System clipboard operations (image paste)
|
package/dist/server/index.js
CHANGED
|
@@ -109,6 +109,24 @@ function scanAllRepos(rootDirs) {
|
|
|
109
109
|
}
|
|
110
110
|
return repos;
|
|
111
111
|
}
|
|
112
|
+
function ensureGitignore(repoPath, entry) {
|
|
113
|
+
const gitignorePath = path.join(repoPath, '.gitignore');
|
|
114
|
+
try {
|
|
115
|
+
if (fs.existsSync(gitignorePath)) {
|
|
116
|
+
const content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
117
|
+
if (content.split('\n').some((line) => line.trim() === entry))
|
|
118
|
+
return;
|
|
119
|
+
const prefix = content.length > 0 && !content.endsWith('\n') ? '\n' : '';
|
|
120
|
+
fs.appendFileSync(gitignorePath, prefix + entry + '\n');
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
fs.writeFileSync(gitignorePath, entry + '\n');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (_) {
|
|
127
|
+
// Non-fatal: gitignore update failure shouldn't block session creation
|
|
128
|
+
}
|
|
129
|
+
}
|
|
112
130
|
async function main() {
|
|
113
131
|
let config;
|
|
114
132
|
try {
|
|
@@ -193,6 +211,27 @@ async function main() {
|
|
|
193
211
|
}
|
|
194
212
|
res.json(repos);
|
|
195
213
|
});
|
|
214
|
+
// GET /branches?repo=<path> — list local and remote branches for a repo
|
|
215
|
+
app.get('/branches', requireAuth, async (req, res) => {
|
|
216
|
+
const repoPath = typeof req.query.repo === 'string' ? req.query.repo : undefined;
|
|
217
|
+
if (!repoPath) {
|
|
218
|
+
res.status(400).json({ error: 'repo query parameter is required' });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const { stdout } = await execFileAsync('git', ['branch', '-a', '--format=%(refname:short)'], { cwd: repoPath });
|
|
223
|
+
const branches = stdout
|
|
224
|
+
.split('\n')
|
|
225
|
+
.map((b) => b.trim())
|
|
226
|
+
.filter((b) => b && !b.includes('HEAD'))
|
|
227
|
+
.map((b) => b.replace(/^origin\//, ''));
|
|
228
|
+
const unique = [...new Set(branches)];
|
|
229
|
+
res.json(unique.sort());
|
|
230
|
+
}
|
|
231
|
+
catch (_) {
|
|
232
|
+
res.json([]);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
196
235
|
// GET /worktrees?repo=<path> — list worktrees; omit repo to scan all repos in all rootDirs
|
|
197
236
|
app.get('/worktrees', requireAuth, (req, res) => {
|
|
198
237
|
const repoParam = typeof req.query.repo === 'string' ? req.query.repo : undefined;
|
|
@@ -207,7 +246,7 @@ async function main() {
|
|
|
207
246
|
reposToScan = scanAllRepos(roots);
|
|
208
247
|
}
|
|
209
248
|
for (const repo of reposToScan) {
|
|
210
|
-
const worktreeDir = path.join(repo.path, '.
|
|
249
|
+
const worktreeDir = path.join(repo.path, '.worktrees');
|
|
211
250
|
let entries;
|
|
212
251
|
try {
|
|
213
252
|
entries = fs.readdirSync(worktreeDir, { withFileTypes: true });
|
|
@@ -276,9 +315,9 @@ async function main() {
|
|
|
276
315
|
res.status(400).json({ error: 'worktreePath and repoPath are required' });
|
|
277
316
|
return;
|
|
278
317
|
}
|
|
279
|
-
// Validate the path is inside a .
|
|
280
|
-
if (!worktreePath.includes(path.sep + '.
|
|
281
|
-
res.status(400).json({ error: 'Path is not inside a .
|
|
318
|
+
// Validate the path is inside a .worktrees/ directory
|
|
319
|
+
if (!worktreePath.includes(path.sep + '.worktrees' + path.sep)) {
|
|
320
|
+
res.status(400).json({ error: 'Path is not inside a .worktrees/ directory' });
|
|
282
321
|
return;
|
|
283
322
|
}
|
|
284
323
|
// Check no active session is using this worktree
|
|
@@ -288,14 +327,16 @@ async function main() {
|
|
|
288
327
|
res.status(409).json({ error: 'Close the active session first' });
|
|
289
328
|
return;
|
|
290
329
|
}
|
|
291
|
-
// Derive branch name from worktree directory name
|
|
292
|
-
const
|
|
330
|
+
// Derive branch name from metadata or worktree directory name
|
|
331
|
+
const meta = readMeta(CONFIG_PATH, worktreePath);
|
|
332
|
+
const branchName = (meta && meta.branchName) || worktreePath.split('/').pop() || '';
|
|
293
333
|
try {
|
|
294
|
-
//
|
|
334
|
+
// Will fail if uncommitted changes -- no --force
|
|
295
335
|
await execFileAsync('git', ['worktree', 'remove', worktreePath], { cwd: repoPath });
|
|
296
336
|
}
|
|
297
337
|
catch (err) {
|
|
298
|
-
const
|
|
338
|
+
const execErr = err;
|
|
339
|
+
const message = (execErr.stderr || execErr.message || 'Failed to remove worktree').trim();
|
|
299
340
|
res.status(500).json({ error: message });
|
|
300
341
|
return;
|
|
301
342
|
}
|
|
@@ -318,8 +359,8 @@ async function main() {
|
|
|
318
359
|
res.json({ ok: true });
|
|
319
360
|
});
|
|
320
361
|
// POST /sessions
|
|
321
|
-
app.post('/sessions', requireAuth, (req, res) => {
|
|
322
|
-
const { repoPath, repoName, worktreePath, claudeArgs } = req.body;
|
|
362
|
+
app.post('/sessions', requireAuth, async (req, res) => {
|
|
363
|
+
const { repoPath, repoName, worktreePath, branchName, claudeArgs } = req.body;
|
|
323
364
|
if (!repoPath) {
|
|
324
365
|
res.status(400).json({ error: 'repoPath is required' });
|
|
325
366
|
return;
|
|
@@ -333,32 +374,89 @@ async function main() {
|
|
|
333
374
|
let cwd;
|
|
334
375
|
let worktreeName;
|
|
335
376
|
let sessionRepoPath;
|
|
377
|
+
let resolvedBranch = '';
|
|
336
378
|
if (worktreePath) {
|
|
337
|
-
// Resume existing worktree
|
|
379
|
+
// Resume existing worktree
|
|
338
380
|
args = ['--continue', ...baseArgs];
|
|
339
381
|
cwd = worktreePath;
|
|
340
382
|
sessionRepoPath = worktreePath;
|
|
341
383
|
worktreeName = worktreePath.split('/').pop() || '';
|
|
342
384
|
}
|
|
343
385
|
else {
|
|
344
|
-
//
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
386
|
+
// Create new worktree via git
|
|
387
|
+
let dirName;
|
|
388
|
+
if (branchName) {
|
|
389
|
+
dirName = branchName.replace(/\//g, '-');
|
|
390
|
+
resolvedBranch = branchName;
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
dirName = 'mobile-' + name + '-' + Date.now().toString(36);
|
|
394
|
+
resolvedBranch = dirName;
|
|
395
|
+
}
|
|
396
|
+
const worktreeDir = path.join(repoPath, '.worktrees');
|
|
397
|
+
let targetDir = path.join(worktreeDir, dirName);
|
|
398
|
+
if (fs.existsSync(targetDir)) {
|
|
399
|
+
targetDir = targetDir + '-' + Date.now().toString(36);
|
|
400
|
+
dirName = path.basename(targetDir);
|
|
401
|
+
}
|
|
402
|
+
ensureGitignore(repoPath, '.worktrees/');
|
|
403
|
+
try {
|
|
404
|
+
// Check if branch exists locally or on a remote
|
|
405
|
+
let branchExists = false;
|
|
406
|
+
if (branchName) {
|
|
407
|
+
const localCheck = await execFileAsync('git', ['rev-parse', '--verify', branchName], { cwd: repoPath }).then(() => true, () => false);
|
|
408
|
+
if (localCheck) {
|
|
409
|
+
branchExists = true;
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
const remoteCheck = await execFileAsync('git', ['rev-parse', '--verify', 'origin/' + branchName], { cwd: repoPath }).then(() => true, () => false);
|
|
413
|
+
if (remoteCheck) {
|
|
414
|
+
branchExists = true;
|
|
415
|
+
resolvedBranch = 'origin/' + branchName;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (branchName && branchExists) {
|
|
420
|
+
await execFileAsync('git', ['worktree', 'add', targetDir, resolvedBranch], { cwd: repoPath });
|
|
421
|
+
}
|
|
422
|
+
else if (branchName) {
|
|
423
|
+
await execFileAsync('git', ['worktree', 'add', '-b', branchName, targetDir, 'HEAD'], { cwd: repoPath });
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
await execFileAsync('git', ['worktree', 'add', '-b', dirName, targetDir, 'HEAD'], { cwd: repoPath });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
const execErr = err;
|
|
431
|
+
const message = (execErr.stderr || execErr.message || 'Failed to create worktree').trim();
|
|
432
|
+
res.status(500).json({ error: message });
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
worktreeName = dirName;
|
|
436
|
+
sessionRepoPath = targetDir;
|
|
437
|
+
cwd = targetDir;
|
|
438
|
+
args = [...baseArgs];
|
|
350
439
|
}
|
|
440
|
+
const displayName = branchName || worktreeName;
|
|
351
441
|
const session = sessions.create({
|
|
352
442
|
repoName: name,
|
|
353
443
|
repoPath: sessionRepoPath,
|
|
354
444
|
cwd,
|
|
355
445
|
root,
|
|
356
446
|
worktreeName,
|
|
357
|
-
displayName
|
|
447
|
+
displayName,
|
|
358
448
|
command: config.claudeCommand,
|
|
359
449
|
args,
|
|
360
450
|
configPath: CONFIG_PATH,
|
|
361
451
|
});
|
|
452
|
+
if (!worktreePath) {
|
|
453
|
+
writeMeta(CONFIG_PATH, {
|
|
454
|
+
worktreePath: sessionRepoPath,
|
|
455
|
+
displayName,
|
|
456
|
+
lastActivity: new Date().toISOString(),
|
|
457
|
+
branchName: branchName || worktreeName,
|
|
458
|
+
});
|
|
459
|
+
}
|
|
362
460
|
res.status(201).json(session);
|
|
363
461
|
});
|
|
364
462
|
// DELETE /sessions/:id
|
package/dist/server/watcher.js
CHANGED
|
@@ -30,15 +30,13 @@ export class WorktreeWatcher extends EventEmitter {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
_watchRepo(repoPath) {
|
|
33
|
-
const worktreeDir = path.join(repoPath, '.
|
|
33
|
+
const worktreeDir = path.join(repoPath, '.worktrees');
|
|
34
34
|
if (fs.existsSync(worktreeDir)) {
|
|
35
35
|
this._addWatch(worktreeDir);
|
|
36
36
|
}
|
|
37
37
|
else {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
this._addWatch(claudeDir);
|
|
41
|
-
}
|
|
38
|
+
// Watch the repo root so we detect when .worktrees/ is first created
|
|
39
|
+
this._addWatch(repoPath);
|
|
42
40
|
}
|
|
43
41
|
}
|
|
44
42
|
_addWatch(dirPath) {
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
var dialogRootSelect = document.getElementById('dialog-root-select');
|
|
38
38
|
var dialogRepoSelect = document.getElementById('dialog-repo-select');
|
|
39
39
|
var dialogYolo = document.getElementById('dialog-yolo');
|
|
40
|
+
var dialogBranchInput = document.getElementById('dialog-branch-input');
|
|
41
|
+
var dialogBranchList = document.getElementById('dialog-branch-list');
|
|
40
42
|
var contextMenu = document.getElementById('context-menu');
|
|
41
43
|
var ctxResumeYolo = document.getElementById('ctx-resume-yolo');
|
|
42
44
|
var ctxDeleteWorktree = document.getElementById('ctx-delete-worktree');
|
|
@@ -89,8 +91,82 @@
|
|
|
89
91
|
var cachedSessions = [];
|
|
90
92
|
var cachedWorktrees = [];
|
|
91
93
|
var allRepos = [];
|
|
94
|
+
var allBranches = [];
|
|
92
95
|
var attentionSessions = {};
|
|
93
96
|
|
|
97
|
+
function loadBranches(repoPath) {
|
|
98
|
+
allBranches = [];
|
|
99
|
+
dialogBranchList.innerHTML = '';
|
|
100
|
+
dialogBranchList.hidden = true;
|
|
101
|
+
if (!repoPath) return;
|
|
102
|
+
|
|
103
|
+
fetch('/branches?repo=' + encodeURIComponent(repoPath))
|
|
104
|
+
.then(function (res) {
|
|
105
|
+
if (!res.ok) return [];
|
|
106
|
+
return res.json();
|
|
107
|
+
})
|
|
108
|
+
.then(function (data) {
|
|
109
|
+
allBranches = data || [];
|
|
110
|
+
})
|
|
111
|
+
.catch(function () {
|
|
112
|
+
allBranches = [];
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function filterBranches(query) {
|
|
117
|
+
dialogBranchList.innerHTML = '';
|
|
118
|
+
if (!query) {
|
|
119
|
+
dialogBranchList.hidden = true;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
var lower = query.toLowerCase();
|
|
124
|
+
var matches = allBranches.filter(function (b) {
|
|
125
|
+
return b.toLowerCase().indexOf(lower) !== -1;
|
|
126
|
+
}).slice(0, 10);
|
|
127
|
+
|
|
128
|
+
var exactMatch = allBranches.some(function (b) { return b === query; });
|
|
129
|
+
|
|
130
|
+
if (!exactMatch) {
|
|
131
|
+
var createLi = document.createElement('li');
|
|
132
|
+
createLi.className = 'branch-create-new';
|
|
133
|
+
createLi.textContent = 'Create new: ' + query;
|
|
134
|
+
createLi.addEventListener('click', function () {
|
|
135
|
+
dialogBranchInput.value = query;
|
|
136
|
+
dialogBranchList.hidden = true;
|
|
137
|
+
});
|
|
138
|
+
dialogBranchList.appendChild(createLi);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
matches.forEach(function (branch) {
|
|
142
|
+
var li = document.createElement('li');
|
|
143
|
+
li.textContent = branch;
|
|
144
|
+
li.addEventListener('click', function () {
|
|
145
|
+
dialogBranchInput.value = branch;
|
|
146
|
+
dialogBranchList.hidden = true;
|
|
147
|
+
});
|
|
148
|
+
dialogBranchList.appendChild(li);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
dialogBranchList.hidden = dialogBranchList.children.length === 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
dialogBranchInput.addEventListener('input', function () {
|
|
155
|
+
filterBranches(dialogBranchInput.value.trim());
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
dialogBranchInput.addEventListener('focus', function () {
|
|
159
|
+
if (dialogBranchInput.value.trim()) {
|
|
160
|
+
filterBranches(dialogBranchInput.value.trim());
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
document.addEventListener('click', function (e) {
|
|
165
|
+
if (!dialogBranchInput.contains(e.target) && !dialogBranchList.contains(e.target)) {
|
|
166
|
+
dialogBranchList.hidden = true;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
94
170
|
// ── PIN Auth ────────────────────────────────────────────────────────────────
|
|
95
171
|
|
|
96
172
|
function submitPin() {
|
|
@@ -917,6 +993,8 @@
|
|
|
917
993
|
dialogRootSelect.addEventListener('change', function () {
|
|
918
994
|
var root = dialogRootSelect.value;
|
|
919
995
|
dialogRepoSelect.innerHTML = '<option value="">Select a repo...</option>';
|
|
996
|
+
dialogBranchInput.value = '';
|
|
997
|
+
allBranches = [];
|
|
920
998
|
|
|
921
999
|
if (!root) {
|
|
922
1000
|
dialogRepoSelect.disabled = true;
|
|
@@ -934,13 +1012,20 @@
|
|
|
934
1012
|
dialogRepoSelect.disabled = false;
|
|
935
1013
|
});
|
|
936
1014
|
|
|
937
|
-
|
|
1015
|
+
dialogRepoSelect.addEventListener('change', function () {
|
|
1016
|
+
var repoPath = dialogRepoSelect.value;
|
|
1017
|
+
dialogBranchInput.value = '';
|
|
1018
|
+
loadBranches(repoPath);
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
function startSession(repoPath, worktreePath, claudeArgs, branchName) {
|
|
938
1022
|
var body = {
|
|
939
1023
|
repoPath: repoPath,
|
|
940
1024
|
repoName: repoPath.split('/').filter(Boolean).pop(),
|
|
941
1025
|
};
|
|
942
1026
|
if (worktreePath) body.worktreePath = worktreePath;
|
|
943
1027
|
if (claudeArgs) body.claudeArgs = claudeArgs;
|
|
1028
|
+
if (branchName) body.branchName = branchName;
|
|
944
1029
|
|
|
945
1030
|
fetch('/sessions', {
|
|
946
1031
|
method: 'POST',
|
|
@@ -961,6 +1046,9 @@
|
|
|
961
1046
|
newSessionBtn.addEventListener('click', function () {
|
|
962
1047
|
customPath.value = '';
|
|
963
1048
|
dialogYolo.checked = false;
|
|
1049
|
+
dialogBranchInput.value = '';
|
|
1050
|
+
dialogBranchList.hidden = true;
|
|
1051
|
+
allBranches = [];
|
|
964
1052
|
populateDialogRootSelect();
|
|
965
1053
|
|
|
966
1054
|
var sidebarRoot = sidebarRootFilter.value;
|
|
@@ -985,10 +1073,18 @@
|
|
|
985
1073
|
});
|
|
986
1074
|
|
|
987
1075
|
dialogStart.addEventListener('click', function () {
|
|
988
|
-
var
|
|
989
|
-
if (!
|
|
1076
|
+
var repoPathValue = customPath.value.trim() || dialogRepoSelect.value;
|
|
1077
|
+
if (!repoPathValue) return;
|
|
990
1078
|
var args = dialogYolo.checked ? ['--dangerously-skip-permissions'] : undefined;
|
|
991
|
-
|
|
1079
|
+
var branch = dialogBranchInput.value.trim() || undefined;
|
|
1080
|
+
startSession(repoPathValue, undefined, args, branch);
|
|
1081
|
+
});
|
|
1082
|
+
|
|
1083
|
+
customPath.addEventListener('blur', function () {
|
|
1084
|
+
var pathValue = customPath.value.trim();
|
|
1085
|
+
if (pathValue) {
|
|
1086
|
+
loadBranches(pathValue);
|
|
1087
|
+
}
|
|
992
1088
|
});
|
|
993
1089
|
|
|
994
1090
|
dialogCancel.addEventListener('click', function () {
|
package/public/index.html
CHANGED
|
@@ -131,6 +131,15 @@
|
|
|
131
131
|
<select id="dialog-repo-select" disabled><option value="">Select a repo...</option></select>
|
|
132
132
|
</div>
|
|
133
133
|
|
|
134
|
+
<div class="dialog-field">
|
|
135
|
+
<label for="dialog-branch-input">Branch</label>
|
|
136
|
+
<div class="branch-input-wrapper">
|
|
137
|
+
<input type="text" id="dialog-branch-input" placeholder="Search or create branch..." autocomplete="off" />
|
|
138
|
+
<ul id="dialog-branch-list" class="branch-dropdown" hidden></ul>
|
|
139
|
+
</div>
|
|
140
|
+
<span class="dialog-option-hint">Leave empty for auto-generated name</span>
|
|
141
|
+
</div>
|
|
142
|
+
|
|
134
143
|
<hr class="dialog-separator" />
|
|
135
144
|
<div class="dialog-custom-path">
|
|
136
145
|
<label for="custom-path-input">Or enter a local path:</label>
|
package/public/style.css
CHANGED
|
@@ -618,6 +618,62 @@ dialog#new-session-dialog h2 {
|
|
|
618
618
|
cursor: not-allowed;
|
|
619
619
|
}
|
|
620
620
|
|
|
621
|
+
/* ===== Branch Input ===== */
|
|
622
|
+
.branch-input-wrapper {
|
|
623
|
+
position: relative;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
.branch-input-wrapper input {
|
|
627
|
+
width: 100%;
|
|
628
|
+
padding: 10px 12px;
|
|
629
|
+
background: var(--bg);
|
|
630
|
+
border: 1px solid var(--border);
|
|
631
|
+
border-radius: 6px;
|
|
632
|
+
color: var(--text);
|
|
633
|
+
font-size: 0.875rem;
|
|
634
|
+
outline: none;
|
|
635
|
+
-webkit-appearance: none;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.branch-input-wrapper input:focus {
|
|
639
|
+
border-color: var(--accent);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
.branch-dropdown {
|
|
643
|
+
position: absolute;
|
|
644
|
+
top: 100%;
|
|
645
|
+
left: 0;
|
|
646
|
+
right: 0;
|
|
647
|
+
max-height: 200px;
|
|
648
|
+
overflow-y: auto;
|
|
649
|
+
background: var(--bg);
|
|
650
|
+
border: 1px solid var(--border);
|
|
651
|
+
border-top: none;
|
|
652
|
+
border-radius: 0 0 6px 6px;
|
|
653
|
+
list-style: none;
|
|
654
|
+
z-index: 10;
|
|
655
|
+
margin: 0;
|
|
656
|
+
padding: 0;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
.branch-dropdown li {
|
|
660
|
+
padding: 8px 12px;
|
|
661
|
+
font-size: 0.85rem;
|
|
662
|
+
color: var(--text);
|
|
663
|
+
cursor: pointer;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.branch-dropdown li:hover,
|
|
667
|
+
.branch-dropdown li.highlighted {
|
|
668
|
+
background: var(--surface);
|
|
669
|
+
color: var(--accent);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.branch-dropdown li.branch-create-new {
|
|
673
|
+
color: var(--accent);
|
|
674
|
+
font-style: italic;
|
|
675
|
+
}
|
|
676
|
+
|
|
621
677
|
.repo-group-label {
|
|
622
678
|
font-size: 0.75rem;
|
|
623
679
|
font-weight: 600;
|