mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
82e28784ce4d77f2e6d32f032b45f4cdab36ea5c
153
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
82e28784ce |
fix(ssh): stop replaying a stale password at keyboard-interactive (#487)
`try_keyboard_interactive` answered a password-shaped round from the keychain, marked the stored password spent whether or not it had been used, and returned on the first `Failure` — so the `MAX_ROUNDS` loop never got a second pass with the stored password withheld. The same dead secret went out on every reconnect and the user was never once asked to type a different one; `ki_submit` always emitted `KeychainWrite::None`, so nothing could clear it either. `collect_ki_answers` now reports where its answers came from, and only a round that actually sent the stored password spends it — which also fixes an OTP-then-password flow that was refusing the stored password for no reason, its first round having burned the allowance on a code. On a rejection whose last round came from the keychain, and where the server still offers the method, the request is started over with the stored password withheld, so the next round reaches the prompt. That retry is bounded twice over: the restart spends the stored password, so no second restart can qualify, and the round counter it shares with the info-request loop caps the method either way. The failure text now says which of the two was turned down. Scope, honestly: the only live scenario is auth mode Auto against a server offering keyboard-interactive but not password, with a stored password for that endpoint — a profile pinned to KeyboardInteractive gets `password: None` and always prompts, and Password never tries KI. Whether the symptom shows also depends on the server: OpenSSH ends a rejected kbdint request with USERAUTH_FAILURE (symptom holds), while a device that re-issues an InfoRequest in the same request already reached the prompt. `AuthPromptKind::KeyboardInteractive` grows a `#[serde(default)]` `stored_rejected`, same both-directions compatibility as `KeyPassphrase`'s `rejected` and the same reason `PROTOCOL_VERSION` stays put. The sheet shows the warning line and, on submit, forgets the rejected password. That needed an endpoint the KI prompt does not carry, which also fixed a bug next door: `raise_routed_auth` called `from_prompt(.., None, false)`, so every routed password write was keyed to port 22 regardless of the real port and the rejected self-heal could never fire there. `PendingAuth` now carries the endpoint and the auto-supplied flag, read straight off the route's `NativeSshSpec`. |
||
|
|
d04d104559 |
fix(ssh): let a rejected stored passphrase ask again (#486)
Saving the wrong passphrase for an encrypted key locked that key out permanently. `passphrase_submit` wrote `SetKeyPassphrase` on the "remember" checkbox alone — before the daemon had tried the secret, since `apply_keychain_write` runs ahead of `respond_active` — and `try_identity_file` treated a stored passphrase as final: a decrypt failure with one went straight to "could not decrypt identity file", with no prompt and nothing in the UI that could let go of it. The daemon now says so. `AuthPromptKind::KeyPassphrase` grows a `rejected` flag, and a stored passphrase that does not open the file falls through to the interactive prompt carrying it, so the typed answer still gets its attempt. A passphrase the user typed this time keeps the hard failure — that is a wrong answer, not stale state. The sheet renders the warning line the password sheet already had, and a rejected prompt answered without "remember" now emits `DeleteKeyPassphrase`, mirroring the password idiom exactly. The flag is a `#[serde(default)]` field on a struct variant of an externally tagged enum, which is compatible in both directions: an older peer never sets it and serde ignores fields it does not know. So `PROTOCOL_VERSION` deliberately does not move — the remote-server handshake gates on it, and a bump would turn away older servers over a field they can safely ignore. `protocol.rs`'s compat test pins both directions. Also: deleting an SSH profile now drops the key-passphrase entries no other profile still references, which is what `delete_profile_confirmed`'s own comment already claimed to do but only ever did for the password. |
||
|
|
425f87e9a4 |
fix(core): key the machine tree to the config directory (#462)
* fix(core): key the machine tree to the config directory The tree resolved from $HOME while everything else an instance owns — views.json, the scrollback, the history, both sockets, the pidfile, and daemon.lock — resolved from the config directory. So --config-dir moved every part of an instance except the one that says which workspaces exist, and two tty7s pointed at different config directories, each holding its own lock and each certain it was the only server on the machine, still co-owned one ~/.local/share/tty7/machine.json. MachineStore::persist writes the document whole. The second one to flush replaced the first one's workspaces with its own, and the next daemon to start read the survivor's tree as the machine's. An empty tree is not distinguishable from a machine that really has nothing on it, so the GUI does what an empty tree means and forgets those workspaces for good. A lock and the thing it protects have to be keyed alike. data_dir() now follows the config directory; TTY7_DATA_DIR stays as the highest-priority override so the test harnesses keep their sandboxes. Moving the path without carrying the file would lose every workspace at the moment of upgrade, which is the failure this change exists to stop, so the daemon adopts the legacy file on startup before it opens the store. The destination already existing is the whole guard: it means a newer run owns the tree and the copy at the old path is stale, from a build that predates the move and still writes where it believes the tree lives. Adopting that over the live file would hand the old tree back. * fix(core): only the machine's own instance inherits the legacy tree The migration moved `machine.json` into whichever config directory started first. In the very setup this change exists to fix — a default install beside a `--config-dir` one — that is the second instance renaming the machine's tree into its own directory, leaving the primary to come up owning nothing. It also fired in our own test suite, where `routed_pane` and friends launch a real `tty7-server --config-dir <TempDir>` under the developer's own `HOME`. Adoption is now the entitlement of the instance running out of the config directory this machine resolves to on its own: `$TTY7_CONFIG_DIR` where the box names one, `$HOME`'s otherwise. Comparing paths rather than asking whether `--config-dir` was passed is what keeps the ordinary install working — `spawn` hands every daemon it starts an explicit `--config-dir`, its own included — and counting `$TTY7_CONFIG_DIR` is what keeps remote hosts upgrading, since a remote `tty7-server` is launched without the flag and finds its directory that way. Also tightens the cross-filesystem fallback: a rename that failed because another process already carried the file over is the one benign race, not an error to report and not something to copy over. What is left copies through `create_new`, so "never overwrite what is already there" holds against a racing writer and not merely against an `exists` check several syscalls old, and a write that does not finish leaves nothing behind. Tests: the gate both ways, the appearance hint riding along, the same directory under two names, the copy path refusing an occupied destination, and two cross-process cases in `machine_tree` that start a real server under a scratch `HOME` — one carrying the legacy tree in, one leaving it alone. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
a55340ed7f |
fix(daemon): sweep a dead daemon's leavings on the writer's tick, not at startup
Review follow-ups on this branch. `history::sweep` still ran at startup, three lines under a new comment explaining why sweeping there is wrong. The reasoning transfers exactly, and worse than by analogy: a restore carries the dead pane's commands to its successor via `history::carry`, so sweeping before the window can ask deletes the file the request is about. Same shape as the scrollback bug, one file over. Both sweeps now run on the writer's tick off one shared id set, and the writer is named for what it does. `pane_attachable` lost its only caller when the restore path moved to `pane_free_for`, leaving a function kept alive by the test asserting on it. The attach site does not need to predict the listing: it tries the attach, and a pane that is gone falls through to the fresh spawn on its own. Gone, with its tests folded into `pane_free_for`'s. `restored_screen` now drops the snapshot in both directions. Keeping the file when it decoded to nothing left it to be re-read and re-rejected by every later restore, and swept never, for a pane the tree still names. Also: the module doc still said scrollback was off unless asked for, which is what this branch reverses; and #449 landed the whole feature with no CHANGELOG entry, so nothing told anyone that pane output now lives on disk. |
||
|
|
b3a66e75d0 |
fix(test): let the machine-tree seed keep up with a new pane field
PaneSeed grew a `shell`, but this test is unix-only, so a Windows box never compiles it and never says so. Build the seed from `bare` and the next field lands on its own. |
||
|
|
852d3178c8 | style: rustfmt | ||
|
|
477d82524f |
feat(daemon): keep every pane's screen, without asking
`persist_scrollback` is gone, and with it the switch, its three translations and the branches that read it. Keeping a capped tail of each pane's output is now what the daemon does, not something it can be asked to do. This reverses the call made when the feature landed. The argument for off-by-default was that the ring holds whatever the pane printed — echoed tokens, `env` output, an agent's transcript — and that writing that down should be the user's decision to make. What the argument missed is when the decision gets made: the moment anyone learns they wanted this is the moment a daemon has already died, and by then the setting could only be turned on for next time. A feature whose entire purpose is to survive an event nobody schedules cannot be opt-in. The cost is real and does not go away: pane output now lives at `<config>/scrollback/*.bin` on every machine, 0600 on unix and behind the config directory's ACL on Windows, capped at 256 KiB per pane and dropped as soon as no window can still ask for it. Old configs naming the key still parse — nothing in `Config` refuses unknown fields — so the key simply stops meaning anything. |
||
|
|
c138be687a |
fix(daemon): keep a pane's shell and its screen across a restart
Two things a pane lost when the background service stopped and started, both of them things the tree was the only possible place to keep. **The shell.** `PaneRecord` and `PaneSeed` carried a pane's cwd, its ssh spec and its agent, but never what it was running. A window rebuilding a dead pane from the tree therefore had nothing to pass and spawned on whatever the default shell is now — so a restart turned a bash pane into a PowerShell one, quietly and in place. The daemon resolves the override against the config at spawn time and is the only party that knows the answer, so it keeps it and reports it; the seed carries it too, for the panes a window spawned itself. A handoff carries it in the blob, because nothing on the far side of an `execve` can work out the command line of a child it never spawned. **The screen.** The startup sweep ran before the endpoint was listening, which is the one moment nothing can answer the question it asks: the registry is empty and the windows that know which screens are still wanted cannot say so yet. A tree that failed to parse made it worse — `read_machine` quarantines it and returns an empty `Machine`, so one bad file took every pane's stored screen with it. The sweep now happens only on the periodic pass, a tick later, with the registry filled in and the tree caught up; nothing is serving a request in between. Turning the setting *off* still clears the directory at once, because there the promptness is the whole promise. Two smaller ones alongside it: `restorable_pane_ids` now counts the tree's pane list and not only the panes some tab currently stands on — the two disagree while a window is between layouts, and being wrong costs a file swept a tick late in one direction and somebody's terminal in the other. And `restored_screen` drops the snapshot file *after* deciding it was not empty, so a snapshot holding nothing is no longer consumed by the request it could not answer. The restore path had no end-to-end test, which is how this shipped: the unit tests cover the file, not whether a window that reattaches is shown anything. The new one runs a real daemon, puts a marker on a real pane, stops the daemon, starts another, and reads the wire. |
||
|
|
edfadb7df2 |
Merge pull request #424 from l0ng-ai/feat/scm-foundation
feat(scm): a full Source Control panel, decorations and commit history |
||
|
|
2dc6a88af6 |
merge: main into the Source Control branch
Conflicts were the two streams touching the same seams, resolved by taking the newer decision on each side: - main's interface font scale (rems tokens) wins in right_panel.rs; the SCM panel keeps its local px steps until it moves onto that scale, and the now-unused PANEL_TEXT constants are gone. - main's l10n_keys! macro (idents only) means the key list carries no doc comments any more; our SCM keys fold into it, and PanelUntracked stays deleted — its only caller was the panel this branch replaced. - main's Command::localized palette style carries our Git group; ORDER keeps main's visibility and our width. - main's ansi_seed/clear_ink refactor in presets.rs carries the lane colours: lanes() now clears through the same helper semantics uses. - file_tree keeps both: main's drag-and-drop targets and this branch's git decorations per row. - diff_overlay keeps both: main's sidebar-count write-back on snapshot install and this branch's epoch read and untracked preview. - main's window.prompt SSH-close confirmation supersedes the bespoke modal our branch still carried; main's tile-glyph revert stands. - main's two new guards are satisfied: the fourteen SCM actions carry authored names on the Keybindings page (their palette wording, plus a new CmdGitToggleGraph), ja translates ScmDetached, and CmdGroupGit joins the kept-in-English list — Git is a name. 2571 tests, 0 failures. |
||
|
|
9815f2d16f |
fix(windows): restore Ctrl+C in panes (#459)
The daemon was created with CREATE_NEW_PROCESS_GROUP, which disables Ctrl+C for the whole new group — and Windows hands that "ignore Ctrl+C" state down to every descendant. Every ConPTY shell a pane spawned inherited it, and so did everything those shells ran: the pane wrote 0x03 and conhost turned it into a keypress, but the CTRL_C_EVENT never came, so `go run` and `npm install` carried on. Git Bash looked fine only because MSYS synthesises SIGINT from the byte itself and never waits for a console event. DETACHED_PROCESS already leaves the daemon without a console for a control event to arrive on, so the group flag bought nothing to begin with. Both daemon spawn paths and tty7-cli's headless server — which spawns panes too — now share one constant without it, and DaemonPane::spawn clears any inherited ignore before it opens the pty, so a tty7 launched from a shell that already had the bit set is covered as well. The regression test has to inherit the state rather than switch it on in place, since that is the shape the daemon was in: an intermediate process created exactly as the daemon used to be runs both arms, and a pane must be interruptible only after the clear. Its observable is the shell rather than the interrupted command — after the ^C, cmd gets its prompt back and acts on the `exit` typed behind it — so it reads no message and holds on a non-English Windows. Fixes #451 Fixes #314 Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
feb027da1f |
feat(scm): show an untracked file's content when its row is opened
Focusing an untracked file in the diff overlay used to fall through to the names-only "Untracked files (N)" card — git has no patch for a file it does not know, and `--no-index` needs a null device whose spelling is platform business. The overlay now reads the file's own bytes (lazily, only the focused file, 4 MiB cap) and synthesizes the card a parsed added-file patch would produce: every line an addition, new-side numbers, true counts past the single-file budget, git's own NUL-in-the-first-8000-bytes binary rule. A fresh snapshot clears the preview so an edit shows up on the same cadence a tracked file's does; a failed read says so instead of showing an empty file. Found in manual acceptance of the panel. |
||
|
|
58d7ef5838 |
fix(scm): close out the review's minor findings across the data and UI layers
The second pass over the branch review: every remaining finding verified against the code, the real ones fixed. Data layer: - A truncated log parse is never called complete: RecordSplitter drops an overlong record whole and reports the count (delivered cut short, a commit body cut mid-way reads as the real message), parse_log carries a truncated flag past MAX_LOG_BYTES, and load_page only says "end of history" when the parse read everything git returned. - Every scope pins symbolic revs to shas before walking, so a commit landing between two pages can no longer shift where page two starts under Head and Refs scopes; unresolvable names read as "no history" rather than as a load failure. --parents was doing nothing and is gone; edge sort is stable so a merge's Outs keep first-parent order. - The lane model's central invariant now names the join case — a merge whose second parent already has a lane reserved sends its Out onto that lane, one line below the cut, not two — with a golden test for the commonest merge topology of all, which no golden covered. - DiffSource revs get the same could-be-an-option guard log already had; C-quoted paths decode the full escape set (a tab decoded to a literal t broke the :(literal) re-probe); rename from/to lines override the ambiguous diff --git header; combined-diff line numbers follow the sides rather than the colour, so a " +" line no longer drifts every number below it. - A rename's old path stays out of the per-file decoration map, where it outranked a file re-created at that path; ignored records decorate as Ignored, not Modified; checkout <branch> gains the trailing -- that keeps a stale name from falling back to a worktree-clobbering path checkout; unstage before the first commit takes -f (worktree- safe with --cached); batches split by bytes as well as count for Windows' 32K command line; a deadline expiry reports Timeout, not "git could not be run"; error details keep both streams. - probe_status distinguishes "not a repository" from "could not ask": a dropped link keeps the cached status (stale beats blank) and rests 10s instead of erasing the panel, while a definitive not-a-repo also drops the cwd→root mappings so the panel stops drawing Loading for a repository that is gone. Probe and watch work are wrapped against panics that would wedge their in-flight bookkeeping forever, watch landings check the wipe counter, superseded probes relaunch through the debounce, and a refused network slot says so instead of eating the click. UI: - Reset --hard confirms with its own words (commits fall off the branch), not the discard dialog's; a merge commit whose prefilled message the user cleared is committable again; the disabled commit button distinguishes "nothing to commit" from "write a message". - Selection highlight matches on the diff source too, so a file staged and edited again no longer lights both of its rows for one overlay. - The graph materializes only the rows in the viewport window (5000 flex children per frame was most of a frame), row clicks carry the page Arc and an index instead of a deep Commit clone per row per frame, filter results are cached per (page, query), and a selected merge ring's hole matches the selection band under it. - A failed commit_files read says the list could not be read instead of "0 files changed"; the STAGED chip and the graph's relative times go through the i18n table; the keys-awaiting-a-caller list is pruned to the seven that still are; the orphaned PanelUntracked key is gone; the zh commit placeholder reads naturally. 2398 tests, 0 failures. Known flake: daemon::singleton's second-claim test, untouched by this branch, fails ~1 in 3 full parallel runs and passes alone. |
||
|
|
a764d92132 |
fix(scm): sequence compound verbs, cap graph paging, back off failed loads
Review findings on this branch, all in the seams between async operations: - Commit-and-push, commit-and-sync, sync and discard-all dispatched both halves into the worker pool at once, so a push could resolve the branch tip before the commit (or pull) it was waiting for and quietly send the old one. Compound verbs now carry a ScmFollowUp that the first half's landing closure starts on success only; a refused commit, a failed pull or a cancelled confirmation drops the follow-up with it. - Push sent `git push <remote> <branch>` with the branch taken from the upstream's name — a bare name means a *local* branch, so `feat` tracking `origin/main` pushed stale local `main`. The refspec is now `HEAD:<branch>`, and the branch is validated with the full branch check since a `:` would smuggle a second refspec in. - `scm.committing` was armed before the amend confirmation and never disarmed on failure, so a cancelled prompt (or a hook rejection) plus any later unrelated HEAD move cleared a message that was never committed. It is armed at dispatch and disarmed when the commit errors. - Discard-all fed staged-only paths to `checkout --`, where a staged deletion sank the whole batch as an unmatched pathspec. Only unstaged paths go in, one confirmation covers both halves, and the two gits no longer run concurrently. - One "load more" click at 5000 commits grew `requested` past what `load_page` clamps to, so the freshness check never passed again and every frame refetched the full page. Growth stops at the cap, the button hides there, and a failing `git log` is remembered per key instead of being retried from every render. - A repository switch now drops the previous repository's page before anything can draw it or grow from it — a stale row's context menu used to build ops for the new repo with the old repo's rev. - A watch that failed to open was retried at frame rate, one host round trip per render; it now rests for WATCH_RETRY between attempts. - Non-network writes on a remote host ran under the interactive 20-second deadline while the server ran the job to completion, so a slow pre-commit hook was reported failed and then landed anyway. Every write now goes through git_with_deadline, 120s for local verbs. |
||
|
|
88bf9a5da5 |
feat(daemon): upgrade in place, keep pane screens across a crash, and give panes their own history (#449)
* feat(daemon): keep a pane's screen across a death nobody chose A daemon that crashes, is `kill -9`'d, or goes down with the machine takes every pane's replay ring with it, and the window comes back to a row of blank shells. The processes cannot be saved that way — nothing written to a file brings a process back — but the picture can. The daemon now keeps a capped tail of each pane's ring under the config directory, and a client whose `Attach` found nothing can ask, on the `Spawn` that replaces it, for the dead pane's screen. The new pane opens showing it, under a rule that says the shell below is new. - periodic and dirty-only: a ring that has not moved is not rewritten, so an idle machine does no IO at all. Write-through would be an enormous amount of write amplification for a few seconds of freshness. - capped at 256 KiB per pane, far below the ring's 8 MiB: the value of scrollback decays with distance from the bottom, and every byte here is a byte of someone's terminal on disk. - off by default. The ring holds whatever the pane printed, including echoed tokens, `env` output and agent transcripts; in memory that dies with the daemon, and writing it down is the whole feature and the whole cost. Files are 0600, and turning the setting off deletes what was kept. - dropped by relevance, not by calendar: a pane the user closed, or one no workspace names any more, has its file removed on the next sweep. Restored bytes are replayed at the geometry they were written at, and are preceded by resets — leave the alternate screen, show the cursor, restore autowrap, clear SGR — because a snapshot is cut at the front and can begin in the middle of any of them. * feat(daemon): upgrade the daemon in place instead of killing every shell Picking up a new build meant stopping the daemon, and stopping the daemon means every pane dies: the pty master is a descriptor this process holds, so when the process goes the slave side raises SIGHUP and takes the shell, the agent and the half-finished command with it. That is why the update path leaves the old daemon serving and Settings has to offer the restart as a thing you schedule for a quiet moment. `execve` does not have that problem. It replaces the image and keeps the process: same pid, same children, same descriptors, same file locks. The daemon now rewrites itself that way on `ClientMsg::Handoff` — it writes what it knows about each pane into a blob, clears FD_CLOEXEC on the pty masters, the blob and the singleton lock, and execs the new binary, which picks the panes back up on the other side. - **the seat travels on the command line, not in the blob.** The lock is still held by this process, so the new image must adopt the descriptor rather than ask for the lock again — asking would be refused by its own lock and it would stand down in favour of itself. A daemon that loses its panes is a bad afternoon; a daemon that exits leaves the machine with nothing serving, so that one fact has to survive an unreadable blob. - **the blob is unlinked before it is written.** It holds every pane's ring, which is the output `scrollback` makes people opt into storing; a handoff must not be a back door for writing it to disk. - **the exec is the last step.** Everything is staged first, so any failure before it costs a log line and the daemon carries on serving — which is what lets callers treat a failed handoff as "fall back to a restart" without having lost anything on the way. Native SSH panes cannot cross — their session is cipher state in memory, not a descriptor — so they are hung up first and the far end sees a clean close. Windows has neither execve nor a transferable ConPTY handle, so it keeps the stop/start path; the dialogs there still promise what they always did, and the new copy is shown only where it is true. Also retries flock on EINTR: a signal landing mid-call said nothing about the lock, but was reported as "could not be evaluated", which starts a second daemon beside the first — the split machine singleton exists to prevent. The end-to-end test sets a variable in the shell, hands over, and reads it back. Nothing but the original process can answer that, and the daemon's instance id changing while its pid does not is what says an exec really happened. * feat(shell): give each pane its own history when asked Two panes running zsh with `share_history` are appending to one file and reading each other's lines back, which is either the feature or the problem depending on what the panes are for. Someone with a pane per task wants Up to walk that task's commands, not an interleaving of four. Each pane can now have its own history file instead. It is seeded from the shell's real history, so a new pane is not blank, and what the pane added is appended back when it closes, so nothing typed is lost — a per-pane history that evaporated would be a way of losing commands, not of organising them. The seeding is done by the shell, not the daemon, and that is the only reason it works: `HISTFILE` belongs to the user's rc file and can point anywhere, long after the pane's environment was decided. tty7's snippet is appended to the rc it wraps, so it runs after that decision and is the one place the real path is known — it copies the tail, records how much it copied, and repoints. Both shells load history after their startup files, so the switch lands before the first line is read. The daemon's half is a filename, a rename when a restored pane inherits its predecessor's file, a merge on close, and a sweep for the panes a killed daemon never got to retire. Off by default: shared history is what a terminal has always done, and someone who did not ask for the change would experience it as their history mysteriously forgetting the other window. bash and zsh only — fish and PowerShell do not keep a HISTFILE, and a shell launched with the user's own arguments gets no snippet to repoint anything in. * fix(daemon): store pane screens on the shutdown a restart actually uses The periodic writer covers a death nobody prepares for and the SIGTERM path covers a signal, but the restart the app itself performs goes through ClientMsg::Shutdown — which killed every pty without taking a copy first. That is the one shutdown where the panes are expected back. * fix(daemon): leave nothing dangerous behind when a handoff fails or lands Review findings on the in-place upgrade and per-pane history: - A failed exec now puts back everything it had staged: FD_CLOEXEC on the seat and every pty master (a child inheriting the seat keeps the flock held past the daemon's death, so no future daemon could seat itself), and the SIGPIPE disposition plus this thread's signal mask, both of which Command::exec resets on its way to the attempt — without this, the still-serving daemon dies on the first client that hangs up mid-write. - The adopting image restores close-on-exec on the seat and on every adopted master, so children it spawns later cannot hold a pty open past its pane, or the seat past the daemon. - The target binary is checked before the handoff gives anything up: native-SSH panes are hung up on the promise that this process is about to be replaced, and an exec that was never going to work must not collect on it. - The integration snippets raise HISTSIZE/HISTFILESIZE (bash) and SAVEHIST/HISTSIZE (zsh) for the pane's private history file. At their defaults the exit rewrite truncates the file below its own seed mark, which the merge-back rightly reads as "replaced under us" — silently losing the pane's commands for anyone with more history than the caps. - The restart dialog's promise now binds the action: where the copy said "nothing is interrupted", a failed handoff is reported instead of silently traded for the restart that kills every pane. - The scrollback writer checks the ring's mark before cloning it, so an idle pane no longer costs a full ring copy under the state lock every tick. Each behavioural fix carries a test that fails without it; the history truncation one was verified to fail with the snippet change removed. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
8dbc7efa1a |
fix(cli): stamp panes with the workspace that holds them, not the client's name (#425)
* fix(cli): stamp panes with the workspace that holds them, not the client's name A pane's owner names the workspace allowed to attach to it. The CLI wrote a literal "tty7-cli" there for every pane it made, so a window opening on a CLI-built workspace found none of them attachable: it spawned a fresh shell for each tab, orphaned the live ones, and — because the tree still carried each pane's agent session — greeted the user with a failing `claude --resume <id>` in every one of them. Both spawn paths now pass the workspace id, and restore treats an owner that parses as no workspace as no claim at all, so panes already stamped by an older CLI attach instead of stranding. * fix(cli): let the OWNER column speak only when it disagrees with WS Now that a pane's owner is the id of the workspace holding it, printing both spells the same id twice on every row of `pane ls --all` — and buries the rows that matter. The column now shows a dash when the two agree, so what is left is exactly what is worth reading: a pane its holder may not attach to, and an orphan still naming where it belongs. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
62b922f2c2 |
Merge origin/main into integration/polish
main dropped the client-side command-mark store (#404) while this branch had just started reading it: the close confirmation names the command it is about to end, and the mark was the only place that text existed on the client. Keep both. The OSC 133 tokenizer main left in place already sees every mark, so the command line now rides alongside `zle_reading` and `shell_vi_mode` as one shared string — set on `C`, cleared on `B` and on a `C` that carries no line — instead of a store with a list, a lock and a cap. `busy()` reads that. The rest: - settings.rs takes main's opaque overlay surface and background layers, keeping this branch's no-match note and scrolled body. The inner `.bg()` goes, per main's reason: the root already paints it, and a second fill hides the theme image. - i18n keeps this branch's `every_key_is_translated_in_every_locale`, which walks `L10nKey::ALL` in all three locales, over main's hand-listed zh coverage test it replaced. It immediately caught three of main's new backdrop keys reading English in ja — Mica, Mica Alt and Acrylic, which is what Japanese Windows calls them, so they join the allowlist with that reason. - app.rs keeps both sides' tests and drops both sides' now-dead imports: `window_background` (main deleted the function) and `humanize_action` (this branch's keybinding note uses `keymap::action_entry` instead). Verified: `sleep 300` then ⌘W asks about "sleep 300"; ⌘W after it ends closes without asking. |
||
|
|
5c65e2b08f |
test(git): pin the git config the fixtures assume, and one path spelling
Two Windows-only failures, both of them the tests asserting on the runner's git rather than on the code. `core.autocrlf` is `true` by default in Git for Windows, so a file written as `one\n`, committed, and restored by `checkout --` comes back as `one\r\n`. The `-c` list the helpers already pass would not have fixed it: the checkout in that round trip is `run_op`, production code running its own git with no overrides. So the pins go into `<repo>/.git/config` right after `init`, where repository config outranks the system config that carries the default. The other is the same path-spelling mismatch fixed earlier in `git_data.rs`: `git rev-parse` answers with forward slashes and no extended-length prefix even on Windows, while `fs::canonicalize` returns `\\?\C:\…`. Both name the same directory and the Win32 APIs take either, so the production path is right and only the comparison needs one spelling. Fixed as classes rather than as instances. `core/git/mod.rs` gains a `test_support` module holding the `-c` list, the repo-config pin and the path normaliser, and `status.rs`, `log.rs`, `ops.rs` and `diff.rs` all read from it — `ops.rs` had no config pins at all and `diff.rs` was missing gpgsign. `git_data.rs` keeps its own copy because a `#[cfg(test)]` item does not exist in the `tty7-core` the binary crate links against; a comment says so and points at the other copy. The normaliser has its own test over literal `\\?\C:\…`, `C:\…`, git's `C:/…` and a unix path, and the line-ending fix was reproduced locally by pointing `GIT_CONFIG_SYSTEM` at a config with `core.autocrlf = true`: that panics exactly as CI did with the pins reverted, and passes with them. One latent hazard hardened while here — `status.rs`'s scratch directory had no pid in its name, unlike its sibling, so a leftover that resisted removal would have been silently reused as a fixture. |
||
|
|
0106430ecd |
merge: main into the Source Control branch
The one conflict is an import list in `diff_overlay.rs`: this branch added `SharedString` for the unified view's row labels, main added `Background` and `Hsla` for the window backdrop work. Both sides are still used, so the resolution is the union. Worth recording why this merge happened when it did. `main` moving is not normally urgent — branch protection dropped its strict check, so a branch behind main still merges — but a *conflicting* branch is different: GitHub cannot compute `refs/pull/N/merge`, and every workflow that triggers on `pull_request` silently stops running. Three pushes in a row registered no CI at all on #424 while other PRs kept going green, which reads as a GitHub incident and is really just an unresolved conflict. |
||
|
|
e76d545655 |
test(git): build the log tests a repository instead of reading this one
Both tests pointed `load_page` and `local_branches` at `CARGO_MANIFEST_DIR` — the checkout the tests happen to be running inside — and then asserted things only a developer's clone is true of. `actions/checkout` clones shallow, so "five commits" found one, and a pull-request build checks out the merge ref detached, so "this checkout is on a branch" found no branch. Both assertions were about the environment; the code under test was answering correctly in each case. They now build their own history, following `status.rs`'s `scratch()`: a temporary directory removed on drop, `user.name` and `user.email` pinned because a runner has neither, and `symbolic-ref` rather than `init -b` so the branch name does not depend on the git version. One test forks a side branch and merges it back over seven commits, so lane layout is checked against a shape with more than one lane in it; the other makes a single commit that modifies, adds, deletes and renames, so the `--numstat` / `--name-status` join is exercised on four paths rather than on whatever HEAD happened to touch. Owning the history let the assertions get stricter rather than looser: the commit count, the branch list and the per-file `(added, removed)` pairs are now exact, the paging check is honest about how much history exists, and the merge row is asserted to carry two parents and two outgoing edges. The three other tests reading `CARGO_MANIFEST_DIR` were audited and left alone — one bails when `git log` fails, one falls back to a short sha on a detached HEAD, and the third is `#[ignore]`. |
||
|
|
61efe27f2d |
feat(windows): add native backdrop material presets (Mica / Acrylic /… (#412)
* feat(windows): add native backdrop material presets (Mica / Acrylic / Blur) Adds a Background material dropdown (Auto / Blur / Mica / Mica Alt / Acrylic / Off) that maps onto the native Windows backdrop APIs already provided by the gpui fork — Mica and Mica Alt via DwmSetWindowAttribute(DWMWA_SYSTEMBACKDROP_TYPE), Acrylic via the new DWMSBT_TRANSIENTWINDOW material, and Blur via the classic ACCENT_ENABLE_ACRYLICBLURBEHIND path — with no fork changes required. * config: introduce WindowBackdrop in tty7-core with lenient kebab-case deserialization, defaulting to Auto for existing configs * theme: resolve the backdrop through a build-number fallback chain (Mica/Mica Alt need Windows 11 22H2, Acrylic needs 22H2 natively and 1809 via classic acrylic, Blur needs 1809; older builds fall back to plain translucency) and default the background alpha to SYSTEM_MATERIAL_OPACITY (0.82) while a material is active * settings: replace the blur toggle with a localized backdrop dropdown that only lists the presets the current Windows build actually supports, and keep the settings panel fully opaque so workspace translucency never shows through it * theme: make the file sidebar and right detail panel follow the window opacity so the backdrop material shows through the whole workspace, keeping row-level accents opaque for readability * i18n: add backdrop keys for en, zh-CN and ja-JP, covered by the translation completeness test * feat(theme): let the sidebar and right panel follow the window opacity * update GPUI * fix(windows): gate the sidebar translucency to translucent windows and sync the opacity slider fix(windows): gate the sidebar translucency compensation to active materials * fix(windows): derive the material opacity default from the resolved appearance * fix(theme): keep WindowBackdrop semantics consistent on non-Windows f * fix(theme): stop Windows-only materials from pinning the blur on other platforms * docs(changelog): document the Windows backdrop material settings * refactor(theme): share the default window-opacity derivation * fix(ui): keep gradient presets behind the settings panel and scope its fallbacks * fix(ui): keep the settings theme picker legible and the backdrop label honest f * fix(theme): let every backdrop variant defer to the local blur toggle on non-Windows * fix(settings): restore the backdrop dropdown selection on locale refresh * fix(ui): keep the opened-file editor surface opaque under window translucency * fix(settings): rebuild backdrop options after selection * fix(settings): ignore synced windows backdrop overrides on other platforms * fix(settings): preserve synced windows backdrop on non-windows reset * fix(diff): keep the full-window overlay background opaque * fix(windows): keep Auto opaque and stop the backdrop from misreporting itself Ten findings from a review of the backdrop-material work, all in the Windows-only paths. The root one: `material_active` treated `Auto` as a material whenever the legacy blur toggle happened to be on. `Auto` is the default in every config written before this setting existed, and plenty of them carry `window_blur: true` from the switch that no longer renders on Windows, so an untouched install would drop from opaque to 0.82 alpha - with its file sidebar and right panel at 0.15 - on first launch after the update, with no visible control to undo it. Only an explicit pick in the dropdown now buys the translucent defaults. The switch comes back on Windows while the backdrop is `Auto`, since that is exactly when the legacy flag still decides something. The rest: - Mica and Mica Alt fell back to `Blurred` with no lower bound, asking for a blur that does not exist below 1809 - and build 0, which is what a failed `RtlGetVersion` reports. They now degrade to plain translucency like `Blur` and `Acrylic` already did. - Acrylic is no longer offered below 22H2, where it resolves to the very same classic WCA blur as `Blur`. A test now asserts that no two offered presets render identically on any build. - `reload_from_config` re-applied the theme and the opacity slider but not the backdrop dropdown, so an external config change switched the window's material while the control kept naming the old one. - The settings, opened-file and diff overlays were made opaque so the OS backdrop cannot show through their text; that also hid the theme background image, which used to show through them. They paint their own copy of it now, and the fill they share moved into `theme::overlay_background`. - The SFTP transfers tray painted `workspace_surface_color` inside the right panel, which already paints it, stacking the same translucent surface twice into a darker band with a hard seam. - `apply_theme` re-issued `set_background_appearance` on every `Config` mutation in every window. With a DWM material that now costs a `SetWindowPos(SWP_FRAMECHANGED)` frame recalc, so dragging the opacity slider recalculated the frame once per mouse sample; it is skipped when the appearance is unchanged. * fix(ui): dim the overlay background image, and stop telling Windows it is macOS Two defects found while driving the previous commit's changes in the app. The overlays repaint the theme background image over their own opaque fill, so it survives them being made opaque - but nothing dimmed it. Before those overlays were opaque the image reached the eye through their translucent fill; painting it at full strength put the settings text straight on top of the wallpaper and made the panel unreadable at any image opacity above about half. They now paint the image and then the workspace's own fill over it, which is exactly the strength the image had through these overlays before, and which needs no new constant to say so. Shared as `app::overlay_surface_layers`, empty when the theme has no image so a themeless window paints no second pass of anything. The Windows-only blur row reused `SettingsBlurDesc`, whose text ends in "(macOS)". It gets its own key in all three locales, describing the job the flag actually still has on Windows: feeding the `Auto` material. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
388e65e96a |
fix(tests): the shell check proves a good shell on every platform
The case meant to show that a real shell reports no problem named `/bin/sh`, a path that does not exist on Windows — so the one assertion guarding the happy path asserted the opposite there, and the Windows job failed on it the moment the suite got far enough to run. Point it at the running test binary instead: a file that exists and carries the execute bit wherever the tests run. |
||
|
|
09a653d10b |
fix(workspace): make the CLI and the GUI agree on what exists (#423)
Five places where a workspace, a tab or an attachment was real on one side of the socket and invisible on the other. They share a root: the GUI kept its own list of which workspaces exist (WindowViews on disk) and consulted the machine tree only for the ones already in that list, so anything created by another client was unreachable by construction. - The switcher lists workspaces the machine holds but this client has never opened, and opening one keeps its id instead of claiming a fresh one. - for_workspace_at hydrates whenever the machine holds tabs, so opening a workspace no longer saves an empty session over them. - finish_hydration writes a full window back over an empty tree, which is what puts a ws rm'd workspace back under the same id. - A deletion nothing has open is forgotten here too, instead of haunting the switcher until a restart. - Workspace::attachment travels over the wire (minus the token that proves the hold, which stays on the connection that owns it) and is stripped in persist, so tty7 ls can name the host holding a workspace. - tab ls / ws tree fall back through name -> agent -> cwd leaf -> process name, and tab ls grew a read-only GROUP column. - tty7 new --open raises a window on the workspace it just made. |
||
|
|
686bd0bcea |
feat(ui): the interface has a font size of its own
The detail panel carried a private run of pixel sizes — 12 for body, 11.5/11 under it — which put its primary text at the size the rest of the window uses for secondary text, so it read a step smaller than the sidebar beside it. Its mono values sat at the same px as their sans labels, where a larger x-height makes them look a size bigger, so a row read as two sizes instead of one line. `forwards.rs` and `sftp.rs`, both drawn inside that panel, had copied the same numbers. Put the panel back on the rem ladder the rest of the chrome already uses, with mono a notch under the sans it pairs with, and make the rem itself settable: `ui_font_size` defaults to gpui's own 16, so an existing config renders unchanged, and every window's root sets it, which reaches the whole interface at once. The terminal grid is absolute px from `font_size` and does not move — a display that is not Retina can now have bigger chrome without touching the text in the panes. |
||
|
|
8981cc7429 |
fix(wsl): start a pane when fish is the distro's default shell, with its integration (#422)
A WSL pane whose distro defaults to fish never started: `wsl.exe --` hands the command line to that default shell, which parses the POSIX bootstrap before `sh` can receive it. Every wsl.exe invocation that carries an argv — the pane bootstrap, `RemoteLink::wsl`, `RemoteLink::wsl_shell`, and the installer's `sh -s` / `tee` calls — now goes out under `--exec`, which runs the program itself. `--` keeps its other meaning, "just start the default shell", for the no-argv case. Those fish panes then came up with no shell integration at all, because the bootstrap only ever had a bash arm: no OSC 133, no OSC 7, so no prompt marks, no exit status, no cwd, and `tty7 wait` and busy/idle status dead in the pane. The `case "$SHELL"` now has a fish arm carrying FISH_INTEGRATION inline, the way `remote::bootstrap_command` already does over SSH — POSIX-quoted, since `sh` parses this script rather than the user's own shell. The PTY tests' completion marker is anchored on its terminator: `133;D;1` is a prefix of `133;D;127` and `133;D;130`, so they would have gone green on a bootstrap that never ran the command they type. |
||
|
|
fee48a4c99 |
Merge origin/main into integration/polish
main shipped v26.8.2 and 15 fixes while this branch was open. Resolved: - zh: main's #417 decided the background process is called "server" in Chinese, and that decision is newer than this branch's "服务器" — took it, kept this branch's typographic quotes around {machine}, and whitelisted SettingsServer in the new every-key-is-translated test, since the zh heading is now that English word on its own. - presets.rs: this branch factored main's inline `clear` closure into Theme::clear_ink / ansi_seed; same arithmetic, so kept the methods. #400's border and caret floors and #413's legible-palette flag both survive untouched. - app.rs: took main's Option-typed `alive` argument, kept this branch's note on why a dropped tab is worth a sentence. - README / docs: agent count is now exactly 18 with Oh My Pi, so the precise number replaces both "17" and "~18"; the zh feature doc keeps its translated menu names and gains Oh My Pi in the fork list. Six keys main added are gone because the surfaces that used them were rewritten here: the home screen's relative time now runs to years, hook failures name install vs remove, Full Screen left the View menu on purpose (AppKit adds its own), the SFTP filter says "search files", and the settings index titles its CLI row by its own label. |
||
|
|
5497252aec |
feat(scm): read a whole commit in the panel's second-level view
The graph gives up text for shape: a 260px row has room for about 26 characters beside its lanes, and this repository's commit subjects run to a median of 64. This is where the rest comes back — the full subject, the body, every ref, the parents, and the files the commit touched. The file list is two commands, not one. Git accepts `--numstat` and `--name-status` together and then quietly drops the numstat half (measured on 2.50.1), so they run separately and join on the path. It is `log -1 --first-parent` rather than `diff-tree -m --first-parent`, which does not narrow a merge: on the same git it emits one diff per parent and concatenates them, so a two-parent merge came back with every path listed twice. `log` is also how `DiffSource::Commit` walks the patch, which is what makes this list and the overlay's cards agree file for file. `DiffSource::Commit` now carries an optional label, so the overlay's header can say what the commit was about instead of eight hex digits. The label deliberately takes no part in the source's identity: `PartialEq`, `Hash` and the overlay's string probe key are all derived from one `tag()` function, because the same commit opened with a subject in hand and without one has to stay one patch, one in-flight probe and one overlay. The key used to be built from `Debug`, which would have split the probe cache the moment a label arrived. Also lifts `local_branches` out of the panel's inline `for-each-ref`, and keeps the `%(upstream)` that `parse_refs` had been asking for and throwing away. |
||
|
|
d14eb0c509 |
fix(agents): say what the hook install did in the user's language
`install_hooks`/`uninstall_hooks` returned the sentence to show, built in
`tty7-core` — which cannot reach `src/ui/i18n`, where every user-visible
string lives. So the note under Settings → Agents reported "Installed",
"Removed" and "Nothing installed; nothing to remove" in English no
matter the locale, next to a panel translated around it.
Return a `HookOutcome` instead and word it at the call site, where the
other five agent-hook strings already are. The match is exhaustive, so a
new outcome cannot reach the UI without wording in all three locales.
The failure half said only "Failed: {error}" while all sixteen sibling
errors name their action; it now says which of install or removal did
not happen.
Not driven in a dev instance: exercising this panel writes to the real
agent config under $HOME, including the Claude hooks this session runs
on. Covered by the core tests and by exhaustiveness instead.
|
||
|
|
8b7215f0d8 |
fix(remote): let a WSL machine restart its server, and call it "server" in zh (#417)
* fix(remote): let a WSL machine restart its server, and call it "server" in zh The machine menu offered "restart server" only when `target.is_ssh()`, so a WSL distro's row had just "new workspace" and "disconnect" — and the router refused the action for anything but SSH, even though `restart_wsl_daemon` had been sitting in `install/wsl.rs` unused since it was written. A distro's server is installed and launched from this computer exactly like an SSH one; only the transport differs. - `RemoteTarget::hosts_our_server()` replaces `is_ssh()` as the gate in the sidebar menu and in `restart_window_daemon`. A `--stdio` workspace still has nothing to restart: it is whatever program the user named. - The router grows WSL arms for both actions — `RestartServer` relaunches the distro's daemon, `ReplaceServer` (new `replace_wsl_server`) publishes the bundled binary first. The mismatch dialog's "update server" used to be a dead end on WSL for the same reason. - `AppRestartServerNotSsh` is now `AppRestartServerNoServer`: the message no longer talks about SSH, since the only machine it can fire for is a local `--stdio` one. The Chinese table called tty7's server 服务器 / 服务器端 / 后台服务; it now says `server`, the way it already keeps `shell`, `pane` and `agent` in English. The macOS Services menu keeps its 服务. * fix(i18n): keep 服务器 where it means the SSH host, and test the new gate The sweep that renamed tty7's own server to `server` in the Chinese table also caught two SSH settings where 服务器 was never tty7's server: the host-key description talks about the key of each machine being connected to, and the banner switch suppresses the login banner sshd prints. Written as `server` both now read as if tty7 ran something there, which is exactly the confusion the rest of the sweep removes. Put the Chinese word back in those two. `hosts_our_server()` is the gate the whole change hangs on and nothing exercised it, while the test next door still asserted under the name "only SSH machines have a server to restart" — the rule this branch retires. Name that test after what it actually checks, the transport, and add one for the new predicate. It matches on the variants rather than negating a single one, so a transport added later has to say for itself whether we can restart what is on the far end. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
ac5983056c | style(git): collapse the ahead/behind fallback into one condition | ||
|
|
38899186dd | merge: the Source Control panel's scaffold and wiring | ||
|
|
80e8e09548 |
feat(scm): turn the Changes tab into Source Control and wire the surface up
Renames `RightPanelTab::Changes` to `Scm` in place. `#[serde(rename = "changes")]` works in both directions, so what lands on disk is unchanged and a build from before this commit reads the config back without kicking anyone off the panel they left open — a fourth variant could not do that, and 260px has no room for a fourth tab tile anyway. The action name `ShowRightPanelChanges` stays put because `Config::keybindings` is keyed by it, and every existing custom binding would otherwise be orphaned. The panel body moves to `src/ui/scm/panel.rs` byte for byte; it still renders the flat `git diff HEAD` list. Alongside it the module gets the pieces the rest of the feature is built from: the shared status glyph and colour tables (one definition instead of three that drift), the path and timestamp helpers a 260px column needs, and the panel's state types. Also wires the whole surface: fourteen actions, their key bindings, ten palette commands in a `Git` group of their own, and the translations. `ScmCommit` takes `secondary-enter`, which macOS already gives `ToggleFullscreen`; the two coexist because the commit binding is scoped to the commit box, and `every_default_chord_is_claimed_by_exactly_one_action` now checks uniqueness per context instead of globally, which is the actual invariant gpui enforces. `Config` gains `diff_view` and `scm_graph_expanded`. Both default to what happens today. Two new icons. `git-sync.svg` is deliberately not `refresh.svg`: the panel header already carries a refresh tile, and the same glyph meaning two different things one row apart reads as a bug. Adds `every_action_has_a_binding_arm`, which walks every action in `default_bindings` rather than only the ones shipping a default keystroke. The gap it closes is an action listed in Settings with no `make_binding` arm behind it: the user assigns a key and the key silently does nothing. |
||
|
|
c675796ef7 | merge: three diff sources, and the rows both views are laid out from | ||
|
|
58e8ba16d1 |
feat(git): ask for three kinds of diff, and lift the row model out of the renderer
`probe` could only ever run `git diff HEAD`. A `DiffRequest` now names the source (worktree, staged, HEAD, one commit, a range), the pathspecs, the context width and the budget, and `DiffSource::args` is the single place any diff argv is built — which is also the single place to test it. Three things were verified against git 2.50.1 rather than assumed: - `-c core.quotePath=false` is a real bug fix, not tidiness. With quoting on, `diff --git "a/\344\270\255\346\226\207\345\220\215.txt" …`; off, `diff --git a/中文名.txt b/中文名.txt`. `parse_quoted_pair` does not decode octal, so every non-ASCII path in the overlay was simply wrong. `ls-files` quotes the same way, so the untracked listing gets the flag too. - `Commit` runs `log -p -1 --format= --first-parent`, not `diff-tree`. `diff-tree` does not honour `--first-parent` as a narrowing of a merge: over a merge of two branches that each added a file, `diff-tree -p -m --first-parent` emits both files (one patch per parent, concatenated) and dropping `-m` emits nothing at all. `log -p -1 --first-parent` gives the one first-parent patch for a merge, an ordinary commit and the initial commit alike, so there is no special case and no `--root`. - A type change is not one patch. git emits a deletion and a creation for the same path, back to back, with nothing in either header saying why; the pair is folded back into a single `TypeChanged` entry. `old mode`/`new mode` only appears for permission changes, which stay `Modified`. `FileStatus` also gains `Copied` and `Unmerged`. A conflicted path arrives as a combined diff (`diff --cc`, `@@@`, one marker column per parent), so the body parser reads its marker width from the hunk header instead of assuming one. The budget is a parameter now (`DiffBudget::PANEL`, `::SINGLE_FILE`) rather than three module constants read inside the parser. Defaults are unchanged. On the UI side, the overlay's reuse test was `(cwd, host)` — clicking a staged file while a worktree overlay was open took the "just move the focus" branch and went on showing the unstaged patch under the staged file's name. The source is part of an overlay's identity now, in that filter, in the panel-seed shortcut and in the in-flight de-duplication key. `maybe_refresh_diff_overlay` only compares HEAD snapshots against the cached `--numstat HEAD` counts, which are the only counts they are comparable to. `split_hunk` and tab expansion move to `ui::diff_rows` alongside a new `unified_rows`, so the two renderings of a hunk are built and tested in one place, without a window. The overlay's file list finally hangs on tty7's own scrollbar instead of a bare `overflow_y_scroll`. |
||
|
|
9285d6d94f | merge: commit log parsing and graph lane layout | ||
|
|
b27d0d322b |
feat(git): lay out the commit history in lanes
Adds the three pieces the graph section needs from the data layer: the lane assigner, the `log --pretty` parser it is fed from, and the paged loader that puts the two together. `LaneAlloc` is append-only. It keeps only what has to survive a page boundary — which oid each lane is holding a place for, and the reverse index — so a later page extends the graph instead of re-flowing the rows already on screen. That is only possible because `GraphRow` is row-local: a row says nothing about the rows below it, which is also why a long-lived branch is drawn from its first row rather than staying invisible until the page holding its parent loads. Colour is the lane number, fixed when the lane is created. A per-branch counter wraps at the palette size and puts branch 0 and branch N in the same colour, which in a three-column panel is very likely two adjacent lines; keying on the lane makes neighbours distinct by construction. The "one branch, one colour" half falls out of the first parent inheriting its child's lane in place. Lanes are recycled but never compacted — compacting would move a lane out from under a row already drawn. The log is read with RS between records and US between fields rather than `-z`, whose record separator is NUL and so collides with the field separator; fields are taken with `splitn` so the body absorbs any US of its own. Paging is a larger `-n`, never `--skip`, and `HeadAndUpstream` resolves to shas first so a push between two pages cannot shift the window out from under page one. The Cargo.lock line is the missing half of tty7-core picking up smallvec, which main already carries. |
||
|
|
6ab4799bc1 |
fix(git): restore the classify test helper dropped with network_env
Removing network_env took its neighbouring `kind_of` shorthand with it. The shared target directory hid this: a stale test binary from another worktree reported green twice before an isolated CARGO_TARGET_DIR showed the ten real errors. |
||
|
|
b8757372b3 |
feat(git): give network operations the long deadline
run_op now routes fetch/pull/push through Host::git_with_deadline. The no-prompt environment turned out not to belong here at all — LocalHost puts it on every call, on both sides of the wire, so ops.rs's own copy of the same four variables was a second definition waiting to drift. Removed it; the conformance case git_terminal_prompt_is_disabled guards the behaviour across local and remote, which the unit test on the constant could not. Also corrects git_output_with_env's doc comment, which claimed read paths must not inherit the no-prompt environment. They do, deliberately: a read path never prompts, and a request arriving over the wire carries no bit saying which kind it is. |
||
|
|
5171880056 | merge: a long deadline for network git, with no protocol change | ||
|
|
94f9f2ba3b | merge: porcelain v2 working-tree status | ||
|
|
df6f028685 |
feat(git): parse porcelain v2 into a working tree status
Adds the parser and the probe behind the source control panel: one `git status --porcelain=v2 --branch --show-stash -uall -z` per refresh, read through `Host::git` so the NUL stream survives a remote workspace byte for byte (`git_lines` would reassemble it into one giant line). - `parse_porcelain_v2` is pure and infallible: headers, `1`, `2`, `u`, `?` and `!` records, submodule sub-state, rename scores. A `2` record spends two NUL tokens, so the parser parks the half-built entry and takes the *next* record as the old path — a regression test asserts the row behind a rename is not swallowed. - `probe_status` costs three round trips: `rev-parse`, `status`, and one `read_dir` of the git dir for the sequencer operation. The operation precedence mirrors git's own `wt_status_print_state`; only a parked rebase pays for a second `read_dir` to tell `am` from rebase and `-i` from plain. - `StatusIndex::build` folds a status into the file tree's lookup, and drops the per-file map past `MAX_DECORATED_FILES`. Two behaviours were measured on git 2.50.1 and shaped the code: - `status.aheadBehind=false` does *not* suppress `# branch.ab` for porcelain v2 (the config is documented for non-porcelain formats), and `--no-ahead-behind` prints `+? -?` rather than dropping the line. So `+? -?` parses as *unknown*, never as in sync, and the `rev-list --left-right --count` fallback fires only when the line is missing — unconditionally re-asking would cost a remote RPC on every refresh for a number we almost always already have. - `-c core.quotePath=false` is a no-op under `-z`, which disables C-quoting on its own. It is kept for consistency with the other git invocations, not because it fixes anything here. `repo_home` becomes `pub(crate)` so the probe can reuse the one `rev-parse` that already resolves root, git dir and common dir. |
||
|
|
5b9c41555c |
feat(host): let long git operations finish over a control link
`push`, `pull` and `fetch` run for as long as the network takes. A remote host sent them as a plain `Git` request, whose client-side deadline is the 20 seconds an interactive query gets, so anything slower came back as a timeout while the far side was still working. Add `Host::git_with_deadline`, a provided method that names the ceiling at the call site. `RemoteHost` sends the same `Git` request it always did and only widens its own patience, so the bytes on the wire are unchanged and a server that predates this serves it as-is: no new verb, no version bump. `LocalHost` does not override it — `Command::output()` has no timeout of its own, so forwarding to `git` already means "wait until git is done". Waiting longer is only useful if the thing we wait for cannot stop to ask a question. Give every git `LocalHost` spawns an environment that forbids prompting: nulled stdin was not enough, because without `GIT_TERMINAL_PROMPT` git opens `/dev/tty` itself. The read path carries the same environment rather than a separate one — `status` and `diff` have nothing to prompt about, and the wire has no bit that says "this request talks to a network", so the remote server has to reach the same rule from the same args to be protected at all. Guard the byte fidelity the SCM panel now leans on: `-z` output keeps its NULs through `Host::git` (`git_lines` would rejoin the records with newlines), and a `:(literal)` pathspec reaches git unrewritten. |
||
|
|
bb9208cf3e |
feat(git): build and run the source control write operations
Adds the three pieces ops.rs was missing: GitOp::commands, the pure argv table every write goes through; run_op, which executes those batches and turns a failure into something the UI can act on; and classify, which reads git's own words to decide what the failure was. Pathspecs always carry :(literal) and always sit after a --, so a file named a[b].txt, HEAD or -f names itself. Long lists split at 200 paths. Non-UTF-8 paths are rejected up front rather than sent lossily. The old command spellings throughout, never git restore, so one argv works on a CentOS 7 dev box and here alike; the only state-dependent branch is an unborn HEAD, and HeadState is a parameter so the table stays pure. |
||
|
|
c3233bb39b |
refactor(git): split core::git into a module tree and move the diff model into it
The source control work needs four kinds of git knowledge — how to run a
process, what the working tree looks like, what a patch looks like, and what
the history looks like — and they were about to pile into one 386-line file.
core/git.rs becomes core/git/{mod,status,diff,log,ops}.rs. Every existing
import path is unchanged: mod.rs still holds the process layer and re-exports
nothing it did not already export.
src/terminal/git_diff.rs moves wholesale into core::git::diff, leaving a
pub-use shim so diff_overlay.rs and right_panel.rs compile untouched. It never
had a gpui dependency, and the headless server should parse a patch with the
same code the GUI does.
Also lands the shared pieces the panel is built on, all inert for now:
- RecordSplitter, LineSplitter's sibling for the two git formats that are not
newline delimited (porcelain v2 -z, and log with an ASCII record separator).
It hands out &[u8] because a path in a -z status need not be UTF-8.
- git_output_with_env, so network operations can be told they have no terminal
to prompt at without the read paths inheriting that.
- The status/log/ops type contracts: the XY pair, unmerged stages, HeadState,
RepoPath (which refuses to produce a pathspec it cannot represent), the
row-local graph edge model, and GitOp with its destructive() policy datum.
No behaviour changes. 867 core and 1036 app tests still pass.
|
||
|
|
51c35aac1f |
fix(terminal): resize ConPTY panes with conhost's semantics, in stream order (#415)
ConPTY emits no repaint after a resize; conhost silently re-anchors its layout and keeps painting with absolute cursor addresses computed against it. Measured live: growing the window keeps rows and cursor pinned and opens blank rows below, and shrinking scrolls the last written row to the new bottom. The grid resized the alacritty way instead, so after a maximize every absolute-CUP paint landed mid-screen inside the old output. The vendored alacritty_terminal now has a conpty_resize mode mirroring conhost's model (fork rev 1276f12); every Windows pane opts in. Separately, a resize during a burst of output reflowed ahead of the backlog (up to the gate's 16 MiB of old-width bytes). The daemon now echoes a Size frame to the controller at the exact stream position where the PTY geometry changes, and a client that probes the new resize-echo feature defers its reflow to that marker. Remote routes and older daemons keep the reflow-at-request-time path. |
||
|
|
d9a6553651 |
fix(theme): lift illegible bright ANSI slots to the text floor (#413)
* fix(theme): keep the bright ANSI half of the palette legible on the theme background fmt * feat(theme): make the bright-color legibility rescue toggleable fmt |
||
|
|
5e6bc1246f |
fix(workspace): stop switching workspaces from destroying live sessions (#410)
* fix(workspace): stop switching workspaces from destroying live sessions Switching workspaces rebuilt every pane it was asked to restore, and a window that rebuilt nothing then deleted the workspace outright — tree and store both. Three separate guesses, each one authorizing an irreversible act: - `session_from_tree` erased a pane's id when the tree said `live: false`. That flag is a cached observation from another process, reloaded as false on every server start, so a quiet pane read as dead while its shell was running. The restore had nothing to attach to and spawned a fresh shell over it. - Two servers could start against one config dir. `run_with` decided another server was dead by failing to connect once, then unlinked its socket and bound its own. The loser kept `control.sock` with an empty pane registry, so `MachineGet` reported every pane dead and nothing logged an error. - `finish_hydration` marked a window informed before the rebuild and without looking at the result. `tabs_from_session` drops any tab whose panes all fail to start, which is every tab when the pane socket is unreachable — leaving a window that was empty and authoritative at once, and the next switch deleted a workspace with ten live tabs. Each is now settled by whoever holds the truth: attaching decides whether a pane is there, an advisory lock decides which process is the server, and a deletion needs the machine's own mirror to agree that the workspace is empty. * fix(state): quarantine a corrupt views.json instead of silently discarding it machine.json already sets a corrupt file aside before falling back to defaults; views.json just logged and returned None, and the next save overwrote whatever the file held. Move the quarantine helpers to config so both loaders share them. Also make the no-lock-primitive fallback in the daemon singleton report Unavailable rather than Taken, so a platform without flock still gets a server instead of one that refuses to start. * fix(restore): a failed List no longer reads as every pane being dead Review follow-ups on #410, all three the same shape the PR exists to stamp out: - alive_panes_on flattened a failed List RPC into an empty alive-set, which made pane_attachable respawn every pane in the batch over its running session. The failure now surfaces as None and the attach itself decides, the way session_from_tree already leaves it to. - pub fn run() bypassed the singleton lock entirely; it had no callers, so a future one would have silently reintroduced the split-brain. Removed. - Singleton::path() and the field behind it were unused. Removed. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
9c54ccf8e8 |
fix(terminal): complete WSL panes over the distro's \\wsl$ share (#408)
* fix(terminal): complete WSL panes over the distro's \\wsl$ share Tab in a WSL bash pane always fell through to the shell: the pane's filesystem is foreign, so the completion engine had no cwd to list and handed every Tab back to bash. Now a WSL pane's POSIX cwd (OSC 7) is translated to the distro's \\wsl$ share, which this process can read like any directory: - complete_foreign lists paths against the share but keeps everything that would consult this machine switched off: no PATH binaries in the command position, no generator scripts, and `~` is left to the shell (it names the distro's home, not this machine's). - Absolute words stay inside the share: Windows join semantics keep the UNC prefix when a rooted word lands on it, so `ls /etc<Tab>` lists the distro's /etc, not C:\etc. The automount stays on the share for the same reason. - A wsl.exe pane spawned without --distribution now resolves the default distro from the registry (Lxss\DefaultDistribution), so its remote context names a real distro instead of an empty placeholder. - A WSL workspace no longer claims the SSH remote-listing path; only a spec-carrying workspace does. Verified end to end against a live Ubuntu-24.04 through an isolated daemon: the default distro resolves, cwd frames flow on cd, and the translated share lists from the Windows side. * style: cargo fmt * fix(terminal): keep the \\wsl$ completion route on this machine's panes A WSL pane owned by a remote host reaches its distro through that host, not through a local \\wsl$ share -- a same-named local distro would list the wrong machine's files. Gate wsl_share_cwd on host locality, so a remote host's WSL pane falls back to handing Tab to the shell. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
a570cb1496 |
fix(windows): carry launch flags into agent resume commands (#406)
On mac/linux the daemon reads the foreground process's real argv, so resume_command can replay launch flags like --dangerously-skip-permissions. The Windows path detects the agent from the OSC 133;C shell-integration command capture but threw the command line away, stamping an empty argv: every resumed agent came back bare, with none of its launch flags. Tokenize the captured command (case-preserved, quotes trimmed, PowerShell call operator dropped) and stamp it as the pane's launch argv. Detection keeps lowercasing its own copy; replay_flags' safety allowlist already rejects tokens a whitespace split would mangle, falling back to the bare resume command as before. Fork commands share the same argv source, so they are fixed by the same change. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
817447bd48 |
feat(agents): recognize Oh My Pi and install its status hooks (#405)
Issue #376 asked for `omp`. Oh My Pi is a fork of Pi (can1357/oh-my-pi, descended from badlogic/pi-mono), but the fork is where the similarity stops for our purposes: it ships one binary of its own — `omp`, the only `bin` in `@oh-my-pi/pi-coding-agent`, and it never installs a `pi` — and it keeps its config under `~/.omp`. A pane running it was therefore not detected at all, and aliasing `omp` onto `CLIAgent::Pi` would have been worse than nothing: the status bridge would land in `~/.pi`, and Resume Session would offer `pi --session <id>` to a binary that spells that flag `--resume`. So it gets its own variant, wired the whole way through: | | | |---|---| | Detection | argv stem `omp`, distinct from `pi` in both directions | | Avatar | its own mark, normalized from the project's `assets/icon.svg` | | Resume | `omp --resume <id>`, opting out on `--no-session` | | Fork | `omp --fork <id>` — a verified fork command, so the menu item appears | | Hooks | Settings → Agents, at `~/.omp/agent/extensions/tty7/index.ts` | The status bridge is the one piece the fork did not change. Oh My Pi inherited Pi's extension contract intact — same default-exported factory, same `session_start` / `agent_start` / `agent_end` / `session_shutdown`, same `ctx.sessionManager.getSessionId()` — so `pi_extension_ts` now takes the agent and substitutes two things, the package it imports the type from and the slug it calls the emitter with. Pi's generated file is byte-identical to before, so no installed bridge goes stale. `--resume`, `-r` and `--session` are three spellings of one flag in Oh My Pi; all three shed when a session command is rebuilt, while `--session-dir` is a different flag and rides along. `fork_command` now honors the same `--no-session` opt-out `resume_command` already did — Oh My Pi rejects `--fork` outright under it, and no existing agent declares an opt-out. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |