mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
8c875d8be623efd2f502df665387b0c16e55da3d
1054
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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> |
||
|
|
1d0b648f66 |
fix(tree-sync): stop workspace switches from rereading the app mid-update (#618)
fmt |
||
|
|
d81dbc5e11 |
fix(i18n): delete unreachable zh arms and 534 lines of stale exemptions
Four arms in zh.rs were dead. A merge appended a second copy of CmdClearScrollback and of PanelMoreChangedFiles (singular plus its zero and one plural branches) below the arms that already matched those keys, so the later copy never ran. The compiler had been reporting all four as unreachable patterns; the warnings were buried among 19 others. Keep the version without backticks around `git diff`, which is what en.rs reads, and move the surviving other branch back next to zero and one. KEPT_IN_ENGLISH exempts keys whose translation is allowed to equal the English string. It had grown to 567 entries, 12 of them duplicated within the same array, while only 33 keys still read as English. The rest were added when the ja-JP locale was split out and every ja value was still a placeholder; the translations landed and the exemptions never left, so the untranslated-string guard had stopped guarding for over 500 keys. Rebuild the list from what the locales actually contain. The comments in that list had also come apart from the keys they explain, the flat block having been spliced through the middle of a group: "A language is named in its own language" sat above a run of SettingsSearch*Keywords rather than above the three language names. Every remaining key now sits under the comment that gives its reason. Verified the guard bites: reverting one formerly-exempt key to its English string fails the test, which it did not do before. |
||
|
|
b2d73ec68b |
feat(update): update an all-users Windows install through one UAC prompt (#562)
* fix(update): surface a failed install instead of silently re-prompting (#540) The GUI quits as soon as tty7-updater is spawned, so an install that failed inside the helper left a trace only in update.log — and because launching the helper had already cleared the prompt state, the next check offered the same version again, and again. The failure mode the user saw was an app that nagged about an update it could not install. The helper now writes update-outcome.json beside update.json on every terminal path it can still reach, and the next GUI launch folds it into the update state: a failure shows in Settings with the installer's own reason until dismissed and stops the version from re-prompting on its own; a success at the running version retires a failure an earlier attempt recorded. A leftover result that exists but cannot be parsed is reported rather than dropped — something ran, and "unreadable" is a result too. The same change moves the config directory off the environment and onto the command line (--config-dir). An elevated child process does not inherit the spawner's environment, so TTY7_CONFIG_DIR would have fallen back to the administrator's config directory exactly in the over-the-shoulder case — the groundwork this lays for #504. The updater re-exports the variable for the helper children it spawns itself, so the relaunched app keeps answering for the same config directory. * feat(update): update an all-users Windows install through one UAC prompt (#504) An Inno install under C:\Program Files could not be replaced in place: the updater ran the release Setup as the signed-in user, which either installed a second, per-user copy beside the real one or let Inno re-launch itself elevated — a bare UAC prompt for an unsigned executable in %TEMP%, seconds after the GUI had vanished. So the layout was refused outright and told to download by hand. It now updates itself, with the split the design in #504 settled on: one UAC prompt covering two privileged stages, and one watcher that is never elevated at all. - The GUI probes the *installed* updater for the new verbs by running it ("capabilities"), so a side-loaded or downgraded binary answers for itself instead of being trusted by version number. An updater that predates the verbs exits with a usage error, and the install falls back to pointing at the release page exactly as before — the first release carrying this still updates the old way, and the one after it updates itself. - The prompt dialog says the UAC prompt is coming before the app quits, and stops offering "Install on Next Launch": nobody is there to answer a prompt before the first window exists. The same guard keeps a staged plan from being armed for the next launch, and apply_pending_at_launch leaves an elevation-needing plan staged rather than raising a windowless prompt at boot. - "Install now" spawns the watcher first (medium integrity, the signed-in user's token, so the relaunched app is never elevated), then ShellExecuteEx "runas" on the installed updater — the trust root a medium-integrity process cannot rewrite. Everything the elevated half needs crosses as command-line arguments, because an over-the-shoulder child inherits neither the environment nor the user's profile. The package's expected SHA-256 crosses the same way, from the checksums the GUI already holds in memory, so a payload and its checksums file cannot be rewritten together behind the IL boundary. - The privileged first stage re-verifies the payload against that digest, pins its helper byte-for-byte to the installed updater, stages both in a fresh administrator-only %ProgramData% directory (an explicit SDDL DACL, swept of stale directories first), and only then runs the install stage — which runs Setup silently, writes the outcome file, and never touches the app binary itself. The watcher follows the chain through the status file and pid liveness (ERROR_ACCESS_DENIED from OpenProcess still means "alive" across accounts), then relaunches the app de-elevated and probes that it actually came up. - Declining the UAC prompt is not an error: the watcher is reaped, nothing ran elevated, and the staged package simply waits in Settings. Persisted plans from before this protocol serde-default a plan version that is_usable rejects, so a stale plan is discarded instead of failing against a helper that would not understand its arguments. The installer script's explorer-menu registration gains skipifsilent: a silent run *is* this update path, and launching the app there would write the menu into the administrator's hive under over-the-shoulder elevation. One note on the test suite: ui::remote_connect's a_routed_auth_prompt_carries_the_machine_that_raised_it fails under parallel test execution on this machine both with and without this change — a pre-existing flake, unrelated. * fix(update): run the UAC request off the UI thread Real-machine verification of the elevated chain caught this on the first click: ShellExecuteExW pumps the calling thread's message loop while the shell raises the consent prompt (its change notifications re-enter the window), and from the UI thread that re-enters gpui with its App already borrowed — the process aborts on a RefCell double-borrow before anything ever elevates. The launch — watcher spawn included, so the pairing stays atomic — now runs on the background executor, and only the bookkeeping (quit / decline / failure) comes back to the UI thread. * fix(update): throttle a failed version instead of retiring it (#540) Per the review on #540: a failed install must not keep the version retired via last_prompted — record last_prompted plus a fresh remind_after deadline (the same three days "Later" uses), so the version asks again once the reminder expires. should_prompt already treats "last_prompted matches, reminder expired" as prompt-again, so no logic change is needed there, and the pinned a_failure_lets_the_version_prompt_again test still holds. Also write update-outcome.json *before* relaunching the previous app on the macOS/Windows/portable non-elevated paths: the GUI that comes up next is exactly the process that absorbs the outcome, and it used to be relaunched before the failure existed on disk. The elevated chain is unchanged — its watcher already waited for the file. * fix(update): let only the elevated updater's own image name the trust root Three holes on the privileged side of the #504 chain, all of the same shape: a value that decides what runs elevated was taken from the medium-integrity caller. - `elevated-stage` pinned the staged helper against `<install-dir>\tty7-updater.exe`, where `<install-dir>` is a command-line argument. Both halves of that comparison were the caller's to choose: name a directory holding two copies of any binary and the pin passes, then stage 2 runs it elevated. The stage now derives the installation from its own image — UAC pointed the prompt at `{app}\tty7-updater.exe`, so `current_exe` is the one path nothing below the boundary could have written — and passes that on to stage 2. A caller that named a different directory only gets a line in the log. - The staging directory's DACL let no standard user in, but its parent did: `%ProgramData%` grants Users the right to create directories, and the creator owns what it creates. A pre-created `%ProgramData%\tty7` gave its owner delete-child over the administrator-only staging inside it — enough to rename the verified staging aside and drop an identical name of their own into the gap between the digest check and the execute. The root is now created with the same protected descriptor, taking down whatever holds the name first; `CreateDirectoryW` applies a descriptor only when it is the one creating the directory, so succeeding is the proof. The per-run sweep goes with it — the root's removal takes the leftovers. - The GUI aimed the prompt at the updater the *plan* named, and `update.json` sits in the user's config directory. It now aims at the installation this process runs from, so the binary the prompt names is the binary that starts. Also quote the elevated command line the way `CommandLineToArgvW` reads it back: a backslash escapes only in front of a quote, so a config directory ending in one used to escape its own closing quote and swallow every argument after it, `--result-file` — the file the watcher waits on — included. * test(update): pin the elevated stage's trust root to its own image A regression test for the shape of the hole rather than the hole: if `installed_root` ever goes back to reading an argument, the pin the elevated stage runs before executing the staged helper stops meaning anything, and nothing else in the suite would notice. * fix(update): bring tty7 back when the elevated chain never reports The watcher's two timeouts returned without relaunching. Every other way out of the chain ends with the app back on screen, but a stage 1 that died before writing its status or its outcome — killed, crashed, an AppInfo service that never delivered it — left the user with the GUI already quit, nothing to replace it, and nothing said. Same for an install still running an hour later. Both paths now end the way the others do: an outcome the watcher wrote itself, then the relaunch. The synthesized outcome is written whether or not the relaunch succeeds, which also closes the same gap on the pre-existing "the elevated updater exited without recording a result" path — the next launch can name what happened instead of silently offering the version again. What kept those paths from relaunching was the risk of a second window beside a GUI that is still up: a declined prompt leaves this process running, and the kill that reaps its watcher can lose. The watcher now takes the GUI's pid and opens a handle to it at startup — while the GUI is provably alive, since it is sitting in ShellExecuteExW waiting on the prompt — so the number cannot be recycled out from under it. Before relaunching, a GUI that is still alive is waited out for 30 seconds: one that is quitting (a chain that failed fast can beat it out the door) is gone well inside that and gets its relaunch, one that is staying is recognized as staying and gets neither a relaunch nor a failure record it did not earn. A live process always answers to its own pid, so the check cannot be wrong in the direction that double-launches. Also give the Japanese elevation notice its closing 。 * fix(update): poll the parent out across the elevation account boundary Under an over-the-shoulder elevation the install stage runs as the administrator, and OpenProcess on the signed-in user's GUI answers ERROR_ACCESS_DENIED - the same boundary pid_alive already documents from the watcher's side. wait_for_exit treated that as a fatal error, so the chain recovered and reported a failure before Setup ever ran. The wait now degrades to polling the pid until it stops answering, bounded so a recycled pid cannot hold the install hostage forever. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Co-authored-by: l0ng-ai <l0ng-ai@users.noreply.github.com> |
||
|
|
2b6eab5f4a |
fix(sidebar): give the workspace head a width of its own
The workspace tile is a chain of percentages — `w_full` on the button, on the div wrapping it, and on the row inside it — and a percentage is only a width while every box above it has one. The row holding the tile declared none: it borrowed the rail's by cross-axis stretch. On a pass that sizes the column from its content the chain has nothing to resolve against, the button falls back to its `px_1` padding, and the tile ends up hugging the workspace name in a rail several times its width. The row now declares the width it was borrowing, which anchors the chain to the rail itself — that is a fixed `w(px(width))`, so every link below it resolves. |
||
|
|
a2d53a9597 |
fix: 19 项低危 UX 问题(#584–#602) (#615)
* fix(scm): say what "discard all" actually discards (#594) The group-level Discard prompt asked to "discard every change in this repository", but discard_all_ops has only ever swept unstaged edits and untracked files — staged changes survive, as the function's own comment notes. Users confirmed under one belief and the code kept another. Narrow the prompt to the operation's real footprint, in all three languages. * fix(scm): keep the amend toggle when its confirmation is cancelled (#595) scm_commit cleared scm.amend when Commit was pressed, before the "rewrite the last commit?" prompt. Answering Cancel returned to a panel whose amend mode had silently been dropped, so the next Commit created a brand-new commit — exactly what the user had just declined to risk. The toggle now clears where scm.committing arms, at dispatch in run_git_op, extending the rule the armed flag already followed: a cancelled confirmation leaves nothing behind. * fix(cli): answer a wait timeout in the success path's JSON shape (#589) The 124 branch returned {pane,status,timed_out} while a finished wait returns {pane,status,matched,stale,activity,message,session_id} — so the one branch a consumer writes error handling for was the one missing its fields. The timeout now carries the full shape plus timed_out, and reference.mdx documents the schema and the flag. * fix(cli): report a failed wait on stderr, even under -q (#590) wait's failures are structured exits (124, or 1 when the pane died first), so they never passed through the anyhow path whose eprintln is the only thing quiet mode cannot silence — contradicting the documented "errors still go to stderr". Both exits now print their headline to stderr, the discipline pane close already established. * docs(cli): describe owner as the workspace that may attach (#591) commands.md still claimed the CLI stamps a literal "tty7-cli" owner on the panes it spawns — the behaviour the orphan-workspaces work removed, because an owner names the workspace allowed to attach and a stranger's stamp got the panes respawned. Every spawn path now writes the workspace id, or nothing while the pane is still unfiled. Bring commands.md in line with reference.mdx, and note the absent case in both. * docs(cli): close five contract drifts between the tables and the code (#592) - The key tables listed pgup/pgdn as aliases but not pgdown, which the parser has always taken; both references name it now. - "Case-insensitive" was flat wrong for Alt: M-x keeps its case because Alt is a prefixed ESC, unlike Ctrl. Both references note the exception. - procs' ports JSON has carried addr since the field exists; both schemas show it. - TTY7_WS is tab ls's default too; both environment tables say so. - split --ratio's clamp to [0.05, 0.95] was discoverable only in code; both split sections document it. * fix(cli): doctor exits 1 when the server is unreachable (#592) doctor is the verb people run when something is not working, so an unreachable server is *the* finding — not a row to exit 0 over while `tty7 doctor || alert` never fires. The table and JSON still go out (the context rows are the other half of what doctor is for), and stderr carries the headline under -q. MockBackend grows an `unreachable` flag so the branch is testable; no Status/Routes round-trips happen once hello has failed. * fix(settings): refuse a Start-in path that names no directory (#601) The custom path was stored unchecked, and the daemon's picker then skipped it — not a directory — so every new pane silently started in the fallback directory and the typo read as a tty7 bug. Settings now marks the field red and refuses to save, the proxy row's pattern (#551), with the red line and the commit gated on one shared predicate so they can never disagree; a hand-edited config.json holding such a path gets a log::warn! naming it at the moment the fallback engages. * fix(terminal): rescan search highlights when the pane's width changes (#586) A match point is an absolute (line, column) against the width it was scanned at, so a column change reflows the text out from under every highlight. Output rescans them (Wakeup → refresh), but a quiet local pane has no output coming and the drift outlasted the resize indefinitely. set_grid_size now rescans on a column change with the output path's discipline — selection and scroll untouched — and takes the Context it needs to do so; a rows-only change reflows nothing and stays cheap. * fix(terminal): keep the grid selection when the search bar opens and closes (#584) The selection that seeds the query is the thing being searched for, yet opening the bar ran recompute_matches' unconditional clear — right for its other callers, where the user *changed* the query and the old selection names nothing — and closing cleared it again, so select → Ctrl+F → Esc lost the selection every time. The seeded selection is now restored after the opening scan, and close_search no longer clears; a query the user actually changed still retires the stale selection, the discipline refresh_matches_after_output already stated. * fix(tabs): a zoomed pane stays zoomed across a tab switch (#599) Zoom was a window-level value that activate() cleared unconditionally, so looking at another tab and coming back restored the split layout — while a zoom is a tab's temporary view state, like its focused pane. It now rides with the Tab: activate stashes the outgoing tab's zoom and brings the incoming tab's back. The clears that genuinely reshape the layout (drag, split, close) still stand, and a stashed zoom whose pane exited while the tab was away is validated away rather than restored. * fix(tabs): track an open rename box by tree id, not index (#598) The rename box held only an index, which drifts the moment any other tab closes or the strip reorders — so close_tab_inner and apply_tab_order threw the half-typed name away on any unrelated tab event, and a reorder mid-rename still left a window where the commit landed on whichever tab had taken the index over. The box now names its tab by tree id end to end (start, render match, commit): only closing the renaming tab itself ends the rename, and the name lands on the tab the box was opened on wherever it has since moved. * fix(i18n): move seven hard-coded user-facing strings into the language tables (#602) Seven spots rendered English no matter which UI language was set: the shell-integration notice that explains why a wrapper was blocked or never engaged, the titles a pane wears once its process exits or the server loses it, the loopback forward's failure line, the tray tooltip that lists running agents (whose separator also wanted a CJK enumeration comma), the cursor-shape choices in settings, the command palette's empty-result hint, and the updater's install hint. Each is a L10nKey now with en/zh/ja entries, so the parity guard keeps them translated from here on. The palette's empty state was also wrong in content, not just language: every menu suggested connecting over SSH when nothing matched, including menus that have no hosts in them. The hint now only appears in the quick-connect menu; everywhere else the palette suggests a different search instead. Verified on Linux: the title/palette/tray suites (48 tests) and the i18n parity guard all pass. * fix(terminal): show remote path completion is listing, and say when it fails (#585) Tab-completing a path on a remote workspace had two silences. The whole network round-trip painted nothing, so a slow link read as a broken Tab key; and a listing that failed was unwrapped into an empty candidate list, so "the directory is empty" and "the listing never happened" ended in the same nothing. A pill over the pane's bottom-right corner — the style the integration notice already uses, factored out — now says the listing is running from the moment it starts, and a failed listing sets a notice with its error instead of the empty vector. The failure pill stays until the next keystroke dismisses it, and the trailing notify after an empty listing closes the menu brings the "listing…" pill down with it. Verified on Linux: the new gpui test covers the idle/listing/failed states, and the neighbouring completion tests still pass. * fix(files): quote cd Here / Insert Path for the shell the pane runs (#593) Both file-tree actions wrapped a path with spaces in POSIX single quotes whatever the focused pane's shell was. In cmd.exe a single quote is an ordinary character, so `cd 'C:\Users\me\My Documents'` split at the first space and cmd complained about 'C:\Users\me\My' — while the same action was fine in PowerShell and bash, which is why only cmd users ever saw it. shell_quote_for takes the pane's shell program (the pane already knows it — the settings page lists it) and picks double quotes for cmd.exe, single quotes for everything else; an unknown shell keeps the POSIX form, and a path that needs no quoting stays bare either way. Windows paths cannot contain a double quote, so the cmd form has nothing to escape. * fix(cli): pane close fails for a pane the registry does not hold (#588) `tty7 pane close %99` printed {"closed":[99]} and exited 0 for a pane that never existed. The workspace path cannot drift this way — PaneClose answers — but an orphan has no workspace to route through, so close hangs it up directly, and that kill is fire-and-forget: the daemon never says whether it knew the pane, so Ok(()) only ever meant the bytes reached the socket. A reaper script chasing the orphans `pane ls --all` points at would read the ghost success as cleanup done. The direct path now reads the running-pane registry once per batch and refuses ids it does not hold: the miss lands in `failed` with exit 1, next to the failures kill itself can report. A pane that exits between the listing and the kill is gone either way, which is what closing it wanted, so that race still reports closed. * fix(session): a launch that leaves workspaces running says so (#597) Quitting with several windows open and starting again restored only the most recent one; every other open window was marked detached — panes alive, nothing on screen, the only trace a "left N detached" log line. The workspaces were reachable from the sidebar, but nothing said they existed, so they were easy to forget entirely. restore_one now returns how many windows it detached, and both launch paths (normal startup and the CLI-driven open) push an in-app notification into the restored window naming the count and where to reopen them. The count rides the return value rather than firing the notification inside the store, because the store has no window to notify in — and a launch that detaches nothing, like the reattach-the-last- closed case, stays silent. * fix(switcher): list the local machine's orphan panes, with a way to close them (#596) A pane whose workspace went away — an interrupted `tty7 run`, a forgotten workspace that kept its shells — was invisible everywhere in the GUI: not in the sidebar, not in the switcher, not in the tray. It kept its process and its memory, and the only way to even learn it existed was the CLI's `tty7 pane ls --all`, which a GUI-only user never runs. The switcher's local machine group now carries a "Background panes" block under its workspace rows: one line per live pane the daemon's registry holds and no workspace does — id, owner, cwd — each with a Close button. The listing is the same PaneClient::list the CLI's reaper reads, fetched off the UI thread when the panel opens; closing kills and then re-lists, so a pane that survived simply stays on the list instead of pretending to be gone. The block steps out of the way while the search field holds a query, which narrows the panel to workspaces. Local on purpose: a remote machine's orphans belong to its own daemon, and routing a listing per host is what the CLI reaper is already for. The block joins no keyboard navigation — the panes are not workspaces and the arrows have no business landing on them. * fix(updater): keep Inno's progress window on screen during the install (#600) The Windows installer ran /VERYSILENT, so from the app quitting for the update to the watcher bringing the new build up — tens of seconds, longer under an antivirus scan — the screen held nothing at all: no window, no progress, no tray note. "Clicked update, the app vanished" reads as a crash, and double-clicking the icon does nothing while the files are being replaced. The installer now runs /SILENT instead. Nothing about the flow becomes interactive — /SP-, /SUPPRESSMSGBOXES, /NORESTART and /CLOSEAPPLICATIONS are untouched — but Inno's own progress window stays on screen for the gap, which is exactly the span the user had no word about. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
fd0a61b9d6 |
fix(tab-strip): ring the status dot in white on dark themes
The badge halo was drawn in the surface colour, which only reads as a ring while the surface is light. On a dark theme it went near-black and looked like a notch bitten out of the avatar instead of a badge sitting on it. Detect a dark surface and give it the same white edge — and the same white hole for the hollow Waiting dot — that light themes get. |
||
|
|
3c0a700907 |
feat(switcher): flat workspace list, create form, connect-time workspace sync (#616)
* feat(switcher): flatten the workspace list, add a create form, sync remote listings on connect The switcher's left column is now one flat most-recently-used list across all machines, each row carrying its machine and link state; the per-machine tree, headers, and the Other Machines band are gone. Machine trouble (install progress, connect errors, parked routes) moves to contextual banners under the list, and machine verbs move into each row's menu. Cmd+Shift+N now opens a create form instead of silently swapping the workspace: a name prefilled with the usual generated codename, and a host combobox (searchable dropdown) defaulting to this computer. Creating on a machine with no live link connects first and completes when the link is up. Connecting to a machine also mirrors its workspace listing into the local store, so its workspaces survive a restart without a connection. Mirrored references are marked synced: launch restore skips them, and their clock follows the machine only until this client opens them. * fix(switcher): drop a parked create when its machine's connect is called off Disconnect clears the in-flight connect, so finish_connect never runs and the PendingCreate outlived the intent behind it: the next successful connect to that machine would have silently created a workspace nobody was waiting for anymore. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
8b42905fca |
fix(terminal): stop menu click-through and surface failed file opens (#541, #542) (#575)
* fix(terminal): stop terminal pop-up menus leaking clicks into the grid (#541) The completion menu and the reverse-search menu (the floating panel and the input-bar row alike) carry no click handlers of their own, and in gpui an element without handlers, cursor or occlude inserts no hitbox — the same rule the app pins with a test pair in app.rs. A press that missed every row therefore fell straight through to the live grid: it moved the cursor and cleared or started a selection there, and a modified click even opened whatever link happened to sit under the menu, so the menu read as broken while the damage landed elsewhere. All three menu roots now occlude, the same remedy the terminal search bar already uses, so a click on menu background is swallowed where it lands. Making the individual candidates clickable instead is a separate feature, not part of this fix. * fix(terminal): toast a file link that fails to open instead of dying silently (#542) The external half of open_file_link has no failure channel: a misspelled link_file_command or a missing xdg-open only produces a log::warn, and the click reads as a dead link — while the path was only ever underlined because the pane's own host verified it exists, so "nothing happens" is the worst possible answer. The URL half at least toasts a failed loopback forward; the built-in editor arm reports downstream of OpenFileRequested; the two spawn arms had nothing. open_file_path and run_file_command now return io::Result, and open_file_link turns an Err into the same kind of notification a failed image paste raises, naming the path and the error. The file tree's directory fallback — the one other caller, handing a path to the OS association — gets the same toast instead of silence. A template whose tokens all expand to nothing (a lone {line} on a link with no line number — a blank template never gets this far, sanitize maps it to None) reports as an InvalidInput config error rather than a silent no-op. Spawn is still all that is reported: an opener that spawns fine and then exits non-zero is nobody's to see, and a test pins both error paths. * test(terminal): pin the press a pop-up menu has to swallow (#541) The menus occlude now, but nothing held them to it: a bare div over the grid renders the same and only the mouse can tell the difference. This presses on a history row and asks the grid whether it started selecting, then takes the menu away and presses again — the second half is what keeps the first from passing on a pane the mouse never reached. * docs(changelog): say what a leaked press actually did (#541) A press on a menu never moved the terminal cursor and the menus have no buttons to miss; what it did was clear the selection, drag out a new one, underline the text under the row on hover, and open the link beneath it on Ctrl+click. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
3cd90c6ca6 |
feat(ssh): name panes after their host, reach the host form from where you are, and test a connection (#566)
* feat(ssh): name panes after their host and reach the host form from where you are Five gaps in the SSH flow reported in #438, and the two silent no-ops around them. Panes now carry a display name: a saved host's own name, or its address when nobody named it — every host imported from ~/.ssh/config arrives nameless, and each one opened a tab reading "tty7". The name survives a title reset and a dropped link, and `strip_host_prefix` no longer cuts `deploy@10.0.0.5:2222` down to "2222" on its way to the tab strip. The host form is now reachable from the machine in front of you: the switcher's machine menu edits the host it is showing, or offers to save one for a machine reached by address or by ~/.ssh/config alias. A live connection dialled by hand can be kept as a host from the palette, with everything it was dialled with carried over — the one thing that cannot come along is an ad-hoc jump hop, and that is said out loud rather than saved broken. The New Tab menu lists the saved hosts, most-used first, and the typed address route that already understood -p, -J and config aliases. Both were behind the workspace switcher, its host dialog and the settings list. Finally, the three proxy fields are exclusive — map_proxy picks the first one filled and ignores the rest — so the ones that lose now say which field won instead of leaving it to be discovered by connecting. * feat(ssh): test a connection from the host form, and pick a host by name Three things the host form and the New Tab menu were missing. The form gets a Test button. It hands the spec to the daemon, which dials it the way Connect would — proxy, jump host, host key, auth — and drops the connection again, reporting how long it took. A test never rides an existing connection: one would answer for the credentials that connection was made with, so a password typed wrong would come back green. Anything the handshake stops to ask a person is declined on the spot and reported as what it asked for, since a form is nowhere to answer a password prompt and waiting out the two-minute prompt timeout would be a worse answer than "it got that far and wants your password". The result clears the moment a field changes: vouching for a host that was edited since is worse than saying nothing. The Auth row becomes a dropdown. Six methods is more than a segmented control can label without squeezing, and it is the row that stacks first on a narrow page. The New Tab menu scrolls — PopupMenu only does that past 20 items, and every shell on the machine plus a handful of hosts already runs off a short window — and past the five hosts it lists, Find a Host opens a filter box over all of them. * feat(new-tab): put a search box on the New Tab menu The menu is as long as the machine is — nine shells here, and a ~/.ssh/config with two dozen hosts in it is ordinary — so it needed filtering, not a scrollbar and a row leading somewhere else. A PopupMenu cannot hold a text field: it claims the keyboard for its own navigation, and there is no search input anywhere in it. So the New Tab button now opens a popover holding the same searchable list the command palette is built from, with the shells and the hosts under their own headings and one box over both. Typing filters across both groups; typing an address offers to connect to it, the way the palette does. The standalone host picker this replaces is gone with it, and the row that led there — one search reachable from the button beats two behind a menu. * revert(new-tab): put the New Tab menu back to shells only The searchable popover was the wrong shape for a button in the chrome: too big and too heavy next to the tab strip it hangs off. The menu is the plain shell list it was before this branch — byte for byte, so nothing about it needs re-reviewing — and the hosts, the search box and the row leading to a host picker are gone with it. Everything that came along to serve it goes too: the positional NewTabWithShell command, the picker's palette delegate and its compact row metrics, the standalone host palette, and the four strings they needed. Connecting to a saved host is the palette's job again, which is where it was and where it works. * fix(switcher): size and weight the machine glyphs like the icons beside them The two machine icons are drawn by hand; every other glyph in that gutter comes from lucide. Ours were built to a tighter box — ink 19.3 × 17.3 of a 24 grid against lucide's 22 × 20 — so at the same nominal 16pt the local machine rendered 11.5pt of ink beside a 14.7pt globe and read as a size smaller. Both are redrawn to lucide's extents, which also makes them agree with each other. The local machine's glyph was muted while every remote one was full strength, and while its own name was full strength either way. Beside the machine under it that read as a disabled row rather than as the computer you are sitting at. One weight for all of them now; the "Other Machines" globe keeps its dimmer register, which belongs to the muted section label it sits next to. * fix(tabs): only a port stops the host head being cut off a title Teaching `strip_host_prefix` that `deploy@10.0.0.5:2222` is an address and not a titled directory was done by requiring the tail to start with `/` or `~`. Two very common titles do neither. Debian's stock bash title is `\u@\h: \w` — a space after the colon — so `user@host: ~/work` would have stopped being cut at all, and every one of those tabs would have gone from reading `~/work` to `user@host: ~/wo…`. And tty7's own PowerShell integration writes `ann@BOX:C:/src` whenever the cwd is off the home drive, which would have read `ann@BOX:C:/src` rather than `C:/src`. Key on the port instead, which is the thing that actually makes the string an address: a tail of nothing but digits is kept whole, and everything else is the path it always was. The space belongs to the head, so the tail is trimmed on the way out. * fix(ssh): keep a connection test off the cache, off a stale form, and clear about a changed host key Three ways the new Test could answer for something other than the host in front of it. It dialled with `reuse: false` but still took the connection cache's slot lock, which is held for the whole handshake. So a test stalled every Connect to the same host behind a connection it was never going to leave them — and, queued behind a session already dialling, spent its own budget waiting and came back "connection timed out" about a host that answers fine. A test that is not going to touch the cache has no business locking it: it now skips the slot entirely, and only a reusing dial takes the guard it later fills in. The form dropped a test result whenever a typed field changed, on the grounds that the answer was about the host as it was a moment ago — but the authentication method is a dropdown, and changing it left the green line standing under a handshake the form would no longer make. And a host key that has *changed* was reported with the same words as one nobody has accepted yet. Those are not the same news: the first is a new host, the second is the server presenting a different key than the one on file. `SshTestNeed` now tells them apart and each gets its own line, in all three locales. Verified against a live sshd on localhost: refused port and unresolvable name come back in milliseconds with the connect path's own message, a password host comes back `NeedsInput { Password }` in 55 ms rather than waiting out the two-minute prompt timeout, and two tests of the same host back to back no longer serialize. * chore(palette): drop the root flag the New Tab revert left behind `grouped_root` was split out of `quick_connect_root` for the searchable host picker on the New Tab menu, which was taken back out again. Every constructor now sets the two to the same value, so the second one is a field and a doc comment describing a list that does not exist. * docs(changelog): record the SSH host form, pane names and connection test Every user-facing change in this branch: panes named after their host, the host form reached from the switcher and the palette, and Test. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
f237cf5c48 |
fix(scm): answer a branchless push and name why a commit is refused (#545, #546) (#576)
* fix(scm): answer a push that has no branch to move (#545) A click on Push at a detached HEAD died in scm_push's let-else without a word, and it was not the only path that did: the key binding, the palette entry and the follow-up half of "Commit and Push" — which lands in scm_push after the commit has already succeeded — all share that guard, so a compound verb read as pushed when only the commit happened. git_data.rs says out loud why that cannot stand: a swallowed click on Push looks exactly like a push that finished instantly, which is why the busy slot toasts ScmNetworkBusy instead of dropping the click. The sync tile made it worse by promising "Publish Branch" — upstream is None at a detached HEAD by definition, and publishing is the one thing it cannot do. The tile and the branch menu's Push item now disable themselves with a tooltip that says why (Fetch works from any HEAD and Pull already fails loud out of git's own error, so they stay), and the guard itself toasts the reason for every path that can still reach it. The guard also swallowed HeadState::Unborn — a branch with a name but no commits yet — which now gets its own answer rather than the detached one. Both dead ends are decided in one pure helper, pushable_branch, so the tile, the menu and the toast cannot drift apart, with a test pinning what each head state says. * fix(scm): name the real reason a commit is refused (#546) Committing with staged work but a blank message was answered with "Nothing to commit" whatever the actual blocker, because scm_commit hard-coded that one key for every disabled plan. The panel's own button gets away with a shared tooltip because it is disabled and the reason shows on hover; the palette entry and the key binding have nothing to hover, so their toast was the whole feedback — and it pointed at the index when the message box was the problem, sending the user staging files they already staged. The toast now carries the commit plan's own reason, the same key the button's tooltip uses, and the commit_plan test pins the split: whitespace-only message on staged work is ScmCommitNeedsMessage, a clean tree is ScmNothingToCommit. * fix(scm): ask pushable_branch for the tile and the menu too The helper was introduced so the tile, the branch menu and the toast could not answer differently about the same HEAD, but the first two were still deciding off their own `detached` check — so an unborn branch, which the helper already answers with "no commits to push yet", kept a live sync tile promising "Publish Branch" and a live Push item, and learned the truth only from the toast after the click. Both now ask the same helper, and the tooltip carries whichever reason it gives back. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
bed3c856ed |
fix(panel): redraw the right panel's tab icons and put the tree glyph on the tile ladder (#578)
* fix(panel): redraw the right panel's three tab icons The tiles render at 13px (`TILE_GLYPH`), and at that size the set had three problems, none of them about style: - `info` and `folder-closed` were both a rounded body split by a horizontal rule at nearly the same proportions, so the first and third tile could only be told apart by a notch a pixel and a half wide. - `git-branch` drew its three nodes as filled r=2.5 discs — 2.7px across at render size — which buried the 2.1 strokes between them and left the glyph reading as three dots. - The interior detail was too dense to survive: `info` carried two rules 4.8 units apart and `folder-closed` a divider 3.2 units below its front edge, both under 2.6px once rendered. Same three metaphors, same 2.1 pen, same rounded joins — only the geometry moves. The nodes go hollow so the branch is drawn with one pen rather than two. The crowded interior rules go, which leaves the folder a clean body and makes the notch the thing that separates it from `info`, rather than a second rule at a slightly different height. And each mark grows into the height `panel-right` already occupies beside them: the three had the width of that box and sat two units short of its height, which on a row of tiles reads as three smaller icons rather than as three different ones. Measured as rendered ink, stroke included, on the 24 grid: | glyph | before | after | | ----------------------------------- | ----------- | ----------- | | `panel-right` (unchanged reference) | 19.3 x 19.3 | — | | `info` | 19.3 x 17.3 | 19.3 x 18.1 | | `folder-closed` | 19.3 x 17.3 | 19.3 x 17.7 | | `git-branch` | 18.5 x 17.3 | 18.3 x 18.9 | All three stay centred on 12, 12. * fix(files): size the tree row's glyph off the tile ladder, not the rem one Glyphs in this window come from the pixel ladder in `app.rs` — `TILE_GLYPH` 13 for a chrome tile, `TILE_GLYPH_XS` 11 for the tiles a row reveals on hover — while text comes from the rem ramp. The file tree's leading folder/file mark was the one place drawing its size from gpui-component's rem sizes instead, and those two systems cannot meet: at the default `ui_font_size` of 16 the rem ladder offers `xsmall` 12 and `small` 14 and nothing in between, so the mark was always a step under the tab tiles above it — a speck beside a 14px name — or a step over them, which is a row of content outsizing the navigation that owns it. `ROW_GLYPH` is that size named once, in pixels, next to the panel's type ramp and defined as the tab tile's own glyph, so the two now agree by construction rather than by coincidence. * fix(icons): put the tree's collapsed folder on the set's box `folder.svg` is the one folder the set never squared up. It inks 19.9 x 16.1 where every other mark in the row is 19.3 wide, because its top edge overshoots the box by 0.6 on the right — which also leaves the whole glyph 0.3 off-centre that way, and 0.2 low. Nothing showed while the tree drew it a step under the chrome. It is on `ROW_GLYPH` now, the same 13px as the Files tab tile above it, and it swaps in place with `folder-open` — already on the box — every time a directory opens or closes, so the overhang is a shift in a fixed column rather than a number in a file. Same folder, same 2.8 corners, same 1.7 shoulder: the top edge gives back the 0.6 it overshot and the body takes the 1.2 of height that puts it on 19.3 x 17.3, centred on 12, 12, which is where `folder-open` and the rest of the row already are. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
3c5f14f71c |
fix(icons): draw the plus on the same optical bound as the rest of the set (#577)
* fix(icons): draw the plus on the same optical bound as the rest of the set Every icon tty7 draws itself sits on one optical bound: 3.4..20.6 of a 24 viewBox, 19.3 units of ink once the round caps are counted. `plus` was the exception at 6..18, 14.1 units — 59% of the box where the rest fill 77-80%, and tighter even than the stock lucide glyph it replaced. That was compensated at the call site instead, by handing the three chrome tiles that carry a `+` a 16px glyph (`TILE_GLYPH_LINE`) where every other tile in the same row gets 13. It worked where it was applied and nowhere else: the port-forward section's `+` sits in a 24px tile, which has no `_LINE` step to grow into, so it rendered at 7.6px of ink against the 10.5px of the icons around it. Redraw the asset onto the bound and the compensation is no longer needed anywhere — the three call sites go back to plain `chrome_tile`, and the 24px tile is fixed for free. Scaling a glyph up buys stroke as well as extent, which is the part that is easy to miss: at 16px the old art also drew 16/13 wider, so moving the `+` to 13px thinned it by a fifth even as it got longer. A cross is two hairlines with no fill to hide behind, so the weight had to come back in the art's own `stroke-width`, and the value is eyeballed rather than derived — 1.0 CSS px reads thin against the closed shapes beside it, 2.0 reads heavy-handed, and 1.75 is where it settled after walking the range on a 2x screen. `TILE_GLYPH_LINE` stays for stock lucide `close`, which has the same tight geometry and is not ours to redraw. * fix(icons): keep the plus's extra weight to the 16/13 the call site was buying The redraw put `plus` on the family's optical bound and then took its `stroke-width` to 3.2308 — 1.75 CSS px at a 13px glyph, settled by eye against `panel-left`, `panel-right` and `ellipsis` in the titlebar row. Those are the three call sites the redraw was checked at, and they are the three whose neighbours are closed shapes carrying solid fills. The other three are not: - Source Control's row strip draws `plus` (Stage) directly beside stock `undo-2` (Discard) at an 11px glyph, and pairs it with stock `minus` (Unstage) one group up. Both stock glyphs stroke 2. At 3.2308 the `+` came out 1.48 CSS px against their 0.92 — the stage button reads as the emphasised one in a strip where nothing is emphasised, and a stage/unstage pair stops looking like a pair. - The switcher's Add SSH Host row draws it at 16px in the same gutter column as `search`, which is ours and strokes 2.1. - Settings → SSH puts it next to stock `search` and stock `ellipsis`. Reading a size heavy is the same failure as reading a size small, so the weight goes to the one value that is derived rather than chosen: 2.1 × 16/13 = 2.5846, the family's own stroke times the scale-up the three chrome tiles were already applying. At `TILE_GLYPH` that renders at 1.4 CSS px, which is exactly what the titlebar `+` has always drawn at — so nothing that was verified on screen moves, and the three tiles that never had a `_LINE` step to grow into stop being overdrawn. The arm ends move to 4.9846..19.0154 to keep the round-cap tips on whole pixels at the new weight; the ink extent is unchanged at 16.6. The test now derives the weight from `panel-left`'s own `stroke-width` instead of carrying an eyeballed band, and checks both arms rather than the vertical one — a cross edited on one axis passed before. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
70ac7f201f |
fix(tree-sync): tell a window the name the machine gave the workspace it made (#604) (#613)
A workspace a window creates is created with a generated name, and that name is what `tty7 ws ls` prints and what `tty7 ws rename` addresses. The window itself was never told it. A client is left out of the deltas its own ops raise, so the WorkspaceCreated delta carrying the name never comes back, and both create paths threw away the copy the reply carried: `pull_or_create` reduced the answered workspace to its tabs, and `pull_workspace` returned the tree it had read before the create, which does not hold the workspace at all. The mirror therefore held the workspace unnamed, the chip fell back to the directory its shells started in, and the GUI and the CLI gave two different answers to what the workspace was called — a user could read `verify-main` on screen and not address it by that name. The first pull of the whole tree, which a daemon restart, a rebuild and a plain relaunch all do, then produced the name it had had all along and looked like a rename that stuck. Both paths now carry the name they were answered with: the prime hands it to the mirror, and the hydrating create puts the workspace it made into the tree about to be installed. A workspace the machine really has no name for still reads the directory it is working in, and a chosen name still wins. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
60ef3fb758 |
fix(shell): name the user, host and home in a Unix pwsh pane's title (#611)
The PowerShell integration read its identity from USERNAME, COMPUTERNAME and USERPROFILE. Those three spellings exist only on Windows; on macOS and Linux they come back empty, so every pwsh pane there was titled `@:` followed by a full un-abbreviated path, and the `~` shortening never fired at all. Read them through .NET instead, and take home from PowerShell's own $HOME, which is correct everywhere. Nothing gated this to Windows and nothing tested it off Windows either: every pty round-trip in this module was `#[cfg(windows)]`, so a Windows-first script shipped to two platforms it had never run on. Open the harness up to Unix and give pwsh its own round-trip there. That took two fixes to the harness. It typed at spawn time, which puts the keystrokes ahead of the cursor-position reply in the same input stream — pwsh, still waiting on that reply, eats `false\r` as the answer to its own query and the command never runs; wait for the prompt-end mark before typing. And nothing was answering `CSI 6n`, which PSReadLine blocks on before it will draw anything. Enter comes in as a parameter now, because a raw-mode reader only accepts `\r` where a line-discipline shell also takes `\n`. While rewriting the home match: require the separator. With a home of `/Users/ann` a bare StartsWith also swallowed `/Users/annex`, retitling it `~ex`. Found while investigating #583, which reports a pwsh pane freezing on `ls`. This is not that bug — the injection runs clean on macOS pwsh 7.6.4 both over a raw pty and under tmux — but it is a real defect on the same untested path. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
c159a86039 |
fix(ui): read a path's ~ off the machine the path is on (#580) (#607)
`abbreviate_home` measured every path against this process's own `$HOME`, whoever the path belonged to. A remote pane sitting in `/home/deploy/app` therefore read as `~/app` on a laptop that happens to log in as `deploy`, and stayed spelled out on one that does not — the `~` naming the wrong machine either way. #568 took the same borrow out of the file-link resolver; this is the display half of it. The home is now the caller's to name, because only the caller knows which machine the path is on, and nothing here has to be asked for: a host reports its home in the control handshake (`ControlHelloOk::home`) and `HostLinks` already keeps it per host, so `path_display::home_for_host` is a map lookup and never a round trip. `TerminalView::display_home` puts a pane's own answer behind one call, and `Tab::leaf_title_and_home` reads a title and its home off one leaf so the two cannot disagree. Everything that draws a shortened path is on it: the Info panel's cwd, the tab strip's label and tooltip, the sidebar's title and cwd lines, and the switcher's workspace and tab rows. A path on a machine with no link — or one a pane's shell has ssh'd away to, which no host here can answer for — is shown in full rather than measured against a home that is not its own, the same answer #568 settled on. A WSL pane gets its distro's home instead of `C:\Users\…` for free, since it is a host like any other. Tests: the borrow itself (a path with no home is left alone, and this machine's home is not offered as a stand-in) at all three seams — `abbreviate_home`, `short_title`, `display_path`. `ui::home`'s test no longer has to set `HOME` on a process everything else is reading. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
84d6fc223a |
fix(tree-sync): stop an abandoned Replace debt deleting the tabs it was pulling (#579) (#608)
`take_rehydrate` dropped an owed `Adopt::Replace` whenever the window had any tabs, on the reading that the user had filled it in themselves. That reading only ever fitted the one caller it was written for: Restart Server, which empties the window before it resyncs. Every other `Replace` — a daemon back as a new process, a restart handoff that was refused, a remote server restarted, a layout delta that would not apply — is ordered over a window that still holds its tabs, because those stale tabs are the whole reason it was asked. The retry was abandoned on its first attempt, every time, and the resync silently did nothing. Abandoning it also left the window `informed`: still licensed to prune a workspace whose layout it had never read. `start_prime` then refilled the mirror from the machine, and the next `SyncScope::Full` diffed whatever the window happened to hold against it — one tab opened over an emptied window became `TabClose` for every tab on the machine, deleting those panes' records while their shells kept running, with nothing tree-driven left that could reach them. The damage #554 describes, through a door #554 did not close, and the same on a remote window. So the debt is now scoped to the layout it was owed over: `hydrate` records the tabs on screen as it orders a pull, and only a tab that debt never saw counts as the user moving on. And a `Replace` takes back the `informed` licence up front, the way `on_preempted` does, so a window waiting on a rebuild adds to its machine without pruning it until the pull lands — however the debt ends. Pre-existing since #472 ( |
||
|
|
81c1e3d5e1 |
test(keymap): ask the keymap what is bound, and cover the reload that binds it (#582) (#609)
The keymap tests asserted against `bound_keystrokes`, a `#[cfg(test)]` mirror of `action_bindings` that #571 left behind when it removed the `NoAction` retire loop — the last production reader of it. A mirror is a second implementation of the table, so those tests could agree with it while the app bound something else. Each one now builds a `gpui::Keymap` from `action_bindings` and performs the lookup gpui performs on a keypress: typing this chord in that context runs that action. The paste test gains the other half it never asserted — that a terminal chord does nothing outside a terminal — and the context test is renamed for what it now checks. `bound_keystrokes` is deleted. The watcher had no coverage at all, so the one thing #548 is for — hand-editing config.json and having the new chord fire without a restart — was pinned nowhere. Its tick moves out of the closure into `apply_reloaded_config`: same statements in the same order, taking the load result as an argument and returning whether it rebound, which the watcher ignores. Three tests drive it against a live keymap — a reload that binds Ctrl+Alt+9 to SplitRight makes that chord dispatch it, a reload that moves no binding does not rebuild the map, and a quarantined reload rebinds nothing, because the global config is deliberately not replaced on that path and the user's keys have to survive a typo. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
6c26b35acc |
fix(control): bump the dialect to v6, and publish a tty7-server for macOS (#605)
* fix(control): bump the dialect to v6 so an out-of-date server says so The control dialect has been renamed, extended and cut since it was last numbered, all of it against CONTROL_VERSION 5: the machine tree replaced WorkspaceList/Get/Put/Delete with WorkspaceTree, MachineGet and the tab/pane verbs, GitStream arrived with its chunk and end events, and ReplyOk::Attached and FileMeta went away. A peer left behind by any of that still answers the hello, because the number it answers with still matches. It is also still sitting at the path the installer looks for, tty7-server-c5p5, so a client decides it already has the server it needs. Then the first call reaches a variant the peer has never heard of, the frame fails to decode, and the read loop takes the whole link down with it. What the user sees is a remote workspace that opens with no tabs and a git detail pane that never fills, with nothing anywhere saying why. Moving the number puts all three guards back: the hello is refused with the message that names the old build, the remote binary is looked for at c6p5 and installed rather than trusted, and a stale local daemon gets the restart prompt it should have been getting all along. Document the rule next to the constant while it is fresh: move it when a variant is added or removed. The feature strings only cover what a peer can safely ignore, and a request it cannot decode is not that. * feat(remote): publish a tty7-server for macOS hosts A remote workspace has been Linux-only for no reason anyone chose: the installer derives the asset name from `uname -sm`, and the only names it knew were the two musl builds. A Mac on the other end of an SSH profile got "a remote tty7 workspace needs a Linux host" and stopped there. Publish the two Apple slices alongside them and teach the installer to ask for them. `Darwin arm64` and `Darwin x86_64` now map to tty7-server-macos-aarch64 and tty7-server-macos-x86_64; everything past that point already worked, because nothing under it was ever Linux- specific — the install path is POSIX, the upload is SFTP, and the dialect probe runs the binary before trusting it. The machine names are matched per system rather than by architecture alone. Linux says aarch64 on one distribution and arm64 on the next, while a Mac only ever says arm64, so honouring Linux's spellings under Darwin would be guessing at output no Mac produces. Static linking is not the instrument on macOS — Apple ships no static libSystem — so assert-macho.sh stands in for assert-static.sh with the guarantee that actually matters: every dependency resolves under /usr/lib or /System/Library, so nothing the destination Mac lacks can be picked up from a build runner, and the binary carries the signature arm64 refuses to run without. Not signed or notarized beyond that, deliberately. The binary is never downloaded by the Mac that runs it: the client fetches it, verifies it against checksums.txt and writes it over SFTP, which sets no quarantine attribute, so Gatekeeper is not in the path. ASSET_X86_64 and ASSET_AARCH64 become ASSET_LINUX_*, which is what they always meant and could not keep meaning next to a macOS pair. * fix(ci): sign the x86_64 macOS server, and stop the guard flaking on it Two faults the first green run hid from each other. The linker ad-hoc signs the arm64 slice because Apple Silicon will not execute anything unsigned, and leaves x86_64 bare. That is fine on an Intel Mac, but the x86_64 server is also what an Apple Silicon box gets when it asks through a Rosetta shell, and handing that machine an unsigned binary is a guess about Rosetta nobody needs to make. Sign both slices ad-hoc in the workflow — no identity, no secrets, nothing to do with the notarized signing the GUI bundles get. The guard that caught it was itself unreliable: `codesign -dv | grep -q` under `pipefail` reports failure whenever grep wins the race, because -q exits on the first match and the writer takes SIGPIPE. Small output means the writer usually finishes first, which is why the arm64 job passed and x86_64 failed on the same signed-or-not question. Capture into a variable and match afterwards, the way the release workflow already does it. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
49901d7f8a |
fix(cli): let --enter press the key it is shorthand for (#581) (#606)
`--enter` is documented as sugar for `--key enter`, but the send dispatch counted only `args.keys`, so `tty7 send %42 --enter` answered "needs TEXT ... or a --key to press" and pressed nothing. The key list is now built before the dispatch and the dispatch counts it, so a marked address with `--enter` and nothing else runs what the pane already has typed, and a bare `send --enter` presses Enter where the caller sits. An unmarked id is deliberately left out of that promotion. #567 made the address slot take bare ids, and `send 83 --key C-c` addressing pane 83 is fine because `--key` says "press this" and nothing else. `--enter` does not: `send 2 --enter` reads at least as much like typing 2 into your own pane and running it, and turning it into a keystroke at pane 2 would be the silent retarget #567 spent its diff closing. It stays a loud error, now naming both spellings (`send %83 --enter`, `send %PANE 83 --enter`) rather than only the typing one. The reference, the bundled skill reference, `send --help` and the `--enter` help all said the old thing in slightly different words; they now say the same thing as each other and as the code. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
664b766698 |
fix(settings): split Shell Arguments like a shell, quote them on the way back (#551) (#573)
* fix(settings): split Shell Arguments like a shell, quote them on the way back (#551) The field split on raw whitespace, so `-c "echo hi"` became four argv fragments with the quotes still attached, and it silently rewrote config too: `build_shell_inputs` refilled the field with `args.join(" ")`, which cannot spell an argument containing a space, so a legal `"args": ["-c", "echo hi"]` in config.json re-committed as three argv on the next blur without the user typing anything. Parse with shell-words rules instead and quote each argument on the refill, so field text and the argv array round-trip losslessly. An unbalanced quote cannot become argv at all, so commit refuses it and the row explains why under the input — the proxy field's pattern. The field description in en/zh/ja now says quoting works. Program gets the milder half of the same treatment: a bare command that detection (a PATH probe) never saw is almost always a typo like `pwsh7` that today only surfaces when a pane fails to open, so the row warns under the field. It never refuses — the field stays free-text so a shell detection missed remains reachable — and anything spelled as a path is taken at its word. The comparison reuses core's `same_shell_program`, so "known" here means exactly what the new-tab menu dedup means. shell-words was already in the tree via portable-pty, so the direct pin adds no new code. * fix(settings): split Shell Arguments as argv, not as POSIX source Review pass over the #551 fix. Splitting with `shell-words` bought the quoting contract at the price of two silent rewrites of its own, both the same shape as the bug being fixed: a backslash outside quotes is a POSIX escape, so `--dir C:\Users\me` committed as `C:Usersme` on the platform where that is how a path is spelled, and `#` opens a comment, so `--tag #1 --verbose` committed as one argument. The refill was noisier than claimed too — `shell_words::join` quotes on `=`, `~`, `*`, `?` and `[`, so an existing `--color=auto` came back as `'--color=auto'`. Nothing here is a shell: the field is a text spelling of an argv array that goes to `CommandBuilder` directly. So split and join are now a local pair sized to exactly that job — quotes group, `\"` and `\\` inside double quotes escape, everything else is a character — and the direct `shell-words` pin goes away again. They are exact inverses, which is what `config.json` needs, and the test walks the round trip over the cases a space-join cannot spell plus the two above. Drops the Program nudge. `shells::inventory()` inserts the *configured* shell into the inventory it returns, so `pwsh7` is in `self.shells` from the next refresh onward: the warning could only ever flash between the commit and the refresh landing, and never appeared at all on a later visit to Settings. Its test passed because it hand-built an inventory that version of the value could not be in. Making it true needs core to say which rows were detected rather than configured, and that is a serialized protocol struct — too much for a nudge the issue itself called the milder half. Refusing the arguments no longer drops the Program typed or picked beside them: the stored argv stays as it was, which is what "this value was not saved" already told the user, and the shell picker works again while the arguments field is mid-edit. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
1987766a14 |
fix(daemon): rebuild the window from the tree when a restart handoff fails (#554) (#564)
* fix(daemon): rebuild the window from the tree when a restart handoff fails (#554) Restart Server clears the window's tabs before attempting the handoff, and the Err branch then only showed a reason on the home page. A refused handoff leaves the daemon exactly as it was — still serving the panes the window just dropped — but every restore path is tree-driven, and the next sync of the emptied window diffed into "close every tab" against the mirror: pane records deleted under shells still running, or the whole workspace removed if the user simply closed the window first, right after the dialog promised nothing would be interrupted. The Err branch now runs the same invalidate + resync the Ok branch runs, pulled out as tree_sync::resync_after_local_daemon_change. Where the daemon really is gone (an exec that never re-listened), the pull misses and the rehydration debt is what keeps the empty window from being pushed up as the layout. The failure is also toasted, since a successful resync takes the window off the home page where the reason was shown. * test(tree-sync): anchor the emptied window giving up its mirror The fix for #554 rests on one property that nothing asserted: after a restart that failed, the window must stop speaking for the machine. An emptied window still `informed` over a `Primed` mirror is the shape that does the damage — the next sync diffs it into "close every tab", and closing it authorizes a `WorkspaceRemove` outright, while every shell it named is still running on a daemon that never went anywhere. The handoff itself is UI-plus-daemon integration and stays uncovered, but the guard is not: the pull drops the mirror, clears the ops queued off the emptied window, and takes the workspace back out of disposable range, all synchronously and before any link is needed. Pinned on `resync_window_from_tree`, the per-window step both of the helpers above it work through — which of them a caller picks decides whether the link is dropped first and which windows are walked, not this. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
8071eddb5b |
fix(ui): clamp the font metrics to the config's own range, and stop buckets mislabeling a hand-set value (#550) (#572)
* fix(ui): clamp the font metrics to the config's own range, and stop buckets mislabeling a hand-set value (#550) The settings steppers and the Ctrl+=/Ctrl+- keys clamped font size to 6-48 and line height to 1.0-2.0, while `sanitize` allows 4-256 and 0.5-4.0. A value inside the config range but outside the GUI's got pushed the wrong way by a single step — `font_size: 50` shrank to 48 on "+" — and `set_font_size` writes the result back to the file, so one misclick permanently changed a value it only meant to nudge. The bounds move into tty7-core beside `sanitize` (the `ui_font_size` precedent), one shared range for validation, the steppers, and the keyboard path. The scrollback and notify-threshold preset rows had the matching display bug: the highlight matched a *range*, so a hand-set 5000 lit up "10,000" and 20s lit up "30s", and clicking that cell silently overwrote the real value with the bucket's. The segmented control now highlights a bucket only on an exact match and otherwise shows a "Custom (N)" cell that names the live value and is not a button. * fix(ui): name a custom preset the way the cells beside it are written Review follow-up on #550. The "Custom (N)" cell rendered the raw integer, so a documented `scrollback_limit: 50000` read "Custom (50000)" between cells reading "10,000" and "100,000" — the one number on the row not written like a count. It is grouped now, and the presets and their labels are one pair of lists each, checked against each other, so a cell cannot come to show one number and write another. The bucket match moves out of the render bodies into `preset_choice`, which is what makes the exact-match rule the issue asked for testable: the presets the default lands on, the 50,000 the example config in `docs/reference/configuration.mdx` carries, and 20s on the notify row. The core test claimed to pin "the GUI steps within the range sanitize allows", but only asserted that sanitize agrees with the constants it is written in terms of — true by construction, and its line-height case took the reset path rather than the clamp, so it passed without touching LINE_HEIGHT_MIN at all. It now pins the published numbers themselves, the clamp in both directions, and the two values the issue was reported with. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
5dd60a6660 |
fix(ui): rebuild the keymap when a hand-edited binding reloads (#548) (#571)
* fix(ui): rebuild the keymap when a hand-edited binding reloads (#548) The config watcher reloaded locale, theme, menus and shells but never the keymap, so a hand-edited `keybindings` entry appeared in the settings list off the live global while the key itself stayed dead until restart — the settings page showed a binding gpui would never dispatch. Two changes make driving a rebind from the watcher safe. First the rebind clears before it binds: gpui `Keymap::add_bindings` only ever pushes — it never dedups and nothing retires a binding — so the old append-only rebind leaked one full table per call, and every keystroke walks the whole map. `rebuild_keymap` now replays the action bindings plus the fixed terminal/modal bindings onto a cleared map, and `init` shares it. Second the watcher gates on it. It fires for every write under the config dir — including the apps own `save()`, which a sidebar drag or a palette open triggers — so it compares the `(keybindings, keybinding_preset, prefix)` triple before and after the reload and only rebinds when a binding actually moved, keeping a no-op save from churning the keymap. Tests pin the triple only moving on a real binding change and the map not growing across repeated rebinds. Per the review this lands after #537 (the watcher keeping the old config on a parse failure): without that, a broken edit would swap in defaults and this rebind would then wipe the users keys from the live keymap too. The unknown-action surface and the prefix-conflict detection the review also scoped are follow-ups. * fix(ui): carry the inherited bindings through a keymap rebuild `rebuild_keymap` clears the whole map, and the map is not tty7's alone: `gpui_component::init` runs first and installs the `Input` context's editing table, the list and menu navigation and the escape that closes a dialog, with no entry point to install them a second time. Clearing and replaying only tty7's bindings dropped all of it — at `init`, so every text field in the app lost backspace, the arrows, enter and escape from the first frame, and the test that pinned the map's size could not see it because it measured after `init` had already cleared. Snapshot whatever is in the keymap before tty7 binds anything and lay it back down first on every rebuild, in the order it was added, so tty7's bindings still win. Drop the `BoundKeystrokes` global with them: the retire-one-`NoAction`-at-a-time path it fed is gone, and its last reader with it. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
e1efae2809 |
fix(ui): read the Info panel agent row off one pane, and shorten Windows paths to their leaf (#543, #544) (#570)
* fix(ui): read the Info panel agent row off one pane, and shorten Windows paths to their leaf (#543, #544) Two rows in the Info panel were misreading a split tab or a Windows path. The agent row took its name from `tab.agent` (the first leaf with an agent) and its status from `tab.agent_status` (the most urgent across the whole tab); the two resolve independently, so a split tab running two agents could show one pane name beside the other pane state — a row no leaf ever had. Both now come from the detail pane when it has an agent, falling back to the tab aggregate only when the focused leaf has none, so the row holds while focus sits on a plain shell but never splices. The cwd row split the path on `/` only, so a backslash-spelled path — any agent-reported cwd (`agent_session.cwd` arrives as `C:\…`), a cmd pane, a shell-integration-off pane, or the seed cwd before the first prompt — elided its tail and hid the directory own name. The split now takes the last of either separator, and is not cfg-gated: the panel shows remote paths, so a Windows build describes Unix paths and vice versa. The `~` shortening moves into a shared `path_display` helper reading `USERPROFILE` as well as `HOME` and comparing with separators normalized and case folded; the tab strip `abbreviate_home` and the home picker `display_path` had the same HOME-only miss and now use it too. Tests pin backslash, mixed-separator, drive-root and UNC shapes. * fix(ui): hold the agent row's fallback to one leaf, and stop the home test leaking Review follow-ups on the #543/#544 pair. - The fallback branch still spliced. When the focused leaf carries no agent the row fell back to `tab.agent` + `tab.agent_status`, which is exactly the pair the fix set out to break up: the first leaf with an agent, beside the highest urgency anywhere in the tab. A three-way split with focus on a plain shell could still read `Claude · Working` off two different panes. `Tab::agent_row` now picks the most urgent agent leaf and answers both halves out of it; `agent_status` is that pair's status, so the tab strip's badge is unchanged. - `path_display`'s test home was a process-global that no test ever cleared, so once one of these tests ran, every later `abbreviate_home` in the binary read the pinned home — including `ui::home`'s own test, which sets `HOME` and expects to see it. Ordering decided whether it passed. The comparison moves into `abbreviate_under(path, home)` and the tests hand their home in; nothing global is left to leak. Adds the trailing-separator home and non-ASCII component cases the byte boundary reasoning turns on. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |