mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 08:02:24 +00:00
9cac4b863b1bec6db94de830fd3fceefc6146ddc
501
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cd90a1afbe |
Merge pull request #90 from l0ng-ai/feat/daemon-handshake-ui-polish
Daemon protocol handshake with user-confirmed restart, SSH status dots, palette QuickConnect fix |
||
|
|
dc89af952d |
style(tabs): hide the close affordance until hover on active tabs too
The active tab row/chip used to keep its × always visible while the others faded in on hover. Treat every tab the same — opacity 0 until hover, space still reserved — so the sidebar and strip read clean. |
||
|
|
0182a072cc |
fix(git-status): share one per-repo snapshot across panes
Each pane used to compute and hold its own git branch/diff snapshot, refreshed only by its own events (cwd change, command end, agent turn end). Tabs sitting idle in the same repo kept whatever they last saw, so rows for one directory showed different +/− counts — or none at all when a tab's last probe landed on a clean tree. Snapshots now live in a process-wide GitStatusCache keyed by work-tree root: every pane whose cwd resolves into the same repo reads the same entry, refreshed by whichever pane probed last, and the sidebar observes the cache so all rows repaint together. In-flight probes are deduped per cwd (concurrent triggers fold into one git shell-out, with a rerun if re-triggered mid-flight), and a failed git diff keeps the previous counts instead of rendering the tree as suddenly clean. |
||
|
|
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. |
||
|
|
2029f2ccf7 |
fix(ui): show SSH state as a corner status dot in semantic colors
The SSH avatar drew a 2px border ring in theme tokens, which read as a second avatar shape next to the flat shell badge — and "connected" used theme.accent, the list-selection grey in this app, so the ring showed no state at all. Reuse the agent status_dot on the badge corner instead, colored from the same hardcoded palette as agent dots (amber connecting, green connected, red failed/disconnected, neutral for a foreground ssh); the tab strip's inline 6px dot picks up the same RGB values. |
||
|
|
611b671a5d |
fix(palette): stop offering QuickConnect rows for bare words
A bare word like "java" parses as a valid hostname, so every command search got Connect/Save rows pinned above the real matches. Require the query to look like a connect target (contain '@', ':' or '.') before injecting QuickConnect rows. |
||
|
|
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. |
||
|
|
ed244680bc | chore(release): v0.15.0-beta.1 v0.15.0-beta.1 | ||
|
|
2bf246f789 |
feat(agents): per-agent hook integrations with install state in Settings (#87)
* feat(agents): hook integrations for Codex, Copilot, OpenCode, and Pi with install state in Settings Generalize the Claude Code hook install into a per-agent integration layer, all feeding the same `tty7 agent-hook <agent> <event>` emitter (still gated on the TTY7 env var): - Claude Code / Codex: tty7-marked entries merged into their hooks map (settings.json / hooks.json); user hooks and settings are never touched. Codex install also runs `codex features enable hooks`, best-effort with manual-run advice when the CLI is missing. - Copilot CLI / OpenCode / Pi: a tty7-owned hook file / JS plugin / TS extension, byte-compared for drift detection and ownership-guarded on install and uninstall. - Copilot's catch-all notification hook is filtered in the emitter: permission/elicitation prompts escalate to permission-request, everything else stays silent. - New Settings -> Agents section: one row per agent with install state (not installed / installed / outdated) and Install / Reinstall / Update / Uninstall actions. - On launch (release builds only), integrations pointing at a moved or stale tty7 binary are rewritten in place. * fix(agents): rustfmt + make the owned-file exe assertion Windows-safe The generated integrations embed the exe path inside JSON/JS string literals, so the test must look for its string-escaped form — on Windows the raw path's backslashes appear as \\ in the content and the raw substring never matches. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
74e3383128 |
fix(terminal): shift the grid up when wrapped input overflows the bottom (#86)
* fix(terminal): shift the grid up when wrapped input overflows the bottom While the local command editor is active, typed input never reaches the grid, so the emulator's scroll-on-wrap never fires: a multi-line command at a bottom-of-screen prompt grew past the window edge and its tail (and caret) were clipped by the surface's overflow_hidden. Emulate the scroll an echoing shell would perform: compute how many rows the wrapped overlay spills past the last grid row (input_scroll_rows) and raise the whole paint origin by that many lines — the top rows clip, the overlay re-anchors to the shifted prompt row, and the wrapped tail lands in the vacated strip. The shift is capped so the caret row never scrolls off the top when the input is taller than the screen, and disabled while scrolled into history or in reverse-search mode. The completion menu follows the shifted anchor, and editor clicks/drags map through an unclamped row so the rows past the old bottom stay clickable. * style: rustfmt --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
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> |
||
|
|
a2c1763de9 |
fix(terminal): keep each tab's active pane across tab switches (#84)
* fix(terminal): keep each tab's active pane across tab switches Switching tabs reset the target tab to its first pane: focus_active always focused first_leaf, and a Tab stored no notion of which pane was active. Tab labels had the same root cause — an inactive tab holds no window focus, so leaf_title always fell back to the first pane. Record the focused pane per tab (Tab::last_focused) when leaving it or opening a focus-stealing overlay (Settings), and restore it on return via Tab::focus_target, degrading to first_leaf when the remembered pane has closed. leaf_title now uses the live focus for the active tab and the remembered pane for inactive tabs, so background labels track the pane each tab is working in. Add Pane::leaf_matching_or_first as the pure selection rule, unit-tested against the u32 leaf model. * fix(terminal): remember the active pane on every tab-leaving path Cmd+T (new_tab_with_shell), native-SSH new tab (open_native_ssh_tab), and reopen-closed-tab (Cmd+Shift+T) all move the active tab away without going through activate(), so the departed tab never recorded its focused pane and switching back jumped to the first leaf — the exact bug this branch fixes, reachable from its most common entry point. Snapshot via remember_active_pane before the switch in all three. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
d2e415153a |
feat(terminal): splitting an SSH pane opens another SSH pane (#83)
Splitting a native-SSH pane dropped back to a local shell because split() only inherited shell_spec (which is None for SSH panes) and never looked at ssh_spec. Detect an SSH pane in split(), re-resolve its persisted secret-free spec from the saved profile (re-applying keychain secrets, mirroring the reconnect path), and spawn the new leaf via new_terminal_native on the same connection. Local panes are unchanged. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
0921770439 |
fix(settings): keep theme panel on-screen on narrow windows (#82)
The settings content pane is a `flex_1` item, which still defaults to `min-width: auto` and so refuses to shrink below its content's intrinsic width. On a narrow-enough window that pushes the fixed 300px theme picker panel — and its close `×` — partly off the right edge, where it gets clipped (reported on Windows). Add `min_w_0()` to both content-pane branches so the pane yields and the panel always stays fully on-screen. This matches the existing shrinkable `flex_1` + `min_w_0` pattern used elsewhere (tab strip, panes, search). Co-authored-by: thomas <thomas@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
54825d3275 |
feat(terminal): multi-line prompt editor + I-beam mouse pointer (#80)
* feat(terminal): multi-line command editor at the prompt The inline prompt editor was single-line: a pasted multi-line command kept its newlines inline but rendered them as blank cells in one flex-wrapped row, flattening the command into a ragged blob with the original indentation left as runs of spaces. Make the editor genuinely line-aware: - render_input_bar splits the buffer on '\n' and stacks each logical line as its own flex-wrap row, so newlines become real breaks (soft-wrap within a line is unchanged); ghost autosuggestion is suppressed for multi-line input. - submit replays each embedded newline as an Enter so the shell's own line editor assembles the command (backslash / open-quote continuation, PS2). - up/down move the caret between visual rows with a sticky goal column, falling through to history recall only from the top/bottom row. - Home/End (Ctrl-A/E) act within the current logical line. - click mapping treats '\n' as a hard break (past a line's content snaps to its end, not onto the next line). - Shift+Enter / Opt+Enter insert a newline to author multi-line commands. Adds tests for logical-line Home/End and newline-aware click mapping. * fix(terminal): I-beam mouse pointer over selectable text The surface only set a pointer style (PointingHand) over hovered links, so everywhere else the OS default arrow showed over the terminal text. Default to an I-beam over the text like every other terminal, keep the pointing hand for links, and leave the arrow only once a program takes over mouse reporting (matching Terminal.app / iTerm). * style: cargo fmt --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
eb6c7ca3b3 |
fix(release): stop {tmp} from closing the Windows installer [Code] comment
The [Code] block comment used Pascal { } braces, but {tmp} inside it
closed the comment early, so ISCC parsed the trailing prose as code and
aborted with "'BEGIN' expected" — no Windows installer was produced.
Switch the comment to (* *) so brace-form constants stay literal.
v0.14.0
|
||
|
|
7bc388f923 | chore(release): v0.14.0 | ||
|
|
41fbed1e30 |
fix(ui): SSH auth-sheet polish + softer primary buttons (#79)
* fix(ui): SSH auth-sheet polish + softer primary buttons - Remember toggle: real Checkbox instead of a full-width ghost button whose selected-state fill read as a grey bar across the whole card - auth sheet: tighten to the shared sheet padding (p_4 / gap_3), width 420 - dead-SSH pane: replace the top-left chip + reconnect notice (which overlaid the daemon's red failure line printed at top-left) with a single bottom-centered 'Disconnected — ⌘⇧R · Reconnect' bar, clear of the output - drop the connecting/authenticating top-left SSH chip entirely (the tab status dot carries the phase; the buffer shows connect progress) - soften primary buttons app-wide: fill from foreground nudged ~20% toward the background (a dark charcoal, not pure black) via the primary / button_primary token family Pairs with l0ng-ai/gpui-component@9484cf9 (checkbox: instant check, no fade), picked up by the Cargo.lock bump. * fix(ui): SFTP parent row is a ".." directory entry, not a "Go up" action The leading go-up row used an ArrowUp icon + muted "Go up" label, reading as a toolbar action stranded in the list. Style it like a directory entry — Folder icon + ".." name in the foreground ink — matching the rows below (the file-manager/WinRAR convention). Click still goes to the parent. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
8695b80c61 |
fix(ui): let the Cmd+F find bar own the top-right slot over SSH action icons (#76) (#78)
The find bar and the pane's tunnel/SFTP action icons both pin to top-2/right-4. The action overlay lives in `body_area` as a later sibling of the pane body, so gpui's child-order stacking paints it on top of the find bar, covering the prev/next/close buttons. While the focused pane has search open, suppress the tunnel/SFTP icon overlay so the find bar has that slot to itself. The bottom-docked SFTP panel is unaffected; the icons return when the find bar closes. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
168abe4cde |
refactor(palette): saved profiles are the single SSH source (#77)
The command palette listed SSH hosts from two parallel sources: saved profiles and a live scan of ~/.ssh/config Host aliases. The same host could appear twice with different behaviors (frecency, edit affordance, credential handling), and config hosts surfaced even when Settings showed no profiles. Make saved profiles the palette's only SSH listing: - drop the live-alias rows and the OpenSshProfile command; ~/.ssh/config hosts appear after Settings -> SSH -> 'Import from ~/.ssh/config' - keep 'ssh <alias>' semantics for *typed* targets: QuickConnect and 'SSH: Add Connection...' now resolve a target naming a config alias on the spot (HostName/User/Port/IdentityFile/ProxyJump), with typed user@/:port/ flags overriding the config's values -- previously only the ProxyJump chain resolved and a typed alias was treated as a literal hostname - remove the now-dead discovery walker (discover_profiles + struct); its alias-filtering and Include-following tests move to import_profiles_from, which exercises the shared parse_config_blocks path - update PRD (FR-P3, section 3.3) and both READMEs to the new model Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
6b625cb3b1 |
feat(terminal): richer buffer search + rebindable, cross-platform shortcuts (#75)
* feat(terminal): richer buffer search — case/regex toggles, ⌘G, persistence
Bring the Cmd+F search bar up to par with mainstream terminals:
- Case toggle ("Aa"): smart-case stays the default (insensitive unless the
query has an uppercase char); pressing it forces case-sensitivity via a
`(?-i)` prefix.
- Regex toggle (".*"): the query is now matched literally by default
(metacharacters escaped), so searching for `.`/`*`/`(` behaves; toggle on
for a real regex pattern.
- Invalid-regex feedback: a pattern that fails to compile (only possible in
regex mode) turns the field border red instead of silently showing zero
matches.
- ⌘G / ⌘⇧G step to the next / previous match while the bar is open, alongside
Enter / Shift+Enter.
- Query + toggle state persist across close/reopen; opening prefills the field
from a single-line terminal selection.
- Navigation and live re-search only scroll when the focused match is
off-screen, so refining the query no longer jerks the viewport.
Also fix a click-through bug the new toggle buttons surfaced: the terminal
registers a pane-wide mouse handler, so a click on the floating search bar fell
through and started a text selection underneath. The bar now `.occlude()`s its
area and the terminal's mouse handlers gate on `Hitbox::is_hovered` (occlusion-
aware) instead of raw bounds containment.
Covered by a new end-to-end test that drives the real search path against a
seeded grid (case/regex toggles, invalid-regex flag, and persistence).
* feat(terminal): make search shortcuts rebindable in Settings, cross-platform
The Cmd+F search shortcuts were dispatched inline in `handle_cmd_shortcut`,
gated on the platform (Cmd) modifier — so they were invisible in the Settings
keybindings page, not rebindable, and effectively macOS-only (on Windows/Linux
that gate is the Super/Win key, which nobody presses to search).
Promote them to first-class, keymap-driven actions:
- `FindInTerminal`, `FindNext`, `FindPrevious` now flow through the same
`default_bindings()` → `make_binding()` path as every other action, so they
render as editable rows in Settings → Keybindings and honor user overrides.
- Sensible per-platform defaults: open find on Cmd+F (macOS) / Ctrl+Shift+F
(elsewhere — Ctrl+F stays readline's forward-char), find-again on Cmd+G /
Cmd+Shift+G (macOS) and F3 / Shift+F3 (Windows/Linux convention). This makes
buffer search reachable off macOS for the first time.
- Bound in the "Terminal" key context (like ClearScrollback), so they stay
inert on the Settings / home pages.
- Removed the inline Cmd+F / Cmd+G handling; the context-menu "Find…" row now
auto-derives its shortcut hint from the registered binding instead of a
hand-rolled mac-only one.
This keeps Copy/Paste/Select-All inline (their Ctrl+C↔SIGINT interaction on
Windows/Linux genuinely needs the conditional handling that reasoning does not
apply to Find).
* style: cargo fmt
---------
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>
|
||
|
|
725bdffdd3 |
feat(ui): tab title follows the active pane (#73)
The tab label derived from a split tab's terminal title always read the first (left/top) leaf, regardless of which pane held focus. Switch it to the focused pane so the label tracks the terminal you're working in, falling back to the first leaf when nothing in the tab is focused. `Tab::leaf_title` / `Tty7App::tab_label` now take `Option<&Window>`: the render paths (tab strip, sidebar) and inline rename pass the window and resolve via `pane.focused_or_first`; the command palette has no window, so it passes `None` and keeps the first-leaf title for its "Switch to Tab" list. 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>
|
||
|
|
dcd401a1dd |
feat(ui): vertical tab sidebar + settings as a full-window page (#70)
* feat(ui): add a vertical tab sidebar and make settings a full-window page Add a left-side vertical tab sidebar as an alternative to the horizontal title-bar strip, gated on a new `tab_bar_position` config (top | left). The rail reuses the existing tab model wholesale — same tabs/active state, `tab_label`, activate/close/move/rename ops, and the `DragTab` payload — so it adds no new business logic, only a new shape: - Config: `tab_bar_position` enum + a resizable, persisted `sidebar_width`. - New `tab_sidebar.rs`: one full-width row per tab (label, inline rename, hover close, drag-to-reorder), a top control bar with a "Search tabs…" filter + new-tab button, a draggable/persisted width (min 180px, max half the window), and active-row scroll-into-view. While the switch modifier is held, the first nine rows show their ⌘N switch digit, matching the strip. - Layout: in `left` mode the rail is a full-height left column that reaches the top of the window (traffic lights rest on its surface), with the title strip + terminal in the right column, so the rail reads as one continuous panel. The strip drops its chips (keeping the "+"/"⋯") in this mode. - Cohesive sidebar surface using the `sidebar*` theme tokens. - `ToggleTabSidebar` action (palette + keybinding-bindable) and a Settings → Window & Tabs "Tab bar position" control. Also make Settings a full-window overlay instead of a tab: it no longer clutters the tab rail or stacks a second sidebar beside it. Settings state moves from `Tab` to `Tty7App::settings`; the `active_settings()` accessor is repointed so every settings widget/handler is unchanged. The overlay covers the whole window with its nav rail reaching the top to match the tab rail, and a close button at the top-right (Esc / Cmd+, also close). Its top band stays a window-drag region (double-click zooms) since the overlay hides the real title bar, and that close button steps aside while the theme picker panel — which carries its own — is open. Settings also gains a search box in the nav header (borderless, focused on open): typing annotates each section link with a `(N)` match count over a keyword index and auto-selects the best-matching section, so the section nav stays put instead of collapsing into a flat list. The settings nav width is unified with the tab sidebar's default so toggling the overlay doesn't shift the left column. * test(app): wrap keybinding-test window root in gpui-component Root The settings overlay's nav-header search box renders on every section (Keybindings included), and gpui-component widgets reach for the window's Root layer, which panics if the window root isn't one. The harness built Tty7App directly as the root, so the three keybinding-capture tests panicked in Root::read. Wrap the app in Root like main.rs does and recover the typed entity via Root::view(); drive it with update_in. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
61da15dc1b |
feat(settings): add bell, notify-threshold, mouse-reporting, and session-restore controls (#68)
Expose four terminal preferences in Settings that previously had no knob (or were hardcoded): - Terminal → Bell: Off / Visual / Audible. Audible rings the system bell (NSBeep on macOS), falling back to the visual flash where no system bell exists so an opted-in bell is never silent. - Terminal → Notifications: configurable "long command" threshold (5s/10s/30s/1m), replacing the hardcoded 10s floor. - Terminal → Mouse: "Report mouse to apps" toggle. Off keeps the mouse local (native selection + scrollback) regardless of what a full-screen app requests; Shift still bypasses per gesture. Cached per view and pushed on config hot-reload. - Window & Tabs: "Restore previous session" toggle. When off, the daemon is restarted on launch so the previous session's shells are hung up instead of left running orphaned (this launch never re-attaches to them). Config gains a BellMode enum plus bell / notify_threshold_secs / mouse_reporting / restore_session fields, each defaulting to the prior behavior. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.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 |
||
|
|
5302fc0f7e |
deps: bump memchr from 2.8.2 to 2.8.3 in the cargo-minor-patch group (#69)
Bumps the cargo-minor-patch group with 1 update: [memchr](https://github.com/BurntSushi/memchr). Updates `memchr` from 2.8.2 to 2.8.3 - [Commits](https://github.com/BurntSushi/memchr/compare/2.8.2...2.8.3) --- updated-dependencies: - dependency-name: memchr dependency-version: 2.8.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: cargo-minor-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
f9560dc30b | chore(release): v0.13.0 v0.13.0 | ||
|
|
e1d502a3f2 |
fix(ci): format code and platform-gate the ctrl glyph in keymap test (#67)
cargo fmt over app.rs/pane.rs/settings.rs/keymap.rs (rustfmt CI job). key_chords_splits_a_sequence_into_keycap_groups hard-coded ⌃ for the ctrl modifier, which only renders that way on macOS; elsewhere key_tokens maps ctrl to "Ctrl", so the test failed on the Linux and Windows runners. Gate the expected glyph behind target_os like the existing SECONDARY and SHIFT constants. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
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. |
||
|
|
2443a951c0 |
fix(ui): trim the first tab's left gap flush to the traffic-light reserve (#62)
Drop the tab strip's leading pl_2 to pl_0 so the first chip starts flush against the title bar's left reserve (80px traffic-light on macOS, 12px elsewhere) instead of adding an extra 8px gutter. Right side and per-chip text inset unchanged. |
||
|
|
9a4e97c0e2 |
fix(ui): remove the active-pane corner indicator dot (#63)
The focused pane is already distinguished by fading inactive panes (opacity 0.55) when a tab is split, so the corner dot was redundant. The focused flag still drives the fade. |
||
|
|
e71efed007 |
feat(keybindings): editable shortcuts, pane/tab actions, tmux preset (#65)
Implements issue #61's tmux-like input model in three layers — editable shortcuts (Settings → Keybindings), directional pane focus/resize/swap + relative tab nav + Activate Tab 1-9, and a tmux prefix preset — without parsing ~/.tmux.conf and without changing zero-config defaults. Closes #61. |
||
|
|
f77d62a918 | chore(release): v0.12.0 v0.12.0 | ||
|
|
4b29b5967f |
fix(ui): stop the title-bar strip from clipping the Windows close button (#60)
On Windows the tab strip reserved only 100px on the right for the native window controls, but gpui-component's TitleBar lays out 12px of left padding plus three 34px caption tiles (─ ▢ ✕ = 102px) beside the strip — 114px in all. The 14px shortfall let the strip overrun the bar and shove the ✕ off the rounded corner once the overflow "⋯" was pinned to the strip's right edge. Reserve the full 114px so the strip's right edge meets the controls and the "⋯" keeps its inset. Co-authored-by: thomas <thomas@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
b5beba6d80 |
feat(release): ship a Linux AppImage alongside the tarball (#55)
* feat(release): ship a Linux AppImage alongside the tarball The Linux release was a bare, dynamically-linked binary built on ubuntu-latest, so it only reliably ran on Ubuntu — Fedora/Arch users hit missing/mismatched runtime libs. Add an AppImage that bundles the x11/wayland/xkb/fontconfig/freetype libs so it launches across distros. - bundle-appimage.sh: linuxdeploy populates an AppDir + deps, completions go beside the binary (usr/bin/completions, matching signature.rs's current_exe lookup), appimagetool packs it. Runs FUSE-less on CI. - release.yml: new "Package Linux AppImage" step after the tarball, libfuse2/file added to the Linux deps, *.AppImage added to the upload list. The AppImage step avoids `rm -rf dist` so the tarball survives. - README (en + zh): recommend the AppImage, keep the tarball as the bare fallback. Note: glibc is not bundled, so ubuntu-latest still sets the glibc floor. * fix(release): downscale AppImage icon to a resolution linuxdeploy accepts linuxdeploy rejected the 1024x1024 app-icon.png (its valid list tops out at 512). Resize to 256x256 with ImageMagick's convert (added to the Linux apt deps) before handing the icon to linuxdeploy. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
78a6f00ec8 |
feat(settings): redesign theme picker with a slide-in panel (#56)
* feat(settings): redesign theme picker with a slide-in panel Redesign the Appearance page's theme UI: - Replace the inline theme gallery with a compact "Current theme" card (preview + name + light/dark) that opens a searchable theme picker in a right-hand panel; applying a theme keeps the panel open so several looks can be tried in a row. - Add `theme_panel_open` / `theme_search` to `SettingsState`, plus toggle/close helpers and an `active_settings_mut` accessor. - Share one `theme_preview` between the card and the panel cards; keep the existing bar-based preview shape. - Group the custom-theme controls (duplicate / color editor / open folder) under the Theme section instead of stranding them at the foot of the page, and drop the near-black `.primary()` fill on the Duplicate button so it fits the soft, mostly-outline sheet. * style: rustfmt theme panel border_color --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
0fdfcf127a |
feat(ui): add a title-bar overflow menu for palette and settings (#57)
* feat(ui): add a title-bar overflow menu for palette and settings The command palette and settings panel were reachable only by keyboard (⌘P / ⌘,) — no on-screen affordance existed, so the app leaned entirely on shortcuts for its two most useful non-terminal entry points. Add a ghost "⋯" button on the title bar's otherwise-empty right edge (same 30px tile rhythm as the "+"), opening a small dropdown: - Command Palette ⌘P - Settings… ⌘, Both rows dispatch the real gpui action, so a click and the shortcut share one path and each row auto-renders its keybinding hint. The menu is hidden while the settings tab is active (both entries are redundant there). On Windows/Linux the window controls sit on the right, so the button gets extra right padding to read as a menu, not a fourth control. * fix(ui): keep the title-bar overflow menu from drifting into the corner The '⋯' is pinned to the right edge of a strip that used '.w_full()', but the title bar sizes its content by intrinsic width, so 'w_full' never tracked the window. Shrinking the window left the strip's right edge — and the pinned '⋯' — lagging behind, sliding the button into the rounded corner where it clipped and read cramped. Derive the strip width from the live viewport instead, mirroring the cap already used for the chip row, so the right edge tracks the window at every size and the '⋯' holds its original tight 8px inset on resize. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
4c2082235d |
revert(branding): restore original terminal-window logo (#59)
The #54 branding refresh replaced the original line-art logo with a heavier Slate-tile + gradient-ring mark. Restore the original assets (rounded terminal window frame, title-bar divider, orange cursor block) across svg/png/icns/ico, reverting to the pre-#54 versions. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
90cae6c731 | chore(release): v0.11.0 v0.11.0 | ||
|
|
c4bee13d83 |
feat(theme): file-based themes, in-app editor, and a UI/branding refresh (#54)
Replace compiled-in presets + colors.*/ansi_colors.* overrides with a file-based theme system: a serializable seed (bg/fg/accent/cursor/selection + ANSI-16, optional gradient/image/opacity/blur) with all chrome derived, light/dark inferred from luminance behind a WCAG guard, built-ins + user YAML + on-the-fly iTerm2 import via a hot-reloaded registry, and an in-app duplicate-to-edit color editor. Also: prompt-editor shift-click/word-drag selection and cross-platform word keys, ghostty-style tab labels, flat menu highlights, a redesigned app icon, the Background Service -> Daemon rename, and a gated TTY7_PROFILE build-timing probe. |
||
|
|
a50300f0ff | chore(release): v0.10.0 v0.10.0 | ||
|
|
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> |
||
|
|
aae33ede23 |
feat(links): let Cmd+click open directories (#50)
Follow-up to #49: iTerm2-style semantic paths — an existing directory in the row text links like a file does, and the system opener (open / xdg-open / explorer) already handles directories natively, so detection is the only change. Bare paths only: a token carrying a :line suffix still requires a file, so localhost:8080 can't link just because a directory named localhost exists in the cwd. 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> |
||
|
|
75c4fed62f | docs(readme): drop the header screenshot | ||
|
|
0ee126a7fd |
docs(readme): restructure around Why/Features, cut prose and boilerplate (#48)
* feat(view): explain a dead Ctrl+R instead of failing silently (#46) When shell integration never engages in a pane — typically because a figterm-style PTY shim (kiro-cli-term, qterm) exec'd over the shell and swallowed its OSC 133 reports — the whole command-editor overlay is absent by design, and Ctrl+R used to fall through to the raw PTY with no hint of why the history menu didn't appear. Now that raw-path Ctrl+R raises a one-shot, per-pane notice (floating bottom-right) saying integration hasn't engaged, refined off-thread with the daemon's foreground-process name when it matches a known shim: the wrapper is the culprit worth naming, since "install integration" advice would mislead — the hooks are installed, something between the shell and tty7 is eating their output. The chord still reaches the PTY, so the shell's own reverse-i-search keeps working as the fallback. Guards keep it honest: silent inside an 8s startup grace window (slow rc files legitimately haven't reported yet), on the alt screen, or once integration has engaged (a running foreground command is then the obvious reason); retracted if a slow shell engages late; dismissed by the next keystroke or a 15s timeout. * docs(readme): restructure around Why/Features, cut prose and boilerplate Replace the prose About section with a four-point Why tty7 list, split Features into prompt vs window groups written as one-line benefit bullets, drop emoji section headers, fold acknowledgements/contributing/ license into a one-line footer, and keep zh-CN in sync throughout. |
||
|
|
5e7a3240ad | chore(tooling): add repomix config for AI-friendly repo packing |