From 4af445290040909877bdb1308bd976fa8edbee73 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Wed, 9 Sep 2026 17:59:29 +0800 Subject: [PATCH] docs(shell-integration): explain why a nested shell stops reporting its cwd (#698) tty7 injects shell integration into the shell it launches for a pane, and only that one. A shell the user starts by hand afterwards -- `zsh` typed at a bash prompt -- is a new process nobody injected into, so it emits no OSC 7 and no OSC 133, and the pane keeps showing whatever directory the outer shell reported last. On a local machine that is invisible, because oh-my-zsh reports OSC 7 itself from lib/termsupport.zsh. Over SSH it is not: that file returns early when SSH_CLIENT or SSH_TTY is set, which sshd always sets, so on a remote host neither oh-my-zsh nor tty7 reports the directory of a hand-started zsh. That combination is the whole of #698, and it is why the reporter sees it work over SSH under bash and stop under zsh. Documented in two places: a "Shells you start yourself" section in the shell integration reference, and a troubleshooting entry that names the symptom the way a user would ("the directory stopped following my shell"). Both give the two remedies -- `chsh -s /bin/zsh` on the remote host, which is what moves the integration into the shell tty7 bootstraps, and, when the login shell cannot be changed, a four-line precmd hook that reports OSC 7 by hand. The entry also says why a login script or `exec zsh` from .bashrc is not a fix: both run after (or instead of) the shell tty7 set up. The same pages now describe the process-inspection fallback honestly. It was one parenthetical -- "tty7 falls back to inspecting the process" -- which overpromises: it exists only where the daemon can see the pane's processes, so a local macOS/Linux pane or a remote-workspace pane gets it and an SSH pane or a Windows pane does not, and it is a poll driven by pane output at most twice a second, not a report, so it trails a `cd`. That poll is the mechanism behind the issue's second, unconfirmed half: with tty7-server on the remote host the daemon is on that host, so a nested zsh's cwd does get picked up out of /proc -- late, and only when the pane writes something. Deliberately no code. Propagating the integration into a shell the user starts by hand would mean exporting ZDOTDIR (or rewriting the user's startup files) from every pane, which leaks into every zsh in the session including scripts, and the remote bootstrap deletes its throwaway ZDOTDIR at the first prompt precisely because an SSH session has no reliable exit hook -- a directory that outlives its deletion is exactly what would break a nested shell rather than help it. Tightening the cwd poll for uninstrumented panes is a real possibility, but it is a Linux-only daemon path that cannot be exercised from here, for a symptom nobody has yet reproduced; the mechanism is written down instead. Claude-Session: https://claude.ai/code/session_01UUyWQXzcBAoBzaSX8pc7nU --- docs/reference/shell-integration.mdx | 43 ++++++++++++++++- docs/reference/troubleshooting.mdx | 69 ++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/docs/reference/shell-integration.mdx b/docs/reference/shell-integration.mdx index 89a029ab..ee162243 100644 --- a/docs/reference/shell-integration.mdx +++ b/docs/reference/shell-integration.mdx @@ -23,7 +23,8 @@ removes itself from the equation if you run the same shell elsewhere. | **Remote panes** | The same three POSIX shells, bootstrapped over the SSH connection. Toggle per profile with **Settings → SSH → Session → Shell integration**. | `TTY7_SHELL_INTEGRATION` is set once it is active, and guards against a second -injection when shells nest. +injection when shells nest. It does not put the integration *into* a nested +shell — [shells you start yourself](#shells-you-start-yourself) have none. A shell launched with arguments *you* wrote — `shell.args`, or a @@ -51,10 +52,48 @@ it just falls back to less precise sources: - Ghost suggestions, the completion menu, and ⌃ R's fuzzy history - "Command finished" notifications and the failure marks in history search -- Exact working-directory tracking (tty7 falls back to inspecting the process) +- Exact working-directory tracking Panes, splits, scrollback, search, SSH, and the CLI are unaffected. +The working-directory fallback is worth knowing in detail, because it is not +available everywhere. Where tty7 can see the pane's processes it reads the +foreground one's own working directory instead — a local pane on macOS or +Linux, or a pane in a remote workspace, where the `tty7-server` on that host +does the reading. It is a poll, not a report: it is sampled while the pane is +producing output, at most twice a second, so it can trail a `cd` until the +pane writes something again. A local pane on **Windows** and a pane connected +to an **SSH host** have no fallback at all — on the far end of an `ssh` there +is no process table to read, so a shell that does not report its directory +leaves the pane showing the last directory that did. + +## Shells you start yourself + +The injection happens when the pane's shell is *launched*. Type `zsh` at a +bash prompt — or `bash`, or `sh`, or `docker exec … sh` — and that second +shell is a new process, launched by your shell rather than by tty7. It emits no +prompt marks and no `OSC 7`, and the pane keeps showing the directory the +outer shell last reported until you exit back to it. + +On your own machine this is usually invisible, because prompt frameworks +report the directory themselves: oh-my-zsh does it from +`omz_termsupport_cwd` in `lib/termsupport.zsh`. **Over SSH it is not.** That +file returns early when `SSH_CLIENT` or `SSH_TTY` is set, so on a remote host +a zsh you started by hand reports nothing at all, and neither does tty7. + +To get integration into that shell, make it the shell tty7 starts: + +- **A remote host.** tty7 asks the host for `$SHELL` when it connects and + bootstraps that shell, so `chsh -s /bin/zsh` (then reconnect) is what + switches it. A login script under **Advanced → Session** that runs `zsh` + does *not* — those lines are typed into the session after the shell is + already up, so they nest like anything else. Neither does `exec zsh` from + your `.bashrc`: it replaces the process tty7 set up. +- **A local pane.** Set `shell` in [config](/reference/configuration), and + leave `args` out — arguments you write turn the injection off. + +[If the file panel stopped following a nested shell →](/reference/troubleshooting#the-directory-stopped-following-my-shell) + ## Per-pane history When `per_pane_history` is on, the integration is also what makes it work. It diff --git a/docs/reference/troubleshooting.mdx b/docs/reference/troubleshooting.mdx index e06c3746..3b9d3a99 100644 --- a/docs/reference/troubleshooting.mdx +++ b/docs/reference/troubleshooting.mdx @@ -84,6 +84,75 @@ If they do nothing at all in a particular pane, the shell there probably has no [shell integration](/reference/shell-integration) — nushell, elvish, xonsh and friends run fine but do not get the prompt layer. +## The directory stopped following my shell + +The file panel, new splits, and the sidebar's repo grouping all follow the +directory the pane's shell reports (`OSC 7`). If the pane is frozen on one +directory while you `cd` around, something in the pane is not reporting it. + +The usual cause is a shell you started by hand. tty7 injects its +[shell integration](/reference/shell-integration) into the shell it launches +for the pane, and nothing else: run `zsh` at a bash prompt and that nested +shell has none. Locally you rarely notice, because oh-my-zsh reports the +directory itself — but its `lib/termsupport.zsh` begins with + +```zsh +if [[ -n "$INSIDE_EMACS" || -n "$SSH_CLIENT" || -n "$SSH_TTY" ]]; then + return +fi +``` + +so over SSH, where sshd sets those, oh-my-zsh does not define the reporter at +all. A nested zsh on a remote host therefore reports nothing, from either +side, and the pane keeps showing wherever the outer shell was. + +**Make it the shell tty7 starts.** On the remote host: + +```sh +chsh -s /bin/zsh # then reconnect +``` + +tty7 asks the host for `$SHELL` at connect time and bootstraps *that* shell, +so this is what moves the integration into your zsh — oh-my-zsh keeps working +beside it. Starting `zsh` from a login script, or `exec zsh` from your +`.bashrc`, does not help: both run after (or instead of) the shell tty7 set +up. + +**Or report the directory yourself.** If you cannot change the login shell, +add this to the remote `~/.zshrc` — it restores directory tracking only, not +prompt marks or command status: + +```zsh +# Report the working directory to tty7 (OSC 7). +autoload -Uz add-zsh-hook +__tty7_cwd() { printf '\e]7;file://%s%s\a' "${HOST:-localhost}" "${PWD//\%/%25}" } +add-zsh-hook precmd __tty7_cwd +``` + +It is safe to leave in place afterwards: when tty7 *is* integrating that +shell, the two hooks simply report the same directory twice. The bash +equivalent is `PROMPT_COMMAND`: + +```bash +__tty7_cwd() { printf '\e]7;file://%s%s\a' "${HOSTNAME:-localhost}" "${PWD//%/%25}"; } +PROMPT_COMMAND="__tty7_cwd${PROMPT_COMMAND:+;$PROMPT_COMMAND}" +``` + +Two other reasons a pane never reports its directory: + +- **The shell is one tty7 does not integrate.** Locally: zsh, bash, fish, + PowerShell, nushell. Over SSH: zsh, bash, fish. A remote host whose + `$SHELL` is anything else gets no bootstrap, silently. +- **You gave the shell your own arguments** in `shell` or a `custom_shells` + entry, which turns the injection off by design. + +Where tty7 can see the pane's processes it falls back to reading the +foreground process's working directory, which is why an unintegrated shell in +a **remote workspace** mostly keeps up — that fallback is a poll, though, +sampled while the pane produces output, so it can lag a `cd` until the pane +writes again. There is no such fallback for an **SSH pane** (the processes are +on the far end) or for a local pane on **Windows**. + ## ⌥ B types `∫` instead of moving a word That is macOS's default. Turn on **Settings → Input → Keyboard → Option (⌥) acts