Merge pull request #825 from l0ng-ai/docs/nested-shell-cwd-698

docs: why a nested shell stops reporting its cwd (#698)
This commit is contained in:
l0ng-ai
2026-09-10 14:47:44 +08:00
committed by GitHub
2 changed files with 117 additions and 5 deletions
+44 -3
View File
@@ -19,11 +19,13 @@ removes itself from the equation if you run the same shell elsewhere.
| **bash** | An rcfile that sources your own first. |
| **fish** | A `-C` init command. |
| **PowerShell** | An encoded init command that wraps your existing `prompt` function and PSReadLine's line reader. |
| **nushell** | A `--config` wrapper whose `config.nu` sources yours first, then adds tty7's hooks. Local panes only. |
| **WSL** *(Windows)* | The distro's shell is bootstrapped with the same scripts. |
| **Remote panes** | The same three POSIX shells, bootstrapped over the SSH connection. Toggle per profile with **Settings → SSH → Session → Shell integration**. |
| **Remote panes** | The same three POSIX shells, bootstrapped over the SSH connection. Toggle per profile with **Settings → SSH →** a profile **→ Advanced → 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.
<Note>
A shell launched with arguments *you* wrote — `shell.args`, or a
@@ -51,10 +53,49 @@ it just falls back to less precise sources:
- Ghost suggestions, the completion menu, and <kbd>⌃ R</kbd>'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 **Settings → Terminal → Shell → Program** (or `shell`
in [config](/reference/configuration)), and leave **Arguments** empty —
arguments you write turn the injection off.
[If the Files 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
+73 -2
View File
@@ -81,8 +81,79 @@ shell:
- **Settings → Input → Prompt → History search**
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.
[shell integration](/reference/shell-integration) — elvish, xonsh and friends
run fine but do not get the prompt layer.
## The directory stopped following my shell
The Files 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 the cwd reporter sits at the end of its
`lib/termsupport.zsh`, behind
```zsh
if [[ -n "$INSIDE_EMACS" || -n "$SSH_CLIENT" || -n "$SSH_TTY" ]]; then
return
fi
```
so over SSH, where sshd sets those, sourcing stops there and the reporter is
never defined — only the title hooks above that line still run. 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 not one tty7 integrates.** It injects into zsh, bash, fish,
PowerShell and nushell locally, and into zsh, bash and fish over SSH — 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**.
## <kbd>⌥ B</kbd> types `∫` instead of moving a word