Commit Graph
868 Commits
Author SHA1 Message Date
l0ng-ai 62b922f2c2 Merge origin/main into integration/polish
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.
2026-08-09 16:38:15 +07:00
ARNOandl0ng-ai 61efe27f2d feat(windows): add native backdrop material presets (Mica / Acrylic /… (#412)
* 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>
2026-08-09 15:14:54 +08:00
l0ng-ai e46bcbcf56 chore(terminal): drop the client-side command-mark store (#404)
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
2026-08-09 14:49:47 +08:00
l0ng-ai 388e65e96a fix(tests): the shell check proves a good shell on every platform
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.
2026-08-09 13:46:10 +07:00
l0ng-ai 83f6415644 fix(tests): the rename test only builds where its harness does
`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.
2026-08-09 13:38:35 +07:00
l0ng-ai 09a653d10b fix(workspace): make the CLI and the GUI agree on what exists (#423)
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.
2026-08-09 14:18:19 +08:00
l0ng-ai ad23a4223b fix(icons): a chrome glyph is the size of its neighbours
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.
2026-08-09 13:09:47 +07:00
l0ng-ai 686bd0bcea feat(ui): the interface has a font size of its own
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.
2026-08-09 13:09:35 +07:00
ARNO 8981cc7429 fix(wsl): start a pane when fish is the distro's default shell, with its integration (#422)
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.
2026-08-09 13:53:23 +08:00
dongyuwei 1c1b4e2c92 feat(terminal): include fish_history in command history search (#421)
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.
2026-08-09 13:52:54 +08:00
l0ng-ai fee48a4c99 Merge origin/main into integration/polish
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.
2026-08-09 12:28:42 +07:00
l0ng-ai 62e2656334 fix(sftp): a download holds its local name until its file lands
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.
2026-08-09 12:02:03 +07:00
l0ng-ai 357fce8878 fix(search): keep the current match by absolute row, not viewport row
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.
2026-08-09 12:02:03 +07:00
l0ng-ai de2e622be5 fix(diff): a read that failed must not overwrite the sidebar's counts
The overlay says "could not read the diff" in words, and then published
the failed read's zero totals over the +N -N the row already had right.
2026-08-09 12:02:03 +07:00
l0ng-ai c625ccbe38 fix(completion): a metacharacter inside quotes is text, not a new command
`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.
2026-08-09 12:01:55 +07:00
l0ng-ai 23867ace27 fix(agents): a finished turn is not work closing would cut short
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.
2026-08-09 12:01:55 +07:00
l0ng-ai 8d396ee220 fix(close): answer the close question about the tab it was asked about
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.
2026-08-09 12:01:47 +07:00
l0ng-ai 6e0a7dcb29 Merge origin/integration/polish into integration/polish
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.
2026-08-09 10:40:33 +07:00
l0ng-ai ab3902f25e fix(completion): keep the menu inside the pane it belongs to
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.
2026-08-09 10:26:10 +07:00
l0ng-ai a171da2484 fix(terminal): bring the view back to the prompt when you paste, too
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.
2026-08-09 10:10:22 +07:00
l0ng-ai fa8f7cee31 fix(terminal): bring the view back to the prompt when you start typing
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.
2026-08-09 10:07:09 +07:00
l0ng-ai a8c2d74ebf docs(prompts): record the one box the shared select cannot serve
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.
2026-08-09 10:00:19 +07:00
l0ng-ai d364c61953 fix(sftp): stop OK offering to create something with no name
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.
2026-08-09 09:39:17 +07:00
l0ng-ai 13f70fd435 fix(prompts): open every filled box ready to be typed over
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.
2026-08-09 09:34:20 +07:00
l0ng-ai fd8ddedfea fix(search): select the query ⌘F reopens with
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.
2026-08-09 09:16:34 +07:00
l0ng-ai 12cb07d008 fix(search): keep searching the pane while it is still printing
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.
2026-08-09 09:08:52 +07:00
l0ng-ai b4e7add65d chore(release): v26.8.2 v26.8.2 2026-08-09 09:59:26 +08:00
l0ng-ai 60f910d06f fix(ssh): stop the disconnect strip naming a chord it cannot know
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.
2026-08-09 08:51:37 +07:00
l0ng-ai 8d52948bd2 fix(ssh): give focus back when the auth sheet closes
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.
2026-08-09 02:56:08 +07:00
l0ng-ai d14eb0c509 fix(agents): say what the hook install did in the user's language
`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.
2026-08-09 02:42:11 +07:00
l0ng-ai 709243f960 fix(switcher): stop opening context menus with every row greyed out
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.
2026-08-09 02:37:08 +07:00
l0ng-ai a74bb674b3 refactor(sidebar): read the tab badge off the order the chord uses
⌘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.
2026-08-09 02:29:51 +07:00
l0ng-ai 92827665b0 fix(switcher): stop eliding a path into half a directory name
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.
2026-08-09 02:15:55 +07:00
l0ng-ai 5195f49b5d fix(home): tell a month-old workspace from a week-old one
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.
2026-08-09 02:14:03 +07:00
l0ng-ai e478c5b9fa fix(close): name the command the confirmation is about to end
Closing a busy tab asks "{what} is still running. Closing ends it." —
and {what} was the window title as it stood when the command began. A
prompt that titles by directory, which is most of them, made tty7 ask
"tty7 is still running. Closing ends it." about a folder. The comment
over CloseReason already says this dialog exists because ending a shell
is permanent and has to name what it ends; it was naming the wrong
thing.

The shell integration already sends the submitted line as the OSC 133;C
mark, and the client records it — only the reader was #[cfg(test)],
because nothing had needed it since the panel that used to read marks
was removed. The wire frame carries whether a command runs, not which,
so the mark is the only place the text exists on this side.

So: prefer the running mark, fall back to the old title when there is no
mark (no shell integration). The text is unescaped — the integration
escapes % and the four bytes that would break OSC framing — and clamped
to 60 characters so the dialog still reads as a sentence.

Verified: `sleep 300` then ⌘W now asks about "sleep 300".
2026-08-09 01:51:49 +07:00
l0ng-ai 3796ec3399 fix(completion): stop a rustup flag describing itself
`rustup doc --unstable-book` carried "--unstable-book" as its
description, so the menu printed the flag twice and explained nothing.
Every sibling reads as the title of what it opens — The Cargo Book, The
Rust Reference, The Embedded Rust Book — and rustup's own help calls
this one The Unstable Book.

Found by sweeping the specs for descriptions that merely repeat their
own flag. The other eleven hits are terse but real: docker login
--password really is "Password".
2026-08-09 00:44:33 +07:00
l0ng-ai 0ab2e47829 fix(completion): describe every entry in the specs this machine can verify
Eleven of the twenty-nine tools with undescribed entries are installed
here, so their descriptions could be quoted from the tool rather than
written from memory. That closes nine more specs completely — rustup,
brew, gh, make, pip, pip3, rg, bundle, cmake — and most of npm.

Each line comes from the tool: rustup help for the --toolchain and
--profile flags and default-host, brew(1) for `edit`, `gh completion`
for --shell, make --help for -j, pip help for --files and --index,
rg --help for --sort and the --no-pcre2-unicode deprecation, bundle help
for `config` and `help`, cmake --help and `cmake -E` for theirs. The
five `npm team` subcommands and the `--` separator are read off their
usage signatures. Every spec here writes descriptions without a trailing
period, so these do too.

194 gaps across 28 specs, now 162 across 19. `npm run --silent` is left
alone: npm ships no man page here to quote.
2026-08-09 00:33:23 +07:00
l0ng-ai 94fd075080 fix(keybindings): stop the menu ellipsis trailing off inside a sentence
Rebinding a chord that another action holds prints "{action} took the
shortcut from {previous}, which is now unset." Both names come off the
keybindings page, where a command that opens something carries a "…" —
so the note read "Rename Tab… took the shortcut from Close Pane / Tab",
and the marker that means "this opens a dialog" turned into a sentence
losing its nerve.

The ellipsis is a menu convention, not punctuation, so it comes off when
the name is interpolated into prose. The rows themselves keep it.
2026-08-09 00:17:17 +07:00
l0ng-aiandl0ng-ai 8b7215f0d8 fix(remote): let a WSL machine restart its server, and call it "server" in zh (#417)
* fix(remote): let a WSL machine restart its server, and call it "server" in zh

The machine menu offered "restart server" only when `target.is_ssh()`, so a
WSL distro's row had just "new workspace" and "disconnect" — and the router
refused the action for anything but SSH, even though `restart_wsl_daemon`
had been sitting in `install/wsl.rs` unused since it was written. A distro's
server is installed and launched from this computer exactly like an SSH one;
only the transport differs.

- `RemoteTarget::hosts_our_server()` replaces `is_ssh()` as the gate in the
  sidebar menu and in `restart_window_daemon`. A `--stdio` workspace still
  has nothing to restart: it is whatever program the user named.
- The router grows WSL arms for both actions — `RestartServer` relaunches
  the distro's daemon, `ReplaceServer` (new `replace_wsl_server`) publishes
  the bundled binary first. The mismatch dialog's "update server" used to be
  a dead end on WSL for the same reason.
- `AppRestartServerNotSsh` is now `AppRestartServerNoServer`: the message no
  longer talks about SSH, since the only machine it can fire for is a local
  `--stdio` one.

The Chinese table called tty7's server 服务器 / 服务器端 / 后台服务; it now
says `server`, the way it already keeps `shell`, `pane` and `agent` in
English. The macOS Services menu keeps its 服务.

* fix(i18n): keep 服务器 where it means the SSH host, and test the new gate

The sweep that renamed tty7's own server to `server` in the Chinese table
also caught two SSH settings where 服务器 was never tty7's server: the
host-key description talks about the key of each machine being connected
to, and the banner switch suppresses the login banner sshd prints. Written
as `server` both now read as if tty7 ran something there, which is exactly
the confusion the rest of the sweep removes. Put the Chinese word back in
those two.

`hosts_our_server()` is the gate the whole change hangs on and nothing
exercised it, while the test next door still asserted under the name
"only SSH machines have a server to restart" — the rule this branch
retires. Name that test after what it actually checks, the transport, and
add one for the new predicate. It matches on the variants rather than
negating a single one, so a transport added later has to say for itself
whether we can restart what is on the far end.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-09 01:04:25 +08:00
l0ng-ai 40698ed344 fix(completion): describe cargo add/remove --color like the other 41
Every other --color in the cargo spec reads "Coloring: auto, always,
never"; add and remove had none, so the one flag that appears on nearly
every cargo subcommand explained itself everywhere except the two
newest.

The spec's other cargo gaps are left alone on purpose. They are not
unwritten text: `cargo tree --no-dev-dependencies`, `--prefix-depth`,
`--no-indent` and the `vendor` flags beside them are from the old
cargo-tree subcommand and current cargo has no such flags, while
`cargo config get` is undescribed in cargo's own help. Writing prose for
those would document flags that do not exist.
2026-08-09 00:03:36 +07:00
l0ng-aiandl0ng-ai a6eccc6937 fix(terminal): complete a WSL workspace's panes over its share again (#418)
`wsl_share_cwd` opens with `if !self.host_id.is_local() { return None }`,
which throws away the very case its second branch exists for. A pane in a
WSL workspace is served by the daemon inside the distro, so `set_workspace`
gives it `RemoteTarget::Wsl.host_id()` — never `HostId::LOCAL` — and the
`workspace.target` branch below is unreachable. Tab in those panes fell
back to bash, while the wsl.exe pane in a local workspace (remote context,
no workspace, host LOCAL) kept working.

The locality check belongs to that first branch alone: a `wsl.exe` pane
reaches the distro of whichever machine hosts it, so a remote host's pane
must not read a same-named share here. A `RemoteTarget::Wsl` workspace is
this computer's distro by construction — tty7 reaches those by running
wsl.exe locally, which is why the sidebar labels them "WSL · this computer".

The decision moves into a free `wsl_share_distro`, testable the way
`loopback_plan` is, with all four combinations covered.

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-09 01:01:32 +08:00
l0ng-ai 50909b0f84 fix(completion): finish the descriptions git's completion menu was missing
The README sells "every flag and subcommand with its description", and
git — the spec people hit most — had five entries with none: the `blame`
subcommand, `bisect help`, and three flags. `blame` sat in the menu as a
bare word between neighbours that all explained themselves.

Filled from git's own help output rather than written from memory:
"Show what revision and author last modified each line of a file",
"Print a long usage message", "Dry run", "Prune remotes after fetching",
"Read pathspec from file".

A sweep of all 97 specs found 194 more gaps in 28 other tools — dotnet
30, yarn 27, bun 22, flutter 18, conda 14, cargo 13 lead it. Not filled
here: those need each tool's own help to quote, not invention.
2026-08-09 00:01:12 +07:00
l0ng-ai 9c26b2c253 fix(worktree): stop the prompt offering a Create that cannot work
Submitting falls back between the two name fields — either alone is
enough, and only both blank has nothing to name a worktree after. The
form did not say so: Create stayed primary and enabled with both empty,
and clicking it could only raise "The worktree needs a name".

Create is now disabled in exactly that state, and the path preview
follows the same fallback it always should have: with only a branch
typed it read ".tty7/worktrees/…", describing a path that would never be
created, where the worktree actually takes the branch's name.

Checked all three states on screen: both blank disables Create, a branch
alone re-enables it and fills the preview in, and a name alone is
unchanged.
2026-08-08 23:42:14 +07:00
l0ng-ai 6b8194bd67 fix(editor): show the file tree the empty editor tells you to use
Opening the code panel with no file draws "Open a file from the file
tree" and hands the tree keyboard focus — but nothing put the tree on
screen. On a fresh tab ⌘⇧E gave you an empty editor across the whole
content area, naming a panel you could neither see nor reach from there;
⌘J did not help, because the code panel had the space.

The branch that reaches for the tree now reveals it first, using the
file_tree_on_screen check that already existed. Verified end to end:
⌘⇧E opens the editor with the tree beside it, and clicking a file in it
loads the file.
2026-08-08 23:22:34 +07:00
l0ng-ai 52219a1a04 fix(panel): keep the end of the path the Info panel is naming
The cwd row truncated like every other value — from the tail — so a deep
checkout read "/private/tmp/claude-501…" and named nothing. The last
segment is the part that says which directory the pane is in.

The row now lets the head absorb the shrinking and keeps the leaf, the
way a file manager draws a path: at a narrow panel it reads
"…and-another-nested-leve…", and widening it fills in
"/private/tmp/claude-501/-Users-thoma…and-another-nested-level-here".
No character cap, so it uses whatever width the panel has.

Only the path row: a branch name reads head-first, so branch still
truncates from the tail.
2026-08-08 23:14:47 +07:00
l0ng-ai e46a2fbbb6 fix(sftp): let double-click open a file, not just a folder
The gesture ran sftp_enter_dir, which returns early for anything that is
not a directory. Double-clicking a file — what every file browser, and
every SFTP client, answers by opening it — did nothing at all: no
transfer, no cursor change, no message. The row menu right beside it
offers Download, and sftp_open_entry already handles both kinds.

Point the gesture at that instead and drop the directory-only handler,
which had no other caller. A download shows up in the transfers tray and
can be cancelled from it, so the worst case is a click you can take back.

Verified against a real SSH connection to localhost: double-clicking a
file now lands it in ~/Downloads at the right size, and double-clicking
a folder still navigates into it.
2026-08-08 23:04:21 +07:00
l0ng-ai 0add6e482b fix(terminal): re-engage conpty_resize - the opt-in line never landed (#419)
The conhost-semantics half of #415 hangs on one line: terminal_config_from_user setting conpty_resize on Windows. It was verified in the acceptance build but lost from the working tree before the commit, and the field's off-by-default kept build, tests, and CI green while nightly 202608081441 shipped with the fix disabled. Restores the flag and pins the wiring with a regression test.
2026-08-09 00:03:30 +08:00
l0ng-ai fc73559e2b fix(panes): give the split divider the 8px grab the panel edges have
The sidebar and right-panel edges hand you an 8px target; a split
divider was 5px, so the one drag you do most often was the fussiest to
start. I had left this alone thinking a wider band would eat clicks at
the edge of the terminal text — it does not: the grid is inset by its
own GRID_PAD_X of 8px, so the extra 1.5px a side lands in padding and
stops 4px short of the nearest cell.

The gutter still measures 5px, so the split looks exactly as it did; the
target is an overlay centred on it, the same shape the panel edges use.

Measured rather than eyeballed: the divider line sits on one pixel
column, and dragging from 1.5px outside the old band now moves it.
2026-08-08 22:45:04 +07:00
l0ng-ai 151ed9dcd6 fix(sftp): give the transfers list its scrollbar too
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.
2026-08-08 22:38:02 +07:00
l0ng-ai cab1260faa refactor(ui): define the panel resize-handle width once
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.
2026-08-08 22:05:09 +07:00