Skip to content

Commit 5e5c85b

Browse files
authored
Merge pull request #64 from grimmerk/feat/cmux-detection-cross-reference
feat: cmux cross-reference detection via tree TTY
2 parents 0704f35 + eea1252 commit 5e5c85b

3 files changed

Lines changed: 102 additions & 4 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Changelog
22

3+
## 1.0.43
4+
5+
- cmux: cross-reference detection via tree TTY field (requires cmux v0.63+)
6+
- Fixes purple dot for bare `claude` / `claude -r` sessions with in-session `/rename` on cmux
7+
38
## 1.0.42
49

510
- iTerm2: cross-reference detection via per-tab TTY + tab name matching

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "CodeV",
33
"productName": "CodeV",
4-
"version": "1.0.42",
4+
"version": "1.0.43",
55
"description": "Quick switcher for VS Code, Cursor, and Claude Code sessions",
66
"main": ".webpack/main",
77
"scripts": {

src/claude-session-utility.ts

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,99 @@ end tell`);
309309
}
310310
};
311311

312+
/**
313+
* cmux cross-reference: refine PID-session mapping using per-surface TTY + title from tree output.
314+
* Same concept as iTerm2 cross-reference but uses cmux CLI instead of AppleScript.
315+
* Requires cmux build with TTY support in tree output (tty= field in surface lines).
316+
*/
317+
const refineDetectionWithCmux = async (
318+
activeMap: Map<string, number>,
319+
claimedSessionIds: Set<string>,
320+
cwdProcesses: { pid: number; line: string }[],
321+
allSessions: ClaudeSession[],
322+
execPromise: (cmd: string) => Promise<string>,
323+
): Promise<void> => {
324+
if (cwdProcesses.length === 0) return;
325+
326+
// Quick check: is cmux running?
327+
const cmuxCheck = await execPromise('pgrep -x cmux 2>/dev/null');
328+
if (!cmuxCheck.trim()) return;
329+
330+
// Get tree output with TTY info
331+
const treeOutput = await execPromise(`${CMUX_CLI} tree --all 2>/dev/null`);
332+
if (!treeOutput.trim()) return;
333+
334+
// Parse surfaces with TTY: look for "tty=ttysNNN" in surface lines
335+
const cmuxSurfaces: { tty: string; title: string }[] = [];
336+
for (const line of treeOutput.split('\n')) {
337+
const surfaceMatch = line.match(/surface (surface:\d+)/);
338+
if (!surfaceMatch) continue;
339+
const ttyMatch = line.match(/tty=(\S+)/);
340+
if (!ttyMatch) continue;
341+
const titleMatch = line.match(/\[terminal\]\s+"(.+?)"\s*(\[||tty=|$)/);
342+
cmuxSurfaces.push({
343+
tty: ttyMatch[1],
344+
title: titleMatch ? titleMatch[1] : '',
345+
});
346+
}
347+
if (cmuxSurfaces.length === 0) return;
348+
349+
// Load custom titles lazily per-cwd
350+
const claudeDir = path.join(os.homedir(), '.claude', 'projects');
351+
const titleCache = new Map<string, Map<string, string>>();
352+
353+
const getTitlesForCwd = async (cwd: string): Promise<Map<string, string>> => {
354+
if (titleCache.has(cwd)) return titleCache.get(cwd)!;
355+
const titles = new Map<string, string>();
356+
const candidates = allSessions.filter(s => s.project === cwd);
357+
const encodedProject = cwd.replace(/[^a-zA-Z0-9-]/g, '-');
358+
await Promise.all(candidates.map(async (session) => {
359+
const jsonlPath = path.join(claudeDir, encodedProject, `${session.sessionId}.jsonl`);
360+
if (!fs.existsSync(jsonlPath)) return;
361+
const out = await execPromise(`grep '"type":"custom-title"' "${jsonlPath}" 2>/dev/null | tail -1`);
362+
try {
363+
const parsed = JSON.parse(out.trim());
364+
const title = (parsed.customTitle || '').replace(/^"|"$/g, '').trim();
365+
if (title) titles.set(session.sessionId, title);
366+
} catch {}
367+
}));
368+
titleCache.set(cwd, titles);
369+
return titles;
370+
};
371+
372+
// Cross-reference: for each cwd-fallback PID, match TTY → cmux surface → title → session
373+
for (const { pid } of cwdProcesses) {
374+
const ttyOutput = (await execPromise(`ps -o tty= -p ${pid} 2>/dev/null`)).trim();
375+
if (!ttyOutput) continue;
376+
377+
const cmuxSurface = cmuxSurfaces.find(s => s.tty.endsWith(ttyOutput));
378+
if (!cmuxSurface) continue;
379+
380+
const cwdOutput = await execPromise(`lsof -p ${pid} -Fn 2>/dev/null | grep "^n/" | head -1`);
381+
const cwdMatch = cwdOutput.match(/^n(.+)$/m);
382+
if (!cwdMatch) continue;
383+
const cwd = cwdMatch[1];
384+
385+
const sessionTitles = await getTitlesForCwd(cwd);
386+
if (sessionTitles.size === 0) continue;
387+
388+
const tabName = cmuxSurface.title;
389+
for (const [sessionId, title] of sessionTitles) {
390+
if (title && tabName.includes(title) && !claimedSessionIds.has(sessionId)) {
391+
const currentSessionId = [...activeMap.entries()].find(([, p]) => p === pid)?.[0];
392+
if (currentSessionId && currentSessionId !== sessionId) {
393+
activeMap.delete(currentSessionId);
394+
claimedSessionIds.delete(currentSessionId);
395+
console.log(`[cross-ref-cmux] corrected PID ${pid}: ${currentSessionId}${sessionId} (surface: "${tabName}")`);
396+
}
397+
activeMap.set(sessionId, pid);
398+
claimedSessionIds.add(sessionId);
399+
break;
400+
}
401+
}
402+
}
403+
};
404+
312405
export const detectActiveSessions = async (): Promise<Map<string, number>> => {
313406
const now = Date.now();
314407
if (cachedActiveMap && (now - activeCacheTimestamp) < ACTIVE_CACHE_TTL_MS) {
@@ -417,11 +510,11 @@ export const detectActiveSessions = async (): Promise<Map<string, number>> => {
417510
}
418511
}
419512

420-
// Third pass: iTerm2 cross-reference for processes that were matched by cwd fallback.
421-
// Uses iTerm2 AppleScript to get TTY + tab name for each session, then cross-references
513+
// Third pass: cross-reference for processes that were matched by cwd fallback.
514+
// Uses terminal-specific APIs to get TTY + tab name, then cross-references
422515
// with claude process TTYs to build a definitive PID → session ID mapping.
423-
// Only runs when there are same-cwd processes that used cwd fallback (no UUID/title in args).
424516
await refineDetectionWithITerm2(activeMap, claimedSessionIds, cwdProcesses, allSessions, execPromise);
517+
await refineDetectionWithCmux(activeMap, claimedSessionIds, cwdProcesses, allSessions, execPromise);
425518
} catch {
426519
// ignore
427520
}

0 commit comments

Comments
 (0)