claude-remote-cli 2.1.0 → 2.2.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 CHANGED
@@ -142,8 +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
- - **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/`
145
+ - **Repo sessions** — open Claude directly in any repo root, with fresh or `--continue` mode (one session per repo)
146
+ - **Branch-aware worktrees** — create worktrees from new or existing branches with a type-to-search branch picker
147
+ - **Tabbed sidebar** — switch between Repos and Worktrees views with shared filters and item counts
148
+ - **Worktree isolation** — each worktree session runs in its own git worktree under `.worktrees/`
147
149
  - **Resume sessions** — click inactive worktrees to reconnect with `--continue`
148
150
  - **Persistent session names** — display names, branch names, and timestamps survive server restarts
149
151
  - **Clipboard image paste** — paste screenshots directly into remote terminal sessions (macOS clipboard + xclip on Linux)
@@ -440,6 +440,7 @@ async function main() {
440
440
  }
441
441
  const displayName = branchName || worktreeName;
442
442
  const session = sessions.create({
443
+ type: 'worktree',
443
444
  repoName: name,
444
445
  repoPath: sessionRepoPath,
445
446
  cwd,
@@ -460,6 +461,36 @@ async function main() {
460
461
  }
461
462
  res.status(201).json(session);
462
463
  });
464
+ // POST /sessions/repo — start a session in the repo root (no worktree)
465
+ app.post('/sessions/repo', requireAuth, (req, res) => {
466
+ const { repoPath, repoName, continue: continueSession, claudeArgs } = req.body;
467
+ if (!repoPath) {
468
+ res.status(400).json({ error: 'repoPath is required' });
469
+ return;
470
+ }
471
+ // One repo session at a time
472
+ const existing = sessions.findRepoSession(repoPath);
473
+ if (existing) {
474
+ res.status(409).json({ error: 'A session already exists for this repo', sessionId: existing.id });
475
+ return;
476
+ }
477
+ const name = repoName || repoPath.split('/').filter(Boolean).pop() || 'session';
478
+ const baseArgs = [...(config.claudeArgs || []), ...(claudeArgs || [])];
479
+ const args = continueSession ? ['--continue', ...baseArgs] : [...baseArgs];
480
+ const roots = config.rootDirs || [];
481
+ const root = roots.find(function (r) { return repoPath.startsWith(r); }) || '';
482
+ const session = sessions.create({
483
+ type: 'repo',
484
+ repoName: name,
485
+ repoPath,
486
+ cwd: repoPath,
487
+ root,
488
+ displayName: name,
489
+ command: config.claudeCommand,
490
+ args,
491
+ });
492
+ res.status(201).json(session);
493
+ });
463
494
  // DELETE /sessions/:id
464
495
  app.delete('/sessions/:id', requireAuth, (req, res) => {
465
496
  try {
@@ -11,7 +11,7 @@ let idleChangeCallback = null;
11
11
  function onIdleChange(cb) {
12
12
  idleChangeCallback = cb;
13
13
  }
14
- function create({ repoName, repoPath, cwd, root, worktreeName, displayName, command, args = [], cols = 80, rows = 24, configPath }) {
14
+ function create({ type, repoName, repoPath, cwd, root, worktreeName, displayName, command, args = [], cols = 80, rows = 24, configPath }) {
15
15
  const id = crypto.randomBytes(8).toString('hex');
16
16
  const createdAt = new Date().toISOString();
17
17
  // Strip CLAUDECODE env var to allow spawning claude inside a claude-managed server
@@ -30,6 +30,7 @@ function create({ repoName, repoPath, cwd, root, worktreeName, displayName, comm
30
30
  const MAX_SCROLLBACK = 256 * 1024; // 256KB max
31
31
  const session = {
32
32
  id,
33
+ type: type || 'worktree',
33
34
  root: root || '',
34
35
  repoName: repoName || '',
35
36
  repoPath,
@@ -96,15 +97,16 @@ function create({ repoName, repoPath, cwd, root, worktreeName, displayName, comm
96
97
  const tmpDir = path.join(os.tmpdir(), 'claude-remote-cli', id);
97
98
  fs.rm(tmpDir, { recursive: true, force: true }, () => { });
98
99
  });
99
- return { id, root: session.root, repoName: session.repoName, repoPath, worktreeName: session.worktreeName, displayName: session.displayName, pid: ptyProcess.pid, createdAt, lastActivity: createdAt, idle: false };
100
+ return { id, type: session.type, root: session.root, repoName: session.repoName, repoPath, worktreeName: session.worktreeName, displayName: session.displayName, pid: ptyProcess.pid, createdAt, lastActivity: createdAt, idle: false };
100
101
  }
101
102
  function get(id) {
102
103
  return sessions.get(id);
103
104
  }
104
105
  function list() {
105
106
  return Array.from(sessions.values())
106
- .map(({ id, root, repoName, repoPath, worktreeName, displayName, createdAt, lastActivity, idle }) => ({
107
+ .map(({ id, type, root, repoName, repoPath, worktreeName, displayName, createdAt, lastActivity, idle }) => ({
107
108
  id,
109
+ type,
108
110
  root,
109
111
  repoName,
110
112
  repoPath,
@@ -145,4 +147,7 @@ function write(id, data) {
145
147
  }
146
148
  session.pty.write(data);
147
149
  }
148
- export { create, get, list, kill, resize, updateDisplayName, write, onIdleChange };
150
+ function findRepoSession(repoPath) {
151
+ return list().find((s) => s.type === 'repo' && s.repoPath === repoPath);
152
+ }
153
+ export { create, get, list, kill, resize, updateDisplayName, write, onIdleChange, findRepoSession };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-remote-cli",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Remote web interface for Claude Code CLI sessions",
5
5
  "type": "module",
6
6
  "main": "dist/server/index.js",
package/public/app.js CHANGED
@@ -39,6 +39,8 @@
39
39
  var dialogYolo = document.getElementById('dialog-yolo');
40
40
  var dialogBranchInput = document.getElementById('dialog-branch-input');
41
41
  var dialogBranchList = document.getElementById('dialog-branch-list');
42
+ var dialogContinue = document.getElementById('dialog-continue');
43
+ var dialogContinueField = document.getElementById('dialog-continue-field');
42
44
  var contextMenu = document.getElementById('context-menu');
43
45
  var ctxResumeYolo = document.getElementById('ctx-resume-yolo');
44
46
  var ctxDeleteWorktree = document.getElementById('ctx-delete-worktree');
@@ -60,6 +62,10 @@
60
62
  var terminalScrollbarThumb = document.getElementById('terminal-scrollbar-thumb');
61
63
  var mobileInput = document.getElementById('mobile-input');
62
64
  var mobileHeader = document.getElementById('mobile-header');
65
+ var sidebarTabs = document.querySelectorAll('.sidebar-tab');
66
+ var tabReposCount = document.getElementById('tab-repos-count');
67
+ var tabWorktreesCount = document.getElementById('tab-worktrees-count');
68
+ var activeTab = 'repos';
63
69
  var isMobileDevice = 'ontouchstart' in window;
64
70
 
65
71
  // Context menu state
@@ -92,6 +98,7 @@
92
98
  var cachedWorktrees = [];
93
99
  var allRepos = [];
94
100
  var allBranches = [];
101
+ var cachedRepos = [];
95
102
  var attentionSessions = {};
96
103
 
97
104
  function loadBranches(repoPath) {
@@ -523,10 +530,12 @@
523
530
  Promise.all([
524
531
  fetch('/sessions').then(function (res) { return res.json(); }),
525
532
  fetch('/worktrees').then(function (res) { return res.json(); }),
533
+ fetch('/repos').then(function (res) { return res.json(); }),
526
534
  ])
527
535
  .then(function (results) {
528
536
  cachedSessions = results[0] || [];
529
537
  cachedWorktrees = results[1] || [];
538
+ cachedRepos = results[2] || [];
530
539
 
531
540
  // Prune attention flags for sessions that no longer exist
532
541
  var activeIds = {};
@@ -606,6 +615,16 @@
606
615
  renderUnifiedList();
607
616
  });
608
617
 
618
+ sidebarTabs.forEach(function (tab) {
619
+ tab.addEventListener('click', function () {
620
+ activeTab = tab.dataset.tab;
621
+ sidebarTabs.forEach(function (t) { t.classList.remove('active'); });
622
+ tab.classList.add('active');
623
+ newSessionBtn.textContent = activeTab === 'repos' ? '+ New Session' : '+ New Worktree';
624
+ renderUnifiedList();
625
+ });
626
+ });
627
+
609
628
  function rootShortName(path) {
610
629
  return path.split('/').filter(Boolean).pop() || path;
611
630
  }
@@ -633,7 +652,42 @@
633
652
  var repoFilter = sidebarRepoFilter.value;
634
653
  var textFilter = sessionFilter.value.toLowerCase();
635
654
 
636
- var filteredSessions = cachedSessions.filter(function (s) {
655
+ // Split sessions by type
656
+ var repoSessions = cachedSessions.filter(function (s) { return s.type === 'repo'; });
657
+ var worktreeSessions = cachedSessions.filter(function (s) { return s.type !== 'repo'; });
658
+
659
+ // Filtered repo sessions
660
+ var filteredRepoSessions = repoSessions.filter(function (s) {
661
+ if (rootFilter && s.root !== rootFilter) return false;
662
+ if (repoFilter && s.repoName !== repoFilter) return false;
663
+ if (textFilter) {
664
+ var name = (s.displayName || s.repoName || s.id).toLowerCase();
665
+ if (name.indexOf(textFilter) === -1) return false;
666
+ }
667
+ return true;
668
+ });
669
+
670
+ // Idle repos: all repos without an active repo session
671
+ var activeRepoPathSet = new Set();
672
+ repoSessions.forEach(function (s) { activeRepoPathSet.add(s.repoPath); });
673
+
674
+ var filteredIdleRepos = cachedRepos.filter(function (r) {
675
+ if (activeRepoPathSet.has(r.path)) return false;
676
+ if (rootFilter && r.root !== rootFilter) return false;
677
+ if (repoFilter && r.name !== repoFilter) return false;
678
+ if (textFilter) {
679
+ var name = (r.name || '').toLowerCase();
680
+ if (name.indexOf(textFilter) === -1) return false;
681
+ }
682
+ return true;
683
+ });
684
+
685
+ filteredIdleRepos.sort(function (a, b) {
686
+ return (a.name || '').localeCompare(b.name || '');
687
+ });
688
+
689
+ // Filtered worktree sessions
690
+ var filteredWorktreeSessions = worktreeSessions.filter(function (s) {
637
691
  if (rootFilter && s.root !== rootFilter) return false;
638
692
  if (repoFilter && s.repoName !== repoFilter) return false;
639
693
  if (textFilter) {
@@ -643,8 +697,9 @@
643
697
  return true;
644
698
  });
645
699
 
700
+ // Inactive worktrees (deduped against active sessions)
646
701
  var activeWorktreePaths = new Set();
647
- cachedSessions.forEach(function (s) {
702
+ worktreeSessions.forEach(function (s) {
648
703
  if (s.repoPath) activeWorktreePaths.add(s.repoPath);
649
704
  });
650
705
 
@@ -663,23 +718,35 @@
663
718
  return (a.name || '').localeCompare(b.name || '');
664
719
  });
665
720
 
666
- sessionList.innerHTML = '';
721
+ // Update tab counts
722
+ tabReposCount.textContent = filteredRepoSessions.length + filteredIdleRepos.length;
723
+ tabWorktreesCount.textContent = filteredWorktreeSessions.length + filteredWorktrees.length;
667
724
 
668
- filteredSessions.forEach(function (session) {
669
- sessionList.appendChild(createActiveSessionLi(session));
670
- });
725
+ // Render based on active tab
726
+ sessionList.innerHTML = '';
671
727
 
672
- if (filteredSessions.length > 0 && filteredWorktrees.length > 0) {
673
- var divider = document.createElement('li');
674
- divider.className = 'session-divider';
675
- divider.textContent = 'Available';
676
- sessionList.appendChild(divider);
728
+ if (activeTab === 'repos') {
729
+ filteredRepoSessions.forEach(function (session) {
730
+ sessionList.appendChild(createActiveSessionLi(session));
731
+ });
732
+ if (filteredRepoSessions.length > 0 && filteredIdleRepos.length > 0) {
733
+ sessionList.appendChild(createSectionDivider('Available'));
734
+ }
735
+ filteredIdleRepos.forEach(function (repo) {
736
+ sessionList.appendChild(createIdleRepoLi(repo));
737
+ });
738
+ } else {
739
+ filteredWorktreeSessions.forEach(function (session) {
740
+ sessionList.appendChild(createActiveSessionLi(session));
741
+ });
742
+ if (filteredWorktreeSessions.length > 0 && filteredWorktrees.length > 0) {
743
+ sessionList.appendChild(createSectionDivider('Available'));
744
+ }
745
+ filteredWorktrees.forEach(function (wt) {
746
+ sessionList.appendChild(createInactiveWorktreeLi(wt));
747
+ });
677
748
  }
678
749
 
679
- filteredWorktrees.forEach(function (wt) {
680
- sessionList.appendChild(createInactiveWorktreeLi(wt));
681
- });
682
-
683
750
  highlightActiveSession();
684
751
  }
685
752
 
@@ -824,6 +891,45 @@
824
891
  return li;
825
892
  }
826
893
 
894
+ function createSectionDivider(label) {
895
+ var divider = document.createElement('li');
896
+ divider.className = 'session-divider';
897
+ divider.textContent = label;
898
+ return divider;
899
+ }
900
+
901
+ function createIdleRepoLi(repo) {
902
+ var li = document.createElement('li');
903
+ li.className = 'inactive-worktree';
904
+ li.title = repo.path;
905
+
906
+ var infoDiv = document.createElement('div');
907
+ infoDiv.className = 'session-info';
908
+
909
+ var nameSpan = document.createElement('span');
910
+ nameSpan.className = 'session-name';
911
+ nameSpan.textContent = repo.name;
912
+ nameSpan.title = repo.name;
913
+
914
+ var dot = document.createElement('span');
915
+ dot.className = 'status-dot status-dot--inactive';
916
+
917
+ var subSpan = document.createElement('span');
918
+ subSpan.className = 'session-sub';
919
+ subSpan.textContent = repo.root ? rootShortName(repo.root) : repo.path;
920
+
921
+ infoDiv.appendChild(dot);
922
+ infoDiv.appendChild(nameSpan);
923
+ infoDiv.appendChild(subSpan);
924
+ li.appendChild(infoDiv);
925
+
926
+ li.addEventListener('click', function () {
927
+ openNewSessionDialogForRepo(repo);
928
+ });
929
+
930
+ return li;
931
+ }
932
+
827
933
  function startRename(li, session) {
828
934
  var nameSpan = li.querySelector('.session-name');
829
935
  if (!nameSpan) return;
@@ -1043,13 +1149,74 @@
1043
1149
  .catch(function () {});
1044
1150
  }
1045
1151
 
1046
- newSessionBtn.addEventListener('click', function () {
1152
+ function resetDialogFields() {
1047
1153
  customPath.value = '';
1048
1154
  dialogYolo.checked = false;
1155
+ dialogContinue.checked = false;
1049
1156
  dialogBranchInput.value = '';
1050
1157
  dialogBranchList.hidden = true;
1051
1158
  allBranches = [];
1052
1159
  populateDialogRootSelect();
1160
+ }
1161
+
1162
+ function showDialogForTab(tab) {
1163
+ var dialogBranchField = dialogBranchInput.closest('.dialog-field');
1164
+ if (tab === 'repos') {
1165
+ dialogBranchField.hidden = true;
1166
+ dialogContinueField.hidden = false;
1167
+ dialogStart.textContent = 'New Session';
1168
+ } else {
1169
+ dialogBranchField.hidden = false;
1170
+ dialogContinueField.hidden = true;
1171
+ dialogStart.textContent = 'New Worktree';
1172
+ }
1173
+ }
1174
+
1175
+ function openNewSessionDialogForRepo(repo) {
1176
+ resetDialogFields();
1177
+
1178
+ if (repo.root) {
1179
+ dialogRootSelect.value = repo.root;
1180
+ dialogRootSelect.dispatchEvent(new Event('change'));
1181
+ dialogRepoSelect.value = repo.path;
1182
+ }
1183
+
1184
+ showDialogForTab('repos');
1185
+ dialog.showModal();
1186
+ }
1187
+
1188
+ function startRepoSession(repoPath, continueSession, claudeArgs) {
1189
+ var body = { repoPath: repoPath };
1190
+ if (continueSession) body.continue = true;
1191
+ if (claudeArgs) body.claudeArgs = claudeArgs;
1192
+
1193
+ fetch('/sessions/repo', {
1194
+ method: 'POST',
1195
+ headers: { 'Content-Type': 'application/json' },
1196
+ body: JSON.stringify(body),
1197
+ })
1198
+ .then(function (res) {
1199
+ if (res.status === 409) {
1200
+ return res.json().then(function (data) {
1201
+ if (dialog.open) dialog.close();
1202
+ refreshAll();
1203
+ if (data.sessionId) connectToSession(data.sessionId);
1204
+ return null;
1205
+ });
1206
+ }
1207
+ return res.json();
1208
+ })
1209
+ .then(function (data) {
1210
+ if (!data) return;
1211
+ if (dialog.open) dialog.close();
1212
+ refreshAll();
1213
+ if (data.id) connectToSession(data.id);
1214
+ })
1215
+ .catch(function () {});
1216
+ }
1217
+
1218
+ newSessionBtn.addEventListener('click', function () {
1219
+ resetDialogFields();
1053
1220
 
1054
1221
  var sidebarRoot = sidebarRootFilter.value;
1055
1222
  if (sidebarRoot) {
@@ -1069,6 +1236,7 @@
1069
1236
  dialogRepoSelect.disabled = true;
1070
1237
  }
1071
1238
 
1239
+ showDialogForTab(activeTab);
1072
1240
  dialog.showModal();
1073
1241
  });
1074
1242
 
@@ -1076,8 +1244,13 @@
1076
1244
  var repoPathValue = customPath.value.trim() || dialogRepoSelect.value;
1077
1245
  if (!repoPathValue) return;
1078
1246
  var args = dialogYolo.checked ? ['--dangerously-skip-permissions'] : undefined;
1079
- var branch = dialogBranchInput.value.trim() || undefined;
1080
- startSession(repoPathValue, undefined, args, branch);
1247
+
1248
+ if (activeTab === 'repos') {
1249
+ startRepoSession(repoPathValue, dialogContinue.checked, args);
1250
+ } else {
1251
+ var branch = dialogBranchInput.value.trim() || undefined;
1252
+ startSession(repoPathValue, undefined, args, branch);
1253
+ }
1081
1254
  });
1082
1255
 
1083
1256
  customPath.addEventListener('blur', function () {
package/public/index.html CHANGED
@@ -52,6 +52,10 @@
52
52
  </select>
53
53
  <input type="text" id="session-filter" placeholder="Filter..." />
54
54
  </div>
55
+ <div class="sidebar-tabs">
56
+ <button class="sidebar-tab active" data-tab="repos">Repos (<span id="tab-repos-count">0</span>)</button>
57
+ <button class="sidebar-tab" data-tab="worktrees">Worktrees (<span id="tab-worktrees-count">0</span>)</button>
58
+ </div>
55
59
  <ul id="session-list"></ul>
56
60
  <button id="new-session-btn">+ New Session</button>
57
61
  <button id="settings-btn">Settings</button>
@@ -140,6 +144,14 @@
140
144
  <span class="dialog-option-hint">Leave empty for auto-generated name</span>
141
145
  </div>
142
146
 
147
+ <div class="dialog-field" id="dialog-continue-field" hidden>
148
+ <label>
149
+ <input type="checkbox" id="dialog-continue" />
150
+ Continue previous conversation
151
+ </label>
152
+ <span class="dialog-option-hint">Resume where you left off (--continue)</span>
153
+ </div>
154
+
143
155
  <hr class="dialog-separator" />
144
156
  <div class="dialog-custom-path">
145
157
  <label for="custom-path-input">Or enter a local path:</label>
package/public/style.css CHANGED
@@ -202,6 +202,36 @@ html, body {
202
202
  border-color: var(--accent);
203
203
  }
204
204
 
205
+ /* Sidebar Tabs */
206
+ .sidebar-tabs {
207
+ display: flex;
208
+ gap: 0;
209
+ padding: 0 8px;
210
+ border-bottom: 1px solid var(--border);
211
+ }
212
+
213
+ .sidebar-tab {
214
+ flex: 1;
215
+ background: none;
216
+ border: none;
217
+ border-bottom: 2px solid transparent;
218
+ color: var(--text-muted);
219
+ font-size: 0.7rem;
220
+ padding: 6px 4px;
221
+ cursor: pointer;
222
+ transition: color 0.15s, border-color 0.15s;
223
+ text-align: center;
224
+ }
225
+
226
+ .sidebar-tab:hover {
227
+ color: var(--text);
228
+ }
229
+
230
+ .sidebar-tab.active {
231
+ color: var(--accent);
232
+ border-bottom-color: var(--accent);
233
+ }
234
+
205
235
  #session-list {
206
236
  list-style: none;
207
237
  flex: 1;