claude-remote-cli 3.4.0 → 3.4.1

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.
@@ -178,6 +178,10 @@ async function getPrForBranch(repoPath, branch, options = {}) {
178
178
  return null;
179
179
  try {
180
180
  const data = JSON.parse(stdout);
181
+ // Only return OPEN PRs — gh pr view returns merged/closed PRs too
182
+ if (data.state !== 'OPEN') {
183
+ return null;
184
+ }
181
185
  return {
182
186
  number: data.number,
183
187
  title: data.title,
@@ -272,4 +276,37 @@ async function getWorkingTreeDiff(repoPath, exec = execFileAsync) {
272
276
  return { additions: 0, deletions: 0 };
273
277
  }
274
278
  }
275
- export { listBranches, normalizeBranchNames, getActivityFeed, getCiStatus, getPrForBranch, getUnresolvedCommentCount, switchBranch, getCommitsAhead, getCurrentBranch, getWorkingTreeDiff, };
279
+ /**
280
+ * Convert a git branch name to a human-readable display name.
281
+ * "fix-mobile-scroll-bug" → "Fix mobile scroll bug"
282
+ * "feature/add-auth" → "Add auth"
283
+ */
284
+ function branchToDisplayName(branch) {
285
+ const stripped = branch.replace(/^(feature|fix|chore|refactor|docs|test|ci|build)\//i, '');
286
+ const words = stripped.replace(/[-_]/g, ' ').trim();
287
+ if (!words)
288
+ return branch;
289
+ return words.charAt(0).toUpperCase() + words.slice(1);
290
+ }
291
+ async function isBranchStale(repoPath, branch, options = {}) {
292
+ const run = options.exec || execFileAsync;
293
+ try {
294
+ for (const base of ['main', 'master']) {
295
+ try {
296
+ const { stdout } = await run('git', ['rev-list', '--count', `${base}..${branch}`], { cwd: repoPath, timeout: 5000 });
297
+ const count = parseInt(stdout.trim(), 10);
298
+ if (count === 0)
299
+ return true;
300
+ return false;
301
+ }
302
+ catch {
303
+ continue;
304
+ }
305
+ }
306
+ return false;
307
+ }
308
+ catch {
309
+ return false;
310
+ }
311
+ }
312
+ export { listBranches, normalizeBranchNames, getActivityFeed, getCiStatus, getPrForBranch, getUnresolvedCommentCount, switchBranch, getCommitsAhead, getCurrentBranch, getWorkingTreeDiff, branchToDisplayName, isBranchStale, };
@@ -16,7 +16,7 @@ import { setupWebSocket } from './ws.js';
16
16
  import { WorktreeWatcher, WORKTREE_DIRS, isValidWorktreePath, parseWorktreeListPorcelain, parseAllWorktrees } from './watcher.js';
17
17
  import { isInstalled as serviceIsInstalled } from './service.js';
18
18
  import { extensionForMime, setClipboardImage } from './clipboard.js';
19
- import { listBranches } from './git.js';
19
+ import { listBranches, isBranchStale } from './git.js';
20
20
  import * as push from './push.js';
21
21
  import { createWorkspaceRouter } from './workspaces.js';
22
22
  import { MOUNTAIN_NAMES } from './types.js';
@@ -559,10 +559,30 @@ async function main() {
559
559
  let resolvedBranch = '';
560
560
  let isMountainName = false;
561
561
  if (worktreePath) {
562
+ // Check if the worktree's branch is stale (merged/at base) and needs a fresh name
563
+ const currentBranchResult = await execFileAsync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: worktreePath }).catch(() => null);
564
+ const currentBranch = currentBranchResult?.stdout.trim();
565
+ if (currentBranch && !needsBranchRename) {
566
+ const stale = await isBranchStale(worktreePath, currentBranch);
567
+ if (stale) {
568
+ // Generate unique temp branch: <mountain>-<short-timestamp>
569
+ const mountainName = worktreePath.split('/').pop() || 'branch';
570
+ const suffix = Date.now().toString(36).slice(-4);
571
+ const tempBranch = `${mountainName}-${suffix}`;
572
+ try {
573
+ await execFileAsync('git', ['checkout', '-b', tempBranch], { cwd: worktreePath });
574
+ }
575
+ catch {
576
+ await execFileAsync('git', ['branch', '-m', tempBranch], { cwd: worktreePath }).catch(() => { });
577
+ }
578
+ isMountainName = true;
579
+ }
580
+ }
562
581
  // Only use --continue if:
563
582
  // 1. Not a brand-new worktree (needsBranchRename flag)
564
583
  // 2. A prior Claude session exists in this directory (.claude/ dir present)
565
- const hasPriorSession = !needsBranchRename && fs.existsSync(path.join(worktreePath, '.claude'));
584
+ // 3. Branch is not stale (isMountainName means we just created a fresh branch)
585
+ const hasPriorSession = !needsBranchRename && !isMountainName && fs.existsSync(path.join(worktreePath, '.claude'));
566
586
  args = hasPriorSession ? [...AGENT_CONTINUE_ARGS[resolvedAgent], ...baseArgs] : [...baseArgs];
567
587
  cwd = worktreePath;
568
588
  sessionRepoPath = worktreePath;
@@ -295,7 +295,7 @@ async function fetchMetaForSession(session) {
295
295
  if (branch) {
296
296
  try {
297
297
  const pr = await getPrForBranch(repoPath, branch);
298
- if (pr && pr.state === 'OPEN') {
298
+ if (pr) {
299
299
  prNumber = pr.number;
300
300
  additions = pr.additions;
301
301
  deletions = pr.deletions;
package/dist/server/ws.js CHANGED
@@ -3,6 +3,7 @@ import { execFile } from 'node:child_process';
3
3
  import { promisify } from 'node:util';
4
4
  import * as sessions from './sessions.js';
5
5
  import { writeMeta } from './config.js';
6
+ import { branchToDisplayName } from './git.js';
6
7
  const execFileAsync = promisify(execFile);
7
8
  const BRANCH_POLL_INTERVAL_MS = 3000;
8
9
  const BRANCH_POLL_MAX_ATTEMPTS = 10;
@@ -20,12 +21,13 @@ function startBranchWatcher(session, broadcastEvent, cfgPath) {
20
21
  const currentBranch = stdout.trim();
21
22
  if (currentBranch && currentBranch !== originalBranch) {
22
23
  clearInterval(timer);
24
+ const displayName = branchToDisplayName(currentBranch);
23
25
  session.branchName = currentBranch;
24
- session.displayName = currentBranch;
25
- broadcastEvent('session-renamed', { sessionId: session.id, branchName: currentBranch, displayName: currentBranch });
26
+ session.displayName = displayName;
27
+ broadcastEvent('session-renamed', { sessionId: session.id, branchName: currentBranch, displayName });
26
28
  writeMeta(cfgPath, {
27
29
  worktreePath: session.repoPath,
28
- displayName: currentBranch,
30
+ displayName,
29
31
  lastActivity: new Date().toISOString(),
30
32
  branchName: currentBranch,
31
33
  });
@@ -147,16 +149,17 @@ function setupWebSocket(server, authenticatedTokens, watcher, configPath) {
147
149
  ptySession._renameBuffer = '';
148
150
  const enterIndex = str.indexOf('\r');
149
151
  if (enterIndex === -1) {
150
- // Buffer without passthrough — don't echo to PTY during buffering.
151
- // Previously we wrote to PTY here for echo and used Ctrl+U to undo on Enter,
152
- // but Ctrl+U doesn't work reliably in Claude Code's Ink/React TUI.
153
152
  ptySession._renameBuffer += str;
153
+ ptySession.pty.write(str); // Echo to terminal so user sees what they type
154
154
  return;
155
155
  }
156
156
  // Enter detected — send rename prompt + full message to PTY in one shot
157
157
  const buffered = ptySession._renameBuffer;
158
158
  const beforeEnter = buffered + str.slice(0, enterIndex);
159
159
  const afterEnter = str.slice(enterIndex); // includes the \r
160
+ // Clear the echoed input line before writing the full prompt+message
161
+ const clearLine = '\r' + ' '.repeat(Math.min(beforeEnter.length + 2, 200)) + '\r';
162
+ ptySession.pty.write(clearLine);
160
163
  const renamePrompt = `Before doing anything else, rename the current git branch using \`git branch -m <new-name>\`. Choose a short, descriptive kebab-case branch name based on the task below.${ptySession.branchRenamePrompt ? ' User preferences: ' + ptySession.branchRenamePrompt : ''} Do not ask for confirmation — just rename and proceed.\n\n`;
161
164
  ptySession.pty.write(renamePrompt + beforeEnter + afterEnter);
162
165
  ptySession.needsBranchRename = false;
@@ -1,6 +1,7 @@
1
1
  import { test, describe } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import { MOUNTAIN_NAMES } from '../server/types.js';
4
+ import { branchToDisplayName } from '../server/git.js';
4
5
  describe('MOUNTAIN_NAMES', () => {
5
6
  test('contains 30 mountain names', () => {
6
7
  assert.equal(MOUNTAIN_NAMES.length, 30);
@@ -26,3 +27,19 @@ describe('MOUNTAIN_NAMES', () => {
26
27
  assert.equal(name3, 'everest'); // wraps back to start
27
28
  });
28
29
  });
30
+ describe('branchToDisplayName', () => {
31
+ test('converts kebab-case to sentence case', () => {
32
+ assert.equal(branchToDisplayName('fix-mobile-scroll-bug'), 'Fix mobile scroll bug');
33
+ });
34
+ test('strips common branch prefixes', () => {
35
+ assert.equal(branchToDisplayName('feature/add-auth'), 'Add auth');
36
+ assert.equal(branchToDisplayName('fix/api-timeout'), 'Api timeout');
37
+ assert.equal(branchToDisplayName('chore/update-deps'), 'Update deps');
38
+ });
39
+ test('handles simple names', () => {
40
+ assert.equal(branchToDisplayName('lhotse'), 'Lhotse');
41
+ });
42
+ test('handles underscores', () => {
43
+ assert.equal(branchToDisplayName('fix_the_thing'), 'Fix the thing');
44
+ });
45
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-remote-cli",
3
- "version": "3.4.0",
3
+ "version": "3.4.1",
4
4
  "description": "Remote web interface for Claude Code CLI sessions",
5
5
  "type": "module",
6
6
  "main": "dist/server/index.js",