Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions src/agents/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<string | null> {
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<pid>" and "n<path>". 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,
Expand Down
77 changes: 38 additions & 39 deletions web/src/components/app/agent-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -364,47 +364,46 @@ export function AgentSidebarContent({
<div className="px-3 pt-1">
<div className="grid gap-2 text-xs text-muted-foreground">
<AgentMeta label="Working dir" value={agent.cwd} mono />
<div className="grid gap-1">
<div className="uppercase tracking-wide text-[10px] text-muted-foreground/80">Git</div>
{agent.gitContext ? (
<div className="grid gap-1 text-foreground">
<div className="inline-flex items-center gap-1.5">
<GitBranch className="h-3.5 w-3.5 text-muted-foreground" />
<span>{agent.gitContext.branch}</span>
</div>
<div className="inline-flex items-center gap-1.5">
{agent.gitContext ? (
<div className="grid gap-1 text-foreground">
<Tooltip>
<TooltipTrigger asChild>
<div className="inline-flex items-center gap-1.5 cursor-default">
<GitBranch className="h-3.5 w-3.5 text-muted-foreground" />
<span>{agent.gitContext.branch}</span>
</div>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-[280px] text-xs">
Current branch
</TooltipContent>
</Tooltip>
{agent.gitContext.isWorktree && (
<Tooltip>
<TooltipTrigger asChild>
<div className="inline-flex items-center gap-1.5 cursor-default">
<FolderGit2 className="h-3.5 w-3.5 text-muted-foreground" />
<span>{agent.gitContext.worktreeName}</span>
</div>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-[280px] text-xs">
Linked worktree at {agent.gitContext.worktreePath}
</TooltipContent>
</Tooltip>
)}
</div>
) : (
<Tooltip>
<TooltipTrigger asChild>
<div className="inline-flex items-center gap-1.5 text-foreground cursor-default">
<FolderGit2 className="h-3.5 w-3.5 text-muted-foreground" />
<span>
{agent.gitContext.isWorktree
? `worktree: ${agent.gitContext.worktreeName}`
: "main repository"}
</span>
<Tooltip>
<TooltipTrigger asChild>
<span>
<Badge
variant="default"
className="ml-1 h-5 border border-border bg-transparent px-1.5 text-[10px] uppercase text-muted-foreground"
>
{agent.gitContext.isWorktree ? "Worktree" : "Repository"}
</Badge>
</span>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-[240px] text-xs">
{agent.gitContext.isWorktree
? "This agent is currently in a linked git worktree checkout."
: "This agent is currently in the primary repository checkout."}
</TooltipContent>
</Tooltip>
<span>Not a git repository</span>
</div>
</div>
) : (
<div className="inline-flex items-center gap-1.5 text-foreground">
<FolderGit2 className="h-3.5 w-3.5 text-muted-foreground" />
<span>Not a git repository</span>
</div>
)}
</div>
</TooltipTrigger>
<TooltipContent side="right" className="max-w-[280px] text-xs">
This directory is not inside a git repository
</TooltipContent>
</Tooltip>
)}
<AgentMeta label="Agent type" value={agentTypeLabel(agent.type)} />
<div className="grid gap-1">
<div className="uppercase tracking-wide text-[10px] text-muted-foreground/80">
Expand Down
Loading