fix: name the pane directory field paneCwd and apply the review items for #740

`cwd` beside the fixed `workingDir` reads ambiguously on a public API surface, and
`paneCwd` says where the value comes from, like `paneExit`. Also pin the field on
the payload the browser really receives (getSessionStateWithRespawn), and give the
pane-directory text its own architecture-invariants paragraph.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-04 16:59:16 +01:00
1 parent 4f50af6a9f
commit 91d157cae4
9 files changed
+83 -70

No files matched your search

+8 -8
View File
@@ -765,7 +765,7 @@ export class Session extends EventEmitter {
* 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;
private _paneCwd: string | undefined;
/**
* `workingDir` with symlinks resolved, so a pane reading (tmux reads the
* process cwd, always the resolved path) can be put back into `workingDir`'s
@@ -1530,8 +1530,8 @@ export class Session extends EventEmitter {
}
/** The pane's live working directory, when the pane read has reported one. */
get cwd(): string | undefined {
return this._cwd;
get paneCwd(): string | undefined {
return this._paneCwd;
}
/**
@@ -1553,7 +1553,7 @@ export class Session extends EventEmitter {
* `workingDir`'s spelling. Until that resolution has settled, only a reading
* lexically inside `workingDir` is accepted.
*/
setCwd(next: string | undefined): boolean {
setPaneCwd(next: string | undefined): boolean {
let resolved = this.paneExitApplies && next?.startsWith('/') ? next : undefined;
if (resolved !== undefined) {
if (
@@ -1571,8 +1571,8 @@ export class Session extends EventEmitter {
resolved = rebasePath(resolved, this._workspaceRealPath, this._workspaceSpelling());
}
}
if (resolved === this._cwd) return false;
this._cwd = resolved;
if (resolved === this._paneCwd) return false;
this._paneCwd = resolved;
return true;
}
@@ -1582,7 +1582,7 @@ export class Session extends EventEmitter {
}
/**
* Resolve `workingDir`'s symlinks in the background for {@link setCwd}.
* Resolve `workingDir`'s symlinks in the background for {@link setPaneCwd}.
*
* Bounded: the folder can sit on a network mount that stopped answering, so it
* is probed with `probePathKind()` first and the `realpath()` is raced against
@@ -2299,7 +2299,7 @@ export class Session extends EventEmitter {
pid: this.pid,
status: this._status,
workingDir: this.workingDir,
cwd: this._cwd,
paneCwd: this._paneCwd,
remote: this._remote,
docker: this._docker,
owner: this._owner,
+2 -2
View File
@@ -441,7 +441,7 @@ export function derivePaneExits(rows: PaneRow[], now: number): Map<string, PaneE
* no entry: those are authoritative absences, not unanswered questions.
*
* The launch directory is NOT filtered here (unlike the rediscovery path): a
* remote or docker pane that never leaves it is refused by `Session.setCwd()`,
* remote or docker pane that never leaves it is refused by `Session.setPaneCwd()`,
* and a local pane reporting it really is there.
*/
export function derivePaneCurrentPaths(
@@ -2049,7 +2049,7 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
* muxName -> `#{pane_current_path}` from the last authoritative pane read
* ({@link derivePaneCurrentPaths}). Replaced wholesale by each non-empty read,
* so it is bounded by the panes on the socket. Like {@link paneExits} it holds
* the raw reading; `Session.setCwd()` decides which sessions may use it.
* the raw reading; `Session.setPaneCwd()` decides which sessions may use it.
*/
private paneCurrentPaths: Map<string, string> = new Map();
/** The pane-exit watcher's own interval. Runs whether or not stats are on. */
+1 -1
View File
@@ -846,7 +846,7 @@ export interface SessionState {
* to `workingDir`, and a path resolved outside it goes through the attachment
* routes like any other out-of-workspace path.
*/
cwd?: string;
paneCwd?: string;
/** Remote execution metadata, present when this session runs over SSH through local tmux */
remote?: SessionRemote;
/** Docker execution metadata, present when this session runs inside a container via local tmux + docker exec */
+6 -6
View File
@@ -2431,23 +2431,23 @@ function resolveLinkedFilePath(filePath, workingDir) {
*
* `workingDir` is recorded once at create time and never follows a `cd`, so it
* stops describing where a shell (or an agent that changed directory) really
* is. `session.cwd` is the pane's live `#{pane_current_path}`, published by the
* is. `session.paneCwd` is the pane's live `#{pane_current_path}`, published by the
* server from its pane read. It only changes which BASE a relative path joins;
* the result is then routed like any other path, so a target outside the
* workspace still goes through the attachment routes.
*
* Remote and Docker sessions are excluded (the server never sends them a cwd,
* Remote and Docker sessions are excluded (the server never sends them a paneCwd,
* and this checks again): their local pane runs the ssh / docker-exec CLIENT,
* so its directory says nothing about the agent. An unknown cwd falls back to
* so its directory says nothing about the agent. An unknown paneCwd falls back to
* `workingDir`.
*
* @param {{workingDir?: string, cwd?: string, remote?: unknown, docker?: unknown}} [session]
* @param {{workingDir?: string, paneCwd?: string, remote?: unknown, docker?: unknown}} [session]
* @returns {string|undefined}
*/
function relativeResolutionBase(session) {
if (!session) return undefined;
const cwd = session.cwd;
if (!session.remote && !session.docker && typeof cwd === 'string' && cwd.startsWith('/')) return cwd;
const paneCwd = session.paneCwd;
if (!session.remote && !session.docker && typeof paneCwd === 'string' && paneCwd.startsWith('/')) return paneCwd;
return session.workingDir;
}
+2 -2
View File
@@ -2866,10 +2866,10 @@ export class WebServer extends EventEmitter {
// or empty read and carries a blank `#{pane_current_path}` forward, so an
// `undefined` here means an authoritative read listed no single directory
// for the session (split into several panes, or gone from the socket).
const cwdChanged = getPaneCurrentPath ? session.setCwd(getPaneCurrentPath(muxName)) : false;
const paneCwdChanged = getPaneCurrentPath ? session.setPaneCwd(getPaneCurrentPath(muxName)) : false;
if (session.setPaneExit(getPaneExit(muxName))) {
this.persistSessionState(session);
} else if (!cwdChanged) {
} else if (!paneCwdChanged) {
continue;
}
this.broadcastSessionStateDebounced(session.id);