Skip to content

Show the terminal's live working directory in the pane header - #209

Merged
Zeus-Deus merged 2 commits into
mainfrom
terminal-pane-cwd-title
Jul 27, 2026
Merged

Show the terminal's live working directory in the pane header#209
Zeus-Deus merged 2 commits into
mainfrom
terminal-pane-cwd-title

Conversation

@Zeus-Deus

@Zeus-Deus Zeus-Deus commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Problem

The terminal pane header rendered a bare Terminal — the same word the tab directly above it already shows. Two labels stacked 28px apart, zero information between them.

What this does

Appends the session's live working directory to the header, only when it differs from the workspace root.

Shell location Header
Workspace root (default) Terminal — unchanged
cd src-tauri Terminal src-tauri
Deep Terminal …/src/pty_daemon
Outside the workspace Terminal ~/dotfiles
Outside $HOME Terminal …/docker/volumes

Design notes

Signal only when it's surprising. ~80% of sessions sit at the workspace root, and the workspace identity is already on screen twice (sidebar + context bar). Staying quiet at the root means the header doesn't churn and doesn't add a third copy of the same string to every pane in a split.

Full absolute paths are deliberately not rendered. The header is a 28px strip that CSS-truncates on the right, so /home/user/.codemux/worktrees/codemux/some-branch would degrade to /home/user/.codemux/worktr… — dropping the only segment that mattered. Eliding the head keeps the meaningful tail and fits a narrow split; the untrimmed path is on the hover tooltip.

Foreground command was considered and rejected — showing the running process would flash the header on every ls.

Known tradeoff: absence is ambiguous — an empty hint means "at the root" or "cwd not known yet." This only bites on remote panes whose shell doesn't emit OSC 7. Judged the better trade against always-on noise; it's a one-line flip in formatCwdHint if we want to revisit.

Two sources, osc7 beats proc

OSC 7 /proc/<pid>/cwd
Mechanism Shell pushes on every prompt Poll + readlink
Needs shell integration Yes (fish, vte-bash ✅; bare zsh ❌) No
Remote/SSH panes Works Can't (pid is on another host)
Latency Instant ≤1 interval

Once a session reports via OSC 7 the poller drops it from its request set, so a fully shell-integrated setup costs zero IPC. A single app-level 2s interval batches one call for the active workspace's remaining sessions and pauses while the window is hidden.

The /proc read targets the session's shell pid, so cd moves the header but a long-running child (a build, an agent) does not. Sessions with no local pid are omitted rather than answered from an unrelated local /proc entry.

Constraints the codebase imposed

  • State is frontend-only. TerminalSessionSnapshot.cwd stays the spawn directory — making it live would fan out a full state rebuild plus a persistence write on every cd for a cosmetic label.
  • The cwd is appended, never substituted. PRESET_TITLE_TO_ICON keys the preset icon off the exact title string, so overwriting node.title with a path would silently kill the Claude/Codex icons. A regression test pins this.

Testing

  • 42 new frontend tests — formatter rules (incl. Windows separators and shared-prefix siblings), the osc7-wins precedence, the OSC 7 payload parser, and the header's render states.
  • 2 new Rust tests over the /proc reader, covering live pid, dead pid, and no-pid (remote) cases.
  • Full suite: 3093 frontend tests pass, including pane-tree-rerender.test.tsx — the hoisted hooks resolve to stable primitives for non-terminal nodes, so there's no re-render fan-out.
  • Verified end-to-end in the running UI by pushing real OSC 7 escape sequences through the pty stream (new __codemuxTerminalMock.emitOsc7 dev helper, which exercises the production handler rather than a mock shortcut). All five label cases confirmed, plus correct truncation at a 520px viewport, and the hint survived many poll ticks without being clobbered — confirming the precedence rule.

⚠️ The /proc fallback is unit-tested against real /proc but was not exercised end-to-end in the desktop app (tauri:dev opens a native window not visible to browser automation). The OSC 7 path is verified end-to-end.

The terminal pane header rendered a bare "Terminal" — the same word the
tab directly above it already shows — so the strip carried no
information at all. It now appends the session's live working directory,
but only when that directory differs from the workspace root.

Design: signal only when it's surprising.

- At the workspace root (the common case, and where every session
  starts) the header is unchanged, so it doesn't churn on every pane.
- Inside the workspace it shows the relative path: "Terminal · src-tauri".
- Deep paths elide their head ("…/src/pty_daemon") and out-of-workspace
  paths contract against $HOME ("~/dotfiles"); the untrimmed path is on
  the hover tooltip.

Full absolute paths are deliberately not rendered. The header is a 28px
strip that CSS-truncates on the right, so a long path degrades to
"/home/user/.codemux/worktr…" and drops the only segment that mattered.
Trimming to a leading "…/" plus the trailing segments keeps the
meaningful tail and still fits a narrow split pane.

Two sources, with osc7 beating proc:

- TerminalPane registers an xterm OSC 7 handler. Shells with integration
  push this on every prompt; it is exact, event-driven, and the only
  source that works for remote/SSH panes, where the shell's pid lives on
  another machine.
- Shells without that integration fall back to a new
  `terminal_session_cwds` command that readlinks /proc/<pid>/cwd for the
  session's shell pid — so `cd` moves the header but a long-running child
  (a build, an agent) does not. Sessions with no local pid are omitted
  rather than answered from an unrelated local /proc entry.

Once a session reports via OSC 7 the poller drops it from its request
set, so a fully shell-integrated setup costs zero IPC. A single
app-level 2s interval batches one call for the active workspace's
remaining sessions and pauses while the window is hidden.

State is frontend-only. TerminalSessionSnapshot.cwd stays the spawn
directory: making it live would fan out a full state rebuild plus a
persistence write on every `cd` for a cosmetic label. Pane titles also
stay write-once, and the cwd is appended as a separate element rather
than replacing node.title, because PRESET_TITLE_TO_ICON keys the preset
icon off the exact title string — a regression test pins this.

Tests: 42 new frontend tests covering the formatter rules, the
precedence rule, the OSC 7 payload parser, and the header's render
states; 2 Rust tests over the /proc reader. The dev mock gains an
emitOsc7 helper that pushes a real escape sequence through the pty
stream, so the production handler is exercised in browser dev.
@Zeus-Deus

Copy link
Copy Markdown
Owner Author

Review — Opus 5 (high effort)

The architecture is right: OSC 7 push beating a poll, the poller dropping OSC-7 sessions from its request set so an integrated shell costs zero IPC, setPolledCwds returning the identical state object when nothing changed, omitting no-pid sessions rather than answering them from an unrelated local /proc entry, the inFlight guard, and the document.hidden pause. parseOsc7 correctly ignores the authority and survives malformed percent-encoding. All good.

One real bug.

formatCwdHint drops the leading / for two-segment absolute paths

MAX_TAIL_SEGMENTS = 2, so trimTail only prepends …/ when parts.length > 2. At exactly two segments it emits the bare tail with no marker of any kind:

"/var/log"                -> "var/log"
"/opt/tools"              -> "opt/tools"
"/etc/nginx"              -> "etc/nginx"
"/var/lib/docker/volumes" -> "…/docker/volumes"   ✓
"/etc"                    -> "/etc"               ✓

workspace-relative /home/zeus/projects/codemux/var/log -> "var/log"

(Ran against formatCwdHint from this branch with ROOT = /home/zeus/projects/codemux, HOME = /home/zeus.)

The last two lines are the problem: cd /var/log and cd var/log render identically. The one-segment case explicitly keeps its slash (/etc) and the three-plus case is marked by …/, so the two-segment arity is the only gap — and it's the arity the tests skip. They cover /etc, /, and /var/lib/docker/volumes, jumping straight over the case that breaks.

Fix is a line in the final branch — parts.length <= MAX_TAIL_SEGMENTS should keep the absolute form, e.g. return { label: \/${parts.join("/")}`, full }— or havetrimTailtake a "this is absolute" flag. Either way, add/var/log` to the test table.

Two smaller notes

terminal_session_cwds is a sync command. It runs on the GTK main thread and does one read_link per requested session, every 2s. Each call is trivially cheap, so this is not a real stall — but docs/core/STATUS.md records the deliberate migration of blocking workspace.rs/files.rs commands onto spawn_blocking "so directory listing / ignore checks / workspace ops can't stall IPC", and this is a new blocking command on the polled path. Making it async fn costs nothing and keeps the invariant intact.

The GC loop writes once per dead session.

for (const id of Object.keys(cwdStore.cwds)) {
  if (!live.has(id)) cwdStore.clearCwd(id);
}

clearCwd is a set() per call, so closing a workspace with six terminals fires six store updates and six render passes on the next tick. setPolledCwds already demonstrates the bulk-write pattern; a pruneCwds(liveIds) would match it.

Also worth noting: this branch and #210 both touch src-tauri/src/lib.rs, src-tauri/src/terminal/mod.rs, and src/App.tsx.

@Zeus-Deus

Copy link
Copy Markdown
Owner Author

Review by GPT 5.6 Sol xhigh found a Windows OSC 7 parsing bug.

parseOsc7("file:///C:/Users/z/proj") currently returns /C:/Users/z/proj. The workspace root is represented as C:\Users\z\proj; after normalization the leading slash remains, so formatCwdHint treats the terminal as outside its workspace and renders a misleading hint even while it is at the root. I reproduced the current code path directly:

{"parsed":"/C:/Users/z/proj","hint":{"label":"…/z/proj","full":"/C:/Users/z/proj"}}

Windows has no /proc/<pid>/cwd fallback, so OSC 7 is the important source there. Please normalize file-URI drive paths (/C:/...C:/...) and cover UNC authorities as well. The existing 42 focused tests all pass because the formatter has Windows cases but the parser only has Unix file-URI cases.

Keep the leading slash on short absolute paths: a two-segment path outside the workspace (/var/log) was trimmed to var/log, indistinguishable from a workspace-relative label. Normalize Windows drive paths in OSC 7 URIs (file:///C:/Users -> C:/Users) and preserve the // prefix of empty-authority UNC URIs. Make terminal_session_cwds async with spawn_blocking so the readlink loop runs off the GTK main thread. Replace the per-session clearCwd loop in the poller GC with a bulk pruneCwds store write so closing a workspace with N terminals costs one render pass instead of N.
@Zeus-Deus
Zeus-Deus merged commit dd00b3b into main Jul 27, 2026
4 checks passed
Zeus-Deus added a commit that referenced this pull request Jul 28, 2026
Audited the eleven PRs merged after the v0.15.0 tag (#204, #205, #206,
#207, #208, #209, #210, #211, #212, #213, #214) against the docs system
and normalized the result.

Core docs:
- STATUS: relabel the v0.15.0 content as shipped, add an unreleased
  section covering the eleven post-tag PRs in four clusters, correct the
  avatar-regression constraint (three row shapes, strip exception).
- PLAN: replace the stale "unreleased after v0.14.3" entry, add the
  v0.15.0 release record, and narrow priority 6 to Archive Project now
  that avatar customization has been re-homed.
- INDEX: correct the Thread Scope and Context Row entries (no trailing
  slot; no message-count gate), credit #205/#212 alongside #211, mark the
  inbox shipped, and index the terminal cwd hint.

Feature docs:
- sidebar: fix pending-row placement, document that parked rows report
  real agent status, add workspace-reorder.ts to the dead-code list, and
  defer the avatar mechanism to project-avatars.md.
- project-avatars: all three row shapes are entry points, document the
  dirty-field write-wins merge, and record that the needs-you strip is a
  color-only surface outside the appearance store.
- agent-chat: rewrite the Context Row section (the pane never renders
  ThreadScopeRow), scope the focusCmdkOnOpen claim to the chat and launch
  pickers, and document the WebKitGTK hardening — 2.40 floor, clean-exit
  sentinel disarm, =0 opt-out, sentinel-delete recovery, remote guards.
- terminal: leading-slash rule for short outside paths, OSC 7 Windows
  drive normalization, Linux-only /proc fallback, strip_renderer_env.
- workspace-context-bar: drop the pre-inbox state-driven density framing.
- presets: useHorizontalWheelScroll replaced the passive onWheel handler.
- settings: add the Appearance -> Scrolling toggle.
- execution: correct the scope note (one production reach point; the
  branch_name.rs prepare_agent_command is an unrelated name collision)
  and the openflow_agent_default parenthetical it contradicted.
- FEATURES: add the cwd hint and done-review checkmark persistence.

Also moved the shipped agent-run-checkpoint plan to docs/archive/ per the
INDEX rule and repointed its four references.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant