Files
Hongwei Qinandl0ng-ai a2d53a9597 fix: 19 项低危 UX 问题(#584–#602) (#615)
* fix(scm): say what "discard all" actually discards (#594)

The group-level Discard prompt asked to "discard every change in this
repository", but discard_all_ops has only ever swept unstaged edits and
untracked files — staged changes survive, as the function's own comment
notes. Users confirmed under one belief and the code kept another.

Narrow the prompt to the operation's real footprint, in all three
languages.

* fix(scm): keep the amend toggle when its confirmation is cancelled (#595)

scm_commit cleared scm.amend when Commit was pressed, before the
"rewrite the last commit?" prompt. Answering Cancel returned to a panel
whose amend mode had silently been dropped, so the next Commit created a
brand-new commit — exactly what the user had just declined to risk.

The toggle now clears where scm.committing arms, at dispatch in
run_git_op, extending the rule the armed flag already followed: a
cancelled confirmation leaves nothing behind.

* fix(cli): answer a wait timeout in the success path's JSON shape (#589)

The 124 branch returned {pane,status,timed_out} while a finished wait
returns {pane,status,matched,stale,activity,message,session_id} — so the
one branch a consumer writes error handling for was the one missing its
fields. The timeout now carries the full shape plus timed_out, and
reference.mdx documents the schema and the flag.

* fix(cli): report a failed wait on stderr, even under -q (#590)

wait's failures are structured exits (124, or 1 when the pane died
first), so they never passed through the anyhow path whose eprintln is
the only thing quiet mode cannot silence — contradicting the documented
"errors still go to stderr". Both exits now print their headline to
stderr, the discipline pane close already established.

* docs(cli): describe owner as the workspace that may attach (#591)

commands.md still claimed the CLI stamps a literal "tty7-cli" owner on
the panes it spawns — the behaviour the orphan-workspaces work removed,
because an owner names the workspace allowed to attach and a stranger's
stamp got the panes respawned. Every spawn path now writes the workspace
id, or nothing while the pane is still unfiled. Bring commands.md in
line with reference.mdx, and note the absent case in both.

* docs(cli): close five contract drifts between the tables and the code (#592)

- The key tables listed pgup/pgdn as aliases but not pgdown, which the
  parser has always taken; both references name it now.
- "Case-insensitive" was flat wrong for Alt: M-x keeps its case because
  Alt is a prefixed ESC, unlike Ctrl. Both references note the exception.
- procs' ports JSON has carried addr since the field exists; both schemas
  show it.
- TTY7_WS is tab ls's default too; both environment tables say so.
- split --ratio's clamp to [0.05, 0.95] was discoverable only in code;
  both split sections document it.

* fix(cli): doctor exits 1 when the server is unreachable (#592)

doctor is the verb people run when something is not working, so an
unreachable server is *the* finding — not a row to exit 0 over while
`tty7 doctor || alert` never fires. The table and JSON still go out
(the context rows are the other half of what doctor is for), and stderr
carries the headline under -q. MockBackend grows an `unreachable` flag
so the branch is testable; no Status/Routes round-trips happen once
hello has failed.

* fix(settings): refuse a Start-in path that names no directory (#601)

The custom path was stored unchecked, and the daemon's picker then
skipped it — not a directory — so every new pane silently started in
the fallback directory and the typo read as a tty7 bug. Settings now
marks the field red and refuses to save, the proxy row's pattern
(#551), with the red line and the commit gated on one shared predicate
so they can never disagree; a hand-edited config.json holding such a
path gets a log::warn! naming it at the moment the fallback engages.

* fix(terminal): rescan search highlights when the pane's width changes (#586)

A match point is an absolute (line, column) against the width it was
scanned at, so a column change reflows the text out from under every
highlight. Output rescans them (Wakeup → refresh), but a quiet local
pane has no output coming and the drift outlasted the resize
indefinitely. set_grid_size now rescans on a column change with the
output path's discipline — selection and scroll untouched — and takes
the Context it needs to do so; a rows-only change reflows nothing and
stays cheap.

* fix(terminal): keep the grid selection when the search bar opens and closes (#584)

The selection that seeds the query is the thing being searched for, yet
opening the bar ran recompute_matches' unconditional clear — right for
its other callers, where the user *changed* the query and the old
selection names nothing — and closing cleared it again, so select →
Ctrl+F → Esc lost the selection every time. The seeded selection is now
restored after the opening scan, and close_search no longer clears; a
query the user actually changed still retires the stale selection, the
discipline refresh_matches_after_output already stated.

* fix(tabs): a zoomed pane stays zoomed across a tab switch (#599)

Zoom was a window-level value that activate() cleared unconditionally,
so looking at another tab and coming back restored the split layout —
while a zoom is a tab's temporary view state, like its focused pane.
It now rides with the Tab: activate stashes the outgoing tab's zoom and
brings the incoming tab's back. The clears that genuinely reshape the
layout (drag, split, close) still stand, and a stashed zoom whose pane
exited while the tab was away is validated away rather than restored.

* fix(tabs): track an open rename box by tree id, not index (#598)

The rename box held only an index, which drifts the moment any other
tab closes or the strip reorders — so close_tab_inner and
apply_tab_order threw the half-typed name away on any unrelated tab
event, and a reorder mid-rename still left a window where the commit
landed on whichever tab had taken the index over. The box now names its
tab by tree id end to end (start, render match, commit): only closing
the renaming tab itself ends the rename, and the name lands on the tab
the box was opened on wherever it has since moved.

* fix(i18n): move seven hard-coded user-facing strings into the language tables (#602)

Seven spots rendered English no matter which UI language was set: the
shell-integration notice that explains why a wrapper was blocked or never
engaged, the titles a pane wears once its process exits or the server
loses it, the loopback forward's failure line, the tray tooltip that
lists running agents (whose separator also wanted a CJK enumeration
comma), the cursor-shape choices in settings, the command palette's
empty-result hint, and the updater's install hint. Each is a L10nKey now
with en/zh/ja entries, so the parity guard keeps them translated from
here on.

The palette's empty state was also wrong in content, not just language:
every menu suggested connecting over SSH when nothing matched, including
menus that have no hosts in them. The hint now only appears in the
quick-connect menu; everywhere else the palette suggests a different
search instead.

Verified on Linux: the title/palette/tray suites (48 tests) and the i18n
parity guard all pass.

* fix(terminal): show remote path completion is listing, and say when it fails (#585)

Tab-completing a path on a remote workspace had two silences. The whole
network round-trip painted nothing, so a slow link read as a broken Tab
key; and a listing that failed was unwrapped into an empty candidate
list, so "the directory is empty" and "the listing never happened" ended
in the same nothing.

A pill over the pane's bottom-right corner — the style the integration
notice already uses, factored out — now says the listing is running from
the moment it starts, and a failed listing sets a notice with its error
instead of the empty vector. The failure pill stays until the next
keystroke dismisses it, and the trailing notify after an empty listing
closes the menu brings the "listing…" pill down with it.

Verified on Linux: the new gpui test covers the idle/listing/failed
states, and the neighbouring completion tests still pass.

* fix(files): quote cd Here / Insert Path for the shell the pane runs (#593)

Both file-tree actions wrapped a path with spaces in POSIX single quotes
whatever the focused pane's shell was. In cmd.exe a single quote is an
ordinary character, so `cd 'C:\Users\me\My Documents'` split at the
first space and cmd complained about 'C:\Users\me\My' — while the same
action was fine in PowerShell and bash, which is why only cmd users ever
saw it.

shell_quote_for takes the pane's shell program (the pane already knows
it — the settings page lists it) and picks double quotes for cmd.exe,
single quotes for everything else; an unknown shell keeps the POSIX
form, and a path that needs no quoting stays bare either way. Windows
paths cannot contain a double quote, so the cmd form has nothing to
escape.

* fix(cli): pane close fails for a pane the registry does not hold (#588)

`tty7 pane close %99` printed {"closed":[99]} and exited 0 for a pane
that never existed. The workspace path cannot drift this way — PaneClose
answers — but an orphan has no workspace to route through, so close
hangs it up directly, and that kill is fire-and-forget: the daemon never
says whether it knew the pane, so Ok(()) only ever meant the bytes
reached the socket. A reaper script chasing the orphans `pane ls --all`
points at would read the ghost success as cleanup done.

The direct path now reads the running-pane registry once per batch and
refuses ids it does not hold: the miss lands in `failed` with exit 1,
next to the failures kill itself can report. A pane that exits between
the listing and the kill is gone either way, which is what closing it
wanted, so that race still reports closed.

* fix(session): a launch that leaves workspaces running says so (#597)

Quitting with several windows open and starting again restored only the
most recent one; every other open window was marked detached — panes
alive, nothing on screen, the only trace a "left N detached" log line.
The workspaces were reachable from the sidebar, but nothing said they
existed, so they were easy to forget entirely.

restore_one now returns how many windows it detached, and both launch
paths (normal startup and the CLI-driven open) push an in-app
notification into the restored window naming the count and where to
reopen them. The count rides the return value rather than firing the
notification inside the store, because the store has no window to notify
in — and a launch that detaches nothing, like the reattach-the-last-
closed case, stays silent.

* fix(switcher): list the local machine's orphan panes, with a way to close them (#596)

A pane whose workspace went away — an interrupted `tty7 run`, a forgotten
workspace that kept its shells — was invisible everywhere in the GUI: not
in the sidebar, not in the switcher, not in the tray. It kept its process
and its memory, and the only way to even learn it existed was the CLI's
`tty7 pane ls --all`, which a GUI-only user never runs.

The switcher's local machine group now carries a "Background panes" block
under its workspace rows: one line per live pane the daemon's registry
holds and no workspace does — id, owner, cwd — each with a Close button.
The listing is the same PaneClient::list the CLI's reaper reads, fetched
off the UI thread when the panel opens; closing kills and then re-lists,
so a pane that survived simply stays on the list instead of pretending
to be gone. The block steps out of the way while the search field holds
a query, which narrows the panel to workspaces.

Local on purpose: a remote machine's orphans belong to its own daemon,
and routing a listing per host is what the CLI reaper is already for.
The block joins no keyboard navigation — the panes are not workspaces
and the arrows have no business landing on them.

* fix(updater): keep Inno's progress window on screen during the install (#600)

The Windows installer ran /VERYSILENT, so from the app quitting for the
update to the watcher bringing the new build up — tens of seconds, longer
under an antivirus scan — the screen held nothing at all: no window, no
progress, no tray note. "Clicked update, the app vanished" reads as a
crash, and double-clicking the icon does nothing while the files are
being replaced.

The installer now runs /SILENT instead. Nothing about the flow becomes
interactive — /SP-, /SUPPRESSMSGBOXES, /NORESTART and /CLOSEAPPLICATIONS
are untouched — but Inno's own progress window stays on screen for the
gap, which is exactly the span the user had no word about.

---------

Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
2026-08-13 18:11:17 +08:00
..