feat(terminal): resolve relative path links against the pane's current directory

A relative path printed in the terminal was resolved against the session's
workingDir, which is fixed at create time and never follows a cd. In a shell
that moved into a subdirectory or a sibling repo, the link opened the wrong
file or reported "File not found".

The pane-exit watcher's batched list-panes read already carries
#{pane_current_path}. derivePaneCurrentPaths() turns those rows into one
directory per single-pane session, TmuxManager.getPaneCurrentPath() serves it
from memory, and the server applies it on the existing paneExitsUpdated tick
as SessionState.cwd, broadcasting only on a change. No new tmux call, no new
SSE event. A failed read keeps the previous answers.

Session.setCwd() uses the paneExit scoping rule: remote and Docker sessions
never get a cwd (their local pane runs the ssh or docker exec client), nor do
direct-PTY or rediscovered sessions. The value is runtime-only.

In the browser, relativeResolutionBase() picks the live cwd when known and
workingDir otherwise. It only changes the base a relative path joins: the
result is routed as before, so a target outside the workspace still goes
through the attachment routes, and file-content/file-raw are unchanged.
This commit is contained in:
Aamer Akhter committed 2026-10-28 09:38:06 -04:00
1 parent f1f7eec4d6
commit 1607b1e488
12 files changed
+543 -8

No files matched your search

+28
View File
@@ -743,6 +743,11 @@ export class Session extends EventEmitter {
* rendered as "alive".
*/
private _paneExit: PaneExit | null = null;
/**
* The pane's live directory, from the pane-exit watcher's read. Runtime-only:
* serialized by toState() but never restored by the constructor.
*/
private _cwd: string | undefined;
/**
* How many starts, attaches or relaunches are running for this session's
* pane. While one is, a dead-pane reading may describe a pane that is being
@@ -1485,6 +1490,28 @@ export class Session extends EventEmitter {
if (this._muxSession) this._mux?.clearPaneExit?.(this._muxSession.muxName);
}
/** The pane's live working directory, when the pane read has reported one. */
get cwd(): string | undefined {
return this._cwd;
}
/**
* Record the pane's live directory. Returns whether it CHANGED, so the caller
* broadcasts only on a real transition.
*
* Scoped by {@link paneExitApplies}, the same rule as the exit reading: a
* remote session's local pane runs the ssh client and a docker case's runs a
* `docker exec`, so their pane directory is a host directory the agent was
* never in, and a direct-PTY or rediscovered session has no pane Codeman can
* vouch for. A relative value is refused rather than stored.
*/
setCwd(next: string | undefined): boolean {
const resolved = this.paneExitApplies && next?.startsWith('/') ? next : undefined;
if (resolved === this._cwd) return false;
this._cwd = resolved;
return true;
}
/**
* Record what the mux layer observed of this pane's agent, and say whether
* that changed the answer. The caller persists and broadcasts on a true.
@@ -2162,6 +2189,7 @@ export class Session extends EventEmitter {
pid: this.pid,
status: this._status,
workingDir: this.workingDir,
cwd: this._cwd,
remote: this._remote,
docker: this._docker,
owner: this._owner,