* 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>
* 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>
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.
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.
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>
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>
* 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>
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.
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.
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.
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.
Typing `:wq!` in vim wrote it onto the row being edited instead of the command
line, and left the cursor there. The command line is not what moved: the cursor
is.
`ParkedCursorRepair` (#362) exists for conhost, which brackets every frame it
paints with `?25l` … `?25h` and, on the frames where it did not paint the
cursor, commits the show wherever the last erase or write left it. The repair
pairs the hide with its show, calls the show parked when the run moved the
cursor around to paint but did not end on a move, and puts the cursor back on
the cell the hide caught it on. The reader ran it on every platform.
Off Windows there is no conhost in between and the application owns the cursor.
Captured from vim 9.1 on a raw macOS pty, opening the command line is
\x1b[?25l \x1b[11;10H: \x1b[1;1H \x1b[11;1H \x1b[K \x1b[11;1H: \x1b[?25h
— a run full of moves that ends on the `:` it wrote, which is exactly the shape
the scanner calls parked. So the repair dragged the cursor off the command line
and back onto the file, and vim, which echoes the following keystrokes as bare
bytes with no positioning of their own, wrote `wq!` over the text.
Gate the repair on `cfg!(windows)`, the way `conpty_resize` already is: the
artifact is ConPTY's, and on a raw pty the cell a frame leaves the cursor on is
the cell the application meant. This also keeps the scanner off the client's
output path entirely on Unix, where it used to walk every batch. A macOS or
Linux client attached to a *remote* Windows daemon loses the repair with it —
the same limit `conpty_resize` has — which costs a stray caret there and buys
back an unshredded screen on every local pane.
main dropped the client-side command-mark store (#404) while this branch
had just started reading it: the close confirmation names the command it
is about to end, and the mark was the only place that text existed on the
client. Keep both. The OSC 133 tokenizer main left in place already sees
every mark, so the command line now rides alongside `zle_reading` and
`shell_vi_mode` as one shared string — set on `C`, cleared on `B` and on a
`C` that carries no line — instead of a store with a list, a lock and a
cap. `busy()` reads that.
The rest:
- settings.rs takes main's opaque overlay surface and background layers,
keeping this branch's no-match note and scrolled body. The inner
`.bg()` goes, per main's reason: the root already paints it, and a
second fill hides the theme image.
- i18n keeps this branch's `every_key_is_translated_in_every_locale`,
which walks `L10nKey::ALL` in all three locales, over main's
hand-listed zh coverage test it replaced. It immediately caught three
of main's new backdrop keys reading English in ja — Mica, Mica Alt and
Acrylic, which is what Japanese Windows calls them, so they join the
allowlist with that reason.
- app.rs keeps both sides' tests and drops both sides' now-dead imports:
`window_background` (main deleted the function) and `humanize_action`
(this branch's keybinding note uses `keymap::action_entry` instead).
Verified: `sleep 300` then ⌘W asks about "sleep 300"; ⌘W after it ends
closes without asking.
* feat(windows): add native backdrop material presets (Mica / Acrylic / Blur)
Adds a Background material dropdown (Auto / Blur / Mica / Mica Alt /
Acrylic / Off) that maps onto the native Windows backdrop APIs already
provided by the gpui fork — Mica and Mica Alt via
DwmSetWindowAttribute(DWMWA_SYSTEMBACKDROP_TYPE), Acrylic via the new
DWMSBT_TRANSIENTWINDOW material, and Blur via the classic
ACCENT_ENABLE_ACRYLICBLURBEHIND path — with no fork changes required.
* config: introduce WindowBackdrop in tty7-core with lenient kebab-case
deserialization, defaulting to Auto for existing configs
* theme: resolve the backdrop through a build-number fallback chain
(Mica/Mica Alt need Windows 11 22H2, Acrylic needs 22H2 natively and
1809 via classic acrylic, Blur needs 1809; older builds fall back to
plain translucency) and default the background alpha to
SYSTEM_MATERIAL_OPACITY (0.82) while a material is active
* settings: replace the blur toggle with a localized backdrop dropdown
that only lists the presets the current Windows build actually
supports, and keep the settings panel fully opaque so workspace
translucency never shows through it
* theme: make the file sidebar and right detail panel follow the window
opacity so the backdrop material shows through the whole workspace,
keeping row-level accents opaque for readability
* i18n: add backdrop keys for en, zh-CN and ja-JP, covered by the
translation completeness test
* feat(theme): let the sidebar and right panel follow the window opacity
* update GPUI
* fix(windows): gate the sidebar translucency to translucent windows and sync the opacity slider
fix(windows): gate the sidebar translucency compensation to active materials
* fix(windows): derive the material opacity default from the resolved appearance
* fix(theme): keep WindowBackdrop semantics consistent on non-Windows
f
* fix(theme): stop Windows-only materials from pinning the blur on other platforms
* docs(changelog): document the Windows backdrop material settings
* refactor(theme): share the default window-opacity derivation
* fix(ui): keep gradient presets behind the settings panel and scope its fallbacks
* fix(ui): keep the settings theme picker legible and the backdrop label honest
f
* fix(theme): let every backdrop variant defer to the local blur toggle on non-Windows
* fix(settings): restore the backdrop dropdown selection on locale refresh
* fix(ui): keep the opened-file editor surface opaque under window translucency
* fix(settings): rebuild backdrop options after selection
* fix(settings): ignore synced windows backdrop overrides on other platforms
* fix(settings): preserve synced windows backdrop on non-windows reset
* fix(diff): keep the full-window overlay background opaque
* fix(windows): keep Auto opaque and stop the backdrop from misreporting itself
Ten findings from a review of the backdrop-material work, all in the
Windows-only paths.
The root one: `material_active` treated `Auto` as a material whenever the
legacy blur toggle happened to be on. `Auto` is the default in every config
written before this setting existed, and plenty of them carry
`window_blur: true` from the switch that no longer renders on Windows, so an
untouched install would drop from opaque to 0.82 alpha - with its file
sidebar and right panel at 0.15 - on first launch after the update, with no
visible control to undo it. Only an explicit pick in the dropdown now buys
the translucent defaults. The switch comes back on Windows while the
backdrop is `Auto`, since that is exactly when the legacy flag still decides
something.
The rest:
- Mica and Mica Alt fell back to `Blurred` with no lower bound, asking for a
blur that does not exist below 1809 - and build 0, which is what a failed
`RtlGetVersion` reports. They now degrade to plain translucency like
`Blur` and `Acrylic` already did.
- Acrylic is no longer offered below 22H2, where it resolves to the very
same classic WCA blur as `Blur`. A test now asserts that no two offered
presets render identically on any build.
- `reload_from_config` re-applied the theme and the opacity slider but not
the backdrop dropdown, so an external config change switched the window's
material while the control kept naming the old one.
- The settings, opened-file and diff overlays were made opaque so the OS
backdrop cannot show through their text; that also hid the theme
background image, which used to show through them. They paint their own
copy of it now, and the fill they share moved into
`theme::overlay_background`.
- The SFTP transfers tray painted `workspace_surface_color` inside the right
panel, which already paints it, stacking the same translucent surface
twice into a darker band with a hard seam.
- `apply_theme` re-issued `set_background_appearance` on every `Config`
mutation in every window. With a DWM material that now costs a
`SetWindowPos(SWP_FRAMECHANGED)` frame recalc, so dragging the opacity
slider recalculated the frame once per mouse sample; it is skipped when
the appearance is unchanged.
* fix(ui): dim the overlay background image, and stop telling Windows it is macOS
Two defects found while driving the previous commit's changes in the app.
The overlays repaint the theme background image over their own opaque fill,
so it survives them being made opaque - but nothing dimmed it. Before those
overlays were opaque the image reached the eye through their translucent
fill; painting it at full strength put the settings text straight on top of
the wallpaper and made the panel unreadable at any image opacity above about
half. They now paint the image and then the workspace's own fill over it,
which is exactly the strength the image had through these overlays before,
and which needs no new constant to say so. Shared as
`app::overlay_surface_layers`, empty when the theme has no image so a
themeless window paints no second pass of anything.
The Windows-only blur row reused `SettingsBlurDesc`, whose text ends in
"(macOS)". It gets its own key in all three locales, describing the job the
flag actually still has on Windows: feeding the `Auto` material.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Removing the Outline panel (#374 / #375) took away the only reader of the
client-side command marks. The scanner kept running on every batch of PTY
output, and it was the one scanner that forced the batch to be split before it
reached the emulator, so it was not free.
Gone: `MarkScanner`, `Marks`, `CommandMark`, `record_mark`, `Cut::Mark` and the
tests that only covered them. With the cursor cut as the sole cut left, the
offset sort is a no-op and goes too — `ParkedCursorScanner` already reports in
ascending order — and `Cut` itself collapses into a plain `CursorCut`.
Kept: `zle_tok` and `mode_tok` read the same OSC 133 bytes and are load-bearing
for `zle_reading` / `shell_vi_mode`, including the deliberate live-vs-snapshot
split. Daemon-side OSC 133 handling is untouched.
Dropping `marks().clear()` left `clear_scrollback` with no anchored-state
invalidation at all, and it never had any for the other store that needs it:
kitty image placements are anchored to an absolute scrollback row, so purging
the history moves every anchor and the frame paints over unrelated text or
resolves past the viewport, with no redraw coming since the daemon does not
replay out-of-band image frames. Clear the image store there, as the reattach
path already does, and route the purge through
`Term::clear_screen(ClearMode::Saved)` so a selection reaching into the
discarded rows is invalidated instead of clamping onto the viewport.
Closes#378
The case meant to show that a real shell reports no problem named
`/bin/sh`, a path that does not exist on Windows — so the one assertion
guarding the happy path asserted the opposite there, and the Windows job
failed on it the moment the suite got far enough to run. Point it at the
running test binary instead: a file that exists and carries the execute bit
wherever the tests run.
`harness_with_tabs` hands back the panes' `UnixStream`s, so it is gated
`#[cfg(unix)]` — but `rename_gpui_tests` imported it under a plain
`#[cfg(test)]`, so the Windows job failed to compile the test binary with
E0432 while every other target passed. `ssh_rebuild_gpui_tests`, which uses
the same family of harness, already carries the right gate.
Five places where a workspace, a tab or an attachment was real on one side of the socket and invisible on the other. They share a root: the GUI kept its own list of which workspaces exist (WindowViews on disk) and consulted the machine tree only for the ones already in that list, so anything created by another client was unreachable by construction.
- The switcher lists workspaces the machine holds but this client has never opened, and opening one keeps its id instead of claiming a fresh one.
- for_workspace_at hydrates whenever the machine holds tabs, so opening a workspace no longer saves an empty session over them.
- finish_hydration writes a full window back over an empty tree, which is what puts a ws rm'd workspace back under the same id.
- A deletion nothing has open is forgotten here too, instead of haunting the switcher until a restart.
- Workspace::attachment travels over the wire (minus the token that proves the hold, which stays on the connection that owns it) and is stripped in persist, so tty7 ls can name the host holding a workspace.
- tab ls / ws tree fall back through name -> agent -> cwd leaf -> process name, and tab ls grew a read-only GROUP column.
- tty7 new --open raises a window on the workspace it just made.
Every icon in the detail panel's top row is drawn by the same tile at the
same 13px, so the sizes looked settled in the code — but each one paints a
different amount of the 24 box it lives in. Measured as rendered ink,
git-branch was 14.6 wide against folder's 19.9, a 36% spread on a row of
five, which is why the Changes tab read a size smaller than the tabs on
either side of it. The two folders were also squat, and folder-open — the
Reveal button directly under the folder tab — put a second, differently
sized folder in the same column.
Redraw the three outliers into the box info and panel-right already use:
3.4–20.6 across, 4.4–19.6 down. git-branch stops 4% short of the others on
purpose — it is three solid discs, and a filled shape reads heavier than a
hairline outline at an equal extent.
The detail panel carried a private run of pixel sizes — 12 for body,
11.5/11 under it — which put its primary text at the size the rest of the
window uses for secondary text, so it read a step smaller than the sidebar
beside it. Its mono values sat at the same px as their sans labels, where a
larger x-height makes them look a size bigger, so a row read as two sizes
instead of one line. `forwards.rs` and `sftp.rs`, both drawn inside that
panel, had copied the same numbers.
Put the panel back on the rem ladder the rest of the chrome already uses,
with mono a notch under the sans it pairs with, and make the rem itself
settable: `ui_font_size` defaults to gpui's own 16, so an existing config
renders unchanged, and every window's root sets it, which reaches the whole
interface at once. The terminal grid is absolute px from `font_size` and
does not move — a display that is not Retina can now have bigger chrome
without touching the text in the panes.
A WSL pane whose distro defaults to fish never started: `wsl.exe --` hands the
command line to that default shell, which parses the POSIX bootstrap before
`sh` can receive it. Every wsl.exe invocation that carries an argv — the pane
bootstrap, `RemoteLink::wsl`, `RemoteLink::wsl_shell`, and the installer's
`sh -s` / `tee` calls — now goes out under `--exec`, which runs the program
itself. `--` keeps its other meaning, "just start the default shell", for the
no-argv case.
Those fish panes then came up with no shell integration at all, because the
bootstrap only ever had a bash arm: no OSC 133, no OSC 7, so no prompt marks,
no exit status, no cwd, and `tty7 wait` and busy/idle status dead in the pane.
The `case "$SHELL"` now has a fish arm carrying FISH_INTEGRATION inline, the
way `remote::bootstrap_command` already does over SSH — POSIX-quoted, since
`sh` parses this script rather than the user's own shell.
The PTY tests' completion marker is anchored on its terminator: `133;D;1` is a
prefix of `133;D;127` and `133;D;130`, so they would have gone green on a
bootstrap that never ran the command they type.
fish's history joins the Ctrl+R menu and inline completion, locally and on
remote hosts.
`fish_history` looks like YAML and isn't: fish escapes only `\` and newline and
quotes nothing, so a YAML reader drops every record holding a `: ` or a leading
`[` and truncates anything with a ` #` — conventional-commit messages, `echo
a: b`, `[ -f x ]` tests. It is read with a line scanner shaped like fish's own
reader instead, which also keeps pane construction off a per-record libyaml
parse.
Each history file fetched from a remote host now carries the name it came from,
so the far end's fish records go to the fish reader rather than arriving as
literal `- cmd:` rows in the menu.
Multiline commands are skipped rather than half-recalled: `append`, the shell
handoff and the reverse-search menu are all single-line, so an entry that
cannot be run is worse than one that isn't offered.
main shipped v26.8.2 and 15 fixes while this branch was open. Resolved:
- zh: main's #417 decided the background process is called "server" in
Chinese, and that decision is newer than this branch's "服务器" — took
it, kept this branch's typographic quotes around {machine}, and
whitelisted SettingsServer in the new every-key-is-translated test,
since the zh heading is now that English word on its own.
- presets.rs: this branch factored main's inline `clear` closure into
Theme::clear_ink / ansi_seed; same arithmetic, so kept the methods.
#400's border and caret floors and #413's legible-palette flag both
survive untouched.
- app.rs: took main's Option-typed `alive` argument, kept this branch's
note on why a dropped tab is worth a sentence.
- README / docs: agent count is now exactly 18 with Oh My Pi, so the
precise number replaces both "17" and "~18"; the zh feature doc keeps
its translated menu names and gains Oh My Pi in the fork list.
Six keys main added are gone because the surfaces that used them were
rewritten here: the home screen's relative time now runs to years, hook
failures name install vs remove, Full Screen left the View menu on
purpose (AppKit adds its own), the SFTP filter says "search files", and
the settings index titles its CLI row by its own label.
Two quick downloads both found the name free — the file does not exist
until the transfer creates it — and the second wrote over the first. With
every name in range taken it now says so instead of overwriting.
Output scrolls the grid, so the stored point names a different line by
the time the rescan runs — and the highlight silently latched onto
whichever occurrence had taken over that screen position.
`git commit -m "fix bug; retry ` put the next word in command position,
so the menu filled with every binary on PATH instead of the files the
argument wants — and remote panes got nothing at all.
Any status but Idle counted as busy, so the green Done badge — the very
cue that sends a reader to close the tab — bought a dialog claiming the
agent was still working.
The pending target lived in one shared field that every other close path
cleared, so a tab exiting under the sheet swallowed the user's Close — or
redirected it. The target now travels with the question, tabs are named by
id rather than index, and the answer carries back as a flag instead of
being read off state, which also stops a tab's last pane confirming twice
and stops a stale slot letting the next Cmd+W kill a busy pane in silence.
Bulk closes go back to skipping only warn-on-close SSH tabs: skipping busy
ones made Close Other Tabs a menu item that quietly did nothing.
The remote's settings_row_wrapping and the local measured stacking are two
answers to the same bug: an SSH form row squeezing its label to a letter
per line. Keep the measured one — it is the later fix, it covers every
page rather than the fourteen SSH rows, and its test pins the SSH page as
the first to cross the breakpoint. The opt-in wrapping helper and its call
sites go with it; the Windows completion-test fix is kept as is.
The menu was anchored under the word being completed and capped at a flat
480px, with nothing checking that 480px was there. In a split — two panes
side by side is the ordinary case — it was not: the box ran past the
pane's right edge and the overflow was clipped, so a row read "switch
Switch bra" with the sentence stopping at the divider.
That clip is also what the description elision exists to prevent. The
budget is measured against the menu's width, so with the width unchecked
it kept eliding to 480px and the ellipsis it added landed outside the
pane. The one signal that the text had been cut was the first thing lost.
Cap the width at the grid and pull the box back inside — which is what
the history menu two functions down has always done — and hand the real
width to `description_budget` so what it elides is what fits. A wide pane
behaves as before: the menu stays under the word until it would hang over
the edge. A pane narrower than the menu pins it to the left, the same
place the history menu sits.
Verified in a dev window, split in two: "git sw" now shows "switch Switch
branches" whole, and unsplit the menu still opens under the word.
Same shape as the keystroke fix one commit ago, and the same two
branches: neither the editor path nor the raw-pty path moved the
viewport, so ⌘V while scrolled up put a whole command on a prompt that
stayed off screen. Dropping files onto a pane goes through here as well,
which made a dropped path vanish just as quietly.
The jump also clears the selection, which the tail of this function was
doing by hand, so that line goes with it.
Verified in a dev window: 200 lines printed, scrolled back, ⌘V — the grid
snaps to the prompt with the pasted command on it. Both paths are held by
tests that fail without the jump.
Scroll up into the scrollback and type: the characters went to the shell,
the prompt line grew, and the viewport did not move. Nothing on screen
changed, so the pane read as having stopped listening — while it was in
fact accepting every keystroke somewhere the user could not see.
`commit_text` has three branches and only the last one jumped to the
prompt: the one taken when tty7 is *not* driving the line. At a shell
prompt with OSC 133 reporting — the normal case, and the one the editor
features exist for — the text goes into `cmd` and returned early. Same
for a reverse-search query. `handle_editor_key` has always jumped, so the
two halves of the same editor disagreed: Left and Backspace came back to
the prompt and the letters between them did not.
Hoisted the jump above all three branches, where it describes the whole
function: text typed goes to the prompt, so the view has to be looking at
the prompt. Held by a test that fails on the old order.
Verified in a dev window: 200 lines printed, scrolled 15 notches back,
typed — the grid snaps to the prompt with the characters on it.
The file tree's inline rename is the last box in the app that opens on a
value and leaves the caret at the end instead of selecting it. That is
not an oversight: it was wired up, and it half worked. The selection
lands — typing does replace the name, and the state reads 0..9 for
"README.md" across twenty forced frames with the box focused — but that
input paints no selection band, while the SFTP form's paints a plain grey
one from the same component and the same call.
So the visible result of selecting there is a caret at the end of an
unchanged name, followed by the whole name vanishing on the first
keystroke. That is worse than the caret being in the wrong place, which
at least tells the truth about what typing will do. Reverted to the
caret, with the finding written down next to the helper so the next
attempt starts from what was already ruled out rather than from the top.
The edit forms' OK was always solid, but committing an empty box returned
without doing anything and without saying why — so the button read as
broken rather than as not yet applicable. New Folder is where it shows:
open it, click OK before typing, nothing happens at all.
Grey it out until the box holds something, which is what the worktree
prompt's Create already does with a name it cannot use. The form redraws
as the name is typed, so it lights up on the first character.
Blank covers all four: a folder, a file and a rename all need a name, and
an empty permissions box is not an octal mode either. A *wrong* mode
still goes through and comes back with the error the panel already
writes — that sentence says more than a dimmed button would.
A box that comes up with something already in it — a rename, a suggested
worktree name, a path, a mode — is showing a value to accept or replace,
not text to write around. Three of them treated it as a prefix and one as
a suffix.
`InputState::default_value` parks the caret at offset 0, so New Worktree
Tab's suggested name turned "login" into "loginfeature", the SFTP rename
box turned "notes" into "notesREADME.md", and the SFTP path box and the
chmod box did the same. The rename boxes fixed earlier this branch went
to `set_value`, which at least appends instead of prepending, with a note
that selecting outright was what the platform does and was out of reach
because `InputState::select_all` is `pub(super)`.
The find bar found the way through last commit: the input binds ⌘A to a
public `SelectAll` action, and a `FocusHandle` takes an action directly.
`ui::prefill` is that, once, for everything — including the find bar,
which now goes through it rather than keeping its own copy.
One thing the find bar's version got away with and a panel does not: the
action routes along the dispatch tree of the *last frame drawn*, and
frame callbacks run before that frame is drawn. A terminal pane redraws
on its own (the cursor blinks), so a second frame always came; the SFTP
panel is still, so the rename box was left unselected. `prefill` now
dispatches, looks at whether the selection landed, and waits for another
frame if it did not — bounded, and it stops the moment it takes.
While in there: the four SFTP edit forms — New Folder, New File, Rename,
chmod — opened cold. The box was not focused, so naming a new folder
meant clicking into the field first, and Return did nothing once you had;
only the OK button committed and only Cancel backed out. Everything else
in the app opens focused, submits on Return and cancels on Escape. Now
these do too, through one helper that also drops the subscription when
the form goes away.
Verified against sshd on localhost: New Folder opens focused, Return
creates the directory and closes the form, Rename opens with the name
selected, Escape backs out. And the find bar still selects its kept
query, so the shared version did not cost it anything.
The find box keeps the last query on purpose — reopening on the word you
just looked for is most of what a find bar is for. But the caret was
wrong for it in both directions, so the kept query behaved like a prefix
or a suffix instead of like a value you are about to replace.
Reopened after closing, `default_value` left the caret at offset 0: ⌘F
then "beta" over a kept "alpha" gave "betaalpha", 0/0, and no clue why.
Re-focused while still open, the caret was at the end and typing appended
instead — "alphaZZ". Every other find bar on the platform selects the
text, and the earlier rename-box fix reached the same wall and settled
for caret-at-end because `InputState::select_all` is `pub(super)` in the
UI crate.
It is reachable after all, just not as a method: the input binds ⌘A to a
public `SelectAll` action, and a `FocusHandle` can be handed an action
directly. Aiming it at the box's own handle rather than at whatever
happens to be focused keeps it from selecting something else if focus
moved. It goes through `on_next_frame` because actions route along the
last drawn frame's dispatch tree, and a box created this turn is not in
it yet.
Verified in a dev window across all three paths: closed and reopened,
re-focused while open, and empty (no selection to make, so nothing
happens). The frame requirement is also why there is no unit test — the
test harness never paints, so the callback never fires there.
The match list was computed once, when the query changed, and never
again. Alacritty stores a match as viewport-relative lines, so the moment
output scrolls the grid every stored match names a different line than
the one it was found on — and nothing recomputed them.
What that looked like: open ⌘F, type a word, then run anything. The
count froze at whatever it was, new hits were never counted, and the old
highlights stayed painted at their old rows over whatever text had
scrolled into those rows. Scrolling back afterwards dragged the ghosts
along with the viewport, five grey blocks sitting on lines that had never
contained the word.
So rescan when the grid changes. `AlacEvent::Wakeup` is the signal;
scanning on every one of them is not affordable, since a scan reads the
whole grid — up to 100k lines — and a pane mid-flood wakes far faster
than anyone reads a count. One debounced task waits 120ms for the
printing to pause, rescans once, and re-arms if more output landed while
it waited.
The rescan deliberately does less than the query-changed path: it does
not clear the selection and does not scroll. Output arriving under an
open bar has no business yanking the viewport or erasing a selection the
user is in the middle of making. It also holds the user on the match they
stepped to when that match survives the rescan, and only falls back to
"nearest the viewport" when output really did scroll it away.
Verified in a dev window: with the bar reading 2/2, a command printing
two more hits took it to 2/4, all four highlighted on the right lines,
the current one still bordered where it was.
The test needed a pane hung under a real `gpui_component::Root` — the
old harness makes the view its own root, and the search input reaches
for `Root` while painting, so any frame drawn with the bar up panicked.
`rooted_harness` is there now for the next test that has to draw one.
Two things on the strip were written as constants when neither is one.
The chord was the literal "⌘⇧R". That spelling is macOS's rendering of
one default binding: on Windows and Linux it names a modifier the
keyboard does not have, and on every platform it kept claiming ⌘⇧R after
the user had rebound Reconnect to something else. Every other in-window
hint — the home screen's shortcut list, the palette — reads the binding
back out of the keymap with `key_hint`, which also returns None when the
action carries no binding at all. Do that here and say nothing when
there is nothing true to say.
The reason ssh gave is capped at 360px and truncated. It runs past that
more often than not ("Permission denied (publickey,keyboard-interactive)"
alone is wider), and by the time the strip is up the pane's own output
has scrolled away, so the elided sentence was the only account left of
why the connection ended. Give it the tooltip that carries the rest.
The sheet focuses an input it owns. Dismissing it dropped that input and
focused nothing, so the window was left with focus on an element that had
stopped rendering: chords still reached the window handlers, but typed
characters went nowhere until the user clicked a pane.
`submit_ssh_prompt` comes through the same path as cancel, which is where
this bites — after a successful login the pane is connected and waiting,
and the first thing anyone does is type.
Hand focus back once nothing else is asking, the way the switcher, the
settings page, the palette, the diff overlay and the worktree prompt all
already do. The branch is guarded on there being no next prompt, so a
server that asks twice still walks the chain: verified against sshd on
localhost, where cancelling the password sheet raises the 2FA sheet with
its field focused and typing lands in it.
`install_hooks`/`uninstall_hooks` returned the sentence to show, built in
`tty7-core` — which cannot reach `src/ui/i18n`, where every user-visible
string lives. So the note under Settings → Agents reported "Installed",
"Removed" and "Nothing installed; nothing to remove" in English no
matter the locale, next to a panel translated around it.
Return a `HookOutcome` instead and word it at the call site, where the
other five agent-hook strings already are. The match is exhaustive, so a
new outcome cannot reach the UI without wording in all three locales.
The failure half said only "Failed: {error}" while all sixteen sibling
errors name their action; it now says which of install or removal did
not happen.
Not driven in a dev instance: exercising this panel writes to the real
agent config under $HOME, including the Claude hooks this session runs
on. Covered by the core tests and by exhaustiveness instead.
Two menus in the switcher could open with nothing to press and no word
about why, which reads as broken rather than as not-yet.
A remote workspace this client has never adopted has no local id, and
all four row verbs address a workspace by id — so right-clicking one
greyed the whole menu. A machine reached over WSL or stdio that has
never connected has no home directory for New Workspace, no live link
for Disconnect, and no Restart Server, which is offered to SSH alone;
its menu greyed out the same way.
Both now say what would make the verbs work, in the register the tab
pane already uses for adopted rows ("Open this workspace to see its
tabs"). The machine case gets a named predicate so the one state that
reaches it stays pinned while the verbs move around.
⌘N runs ActivateTabN, which goes through `activate_visual` and lands on
the Nth row as the sidebar lays it out — grouping gathers each
workspace's tabs together, so that is not the Nth entry of `self.tabs`.
The badge agreed, but only because `tab_sidebar` flattened `sections`
into the same order by hand; the badge and the chord were two readings
of one order taken in two places, free to drift apart with nothing to
catch it.
Take the badge from `visual_tab_order`, the function the chord already
calls, and pin the pair with a test over a grouping that reorders.
No change in what any badge says today.
The picker cuts a long path to a character count and prefixes "…", which
lands mid-name as often as on a separator. What is left reads as a
directory that exists — "…eeply/nested/projects" offers "eeply" with the
same weight as "nested", and there is no way to tell from the row which
of the two is a real name.
Snap the cut forward to the next separator. A partial name carries
nothing the rest of the path does not, so it goes — unless it is the
longer half, which is a single component overrunning the whole budget
with nothing else left to show; there the fragment is all there is.
The relative-time ladder stopped at "over a week ago", so in the
switcher — where the timestamp is the whole reason the line is there —
a workspace last opened yesterday-week and one opened a year ago read
identically. The interesting half of the range was one label.
Carry it on through weeks, months and "over a year ago". Weeks run to a
twelfth of a year rather than to 30 days: a 30-day month leaves the five
days between the last whole week and the first whole month rounding down
to "0 months ago", and a twelfth tiles the year with no such seam. The
new test walks every day from 7 to 400 to hold that.