The last scroll area in the app without one. It caps at 200px and
scrolls past it, so eight queued downloads run off the bottom edge with
nothing to say so.
The shared helper grows into a flex parent, which is wrong for a box
that already carries its own height — taking flex_1 there would stretch
it past its cap or collapse it, which is why this one had been left
alone. `over_vertical_scroll` lays the same bar over an area that
measured itself, so the layout is exactly what it was without it.
Verified over a real SSH connection to localhost: queued eight
downloads, confirmed the list still scrolls through them and the bar
renders.
The tab sidebar and the right panel each declared their own
RESIZE_HANDLE_WIDTH = 8. Two edges the same window drags, two numbers
that only happened to agree — the same shape as the target fade that had
already drifted apart. One constant now, named where the panel lives.
The workspace list and the tab list both scroll once they outrun the
card, and neither showed a bar — with eight tabs the ninth is cut off
mid-row with nothing to say more is below. Both wrap in the shared
helper now, and the column border moves out of the scroll area so it
stays put while the rows move under it.
Checked that arrow-key navigation and the scroll anchors still carry the
cursor through the list with the wrapper in place.
A working tree can run to thousands of lines here, and the overlay was
the one long document in the app with nothing to say how far it went or
where in it you were. Nine scroll areas already wrap in the shared
helper; this one had not even been given a ScrollHandle.
A row's +N −N opens the working-tree diff, but the only sign of it was
the pointer shape: the row highlights as a whole, which says nothing
about the counts being a button inside it, and with the setting off they
look exactly the same. They now underline under the pointer, the idiom
the SFTP breadcrumb already uses for clickable text.
Jump host, Port forwarding and Advanced fold and unfold on click, but
only changed the cursor. Every other clickable row on the page answers
the pointer with a band — the settings rows, and the SSH group header,
whose comment already explains it wants the same 8px inset the rows
hover with. These three now take the same band and radius.
Three of the four keychain writes log the error the store hands back;
DeletePassword dropped it. That is the arm behind "The stored password
was rejected. Enter a new one." — if the delete keeps failing, the
rejected secret stays in the keychain and gets offered again on the next
connect, with nothing in the log to say why.
Thirty-four of the app's thirty-eight menu items are Title Case — the
menu bar, the command palette, the tab menu, the local file tree. Nine
were not, and the split ran along a seam a user actually crosses: the
local Files panel offers "New File" and "New Folder" while the remote
Files panel in the same slot offered "New file" and "New folder".
SFTP: New Folder, New File, Go to Shell Directory, Transfer History,
Hide Transfer History, Follow Symlink. Settings → SSH: Copy Address,
Expand All Groups, Forget Password.
Left alone: "cd Here" and "chmod…" start lower case because the command
is spelled that way, "Import from ~/.ssh/config" already was Title Case
(prepositions stay down, as in "Close Tabs to the Right"), and the SFTP
edit form keeps sentence case because it is a form title, not a menu.
The tab reorder slide runs 140ms on ease_out_quint from a named
constant; the home page fade-in ran 150ms linear from a literal. Two
transitions, two answers to the same question. TRANSITION_MS now names
the duration both use, with REORDER_SLIDE_MS defined as it, and the fade
takes the same curve.
A sweep of every .opacity() call turned up three places where one
meaning had two strengths:
- A Dynamic forward has no target, so the target endpoint fades. The
rules editor in Settings faded it to 0.35 and the live Forwards panel
to 0.4 — the same row, drawn twice. Now a named constant both use.
- switcher.rs destructures (fg, muted, dim) in five render functions;
four dimmed to 0.7 and render_group_header to 0.75, so a header sat a
step brighter than the rows under it for no reason.
- The tinted-border notice box is 0.4 in the window, the Forwards panel
and the themes-rejected note, and was 0.35 in the switcher.
The Appearance page and the theme panel draw the same object — a theme
preview, its name, a border — and drew it differently. The page card was
rounded_xl, the only 12px corner in the app, while the panel card that
opens from it uses TRACK_RADIUS. Worse, the preview inside the page card
kept its own 8px corners inside a 12px border, the concentric mismatch
that rounding::inner_radius exists to prevent; the panel already insets
its copy of that same preview.
Both now use TRACK_RADIUS with the preview inset by the hairline, and
theme_preview names the token instead of repeating its value.
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.