mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
feat/dock-document-column
1078
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b26a9b1801 |
fix(window): hold the docked column to widths the strip and the file agree on
Three defects in the document column, each with a guard test that fails without its fix. The tab strip did not know a column had taken width off it. On macOS the strip lives inside the terminal column and sizes itself to the window less the detail panel, so a docked document left it 340 points wider than the column it sits in and the chips ran on under the column — the same overrun the panel's own reservation was added for. Everywhere else the strip spans the workspace and the column's hoisted header is drawn over its trailing end with no fill of its own, so a chip left under it showed through the file name and stayed clickable through it. The column's width now comes off `strip_w` on macOS and off `corner_w` elsewhere, which is where the panel's already goes. The divider wrote widths the file would not keep. `Config::sanitize` holds `document_ratio` to 0.2..=0.8; the drag clamped in pixels only, so a column pushed against either edge of a wide window was saved outside that band and reopened somewhere else — on a 2560-point body, 232 points from where it was dropped. The band is a pair of shared constants now and the drag clamps to it, the way the font size and its stepper were made to agree in #550. The palette named the config's layout rather than the tab's. Fill is per tab, so a tab told to fill was still offered "Document: Fill Window" — a row that named the state it was already in and did the opposite. It reads the active tab through `ChromeState` now. Also: `document_layout`'s doc comment still described the global switch an earlier draft had, three lines after the field became a per-tab default. |
||
|
|
1b1e52284b |
feat(window): dock the code panel and the diff overlay beside the terminal (#625)
Opening a file covered the workspace. The terminal underneath kept running and was neither visible nor typeable, so reading a file while an agent talked was a toggle loop: open it, close it to read the reply, open it again. The Files tree already docks; the two surfaces you go to *from* it did not. They dock now, as a flex sibling of the terminal column rather than a narrower overlay — that distinction is the feature. `set_grid_size` is driven by the terminal element's laid-out bounds, so a column takes width away from the grid and the PTY reflows into what is left; a card painted over half the workspace would have left the grid full width with half of it hidden. `overlay_top` stops ordering a pair and starts choosing between them: a column has one child, and two `flex_1` siblings would split it and fight. Fill mode keeps the old vector, the old opaque paint and the old platform hoist untouched, so nothing about today's overlay changes for anyone who picks it. - Half the terminal column by default; drag the divider, double-click it to cycle a third / half / two thirds, or use the palette commands. Two thirds deliberately runs past the half-window cap the side panels obey — only the terminal's floor binds it. - `DOCUMENT_MIN_W` joins the width budget: both side panels reserve it the way they already reserve each other, and the column is derived from the *live* sidebar and panel widths rather than their floors, so a panel someone dragged wider is width the terminal keeps. - A window too narrow to seat both fills for that frame. The fallback is derived at render time and never stored, so widening re-docks on the next frame with nothing to undo. - Fill or dock is per tab, on the header's context menu. Reading a long file over the whole window in one tab while an agent keeps half of another is the normal case, and one global switch made each of those flip the other. A tab that has not been told reads `document_layout` from the config, which is what a fresh tab starts as — and which the menu therefore does not write, since every untold tab is reading it. - Everywhere but macOS the title bar spans the workspace, which left a bar's height of nothing above the column. The header is drawn into it, and behaves like the title bar it now sits in. With the detail panel closed the column reaches the window's right edge, so the header stops short of the trailing chrome through a width the tab strip's own reservation shares. - The docked headers drop the traffic-light inset they never had to clear, and the diff header's branch name becomes the thing that yields so the view toggle and the close tile survive a column's width. New in `config.json`: `document_ratio`, and `document_layout` for what a fresh tab starts as. Four new actions, bindable and unbound by default. |
||
|
|
3c95995e82 |
fix(input): hand Ctrl+V to a full-screen program on the alternate screen (#677) (#682)
In vim or neovim on Windows and Linux, Ctrl+V pasted the clipboard where the editor expected blockwise Visual mode. Windows Terminal (with its ctrl+v binding removed), WezTerm and Alacritty all send the key; macOS was never affected, since Cmd+V is the paste chord there. Ctrl+V was not a keybinding at all. `on_key_down` hands plain Ctrl+C, V and X to `handle_cmd_shortcut` off macOS, and of the three the "v" arm was the only unconditional one: Ctrl+C copies with a selection and otherwise falls through to SIGINT, Ctrl+X falls through outside the editor, but Ctrl+V always consumed, so SYN never reached the PTY -- `input.rs` had the byte, unreachably -- and an empty clipboard turned the key into nothing at all. #270 set the rule that off macOS ctrl-<letter> belongs to the terminal and anything sitting on one must fall through; Ctrl+V was the exception that had escaped it. The arm is now contextual like its neighbours. On the alternate screen it falls through, and `keystroke_to_bytes` sends 0x16, or the CSI u form when the program has the kitty protocol on; off it Ctrl+V pastes exactly as before, and Cmd+V on macOS is untouched. The alternate screen is the gate rather than `input_active` because the editor is inactive whenever shell integration is missing or the prompt editor is off, and gating on that would take paste away from every such user; a program that has switched screens is precisely the case reported. Inside such a program paste is Ctrl+Shift+V, Shift+Insert or the right-click menu, all of which still stage a clipboard image for an agent. The same block did not exclude Shift, so Ctrl+Shift+C/V/X reached the hardcoded path whenever the keymap had nothing on them -- exactly the state rebinding Paste leaves behind, which #271 promised would retire Ctrl+Shift+V, but it went on pasting behind the user's back. Only unshifted chords enter the block now; the shifted ones are the keymap's alone. The right-click menu advertised Ctrl+C, Ctrl+X and Ctrl+V off macOS as though they were the bindings, next to a Select All row that already showed its hint on macOS only. The three rows take the same treatment, which is also what the command palette does. Three view tests pin the split -- Ctrl+V falls through on the alternate screen while Cmd+V still pastes there, Ctrl+V pastes off it, and a key down on the alternate screen arrives at the PTY as SYN and nothing else -- and the keymap's paste test now asserts that no default claims ctrl-v in the Terminal context. The shortcuts reference notes where plain Ctrl+V pastes and where it is the program's. Fixes #677. |
||
|
|
ef333bf055 |
feat(terminal): make the wheel-zoom modifier configurable (#676)
Cmd-scroll zoomed the font with no way to move it or switch it off, so a thumb left on Cmd resized the terminal mid-scroll (#668). The modifier is now a setting: the platform modifier by default, or Ctrl, Alt, or none. Stored as the choice rather than the resolved key, so one config file still means the same thing on a Mac and on a Linux box. Settings -> Terminal -> Mouse carries the picker; off macOS Ctrl and the platform modifier are the same key, so it shows one cell for them. |
||
|
|
8b5aeb0077 |
Wire hooks, resume and fork for the CLI agents that support them (#666)
* feat(agents): hook, resume and fork support for nine more CLI agents Hooks go from 7 agents to 11. Gemini, Droid and Qwen merge into their own settings.json the way Claude and Codex already do; Goose gets an owned file under the Open Plugins layout it implements. Qwen is the only one of them with a first-class PermissionRequest event, so it needs none of the notification sniffing the others do -- and deliberately gets no Notification hook at all, since that event fires for non-blocking alerts too and would strand a pane on "waiting". Resume goes from 10 agents to 17, fork from 5 to 9. Amp's `threads fork` is a real subcommand that is simply missing from `amp threads --help`. Four detection and replay bugs turned up while checking each CLI: - `python3 -m antigravity`, the documented way to trigger Python's own easter egg, was detected as a coding agent. The `antigravity` binary is the IDE's launcher shim anyway, in the shape of VS Code's `code`, not the terminal agent -- that one is `agy`. - Amp lost every launch flag on resume. It names a thread with a positional argument, so the stale-flag list had nothing to drop and the generic bare-token check rejected the whole tail along with it. - Gemini could be handed a command line it refuses to start from: `--session-id` and `--session-file` are mutually exclusive with `--resume` and were never stripped. - Cursor's `--continue` was not stripped either, leaving it to collide with the injected `--resume <id>`. Brand colours for Aider, Goose, Droid, Vibe, Qwen and Antigravity now come from first-party sources -- logo SVG fills and site CSS variables -- rather than approximations. Qwen ships its real mark instead of the generic bot glyph. Hooks stay unwired for Aider (no lifecycle mechanism exists at all), Cursor (its usable events gate permissions, and tty7's silent hook would read as a failed check and auto-allow the command), Auggie (its command field takes only script paths, needing generated wrappers, and the constraint could not be verified without a billed run), and for Hermes, Amp, Vibe and Antigravity, whose event sets are too thin to report a blocked turn. * fix(agents): strip every session-naming alias before replaying launch flags Goose spells --session-id also as --id, --name as -n, and keeps a legacy --path, all in one exclusive clap group; Qwen rejects --session-id next to --resume; Vibe shortens --continue to -c. Any of these surviving a replay broke the regenerated resume command. Qwen's --no-chat-recording also persists nothing, so it now opts the pane out of resume and fork like Auggie's --dont-save-session. The Qwen icon gains the 24x24 width/height every other agent mark carries. |
||
|
|
9c2869a25f |
Trim the app's long-winded copy, add four dark themes (#663)
* refactor(i18n): drop the About page shell primer and trim the long copy The About page carried a "How shells work" section explaining that shells live in a background server. Nothing linked to it and the Updates and Server sections below already say what happens to those shells, so it was a paragraph of prose the page did not need. Remove it, its search index entry, and its three L10nKeys. Then cut the padding out of 48 strings across settings rows, dialogs and notices. Two patterns accounted for most of it: the restart-server dialogs stated "your shells keep running" up to four times each in different words, and the config.json failure notices packed three subordinate clauses into every sentence. Nothing is dropped but repetition and clauses the reader can infer — every consequence a dialog asks the user to weigh is still spelled out. en, zh and ja stay in sync. * feat(themes): add Catppuccin Mocha, Gruvbox Dark, Nord and Tokyo Night Four more dark built-ins, taking the set from nine to thirteen. The docs table and description are updated to match. * fix(themes): give Catppuccin Mocha its rosewater caret, refresh a stale builtin count |
||
|
|
f4c31222a4 |
feat(cli): restart the server in place by default, keeping sessions (#669)
* feat(cli): restart the server in place by default, keeping sessions `tty7 server restart` used to be stop + start, killing every pane, while the GUI's Restart Server hands the daemon off to a new image via execve and keeps everything running. Same verb, opposite side effects. The CLI now probes the daemon for the handoff feature and asks it to exec the tty7-server binary in place: same pid, same ptys, sessions survive. Success is judged by the version endpoint answering with a new per-process instance id, not by build strings, since the CLI and server binaries can be on different versions. A refused or stalled handoff leaves the daemon untouched and reports an error suggesting `--hard` instead of silently killing sessions. The stop + start path remains for `--hard` and for daemons that cannot exec themselves (Windows, pre-handoff builds). * fix(cli): leave a slow handoff's seat holder alive, and let a hard restart say sessions ended After a taken handoff, the poll timing out does not mean the daemon died: the singleton lock survives the exec, so a held seat is the new image still coming up with every session aboard. Falling back to start() there would grant it one second of grace and then reap it — bail with the seat still held instead, and only start over a genuinely free seat. The stop-and-start fallback (--hard, Windows, pre-handoff builds) now reports that sessions ended instead of relaying start()'s plain report, since the default restart's promise is sessions kept. |
||
|
|
3dc63e2d87 |
fix(daemon): find and reap a seat-holding daemon that lost both its names (#671)
A daemon can survive quit-and-stop with its endpoint unlinked and its pidfile gone while still holding the singleton seat (#667). Every later launch then spawns a daemon that stands down against the lock and times out red, and nothing on the machine can recover: stop answers "not running", ensure_running reaps only through the pidfile, and flock cannot say who the holder is. Two roads led there, and both are closed: - The reap identified a daemon by proc_pidpath alone, which fails outright for a live process whose binary was deleted — every nightly update replacing the installation. The identity check now falls back to the kernel's comm name (proc_name on macOS, /proc/pid/comm on Linux, both recorded at exec and immune to deletion), strips Linux's " (deleted)" marker, and — decisively — no longer deletes the pidfile of a live process it cannot identify: the record was the only handle left on the survivor. - When the pidfile is gone entirely, the pid the claimant now writes into daemon.lock at claim time is the handle of last resort. The lock file is never deleted and holding the flock is the definition of being the server, so while the seat is held its content names the holder; stop() and the reap fall back to it, and a confirmed reap clears the record (only under a momentarily-free seat) so a stale number cannot outlive its process. Unix-only: the Windows seat is share_mode(0), unreadable while held. Every road back now clears a stranded seat, not just the GUI's: ensure_running's stale cleanup is factored into spawn::reap_stranded, tty7 server start runs it too, and tty7 server stop no longer takes "nobody answered" for "nothing to stop" when the seat is still held. A short grace keeps the reap away from a daemon that is merely mid-handoff or mid-startup — where health is an answered handshake, never a bare connect: a wedged daemon's listener still completes connections out of the kernel's backlog. The startup-timeout errors name the seat-holding pid, with the kill advice identity-gated so a stale record never tells anyone to kill an innocent process. Two liveness corrections round it out: a zombie now reads as dead — it answers kill(pid, 0) like the living but holds no lock and no image, and no signal can end it, so counting it alive spent both reap timeouts on a corpse (the GUI never waits on the daemons it spawns, so crashed daemons are zombies as a rule) — and stop() only pays the process-exit wait for a shutdown it actually delivered, instead of watching an unreached survivor not move for five seconds. The guard tests were each verified to fail against the behavior they pin (fallbacks, the handshake criterion, the grace, and the wait gate removed by mutation) before being trusted green; the zombie probe semantics (proc_pidinfo failing for a zombie that still answers signal 0) were measured, not assumed. |
||
|
|
89e4ae833d |
fix(terminal): stop hidden panes from repainting the whole window (#670)
* fix(terminal): stop hidden panes from repainting the whole window Every pane's PTY pump ended a batch with an unconditional window.refresh(), and a pane in a background tab still resolves to its window there — so any hidden pane producing output pinned the visible tab at full frame rate. With 30 tabs, 29 of them chatty in the background, the window repainted at a steady 60 calls/s and the GUI process sat at ~45% CPU with nothing visible changing. Dropping the refresh is not enough: the chrome reads every pane entity while the window draws, so gpui tracks them all and a hidden pane's notify() dirties the window anyway. The pump's Wakeup notify is now gated on a per-pane displayed flag — an Arc<AtomicBool> outside the entity map, declared each frame by the root render (active tab true, everything else false). Flags default to displayed, so a path that never declares can only cost extra repaints, never a frozen grid. Low-frequency events (title, exit) keep notifying unconditionally so tab chips stay fresh. Same load after the change: ~20 renders/s driven only by the visible pane, ~520 background wakeups/s suppressed, and an idle window with 30 quiet tabs sits at a few renders/s. * test(terminal): pin the output gate's semantics; scope the registry per app The displayed registry moves from a process-wide static into a gpui Global. Entity ids are only unique within one App, and parallel gpui tests each mint their own App with colliding id sequences — through a static, one test's frame declarations could flip another test's pane flags. The shipped binary runs exactly one App, so behavior there is unchanged. Three tests now hold the gate to its contract: the active tab's panes count as displayed and a tab switch hands the frame loop over; a pane nobody declared (and an id nobody registered) errs toward displayed, because the failure direction that matters is a visible pane that stops repainting; and a released pane's flag does not outlive it. Also restores touch_active_tab's doc comment, which the previous commit had accidentally fused onto declare_displayed_panes. |
||
|
|
2d517fa0f3 | ci: pin the GITHUB_TOKEN to read-only in the CI workflow (#665) | ||
|
|
9f34cd3501 |
fix(editor): stop scrolled-out text painting over the line numbers
Bump the gpui-component fork to 070d1a2, which clips the editor's scrolling content to the right of the gutter. Text, selections, indent guides and the cursor all paint from a bounds origin that horizontal scrolling has already shifted left, so scrolled-out content kept painting under the line-number column; the only thing hiding it was the gutter quad painted afterwards, which works only while `editor.gutter.background` is opaque. `apply_theme` clears that key to transparent so the panel can sit on a gradient or image window background without a seam, which is exactly the case the upstream code does not cover. Note that dependency in the theme, so the next person to touch it knows the transparent gutter is not free. |
||
|
|
95305d50dd |
fix(sidebar): give the tab rows a width that resolves
The rows, their group blocks and the scroll area all ask for `w_full`, and a percentage is only a width while some box above it has a real one. The column inside the rail declared `size_full`, which is another percentage: on the passes that size that column from its content there was nothing for any of them to resolve against, so every row fell back to hugging the longest tab name and the active row's capsule stopped well short of the rail's edge. Hand that column real pixels instead. The rail is `w(px(width))` and layout is border-box, so its content is one pixel narrower because of the right border. With a definite width there, the whole chain below resolves — which also makes the same trick on `workspace_head` redundant, though it is left in place as a harmless explicit width. `w_full` on the scroll area itself is the second half: a stretched width sizes it the same but not definitely, and the rows inside need a definite one to be a percentage of. |
||
|
|
0295a98915 |
feat(sftp): open remote text files in the built-in editor
A click on a file in the SSH Files panel used to start a download; the only way to change a remote file was download, edit, re-upload. Now a click opens it in the built-in editor and Cmd-S saves straight back over the pane's own SFTP channel, matching what the Files panel already does locally and over a remote workspace. - protocol: SftpOp::ReadFile/WriteFile and SftpOpResult::File, bytes as base64; the reply carries the body plus the stat it was read under - daemon: ReadFile enforces the caller's size ceiling before and during the read; WriteFile rewrites in place (truncate, not temp-and-rename) so the file keeps its mode and ownership - SftpHost: a Host over the pane's SFTP route, so the editor's existing open/save path works unchanged; git/search/watch honestly Unsupported - editor: an open buffer holds the host it was read from, and save/reload/dedup/watch key on (host, path) instead of the active host - panel: single click opens (dirs navigate, text files edit), the same gesture as the local tree; binary or oversized files get the local tree's toast, and Download moves to the context menu Review follow-ups, in this PR: the SFTP host stays out of HostRegistry, which means "a machine this window has a link to" and is swept as such — filing the pane's channel there made Cmd-S return silently once a workspace deletion took it back out. The cursor-jump lookup, the status bar's path, and the SCM panel's repository all key on the buffer's own host now. Closes #656. |
||
|
|
7b0660bd42 |
fix(sidebar): give the workspace head a width that always resolves (#662)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
6e193c404f |
fix(editor): kill paths one component at a time on ctrl-w (#658) (#659)
The built-in command editor intercepts ctrl-w before the shell sees it, and its whitespace-only word boundaries killed a whole path in one stroke. fish binds ctrl-w to backward-kill-path-component, so users coming from kitty or Terminal.app expect /usr/local/bin to go one segment at a time. Mirror fish's path-component word motion: at most one run per character class, separators (slash, equals, quotes, ...) end a kill next to whitespace on their own. alt-backspace keeps the coarse whitespace-delimited kill, matching fish's split between the two chords. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
ccd21fe97d |
fix(windows): keep a restored screen out of ConPTY's viewport, and stop Restart Server crashing the window (#657)
* fix(restore): keep a restored screen out of ConPTY's viewport On Windows a restored pane came back with its shell drawing in the wrong place: the prompt stopped responding where it stood and the restored text filled with fragments of whatever was being typed. A ConPTY does not hand the terminal a stream, it hands it a rendering of a screen buffer conhost owns, addressed absolutely and counted from that buffer's top-left, which starts blank with the cursor at (0,0). PSReadLine redraws the line being typed as `ESC[6;20H ... ESC[6;26H` on every keystroke, and conhost frames what it paints the same way. Those row numbers are only right if the client's viewport is conhost's buffer, row for row. Restored output is output conhost never produced and knows nothing about. Left on screen it shifts every row conhost names, so the first repaint of the input line lands on the old text. Nothing the client can do fixes it afterwards: the offset is not constant, and it would have to be unpicked from every absolute address in the stream. So the restore preamble now ends by scrolling the restored screen out of the way. `ESC[2J` on the primary screen scrolls the viewport into history rather than erasing it, so the screen the daemon restored is one scroll up rather than gone, and `ESC[H` leaves the cursor where a fresh ConPTY expects to find it. Unix keeps the old behaviour: a shell there positions itself relatively, so the restored screen can stay where it can be seen. * fix(restart): stop Restart Server taking the window with it Clicking Restart Server made the whole app disappear, with a double-lease panic in the crash log: cannot read Tty7App while it is already being updated. The work that puts the window back together after the restart ran inside `update_in` on this window's own entity, and it ends by rebuilding every local window from the machine tree. The first thing that rebuild asks each window is which tabs it is showing, which it reads back out of the window registry — so the first window it reaches for is the one the closure already holds leased, and gpui answers a double lease by panicking, which on the main thread is the process. Split into `settle_after_restart`: the window's own state first, then the resync outside the lease, then the focus. The resync still runs either way the restart went, because a refused handoff leaves the daemon serving the panes this window already dropped (#554). --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
ac3c95a647 |
feat(update): install verified Linux AppImage releases in app (#306) (#652)
The last platform from #306: a Linux install running as an AppImage can now download, verify, and apply a release from inside the app, through the same tty7-updater helper the macOS (#309) and Windows (#330) paths use. Tarball and distro installs are deliberately untouched — they keep the named-package hint and the release page, because replacing a file a package manager may own is not this code's call to make. The installed artifact is one file, the path $APPIMAGE names, so the install is the simplest of the three platforms: stage the download beside the image (two renames only stay atomic on one filesystem), verify, swap, relaunch, and restore the preserved previous image if the new one does not survive its launch grace. What is Linux-shaped about it is the mount: the image the GUI runs from is FUSE-mounted by the AppImage runtime and torn down when the app exits, which is the moment the installer starts working — so the GUI copies the helper out of the mount into staging and runs the copy, the way the Windows path runs a private copy because Setup replaces the installed one. The daemon is left running throughout, as on macOS: nothing on Linux locks a running executable's file, and the panes it serves are the reason the update restarts only the GUI. The swap also carries the installed image's own mode onto its replacement, so a 0700 image stays private and the download's missing execute bit never reaches the installation. Verification holds the issue's requirements with what an unsigned ELF can offer: the bytes must match the release's checksums.txt, the file must actually be a type-2 AppImage — a mis-published asset fails with a name instead of at launch — and the image must state the version it claims. That statement is new: bundle-appimage.sh stamps X-AppImage-Version into the desktop entry, and the updater reads it back with one --appimage-extract, answered by the runtime before any application code and without FUSE. The same pass requires the new image to bundle its own tty7-updater, because an image without one would install fine and then be the last version that ever could. release.yml and nightly.yml now build the updater on the Linux leg and bundle it into the AppImage, and both check the packaged image for the same facts the updater checks on a user's machine — helper present, version stamped — so a packaging mistake fails the workflow instead of the update. The first release carrying this can only bootstrap: images already installed predate the helper and keep the manual hint, so the first complete in-app update is the release after it. |
||
|
|
0df604054d |
fix(daemon): keep a lingering daemon findable and reapable after quit-and-stop (#655)
* fix(daemon): keep a lingering daemon findable and reapable after quit-and-stop Quit-and-stop could strand a daemon that had already unlinked daemon.sock and deleted daemon.pid but never finished exiting: libc exit() runs atexit handlers and static destructors beside dozens of live threads, and a finalizer that blocks leaves the process holding the singleton lock with no name on disk. Every later launch then spawns a daemon that stands down against the lock and times out red, forever. Three changes, each a fallback for the others: - on_shutdown keeps the pidfile: once the endpoint is unlinked it is the only handle anything has on a process that is not gone yet. A pidfile that outlives a clean exit was already handled by recorded_daemon_is_dead and the reap path. - The daemon exits through _exit(2) (after flushing the logger), skipping the atexit/destructor window entirely; everything owed to disk is flushed explicitly in on_shutdown. - spawn::stop reaps with the pid it captured before asking the daemon to die, instead of re-reading a pidfile an old build's shutdown may have wiped mid-stop; reap_recorded_daemon keeps the pidfile when the process survives even SIGKILL, so the next attempt still has someone to reap. * review: fix stale stop() comment, pin the mid-stop pidfile-vanish ordering in the test The comment at the top of stop() still claimed a clean shutdown removes the pidfile, which this branch just made untrue; it now states the real reasons the pid is captured early. The vanishing-pidfile test now asserts the sweeper's delete actually landed while stop() was waiting, so a future shrink of PROCESS_EXIT_TIMEOUT cannot silently turn it into a weaker scenario. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
9fc0f331e8 |
feat(tabs): drag a tab in as a pane, and a pane out as a tab (#651)
* feat(tabs): drag a tab in as a pane, and a pane out as a tab A tab dragged by its chip or its sidebar row can be dropped over the panes to become one of them, and a pane dragged by its grip can be dropped on the strip or the sidebar to become a tab of its own. Both carry the panes across as they are: nothing is spawned and nothing is killed, so a shell mid-command, an SSH session or an agent mid-turn keeps running. The landing is read the way a pane drag's already is, minus the middle: an arriving tab has nothing here to trade places with, so a pane's core means "split it the way it is longest". A tab that was itself split arrives with its own shape intact and takes one share of the row or column it joined. A pane on its way out is offered a caret between two tabs, and the last pane in a tab is offered nothing, being a tab of its own already. Picking a tab up no longer switches to it: the strip and the sidebar now activate on the click rather than on the press. Without that the merge cannot be expressed at all — pressing the tab to drag it would put it on screen, leaving no other tab to drop it into. Two things in the machine tree had to follow: * Panes that change tabs are told as PaneMove, one at a time, rather than as a tab closing and another being rebuilt around them. * The tabs the machine already has are reconciled before new ones are created, so a pane leaving for a tab of its own is given up by the old tab before the new one asks to register it. The machine refuses a pane that is in two tabs at once, and the refusal desynced the window. Closes #621 * test(tree-sync): a tab grafted above a whole layout still converges * fix(tabs): keep a click on the close button from switching tabs Switching on the release rather than the press means every click inside a chip or a sidebar row now reaches the row itself, and gpui-component's `Button` does not stop propagation on a click it handled. So one click on a tab's close button ran `close_tab(i)` and then `activate(i)` — with `i` by then naming whichever tab had slid into that slot, which moved the active tab somewhere nobody asked for. A click into the rename field did the same: it switched away from the tab whose name was being typed, and took the focus out of the field with it. Both now hold the click where they handled it, the way they already held the press. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
3ef644d267 |
fix(scm): keep the sync tile on the branch row at any panel width (#650)
* fix(scm): keep the sync tile on the branch row at any panel width The branch row's flex constraints were set on the Button, but `dropdown_menu_with_anchor` hands that Button to a `Popover`, which wraps it in a plain div and never applies the trigger style it was given (`trigger_style` is stored and never read). The constraints landed inside a box that still measured its own content, so at the panel's 216px floor a 24-character branch name overflowed the row and pushed the sync tile out of the panel entirely, with no way to reach it. Carry `flex_1` on a wrapper instead, and truncate the name against the width it is actually given rather than against a character budget that was guessing at that width. The `elide_middle` ceiling is gone: stacked on top of a real truncation it produced two ellipses in a row (`fix/new-tab-……`) and threw away the tail it existed to keep. Notes and chips move into one `overflow_hidden` group that is allowed to shrink, so the order of who gives way is explicit: branch name first, badges second, the tile never. Also stop offering "Publish Branch" from a HEAD that cannot publish. A detached or unborn HEAD has no upstream by definition, so the token fired there unconditionally — the widest thing on the row, naming the one operation the tile beside it already refuses (#545), and on its own enough to push that tile off a 216px panel. * fix(scm): drop the branch row's note box when it holds nothing The row lays the notes and chips out in one shrinkable box so the sync tile keeps its place. An empty box is still a flex item, so the row's 6px gap was spent on either side of nothing: on the quiet branch that is most of what anyone looks at, the caret sat 12px off the tile instead of 6px. Build the notes first and only add the box when there is something in it. Also drop two comment citations of #549, which is about palette commands that no-op silently and has nothing to do with this row. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
05de7ae33a |
fix(new-tab): keep the SSH menu inside a menu's shape (#649)
* fix(new-tab): keep the SSH menu inside a menu's shape The saved-host rows carried names and endpoints long enough to drag the panel out to the 500px ceiling PopupMenu falls back to, and the row that meant to elide was clipped mid glyph instead. The menu now stops at 360px, and a row that runs out of room cuts the endpoint first — the name is what the reader is picking by, so it keeps whatever is left rather than being squeezed to "..". The height ceiling moves up to fit the shape everyone actually sees — nine shells, both headings, six hosts and the two closing rows — so the default menu arrives whole instead of scrolled with "Local" cut off above, and is capped again against the window so a short one never gets a menu taller than itself. The rule above the split hint goes: a separator divides two lists of things to pick, and the hint is a footnote about the list it follows. Bumps gpui-component, where a scrollable PopupMenu painted a scrollbar whether or not it overflowed, custom rows could not elide, and labels had no padding of their own. * fix(new-tab): measure the menu ceiling off the viewport, and elide nameless hosts `window_bounds()` answers how a window should be reopened after it is closed, so a fullscreen macOS window reports the bounds it would restore to rather than the screen it currently fills. A terminal spends much of its life fullscreen, where that reading capped the menu at 80% of a window nobody is looking at — putting back the scrollbar and the cut-off `Local` this branch is here to remove. `viewport_size()` is what every other window-relative size in the app already measures against. A host saved on its address alone is *named* `user@host:port` and carries no note, so it took the plain-item path — bare text with nothing to elide against, on the longest string in the menu and the row least able to cut it. Every host row is a custom element now, and `menu_row` drops its right half when the note is empty rather than holding the gap open with a zero-width child. Also drops 17 unrelated dependency downgrades that rode along with the `gpui-component` bump. The lockfile moves only the three `source` lines it meant to; `cargo check --locked` accepts it. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
77590b5c70 | docs: bring back a single platforms badge, in blue | ||
|
|
7aacbdca7e | docs: fold platform support into the subtitle, point the version badge at releases | ||
|
|
b884aa36c5 | docs: restore per-OS platform badges with a flat Windows mark | ||
|
|
8c875d8be6 | docs: swap the OS logos for a single shell-prompt platform badge | ||
|
|
9673c1ab0b | docs: inline the Windows logo, Simple Icons no longer ships one | ||
|
|
a59d6ec28d | docs: give the platform badges their own row with per-OS logos | ||
|
|
6031570798 |
feat(new-tab): reach saved SSH hosts from the New Tab button (#647)
Adds a saved-SSH-hosts section to the New Tab menu on both the tab strip and the sidebar, ordered by frecency, with a row that opens the full host palette. Holding the modifier opens the host in a split instead of a tab. The menu scrolls once the rows outgrow the popup, long host names and their endpoints truncate rather than overflow, a host with no name of its own draws its endpoint once instead of twice, and the rows are built when the menu opens rather than on every painted frame. |
||
|
|
44bee953da | docs: badge the supported platforms in both READMEs | ||
|
|
cf6df5b469 |
fix(ssh): cover the whole window with the password prompt's scrim, and name the machine in a failed reconnect (#645)
Hoists the SSH password prompt's overlay from the body area to the window root so its scrim covers the title bar, tab strip and side panels, and aligns its top offset with the switcher card. Replaces the raw target string with the resolved machine label on the reconnect banner and on the connecting pane, so a profile-backed machine no longer shows a bare config UUID in "Connecting to …" or "Could not reach …". |
||
|
|
f1144deb9f |
fix(ui): restyle the in-app notification to sit in tty7's own visual language (#646)
Bumps the gpui-component pin to pick up the notification restyle: flow-positioned status icon and close button that centre on the first line at any wrap count, a hairline-shadow surface in light theme, and a type ranking expressed in rems so it survives the ui_font_size setting. |
||
|
|
2e6103cf19 |
Retire to the tray on window close; cold start no longer stalls on stale daemon files (#639)
* feat(ui,daemon): retire to the tray on window close and make cold start immune to stale daemon files Two problems shared a root: the daemon outlived every window, and nothing could stop it gracefully. Window lifecycle: - Closing the last window retires the app to the tray instead of quitting (QuitMode::Explicit), so the daemon stays reachable. The tray restores the most recent workspace, and Quit — after the confirmation that protects running shells — stops the daemon. Every explicit exit path (tray, palette, keybinding) now stops the server; no exit leaves an orphaned daemon behind a dead icon. - A pathless launch (double-click) hands off to the registered GUI via GuiOpen(None) and exits, instead of starting a second process with a second tray icon. - The tray subsystem initializes once per process; reopening a window no longer creates a duplicate icon. Cold-start robustness: - Liveness connects are bounded to 500 ms, the version handshake times out in 1 s, and an unresponsive daemon is reaped by its recorded pid instead of polled for a 6 s graceful stop. - A dead recorded pid skips the TCP probes entirely — the GUI's ensure_running, the new daemon's endpoint check, and the control-listener occupancy check (which could also misread a reused port as a live control server and refuse to boot). Stale cleanup now also removes the leftover control.port. * fix(daemon,gui): skip the GuiOpen handoff probe when the recorded daemon is dead * fix(lifecycle): keep the stale-endpoint cleanup, and do not retire into a tray that is not there Three gaps in the tray-persist and cold-start work. `ensure_running` moved the refused-connect branch under the new liveness check, so a connect that fails while `recorded_daemon_is_dead` says "not dead" now skips the reap and the stale-endpoint removal entirely. The pidfile answers "not dead" to two cases it has no evidence about: it is missing (the daemon died between `transport::bind`, which writes daemon.port, and `pidfile::write_current`), or it records a pid the OS has since reused. Both then leave daemon.port on disk and the spawn poll pays the OS's refusal delay on it — the cost this path was rewritten to avoid. Restore the branch, and split the rule into `recorded_daemon_is_dead_with` so a test can state that a missing pidfile is not evidence of death, without an env var every parallel test would inherit. The tray's windowless Quit stopped the server without a prompt, reasoning that the confirmation is about the panes behind a window. It is not: it says "anything still running in your shells is terminated", and retiring to the tray is precisely what leaves those shells running with no window. Bring the window back and deliver the action to it, so the confirmation appears; only when no window can be opened does the bare stop remain, with a warning. `show_tray_icon` is a request, not an outcome. `Backend::create` can fail for a whole run — a Linux session with no StatusNotifier host is the ordinary case — and after MAX_ATTEMPTS the loop gives up and logs. Retiring on the config alone then leaves a process with no window and no icon: not reachable, and still holding the daemon. Gate the retirement on an icon actually being up. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
4f1e181bdf |
feat(shell): give Nushell panes OSC 7 cwd tracking and OSC 133 marks (#637)
* feat(shell): give Nushell panes OSC 7 cwd tracking and OSC 133 marks feat(shell): give Nushell panes OSC 7 cwd tracking and OSC 133 marks Nushell was the only detected shell with no integration: shell_kind never matched `nu`, so panes were spawned with no OSC 7 cwd reports and the daemon could not follow `cd` — pwsh, zsh, bash and fish all do. Recent Nushells emit their own OSC 133 marks and a Windows `OSC 9;9` cwd protocol, but tty7 only consumes OSC 7, and the native `shell_integration.osc7` toggle defaults to off. The injection rides `nu --config` (Nushell has no ZDOTDIR analogue): a throwaway config.nu sources the user's own config.nu back in first, then appends hooks. `source` is a parse-time construct in Nushell — it cannot be guarded at runtime or name a missing file — so the Rust side resolves the path with the same rules `$nu.default-config-dir` uses (APPDATA / XDG / HOME) and substitutes a literal, or a no-op line when there is no config.nu. The hooks report cwd (OSC 7, %-escaped, `/C:/…` shape on Windows), prompt start (A) and the previous command's exit (D, gated on a flag the pre_execution hook arms so the first prompt emits nothing), mark command output (C), and wrap prompt_indicator for the prompt-end mark (B) only when the config defines one — recent Nushells' built-in prompt keeps drawing its own indicator and B mark, and overlapping marks merge in the daemon's prompt-state machine. Remote SSH and WSL panes are unchanged: their bootstraps cannot carry a Nushell config, and a nu-as-login-shell session would break on one. Tests: static assertions on the script, setup dispatch, literal substitution, and a Windows real-PTY cycle asserting OSC 7 follows `cd` and every mark arrives with the correct exit status. fmt * fix(shell): resolve the Nushell config dir the way nu does The wrapper's `--config` replaces the user's config.nu entirely, so the path it sources back must be the one nu itself would load — anything else silently strips macOS panes of their prompt, aliases and keybindings. `dirs::config_dir()` is `~/Library/Application Support` on macOS, not `~/.config`, and nu-path consults `$XDG_CONFIG_HOME` on every platform (Windows included) but only when it is non-empty and absolute. The resolution now mirrors that: a per-platform default plus XDG winning only in the exact shape nu accepts. Also gate the OSC 7 backslash translation on Windows (`$nu.os-info.name`) so a Unix path with a literal backslash survives, and harden the real-PTY tests: one line per submitted command (reedline drops input while a command runs) and a grace period after the D mark so the cwd report that follows it in the same prompt cycle lands in the transcript. |
||
|
|
5c284799aa |
fix(tabs): stop a command that is over in a blink from flashing across the tab
The tab title follows the terminal's OSC title, and nearly every prompt framework sets that to the command it is about to run and puts the old title back at the next prompt. For anything that finishes in a blink both edges arrive within a few frames, so the label showed the command and snapped straight back — a flicker that reads as a rendering glitch rather than as information. Hold a new title for 400ms before the tab adopts it. A title that reverts inside the wait matches what the tab already shows and drops the pending one, so a short command never reaches the label at all; one still running when the wait elapses names the tab as before, 400ms later. A second title arriving mid-wait rides the wait already in flight instead of restarting it. Restarting is what would let a program that rewrites its own title faster than the wait — a download reporting progress — put the tab's next update off for as long as it ran. Child exit clears the pending title and writes its own immediately: a title still waiting its turn would otherwise land on top of "(process exited)" a moment later. |
||
|
|
3d6528737a |
fix(settings): give the page back its scroll range, and hold the bar off the window corner
The centring added in #631 turned the settings content box into a flex column, and that cost the page most of its scroll range: the box is an item of the scroll pane, which is itself a flex column, so its height came out of a negotiation with the pane rather than from the rows it stacks. `content_size` is just that box's laid-out bounds, so the range ended a screen short of the last row — dragging to the bottom still left content cut off. `flex_shrink_0` does not help; the height is agreed, not squeezed. Centre with `mx_auto` on the column instead and leave the box a block, which reports the full height it stacks. While there, hold the content scrollbar 12px clear of the top and bottom. Every other list this bar serves sits in a bordered panel where running the full height is right; this pane is the window, and a bar drawn to the last pixel lands on the rounded corner. New `with_inset_vertical_scrollbar` takes the inset, and the existing `with_vertical_scrollbar` keeps its behaviour for the other twelve call sites. |
||
|
|
f08d8c2764 |
fix(remote): stop the server on machines that have no /proc, and show the install on the strip (#627)
* fix(remote): stop the server on machines that have no /proc, and show the install on the strip Restarting the remote server timed out after ten seconds on every Mac and BSD, with the old daemon still running and the new binary already sitting next to it, unlaunched. Both the probe that finds the running `tty7-server-*` and the command that terminates it walked `/proc/[0-9]*` and read each `exe` symlink. There is no `/proc` there. Two things then went wrong at once. zsh is the login shell on macOS, and it aborts the whole command line when a glob matches nothing, so even the trailing `true` never ran; and `cycle_daemon` discards the result of the terminate, so a command that killed nothing was indistinguishable from one that worked. `daemon_is_serving` then answered yes until the deadline. Guard the glob behind `[ -d /proc ]` — unreached, it is never expanded, so zsh has nothing to abort on — and fall back to `ps`, whose `comm` is the full path on the BSDs. It cannot be the only branch: Linux truncates `comm` to 15 characters, one short of `tty7-server-c7p5`, which is why `/proc` stays the first choice where it exists. `check_running_build` reads the same probe and was equally blind on those machines; it can see now. Separately, the install progress bar only ever existed inside the switcher. Pressing Update Server from a parked workspace with no switcher open froze the window for the length of the download and then produced a modal, with nothing in between. The strip draws it too now — caption and bar from the same source the switcher uses, and no button while an install is in flight, since pressing it again would start a second one on top of the first. * fix(remote): say why a stop failed, and stop a leaked install from eating the strip's button Three things the no-/proc fix left standing. `cycle_daemon` still discarded the terminate's result, which is the other half of why a Mac cost a bug report: the command ends in `true`, so anything short of success means the far end never reached the kill at all, and that is exactly what a zsh abort looks like. It is now logged, and named in the timeout error — "the running remote daemon did not stop within 10s" on its own blames a daemon for ignoring a request nobody managed to send it. The strip hides its Update Server button whenever an install is in flight, which is right, but it reads the progress registry with no link state to temper it — unlike the switcher. `finish_connect` bows out before clearing that entry whenever `connect` has moved on in the meantime, and a switcher disconnect or a move to another workspace both do that mid-install. The leftover froze a progress bar on every window pointed at the machine and took away the one button that could have fixed it. Cleared where the attempt actually ends instead, however it ended. The switcher kept its own copy of the progress bar after the caption was shared; it draws the shared one now. Tests: the probe runs for real in every shell on the machine rather than only parsing under `sh -n` — the glob that started this was valid syntax and only fell over when zsh ran it, which no `-n` can see. `ps` is checked on its own where the fallback would actually be taken, since that arm eats its own stderr and a rejected flag would otherwise cost nothing visible. And a stop that fails is asserted to reach the error. `with_shutdown_timeout` exists so that last test does not sit out ten seconds. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
3bc8a764f7 |
fix(theme): stop the code editor painting its gutter and current line in the stock syntax theme's colours (#636)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
34957f8659 |
docs(shell): name custom arguments as a reason integration never engaged (#634)
A pane that never armed shell integration blamed a PTY wrapper or an unsupported shell setup. Since #629 a zsh or fish the user gave arguments to is deliberately left alone, so the notice now names that first — it is the one cause the user can undo. All three locales. The release notes gained the matching entry: the change turns integration off for an existing config that sets `shell` or a `custom_shells` entry with `args`, which is worth stating outright. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
52b823a9f3 |
test(history): stop the sweep test deleting the panes of tests running beside it (#638)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
7fded5afd4 |
fix(tree-sync): record the panes a window seeded in its own mirror (#628)
A window's mirror of its machine held no `PaneRecord` for a pane the window itself created. Records ride inside layout deltas and a client is left out of the deltas its own ops raise, so the record the daemon mints when it registers a seed reached every window but the one showing the pane; `PaneFacts` closed the gap only when a fact changed, and a pane spawned into its directory and left at its prompt never changes one. The workspace answered no subject path, an unnamed one read "Untitled", and `record_geometry` stamped a null subject over the path views.json remembered. The window knows what it seeded, so it now puts those records into its own mirror through the same `PaneSeed::into_record` the daemon mints with — opened up rather than duplicated, so ssh-secret stripping stays shared — with the window's own Ready terminals standing in for the daemon's liveness probe. The write is insert-only: a record the mirror already holds came from the machine and outranks what a seed knows. Also closes the race that reopened the same symptom by another route: a `MachineGet` already in flight installed its tree whole and took the client's not-yet-acknowledged writes with it, and nothing re-inserted them until the next non-empty op. All four optimistic writers now go through one path that keeps each write for the life of a pull in flight and replays it over the tree that lands. The tree stays authoritative for everything it speaks about — only the ops it was built too early to know are put back on top, and the journal is drained once it has landed, so nothing a later tree dropped is resurrected. This covers #604's pushed tabs and workspace ops, not only the seeded records. Fixes #612. |
||
|
|
84a424006e |
fix(resize): defer the reflow to the daemon's Size echo on remote routes too (#632)
Since #415 the daemon echoes a `Size` frame at the stream position where the pty changes geometry and the client defers its grid reflow to that marker — but only on local routes, so a remote pane resized mid-flood still parsed queued old-width bytes into the new-width grid, which network transport makes worse. Rather than probing `Version` per pane (a whole routed connection, and for ssh/WSL a whole bridge process, on every spawn and attach), the server advertises the pane protocol's features on its control hello. The answer is cached on the link and read off the host when a pane's route is built, and the route carries it to the terminal at spawn, attach and relink. This is additive within `CONTROL_VERSION` 7: no new field, just extra names in the existing `ControlHelloOk.features`, so an older client cannot choke and an older server that names no echo makes the client reflow at request time as before. A route built while the link is down answers false. Known limitation, inherited from #415's design and not introduced here: there is no timeout if a promised echo never arrives — once deferred, a later identical resize neither re-sends nor reflows, so a wrongly-set bit would freeze the grid at the old geometry. Every traced path makes the control hello and the pane daemon the same build, normally the same process. Closes #416. |
||
|
|
422808191d |
feat(sidebar): group a tab by its folder when its cwd is not a repo (#631)
`sidebar_grouping` gains a third, opt-in mode, `repo-or-directory`: group by repository home as before, and when the repo probe has landed and answered "not a repo", group under the cwd itself instead of filing every such tab under Scratch. A probe that has not run yet resolves to no decision, so a tab keeps the group it already has rather than bouncing through Scratch mid-probe. The decision lives in one `resolved_group` free function shared by the per-frame key derivation and spawn-time seeding. The default (`repo`) and flat modes behave exactly as before, and an unknown value in an existing config still degrades to `repo`. Knock-on: `machine_mirror::subject_path_of` names a window after its most common group, so in the new mode a window of plain shells takes its name from the most common directory rather than from the first pane's cwd. Closes #620. |
||
|
|
0346e35b40 |
fix(shell): stop injecting into a zsh or fish the user gave arguments to (#629)
The zsh and fish arms of `shell_integration::setup` never checked `has_custom_args`, so a shell the user launched with their own arguments was injected anyway — fish had `-C <script>` appended to its argv, zsh had its ZDOTDIR swapped. Both arms now sit behind the same gate bash, PowerShell and WSL already used, hoisted to a single early return ahead of the dispatch so a new ShellKind cannot silently reintroduce the bug.
Docs now describe what the code does: the `shell` row's own `{"program": "fish", "args": ["-l"]}` example loses integration under this rule, and the shell-integration note distinguishes user-written arguments from the ones detection supplies (Git Bash, WSL).
Part of #624; the native-input-mode half is separate.
|
||
|
|
72db26d15a |
feat(prompt): let the shell's own line editor own the prompt (#633)
Closes #624 tty7's inline editor takes the prompt the moment OSC 133 reports one, and until now the only way to keep it off was to hide the shell's own name from tty7 so integration never armed — which costs the prompt boundaries, cwd and exit codes as well. Someone who binds `history-beginning-search- backward-end` to Up in their zshrc had no way to reach it, and the local history the editor walks instead is per-view: a command run in one pane is not in another's list, so the shell's shared history looked broken too. The new `prompt_editor` switch (Settings -> Input -> Prompt, on by default) hands the line back. Off, every key at the prompt goes to the PTY, so ZLE / readline / fish do the editing and what the user bound behaves as written. Shell integration is untouched by it. The gate is one line in `input_inactive_reason`, which every path that could take the prompt from the shell already asks: keys, IME commits, paste, Tab, the completion and reverse-search menus, the input bar. That is what makes this a mode rather than a special case per key. `shell_owns_prompt` learns the flag too, and that half matters more than it looks: the gap hold and the typeahead record both exist to feed the local editor, and `flush_typeahead` sends ^U to erase the line before moving it there — on a line only ZLE is editing, that erases the user's work. Ctrl-R landing on the PTY also stops raising the missing-integration notice: the shell owning it is what was asked for. Turning it off mid-line hands what is typed to the shell the way an unknown chord does, so the text is still on the prompt to finish. Live panes follow the switch, including a hand edit of config.json in another window. Tab completion and history search are menus tty7 opens inside that editor, so the page greys them out and says why while it is off. Only their text dims — a switch already draws its thumb at 35% when disabled, and dimming the row on top of that leaves a pill with nothing visible in it. Their stored values are left alone and come back with the editor. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
b3698f4b59 |
fix(worktree): lift the new-worktree prompt to a window-level modal (#626)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
1424da0891 |
fix: nine UX fixes across the diff overlay, layout, settings and pane spawn (#623)
Found by driving a dev instance and measuring what an idle window costs. Two of them were frame loops that never stopped. A `Head` diff overlay calls itself stale when the cached git status disagrees with the snapshot on screen, and every landed probe wakes that check by touching the cache — but the read published its counts and left the branch behind, so a branch switched outside tty7 made the disagreement permanent: two `git` processes a lap, forever, with `refreshing…` pinned to the header and an idle window at 7% of a core. And the home page asked for a frame sixty times a second to change one glyph's opacity twice, which made a window with nothing open in it eight times more expensive than one running a shell. Both now settle: the diff read publishes the branch it found, and the home cursor flips a bool on a timer the way the terminal's own does. The rest: - The tab sidebar and the right panel each capped themselves at half the window and knew nothing about the other, so together they could take all of it — 260 points of terminal on a 720-point window. Both now cap at whichever binds harder: half the window, or what is left after the terminal's floor and the other panel's floor. The same cap bounds the drag, so a panel dragged to its limit stays where it was dropped. - Only a HEAD diff may correct the sidebar's counts. Those numbers mean `git diff --numstat HEAD`; an unstaged or staged patch answers a smaller question, so opening an untracked file from the Source Control panel took the staged lines off the total on the click. - An untracked row in the diff overlay had no click target, and once focused could not be left — the breadcrumb looks the path up in `files`, where an untracked file has no entry. Both ends fixed. - A new pane keeps the name its directory was reached by. `cwd()` alone loses it: the shell falls back to `getcwd()`, so `/tmp/x` became `/private/tmp/x` in every tab opened from the first. `PWD` carries it, and POSIX has the shell discard a `PWD` that names the wrong directory, so this can correct the name and cannot invent one. - The settings search now sees into the Keybindings page, which is generated from the binding table rather than the static index — so searching for a feature finds its shortcut, and the page filters to the matches. Closes #444. - The settings reading column is centred rather than pinned to the nav: on a window as wide as the display it was made for, 640 points of settings sat beside 1600 points of nothing. - `New Workspace…` takes the ellipsis its three sibling actions already carry — it opens a form asking for a name and a host. Every fix has a test. The re-probe loop is pinned end-to-end with `render_probe::draws() == 0` against a real repository, confirmed to fail on the old behaviour before it was kept. |
||
|
|
0e35611284 |
fix(switcher): scrub a deleted remote workspace from the listing snapshots (#622)
Deleting a remote workspace removed its store entry but left the machine-listing snapshot every window keeps for the switcher untouched. That snapshot is merged into the panel every frame, deduped against the store — so with the store entry gone nothing held the row back, and the workspace the user just deleted popped straight back into the switcher as an adoptable machine row until the next reconnect replaced the snapshot. delete_workspace now captures the workspace's RemoteRef before removing the store entry and drops that machine workspace's row from every open window's snapshot. forget_workspace deliberately does not: forgetting keeps the machine's session, and re-discovering it from the listing is that flow's whole point (#485). One test, confirmed to fail without the scrub. It drives the real switcher_groups, so it covers the frame-time merge that resurrected the row, not just the snapshot bookkeeping. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
84f9d54ad6 |
fix(remote): finish the create a server update interrupted, and retire the note it answers
Creating a workspace on a machine whose server is the other side of a dialect bump took two creates and two update clicks in different places. Two holes in one flow: The create parked on the connect died with the refusal — finish_connect's Err arm dropped pending_create — so the update the refusal band offered ran to completion and then nothing happened: no workspace, and nothing left for any reconnect to finish. A create refused for the dialect now moves aside to parked_create, the replacement's success connects at the machine again, and whichever connect finally lands spends it, name and all. Dismissing the refusal or disconnecting the machine still calls the create off; every other failure does too. The mismatch note recorded during that failed attempt outlived the very replacement that answered it: the queue is only drained after a successful connect, so the next one raised "update this server?" about a server that was already updated — and confirming killed the fresh daemon all over again, sessions and all. reconnect_after_restart now retires the origin's notes the moment a restart or replacement lands. Three tests, each confirmed to fail without the change it guards. The end-to-end one drives finish_connect against a real control server over a socketpair, so the parked create is spent by the same code path a live reconnect uses. |
||
|
|
e781bfddc6 |
fix(workspace): let a new workspace keep the name it was given, and cover the abort #618 fixed (#619)
A name typed into the create form was sent as `WorkspaceRename` the moment the window switched — before the workspace existed on the machine to be renamed. The machine answered `NotFound`, `unsendable` logged it at debug and dropped it, and the create that ran afterwards named the workspace whatever codename it rolled. Nothing replays it: the next sync diffs tabs, not names. It flashed on screen first, because `fire_workspace_op` notes the op in the mirror before sending it, so the name appeared and then reverted. The name now travels with the create instead of chasing it. It is parked on the window's sync state by `name_new_workspace` and spent by whichever create runs — `pull_workspace`, which is the one `switch_workspace` actually reaches, and `pull_or_create`, which races it (both create when the tree they read did not hold the workspace yet, as the `Err` arm of `pull_workspace` already described). Both read it inside their spawned task rather than before it: a window orders its pull first and is named second, so anything read earlier is still empty. `settle_chosen_name` arbitrates against what the machine answers. A name it read back was spent by the create. One it did not means the create never ran — the workspace was already there — so it goes out as the rename it has become. A window that chose no name still reads whatever the machine says, which is what #604 fixed. Also covers the abort #618 fixed a commit ago. That fix is right and is left as it is; it landed without a test, and `tabs_on_screen` opening with `if !cx.has_global::<WindowRegistry>()` is why the whole suite passed over the read below it — no test installs a registry. The test here installs one, which is what `WindowRegistry::register` is no longer private for, and fails with an abort against the code as it stood before #618. Six tests, each confirmed to fail without the change it guards. Not verified end to end: there is no fake control client in the tree, so what `WorkspaceCreate` carries over the wire is covered by reasoning and unit tests only. |
||
|
|
8296161b4a |
fix(remote): give a dialect refusal a way out instead of a retry loop (#617)
* fix(remote): give a dialect refusal a way out instead of a retry loop A remote workspace whose server is the other side of a control-dialect bump reconnected forever: the strip quoted the protocol layer's own wording verbatim inside a localised sentence, offered Retry Now, and counted attempts at 30s intervals. Retrying cannot work — neither build changes between attempts — and the only Update Server button lived in the switcher's error band, which a window that opens straight onto the remote workspace never reaches. Park the link on a refusal and put the working action on the strip. Restart Server now routes to the replace flow when the far end speaks another dialect, because restarting was the wrong action there twice over: it killed any running tty7-server-* and then launched the path named after *this* build's dialect, which on such a machine does not exist. Installer::restart_daemon now probes that binary before killing anything and refuses when there is nothing to start. CONTROL_VERSION moves to 7 with no message change, so the refusal path can be exercised against the v6 servers already deployed. * fix(remote): recheck a parked link, and name a downgrade a downgrade Review follow-ups on the dialect-refusal parking. `is_dialect_refusal` was a substring sniff on the marker while every reader of a `true` went on to parse the whole shape. Two predicates for one question, and the weaker one decided whether to park a link that only a person could free. It is the parse now. A parked link never looked again. `RouteLost`, the state it was modelled on, is re-tested every tick and comes back by itself; this one could not, so a machine somebody else updated — or one that rebooted onto a build that does speak to us — sat there claiming to be broken for the rest of the session. It looks again every five minutes: a slow clock, deliberately two orders of magnitude off the reconnect one, and the strip says nothing while it does. `retry_now` cleared `last_error` for every caller, so pressing Retry Now on an unreachable machine cost the user the reason why until the next attempt finished. Only leaving a park clears it. The one button read Update Server in both directions, including the one where installing our server takes the far end back a version. That direction reads Replace Server, and the confirmation it opens offers the same word the button did rather than renaming the act between the click and the prompt. The switcher's band gates that button on `hosts_our_server` as well now, the way the workspace strip already did. `a_dialect_refusal_parks_the_link_instead_of_counting_attempts` passed without reaching what it named: an `Alias` resolves only if the machine running the tests has that name in its ssh config, and the pump drops an unresolvable route before it reaches any parking. It uses a target that always resolves now, and both new pump tests were checked against a mutation that removes the recheck. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |