Commit Graph
84 Commits
Author SHA1 Message Date
l0ng-ai cbda78ebcf Merge branch 'ssh-ws4-forwards' into ssh-connection-manager
# Conflicts:
#	src/daemon/protocol.rs
#	src/daemon/server.rs
#	src/daemon/ssh/mod.rs
#	src/terminal/remote.rs
#	src/ui/app.rs
2026-07-14 01:37:37 +08:00
l0ng-ai bb1346f887 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.
2026-07-14 01:33:57 +08:00
l0ng-ai 827f7f573f Merge branch 'ssh-ws5-sftp' into ssh-connection-manager
# Conflicts:
#	src/daemon/protocol.rs
#	src/daemon/server.rs
#	src/terminal/remote.rs
#	src/ui/app.rs
#	src/ui/mod.rs
2026-07-14 01:27:50 +08:00
l0ng-ai 987536f21c 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.
2026-07-14 01:21:36 +08:00
l0ng-ai 5c23156acb 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.
2026-07-14 01:11:38 +08:00
l0ng-ai a7ed49028b Merge branch 'ssh-ws1-data' into ssh-connection-manager
# Conflicts:
#	Cargo.lock
2026-07-14 00:40:09 +08:00
l0ng-ai fc1e69e112 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.
2026-07-14 00:36:08 +08:00
l0ng-ai 6803db4240 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.
2026-07-14 00:02:03 +08:00
l0ng-aiandl0ng-ai 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>
2026-07-13 22:06:34 +08:00
l0ng-aiandl0ng-ai 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>
2026-07-13 20:06:53 +08:00
ayamir 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
2026-07-13 19:55:44 +08:00
dependabot[bot] 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>
2026-07-13 19:55:34 +08:00
l0ng-ai f9560dc30b chore(release): v0.13.0 v0.13.0 2026-07-13 19:33:29 +08:00
l0ng-aiandl0ng-ai 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>
2026-07-13 16:14:21 +08:00
ayamir 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.
2026-07-13 16:02:02 +08:00
l0ng-ai 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.
2026-07-13 15:35:50 +08:00
l0ng-ai 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.
2026-07-13 15:35:45 +08:00
l0ng-ai 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.
2026-07-13 15:28:36 +08:00
l0ng-ai f77d62a918 chore(release): v0.12.0 v0.12.0 2026-07-13 11:03:47 +08:00
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>
2026-07-13 10:53:21 +08:00
l0ng-aiandl0ng-ai 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>
2026-07-13 10:43:26 +08:00
l0ng-aiandl0ng-ai 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>
2026-07-13 10:38:45 +08:00
l0ng-aiandl0ng-ai 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>
2026-07-13 10:37:29 +08:00
l0ng-aiandl0ng-ai 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>
2026-07-13 10:37:24 +08:00
l0ng-ai 90cae6c731 chore(release): v0.11.0 v0.11.0 2026-07-12 19:52:26 +08:00
l0ng-ai 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.
2026-07-12 19:51:07 +08:00
l0ng-ai a50300f0ff chore(release): v0.10.0 v0.10.0 2026-07-11 19:14:19 +08:00
l0ng-aiandl0ng-ai 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>
2026-07-11 19:12:10 +08:00
l0ng-aiandl0ng-ai 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>
2026-07-11 17:32:26 +08:00
ayamirandl0ng-ai 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>
2026-07-11 14:32:51 +08:00
l0ng-ai 75c4fed62f docs(readme): drop the header screenshot 2026-07-11 10:24:37 +08:00
l0ng-ai 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.
2026-07-11 10:22:54 +08:00
l0ng-ai 5e7a3240ad chore(tooling): add repomix config for AI-friendly repo packing 2026-07-10 19:05:12 +08:00
l0ng-ai 3cc5d3ee0f chore(docs): drop the openwiki directory 2026-07-10 18:59:06 +08:00
l0ng-aiandClaude Fable 5 bf9499260f chore(release): v0.9.0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v0.9.0
2026-07-10 18:30:05 +08:00
7401fd59df feat(history): Ctrl+R fuzzy search menu with run metadata (#45)
* feat(history): Ctrl+R fuzzy search menu with run metadata

Ctrl+R grows from the single-line reverse-i-search into a browsable
menu of ranked candidates floating beside the prompt:

- Matching is fuzzy (src/terminal/fuzzy.rs, a dependency-free
  affine-gap aligner: word-boundary and consecutive-run bonuses, gap
  penalties; space-separated query terms must all match), blended with
  the existing frecency scores so a command you run constantly — or
  ran in this directory — outranks an equally-good textual match.
- An empty query lists the whole history by frecency, so bare Ctrl+R
  is a "recent & relevant" browser. Matched characters highlight in
  the rows; Ctrl+R/Down and Ctrl+S/Up move the selection, Enter loads
  the line into the editor, Cmd+Enter runs it outright. The classic
  (reverse-i-search) prompt line stays.
- History records now carry run metadata: new lines are
  <ts>\t<exit>\t<cwd>\t<command>, written when the command finishes
  (zsh INC_APPEND_HISTORY_TIME-style) so the exit code sniffed from
  OSC 133;D lands in the record; older formats still parse, and
  zsh/bash HISTFILE timestamps carry over when seeding. The menu shows
  "ran 3h ago" and a red x-badge on commands whose last run failed.
- RemoteTerminal exposes prompt_seq/last_exit_code so the view can
  tell a fresh post-command prompt report from the stale pre-submit
  state even when 1 Hz polling misses a fast command's running window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* style: cargo fmt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 18:28:42 +08:00
l0ng-aiandl0ng-ai 2c3b13fc99 fix(links): balance parens/brackets in URL detection (#44)
ASCII () and [] are legal URL characters, so the URL token scan let an
orphan closer glue the surrounding prose onto the link:
'(https://github.com/l0ng-ai/tty7/pull/43)(Fixes' detected everything
through '(Fixes' as one URL. Meanwhile trim_trailing_punct stripped a
trailing ')' unconditionally, so a balanced Wikipedia-style
'…_(programming_language)' lost its real closing paren.

Treat closers by balance instead, both ways:
- cut the URL at the first ')' or ']' with no matching opener inside it
  (the '(' before the scheme is prose, dropped with the prefix), and
- keep a trailing ')' / ']' whose opener sits inside the URL, stripping
  only unmatched ones (full-width closers can't appear in a URL at all
  and stay unconditional).

The www. fallback re-trims after dropping leading wrappers, since
'(www.x)' now keeps its ')' through the first, balance-aware trim.
IPv6 '[::1]' hosts survive the same way.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-07-10 14:37:07 +08:00
l0ng-aiandl0ng-ai 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>
2026-07-10 14:36:44 +08:00
l0ng-ai c514df8e5a chore(release): v0.8.0 v0.8.0 2026-07-10 11:48:38 +08:00
l0ng-aiandl0ng-ai e1a552ec99 feat(clipboard): add copy-on-select (#40)
Selecting with the mouse — drag, double-click word, triple-click line,
over terminal output or the prompt's command editor — copies the
selection the moment the gesture ends, no Cmd+C needed. Opt-in via
Settings -> Terminal -> Clipboard (config key copy_on_select), off by
default so a stray selection never overwrites the clipboard.

Closes #34

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-07-10 11:42:25 +08:00
l0ng-aiandl0ng-ai 388fffeaaf fix(hints): dismiss held-modifier tab badges on window activation flip (#41)
* fix(hints): dismiss held-modifier tab badges on window activation flip

The badges were dismissed only by ModifiersChanged (release) or a real
keypress. Deactivating mid-hold — cmd-tabbing away, Spotlight, a click
into another app — delivers the modifier release to whatever app is key
by then, so this window never saw it and the badges stuck on until some
later keypress; mouse-only use left them up forever.

Dismiss on every window-activation flip via observe_window_activation.
Flipping on *both* directions also cancels a reveal scheduled just
before the switch, so the timer can't pop badges up in a window the
user already left.

Tested with a headless gpui harness: Tty7App now builds through a
with_session seam (a zero-tab session restores the home page, spawning
no terminal/daemon), the test activates the window for real, simulates
the bare-secondary hold, then deactivates and asserts the badges (and a
pending reveal) are gone. Session's save/load test takes a shared file
lock so parallel tests can't clobber the pinned session.json.

* style(hints): satisfy rustfmt in gpui test harness

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-07-10 11:29:17 +08:00
l0ng-aiandl0ng-ai 3cd9dee454 feat(selection): auto-scroll scrollback when dragging past the pane edge (#39)
Dragging a selection above the top (or below the bottom) of the pane
used to pin at the visible edge: pos_to_cell clamps the row and
on_select_update never moves the display offset, so scrollback content
just out of view was unreachable by mouse selection.

Now a left-drag that crosses the pane's vertical edge arms a repeating
50ms task that scrolls the display and re-anchors the selection's
moving end to the edge row, so the scroll continues while the pointer
holds still past the edge (mouse-move events alone stop the moment the
hand does). Speed scales with the overshoot distance — 1 line/tick when
grazing the edge up to a capped 8 lines/tick (~160 lines/s) — and the
task stops when the pointer re-enters, the drag ends, or a newer task
supersedes it (epoch guard, so a quick leave/re-enter/leave can't
double the speed). On the alt screen scroll_display no-ops (no
scrollback), leaving TUIs untouched.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-07-10 10:57:54 +08:00
l0ng-ai 9b03f006b1 chore(release): v0.7.0 v0.7.0 2026-07-10 09:23:28 +08:00
ayamirandl0ng-ai 8a65ca9cf8 feat(settings): add font ligature toggle (#38)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-07-10 09:16:02 +08:00
ayamir eeb8eed6ba feat(theme): support ANSI color overrides (#37) 2026-07-10 09:05:10 +08:00
ayamir 6b202bffc6 feat(input): support <C-l> to clear screen (#36) 2026-07-10 09:04:42 +08:00
l0ng-ai 315dd85831 chore(release): v0.6.2
Claude-Session: https://claude.ai/code/session_01ABey161AUxhgmJC3PRoYtF
v0.6.2
2026-07-08 22:36:43 +08:00
l0ng-ai 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
2026-07-08 22:31:57 +08:00
l0ng-aiandl0ng-ai 59a4c9d859 fix(terminal): stop severing wide Nerd Font prompt glyphs (#17) (#31)
* fix(terminal): give solo prompt glyphs a two-cell paint window

Nerd Font prompt icons and the multiline `➜`/`❯` rendered sliced off on
the right (#17). tty7 paints any single-column glyph clipped to exactly
one cell, but a symbol face routinely draws ink past that cell: a
non-Mono Nerd Font sets a one-cell *advance* on its icons yet fills up to
~1.9 cells (measured across Hasklug / Meslo / JetBrainsMono NF), and the
OS cascade serves a proportional `➜` the same way when nothing in the
font list covers it. The per-cell content mask then severed the overflow
— the incomplete icons and cut-off arrow in the report.

Advance is no signal here (it reads one cell for those overflowing
icons), so widen every solo glyph's clip to two cells. A glyph that
already fits is untouched — it has no ink to spill — while a symbol that
overflows renders whole, bleeding into a trailing blank the way iTerm2
and Terminal.app do with non-Mono faces; the two-cell bound keeps a
pathological face from smearing a lone glyph across the row. Batched
Run/Wide segments keep their exact column-span clip, and Mono Nerd Fonts
are pixel-identical. Pairs with the native powerline separators from #19.

Verified in-app on Apple Silicon with a non-Mono "Hasklug Nerd Font"
primary: icons and a powerline prompt that were severed now render whole,
with no change to ASCII, CJK, or the Mono-font path.

* test(terminal): cover both clip-width branches, not just the solo constant

Fold the solo/batched clip-width decision into one pure `seg_clip_width`
so the branch itself is testable (per PR review): the test now asserts a
batched Run/Wide segment clips to its exact column span while a solo
glyph gets the two-cell window, instead of only checking the standalone
constant. No behavior change.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-07-08 22:10:49 +08:00
l0ng-aiandl0ng-ai c6e071362a fix(menu): soften context-menu and dropdown row hover to match the palette (#24)
gpui-component's MenuItemElement highlights the hovered/selected row from
`tokens.accent` + `accent_foreground`. tty7's theme never overrode those, so
menu rows fell back to the stock saturated accent — a hard highlight out of
step with the app's soft mix-based palette.

Point the accent tokens (and the plain `accent`/`accent_foreground` fields the
input completion / code-action popovers read) at `list_active` (mix 0.17), the
same soft fill the command palette uses for its selected row, and keep the
hover text at `foreground` so it stays legible on the low-contrast fill. Right-
click menu, "+" dropdown and command palette now share one hover language.

Claude-Session: https://claude.ai/code/session_01ABey161AUxhgmJC3PRoYtF

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-07-08 18:58:57 +08:00