Add a right-hand detail column showing what the active pane is, not what
it prints: session facts plus its process tree and listening ports
(daemon-side procinfo, pull-based via QueryProcs), the working-tree diff,
and the file tree. Tab row lives in the title bar, body in right_panel.
Also record OSC 133 command marks client-side so the panel's Outline can
list a pane's commands and scroll back to one, keyed on row text since
absolute scrollback indices drift once history fills.
In vertical tab-bar mode the sidebar is a full-height left column and
the real TitleBar only spans the right column, so the rail's
title-bar-height top strip was dead space — you couldn't grab the window
by it. Give it the title bar's behaviour: drag to move, double-click to
zoom, driven the same way TitleBar (and the settings overlay's stand-in
strip) does it, so a plain click and a double-click still land intact.
On Windows a bare `bash` resolves through PATH to
`C:\Windows\System32\bash.exe` — the WSL launcher, not a shell. With no
distro installed it exits non-zero with an empty stderr, which is
indistinguishable from "the shell rejected this script", so the checks
failed the Windows job while reporting nothing to explain why.
`is_msys_bash` guards the production path against the same trap; the test
had no such guard.
Nothing is lost by gating them to unix: these scripts are destined for a
remote POSIX host, so their syntax has nothing to do with the platform
running the test, and the macOS and Linux jobs already exercise them.
A remote pane passes no local cwd, and the argument-position branch answered
that by returning nothing at all. The reasoning in its comment only covered
filesystem paths — offering this machine's filenames for a remote command line
— but the code took `complete_signature` down with them, and a signature's
subcommands and flags are static text describing the *command*, not the
machine. `git push` is just as true over SSH.
The cost was not merely a missing menu. A position with no candidates hands
the line to the shell (`handoff_tab_to_shell`, #136), which clears the inline
editor for the rest of that prompt — so every `git <Tab>` in a remote pane
looked like the editor had crashed. Every git position measured as a handoff:
`git `, `git c`, `git checkout `, `git status`.
Generators stay disabled without a cwd, and that exclusion matters more than
the path one: a generator is a local `/bin/sh -c`, so `git checkout <Tab>`
would have offered the branches of whatever repo tty7's own cwd sat in. A
wrong filename fails loudly when it runs; a wrong branch name is plausible
enough to be accepted. Positions that are generator-only now yield nothing and
hand the Tab to the remote's own compsys, which can actually see that host.
Native-SSH panes reported no OSC 133, so the inline line editor, exit-code
marks and cwd tracking were all inert there — the daemon's OSC sniffer was
already wired up for them and simply never received anything.
Every existing integration configures a *local* process spawn (ZDOTDIR, a
bash --rcfile, fish's -C). An SSH channel offers no spawn to configure, only
the string an `exec` request carries, so the remote path recreates those same
files on the remote side and execs through them. The integration bodies are
reused verbatim rather than forked.
The bootstrap can't be shell-agnostic: sshd runs it as `$SHELL -c <string>`,
so a POSIX script is parsed by fish and a fish script by zsh. Rather than
contort one expression into parsing identically everywhere, spend a probe
round-trip (`echo __tty7_shell; echo $SHELL` — no substitution, assignment or
grouping, so it is valid in all of them) and then emit the dialect we know we
are talking to. The probe is memoized on the connection key, so extra tabs to
an open host cost nothing.
The probe's negative answer is load-bearing: a remote whose login shell is
unrecognized — or that isn't POSIX at all, where `$SHELL` echoes back
unexpanded — falls through to the plain shell request it always used.
Every arm ends by exec'ing the user's own shell, including the failure paths,
so a remote with a read-only $TMPDIR loses the integration and not the session.
zsh only gets ZDOTDIR pointed at the throwaway dir once all four redirectors
are confirmed written; a half-populated dir would silently cost the user their
dotfiles. The dir removes itself on the first precmd, by which point every
startup file has been read.
Add a per-profile switch, on by default and defaulting to on for profiles
saved before it existed, for remotes we *can* integrate but shouldn't.
Reordering used to be a swap on release: gpui's floating drag card followed
the cursor and `on_drop` moved the tab to whichever slot it landed on, with
the list itself frozen for the whole drag. Replace it with a reorder the list
performs live, and add repo group blocks as a second draggable surface.
The new `ui::reorder` module owns the geometry. A drag freezes every slot's
measured bounds and the grab point; each frame the surface reads the pointer,
asks which slot the held item now belongs in, and renders the list in that
order. The held item stays in the list (dimmed, painted over its neighbours)
and tracks the cursor pixel for pixel; the items it displaces slide 140ms.
Slot selection uses half-overlap — the held item's trailing edge past a
neighbour's centre going forward, its leading edge going back — so a tall
group block and a short one swap symmetrically.
Commit no longer goes through `on_drop`, which only fires when the pointer is
over that element at release: every frame the surface records the whole-tab
permutation a release would produce, and the root applies it when the drag
ends, wherever the cursor is. `apply_tab_order` becomes the single commit
path, replacing `move_tab`/`move_group`; it also lays every group out
contiguously, which fixes group-order corruption when a group's tabs were
non-contiguous in the tab vector.
The counts a probe produces are repo-wide — `git diff --numstat HEAD`
ignores the subdirectory it ran in — so panes at `repo/`, `repo/src` and
`repo/docs` were three ways of asking one question, and a window
activation spent one full-repo diff on each.
Count the throttle against the work-tree root once a probe has resolved
one for the cwd, and stamp the clock when the probe is *claimed* rather
than when it lands: without that, panes claiming in the same instant all
pass a throttle nothing has answered yet, which is exactly the shape a
window activation has. In-flight dedup stays keyed by cwd, since it
brackets a specific spawn that finish_probe has to release.
The release workflow is a plain checkout of the tag — nothing rewrites
Cargo.toml there, so the lockfile guard the CI build just gained applies
just as well, and a release is the build you least want silently
re-resolving dependencies. Only nightly stays unlocked: it stamps
Cargo.toml's version, which makes the lock's own root entry stale by
design.
Two lockfile-only dependabot bumps (#139, #140) raised resvg to 0.47.0 and
sha2 to 0.11.0 in Cargo.lock without touching Cargo.toml, which asks for
`resvg = "0.45"` and `sha2 = "0.10"`. Under cargo's 0.x rules the minor
version is the major, so neither requirement accepts the locked version and
the lockfile has been self-contradictory ever since:
$ cargo metadata --locked
error: cannot update the lock file ... because --locked was passed
Nothing failed loudly — CI never passed `--locked` — so the cost landed on
contributors instead: every local cargo invocation rewrote the lock, leaving
a permanently dirty working tree to discard before each commit.
Resyncing drops the duplicates too. gpui-component already pulls resvg
0.45.1, so the tree no longer builds two copies each of resvg, usvg,
tiny-skia, tiny-skia-path, kurbo, svgtypes, roxmltree, imagesize and
polycool.
CI now builds and tests with `--locked` so the next such drift fails in the
PR rather than in a working tree. The release and nightly workflows keep
their unlocked builds on purpose: both stamp Cargo.toml's version and depend
on cargo refreshing the lock's root entry.
The sidebar's `+N -N` only refreshed on three rare edges: the pane changing
directory, a command ending, and an agent turn ending. Edits made anywhere
else produced no signal at all, so the counts sat stale — a long agent turn
showed nothing until it finished minutes later, and a file edited in another
editor never registered until the user happened to run a command in the pane.
Two new triggers close the gap:
- Window activation re-probes every pane. Coming back to the window is the
only cue we get that the tree moved while the user was elsewhere, and the
sidebar lists every tab, so refreshing just the focused pane isn't enough.
- An agent's tool completions re-probe mid-turn. `AgentSessionState` gains an
`activity` counter because `ToolComplete` is deliberately a status no-op
during normal work, leaving status-watchers unable to see it.
Both go through a new throttled claim on `GitStatusCache` that drops triggers
instead of queueing them, so a busy agent or a window full of panes collapses
into one shell-out per repo per 1.5s rather than a `git` storm.
Also: fold the probe's two `rev-parse` calls into one (it now asks for
toplevel, git-dir and common-dir together), which makes `repo_home` a pure
function and unit-testable; and land probe results in the shared cache
independently of the pane entity, so a pane closed mid-probe can't wedge the
cwd-keyed in-flight claim for every other pane in that directory.
Closing the window with zero tabs took the early-return path in
on_window_should_close, which skipped the cx.quit() the confirmed path
runs. gpui does not quit on last-window-close and no on_reopen handler is
registered, so the process stayed alive with no window and clicking the
Dock icon did nothing (#147). Defer the quit onto the next tick so the
close completes first, matching the confirmed path.
Three fixes for tty7's Tab completion:
- Tab is no longer swallowed when the engine has no candidates: the
locally edited line is handed off to the shell (text shipped raw,
cursor walked back, Tab sent) and the local editor suspends until the
next prompt cycle, so shell-native completion (compsys, fzf-tab, ...)
answers instead. The handoff release keys off a new entered-prompt
cycle counter rather than the raw Prompt-frame seq, so same-prompt
redraws (PS1-embedded 133;B re-emissions) cannot re-engage the editor
while zle still holds the handed-off text.
- cd/pushd/popd/rmdir complete directories only in the no-signature
path fallback; Fig 'folders' templates narrow signature slots the
same way. Symlinks now classify by their target.
- New tab_completion config field (default true) plus a Settings ->
Terminal -> Keyboard toggle; when off every Tab goes to the shell.
The sidebar row kept a permanent in-flow slot for the hover-only close
button, so titles and branch names truncated while an empty column sat
at the rail's right edge. Move the close affordance out of flow, same
Safari-style float the strip's chips already use: on hover the x sits
over the title line's right end on a solid backing (the row's hover
fill, flattened against the sidebar surface) with a short gradient
run-in, so covered text fades out instead of hard-cutting.
Pinned to the row top rather than vertically centered: on a two-line
row a centered x would straddle both lines and cover the branch line's
+n/-n counts, which are the diff-overlay click target. The cmd-N hint
badge keeps its in-flow slot (deliberate all-rows modal reflow).
Resume-after-restart replayed a hardcoded per-agent command
(claude --resume <id>), dropping whatever flags the agent was
originally launched with (--dangerously-skip-permissions, --model).
The daemon's foreground poll already reads the agent's argv for
detection; keep it, stream it to the client inside AgentSessionState
(serde-default, wire-compatible both ways), persist it in the session
Leaf, and splice a conservatively-gated flag tail into the resume
command. The gate refuses anything that is not a plain flag-shaped
token sequence and falls back to the bare table command.
The Windows 133;C typed-command capture is forgeable by terminal
output, so it contributes identity only, never flags. Copilot gains a
resume entry (copilot --resume <id>, hooks already report its session
id) and Amp's threads continue verified to accept global flags.
`wsl_remote_context` read the per-spawn `ShellSpec` override, but the
program actually spawned comes from `choose_shell`, which falls back to
`config.json`'s `shell` when there is no override.
So `{"shell": {"program": "wsl.exe", "args": []}}` reached `setup_wsl`
— empty args, so nothing custom to preserve — and the distro began
reporting its own cwd over OSC 7, while `wsl_remote_context(None)` left
the pane untagged. `local_cwd` then accepts `/home/me/proj` and hands it
to the local git probe, which Windows resolves drive-relative to
`C:\home\me\proj`: exactly the collision the tag exists to prevent, and
newly reachable because integration is what makes such a pane report a
cwd at all.
`choose_shell` now runs in `build_spawn_config` and its result feeds both
the tag and `build_shell_command`, so the two cannot describe different
shells. Taking `ChosenShell` rather than `ShellSpec` is what keeps it
that way — the resolved type is only available after the fallback.
Also drop the second distro parser: the tag now reads the distro through
`shell_integration::wsl_distro`, which additionally understands
`--distribution=NAME`. Both are handed the same argv, so one parser is
the only way they agree.
736 tests pass, clippy warning count unchanged (124).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`setup_wsl` resolved the distro's login shell with a synchronous `wsl.exe`
call. The client waits for the daemon's `Spawn` reply
(`terminal::remote::spawn`), so on a cold WSL start — seconds, while the
distro boots — the entire window froze. Reported from a real session; the
`--cd`/`-d` unit tests never saw it because they never reach the probe,
and the live-PTY test only ever ran against an already-warm distro.
Caching per distro was not a fix: the first WSL pane after launch is
exactly when the distro is cold, so the freeze hit precisely the case the
cache could not cover.
Fold the decision into the one `wsl.exe` invocation we were always going
to make. The command is now `sh -c` over a `case` on `$SHELL` that execs
bash with our rcfile, or falls back to a plain login shell for a distro we
don't integrate. It cannot block, because there is no second invocation.
`$SHELL` rather than `getent passwd`: WSL populates it from the user's
passwd entry, so inside the distro it already is the login shell of
record — the same source `shell_kind` trusts on Unix. Written without a
variable assignment so the whole thing stays one `case`, robust to the
layers of quoting between the daemon and `sh`.
The rcfile is now written before the shell is known. That is a local write
into a throwaway dir the terminal already cleans up on drop, and paying it
unconditionally is what buys the decision being free.
Regression test names a distro that cannot exist and asserts setup still
succeeds — if anything asked the distro a question, it could not. A timing
bound would only have caught this on a cold machine, which is the same
blind spot that let it ship.
Removes `wsl_login_shell` and `inner_shell_kind`, both now unreachable.
736 tests pass, clippy warning count unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`wsl.exe` is a launcher, not a shell, so the integration has to reach
through it into the distro. Probe the distro's login shell, write the
matching rcfile on the Windows side, and pass its path in via `WSLENV`,
whose `/p` flag rewrites it to the distro's own view of the filesystem
(`C:\…` -> `/mnt/c/…`) — so the `/mnt` automount root, which is
configurable in `/etc/wsl.conf`, is never hardcoded.
The argv becomes `[<launch flags>] -- sh -c 'exec <shell> --rcfile "$RC" -i'`
rather than `-- <shell> --rcfile <path>`: the path only exists as an env
var *inside* the distro after translation, and `wsl.exe` execs its command
directly with no shell to expand it. The one-shot `sh` execs away at once.
No new shell code — the distro runs bash, so the existing snippet applies
verbatim. Only bash is wired up; zsh and fish inside a distro are
reachable the same way but each needs its own verification pass, and
declining leaves those panes launching bare, as every WSL pane did before.
Tag WSL panes with a new `RemoteKind::Wsl` so `TerminalView::local_cwd`
declines their cwd. This is the load-bearing half: the distro reports
`/home/me/proj`, which Windows reads not as invalid but as *drive-relative*,
resolving to `C:\home\me\proj`. Without the tag, the local git probe, path
completion, link resolution and cwd inheritance would all consume it — and
on a machine that happens to have such a directory, silently consume the
wrong one. The gate itself landed in #133; this adds the third kind to it.
Two consequences of that tag needed explicit handling, since nothing
matches exhaustively on `RemoteKind` and every miss would have been a
silent fall-through:
- the foreground-`ssh` poll cleared any context the probe didn't produce,
which would have blanked the WSL tag (and the pane's cwd with it) twice
a second. It now only replaces the kind it authors.
- the tab status dot and `active_ssh_pane` treated "has a RemoteContext"
as "is an SSH pane". Both now test the kind.
`Injection::force_non_login` is renamed `replaces_argv`: bash needed it
because `--rcfile` is ignored for login shells, WSL needs it because the
launch flags and command must be reordered around `--`. The mechanism was
always "these args replace rather than extend"; only the name was bash's.
Verified end-to-end on a real ConPTY into a real distro — the new test
asserts the full A/B/C/D cycle comes back through `wsl.exe`, which is the
only way to show `WSLENV` translation, `wsl.exe`'s argv passing and the
distro's own startup chain all survive together. It shares its harness
with the Git Bash test, including the two ConPTY behaviors that harness
encodes.
736 tests pass, clippy warning count unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>