Files
tty7/crates/tty7-core/Cargo.toml
T
l0ng-ai 12ec0c92a9 fix(sidebar): stop ungrouping tabs when a git probe fails (#1011)
A tab's auto group comes from probing its cwd for a repository. Any
failure of that probe - git failing to spawn, the macOS /usr/bin/git shim
dying while Xcode is mid-switch, a remote link dropping - came back as
"not a repository", overwrote the tab's remembered group, and was never
re-checked while the pane sat idle. Tabs in perfectly good repositories
dropped into Ungrouped and stayed there.

- Read root, home and branch in-process on this machine with
  gix-discover instead of three git processes. Only the line counts
  still come from `git diff --numstat`, so they keep matching the diff
  views; a detached HEAD is still named by git for the same reason.
- Remote hosts keep asking their own git (a new control request would
  force a dialect bump), but the probe now tells "not a repository"
  (exit 128, or the directory is gone) apart from a failure.
- A failed probe leaves the cache as it was, so a tab keeps its
  remembered group, and an unanswered cwd is retried every 10s.
2026-09-29 16:55:03 +08:00

257 lines
12 KiB
TOML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[package]
name = "tty7-core"
version.workspace = true
edition.workspace = true
description = "tty7's framework-free core: wire protocol, session daemon, PTY, SSH engine, and the domain model both the GUI and the headless server build on"
repository = "https://github.com/l0ng-ai/tty7"
license = "Apache-2.0"
publish = false
# The whole point of this crate is that it does *not* depend on gpui. Everything
# here has to compile and run on a headless Linux box (that is what
# `tty7-server` is), so nothing windowing-, rendering- or GUI-shaped belongs in
# these dependencies.
[dependencies]
anyhow.workspace = true
log.workspace = true
serde = { workspace = true }
serde_json.workspace = true
# SSH connection-manager data layer (`core::ssh_profile` / `core::keychain`) and
# workspace identity (`core::session`). `uuid` mints stable ids (v4) and
# serde-serializes them as strings.
#
# These live here rather than in the GUI because `Config` embeds
# `Vec<SshProfile>` and `config.json` has to parse identically on the server.
#
# Deliberately *absent*: `keyring`. Nothing in this crate reads or writes a
# secret — the daemon gets them pre-resolved on the wire (`NativeSshSpec`) — and
# a headless `tty7-server` has no OS keychain to read from, so the vault's
# storage half lives in the GUI crate instead (`tty7::core::keychain`). Keeping
# it out here is what stops a static server binary from linking
# `zbus`/`secret-service` and 30-odd crates behind them for code it can never
# call. What stays is the *naming* half: `CredentialRef` and the account scheme,
# which `config.json` parsing needs.
uuid = { version = "1", features = ["v4", "serde"] }
# Two unrelated hashes, both server-side: `daemon::install::checksums` verifies a
# downloaded `tty7-server` asset against the release's sha256 manifest, and
# `core::keychain::key_account_from_contents` derives the sha512-hex account key
# a private key's passphrase is stored under (PRD §7.2) — the account *name* is
# part of the persisted config contract, so it belongs next to `CredentialRef`
# even though only the GUI computes one today.
sha2 = "0.11"
# Finding the repository a pane's cwd is in (`core::git::head`) without a
# `git` process: the sidebar asks it of every pane, and a spawn that fails —
# `/usr/bin/git` on macOS is a shim that dies whenever the selected Xcode is
# mid-switch — used to read as "not a repository" and ungroup the tab. Only the
# discovery crate: counts, diffs and every write still go through the `git`
# CLI, so what the sidebar says stays the same answer the diff views give.
gix-discover = { version = "0.56", features = ["sha1"] }
# The gitignore matcher chain (`core::gitignore`) the file tree dims entries
# with — the same crate ripgrep uses. Lives here rather than in the GUI because
# the remote server has to answer "is this path ignored?" with the identical
# implementation.
ignore = "0.4"
# The matcher behind `Host::search_content` (the right panel's Search tab):
# a literal, whole-word or regular-expression query compiled once and run over
# each file the `ignore` walk above hands it. Lives here for the same reason
# `ignore` does — a remote `tty7-server` answers the search with the identical
# implementation. Already in the tree via the GUI and `ignore`, so this pins no
# new code.
regex = "1"
# Format-preserving TOML editing for `core::agent_hooks`: Kimi Code takes its
# hooks as `[[hooks]]` entries in the same `config.toml` that holds the user's
# providers, models and comments, so installing must edit that file in place
# without reformatting it — which rules out the plain `toml` crate's
# parse-and-reserialize round trip. Already in the tree transitively, so this
# pins no new code.
toml_edit = "0.25"
# Lane assignment for the commit graph (`core::git::log`) keeps a couple of
# parents and a handful of edges per row; a SmallVec keeps those off the heap
# for the shapes that make up almost all of a real history. Already in the tree
# via the GUI, so this pins no new code.
smallvec.workspace = true
# Cross-platform PTY for the daemon: a Unix pty on Unix, ConPTY on Windows,
# behind one blocking `Read`/`Write`/`resize` API. This is what lets
# `daemon::pane` share a single code path across platforms instead of
# hand-rolling fd/ioctl/signal code.
portable-pty = "0.8"
# Native (pure-Rust) SSH client for the daemon's russh session engine
# (`daemon::ssh`) — see the root manifest's note for why we own this stack
# instead of shelling out to `ssh`.
# HTTPS client for the remote-server installer (`daemon::install::download`).
# The GUI's own update check rides `reqwest_client`, which wraps Zed's reqwest
# fork behind `gpui::http_client` — unavailable here, since this crate must not
# depend on gpui. `ureq` is blocking (matching the installer, which runs on a
# daemon std thread), rustls-based (no OpenSSL to find at build time), and
# reuses the `rustls`/`http` versions already in the tree. `gzip` is off: the
# assets are already-compressed binaries.
#
# Optional, and off by default, so the static musl `tty7-server` never links a
# TLS stack for a code path it cannot take — it *is* the binary being
# downloaded. The GUI package turns the feature on (see the root `Cargo.toml`).
ureq = { version = "3", default-features = false, features = [
"rustls",
"win-system-proxy",
"socks-proxy",
], optional = true }
russh = "0.62"
russh-sftp = "2"
# tokio powers only the russh session engine — a single runtime `daemon::ssh`
# owns. The daemon's PTY/reader/writer threads remain std threads and never
# touch it; they cross into async through bounded/unbounded channels.
tokio = { version = "1", features = [
"rt-multi-thread",
"net",
"io-util",
"sync",
"time",
"macros",
"process",
"fs",
] }
# Filesystem watching for `host::local` (`Host::watch`): one non-recursive
# watcher per expanded directory, coalesced into 100ms batches. Same crate and
# version the GUI already used for the file tree, so the server watches a remote
# tree exactly the way the client watched a local one.
notify = "8"
# `smol::channel` carries watch batches out of the coalescing thread. Only the
# channel is used — the executor stays in the GUI — but taking it from `smol`
# rather than `async-channel` directly keeps the `Receiver` type identical to the
# one gpui code already awaits.
smol.workspace = true
# SIMD byte search for the OSC tokenizer's Ground/Ignore fast paths — the
# sniffers sit on the full-throughput output stream (100+ MB/s at full drain),
# where a per-byte state machine costs a measurable slice of the reader loop.
memchr = "2"
# Base64 for the byte fields that cross the control dialect's JSON wire
# (`host::Output`'s stdout/stderr). JSON has no byte type — `serde_json` renders
# a `Vec<u8>` as an array of decimal numbers, inflating a 1 MB `git diff` to
# roughly 4 MB. Base64 costs 1.33× instead. Already in the tree via russh.
base64 = "0.22"
# zlib inflate for the kitty graphics protocol's `o=z` (compressed) pixel
# payloads (`core::kitty_graphics`). Pure-Rust, no new native code, and already
# in the tree transitively — the same crate `terminal-browser` uses for the
# deflate side, so the two ends agree on the wire format.
miniz_oxide = "0.8"
# Reading coding agents' past sessions (`core::agent_history`), on this
# machine or a remote one's server: Copilot names a session in YAML, Cursor
# keys a chat by the md5 of its directory and hex-encodes its metadata, and
# OpenCode and Cursor keep history in SQLite — bundled, so no system library
# is needed on any host the server lands on.
serde_yaml = "0.9"
md5 = "0.8"
hex = "0.4"
rusqlite = { version = "0.40", features = ["bundled"] }
[target.'cfg(unix)'.dependencies]
libc = "0.2"
# GSSAPI/Kerberos SSH auth — see the `gssapi` feature below for why it is
# optional rather than an unconditional Unix dependency.
libgssapi = { version = "0.11", optional = true }
# CFStringTokenizer-adjacent CoreFoundation FFI: `daemon::pane` reads a pane's
# foreground process name through it on macOS.
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.10"
# Read the system proxy out of SCDynamicStore for tty7's own downloads
# (`daemon::install::proxy`). It pins core-foundation 0.9, so that module goes
# through this crate's `core_foundation` re-export rather than the 0.10 above —
# `get_proxies` hands back 0.9 types and the two versions do not interoperate.
system-configuration = { version = "0.6", optional = true }
system-configuration-sys = { version = "0.6", optional = true }
# The Windows GUI⇄daemon transport is loopback TCP, which (unlike a Unix socket)
# any local process can connect to — so the daemon authenticates each connection
# against a random token it writes into the user-private port file. `getrandom`
# is the OS CSPRNG that mints that token.
[target.'cfg(windows)'.dependencies]
getrandom = "0.3"
# Toolhelp process enumeration + `TerminateProcess`, used by `daemon::winproc` to
# title a pane by its foreground command and to tear down a shell's descendant
# tree on hangup (ConPTY's `kill` only reaches the shell itself). `Registry`
# additionally lets `daemon::windows_env` re-read the two environment hives at
# pane-spawn time so a long-lived daemon stops handing out its startup `PATH`.
# `IpHelper` is `GetExtendedTcpTable`, which is how `daemon::procinfo` answers
# the Info tab's Ports section here — the platform has no `lsof` to shell out to.
windows-sys = { version = "0.59", features = [
"Win32_Foundation",
"Win32_NetworkManagement_IpHelper",
"Win32_Security",
"Win32_System_Console",
"Win32_System_Diagnostics_ToolHelp",
"Win32_System_Registry",
"Win32_System_Threading",
"Win32_UI_WindowsAndMessaging",
] }
# Read the system proxy out of the registry for tty7's own downloads
# (`daemon::install::proxy`).
winreg = { version = "0.52", optional = true }
[dev-dependencies]
# Sandboxes for the `host::conformance` suite: a fresh empty directory per case,
# removed on drop.
tempfile = "3"
# `test-util` unlocks tokio's paused clock (`start_paused`) so the ssh prompt
# broker's timeout/retry tests run instantly instead of in real time.
tokio = { version = "1", features = ["test-util", "macros", "rt"] }
# `gssapi` — GSSAPI/Kerberos `gssapi-with-mic` SSH auth (`daemon::ssh::auth`).
#
# Off by default, and the `tty7` GUI turns it on, so the GUI's behavior is
# unchanged. It has to be optional because `libgssapi` binds the *system* MIT /
# Heimdal krb5 through bindgen: a machine without krb5 headers cannot build it
# at all, and a static musl `tty7-server` — the binary remote workspaces push
# onto arbitrary hosts (decision D10) — cannot link it under any
# circumstances. Without the feature, `SshAuthMode::Gssapi` reports that the
# method is unavailable in this build and the other auth families are untouched.
#
# **Test coverage.** Everything in `daemon::ssh::auth` that can be tested without
# krb5 — the service-host list — is gated on `unix` alone, so a plain
# `cargo test -p tty7-core` runs it. Only the libgssapi FFI half needs
# `--features gssapi`, and it has no unit tests (it is all foreign calls). Do not
# gate a testable helper on this feature: `cargo test --workspace` unifies it on
# from the GUI package, so a feature-gated test looks green there and silently
# vanishes the moment anyone narrows to `-p tty7-core`.
[features]
default = []
gssapi = ["dep:libgssapi"]
# Lets this build download a `tty7-server` release asset over HTTPS and push it
# onto a remote machine. Off by default so `tty7-server` — which is
# the thing being downloaded, and never the thing doing the downloading — links
# no HTTP client. Without it, `daemon::install` still installs from bytes it is
# handed and still launches/probes a remote daemon; only the fetch fails, with a
# message saying so.
# The platform proxy readers ride along: they exist only to configure the
# downloader, and each is a no-op on the targets it is not written for.
remote-install = [
"dep:ureq",
"dep:winreg",
"dep:system-configuration",
"dep:system-configuration-sys",
]
# The GitHub tab's REST client (`core::github::http`). It rides the same HTTPS
# stack and system-proxy resolution as the installer, so it is that feature
# plus a name of its own: the server, which never reads GitHub, builds neither.
github = ["remote-install"]
[lints]
workspace = true