mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-24 00:02:24 +00:00
6f91c9a640887c317adc87a39f2fe85214d55708
53
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
90763385e2 |
fix(macos): derive the seeded UTF-8 locale from the system locale
The GUI-launch locale fallback exported a literal `LC_CTYPE=UTF-8`. That name is a BSD libc alias with no glibc equivalent, and the stock `ssh_config` ships `SendEnv LANG LC_*`, so it rode along to every host we ssh into. There `LC_CTYPE` outranks the `LANG` the host sets for itself and then fails to load, dropping the remote shell to the C locale -- re-creating the mangled non-ASCII output the fallback exists to prevent. Derive the locale from the system locale instead, the way Terminal.app and iTerm2 do: reduce the CFLocale identifier to its POSIX `lang_REGION` stem, and fall back to `C.UTF-8` then `en_US.UTF-8`. Every candidate is checked against `/usr/share/locale` before it is exported, so tty7 never hands a shell a name the C library cannot load -- including on machines whose region combination has no installed locale (`en_CN` is an ordinary macOS setting that resolves to no locale at all). Still `LC_CTYPE` only, and still only when no locale is inherited or configured, so message/date/number localization and explicit user overrides are untouched. Fixes #178 |
||
|
|
d95229411d | fix(config): respect explicit locale overrides | ||
|
|
8b2b4645c6 | fix(macos): seed UTF-8 locale for GUI-launched shells | ||
|
|
7789dc4c36 |
Merge branch 'main' into feat/multi-window
Conflict in the sidebar's control row: main wrapped both tiles in `occlude()` so Windows' HTCAPTION drag doesn't swallow their clicks, while this branch moved their geometry onto `chrome_tile_sized` / the shared `TILE_*` constants. Kept both — occluded wrappers around helper-sized tiles. |
||
|
|
e41afaf857 |
feat(windows): one window per workspace
tty7 had exactly one window, so `main` opened it inline and every app-wide duty — tray, menus, the quit hook — lived in `Tty7App`'s constructor. This splits those apart: a *workspace* is the persistent identity (tabs, splits, cwds, name) and a *window* is a transient view onto exactly one of them. - `ui::windows` — the app-level window registry and the single place that opens a window. Exactly one window per workspace is enforced there: the daemon gives each pane a single subscriber, so a second window on the same panes would silently steal the first's output. `open` focuses the existing window instead. New windows cascade so one never lands on top of another. - `WorkspaceStore` owns session.json, so windows never race each other as writers. Closing a window *detaches* — panes keep running in the daemon and the entry stays for the picker; `StopWorkspace` kills the sessions and keeps the layout; `DeleteWorkspace` also forgets it. - Window menu lists every workspace with a monogram badge and a liveness dot, ⌘1–9 for the first nine. Same list in the palette; closed ones also appear in a home-page picker with a coarse relative age. - Sidebar collapse and right-panel visibility move onto `Tty7App`, so toggling one window's chrome leaves the others alone; the config value becomes what a new window starts with. Panel *width* stays shared — a width is a preference, not a view state. - Tray, menus, and the quit hook now walk the registry rather than belonging to a single window. Protocol goes to v2: `RemoteKind::Wsl` is a new enum variant, which is not the additive change it looks like — the enums carry no `#[serde(other)]`, so a v1 peer fails the whole decode and drops the pane's connection. The handshake now catches that skew and offers a restart. |
||
|
|
0ebe68a453 |
feat(ssh): fold port forwarding and SFTP into the detail panel
Both SSH tools floated over the terminal: a tunnel icon and an SFTP icon pinned top-right, opening a 460px popover and a bottom dock. They are pane facts, so they now live where the pane's other facts already are. Port forwarding becomes a Forwards band on the Info tab, under Ports — one says what the pane listens on locally, the other what it routes across the connection. Rows take the panel's language: a mono kind letter, the bound port as the same chip a listening port gets, hover to remove, click to edit. The add form is inline, stacked to fit the column. The list re-lists on the Info tab's existing 2s poll, so a forward that dies remotely turns red on its own. SFTP becomes the Files tab's remote mode: the tab follows the detail pane, showing a local repository tree or that machine's filesystem. Same browsing model as before (breadcrumb, filter, `..`-led list, per-row right-click) relaid out for ~260px — the toolbar collapses to refresh plus a `⋯`, and the permissions column moves into the chmod form, which now names the mode it is editing. The header carries the hostname: the tab swaps between two filesystems as the pane changes, and it can rename and delete. Transfers become a footer on the panel column rather than a tray inside SFTP. It sits below every tab, so reading Info doesn't hide a running upload, and stays pane-scoped rather than aggregating every pane, which would quietly make the panel a window-level transfer centre. Opening the browser gained a step: the shell's cwd needs tty7's shell integration on the remote, which a freshly-connected host rarely has, so it fell through to `/`. A new SftpOp::Realpath resolves the login directory instead. Per-pane positions are recorded on arrival, so a first landing at `/` can no longer be remembered as a preference. With nothing floating over the terminal any more, the ⌘F find bar gets its top-right slot back — it used to be suppressed while those icons were up. |
||
|
|
f6ba623af0 |
fix(procinfo): the Windows process table reads Proc::parent
`winproc::Proc` names the parent link `parent`, not `ppid` — the Windows arm of `process_table` had never been compiled, so it took a CI run to find it. |
||
|
|
fd1062f564 |
fix(right-panel,editor): restore the git dependency and address review findings
The branch had `gpui-component` pointed at a sibling checkout by absolute path, which is why every CI job failed at manifest load. Point it back at the fork's `tty7` branch (now carrying the custom-button label-color fix the chrome tiles depend on) with the `tree-sitter-languages` feature, and re-lock. Review fixes on top: - **Changes tab churned.** `right_panel_invalidate` dropped the cached diff on every `GitStatusCache` notification — including unrelated repos' — so the list blanked to "Loading…" and spawned a fresh `git diff` several times a second while a pane produced output. Replaced by `right_panel_refresh_changes`, which compares branch and totals first and re-probes in place, mirroring the diff overlay. - **Changes tab could wedge on "Loading…".** A probe dropped because the cwd changed mid-flight left `diff_cwd` set and `diff` empty, and the render path only spawns when the cwd *changes* — so nothing re-probed. Spawn when nothing is cached and nothing is in flight. - **Find references blocked the UI thread.** `cx.spawn_in` runs on the main thread; the up-to-200 `read_to_string`s for the row previews now run on the background executor, as the comment already claimed. - **LSP frames could be lost or reordered at startup.** `send` checked `ready` outside the `queued` lock, so a frame could park behind a handshake that had just finished and never go out. `ready` now flips under that lock in `mark_ready_and_flush`. - `MarkScanner`'s ESC-in-payload branch bypassed the payload cap, so a stream of bare ESCs inside an unterminated OSC grew the buffer without bound. - The file tree's search frontier used `Vec::remove(0)`; a wide tree made that quadratic. `VecDeque`. - `procs()` documented a pane check it didn't make; it takes the pane id and makes it. - Four doc comments had been orphaned onto newly inserted functions (`pty`, `smooth_scroll`, `foreground_agent`, `file_expanded`). |
||
|
|
649fdef51e |
Merge branch 'main' into worktree-code-panel
# Conflicts: # src/core/config.rs # src/ui/mod.rs # src/ui/tab_sidebar.rs # src/ui/tab_strip.rs |
||
|
|
403cfd47a1 |
feat(right-panel): docked detail panel with Info, Changes and Files tabs
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. |
||
|
|
7cfdba1772 |
test(ssh): skip the real-shell parse checks on Windows
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. |
||
|
|
27c1cdbf3f |
feat(ssh): bootstrap shell integration into native SSH sessions
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. |
||
|
|
8955b1545f |
fix(git-status): refresh the sidebar counts on window focus and tool calls
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. |
||
|
|
e3edcdb153 |
feat(agent): carry launch flags onto session resume commands
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. |
||
|
|
ef4160d398 |
style: rustfmt
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
c780799c31 |
fix(wsl): tag the shell we launch, not the one we were asked for
`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>
|
||
|
|
f91707e2a7 |
fix(wsl): don't block the spawn path probing the distro's shell
`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> |
||
|
|
5f7a771938 |
feat(shell-integration): support WSL
`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> |
||
|
|
6442d33c8c | Merge remote-tracking branch 'origin/main' into fix/remote-cwd-local-ops | ||
|
|
9023e1ada8 | Merge remote-tracking branch 'origin/main' into feat/gitbash-shell-integration | ||
|
|
08ca3a3cf7 |
fix(cwd): close the agent-cwd bypass and validate inherited dirs
Follow-ups to the `local_cwd` gate. The agent-reported cwd sat ahead of `local_cwd` in the git-status chain and bypassed the gate entirely. A native-SSH pane keeps sentinel-sourced agent state on purpose, so an agent running *on the remote host* reported a remote path that won unconditionally and reached the local `git` — the exact collision the gate exists to prevent, on its most likely trigger (running `claude` in an SSH pane). Route the agent's report through the same remote check. Completion's fallback to `std::env::current_dir()` meant a remote pane now offers *this* machine's filenames for insertion into a remote command line, where before the remote path simply failed `read_dir` and produced nothing. `complete` takes `Option<&Path>` so "no local filesystem" is an explicit contract: command completion still runs, path and signature sources are skipped. `apply_remote_context` left `st.cwd` pointing into the namespace it just left, so after `exit` from `ssh` a local shell without shell integration kept serving the remote's last path to the local `git` probe. Clear it on both sides of the boundary; `DaemonMsg::Cwd` has no cleared form, so the client mirrors it off `RemoteContext`. Finally, validate in `initial_working_directory`: whatever wins must be a directory *here*. This bounds the whole class rather than one shell's spelling — an unresolvable path now falls through to the next candidate instead of failing the spawn with "The directory name is invalid". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
e723bc7fd1 |
fix(shell-integration): decline WSL's bash.exe and untranslatable cwds
Two holes in the Git Bash support. `shell_kind` matched any `bash.exe`, including `C:\Windows\System32\bash.exe` — the WSL launcher, which exists on any machine with WSL and normally precedes `Git\bin` on PATH. Injecting into it is destructive rather than merely useless: `--rcfile` *replaces* `~/.bashrc` instead of supplementing it, and the Windows path we pass does not exist inside the distro, so the user silently loses aliases, prompt, and PATH. Identify msys bash positively and fail closed — a bare `bash`/`bash.exe` is declined too, since its PATH lookup is exactly what we cannot predict. The cost of a false negative is only the absence of a feature that did not exist before. `pwd -W` is the identity for msys-only virtual mounts (`/proc`, `/dev`), which have no Windows path at all, and the `[[ "$d" != /* ]]` test could not tell a translated path from an untranslated one. Require a drive letter and report nothing otherwise: the `$PWD` fallback would have re-emitted the very msys path this branch exists to avoid, landing as drive-relative `C:\proc` and failing the next spawn. Staying silent leaves the daemon holding the last usable cwd. The prior OSC 7 test asserted only that the marker was emitted, so the payload shape went unchecked; it now round-trips through the daemon's own `parse_osc7`, including the case the guard suppresses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
87f865d55a |
fix(ssh): hide the console for ProxyCommand children too
The daemon is spawned detached with no console of its own, so a ProxyCommand launched from it (`ssh -W`, `connect.exe`, `cloudflared`) had Windows allocate one — not a flash but a black window that stayed up for the whole session. `hide_console` takes `std::process::Command`; this site builds a `tokio::process::Command`, which is a distinct type with its own `creation_flags`. Add `hide_console_tokio` alongside it so the module comment's claim that every non-PTY Command goes through this file holds again. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
caec744bfa |
fix(shell-integration): report a Windows path from Git Bash
Git Bash's $PWD is an msys path (`/c/Users/x`, and `/tmp` for mounts with no drive at all). The daemon runs Windows-side, where `/c/Users/x` is not absolute but drive-relative, so it resolved to a bogus `C:\c\Users\x`: `strip_uri_drive_slash` only un-prefixes the `/C:/…` shape. That silently disabled the git-status probe and path completion in every Git Bash pane, and broke split/new-tab outright — an inherited cwd wins over every fallback in `initial_working_directory`, so the next shell was spawned with a working directory that does not exist. Worse than either, `/c/Users/x` is a *valid* drive-relative path, so a machine that happens to have `C:\c\...` would have shown an unrelated repo's status as the pane's own. Use `pwd -W`, msys's own translation to the real Windows path, which also maps msys-only mounts correctly (`/tmp` -> `AppData/Local/Temp`). It has no leading slash, so add one for the file: URI shape. Falls back to $PWD if it fails or returns empty, and the branch is taken once at install time rather than on every prompt. The live-PTY test now round-trips what Git Bash actually emits through the daemon's own `parse_osc7` and asserts the path exists, so the two halves are verified to agree rather than checked in isolation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
a95f890497 |
feat(shell-integration): support Git Bash on Windows
PowerShell was the only Windows shell that got OSC 133 integration. Git Bash was excluded by two unrelated gates, neither deliberate: `shell_kind` stripped `.exe` only for PowerShell, so the `bash.exe` that `core::shells::find_git_bash` resolves matched nothing — even though the comment right above it reasons about exactly that suffix. Strip it for every shell instead. The Git Bash dropdown row also ships `-i -l`, which tripped `has_custom_args` and made `setup` decline bash outright. But that guard exists to protect args the *user* configured; these are tty7's own, from `detect_shells`. `ShellSpec` now carries who authored its args, so integration may respell tty7's (`--rcfile … -i` plus the replayed login-file chain means the same thing) while still leaving the user's alone. The `-i -l` stay as the fallback for when integration doesn't apply or fails to set up. One msys2 detail: the rcfile path is now spelled with forward slashes, which its runtime accepts just as readily and which carry no second meaning in the bash string contexts the path can reach. Verified end to end, not just by construction: a new live-PTY test spawns the real Git Bash through the real `setup` output and asserts the full A/B/C/D cycle plus OSC 7 come back. It skips when Git for Windows isn't installed. Getting it green surfaced two ConPTY-isms worth recording — the master doesn't reliably EOF when the child exits, and closing its input side raises a console control event that kills the shell with STATUS_CONTROL_C_EXIT — both noted at the call sites. cmd and WSL stay unintegrated, now documented as decisions rather than gaps: cmd's only hook is PROMPT, which cannot emit C or D, and since only C clears `at_prompt` an A/B-only shell would leave the line editor holding the keyboard for the whole of every command. WSL would need per-distro shell detection and WSLENV path translation to reach the shell that actually runs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c70f9e0881 |
feat(agent-hooks): follow the agent's own cwd for the sidebar git line
Claude Code's EnterWorktree chdirs the agent without any shell cd, so the sidebar's branch/diff line could not see the move on platforms without the proc-cwd fallback. Forward the cwd field every Claude Code hook payload already carries through the OSC 777 sentinel, keep it on the pane's agent session state, and let it take precedence over the proc probe for the git line. The claim is released on session-end (and when the agent leaves the foreground), so an exited agent falls back to the pane's real directory. |
||
|
|
4d2094ff90 |
test(daemon): poll for the child's exec before asserting its path
Command::spawn returns after the fork, possibly before the child has exec'd sleep; until then /proc/<pid>/exe still points at the test binary itself, so asserting the basename immediately is a race. This intermittently failed the Linux CI job on main (runs 29549626571, 29560431777, 29563049775). Poll process_path for up to 5s and carry the last-seen basename into the failure message. |
||
|
|
e16708956f |
fix(agent): only apply mark-derived detection when the C capture changes
A stray foreign A/B mark mid-command re-delivers the same capture; when that capture names no agent (a wrapper script the matcher can't see through), re-applying its None wiped an identity the sentinel events had established. Gate the Windows apply on the capture changing, document the bare-C trade-off (our own PowerShell fallback emits it), and cover the gate, %0A multi-line decode, and whitespace-only payloads in tests. |
||
|
|
775f0b7024 |
feat(agent): detect coding agents on Windows via shell-integration command capture
ConPTY has no foreground process group, so the Unix process-table poll (pgid -> argv) has no Windows equivalent and foreground_agent was a stub returning None. Follow Warp's approach instead: the shell integration captures the submitted command line at preexec and carries it percent-encoded on the OSC 133;C mark; the daemon detects the agent from that string. - shell_integration: all four bodies (zsh/bash/fish/PowerShell) append the submitted line to 133;C, truncated to 512 chars and escaped (% ESC BEL CR NL). PowerShell guards the surrogate-splitting truncation and wraps the emission in try/catch (EscapeDataString throws on lone surrogates under .NET Framework / PS 5.1). - pane: ShellState.command stores the capture (cleared on D only, so a stray foreign A/B mid-command can't wipe the chip); on Windows apply_signals feeds it through the new detect_from_command_with. - cli_agent: detect_from_command_with tokenizes a typed command line (quotes, & call operator, case-insensitive) and reuses the argv detection; base_stem handles backslash paths and .exe/.cmd/.bat/.ps1. - ForegroundProbes::agent is now Option<Option<CLIAgent>>: None means 'no process-table view' (native SSH, Windows) and is never applied, fixing the 0.5s poll wiping event-branded agents on native-SSH panes. |
||
|
|
f4e63d076e |
fix(daemon): detect shell vi mode via durable signals
zsh: plugins like zsh-vi-mode rebind ^[ to their own widgets, so sniffing the Esc widget for vi-cmd-mode missed them. Key off the main keymap link instead (bindkey -A viins main), which both plain bindkey -v and zsh-vi-mode establish. bash: [[ -o vi ]] misses vi mode configured only in ~/.inputrc (set editing-mode vi flips readline without the shell option); read readline's actual mode via bind -v instead. |
||
|
|
8180d22166 | fix(terminal): support shell vi mode | ||
|
|
19430424f5 |
Merge pull request #91 from l0ng-ai/fix/ring-replay-geometry
fix(daemon): segment the replay ring by geometry so attach replays wrap correctly |
||
|
|
0334c97a65 |
fix(daemon): cap the replay ring's segment count
Drag-resizing a pane whose TUI redraws on every SIGWINCH cuts a tiny segment per column change; such segments never fill RING_CAP, so over a long-lived pane's life they accumulated without bound and attach replay (one full client-side reflow per Size) degraded linearly. Past MAX_RING_SEGMENTS (64) the two oldest segments now merge, replaying the older bytes at the newer geometry — like the byte cap, precision degrades from the oldest scrollback first. Also correct the client reader's pending_size comment: the one-lock guarantee is per Size/Snapshot pair, not across the whole replay. |
||
|
|
b33fcc7035 | chore(docs): drop the docs directory and stale plan files | ||
|
|
363d2592f6 |
fix(daemon): segment the replay ring by geometry so attach replays wrap correctly
The replay ring stored raw PTY bytes with no geometry history and attach replayed all of it at the final recorded size. Any resize during a session (pane split, window drag) meant older bytes re-wrapped at the wrong width on replay, so a TUI's cursor-up redraws (Claude Code's inline renderer is the canonical case) landed mid-frame and every redraw leaked stale rows into the reattached pane's scrollback -- duplication that never existed live (10 markers live vs 45 replayed in the regression scenario). The ring is now a sequence of geometry-tagged segments: resize seals the current segment (retagging an empty tail in place), cap eviction drops emptied segments, and attach replays a Size -> Snapshot pair per segment. The client reader already applies each Size to its grid right before the paired Snapshot advances (pending_size), so it reflows between segments exactly where the live client did -- no client changes needed. |
||
|
|
5a1f4c8181 | style: cargo fmt | ||
|
|
35a16638f1 |
feat(daemon): ask before restarting a version-mismatched daemon
Startup used to silently stop a daemon speaking a different protocol, killing every persisted session without warning. The old daemon is still serving its panes fine — the mismatch may be benign for the messages actually exercised — so the call is now the user's: keep it and reuse the sessions, record the mismatch, and have the first window raise a Keep Sessions / Restart Daemon prompt (restart reuses the confirmed half of the existing Restart Daemon flow). Only a daemon that cannot answer the handshake at all (wedged, timeout) is still replaced outright, since it cannot serve its sessions either way. |
||
|
|
96360c544e |
feat(daemon): version handshake so an upgraded GUI restarts a stale daemon
The daemon outlives the GUI binary, so after an app upgrade the running daemon can speak an older wire dialect. ensure_running now asks a live daemon for its protocol version (new Version request/reply, kind 40) before reusing it and restarts it on a mismatch; a pre-versioning daemon drops the unknown kind, which reads as "replace it" too. |
||
|
|
cd9577c590 |
feat(agents): recognize CLI coding agents + git branch in the sidebar (#85)
* feat(agents): recognize CLI coding agents + show git branch in the sidebar Observe (never wrap) third-party coding agents running in a pane — Claude Code, Codex, Gemini CLI, Aider, Amp, OpenCode and ~10 more — and enrich the UI around them, plus front each sidebar row with its git branch and diff. Detection & identity - Command-based detection over the foreground argv (launcher basename, and interpreter-wrapped `node …/cli.js` / `npx …` forms), with user rules via `agent_commands` in config. Brand avatars on the tab chip and sidebar row. Rich status channel - A per-pane state machine (idle / working / waiting-for-you / done) driven by agent-reported events over an OSC 777 sentinel channel (`tty7://cli-agent`, versioned JSON), sniffed daemon-side and streamed to the client (DaemonMsg::AgentStatus). - `tty7 agent-hook claude <event>` + a palette installer wire Claude Code's lifecycle hooks up; the hook writes the sentinel to the controlling tty (with an ancestor-tty fallback for detached hook processes). - Avatar status dot: working (blue) / waiting (amber) / done (green); an unread finished turn gets a crisp outer ring that clears on focus. Notifications, resume, context feed - "Needs your permission…" the moment an agent blocks; "finished after Ns" per turn, honoring the notify policy (rich turns suppress the coarse exit). - Session resume: restored panes re-launch their conversation (`claude --resume …`), gated by `restore_agent_sessions` (default on). - Palette commands send the current selection or the repo `git diff` to the running agent as a ready-made prompt. Sidebar git line - New `terminal::git_status`: off-thread `git` probe (branch, or short sha when detached; `git diff --numstat HEAD` line counts) with GIT_OPTIONAL_LOCKS=0, refreshed on cwd change or command finish, dropped on a stale cwd via a generation tag. - Each row is avatar + title + `⎇ branch +N −M` (green/red), sized to content; the redundant cwd/"Working…" lines and the aggregate rollup are gone — the status dot and branch line carry it. 672 tests pass. * fix(agents): repair CI and refresh the git line when an agent turn ends - The live PTY detection test used `sh -c 'exec -a codex cat'`, but `exec -a` is a bashism dash (Ubuntu's /bin/sh) rejects — spawn bash. - cargo fmt over cli_agent.rs / view.rs / app.rs. - An agent session is one long foreground command, so the back-to-prompt edge never refreshed the sidebar's branch/diff line while the agent worked — exactly when the working tree changes. poll_agent_status now reports a turn ending (transition into Done) and the poll reprobes git on that edge too. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
e44855c3b6 |
feat(ssh): support Unix GSSAPI auth (#81)
* feat(ssh): support gssapi auth * fix(ssh): pin the russh patch to an exact rev + fail on a stalled gssapi context - [patch.crates-io] now pins rev 0d1d073 instead of tracking the fork's branch: russh is the credential-handling SSH protocol layer, and a moving branch would let `cargo update` silently pull unreviewed code. Documented the removal condition (upstream russh PR #737 releasing). - gssapi_step: an incomplete context with no output token used to claim GssapiStep::Complete without a MIC, which servers reject with an opaque failure; return an error naming the stall instead. - auth.rs module doc: include gssapi-with-mic in the Auto ordering. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
1b613e90e2 |
feat(ssh): native russh connection manager (profiles, auth, forwarding, SFTP) (#74)
* feat(ssh): profile model, keychain vault, and ssh_config import (WS1 data layer)
Add the connection-manager data layer per PRD §7:
- core::ssh_profile: the SshProfile model (connection/auth/forwarding/session/
advanced fields, uuid ids), HostPort/AuthMode/ForwardRule/Algorithms, and
QuickConnect parsing (parse_quick_connect / to_connect_string, IPv6-bracket
and @-in-username aware) plus %h/%r identity-file placeholder expansion.
- core::keychain: a CredentialStore trait over the OS keychain (keyring 4.x)
with an in-memory test store, endpoint-keyed entries (tty7-ssh / tty7-ssh-key
per PRD §7.2), and a secret-free CredentialRef persisted in config.
- core::ssh_config: import_profiles/merge_imported resolve common ssh_config
fields (HostName/User/Port/IdentityFile/ProxyJump/ProxyCommand/ForwardAgent)
with first-match-wins incl. wildcard fallbacks; Match/canonicalize skipped.
discover_profiles is untouched. Import is repeatable/idempotent.
- Config gains #[serde(default)] ssh_profiles: Vec<SshProfile>.
Unit tests cover quick-connect parsing (IPv6/@/port bounds), placeholder
expansion, profile+config serde round-trip through disk, ssh_config import
parsing, and keychain mock behavior.
* feat(ssh): native russh session engine in the daemon (WS2)
Add a native (pure-Rust) SSH path for daemon panes, replacing shell-out
`ssh` for managed connections. A russh shell channel is bridged into the
existing pane byte pipeline so it is indistinguishable from a local PTY:
the reader thread, 8 MiB replay ring, OutputGate backpressure, and OSC
7/133 sniffer are reused unchanged. Only the handle-owning methods
(resize→window-change, kill→channel close, foreground pgid→None) dispatch
on a new PaneBackend seam.
Engine (`src/daemon/ssh/`):
- Per-daemon tokio runtime owning all russh connections; the rest of the
daemon stays std-threads and crosses in via blocking Read/Write adapters
over bounded/unbounded channels (backpressure reaches the SSH window).
- Connection registry keyed by host/port/user/proxy/jump chain with reuse
(new tab = new channel, no re-auth) and documented blast-radius semantics.
- Transports: direct TCP, ProxyCommand (%h/%p/%r substituted), SOCKS5,
HTTP CONNECT, and jump host via direct-tcpip (multi-level chains).
- Auth (Tabby-ordered): none-probe, publickey (multi-identity, %h/%r,
.pub-misconfig skip, encrypted-key passphrase), agent, password,
keyboard-interactive (zero-prompt quirk, password auto-fill).
- known_hosts: plaintext + hashed (HMAC-SHA1) + @revoked + @cert-authority
skip; append preserves the file. Self-contained SHA-1/HMAC/base64.
- Interactive prompt broker: AuthPrompt/AuthResponse/SshStatus over the
pane's connection; blocks auth with a 120s per-prompt timeout.
Protocol (`daemon::protocol`):
- New kinds: SPAWN_NATIVE_SSH(14), AUTH_RESPONSE(15) client->daemon;
AUTH_PROMPT(13), SSH_STATUS(14) daemon->client. New kind so a pre-WS2
daemon rejects rather than mis-spawns.
- NativeSshSpec wire type (redacted Debug + without_secrets), prompt/host-key
enums, RemoteKind::NativeSsh.
Session restore: `SessionPane::Leaf.ssh_spec` (secret-free) so a dead
native pane can be respawned by WS6; live panes reattach for free.
Docs: `docs/ssh-native-architecture.md` (protocol, broker flow, the
connection-registry API WS4/WS5 use, and the forwards/X11/SFTP seams).
Tests: known_hosts parse/check/append, spec serde + redacted Debug,
ProxyCommand %h/%p substitution, blocking adapter EOF + backpressure,
connection-key identity, prompt-broker delivery/cancel. Full suite green.
* feat(ssh): GUI auth/host-key sheets, known_hosts management, spec resolution (WS3)
Workstream 3 of the native SSH connection manager: the GUI side of the
russh auth/host-key flow, known_hosts hardening + management, and pre-connect
credential resolution.
Client prompt plumbing (terminal/remote.rs):
- Handle DaemonMsg::AuthPrompt / SshStatus in the reader loop: queue prompts
per pane (banners ride the same queue, id 0) and cache the spawn phase, waking
the view. take_auth_prompt / has_pending_auth / ssh_phase / ssh_endpoint /
auto_supplied_password accessors; respond_auth writes ClientMsg::AuthResponse.
- spawn_native_ssh client entry (retains endpoint + stored-password flag for the
sheet), and list/delete_known_hosts one-shots.
- TerminalView emits AuthPromptReady; Tty7App subscribes at the single leaf
build site (new_terminal) and drains prompts into the sheet.
In-pane auth sheets (ui/ssh_prompt.rs): password (masked + remember), key
passphrase (remember by key-content hash), keyboard-interactive/2FA (echo/no-echo
rows), unknown-host confirm, and a red CHANGED-key MITM warning whose default
action is ABORT — trusting requires typing "yes" (never auto-accept). Pure,
unit-tested state machine (PromptModel + submit/keychain decisions) under a thin
gpui layer; sheet keyed to the raising pane so tab switches never misroute it.
FR-A6: password_submit deletes the stored keychain entry ONLY in the
stored-password rejection path (a Password prompt after an auto-supplied
password) when the user declines to remember — a plain failed attempt never
clears a credential.
Pre-connect resolution (ui/ssh_connect.rs): build_native_ssh_spec resolves a
profile into a self-contained NativeSshSpec — keychain password/passphrases,
jump_host profile chain (cycle-guarded), identity placeholder expansion, proxy
precedence. The single place secrets enter a spec. (WS6 wires the UI entry.)
known_hosts hardening (daemon/ssh/known_hosts.rs): OpenSSH glob (*/?) + negation
matching, case-insensitive host compare, plus list/delete management preserving
the file byte-for-byte elsewhere. New protocol pair: ClientMsg::ListKnownHosts
(16) / DeleteKnownHost (17), DaemonMsg::KnownHostsList (15); daemon server
handlers; Settings "SSH → Known hosts" section + global verify_host_keys toggle.
Tests: known_hosts wildcard/negation/case/list/delete(byte-preserving); reader
surfaces AuthPrompt/SshStatus; spec builder password/jump/cycle/proxy/verify;
prompt state machine incl. the FR-A6 matrix; protocol round-trips.
* feat(ssh): SFTP file panel and transfer engine (WS5)
Add native-SSH SFTP on top of the WS2 russh engine.
Daemon (src/daemon/ssh/sftp.rs):
- One cached russh_sftp SftpSession per SshConnection (keyed by
ConnectionKey, validated by Arc identity + liveness), reused across panes
and transparently re-opened if the subsystem channel dies while the
connection lives.
- list dir (symlink follow-stat to classify targets), stat, mkdir, remove
file, recursive remove dir, rename, chmod, readlink.
- Background upload/download jobs: 256 KiB chunks, recursive dirs, temp-file
upload (<name>.tty7-upload-<rand> then rename-over-target), mode
preservation on download, cancellable, poll-based progress with a latching
job state machine.
Protocol (src/daemon/protocol.rs): client kinds 30-34
(SftpList/SftpOp/SftpTransferStart/Cancel/List), daemon kinds 30-33
(SftpEntries/SftpOpResult/SftpTransferStarted/TransferProgress). Round-trip
tests for every new message.
Client (src/terminal/remote.rs): one-shot RemoteTerminal::sftp_* helpers.
UI (src/ui/sftp.rs): a right-docked slide-in panel for the focused native-SSH
pane -- breadcrumb bar, filter, dir-first entry list, toolbar (up / refresh /
new folder / upload / go-to-shell-cwd for FR-T4), per-row download / rename /
delete / chmod / follow-symlink, Finder drag-and-drop upload (on_drop
ExternalPaths) plus a file-picker fallback, and a bottom transfer tray that
polls progress every 500ms off the main thread. New ToggleSftp action +
keymap arm + palette 'SFTP Panel' entry.
Tests cover protocol round-trips, path utilities (join/parent/basename,
unicode), temp-name generation, entry classification, dir-first sort/filter,
breadcrumb split, and job state-machine transitions. No real-sshd needed.
* feat(ssh): native port forwarding — Local/Remote/Dynamic + loopback (WS4)
Add the WS4 port-forwarding engine on top of WS2's native russh session
engine. Forwards ride a pane's shared SshConnection (no ControlMaster
socket), keyed per pane for the UI and torn down on pane death.
Daemon engine (src/daemon/ssh/forward.rs):
- Local (FR-F1): TCP listener -> per-conn direct-tcpip -> bidirectional
bridge with exact EOF/close propagation.
- Dynamic/SOCKS5 (FR-F1): hand-rolled minimal SOCKS5 (no-auth greeting,
CONNECT for IPv4/IPv6/domain; BIND/UDP rejected) -> direct-tcpip.
- Remote (FR-F1): tcpip_forward global request + RemoteForwardTable
consulted by the client Handler's server_channel_open_forwarded_tcpip;
unmatched channels rejected; cancel_tcpip_forward on teardown.
- SshForwardRegistry keyed by pane_id; auto-teardown from DaemonPane::drop
(covers the FR-C2 blast radius when a shared connection drops).
- Preconfigured forwards (FR-F2) established post-auth in run_session;
failures are non-fatal (ForwardStatus::Error rows, never a killed session).
- Native loopback one-click (FR-F4): EnsureLoopbackForward branches on
RemoteKind::NativeSsh to a Local direct-tcpip forward, same reply shape.
Protocol: AddForward/RemoveForward/ListForwards (client kinds 20-22) ->
ForwardList (daemon kind 20); ManagedForward/ForwardStatus wire types.
Client: RemoteTerminal::{add,remove,list}_forward one-shots; view.rs
can_forward_loopback also accepts native panes.
UI (src/ui/forwards.rs): native panes show managed forwards (L/R/D badge,
bind -> target, description, status, delete) + an add form with a segmented
kind selector, alongside the existing loopback list; shell-out panes
unchanged.
X11 (FR-X2) left as a documented seam in daemon::ssh::handler (P1).
Tests: SOCKS5 handshake (v4 reject, v5 CONNECT ipv4/domain/ipv6, BIND
reject), bridge EOF both directions, registry add/remove/teardown, and
protocol round-trips for the new messages.
* style: cargo fmt across ssh connection-manager workstreams
* feat(ssh): UX integration — native connect, palette entry, profile editor, session UX (WS6)
Make the SSH connection manager reachable and alive from the UI:
- Native SSH spawn keystone: TerminalView::new_native_ssh + Tty7App
connect paths. Saved profiles connect via the native russh engine;
use_system_ssh profiles fall back to the frozen shell-out path (FR-C5).
- Unified palette entry (FR-P3): saved profiles (frecency-ordered) +
~/.ssh/config aliases + live QuickConnect all in the root flow. Enter
connects; Cmd-Enter / -> opens the profile editor. Per-profile frecency
(count + last-used) persisted in config and used to rank rows.
- Profile editor (FR-P1/P5): full-window page like Settings, list + edit
views with progressive disclosure (4 core fields; collapsed jump host,
forwards, and advanced sections incl. the use_system_ssh compat toggle
with its disabled-features note). Import from ssh_config, duplicate,
delete, copy user@host:port, connect.
- Session UX (FR-E1..E4): in-pane phase-coloured SSH status strip with the
reconnect notice; per-tab status dots in the strip and sidebar;
warn-on-close confirm sheet (global toggle + per-profile override);
RestartSshSession (Cmd-Shift-R) reconnecting a dead pane in place; and
session-restore respawn of dead native panes (re-resolving secrets from
the profile, else prompting).
- Actions/keymap/palette wiring for OpenSshProfiles and RestartSshSession.
* feat(ssh): consolidate paths — russh default, freeze system-ssh compat (WS7)
Make native russh the default for every non-compat SSH entry point and
confine the shell-out `ssh` path to a frozen compat escape hatch (PRD §3.1).
Entry-point routing (ui::app):
- Typed "SSH: Add Connection…": a bare `user@host[:port]` now takes the
native QuickConnect path; only arg-bearing `ssh … -flags` lines (and bare
tokens that only name a config alias) fall to the compat shell-out.
- `~/.ssh/config` alias rows route through a documented `open_compat_alias`
funnel (same funnel as `use_system_ssh` profiles) and their palette
subtitle now reads `~/.ssh/config · system ssh`.
- `open_managed_ssh_spec` documented as the single compat funnel; its only
callers are the three deliberate escape hatches.
Freeze audit: module-level freeze notes on `SshSpec`,
`build_managed_ssh_command`/`SPAWN_MANAGED_SSH`, and `daemon::forward`
(ControlMaster loopback). Verified `daemon::forward` is reachable only from
compat panes (server branches `EnsureLoopbackForward` on `RemoteKind`); no
non-compat code depends on shell-out.
FR-C5 compat gating with a visible reason: SFTP toggle on a compat pane now
opens a short "unavailable" notice instead of silently no-op'ing; the Ports
panel shows a muted compat-mode line; managed L/R/D add-form stays
native-only.
Docs: Path policy section in ssh-native-architecture.md (WS6/WS7 seams
marked resolved); SSH connection manager feature section in README +
README.zh-CN.
* fix(ssh/sftp): harden downloads — path-traversal guard, atomic temp, scoped retry
Three SFTP fixes, all in the download/session path:
- Security (P0): reject server-supplied directory-entry names that aren't a
single normal path component before using them as a local path component.
A recursive download built `lpath.join(name)` straight from entry names, so
a malicious/compromised server could return `..`, `a/b`, or an absolute
`/etc/...` and escape the destination for arbitrary local file write with
server-chosen mode bits (CVE-2019-6111 class). New `safe_local_name` guard is
applied in both the download walker and the `remote_size` pre-pass so the size
denominator matches what is actually transferred.
- Correctness: download to a per-file `<local>.tty7-download-<rand>` temp then
rename over the target on success; on error/cancel remove the temp and leave
any pre-existing target intact. Mirrors the upload temp+rename discipline so a
failed download never truncates a local file in place. preserve_mode still
applies to the final file.
- Correctness: `with_session` now retries the one re-opened-session attempt only
on a transport/channel failure, not on a logical SFTP error (permission
denied, no such file). A server status code returns directly instead of
wasting a second identical round-trip.
Adds unit tests for safe_local_name, download_temp_path, and is_transport_failure.
* fix(ssh/known_hosts): @revoked takes precedence over an earlier trusted line
check_in_str returned Known on the first exact match, so a later @revoked line
for the same host+key was never reached and a revoked key could read as trusted.
Scan for revocation in a first pass across the whole file (a matching @revoked
line rejects the key regardless of a trusted match elsewhere), then run the
normal known/changed resolution. Adds a unit test with a trusted line followed
by a @revoked line for the same host+key asserting Revoked.
* fix(daemon/transport): tighten Unix socket perms now it carries SSH secrets
The daemon socket now conveys NativeSshSpec cleartext secrets, but the socket
file was left at umask-default perms, so a co-local user could connect. On Unix,
chmod the socket file to 0600 (connecting requires write permission on the node,
so this is the access boundary) and chmod the config dir to 0700 — but only when
the socket lives in the config dir tty7 owns, never the overlong-path fallback
under a shared $XDG_RUNTIME_DIR / temp dir. Best-effort: log at warn and continue
on failure. Windows loopback+token path is untouched (it already authenticates).
* fix(ssh): self-heal reuse of a connection whose transport silently died
mark_dead() only runs from Drop, but a parked forward/loopback accept loop holds
an Arc<SshConnection>, so a dead connection's Drop never runs and is_alive()
stayed true. A reconnect for the same ConnectionKey reused the dead russh handle,
the first channel-open errored, and the whole reconnect failed until forwards
were torn down.
Two complementary fixes:
- is_alive() now also consults the russh handle's own liveness via a non-blocking
try_lock + handle.is_closed() (the session task ending closes its command
sender), catching the stale-flag case cheaply.
- run_session treats the first shell-channel open on a *reused* connection as a
liveness probe: on failure it marks the connection dead, evicts its registry
slot, and reconnects fresh once (a fresh connection failing there is a real
error). Preconfigured forwards now establish after this probe, on the
confirmed-live connection. open_connection returns a `reused` flag to drive this.
Adds a unit test that evicting a key from the registry map clears its slot. The
end-to-end reuse-after-death path needs a live server, so it stays covered by E2E.
* resolve ssh_config aliases natively
Expand the ssh_config resolver to map the russh-mappable directives onto an
SshProfile: ConnectTimeout, ServerAliveInterval/CountMax, Ciphers, MACs,
KexAlgorithms, HostKeyAlgorithms, Compression, ForwardX11,
StrictHostKeyChecking (no -> verify_host_keys=false), and
LocalForward/RemoteForward/DynamicForward. Algorithm +/-/^ modifier syntax is
dropped rather than mis-applied; Match/canonicalize stay unevaluated.
Add resolve_alias_to_profile(_from) returning a transient in-memory profile
(fresh id, no group/credential) plus the raw ProxyJump target, so a config
alias can connect over the native engine.
* remove system-ssh compat mode; unify loopback on the native tunnel
There is no longer a shell-out `ssh` path. Every SSH entry point resolves to
the native russh engine:
- Delete the `use_system_ssh` profile field (old config.json still loads: the
struct is `#[serde(default)]` with no `deny_unknown_fields`) and its
profile-editor switch/note.
- Route `~/.ssh/config` aliases and typed connect lines to native. The typed
parser now yields a transient profile + raw ProxyJump (native spec data), not
a shell-out SshSpec; an unparseable line surfaces a dismissable inline banner
instead of silently shelling out. Alias ProxyJump resolves recursively into a
nested jump chain (config alias hops or user@host:port), with a cycle guard.
- Delete the FR-C5 compat gating UI (SFTP notice, forwards hint): SFTP and
managed forwards are available on every native pane.
- Delete the daemon shell-out path: protocol `SshSpec`/`SPAWN_MANAGED_SSH`,
`ShellSpec.ssh`, `build_managed_ssh_command`/`ssh_control_*`, and
`daemon::forward` (the ControlMaster `ssh -O forward` engine).
- Loopback one-click forwards are native-tunnel-only (`direct-tcpip`):
`can_forward_loopback` gates on `RemoteKind::NativeSsh`; the server
Ensure/List/Close handlers drop the ControlMaster branch.
- `RemoteContext.control_path` is removed; the reader skips foreground-ssh
detection for a pane already tagged `NativeSsh`. Foreground-ssh detection for
a manually-typed `ssh` in a shell stays (status/label only).
* docs: native russh is the only SSH path
Rewrite the architecture doc's path policy (no shell-out / ControlMaster; the
sole path is russh; ~/.ssh/config aliases resolve natively, best-effort, with
Match/canonicalize/GSSAPI unsupported and no fallback), update the loopback
seam row, and drop compat-mode mentions. Sync the README (EN + zh-CN) SSH
sections to the single native path.
* fold SSH profile editor into Settings
Manage saved SSH profiles under Settings -> SSH instead of a parallel
full-window page, for UX consistency with the rest of the app.
The SSH settings section is now one scrollable page with three blocks:
Profiles (the saved-profile list plus an inline edit form, moved from the
standalone editor), then Known hosts, then the security toggles (verify
host keys / warn-on-close). The edit form keeps the same progressive
disclosure (name/host/user/auth up front; collapsible Jump host / Port
forwards / Advanced) and every field the old editor exposed, saving
through the same update_config path.
The edit form's widgets live in a lazily-built SshProfileForm on
SettingsState, rebuilt (a fresh input set) each time a profile is
selected so the section never carries N profiles' inputs at once.
Entry points now open Settings at the SSH section: the OpenSshProfiles
action and the "SSH: Manage Profiles..." palette entry via a new
open_settings_section helper; a profile row's edit affordance preselects
that profile via open_ssh_profile_in_settings; "save as profile" from a
quick-connect via open_ssh_profile_new_from_target. The palette connect
flow (Enter to connect, frecency) is untouched.
Deletes src/ui/profile_editor.rs, its module registration, and the
Tty7App profiles_editor field / overlay mount / render path.
* SSH pane: tunnel + SFTP icon buttons
Replace the top-right "Ports N" text chip with two minimalist icon
buttons for a connected native-SSH pane: a tunnel icon
(IconName::ExternalLink) that toggles the port forwarding panel and an
SFTP icon (IconName::Folder) that toggles the file browser. Both carry a
hover tooltip; the tunnel icon shows a small count badge when one or more
forwards are active.
The buttons are gated to a connected native pane via a new
active_connected_native_ssh_pane helper (RemoteKind::NativeSsh +
SshPhase::Connected), so a foreground `ssh` or a still-connecting session
shows only the top-left status strip. The forwards / SFTP panels
themselves are unchanged, and the ToggleSftp hotkey / palette entry stay
as an additional entry point. Status (strip / tab dots) stays separate
from actions (the buttons).
* fix(ssh): hide the in-pane SSH status chip once connected
The tab status dot already carries connection state and the top-right
tunnel/SFTP icons signal the pane is SSH, so a connected-state chip just
floats over the shell output. Keep the strip only while connecting and for
the post-drop reconnect notice.
* SFTP: per-row actions in a right-click context menu
* Settings SSH profiles: clean rows with hover ⋯ / right-click menu
* Settings SSH: two-column master-detail layout
* style(ssh settings): soften Add/Save buttons off the heavy primary fill
Match the existing soft-sheet convention (Duplicate-to-Edit, About's update
button): a solid near-black `.primary()` fill is too jarring against the
mostly-outline settings sheet. Use the subtle default fill instead.
* feat(ssh): 'Forget password' entry in the profile ⋯ menu
Deletes the keychain-stored password for the profile's endpoint
(user@host:port); the profile is untouched and the next connect re-prompts.
No-op when nothing is stored. Surfaces a window notification. Credentials are
endpoint-keyed, so this matches only when the profile pins an explicit user.
* SSH tunnel: merge loopback into a single unified forwards list
The tunnel panel stacked two parallel forwarding systems: a general
Local/Remote/Dynamic managed-forwards list and a separate
loopback (localhost links) section with its own add form, list, and
Refresh button. A loopback forward is just an auto-created Local forward
(127.0.0.1:<ephemeral> -> 127.0.0.1:<port>) minted when the user
Cmd-clicks a localhost:PORT link, so the separate UI and its parallel
backend bookkeeping were redundant.
Backend: ensure_loopback now registers the auto-forward in the same
managed registry as establish (a normal Local ManagedForward with a
'localhost link -> :<port>' description), so it shows up in
list(pane_id). It still returns the resolved local port in the existing
LoopbackForward reply shape, so the wire protocol is unchanged. Dedup is
preserved: a live auto Local forward to the same target is reused. The
parallel LoopbackEntry map and list_loopback/close_loopback are removed;
the ListLoopbackForwards/CloseLoopbackForward handlers stay wire-
compatible (now empty/no-op).
UI: delete the loopback section (form, rows, Refresh, empty state) and
its panel state/handlers. The single section is renamed 'Port
forwarding' and now includes the auto localhost forwards as Local rows.
* feat(ssh tunnel): X-icon close + editable forwards
- Panel close is now an X icon button (matching the SFTP panel) instead of a
text button.
- Each forward row gains Edit: it loads the forward into the add form; Save
re-establishes it (remove old + add new) so you can change bind/target ports
like VSCode's remote tunnels. Cancel leaves edit mode.
* fix(ssh forward): free the listening socket synchronously on remove/teardown
* feat(sftp): tabby-style bottom panel — off-thread ops, new file, path input, transfers tray
Redesign the SFTP panel from a right-docked strip into a bottom-docked
panel modelled on tabby:
- Move blocking daemon round-trips (list / readlink / one-shot ops) onto a
background executor so navigation never freezes the UI; a nav generation
counter discards stale replies, and a loading flag distinguishes an
in-flight listing from a genuinely empty directory.
- Add a CreateFile SFTP op (OPEN with CREATE|EXCLUDE) plus a "New file"
toolbar action and inline edit form.
- Replace the breadcrumb toolbar with a compact ghost-icon action cluster
and an always-visible search box; double-clicking the breadcrumb switches
to a "type a path" text input (Enter navigates, Esc/blur cancels).
- Lead the list with a "Go up" row; enter directories on double-click
(downloads stay explicit via the right-click menu).
- Rework the transfers tray: dismiss/auto-reopen on new jobs, a pinnable
history view, and "Show in Finder" for finished downloads.
* fix(ssh): platform-split agent connect — russh connect_env is Unix-only
AgentClient::connect_env dials $SSH_AUTH_SOCK over a Unix-domain socket and
does not exist on Windows, breaking the windows-msvc build. Split try_agent
per platform (Unix keeps connect_env; Windows dials the OpenSSH agent named
pipe, honoring SSH_AUTH_SOCK as an override) and share the identity loop via
a stream-generic try_agent_identities.
* fix(ssh): review fixes — data-loss, security, and lifecycle bugs
Daemon/SFTP:
- user Rename no longer routes through rename_over: a refused overwrite was
silently deleting the existing destination file
- recursive download/upload/size walkers classify children by lstat attrs and
skip symlinks (cyclic links looped forever; a link to / copied the world)
- flush/shutdown failures now abort a transfer before the temp→target rename
commits a truncated file over a good one
- the top-level download entry name passes the same safe_local_name guard as
walked names (hostile server '..'/absolute names escaped ~/Downloads)
Host keys:
- a known host presenting a key type absent from known_hosts now raises the
changed-key warning instead of the benign first-connect prompt
- verify_host_keys=false still hard-rejects @revoked keys (OpenSSH parity)
- known_hosts delete writes temp+rename instead of truncate-in-place
Auth:
- keyboard-interactive rounds are capped and a rejected stored password is
no longer auto-refilled forever (users can now type the right one)
- host-key/auth prompts pause the connect timeout (a slow 'trust this
fingerprint?' click no longer kills the connection under it)
- identity paths expand a leading ~ so keychain passphrase store/resolve
works for ~/.ssh/... paths; keychain write failures are logged
Forwarding:
- duplicate remote forward registration is refused instead of overwriting the
live entry (whose rollback then unroutably stranded the original forward)
- forwarded-tcpip port-only fallback no longer guesses between two bindings
- accept loops retry transient errors (EMFILE/ECONNABORTED) with backoff
instead of dying while the UI still shows 'listening'
GUI lifecycle:
- native-SSH spawn failures return an error surfaced as a notification
instead of panicking the app (incl. against a stale pre-SSH daemon, which
now gets the same restart-once retry as local spawns)
- a dead native-SSH pane lingers for in-pane reconnect (PRD FR-C2/E4)
instead of auto-closing with its diagnostic
- a second pane's auth prompt is left queued while another sheet is active
(was popped and dropped → broker timeout) and picked up on dismiss
ssh_config:
- HostName %h expands to the alias; # only comments whole lines (a # inside
a ProxyCommand value is literal)
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
|
||
|
|
90515d9fc3 |
fix(windows): stop the daemon before install/uninstall so it can replace tty7.exe (#72)
The persistent daemon (`tty7.exe --daemon`) is a detached background process
that outlives the GUI and is the running image of tty7.exe, so Windows locks
the file. An upgrade or uninstall then can't overwrite/remove the binary and
fails ("file in use" / reboot required) — the Restart Manager doesn't reliably
catch a no-window, DETACHED_PROCESS daemon in its own process group.
- spawn: extract the "stop the running daemon" half of `restart()` into a
reusable `stop()` (Shutdown -> await exit -> pid reap fallback -> clear
endpoint); `restart()` is now `stop()` + `ensure_running()`.
- main: add a `--stop-daemon` CLI entry that runs `stop()` and returns before
any GUI init, so it never opens a window.
- installer: in PrepareToInstall, extract the *new* tty7.exe to {tmp} and run
`--stop-daemon` (the new binary understands the flag; an old installed one
would launch the GUI instead), releasing the lock before file copy. Mirror it
in [UninstallRun]. Keep CloseApplications as a backstop but RestartApplications=no
(the GUI respawns the daemon on next start).
Co-authored-by: thomas <thomas@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7606c19a51 |
fix(ssh): move forwards to pane context (#71)
* feat(ssh): add palette SSH connection entry * fix(ssh): clarify add connection placeholder * fix(ssh): move forwards to pane context * fix(ssh): show host in forward panel * fix(ssh): open forwarded local links * fix(ssh): simplify forward form label |
||
|
|
f25667cc1d |
feat(links): add SSH loopback forwarding (#58)
Detect foreground SSH sessions in the daemon and cache the active remote context per pane; open Cmd-clicked loopback URLs through daemon-owned ssh -N -L local forwards over a ControlMaster socket; add settings controls to enable SSH loopback forwarding and view/close active forwards. Kept behind an explicit ssh_loopback_forward setting; the daemon validates the pane's foreground process is a plain SSH session and rejects unsafe invocations. Includes a follow-up hardening commit rejecting option-like ssh targets (leading '-') to close a local argument-injection gap. |
||
|
|
21b7f82392 |
feat(completion): execute dynamic generators for live candidates (#52)
* feat(completion): execute dynamic generators for live candidates The completion engine consumed Fig specs' static shape but never ran their dynamic generators, so positions whose candidates come from the live system — ssh hosts, git branches — fell through to filesystem path completion (#51: ssh <Tab> listed the cwd). Local-only by design: the pure engine returns each pending script, the view runs it on the background executor (/bin/sh -c in the session cwd, 800ms timeout, kill-on-drop, 256KiB stdout cap, 5s TTL cache) and merges the parsed lines into the open menu, generation-tagged so a result can't outlive its session. A per-script parser registry ports the specs' dropped postProcess transforms (git markers, docker {{json .}}, package.json scripts, …); unmatched scripts default to one-candidate-per-line, and hopeless outputs are suppressed rather than inserted as garbage. ssh/scp/sftp/rsync specs gain host generators reading ~/.ssh/config (Include-aware, wildcard patterns skipped) and known_hosts (hashed entries skipped, [host]:port unwrapped). Fixes #51 Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> * docs(comments): describe borrowed conventions directly Prior-art name-drops in comments aged poorly as the implementations diverged; keep the behavioral rationale, drop the product citations. * fix(completion): make generator execution CI-portable Linux: sh -c may fork the command instead of exec'ing it, so killing only the shell on timeout left a grandchild holding the stdout pipe — the reader (and the caller) then blocked until the grandchild exited on its own. Spawn the child as its own process-group leader and kill the group; the timeout test now forces the fork case (trailing true) so the group-kill is what's actually proven. Windows: generator scripts are POSIX sh + awk, so the execution path now compiles to no-suggestions there instead of failing at runtime on a missing /bin/sh; the process-spawning tests are Unix-only to match. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
63a05b256e |
feat(links): add file path links (#49)
* feat(links): add file path links * fix(links): reset the cached link modifier on activation flips, cover all tabs The per-pane link_modifier_down cache was refreshed only for the active tab's leaves, and never on window (de)activation — so releasing Cmd after a mouse tab-switch, or during Cmd-Tab/Spotlight (the release lands in whatever app is key by then), left panes stuck at true. A stale true makes a plain unmodified left click open links and steals clicks from mouse-tracking TUIs. Route the refresh through a helper that walks every tab, and treat the window-activation flip as a release, exactly like the badge dismissal right next to it. Also reword the search.rs docs that still described the now test-only url_at as the production entry point. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
cf59fc67fc |
fix(daemon): reap a live-but-unreachable daemon instead of stranding it (#43)
A replaced daemon used to linger forever: both takeover paths (ensure_running on a failed connect, restart after the Shutdown wait) would unlink the endpoint and spawn a fresh daemon without checking whether the old process was still alive. A daemon that couldn't be stopped — a binary predating ClientMsg::Shutdown, or a wedged teardown — survived unreachable, still holding every pane's PTY and children. That is exactly how 14 panes (11 live sessions) got silently stranded across an app update. Now the daemon records its pid in <config>/daemon.pid after bind, and both takeover paths reap the recorded daemon before claiming the endpoint: SIGTERM first — handled by a new sigwait thread that tears down like Shutdown, giving every pane's child its SIGHUP grace — then SIGKILL if it won't die. The pidfile is never trusted blindly: the pid must be alive and its executable basename must match our own, so a crashed daemon's stale pidfile (pid possibly recycled) is cleared, not killed. Windows reaps via the existing winproc helpers, descendants-first, mirroring the pane hangup order. Fixes #42 Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
b21e299fba |
fix(windows): close a pane when its shell exits on its own (#30)
Windows: a shell that exits on its own (exit / Ctrl-D / crash) left the pane wedged open because ConPTY's output pipe never EOFs on a natural child exit. Add a Windows-only exit monitor that waits on the child handle directly and reports through a shared, run-once DeathReporter latch — the same Exited / on_dead path the reader's EOF drives on Unix. Unix behavior is unchanged. Claude-Session: https://claude.ai/code/session_01ABey161AUxhgmJC3PRoYtF |
||
|
|
86857f8cb2 |
fix(shell): keep zsh plugin managers out of the throwaway ZDOTDIR (#29)
Opening a new tab or split stalled for seconds while a zsh plugin manager
reinstalled itself from scratch. tty7 launches zsh through a per-pane throwaway
ZDOTDIR so it can layer its OSC 133 shell integration on top of the user's
config, but it left ZDOTDIR aimed at that empty temp dir for the whole shell
lifetime. Tools that resolve their own state via ${ZDOTDIR:-$HOME} — Zim
(.zimrc/.zim), oh-my-zsh, compinit's .zcompdump — therefore looked inside the
empty throwaway dir and rebuilt on every pane: Zim redownloads/recompiles every
module (the ~3s stall and "modules/…: Installed" spam of #15), compinit rewrites
its dump into the temp dir, etc.
Each redirector now stashes our dir, points ZDOTDIR at the user's real config
dir while their startup file runs, then restores ours so zsh still reaches the
next redirector; the integration body restores the real dir for the live
session via a one-shot precmd hook. When the user never set a ZDOTDIR we unset
it (rather than force $HOME) while sourcing, so their file sees exactly what a
real launch gives it and the `: ${ZDOTDIR:=~/.config/zsh}` relocate idiom still
fires. .zshenv recaptures a user-relocated ZDOTDIR so the classic tiny-~/.zshenv
layout keeps both its config and tty7's integration (previously it silently lost
the integration).
Verified end-to-end against real zsh for three layouts (config in $HOME,
unconditional relocate, and `-z`-guarded relocate): a second pane now hits the
plugin manager's cache instead of reinstalling, and runtime ZDOTDIR resolves to
the real dir. Adds three unit tests covering the redirector ordering, the
.zshenv recapture, and the runtime restore hook.
Fixes #15
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
|
||
|
|
c66a7fa014 |
fix(daemon): keep the local line editor out of remote ssh sessions (#28)
A remote shell that emits its own OSC 133 prompt marks — fish 4.x over ssh, most visibly, which ships them on by default — made the daemon report at_prompt=true, so tty7 engaged its local line editor and Tab ran completion against the local filesystem instead of reaching the remote shell. The shell tty7 spawned only emits its OSC 133 marks while it is itself the PTY's foreground process group (idle at its prompt). Reject a prompt mark whenever a foreground command (ssh, a TUI, a nested shell) owns the PTY — comparing the PTY's foreground pgid against the shell pid — so keystrokes pass raw to whatever is really reading them. The normal local case (shell idle ⇒ it is the foreground group) is untouched. Fixes #26. Claude-Session: https://claude.ai/code/session_01ABey161AUxhgmJC3PRoYtF |