mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 00:02:23 +00:00
c4645aeea8a60c7a20cf23ffbfae225091d27a7e
966
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c4645aeea8 |
feat(terminal): draw a scrollback scrollbar down the right edge of a pane (#480)
A pane's scroll position lives in alacritty's `display_offset` — rows of scrollback, not pixels of laid-out content — so it has no `ScrollHandle` to hand a scrollbar. `TerminalScrollHandle` implements gpui-component's `ScrollbarHandle` over the grid instead, which lets the pane draw the same `Scrollbar` the sidebar and every list already use: same theme, same `Scrolling` show mode, same fade-out. The bar never touches the terminal. `set_offset` only records the row it wants; `sync_scrollbar` applies that on the next render — clearing the sub-line remainder and cancelling an in-flight smooth scroll on the way — and reports back where the grid actually ended up. Scrollback piling up at the live edge is deliberately not reported: the bar shows itself whenever the offset it reads changed, so a pane printing a build log would otherwise hold a thumb on screen for as long as the output ran. Every other change passes through, including the history shrinking, which is a cleared scrollback rather than growth. Closes #432 Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
9735a490b5 |
Merge pull request #479 from l0ng-ai/perf/wsl-tab-open-cost
perf(wsl): stop re-proving the distro on every new tab |
||
|
|
d09fc10878 |
fix(wsl): make the remembered server path safe to trust
The note the last commit introduced had no working way to be wrong. Its only repair was the router forgetting the distro when `RemoteLink::wsl` returned an error, and that call only spawns `wsl.exe` — which starts perfectly happily with a server path that no longer exists inside the distro. The exec failure arrives later, as an EOF on the bridge, so a distro that was reinstalled or had its bin directory cleaned out failed every WSL tab from then on, with nothing re-installing it and no way out but restarting tty7. So forget it where the truth actually shows up: a bridge that closed without ever sending a byte never ran, and after one of those the next pane proves the distro again. The spawn-error retry stays, but only when the path came from memory — a path proved a moment ago will prove the same, and re-probing it just doubles the wait before the error reaches the user. Two more things the note quietly took away. It was read before `install_lock`, so a pane spawn was no longer mutually exclusive with `replace_wsl_server`, whose whole job is to move the file the note names: a window restoring panes while the user updates the WSL server could spawn the binary being replaced. The read moves under the lock, which costs nothing when no install is running and correctly waits when one is. And it short-circuited `Installer::run`, the only thing that notices a foreign build serving the distro — so the "a different build of tty7-server is serving this machine" warning reached the first pane of the daemon's lifetime and no other, including a whole new GUI session, since the daemon outlives one. The note now carries the mismatch it found and re-files it for each later pane, which is the same warning without the five round trips that found it. The wall-clock budget in the remembered-answer test is gone: the returned path already proves no probe ran, and 200ms of elapsed time on a loaded CI box only ever proved the box was loaded. |
||
|
|
e0745329db |
fix(wsl): do not pass off a half-read registry as the distro list
`registry_user_subkeys` ended its walk on any non-zero return and reported what it had as the answer. Only one of those returns means "that was all of them"; the rest mean the walk stopped early — a `wsl --unregister` running right now, a Store install rewriting `Lxss` underneath it — and a failure at the very first index came back as `Some(vec![])`, an authoritative "there are no distros". The sweep that feeds the shell menu keeps the last good list only when the probe says `None`, so that empty answer erased the user's distros for the length of the TTL, with no error anywhere and no `wsl -l -q` to catch it: the fallback only runs when the key will not open at all. The walk now says nothing unless it reached the end. `State` is now read too, the way Windows Terminal reads it. A `DistributionName` is not a promise that the distro can be entered: an install that was cancelled half way, a failed `--import`, one being uninstalled as we look, all leave the key behind. `wsl -l -q`, which this replaced, never listed those; without the filter they arrive in the shell menu and open a pane that dies of a WSL registration error. A key with no `State` at all is still taken at its word, which is the conservative direction — inventing one would hide working distros, which is the mistake `Modern = 1` would have been. That also makes `registry_user_dword` production code rather than a `cfg(test)` copy of `registry_user_string`'s FFI scaffolding kept alive for one assertion. The timing test now skips when the registry has nothing to read: on a machine with no `Lxss` key the listing is *supposed* to go to `wsl.exe` and wait, so timing it there failed the test on exactly the machines the fallback is for. And hoisting `LXSS` had left `default_wsl_distro`'s doc comment attached to the const; it goes back on the function. |
||
|
|
49bfe59410 |
docs: drop the orchestration skill tty7 no longer installs
The in-app switch that wrote `~/.claude/skills/tty7-orchestration` was
removed in
|
||
|
|
7f16f6a6ff |
fix(wsl): read the distro list from the registry, not the WSL service
`wsl -l -q` has to reach the WSL service, and reaching the WSL service is the part that can be slow. Behind a hardcoded 3s timeout that made the listing all-or-nothing: on the machine in #454 a round trip took 3.3s, so the call timed out every time, the list came back empty every time, and no WSL distro was ever offered in the shell menu. Not slow — absent. `Lxss` is where `wsl.exe` registers them, it is the same key `default_wsl_distro` already reads for the same stated reason, and nothing is listening on it, so it cannot hang. `wsl -l -q` stays as the fallback for when the key will not open at all, which means this is not a machine with WSL on it rather than a machine whose WSL is busy. Windows Terminal made this move in 2021 (microsoft/terminal#10967) after the same symptom — distros "missing entirely" on first launch. It skips distros whose key carries `Modern = 1`; we must not. That is a deduplication rule specific to Terminal, which modern distros hand a profile fragment of their own. Nothing hands tty7 anything, and on an up-to-date machine `Modern = 1` is the ordinary case — on the box this was written on, the only distro installed. There is a test pinning that. |
||
|
|
c5a0d8665b |
perf(wsl): let a distro say once where its server is
`ensure_wsl_server` re-proved everything on every pane: uname, $HOME, a stat, a liveness probe, a look at what is running — five serial `wsl.exe` round trips to re-learn what the previous pane had just learned. Fine once per distro, absurd per pane. It now keeps the answer in memory, and nothing expires on a timer, because the answer barely rots. A tty7 upgrade renames the binary, but a new build is a new process and the map starts empty. A distro shutting down does not invalidate it either: `wsl.exe` restarts a stopped distro on demand, and the bridge starts its own daemon when none is listening, so the one claim that really does stop being true is repaired a layer below without anyone asking. What is left is a path that could stop existing — the distro reinstalled, the directory cleaned out. Starting the bridge is what discovers that, so the router forgets the distro and proves it again from scratch, once. The two operations that deliberately disturb what is running forget first, so a restart that fails halfway leaves no note claiming otherwise. |
||
|
|
f676fb96de |
perf(wsl): stop asking twice whether a distro is ready
Every pane on a WSL workspace ran `ensure_wsl_server` from the client before it even connected to the daemon — and then the daemon ran the very same probe inside `router::open_link` before opening the link. Two full rounds of five serial `wsl.exe` calls, to learn one fact. The client's copy bought nothing. It threw the answer away and kept only the error, which the route ack reports just as well; and the consent question for a first install still finds its way here, because the daemon runs its probe under `RouteSetup::blocking`, which installs the relay that turns that question into a frame on this connection. Measured on a distro that was already running and connected: 800ms to open a tab, down to 440ms. Issue #454 is the same code on a machine where one `wsl.exe` round trip takes 3.3s, where the duplicate was costing 15s a tab. |
||
|
|
707fd1867b |
docs: add a Mintlify documentation site (#478)
38 pages under docs/, written against the source rather than the README: config keys and their clamps from core::config, default keybindings from ui::keymap, every CLI verb and flag from tty7-cli, agent aliases and hook/fork/resume support from core::cli_agent, and Settings paths taken from the actual en-US strings. docs/features.md and its zh-CN translation are retired — everything in them now lives in a page of its own, plus the two things they carried that nothing else did (IME input, the performance notes). README and README.zh-CN point at docs/ instead. Screenshots and videos are placeholders for now: docs/images/placeholder.svg with a caption naming what each shot should be. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
af3928da1e |
fix(tree-sync): pay back a remote window's owed tree pull (#472)
* fix(tree-sync): pay back a remote window's owed tree pull A window opening onto a remote workspace is empty until `hydrate` pulls the machine's tree and rebuilds its tabs from it, and it has to be: an empty window diffs into "close every tab", so `sync_window` holds anything back until the pull lands. When the pull fails, `owe_rehydration` records the debt and returns, on the promise in its own doc comment that the next sync settles it — "which is what a reconnect does through `on_link_up`". `on_link_up` is called for `HostId::LOCAL` and nowhere else. On a remote host the debt was only ever settled by a reconnect completing, by an edit in the window, or by restarting the app. So a pull that failed while the link stayed up was never noticed again: no reconnect, and an empty window has nothing in it to edit. The window sat on the home page with every tab and every shell still on the machine, and only a restart brought them back. Two ways to fail a pull with a healthy link, both routine. A `MachineGet` can overrun its ten seconds on a slow link. And a `WorkspaceCreate` can lose its race with `start_prime`, which runs the same create from the other side of the same window opening — that one fires on every remote workspace opened, and is only invisible because the workspace it usually lands on is empty anyway. So: arm a backed-off retry when the debt is taken on, drive it through `sync_window` where the rules about whether a window may still adopt the machine's layout already live, and stop treating a lost create as a failure — read the tree again and hydrate from what is really there. `on_link_up` is also wired to a remote link coming up, which is what the comment always claimed: a link the switcher connects finishes no attempt, so nothing told its windows the machine could be reached. * fix(tree-sync): end the backoff with the run of failures, and stop shouting Review follow-ups on the owed-pull retry. The attempt count paces the retry, so it has to mean "failures in a row", but it was only cleared when a hydration landed. A debt abandoned rather than paid — a `Replace` dropped because the user filled the window in themselves — and a prime that landed both left it standing, so the next first failure waited the 30s cap on an outage that was already over. It is now cleared wherever the run ends. A window left open on a machine that is really gone retries forever by design, which meant a warn and an info every ~45s for as long as it stayed open. Once the backoff settles at its cap those lines stop being events and become a fact about the machine, so they step down to debug. The retry is exactly as persistent; only the volume drops. Also: report the create's own refusal when the reread finds the workspace still missing, and say at debug that the reread happened at all — the race recovery was silent, so the extra round trip was invisible when reading a log. And correct the comment on the window-gone guard: closing a window drops its whole `WsState` through `forget`, debt and all, so nothing is parked for the next opener. Tests: the count ends with the run at all three sites, the level steps down at the settle point, and the armed retry is driven through a real timer (advance_clock) into the window-gone guard — the first coverage of the retry actually firing rather than of the predicate it consults. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
12df66fb0f |
fix(ui): dim panes by blending terminal colors toward the window background (#464)
* fix(ui): dim panes by blending terminal colors toward the window background * fix merge * fix(ui): tighten the pane-dim blend |
||
|
|
c216b389ae |
fix(macos): size the DMG ourselves, and stop blaming the runner's disk (#477)
The nightly channel has been frozen since 06:32 on 2026-08-10: every run dies in bundle-macos.sh with "hdiutil: create failed - No space left on device", on macos-15-intel, after the build, the signing and the notarization have all succeeded. The host disk was never full. #476 read that message as the runner running out of room and freed space for it; the `df -h` it added to prove the point disproved it instead — 105 GiB available, and the run failed anyway. The path in the error is under /Volumes/tty7, which is the image being created, not the runner: the volume ran out, not the disk. `hdiutil create -srcfolder` sizes the image from the bytes it is about to copy and does not cover what the filesystem spends carrying them, so a bundle that fits by measurement still runs the volume dry partway through the copy. It is a threshold rather than a cliff, which is why this began without anyone touching packaging: the binaries grew over edfadb7..fafcaa0, the x86_64 pair is the larger one and crossed it first, and arm64 kept building fine just underneath. Ask for the room explicitly — twice the content plus 64 MiB. The image is compressed on the way out, so the slack is nearly free: on a stage of this shape, 127 MiB of empty volume cost 672 KiB in the published DMG. Also drop #476's deletion of the build tree. It was paying for a problem that did not exist, and the bill was rust-cache finding nothing to save and every macOS build recompiling the dependency graph. The `mv` from that commit stays: a second full copy of the bundle is genuinely redundant, and nothing reads dist/tty7.app after this point. Verified locally against a staged bundle of the real shape (73 MiB, 101 files): the image is created, mounts with every file present, and detaches clean. The remaining unknown is only whether CI agrees, which the next nightly answers. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
05ed9fa4ff |
ci(macos): give the DMG somewhere to go on a runner that ran out of disk (#476)
Three nightlies in a row died in bundle-macos.sh with "hdiutil: create failed - No space left on device", across two different commits, always on macos-15-intel and never on arm64. The build, the signing and the notarization all succeed; the volume simply cannot hold the disk image on top of everything already staged on it. At `hdiutil create` the volume carries the whole release `target/` tree, the signed dist/tty7.app, the compressed update zip, a second full copy of the bundle under dist/dmg-stage, and the image being written. Two of those five are avoidable: Stage the bundle with `mv` instead of `cp -R`. Nothing reads dist/tty7.app after this point — the updater ships the zip, nightly.yml verifies that zip by extracting it elsewhere, and release.yml knows tty7.app only as an intermediate to keep out of the upload globs. Drop the build tree before the image is written. Every binary it produced is already inside the bundle and no later step in either workflow reads it. The cost is that rust-cache finds little left to save and the next macOS build recompiles the dependency graph — a slower nightly, against no nightly at all. `df -h` runs first so the next person to touch this has the number. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
6d47406544 |
docs(readme): add a hero screenshot of the workbench (#475)
One 1600x1132 WebP (184 KB), shown at 900 px in both READMEs. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
0f5e63701e |
fix(ui): let the overlay scrollbars fade out again (#471)
* fix(search): wash a match in the accent, at a strength the theme can afford A search hit was washed from the terminal palette's selection colour at a fixed 1.45:1 against the background, so it read as a weaker selection on a grid that is already grey on grey — and 1.45:1 is under what a hairline is worth, spread over a whole cell. Two changes. The tint is now the theme's accent (`ActiveAccent`, already floored at 3:1 by `legible_accent`), which is the one colour the terminal surface has nothing else in. And the strength is derived per theme instead of fixed: the wash is opaque with the glyph drawn on top, so what it may spend is the theme's own text-contrast budget. A palette with 21:1 between text and background can afford a wash you cannot miss; one with 6.6:1 cannot, and a single constant has to be safe for the second. The current match drops its caret-coloured outline. That existed because a fill 2.1:1 off the background could not say "this one" on its own; now that it sits at the top of the theme's budget, the outline is the same colour saying the same thing twice. * fix(ui): let the overlay scrollbars fade out again macOS reports should_auto_hide_scrollbars() = false for anyone with a mouse plugged in, and apply_theme turned that into ScrollbarShow::Always for every list in the app. That preference is about legacy scrollbars, which take a gutter out of the layout; ours are overlay bars painted on top of the content, so Always parked an opaque bar over the switcher's tab column for as long as the panel stayed open, with nothing to fade it. Pin scrollbar_show to Scrolling instead, so every list fades its bar out after it stops scrolling. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
99a388331c |
fix(search): wash a match in the accent, at a strength the theme can afford (#470)
A search hit was washed from the terminal palette's selection colour at a fixed 1.45:1 against the background, so it read as a weaker selection on a grid that is already grey on grey — and 1.45:1 is under what a hairline is worth, spread over a whole cell. Two changes. The tint is now the theme's accent (`ActiveAccent`, already floored at 3:1 by `legible_accent`), which is the one colour the terminal surface has nothing else in. And the strength is derived per theme instead of fixed: the wash is opaque with the glyph drawn on top, so what it may spend is the theme's own text-contrast budget. A palette with 21:1 between text and background can afford a wash you cannot miss; one with 6.6:1 cannot, and a single constant has to be safe for the second. The current match drops its caret-coloured outline. That existed because a fill 2.1:1 off the background could not say "this one" on its own; now that it sits at the top of the theme's budget, the outline is the same colour saying the same thing twice. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
fafcaa0685 |
feat(splits): draw the pane grip as Ghostty draws its own (#463)
The bar a pane was picked up by grew and recoloured under the pointer, and showed the moment the pointer was anywhere in the pane at all. It was loud in the wrong places: a mark on top of the terminal wherever the mouse happened to rest, and a target that moved while being reached for. Cut to the shape Ghostty gives its grab handle instead: * three dots, 80x12 of reach around them, and nothing between the two states but ink — 0.3 in the band, 0.8 on the grip itself. * the dots are asked for by the pane's top fifth (floored at 24px), not by the whole pane, so the terminal is left alone everywhere else. * the target is there for as long as the pane can be moved, and only the dots come and go, so a pointer going straight for the top of a pane can press the grip on the frame it arrives. * a 150ms fade in, so the dots read as arriving rather than blinking. The fading a pane is under is now worn by the terminal it holds rather than by the pane, which keeps the grip legible on the very panes `dim_inactive_panes` fades — the ones being reached for. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
425f87e9a4 |
fix(core): key the machine tree to the config directory (#462)
* fix(core): key the machine tree to the config directory The tree resolved from $HOME while everything else an instance owns — views.json, the scrollback, the history, both sockets, the pidfile, and daemon.lock — resolved from the config directory. So --config-dir moved every part of an instance except the one that says which workspaces exist, and two tty7s pointed at different config directories, each holding its own lock and each certain it was the only server on the machine, still co-owned one ~/.local/share/tty7/machine.json. MachineStore::persist writes the document whole. The second one to flush replaced the first one's workspaces with its own, and the next daemon to start read the survivor's tree as the machine's. An empty tree is not distinguishable from a machine that really has nothing on it, so the GUI does what an empty tree means and forgets those workspaces for good. A lock and the thing it protects have to be keyed alike. data_dir() now follows the config directory; TTY7_DATA_DIR stays as the highest-priority override so the test harnesses keep their sandboxes. Moving the path without carrying the file would lose every workspace at the moment of upgrade, which is the failure this change exists to stop, so the daemon adopts the legacy file on startup before it opens the store. The destination already existing is the whole guard: it means a newer run owns the tree and the copy at the old path is stale, from a build that predates the move and still writes where it believes the tree lives. Adopting that over the live file would hand the old tree back. * fix(core): only the machine's own instance inherits the legacy tree The migration moved `machine.json` into whichever config directory started first. In the very setup this change exists to fix — a default install beside a `--config-dir` one — that is the second instance renaming the machine's tree into its own directory, leaving the primary to come up owning nothing. It also fired in our own test suite, where `routed_pane` and friends launch a real `tty7-server --config-dir <TempDir>` under the developer's own `HOME`. Adoption is now the entitlement of the instance running out of the config directory this machine resolves to on its own: `$TTY7_CONFIG_DIR` where the box names one, `$HOME`'s otherwise. Comparing paths rather than asking whether `--config-dir` was passed is what keeps the ordinary install working — `spawn` hands every daemon it starts an explicit `--config-dir`, its own included — and counting `$TTY7_CONFIG_DIR` is what keeps remote hosts upgrading, since a remote `tty7-server` is launched without the flag and finds its directory that way. Also tightens the cross-filesystem fallback: a rename that failed because another process already carried the file over is the one benign race, not an error to report and not something to copy over. What is left copies through `create_new`, so "never overwrite what is already there" holds against a racing writer and not merely against an `exists` check several syscalls old, and a write that does not finish leaves nothing behind. Tests: the gate both ways, the appearance hint riding along, the same directory under two names, the copy path refusing an occupied destination, and two cross-process cases in `machine_tree` that start a real server under a scratch `HOME` — one carrying the legacy tree in, one leaving it alone. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
d223818e1b |
Merge pull request #461 from l0ng-ai/fix/scrollback-restore-survives-restart
fix(daemon): give a restarted server back its panes' shells and screens |
||
|
|
a55340ed7f |
fix(daemon): sweep a dead daemon's leavings on the writer's tick, not at startup
Review follow-ups on this branch. `history::sweep` still ran at startup, three lines under a new comment explaining why sweeping there is wrong. The reasoning transfers exactly, and worse than by analogy: a restore carries the dead pane's commands to its successor via `history::carry`, so sweeping before the window can ask deletes the file the request is about. Same shape as the scrollback bug, one file over. Both sweeps now run on the writer's tick off one shared id set, and the writer is named for what it does. `pane_attachable` lost its only caller when the restore path moved to `pane_free_for`, leaving a function kept alive by the test asserting on it. The attach site does not need to predict the listing: it tries the attach, and a pane that is gone falls through to the fresh spawn on its own. Gone, with its tests folded into `pane_free_for`'s. `restored_screen` now drops the snapshot in both directions. Keeping the file when it decoded to nothing left it to be re-read and re-rejected by every later restore, and swept never, for a pane the tree still names. Also: the module doc still said scrollback was off unless asked for, which is what this branch reverses; and #449 landed the whole feature with no CHANGELOG entry, so nothing told anyone that pane output now lives on disk. |
||
|
|
b3a66e75d0 |
fix(test): let the machine-tree seed keep up with a new pane field
PaneSeed grew a `shell`, but this test is unix-only, so a Windows box never compiles it and never says so. Build the seed from `bare` and the next field lands on its own. |
||
|
|
852d3178c8 | style: rustfmt | ||
|
|
477d82524f |
feat(daemon): keep every pane's screen, without asking
`persist_scrollback` is gone, and with it the switch, its three translations and the branches that read it. Keeping a capped tail of each pane's output is now what the daemon does, not something it can be asked to do. This reverses the call made when the feature landed. The argument for off-by-default was that the ring holds whatever the pane printed — echoed tokens, `env` output, an agent's transcript — and that writing that down should be the user's decision to make. What the argument missed is when the decision gets made: the moment anyone learns they wanted this is the moment a daemon has already died, and by then the setting could only be turned on for next time. A feature whose entire purpose is to survive an event nobody schedules cannot be opt-in. The cost is real and does not go away: pane output now lives at `<config>/scrollback/*.bin` on every machine, 0600 on unix and behind the config directory's ACL on Windows, capped at 256 KiB per pane and dropped as soon as no window can still ask for it. Old configs naming the key still parse — nothing in `Config` refuses unknown fields — so the key simply stops meaning anything. |
||
|
|
412bfcfc90 |
fix(session): let a dead pane keep its id so its screen can be asked for
`pane_attachable` answered one question and was used for two. Deciding whether to attach needs to know the pane is alive; deciding which dead pane a fresh one replaces needs only its id — and that is the case the stored screen exists for. Using the first answer for the second was self-defeating. After the daemon restarts, every pane the window held is missing from the new daemon's listing, so the id was ruled out, so `restore_pane` was `None`, so the window spawned a pane that had never heard of a predecessor. No attach was tried, no restore was requested, and the screen the daemon still had on disk was swept a tick later without anyone reading it. The setting was on, the snapshot was written, the daemon was ready to hand it over, and nothing ever asked. Ownership still rules an id out, because another workspace's pane is neither ours to attach to nor ours to show. Liveness no longer does: the attach is still tried first and still gives way to a fresh spawn when the pane really is gone, which is the arrangement the tree path already argues for at length — `live` is a hint about what to show, never the judge of what to destroy. |
||
|
|
3093babddd |
fix(pane): say when a restore is not asked for
Dropping the request here produced a blank pane, which is also what a pane with nothing stored looks like and what a daemon that refused would produce. Three causes and one appearance, with nothing anywhere to tell them apart — the filter was silent, so reading the source was the only way to find out which had happened. |
||
|
|
c138be687a |
fix(daemon): keep a pane's shell and its screen across a restart
Two things a pane lost when the background service stopped and started, both of them things the tree was the only possible place to keep. **The shell.** `PaneRecord` and `PaneSeed` carried a pane's cwd, its ssh spec and its agent, but never what it was running. A window rebuilding a dead pane from the tree therefore had nothing to pass and spawned on whatever the default shell is now — so a restart turned a bash pane into a PowerShell one, quietly and in place. The daemon resolves the override against the config at spawn time and is the only party that knows the answer, so it keeps it and reports it; the seed carries it too, for the panes a window spawned itself. A handoff carries it in the blob, because nothing on the far side of an `execve` can work out the command line of a child it never spawned. **The screen.** The startup sweep ran before the endpoint was listening, which is the one moment nothing can answer the question it asks: the registry is empty and the windows that know which screens are still wanted cannot say so yet. A tree that failed to parse made it worse — `read_machine` quarantines it and returns an empty `Machine`, so one bad file took every pane's stored screen with it. The sweep now happens only on the periodic pass, a tick later, with the registry filled in and the tree caught up; nothing is serving a request in between. Turning the setting *off* still clears the directory at once, because there the promptness is the whole promise. Two smaller ones alongside it: `restorable_pane_ids` now counts the tree's pane list and not only the panes some tab currently stands on — the two disagree while a window is between layouts, and being wrong costs a file swept a tick late in one direction and somebody's terminal in the other. And `restored_screen` drops the snapshot file *after* deciding it was not empty, so a snapshot holding nothing is no longer consumed by the request it could not answer. The restore path had no end-to-end test, which is how this shipped: the unit tests cover the file, not whether a window that reattaches is shown anything. The new one runs a real daemon, puts a marker on a real pane, stops the daemon, starts another, and reads the wire. |
||
|
|
edfadb7df2 |
Merge pull request #424 from l0ng-ai/feat/scm-foundation
feat(scm): a full Source Control panel, decorations and commit history |
||
|
|
2dc6a88af6 |
merge: main into the Source Control branch
Conflicts were the two streams touching the same seams, resolved by taking the newer decision on each side: - main's interface font scale (rems tokens) wins in right_panel.rs; the SCM panel keeps its local px steps until it moves onto that scale, and the now-unused PANEL_TEXT constants are gone. - main's l10n_keys! macro (idents only) means the key list carries no doc comments any more; our SCM keys fold into it, and PanelUntracked stays deleted — its only caller was the panel this branch replaced. - main's Command::localized palette style carries our Git group; ORDER keeps main's visibility and our width. - main's ansi_seed/clear_ink refactor in presets.rs carries the lane colours: lanes() now clears through the same helper semantics uses. - file_tree keeps both: main's drag-and-drop targets and this branch's git decorations per row. - diff_overlay keeps both: main's sidebar-count write-back on snapshot install and this branch's epoch read and untracked preview. - main's window.prompt SSH-close confirmation supersedes the bespoke modal our branch still carried; main's tile-glyph revert stands. - main's two new guards are satisfied: the fourteen SCM actions carry authored names on the Keybindings page (their palette wording, plus a new CmdGitToggleGraph), ja translates ScmDetached, and CmdGroupGit joins the kept-in-English list — Git is a name. 2571 tests, 0 failures. |
||
|
|
9815f2d16f |
fix(windows): restore Ctrl+C in panes (#459)
The daemon was created with CREATE_NEW_PROCESS_GROUP, which disables Ctrl+C for the whole new group — and Windows hands that "ignore Ctrl+C" state down to every descendant. Every ConPTY shell a pane spawned inherited it, and so did everything those shells ran: the pane wrote 0x03 and conhost turned it into a keypress, but the CTRL_C_EVENT never came, so `go run` and `npm install` carried on. Git Bash looked fine only because MSYS synthesises SIGINT from the byte itself and never waits for a console event. DETACHED_PROCESS already leaves the daemon without a console for a control event to arrive on, so the group flag bought nothing to begin with. Both daemon spawn paths and tty7-cli's headless server — which spawns panes too — now share one constant without it, and DaemonPane::spawn clears any inherited ignore before it opens the pty, so a tty7 launched from a shell that already had the bit set is covered as well. The regression test has to inherit the state rather than switch it on in place, since that is the shape the daemon was in: an intermediate process created exactly as the daemon used to be runs both arms, and a pane must be interruptible only after the clear. Its observable is the shell rather than the interrupted command — after the ^C, cmd gets its prompt back and acts on the `exit` typed behind it — so it reads no message and holds on a non-English Windows. Fixes #451 Fixes #314 Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
feb027da1f |
feat(scm): show an untracked file's content when its row is opened
Focusing an untracked file in the diff overlay used to fall through to the names-only "Untracked files (N)" card — git has no patch for a file it does not know, and `--no-index` needs a null device whose spelling is platform business. The overlay now reads the file's own bytes (lazily, only the focused file, 4 MiB cap) and synthesizes the card a parsed added-file patch would produce: every line an addition, new-side numbers, true counts past the single-file budget, git's own NUL-in-the-first-8000-bytes binary rule. A fresh snapshot clears the preview so an edit shows up on the same cadence a tracked file's does; a failed read says so instead of showing an empty file. Found in manual acceptance of the panel. |
||
|
|
1df43b72b5 |
feat(files): copy dropped files into the folder they were dropped on (#458)
* feat(files): copy dropped files into the folder they were dropped on The Files panel has only ever been a drag *source* — a row dragged into a terminal inserts its path. Nothing on the tree ever registered a drop, so a file dragged in from the desktop did nothing at all, not even a highlight. Closes #453. The drop is the whole gesture: files land where the cursor was, not somewhere a dialog asks about afterwards. A folder row takes them itself, a file row stands in for the folder holding it — "next to this one" — and the space the rows do not cover belongs to the top of the tree. The placeholder inside an empty folder takes a drop too; it is the only thing drawn there, and letting it fall through to the root would put files somewhere the cursor never was. A row under the cursor wins over the column, which is what gpui's innermost-first dispatch already does. The copy itself goes through the `Host` the tree is listing, so a remote workspace reads here and writes there. Locally it is `fs::copy`, which is what keeps the executable bit that `write_file` would drop; remotely the bytes ride one control frame, and a file too big for that is refused with the advice to use SFTP rather than half-sent. Names already taken are asked about before anything is written, and the answer governs the whole drop — a half-done copy would have to be undone to honour a "no". Replacing a folder replaces it rather than merging into it. A drag let go where it started is a miss, not an error, so it says nothing. * fix(sftp): list the directory again once an upload lands An upload is written to `<name>.tty7-upload-<hex>` and renamed into place at the very end. The browser listed the directory the moment the transfer was handed to the daemon, so it caught that temporary name — and nothing ever listed again, so a finished upload sat on screen as a file with a hash glued to its name until the directory was navigated by hand. The premature listing is gone, and the panel now remembers the job ids it started: once one stops running — done, failed, cancelled, or dropped off the job list entirely — the directory is listed once more. Two uploads in flight settle independently, so the second one finishing does not depend on the first. * docs(changelog): note the SFTP upload listing fix * ci(host-boundary): allow the source side of a file drop, and stop scanning two files as empty The Files panel now copies dropped files in, and what the desktop hands over is by construction a path on the desktop's own machine: reading it is a local read even when the tree being dropped on is remote. The destination side goes through `Host`, and the one `std::fs::copy` that touches a destination sits inside a branch already gated on `host.id().is_local()`. While adding that entry: `attr` starts unset, which awk reads as 0, so a file whose first line is `mod something` matched `attr == NR - 1` and cut its body at line 0. `head -n -1` then errored and the file was scanned as empty — `src/terminal/mod.rs` and `src/ui/tray/mod.rs` both open that way, and the guard had been blind to both. Neither contains a violation, so seeing them is free. |
||
|
|
58d7ef5838 |
fix(scm): close out the review's minor findings across the data and UI layers
The second pass over the branch review: every remaining finding verified against the code, the real ones fixed. Data layer: - A truncated log parse is never called complete: RecordSplitter drops an overlong record whole and reports the count (delivered cut short, a commit body cut mid-way reads as the real message), parse_log carries a truncated flag past MAX_LOG_BYTES, and load_page only says "end of history" when the parse read everything git returned. - Every scope pins symbolic revs to shas before walking, so a commit landing between two pages can no longer shift where page two starts under Head and Refs scopes; unresolvable names read as "no history" rather than as a load failure. --parents was doing nothing and is gone; edge sort is stable so a merge's Outs keep first-parent order. - The lane model's central invariant now names the join case — a merge whose second parent already has a lane reserved sends its Out onto that lane, one line below the cut, not two — with a golden test for the commonest merge topology of all, which no golden covered. - DiffSource revs get the same could-be-an-option guard log already had; C-quoted paths decode the full escape set (a tab decoded to a literal t broke the :(literal) re-probe); rename from/to lines override the ambiguous diff --git header; combined-diff line numbers follow the sides rather than the colour, so a " +" line no longer drifts every number below it. - A rename's old path stays out of the per-file decoration map, where it outranked a file re-created at that path; ignored records decorate as Ignored, not Modified; checkout <branch> gains the trailing -- that keeps a stale name from falling back to a worktree-clobbering path checkout; unstage before the first commit takes -f (worktree- safe with --cached); batches split by bytes as well as count for Windows' 32K command line; a deadline expiry reports Timeout, not "git could not be run"; error details keep both streams. - probe_status distinguishes "not a repository" from "could not ask": a dropped link keeps the cached status (stale beats blank) and rests 10s instead of erasing the panel, while a definitive not-a-repo also drops the cwd→root mappings so the panel stops drawing Loading for a repository that is gone. Probe and watch work are wrapped against panics that would wedge their in-flight bookkeeping forever, watch landings check the wipe counter, superseded probes relaunch through the debounce, and a refused network slot says so instead of eating the click. UI: - Reset --hard confirms with its own words (commits fall off the branch), not the discard dialog's; a merge commit whose prefilled message the user cleared is committable again; the disabled commit button distinguishes "nothing to commit" from "write a message". - Selection highlight matches on the diff source too, so a file staged and edited again no longer lights both of its rows for one overlay. - The graph materializes only the rows in the viewport window (5000 flex children per frame was most of a frame), row clicks carry the page Arc and an index instead of a deep Commit clone per row per frame, filter results are cached per (page, query), and a selected merge ring's hole matches the selection band under it. - A failed commit_files read says the list could not be read instead of "0 files changed"; the STAGED chip and the graph's relative times go through the i18n table; the keys-awaiting-a-caller list is pruned to the seven that still are; the orphaned PanelUntracked key is gone; the zh commit placeholder reads naturally. 2398 tests, 0 failures. Known flake: daemon::singleton's second-claim test, untouched by this branch, fails ~1 in 3 full parallel runs and passes alone. |
||
|
|
35bbad5155 |
docs(features): drop the removed orchestration skill entry (#457)
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
5f1ee966ec |
fix(windows): give the pane grip and drags in flight a cursor (#455)
Win32 ships neither an open- nor a closed-hand cursor, and gpui's Windows backend answers both with the plain arrow. The pane drag grip asked for `cursor_grab()` and so read as ordinary background there, and the pointing hand the sidebar's group header had worked around it with was dropped again the moment a drag began, since the active drag cursor is `ClosedHand`. Lift that workaround into `reorder::cursor_grab` so the grip and the group header share one answer, and pick the held cursor per platform too. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
a764d92132 |
fix(scm): sequence compound verbs, cap graph paging, back off failed loads
Review findings on this branch, all in the seams between async operations: - Commit-and-push, commit-and-sync, sync and discard-all dispatched both halves into the worker pool at once, so a push could resolve the branch tip before the commit (or pull) it was waiting for and quietly send the old one. Compound verbs now carry a ScmFollowUp that the first half's landing closure starts on success only; a refused commit, a failed pull or a cancelled confirmation drops the follow-up with it. - Push sent `git push <remote> <branch>` with the branch taken from the upstream's name — a bare name means a *local* branch, so `feat` tracking `origin/main` pushed stale local `main`. The refspec is now `HEAD:<branch>`, and the branch is validated with the full branch check since a `:` would smuggle a second refspec in. - `scm.committing` was armed before the amend confirmation and never disarmed on failure, so a cancelled prompt (or a hook rejection) plus any later unrelated HEAD move cleared a message that was never committed. It is armed at dispatch and disarmed when the commit errors. - Discard-all fed staged-only paths to `checkout --`, where a staged deletion sank the whole batch as an unmatched pathspec. Only unstaged paths go in, one confirmation covers both halves, and the two gits no longer run concurrently. - One "load more" click at 5000 commits grew `requested` past what `load_page` clamps to, so the freshness check never passed again and every frame refetched the full page. Growth stops at the cap, the button hides there, and a failing `git log` is remembered per key instead of being retried from every render. - A repository switch now drops the previous repository's page before anything can draw it or grow from it — a stale row's context menu used to build ops for the new repo with the old repo's rev. - A watch that failed to open was retried at frame rate, one host round trip per render; it now rests for WATCH_RETRY between attempts. - Non-network writes on a remote host ran under the interactive 20-second deadline while the server ran the job to completion, so a slow pre-commit hook was reported failed and then landed anyway. Every write now goes through git_with_deadline, 120s for local verbs. |
||
|
|
30b16c65b5 |
fix(settings): keep one restart button for the stale background server (#452)
The in-place-update notice carried its own Restart server button while the Server section right below it carried an identical one, both calling restart_daemon. Move the notice into the Server section: the stale build line sits under the header and its explanation replaces the generic one, so the single button that ends every running pane is the only one on the page. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
88bf9a5da5 |
feat(daemon): upgrade in place, keep pane screens across a crash, and give panes their own history (#449)
* feat(daemon): keep a pane's screen across a death nobody chose A daemon that crashes, is `kill -9`'d, or goes down with the machine takes every pane's replay ring with it, and the window comes back to a row of blank shells. The processes cannot be saved that way — nothing written to a file brings a process back — but the picture can. The daemon now keeps a capped tail of each pane's ring under the config directory, and a client whose `Attach` found nothing can ask, on the `Spawn` that replaces it, for the dead pane's screen. The new pane opens showing it, under a rule that says the shell below is new. - periodic and dirty-only: a ring that has not moved is not rewritten, so an idle machine does no IO at all. Write-through would be an enormous amount of write amplification for a few seconds of freshness. - capped at 256 KiB per pane, far below the ring's 8 MiB: the value of scrollback decays with distance from the bottom, and every byte here is a byte of someone's terminal on disk. - off by default. The ring holds whatever the pane printed, including echoed tokens, `env` output and agent transcripts; in memory that dies with the daemon, and writing it down is the whole feature and the whole cost. Files are 0600, and turning the setting off deletes what was kept. - dropped by relevance, not by calendar: a pane the user closed, or one no workspace names any more, has its file removed on the next sweep. Restored bytes are replayed at the geometry they were written at, and are preceded by resets — leave the alternate screen, show the cursor, restore autowrap, clear SGR — because a snapshot is cut at the front and can begin in the middle of any of them. * feat(daemon): upgrade the daemon in place instead of killing every shell Picking up a new build meant stopping the daemon, and stopping the daemon means every pane dies: the pty master is a descriptor this process holds, so when the process goes the slave side raises SIGHUP and takes the shell, the agent and the half-finished command with it. That is why the update path leaves the old daemon serving and Settings has to offer the restart as a thing you schedule for a quiet moment. `execve` does not have that problem. It replaces the image and keeps the process: same pid, same children, same descriptors, same file locks. The daemon now rewrites itself that way on `ClientMsg::Handoff` — it writes what it knows about each pane into a blob, clears FD_CLOEXEC on the pty masters, the blob and the singleton lock, and execs the new binary, which picks the panes back up on the other side. - **the seat travels on the command line, not in the blob.** The lock is still held by this process, so the new image must adopt the descriptor rather than ask for the lock again — asking would be refused by its own lock and it would stand down in favour of itself. A daemon that loses its panes is a bad afternoon; a daemon that exits leaves the machine with nothing serving, so that one fact has to survive an unreadable blob. - **the blob is unlinked before it is written.** It holds every pane's ring, which is the output `scrollback` makes people opt into storing; a handoff must not be a back door for writing it to disk. - **the exec is the last step.** Everything is staged first, so any failure before it costs a log line and the daemon carries on serving — which is what lets callers treat a failed handoff as "fall back to a restart" without having lost anything on the way. Native SSH panes cannot cross — their session is cipher state in memory, not a descriptor — so they are hung up first and the far end sees a clean close. Windows has neither execve nor a transferable ConPTY handle, so it keeps the stop/start path; the dialogs there still promise what they always did, and the new copy is shown only where it is true. Also retries flock on EINTR: a signal landing mid-call said nothing about the lock, but was reported as "could not be evaluated", which starts a second daemon beside the first — the split machine singleton exists to prevent. The end-to-end test sets a variable in the shell, hands over, and reads it back. Nothing but the original process can answer that, and the daemon's instance id changing while its pid does not is what says an exec really happened. * feat(shell): give each pane its own history when asked Two panes running zsh with `share_history` are appending to one file and reading each other's lines back, which is either the feature or the problem depending on what the panes are for. Someone with a pane per task wants Up to walk that task's commands, not an interleaving of four. Each pane can now have its own history file instead. It is seeded from the shell's real history, so a new pane is not blank, and what the pane added is appended back when it closes, so nothing typed is lost — a per-pane history that evaporated would be a way of losing commands, not of organising them. The seeding is done by the shell, not the daemon, and that is the only reason it works: `HISTFILE` belongs to the user's rc file and can point anywhere, long after the pane's environment was decided. tty7's snippet is appended to the rc it wraps, so it runs after that decision and is the one place the real path is known — it copies the tail, records how much it copied, and repoints. Both shells load history after their startup files, so the switch lands before the first line is read. The daemon's half is a filename, a rename when a restored pane inherits its predecessor's file, a merge on close, and a sweep for the panes a killed daemon never got to retire. Off by default: shared history is what a terminal has always done, and someone who did not ask for the change would experience it as their history mysteriously forgetting the other window. bash and zsh only — fish and PowerShell do not keep a HISTFILE, and a shell launched with the user's own arguments gets no snippet to repoint anything in. * fix(daemon): store pane screens on the shutdown a restart actually uses The periodic writer covers a death nobody prepares for and the SIGTERM path covers a signal, but the restart the app itself performs goes through ClientMsg::Shutdown — which killed every pty without taking a copy first. That is the one shutdown where the panes are expected back. * fix(daemon): leave nothing dangerous behind when a handoff fails or lands Review findings on the in-place upgrade and per-pane history: - A failed exec now puts back everything it had staged: FD_CLOEXEC on the seat and every pty master (a child inheriting the seat keeps the flock held past the daemon's death, so no future daemon could seat itself), and the SIGPIPE disposition plus this thread's signal mask, both of which Command::exec resets on its way to the attempt — without this, the still-serving daemon dies on the first client that hangs up mid-write. - The adopting image restores close-on-exec on the seat and on every adopted master, so children it spawns later cannot hold a pty open past its pane, or the seat past the daemon. - The target binary is checked before the handoff gives anything up: native-SSH panes are hung up on the promise that this process is about to be replaced, and an exec that was never going to work must not collect on it. - The integration snippets raise HISTSIZE/HISTFILESIZE (bash) and SAVEHIST/HISTSIZE (zsh) for the pane's private history file. At their defaults the exit rewrite truncates the file below its own seed mark, which the merge-back rightly reads as "replaced under us" — silently losing the pane's commands for anyone with more history than the caps. - The restart dialog's promise now binds the action: where the copy said "nothing is interrupted", a failed handoff is reported instead of silently traded for the restart that kills every pane. - The scrollback writer checks the ring's mark before cloning it, so an idle pane no longer costs a full ring copy under the state lock every tick. Each behavioural fix carries a test that fails without it; the history truncation one was verified to fail with the snippet change removed. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
44f0683d0a |
fix(sidebar): cut labels on grapheme clusters, not on chars (#450)
The sidebar's elision measures against real glyph widths but slices by
`char`, so it can satisfy every width check and still hand back a torn
cluster. Scanning budgets from 30px to 200px over emoji fixtures, 48
widths produced output no font can render as intended:
"release-…\u{200d}👩\u{200d}👧" a joiner with nothing in front of it
"lon…\u{fe0f}" the variation selector lands on the ellipsis
"abcdef…🇳ghijklmnopqr" half a flag, which renders as a bare N
A tab title carrying an emoji is not exotic — plenty of TUIs and coding
agents put one there — and the second case is the same U+FE0F this repo
already carries an alacritty patch for.
`elide_keep_edges`, the tail-only fallback, and `short_title`'s 40-glyph
clamp now index grapheme clusters. `elide_path_keep_tail` cuts on `/`
and was already safe. Widths are unchanged: clusters are measured the
same way chars were, so every existing elision test still passes on the
same fixtures.
`unicode-segmentation` is already in the tree via gpui; pinning it here
adds one line to Cargo.lock and no new code.
Tests assert the property rather than the symptom: whatever survives on
either side of the ellipsis has to be a cluster-aligned prefix and
suffix of the input. That catches any tear, not just the three shapes
found here. Written first, confirmed failing on all three cut sites, and
green after.
|
||
|
|
1fd99ca7c6 |
feat(sidebar): tail-first label elision and a hover card for hidden details (#446)
* feat(sidebar): tail-first label elision and a hover card for hidden details ff * fix(sidebar): keep the head of a non-path label, and let the card expand what the row hid The elision landed with four gaps between what a row hides and what the hover card can give back. A renamed tab is elided like anything else, but `sidebar_info` still carried the old tooltip's guard and bailed out on any non-empty `tab.name`, so a long custom name was shortened by the row and the card refused to spell it out. An empty terminal title took the opposite path: the row falls back to `Shell 3`, the card compared that placeholder against the empty string it came from, found them different, and opened on a row that had hidden nothing. Both came from deriving the same strings twice; the row now hands `sidebar_info` what it rendered next to what it rendered it from, and the card is decided by comparison alone. `elide_path_keep_tail` was applied to every title, but a title is not always a path — `npm run dev`, or a name someone typed. Dropping the head of those says less than the truncation this replaced, so `elide_label` picks the rule: tail-first for a path, both edges otherwise. `elide_keep_edges` promised both ends and delivered neither on a token with no break in reach: the head ran to the 12-glyph cap, `head…` alone overran the budget, and it fell through to a bare tail. It now tries shorter heads before giving up, and never trades the whole tail away for a longer head (`feature/…` became `fea…thing`). Also: measure the active row at the MEDIUM weight it actually renders at; subtract the list's own padding from the text budget, so a label that "just fits" is not handed back to CSS truncation; rejoin a path with the separator it arrived with, instead of spelling one tab `C:\Users\dev\app` while it fits and `C:/…/app` once it does not; count the gap between the two diff counts rather than the space standing in for it; let the card wrap instead of truncating the one string it promised in full; and read the remote host off the same leaf the title came from. The elision tests shape through gpui's `NoopTextSystem`, where every glyph is one em — deterministic across the three CI targets, but blind to the proportional and CJK widths this exists for. Said so where the fixtures are built, rather than implying the pixels are real. --------- Co-authored-by: l0ng-ai <ysdpk123@gmail.com> |
||
|
|
86799220ca |
feat(splits): rearrange a tab's panes by dragging one onto the layout (#445)
* feat(splits): rearrange a tab's panes by dragging one onto the layout Hovering a pane floats a small grip along its top edge; dragging it picks the pane up and puts it somewhere else in the same tab. Three landings, resolved from where the pointer is: * a pane's edge — split that pane and take the side dropped on * a pane's middle — trade the two panes' places * the band along the outside of the tab — sit beside everything else as a full-width or full-height band, which is the only way to say "make this a full-height column" in one gesture from the middle of a 2x2 The landing is highlighted while the drag is in flight, and is offered only once the tree agrees the drop changes something, so the highlight is never a promise the drop does not keep. * pane: move_leaf / move_leaf_to_edge / swap_leaves, each built on a clone and installed only when the layout really differs * pane_drag: the pointer-to-landing geometry, the drag state, and the grip * tree_sync: reconcile a tab that kept its panes but changed shape with a single PaneMove instead of closing and rebuilding the tab * feat(splits): drop a pane beside its neighbours, not on top of one Trying the drag out on real layouts turned up three ways the drop model asked for more precision than it should have. A drop on a pane's side always halved that pane, so putting a new column into a row of columns was only reachable at the very edge of the window, where the band rule took over. A side facing a neighbour in the same row or column now joins that run: the newcomer takes an equal share and the others give it up in proportion, keeping whatever relative sizes they were dragged to. A side facing across the run has no run to join and still halves the pane it landed on. The band along the tab's edge was a flat 26px, which on any real window is a hair's breadth. It is now measured against the pane it is read in — a sixth of it, floored at 32px and capped at 120 — and only counts on a side that faces the window rather than another pane. Landing there takes an even share of the columns that side already has instead of half the tab, so a third column is a third and not a half. The highlight is no longer drawn from the rule. The drop is carried out on a deep copy and the dragged pane's new rectangle is measured off it, so the preview and the result cannot disagree; the copy is deep because sharing a run out writes ratios the live tree's splits hold in common. Also: the grip is a quiet 22x3 bar that grows to 40x5 under a fixed 56x10 target (it needs an id of its own, or gpui settles its size before the group-hover is known), and every rearrangeable pane keeps an 8px strip clear above its grid so the grip never sits on the first row. * fix(splits): pin a drop to the pane it was offered against Review follow-ups on the pane drag. A drop zone named its target by position in the tab's leaf order, but it is read on one frame and carried out on the next: a pane closing in between shifts every index after it, and the drop lands beside a pane the user never aimed at. The zone now carries the target itself once the frame that drew it has resolved it, so a target that has gone refuses the drop instead of sliding it sideways. Alongside it: * `Pane` is no longer `Clone`. The two copies it can be asked for differ in whether they share their splits' sizes, which is not a difference to leave to whichever one `.clone()` happens to mean; `shallow_clone` is now named and private, next to `deep_clone`. * `edge_landing` no longer hands back a share that only a test read. The test reads it off the split the landing produced instead, which is the number the drop actually lands. * A test pins the invariant the drop zones rest on: `leaf_rects` comes back in the order `leaves` does. * Drop a doc comment that had landed on `close_focused` describing a different method, and an `Option` in `drop_pane` that was wrapped only to be unwrapped two lines later. * The changelog claimed every rearranged tab now syncs as one `PaneMove`. Only a drop beside a single pane does; a drop beside a whole group is not something `PaneMove` can name, and still takes the rebuild. Both entries move under `Unreleased` — v26.8.2 was tagged before either landed. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
c2b6ba2ec0 |
Merge pull request #442 from l0ng-ai/fix/issues-426-430
fix: small-window Settings layout, vim caret on a raw pty, wheel zoom, theme preview and cross-locale palette search |
||
|
|
2f31a11df0 |
fix(settings): size the nav floor for the longest label in any locale
SidebarMenuItem clips its label rather than eliding it, so a 140pt floor that fits English cut a glyph in half elsewhere: zh-CN lost the right half of the last character of 窗口与标签页. Size the floor for ja-JP ウィンドウとタブ, the widest of the three, which costs the page 35pt at the narrowest window and keeps every nav label whole. |
||
|
|
3dcffe6f4e |
fix(windows): grow a remembered bound back up to the minimum size
window_min_size governs what a drag may do to a window, not the bounds it opens with, so a remembered bound walked straight under the declared 720pt minimum — the reported settings window measured 641. Clamp the restored size on the way in, keeping the origin. |
||
|
|
c13efbce32 |
fix(terminal): draw a multiline history entry on one menu row (#436)
A command composed in the inline editor keeps its newlines when `submit_command` pushes it into the in-session history, even though `history::append` and the zsh/bash/fish parsers all refuse to carry an entry across lines. gpui breaks text on `\n` whatever `white_space` says, so one such entry painted a dozen lines inside a one-line row and covered the whole reverse-search menu. Fold line breaks to a visible `↵` when drawing — one char for one char, so the fuzzy matcher's highlight positions still line up — and leave the stored entry untouched, so recalling and running it are unchanged. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
8b08f51773 |
ci: drop the two Claude review workflows (#441)
Remove `claude-code-review.yml` and `claude-review-fork.yml`. Both are advisory-only and never gated a merge, so the required checks on main stay exactly `rustfmt` and the three `build & test (<target>)` jobs from ci.yml. Nothing else references them: the `claude-review` label and the CLAUDE_CODE_OAUTH_TOKEN secret were used only by these two files. Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
8dbc7efa1a |
fix(cli): stamp panes with the workspace that holds them, not the client's name (#425)
* fix(cli): stamp panes with the workspace that holds them, not the client's name A pane's owner names the workspace allowed to attach to it. The CLI wrote a literal "tty7-cli" there for every pane it made, so a window opening on a CLI-built workspace found none of them attachable: it spawned a fresh shell for each tab, orphaned the live ones, and — because the tree still carried each pane's agent session — greeted the user with a failing `claude --resume <id>` in every one of them. Both spawn paths now pass the workspace id, and restore treats an owner that parses as no workspace as no claim at all, so panes already stamped by an older CLI attach instead of stranding. * fix(cli): let the OWNER column speak only when it disagrees with WS Now that a pane's owner is the id of the workspace holding it, printing both spells the same id twice on every row of `pane ls --all` — and buries the rows that matter. The column now shows a dash when the two agree, so what is left is exactly what is worth reading: a pane its holder may not attach to, and an orphan still naming where it belongs. --------- Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> |
||
|
|
150f6ff76f |
fix(settings): stop the page being the only column that gives width back
The settings nav, the SSH host list and the theme panel were fixed widths that never yielded, so the page they frame absorbed every shortfall. In a 641pt window — the one the report came from — that ran all the way down. On SSH, 220 of nav and 280 of host list left the detail panel 141pt and its empty state painted a couple of hundred points past the right edge of the window. On Appearance with the theme panel open the page got about 125pt, and a Chinese description came out one character per line. The columns are now allocated against the window instead of asserted: each list is handed its full width, then gives back a share of whatever is missing until the page reaches 420, and no list goes below the width at which it stops being itself. Below the width where the nav, the panel and a readable page cannot all fit in one row, the theme panel stops being a column and lays itself over the page — it is a temporary layer over one choice, and Escape already closed it first. The floor the page keeps is derived rather than picked: it is what the narrowest window in the wild leaves the SSH page, the one that spends a second list, once both lists stand on their own floors. It is a target for the allocator and not a `min_w` — a floor a flex row cannot honour does not push its siblings back, it overflows, and overflow here means content painted off the window, which is the failure being fixed. What makes the floor liveable instead is that the wide controls can now shrink into it. The thresholds that decide when a row stacks are widths a label needs, so they follow the interface font size — at 24pt every label is half as wide again while the slider beside it is still 240px. The rows that were hand-rolled rather than built by `settings_row` get the same treatment: the keybinding preset and prefix rows stack at the same width, every binding row lets its label wrap and its key caps wrap to a second line, the theme card drops its preview and stops pushing "change theme" off the card, the SSH quick-connect field shrinks instead of running past the pane it sits in, and a port-forwarding rule takes two lines — or three, at the width the report came from — rather than one that does not fit. |
||
|
|
14c092b0d1 |
feat(palette): preview themes live while the picker is open
Picking a theme from the command palette closed it, so finding out what a theme looks like meant reopening the palette and retyping the search for every single one. The theme picker now applies whatever row is highlighted straight to the running window and stays open: Return persists the pick, Escape or any other way of closing the palette puts the previous theme back. A preview only touches the in-memory config, so arrowing through the list never writes config.json. The picker opens on the theme already in use, so opening it changes nothing by itself. |
||
|
|
54960be6bf |
feat(palette): match commands in every locale, not just the shown one
The palette matched a query against the label it was rendering and nothing else, so a window running in Chinese answered "no matching commands" to `theme` — the English wording of that row did not exist anywhere the filter could see it. Each entry built from a locale key now carries the same key as every other locale words it, plus the stable command id, as hidden search aliases. They are built once with the entry, cost nothing per keystroke, and are never rendered: the row keeps showing its localized label, and an alias hit scores just below the same hit on that label so a visible match still comes first. |
||
|
|
68d6b4b062 |
feat(terminal): zoom the font with the platform modifier and the wheel
Holding Cmd (Ctrl off macOS) and scrolling over a terminal now resizes the font instead of the scrollback, which is what you reach for when showing a pane to someone else. A wheel detent is one step whatever the platform bills it as, and a trackpad accumulates until the fingers have travelled three lines, so a flick does not run the font end to end. Steps go out as the existing IncreaseFontSize/DecreaseFontSize actions, so the clamp and the saved setting stay in one place. |