Typing a filter that matched no entry left the remote Files panel saying
"Empty directory." — not silence, a false statement, and about a machine
the reader cannot see for themselves. It now says which query matched
nothing, the way the local file tree already does for the same case.
Not verified on screen: reaching this panel needs a live SSH host. The
branch is the one the local tree takes, and the full suite passes.
Type a word no theme is named after and the panel emptied out under its
own search box with no explanation. Every other filter in the app
answers: the settings search, the tab sidebar, the SSH host list, the
switcher and the palette all say so, and four of them already say it
with this exact string.
"Nothing matches X." is drawn at the top of the content pane, but the
reveal that carries the page to a search result deliberately skipped the
case where the count was zero — the one case where the thing worth
seeing is pinned to the top. Search from halfway down a page and you got
the untouched page the note exists to explain, with the note itself off
screen above you.
The note now claims the same scroll anchor the rows use, and the reveal
fires when the query matched nowhere as well as when it matched here.
Checked on screen: scrolled to the bottom of Terminal, typed a word that
matches nothing, and the page came back up to the note.
"background image" is a headline feature — the README sells solid,
gradient and image backgrounds — and searching for it found nothing.
Worse than nothing: typing it walked the page to About, because
"background" on its own matches Download updates in the background, and
the last two words then dropped the count to zero and left the reader
stranded there with no badge and nothing dimmed.
Background image and Image opacity now carry index entries, and the
Custom themes intro gains the same keywords so the query has something
to light in the state where those two rows do not exist yet — which is
the state you are in before you duplicate a theme, next to the button
that gets you there.
The badge counts index entries, not rendered rows, so it can read (3)
while one is on screen. That is how the already-indexed ANSI colors
entry behaves too; not changed here.
Only settings_row could claim the scroll anchor, so a query whose only
match on the page is a section heading moved nothing. Search "ansi" from
the bottom of Appearance and every row greys out while the one thing
still lit — ANSI colors — stays above the fold; "how shells work" and
"keybindings" are in the same position.
Section headers and intros now claim the anchor the same way, through a
first_hit_anchor helper the rows share, so the first match on the page
is the first match whatever kind of element it is.
Checked both paths on screen: "ansi" from the page bottom now lands on
the ANSI colors heading, and "smooth" still lands on its row.
The + and the ⋯ sit side by side above the Hosts list at the same size
and weight; only the ⋯ named itself on hover. It now says "New host",
the same words the form it opens puts in its title.
The transfer row draws its reveal button as an IconName::Close-family
icon inside a 24px hit target with a tooltip; the cancel button next to
it was a bare "✕" text label with neither. It is the only glyph-as-label
button in the app — every other close in this file and the eight others
across the UI use IconName::Close.
Verified by matching the sibling's construction, not on screen: showing
it needs a transfer in flight against a live SSH host.
CLIAgent::ALL is [CLIAgent; 17], so "~17 CLIs" and "and ~10 more" were
hedging about a constant. Six agents are named in the features list, so
the remainder is exactly 11.
Five of the paths named a page and section pair that was never there:
tab completion and history search moved to Input → Prompt, copy on
select and smart selection to Input → Selection & clipboard, and
opacity/blur sit under Appearance → Transparency, not a "Window"
section — that name belongs to a heading on the Window & Tabs page, so
following the old path landed you somewhere real and wrong.
The Chinese page also quoted labels in English that its reader sees in
Chinese — Appearance, Window, Follow theme, Scratch, Copy Working
Directory, Settings → Keybindings — and shortened 窗口与标签页 to
窗口与标签. All checked against the strings the app ships.
"Executable name on PATH or an absolute path. e.g. zsh, fish, pwsh."
opens a sentence in lower case. The app's other two examples are
parenthetical — "(e.g. Ctrl-B then C)" — so put these two in the same
form. Chinese and Japanese already fold the example into the sentence.
The keybindings page read ToggleSwitcher's name off the home screen's
string while TogglePalette read its own off the menu bar, so the one
action that appears in both places was the one whose name could drift.
Point it at CmdSwitchWorkspace, the name the palette shows, and pin it
with the assertion the page's other names already carry.
The editor stored each row's label as a String when it was built, so
Background, Foreground, Accent, Cursor, Selection and the sixteen ANSI
rows kept whatever language was set the moment you clicked "Duplicate
to edit" — switching language a few rows further down the same page
left them all behind.
Look the label up from the ThemeEdit at render time instead of caching
it. Verified by switching language with the editor open: the rows now
follow.
The theme editor's palette rows read "Color 0" through "Color 15" —
the only labels on the settings page that never went through the
translation table, sitting directly under Background, Foreground,
Accent, Cursor and Selection, which all do.
They now read Black, Red, … Bright white, in the order a terminal
numbers the slots, so a theme author can see which entry `\e[31m` paints
without counting rows. Chinese and Japanese included.
KEPT_IN_ENGLISH is meant to list the handful of keys that read as
English in every locale on purpose. It had grown to 834 of the 989
keys — 84% of the table, fifteen of them listed twice — so the
"still reads the English" assertion was switched off for almost
everything it was written to protect.
Only 27 keys are genuinely English everywhere: protocol and command
names, product names, the three pure templates, each language on the
language list, and the shell / agent / ssh set. Trimmed to those, with
the reason recorded next to each group. A key added without a Chinese
or Japanese translation now fails the test — verified by regressing
one on purpose.
"Closing a tab ends its shell, and Restart server and Quit and Stop
Server end all of them" hides two UI names inside a chain of "and"s.
Quote them and split the clause, which is what the Chinese and Japanese
versions of this paragraph already do.
Also: the serial comma in the two lists that were missing it, and name
the directory shell integration reports — "prompts, exit codes and
directory" never said which one.
Thirteen failure lines start "Could not"; five started "Couldn't" or
"Can't" instead, so the editor, the diff panel and the settings page
each had their own voice for the same kind of bad news. Chinese and
Japanese already say one thing throughout. The SSH field's
"Can't parse host" now reads "Invalid host", matching the
"Invalid port" it sits beside.
Explanatory prose keeps its contractions; this is only the short
one-line failures.
The five notes that can appear in the tab column all render through one
helper, at one size and one colour — but "No tab matches." ended in a
period and "No tabs in this workspace" did not. English and Chinese
close the sentence; Japanese, which leaves these short notes open
everywhere else in the app, now does so here too.
Switch Workspace and Command Palette both open a picker you then type
into, and the menu already calls them "Switch Workspace…" and
"Command Palette…". The home screen, the tab-strip tooltip and the
keybindings list used the bare names, so the same action read two ways
depending on where you found it. "Settings…" next to them on the home
screen already had its ellipsis.
Every other input has its placeholder reapplied on a language switch.
The remote Files filter is built once with the app and was never in
that list, so it kept the old language until the next restart.
Six of the nine search and filter placeholders trail off with an
ellipsis; the terminal find bar, the workspace switcher and the remote
Files panel did not. The remote panel also carried its own "Search"
key next to the local file tree's "Search files…", so the two Files
panels disagreed about what their search box is called. Point the
remote one at the same key and drop the near-duplicate.
1108707 settled on one name for the background process — server, in the
app, the menu bar and the README — and the features reference kept the old
one in the two places it appears: a tray menu item quoted as *Quit and Stop
Daemon*, which no build has said for some time, and a performance note about
what "the daemon" buffers.
The menu item is quoted as it reads now, ellipsis and all, and in Chinese
the Chinese label rather than the English one.
The file tree's delete failure read "Delete failed" with the OS reason
appended — "Delete failed: Permission denied" — and never the name. In a
tree of files that is the first thing you need, which is exactly the reason
the editor's save failure stopped saying "Save failed" and started naming
its file.
The name is already in hand for the confirmation the delete opens with.
A row the index has no entry for is invisible to search, not just
unkeyworded: `section_match_count` only counts index entries, and
`settings_row` leaves a page alone entirely when that count is zero. So
searching "smooth" or "nightly" produced no badge, no navigation and no lit
row — the setting was simply not there.
Four rows were in that position. The whole Updates group on About — update
channel, check on launch, download in the background — which is the part of
that page people go to the search box for, and Smooth scrolling on
Terminal, sitting between Scroll speed and Focus follows mouse, both of
which were findable.
`previously_unsearchable_settings_are_findable` grows five queries that
each landed nowhere before.
Two tiles in the title bar row name their shortcut on hover — Switch
Workspace ⇧⌘O, New Tab ⌘T — and the two beside them did not, though both
have one: the sidebar toggle is ⌘B and the detail panel is ⌘J. Four tiles
in a row, two teaching their key and two not.
All four go through `chord_hint` now, which prints nothing extra when an
action has no binding, so a rebind or an unbound action still reads right.
Every other bare glyph in the app names itself on hover — the code panel's
close says "Back to Terminal (Esc)", the diff overlay's says "Close Diff
(Esc)", the SFTP tray's says "Dismiss", a workspace row's ellipsis says
"More". Three were still silent, and each one has a twin that is not:
- the themes panel's close, beside two closes that both name their key;
- the ellipsis on a machine group in the switcher, beside the identical
ellipsis on a workspace row one line below it;
- the × that removes a port-forwarding rule, beside a + that says "Add
rule".
The switcher's takes the string its twin already uses. The other two get
one each.
Rename Tab, Rename Workspace and the switcher's rename all opened with the
current name in the box and the caret at offset 0, so the first thing you
typed landed in front of it: renaming "tty7" to "build" got you "buildtty7"
until you noticed and went back. Only the file-tree rename had it right,
because it happens to fill the box with `set_value` rather than
`default_value` — and `set_value` puts the caret at the end of a
single-line input.
All three now go through one helper that does the same, with a test holding
the caret where it belongs. Selecting the name outright is what the platform
does and would be better still, but `InputState::select_all` is
`pub(super)` in the UI crate; that one wants a change over there.
`Algorithms` holds five lists — kex, cipher, mac, hostkey, compression —
and the SSH form showed four of them under Algorithms and the fifth under
Connection, next to the keepalives. There it was labelled just
"Compression", which in an ssh_config is a yes/no, over a description
reading "Comma-separated (blank = default)." — an instruction that made no
sense for the switch the label implied.
It moves up to its siblings and takes their naming: "Compression
algorithms", beside "Host-key algorithms". Connection is left with the two
keepalives and the connect timeout, which is what that word means here.
The disclosure headers on a host print what is inside them — a jump host's
name, "2 rules opened with the connection", "algorithms / keepalive /
proxies / X11 / login scripts". Two of them print emptiness, and they did
not agree: Jump host said "(none)" and Port forwarding said "none", one
above the other.
They share the parenthesised one now, which is the one that cannot be
mistaken for a hostname someone typed. `SettingsNoneLower` had no other
caller and goes with it.
The +N −N on a tab row is refreshed on an edge — the directory changing, a
command finishing, an agent turn ending, the window coming back — and never
polled. That is the right shape for it, but it means a working tree edited
from anywhere else keeps the old number until one of those happens. Click
the number and the overlay reads the tree there and then: the row said
+27 −8 while the overlay it had just opened said +3 −1.
A diff read is a fresher answer to the same question, so it now hands its
totals back to the status cache. The Changes panel feeds the same path, so
watching a diff shrink there keeps the row honest too. Only the numbers
move — the branch, the root and the probe's own schedule are untouched, and
a diff for a repo nothing has probed is dropped rather than inventing a row
with no branch on it.
The totals are exact even when the diff bodies were truncated by the
repo-wide budget, which `repo_wide_budget_keeps_totals_exact` holds.
The editor itself gets one from `Input`; the rendered-Markdown pane beside
it scrolled a whole README with nothing to say how far down it was. It now
carries the same shared bar as the sidebar, the file tree, the right panel
and the settings pages, on a handle kept per open file — so switching away
and back lands where you were reading.
`with_vertical_scrollbar` grows by `flex_1`, which needs a column with a
height of its own around it; dropped straight into the overlay it sizes to
its content and the pane stops scrolling altogether. That requirement is
now written on the helper, where the next caller will read it.
Zoom Pane is ⇧⌘↵. The View menu drew it as ⇧⌘E, and ⇧⌘E is Code Panel —
press what the menu says and the code panel opens instead.
gpui's macOS menu builder maps a keystroke to an AppKit key equivalent
through a table that has no entry for `enter`; the fallback hands AppKit
the literal string "enter", and AppKit takes the "e". Every other chord in
these menus is a single character or a key that table knows, so Zoom Pane
was the only item it could happen to.
The item leaves the View menu, the way Enter Full Screen did above it for
its own reason. Zoom Pane is still in ⌘P, in a pane's own context menu —
which we draw ourselves and which prints ⇧⌘↵ correctly — and on the
Keybindings page.
Everything here that asks before it acts ends in an ellipsis — Rename Tab…,
Delete Workspace…, Add SSH Host…, Save "…" as profile… — in all three
locales. New Worktree Tab never does anything on its own either: it probes
the repo and opens the worktree sheet for a branch and a path. It was the
one such command written as though it just went ahead.
Japanese UI copy here drops the closing 。 on a setting's description and
keeps the ones between its sentences — 26 multi-sentence descriptions are
written that way, and 80 in total. Three were not: the update channel, the
background download, and the stale-server note each carried a final 。 that
nothing else on the page has.
The Scrolling settings row is 回滚行数; the pane menu and the command
palette both said "清除 scrollback" — the same word, once translated and
once not, two surfaces apart. Japanese translates both. c32c191 already
drew the line for this locale: zh keeps English for the tokens you type
(shell, PATH, SSH), and a menu item is not one of those.
The completion menu stops at 480px and clipped whatever ran past it, mid
word and without a mark: git's menu offered "Move or rename a file, a
directory, or a symli" and "Create an empty Git repository or reinitiali".
The menu is monospaced, so what fits is arithmetic: the row spends its
width on the padding, the kind icon, and the name, and the description gets
the cells that are left. Anything longer is elided with the ellipsis inside
that budget, so the cut lands on the menu's edge and says it happened.
`min_w_0` + `truncate` was the obvious reach and the wrong one — it takes
the row's contribution to the container's width down to nothing, and the
whole menu collapses to its 120px floor.
The palette is walked with the arrows and run with Enter; Tab has no job
there. Root's focus walker had one for it anyway, and sent it out of the
modal onto whichever chrome tile sat behind the scrim — the New Tab plus,
the panel toggle — which lit up with a focus ring the palette had no way to
take back.
Tab and Shift-Tab bind to `NoAction` in the palette's own context, which is
deeper than Root's and stops the search before the walker is reached.
The panel's footer says Tab is the way across once a query is in the box,
and it never was. gpui-component's Root binds `tab` to its focus walker,
gpui dispatches bindings *before* key listeners, so `on_switcher_key` never
saw the key: Tab put a blue focus ring on the ellipsis button in the
panel's own header and left the columns where they were.
Tab and Shift-Tab now bind to the panel's own key context, which sits
deeper in the dispatch path than Root's — the same shape the terminal
already uses to keep Tab for the shell.
Crossing over only helps if you can see where the cursor went, and you
could not: both columns keep their rows inside one child element, and
`ScrollHandle::scroll_to_item` indexes a scroll's *direct* children, so it
could only ever find item 0. Walking a long tab list quietly left the
selection below the fold. The selected row now wears a `ScrollAnchor`, and
stepping the cursor scrolls to it — in either column.
The tab sidebar, the file tree, the right panel and the SFTP browser all
run their scroll area through `with_vertical_scrollbar`. The settings
pages — the longest scrolling surfaces tty7 has, several viewports of them
on Appearance and Terminal — had no bar at all: no thumb, no sense of how
far down the page you were or how much was left. The SSH host list, the SSH
form and the theme picker's list were the same.
All four now carry the shared bar. It follows the OS "show scroll bars"
preference like every other one, so nothing appears for anyone who asked
for scrollbars to stay hidden.
The scroll areas move inside a wrapper that holds the constraints they used
to hold themselves, and two of those do not survive the move. `min_w_0` on
a column child means the *cross* axis, and carrying it inside let the SSH
form's label column shrink to nothing while the toggles stayed put; it
belongs on the wrapper, which is still the row item it always was. And the
wrapper has to take its width explicitly rather than by stretching, or the
`w_full` inside has no width to be a percentage of — which is how the
settings reading column lost its 640px cap on the Chinese page, exactly the
way 00ad4eb first found it.
`chrome_tile_variant_for` passed `sidebar_accent` to both `hover` and
`active`, and `sidebar_accent` *is* the sidebar surface's selected step. So
a hovered tile wore the exact fill of a selected one: with the right panel
open, pointing at Changes made it and the current Files tab read as two
current tabs, told apart only by a shade of glyph.
The palette already derives the step below it — 1.18 from the surface where
selected is 1.30, with a test holding them apart — and hover now takes it.
A selected button never renders the hover style, so the selected tiles are
untouched; every other tile simply stops overstating a pointer.
Hovering the tiles either side of it names them — Switch Workspace ⇧⌘O,
More, Hide Sidebar — and the plus between them said nothing, in all three
places it appears: the sidebar head, the top strip, and the collapsed rail.
It now names itself and its chord, from the one helper all three share.
`switcher_hint` was that helper for exactly one tooltip; it becomes
`chord_hint(what, action)` so the second caller does not copy it.
Typing in the settings search dims the page and lights up the rows that
match, and the nav badges count them per section. But nothing moved the
page. Search "cursor" and Appearance says (2) while the screen shows a
greyed-out Theme section: both matches live under Cursor, most of a page
below the fold, with no sign they are down there. On Terminal the single
match sat right on the bottom edge.
The content pane now carries a ScrollHandle, and the first matching row on
the page claims a ScrollAnchor on it. Whenever the query changes, or a
section is opened with a query already live, the page scrolls that first
match into view — one row per page, so the view never travels past matches
above it. Clearing the query leaves the page where the reader left it.
The host rows carry a liveness dot in front of the title; Defaults, which
has no session to be live, carried nothing and skipped the space too. Its
title and subtitle therefore began 14px to the left of every host title
under them — the one row at the top of the list, hanging off the column the
rest of the list holds.
The gutter is now unconditional and simply stays empty on the rows that
have no dot to put in it. The group headers were 2px off the same column
for the same kind of reason: 6 + 10 + 4 instead of 8 + 6 + 8. Their inset
becomes the 8px the rows already hover with, which lands them on it.
`cargo fmt --check` is the first job CI runs, and main passes it. Three
files on this branch no longer did: a long method chain in the settings
shell picker, the card-width clamp the switcher gained when it learned to
fit its window, and a Chinese plural that shrank below the line limit when
its backticks came off. None of it changes behaviour — it is what rustfmt
would have written, restored so a merge does not fail on formatting.
Right-clicking a pane offers "Split Right ⌘D", "Close Pane / Tab ⌘W" and
six more with their shortcuts printed, because those items are built with
`menu(label, action)` and PopupMenu looks the binding up from the action.
Right-clicking the tab that owns the pane offered the same commands as bare
labels — every item there is built from a click handler instead, since it
has to act on that tab rather than the active one, and a handler carries no
action to look up.
The items keep their handlers and now carry the matching action purely so
the chord can be found: `confirm` prefers the handler whenever both are
set, so nothing about what a click does changes. Split Right, Split Down
and Close Tab pick up the chords they have always had; the rest are ready
for the day someone binds them.
The home page is what a window shows with no tabs open, and it listed
Split Right and Split Down among the things you can do from there. Both
ask the active tab for a pane to split and return without a word when
there is none, so the two chords the screen taught did nothing on the one
screen that taught them. Reopen Closed Tab had the same shape whenever the
closed stack was empty.
The splits leave the list, and Reopen Closed Tab appears only while there
is something to reopen. A fresh window now offers four rows, and every one
of them works.
Nine strings wrapped a command in backticks — `tty7`, `git diff`, `git
status`, `{alias}` — and nothing in this app renders Markdown, so the
marks landed on screen as literal characters. The Settings label read
"Install the `tty7` command on PATH", and its description wrapped mid-token
and left a lone backtick opening a line.
Everywhere else the same copy writes known_hosts, ~/.ssh/config and
"git diff →" bare, so the backticks go and the minority follows the
majority. The one that quotes a name — the missing ~/.ssh/config alias —
takes the quotes the rest of the app already uses for a name: "..." in
English, “...” in Chinese.
An icon-only xsmall button is a 20x20 box, under the 24x24 desktop floor
in WCAG 2.5.8 that hit_target exists to hold. The glyph is unchanged; only
the box it sits in grows.
The card is fixed-size on purpose — one that resized under the pointer
while the tab column changed length would be unusable — but 840x420 was
fixed in the absolute sense too, so on a small window it hung off both
edges with its search box reading "orkspaces, tabs and machines". It now
takes what the window has when that is less than what it wants, and is
untouched above that.
Nothing stopped tty7 being dragged down to a couple of hundred pixels,
and below roughly 700 the chrome stops being chrome: the sidebar sits at
its 180px floor with barely forty columns beside it, and the overlays the
app drops on top have nowhere to land. 720x520 is the smallest window it
still holds its shape in.
The control never shrinks and the label column must keep min_w_0 or long
descriptions stop wrapping, so on a narrow pane the label was squeezed to
nothing: at 600px the Terminal page rendered "Program" as a vertical
column of single letters, and the SSH form did the same below about
1000px, which 4275bb1 left open as the real remaining bug.
flex_wrap was the wrong answer — it let the label column size to its own
description, which then ran out past the row on every wide page. Measure
instead: render_settings knows the viewport and which page it is drawing,
so it can say how much width a row will actually get, and the row lays
itself out side by side or stacked from that. Nothing moves above the
breakpoint.