diff --git a/src/agents/manager.ts b/src/agents/manager.ts index 6d5adcf0..981fd999 100644 --- a/src/agents/manager.ts +++ b/src/agents/manager.ts @@ -569,6 +569,16 @@ export class AgentManager { } try { + // First, try to resolve the CWD from the agent CLI process itself. + // tmux pane_current_path only tracks the shell's CWD, but agent CLIs + // (claude, codex, opencode) may cd internally without updating the shell. + const agentCwd = await this.resolveAgentProcessCwd(session); + if (agentCwd) { + this.runtimeCwdCache.set(cacheKey, { value: agentCwd, expiresAt: now + 10_000 }); + return agentCwd; + } + + // Fall back to tmux pane_current_path (the shell's CWD). const result = await runCommand("tmux", ["display-message", "-p", "-t", session, "#{pane_current_path}"], { allowedExitCodes: [0, 1], timeoutMs: 800 @@ -584,6 +594,81 @@ export class AgentManager { } } + /** + * Resolve the CWD of the agent CLI process (claude/codex/opencode) running + * inside a tmux pane. The CLI process may have cd'd into a worktree + * internally, which tmux's pane_current_path won't reflect. + */ + private async resolveAgentProcessCwd(session: string): Promise { + try { + // Get the PID of the tmux pane's shell process. + const pidResult = await runCommand( + "tmux", ["display-message", "-p", "-t", session, "#{pane_pid}"], + { allowedExitCodes: [0, 1], timeoutMs: 800 } + ); + const panePid = pidResult.stdout.trim(); + if (pidResult.exitCode !== 0 || !panePid) { + this.logger.debug({ session }, "resolveAgentProcessCwd: no pane_pid"); + return null; + } + + // Find the agent CLI child process (claude, codex, or opencode). + const childrenResult = await runCommand( + "pgrep", ["-P", panePid], + { allowedExitCodes: [0, 1], timeoutMs: 800 } + ); + if (childrenResult.exitCode !== 0 || !childrenResult.stdout.trim()) { + this.logger.debug({ session, panePid }, "resolveAgentProcessCwd: no children"); + return null; + } + + const childPids = childrenResult.stdout.trim().split("\n"); + let agentPid: string | null = null; + + for (const pid of childPids) { + const commResult = await runCommand( + "ps", ["-o", "comm=", "-p", pid.trim()], + { allowedExitCodes: [0, 1], timeoutMs: 800 } + ); + const comm = commResult.stdout.trim(); + // Match agent CLI binaries by basename. + const basename = comm.split("/").pop() ?? ""; + if (basename === "claude" || basename === "codex" || basename === "opencode") { + agentPid = pid.trim(); + break; + } + } + + if (!agentPid) { + this.logger.debug({ session, panePid }, "resolveAgentProcessCwd: no agent CLI among children"); + return null; + } + + // Read the process's CWD via lsof (works on macOS and Linux). + const lsofResult = await runCommand( + "lsof", ["-a", "-p", agentPid, "-d", "cwd", "-Fn"], + { allowedExitCodes: [0, 1], timeoutMs: 800 } + ); + if (lsofResult.exitCode !== 0 || !lsofResult.stdout) { + this.logger.debug({ session, agentPid }, "resolveAgentProcessCwd: lsof failed"); + return null; + } + + // lsof -Fn outputs lines like "p" and "n". Extract the path. + for (const line of lsofResult.stdout.split("\n")) { + if (line.startsWith("n/")) { + const cwd = line.slice(1); + this.logger.debug({ session, agentPid, cwd }, "resolveAgentProcessCwd: resolved"); + return cwd; + } + } + + return null; + } catch { + return null; + } + } + private async startAgentSession( sessionName: string, cwd: string, diff --git a/web/src/components/app/agent-sidebar.tsx b/web/src/components/app/agent-sidebar.tsx index d51aea7c..2110bf47 100644 --- a/web/src/components/app/agent-sidebar.tsx +++ b/web/src/components/app/agent-sidebar.tsx @@ -364,47 +364,46 @@ export function AgentSidebarContent({
-
-
Git
- {agent.gitContext ? ( -
-
- - {agent.gitContext.branch} -
-
+ {agent.gitContext ? ( +
+ + +
+ + {agent.gitContext.branch} +
+
+ + Current branch + +
+ {agent.gitContext.isWorktree && ( + + +
+ + {agent.gitContext.worktreeName} +
+
+ + Linked worktree at {agent.gitContext.worktreePath} + +
+ )} +
+ ) : ( + + +
- - {agent.gitContext.isWorktree - ? `worktree: ${agent.gitContext.worktreeName}` - : "main repository"} - - - - - - {agent.gitContext.isWorktree ? "Worktree" : "Repository"} - - - - - {agent.gitContext.isWorktree - ? "This agent is currently in a linked git worktree checkout." - : "This agent is currently in the primary repository checkout."} - - + Not a git repository
-
- ) : ( -
- - Not a git repository -
- )} -
+ + + This directory is not inside a git repository + + + )}