track_progress only fires on opened, synchronize, ready_for_review and
reopened. The approval gate depends on `labeled`, which is none of
them, so the action refused the run outright rather than falling back
to the log.
The review is written to review.md and posted by a final step that runs
no model and reads one file. That adds Write to the allowlist, which
costs nothing: there is still no Bash, so still no curl, and the runner
is discarded after the comment goes out.
Three changes, one shape. The run was failing at the app-token exchange
because the action checks the triggering actor's repository access and
a fork PR's author has none; `github_token` plus
`allowed_non_write_users` is the documented pair for
pull_request_target, and the label gate is what makes trusting that
actor a decision somebody made.
Findings now go on the pull request instead of the run log, which needs
`pull-requests: write`. That is affordable only because the tool
allowlist stays read-only: with no Bash there is no curl, so the worst
a successful injection buys is a silly comment.
The fork's tree comes back, one directory down. #392 removed it
entirely because a checkout at the workspace root is what Claude Code
reads as the project -- but the action's own security guide gives the
middle path, a subdirectory, which keeps the project files ours while
letting the review see whole files instead of hunks. `.claude/` and
friends are dropped from that tree and a top-level CLAUDE.md is renamed
rather than deleted, since a PR that edits it still deserves review.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
actions/checkout refuses a fork ref under pull_request_target without
allow-unsafe-pr-checkout, and the flag is not the fix. The working
directory is what Claude Code reads as the project, so checking out a
fork hands it that fork's CLAUDE.md as instructions and that fork's
.claude/settings.json hooks as commands -- neither of which the
--allowedTools list governs. Constraint 2 said nothing from the pull
request is executed; a checked-out tree could not honour it.
Check out the base branch instead and bring the contribution down as
diff text in one file. The reviewer reads the diff against trusted
sources rather than the merged tree, which is less context than the
same-repo path gets, and the right trade for code we do not control.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
A fork PR gets no secrets on `pull_request`, so claude-code-review.yml
skips it. `pull_request_target` is the only event that reaches the
diff with our token, and it puts that token in a job beside code we did
not write, so the fork path is a separate file under four constraints:
a label applied by someone with write access is the only trigger,
nothing from the PR is executed, the tool allowlist is read-only, and
the job holds no write permission to carry anything back out.
The same label also re-runs the ordinary review, which a PR opened
before that workflow existed otherwise has no way to start.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* feat(terminal): animate wheel scrolling instead of jumping a notch at once
Sub-line scroll positions were already in place — the view keeps a
fractional remainder and paints the grid shifted by it — but the position
was a function of the event, not of time. A notch arrived and the whole
distance was applied at once, so whether it looked smooth came down to how
fine-grained the platform's deltas happened to be.
A macOS trackpad reports pixels, so it did. A wheel on Windows reports
whole lines (gpui multiplies the notch by the system's scroll-lines
setting, three by default), the fraction came out zero every time, and the
view jumped three lines per notch. The sub-line machinery was present and
never engaged.
A 120-step notch is one discrete pulse; no arithmetic on the delta recovers
a continuous gesture from it. So make position a function of time: a notch
adds to a remaining distance and each frame consumes a share of what is
left, ~120ms to land, exponential, with a sub-pixel remainder snapped
rather than approached since every frame of it costs a repaint.
Line deltas are discrete and get animated; pixel deltas are continuous and
do not — putting an animation between a trackpad and the grid would only
add lag. Mouse reporting and alternate-scroll keep forwarding whole lines,
which cannot be spread over frames either.
The distance in flight is relative rather than an absolute target, so
output arriving mid-scroll shifts the grid without dragging the animation
elsewhere. Everything that moves the view on its own cancels what is in
flight first.
Settings -> Terminal -> Scrolling -> Smooth scrolling, on by default.
Also pin the scratch config dir in the terminal-view test harness: building
a view reads the config, and which test got there first decided whether
that touched the real user directory.
* fix(terminal): tell a trackpad from a wheel by phase, not delta type
The first cut split the two on the delta variant — Lines meant a wheel,
Pixels meant a trackpad. That holds on Windows and Linux. It does not hold
on macOS, which sets hasPreciseScrollingDeltas for a wheel mouse as well, so
a wheel reported pixels, took the trackpad path, and was never animated.
Measured on this machine, one detent arrives as a single ~103px event —
about five lines at a 21px line height, applied in one go. That is a worse
jump than the three lines Windows produced, and it was invisible to a check
based on the delta's type.
Size cannot separate them either: a trackpad flick reaches ~3 lines in one
event while an inched wheel moves ~0.6, so the ranges overlap and any
threshold misclassifies a quarter of the trackpad stream.
Phase can. Only a device that can gesture ever reports Started/Ended; a
wheel is Moved forever, on every backend. So track the gesture instead —
and hold it open on a 150ms idle timer rather than closing it on Ended,
because lifting the fingers is not the end of the stream: the momentum tail
keeps delivering Moved events larger than the gesture that spawned them,
and animating those would smooth what the system already smoothed.
Also skip the animation for jumps under a line. Inching a wheel one detent
at a time lands there, and spreading a half-line move only adds lag to
something that already reads as continuous.
Tests are now shaped after the measured event streams rather than after an
assumption about which variant each device sends.
* chore: trigger a Claude review of this branch
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Runs the code-review plugin on every PR open and push, on our own
Actions minutes rather than the managed Code Review service, and posts
the findings back onto the PR. Advisory only: the required checks on
main stay rustfmt and the three build & test jobs.
The append-system-prompt carries the four rules a general-purpose
reviewer cannot infer -- dialect bumps staying readable to an older
peer, src/ui paths going through Host, i18n keys landing in all three
locales -- and tells it not to repeat what rustfmt and clippy already
decide.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(daemon): notice a server that is only a control dialect behind
The pane protocol and the control dialect are versioned apart, and the
launch check only compared the first. A server from before the v4 to v5
control bump answers the pane handshake with this build's own number, so
it was waved through as ours while every machine-tree call was refused:
the window opened with no tabs, the only trace a log line the default
config does not write anywhere.
Ask the control socket too, before calling the server ours, and say which
version disagrees when offering the restart. A window that still opens
empty now says why in the window it happened to.
* fix(daemon): do not call a newer server old, and say the empty-window reason once
The dialect handshake reports disagreement, not direction, but the restart
prompt read every mismatch as "from an older build". A daemon left running by
a newer build passes the pane-protocol check the same way an older one does,
so the prompt told users their newer server was old — the same wrong guess the
remote path stopped making in #384. Split the copy in two and pick by which
side is ahead, as `dialect_complaint` already does for remotes.
The window's own explanation had two ways to misfire. A hydration superseded
by a newer one still announced an emptiness someone else was already filling,
because only `owe_rehydration` checked the epoch; it now reports whether it
claimed the debt and only the owner speaks. And since a failed pull is retried
from every `sync_window`, a machine that never answers repeated the same
notification every fifteen seconds — latched to once per window, cleared when a
pull finally lands so a later outage still gets a word.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
The update channel becomes a property of the installation rather than
something derived from how version numbers happen to sort, so a Nightly
follows Nightly instead of being walked back onto Stable by an update it
never asked for. Stable reads /releases/latest, which excludes
prereleases; Nightly reads /releases/tags/nightly. Neither feed can hand
the other an update, so an installation only changes channel when the
user changes it in Settings.
The nightly release cannot state its version in its tag — `nightly` is
force-moved every night, so `tag_name` is the literal string. It now
publishes nightly.json beside the packages, falling back to parsing asset
filenames for builds that predate the manifest. Prereleases are ordered by
every numeric identifier in the stamp, and the stamp goes to the minute so
two builds in one day are distinguishable; a stable release still outranks
every dated build of its core version, which is how switching back to
Stable graduates instead of downgrading.
Switching channel invalidates what the old feed produced: the staged
package, the deferred prompt, and the transfer still in flight, which
would otherwise finish and stage a build from the channel the user just
left. Settings keeps one action on the update row rather than three —
the update dialog covers the rest, but it is a moment rather than a place,
and where the package cannot be installed for the user the release page is
the whole update path. Skipping a version is retired along with its state,
its Settings row, and its localization keys.
Also carries the staging work this was branched from: an update is fetched
and verified while the prompt is up, so installing it is a restart, and
declining one defers it instead of retiring it permanently.
A handshake refused on the control dialect was shown with the protocol
layer's own wording — "java answered, but not as a tty7 server: control
peer (build …) speaks control v4, this build speaks v5". The far end is
tty7; it is a build on the other side of a dialect bump, and the reader
cannot act on the dialect numbers either way. Restate it as which side is
behind, and name the action on the button: "Update Server", not
"Restart Server".
The failure that followed was also invisible. The switcher paints a failed
`connect` in preference to `remote_host_errors`, and restarting or
replacing a server cleared only the latter, so whatever went wrong during
the install was covered by the complaint that started it — the button read
as doing nothing at all.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* feat(switcher): two-column panel with IDEA-style Ctrl+Tab
The switcher listed workspaces only; reaching a tab inside one meant
opening it first. It is now two columns — workspaces on the left, the
tabs of whichever one the cursor sits on to the right — and Ctrl+Tab
raises it as a most-recently-used tab switcher that commits when the
modifier comes up.
Picking a workspace or a tab now switches this window in place. A second
window is something you ask for, with the platform modifier or "Open in
New Window", rather than what happens by default.
New workspaces get a codename ("amber-yak") instead of inheriting
whatever directory their first shell started in. The generator moves out
of worktree.rs into core::codename so both callers share it.
* fix(switcher): review fixes — platform-gate the Ctrl+Tab key test, park the new-window tab
- The key_intent test asserted only the macOS half of the Tab chord; off
macOS Ctrl *is* the secondary modifier, the raw key falls through on
purpose (the chord arrives as NextTab), and the test now says so
instead of failing there.
- open_at_tab probed a freshly opened window's tabs exactly once, but a
new window hydrates them asynchronously — route through
activate_tree_tab so the pick parks until the tab arrives.
- Escape mid-rename backs out of the rename instead of tearing down the
whole panel.
- A one-tab workspace reads '1 tab', not '1 tabs'.
Claude-Session: https://claude.ai/code/session_01V7xjxdUGWQp93KQDuFuh8d
* style: cargo fmt
Claude-Session: https://claude.ai/code/session_01V7xjxdUGWQp93KQDuFuh8d
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
The single en/zh tuple table becomes one module per language behind a
`SUPPORTED_LANGUAGES` table, and Japanese joins English and Simplified Chinese.
- `gui_language` accepts `ja-JP`; anything unrecognized still falls back to `en`.
- The language picker and `refresh_locale_state` both read `SUPPORTED_LANGUAGES`
instead of keeping their own copy of the code list.
- Language names in the picker stay endonyms (English / 简体中文 / 日本語) in
every locale, as English and Chinese already were.
- The zh and ja key tables are exhaustive, so a new `L10nKey` fails the build
until it is translated rather than silently rendering English.
Co-authored-by: Chihiro WATANABE <chihiro.watanabe@live.jp>
The GUI update check already uses reqwest, which reads the Windows
system proxy from the registry by default. The remote server
installer / bundled-server fallback uses ureq, which only reads
HTTP_PROXY/HTTPS_PROXY environment variables unless the
win-system-proxy feature is enabled.
Enable ureqs win-system-proxy feature so that release downloads
inside the daemon also honor the Windows system proxy set by tools
like Clash (System Proxy mode), v2rayN, etc. This is a no-op on
non-Windows platforms.
Fixes the inconsistency where the update check could reach GitHub
through the proxy but the actual download would time out trying to
connect directly.
Resolve an HTTP/SOCKS proxy for tty7's own update checks and release downloads, from (in order) a new `http_proxy` config field, the platform system proxy — Windows registry / macOS SCDynamicStore — and the HTTP_PROXY/HTTPS_PROXY/ALL_PROXY environment variables.
Programs running in a pane are deliberately unaffected: they inherit their proxy from their own environment, as in any other terminal.
Fixes#365.
Desktop notifications now carry the pane they came from: clicking one reveals
that pane's window, tab and split. Windows shows a WinRT toast with an
`Activated` handler, macOS uses mac-notification-sys' click response, and both
route through the existing tray dispatch channel. Linux keeps the plain
notify-rust path.
Titles gained context — an agent name or the machine, then the workspace — and
bodies name the command or agent alongside the duration, all of it translated.
Notification text is sanitized on every path: it comes off the terminal, and a
stray control byte used to make the Windows toast XML fail to parse and lose the
notification outright.
Co-authored-by: Hongwei Qin <exqinhongwei@outlook.com>
* fix(bundle): declare macOS TCC privacy keys for child processes
tty7 currently ships no NS*UsageDescription keys and no data-access
entitlements, so macOS falls back to a repeated "access other apps' data"
prompt whenever a child process (shell, coding agent, mole, etc.) touches a
protected folder such as ~/Library/Containers, Mail, Messages, or Calendar.
kitty and Kaku both declare these privacy intents, which converts the prompt
into a single, clear one-time grant.
Add the folder/volume usage descriptions and the matching personal-information
and device entitlements to the macOS bundle so the app behaves like its
terminal peers.
* fix(bundle): rework TCC usage strings per review
- Correct problem statement: describe child-process-denied-without-prompt
instead of the Full Disk Access framing (no NS*UsageDescription key exists
for that class).
- Add the full usage-string set (camera, microphone, contacts, calendars,
reminders, photos, location, motion, local network, bluetooth, speech
recognition, system administration, apple events), kitty-style wording.
- Use macOS spellings: NSCalendarsFullAccessUsageDescription /
NSRemindersFullAccessUsageDescription / NSLocationUsageDescription.
- Drop every entitlement that has no matching usage string; keep only
com.apple.security.automation.apple-events.
- Restore trailing newline at EOF in bundle-macos.sh.
- Document the Full Disk Access manual-grant requirement in docs/features.md.
* docs: rewrite macOS privacy as feature notes (en + zh-CN)
* fix(bundle): drop the apple-events entitlement, tidy the privacy docs
The entitlement did not do what its comment claimed. Nothing in tty7 or in
gpui's mac platform layer sends an Apple event, and it would not help the
case this change is about either: the hardened-runtime automation check runs
against the process actually sending the event, which is the pane's child
carrying its own signature. What TCC reads off tty7.app is the usage string
in Info.plist, which stays. Entitlements are per-executable and never
inherited, so granting this one only widened what injected code could reach
under an identity that already holds disable-library-validation.
Docs: spell out the four Full Disk Access paths instead of running them
together as one nested path, drop motion from the user-facing list (Core
Motion has no macOS implementation, though the key stays for kitty parity),
and place the section identically in the English and Chinese files.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(terminal): honour application cursor key mode (#361)
Arrow, Home and End were always sent as their CSI form, no matter what
the foreground program had asked for. Programs that turn on DECCKM via
smkx -- which is every ncurses full-screen app -- expect the SS3 form,
because that is what xterm-256color spells kcuu1 & co. as, and ncurses
matches terminfo byte for byte.
htop was the report: ncurses failed to match \E[A, handed the bytes to
htop one at a time, and htop binds `[` to "lower priority", so every Up
or Down bumped the selected process's nice value instead of moving the
selection. The same breakage hits ncdu, mc, dialog, menuconfig, nmtui.
Shells were unaffected because readline and zle bind both forms, and
the mouse wheel already got this right in wheel_route().
Named keys now also carry their modifiers the way terminfo declares
them (kLFT=\E[1;2D, kUP5=\E[1;5A, kDC3=\E[3;3~), instead of dropping
Shift/Ctrl entirely and prefixing Alt with a bare ESC. Cmd stays out of
the modifier parameter -- xterm has no encoding for it.
The legacy and kitty encoders shared this table already; they now share
one function, so both follow DECCKM. KittyFlags grew past its name and
becomes KeyFlags.
Verified in a dev instance: with DECCKM on the arrows arrive as ^[OA
^[OB ^[OD, with it off as ^[[A ^[[B ^[[D.
* test(terminal): guard the SS3 cursor walk-back on line handoff
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Removes the Outline right panel and all of its wiring: the `RightPanelTab::Outline` variant, the `ShowRightPanelOutline` action and its handler, the tab-strip button, the palette command, the keymap arm, `render_panel_outline`, `TerminalView::{command_marks, scroll_to_mark}`, the `one_line` helper, and four i18n keys.
An existing config value of "outline" falls back to the default Info tab via `de_lenient`, and a user keybinding naming `ShowRightPanelOutline` degrades to a logged warning rather than breaking the keymap.
Also drops what the removal stranded: `Marks::list` is now `cfg(test)` (its last production caller was `command_marks`), and `icons/list.svg` — the Outline button's icon, with no other user — is gone.
Closes#374
The per-window taskbar overlay badge (#355, for #199) is removed, and with
it the in-flight follow-up that was making its green "finished a turn"
state reachable: the feature is not wanted. Nothing shipped — the badge
only ever existed in Unreleased — so this is a plain removal rather than a
deprecation, and its CHANGELOG entry goes with it instead of gaining a
"Removed" counterpart.
What goes: `ui::taskbar` and its `ITaskbarList3::SetOverlayIcon` poll, the
`taskbar_status_icon` config flag and its Settings → Window & Tabs row and
strings, `Tty7App::taskbar_signals`, `TerminalView::shell_busy` /
`RemoteTerminal::shell_busy` (the overlay was their only caller), the
`raw-window-handle` dependency and the `Win32_UI_WindowsAndMessaging`
feature it needed, and the feature docs in both languages. A stale
`taskbar_status_icon` left in someone's `config.json` is ignored, as any
unknown key is.
The tray badge and the in-window status dots are untouched; they were
always the ones the taskbar was mirroring.
`verify-windows-package.ps1` reads the Inno staging directory to check what
lands in {app} — the compiled setup.exe cannot be read back without
innoextract, which the runners do not carry. But `bundle-windows.ps1` deleted
that directory as its last act, so the verifier has failed on every Windows
build since the check arrived in #330: "the Inno staging directory is missing".
Nightly has been red for two nights (2026-08-05, 2026-08-06) and a stable
release would fail the same way — release.yml runs the same step.
Both workflows already expected the directory to survive: their upload steps
name it among the dist/ intermediates the asset globs deliberately skip. So
this drops the removal rather than teaching the verifier to tolerate an absent
payload, which would retire the check it was added to make.
Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
The About page carried a marketing paragraph and two multi-sentence
explanations that walked through updater internals and per-platform
support. None of it helps someone who is already running the app.
Drop the feature-list paragraph, cut the update and server text to a
single sentence each, move the tech credits to the bottom of the page,
and render the update toggle with settings_row so it matches every
other switch in Settings. The tagline and credits line now match the
README.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
conhost's VT renderer brackets every frame it paints with `?25l` … `?25h` so
the cursor does not flicker across the repaint, and it moves the cursor
explicitly just before the `?25h` only on the frames where it painted the
cursor. On the other frames the show commits wherever the last erase or write
left it — the tail of the status line, the head of a row — and the cursor
blinks there until conhost's next frame moves it back.
tty7 repaints when a batch of pty output lands, so it draws that cursor for a
frame. A TUI that repaints on a spinner produces one every tick, which reads as
a second cursor blinking in the wrong place. macOS never shows it: no ConPTY
sits in between, and the TUI itself always moves the cursor before showing it.
Measured on Windows 11 26200 from a raw ConPTY capture of a Codex session, a
110x30 pty, cursor-visible dwell per cell:
in-box conhost: 295 ms across 42 frames parked at the end of the status line,
each stray corrected 7-15 ms later by the following frame
with this fix: that cell never appears; those frames fold back into the
composer cell the repaint hid the cursor on
A scanner over the stream pairs the hide with its show and marks the show as
parked when the run in between moved the cursor around to paint but did not end
on a move — nothing chose the cell it is about to appear on. The repair then
restores the cell the cursor stood on when it went invisible, which is where the
correcting frame would have put it anyway. A hide and a show more than 100 ms
apart are an application keeping the cursor off for the length of some work, not
a renderer bracketing one frame, and are left alone.
This does not cover the other ConPTY cursor artifact: conhost also samples an
application's partially written frame, and then it *does* emit an explicit move,
so the stray position is genuine — just transient — and nothing in the stream
tells it apart from a real one. Only a render-side settle would catch that.
Microsoft's ConPTY redistributable avoids both, so with the bundled pair beside
the daemon this is inert; it earns its keep on hosts without it, notably a
remote Windows server.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(windows): bundle Microsoft's ConPTY so panes can answer color queries
The in-box conhost swallows a pane process's OSC 11 background query: it
never reaches tty7's emulator and no reply is ever written back, so
applications that choose a light or dark UI from the terminal background
render a dark UI under a light theme.
tty7 already answers OSC 10/11/12 from the live theme, so nothing was
missing but a pseudoconsole that forwards the question. Microsoft ships one
as a redistributable, and portable-pty already prefers a sideloaded
conpty.dll over kernel32's, so this is packaging rather than code: the pair
goes beside tty7-app.exe, where the DLL search path finds it.
Measured on Windows 11 26200, same binary, only the pair added beside it:
in-box conhost: the terminal side never sees the query; the client times
out with no reply
bundled ConPTY: the terminal side sees ESC]11;?BEL and a real pane reads
back rgb:efef/f1f1/f5f5 under catppuccin_latte, which is
the preset's exact background
The two files are one supported unit, so the release verifier fails a
package that carries only one, a mismatched pair, or the MIT notice-less
DLL. They also join PORTABLE_MANAGED_ROOTS, without which the updater would
reject every portable archive that contains them; they are deliberately not
required by verify_portable_payload, since tty7 runs without them and a
packaging slip should fail the release rather than a user's update.
build.rs stages the pair beside cargo's output so a development build does
not quietly run on the in-box host, and the daemon logs which pseudoconsole
it got.
Closes#345
* fix(windows): restage the bundled ConPTY when it goes missing
Watching only the vendored sources meant a staged copy that left the target
directory stayed gone: the build script was cached, so it never ran again to
put it back, and the build silently fell back to the in-box conhost. Cargo
treats a rerun-if-changed path that does not exist as changed, so naming the
destinations makes the staging self-healing.
Found by deleting target/debug/conpty.dll and watching the next build not
bring it back.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
ring_system_bell() was macOS-only and returned false everywhere else, so
on Windows and Linux the Audible mode fell straight through to its visual
fallback: Visual, Audible, and the new Both were three names for one
behavior. Windows has MessageBeep, so two of those three now differ.
MB_OK plays the "Default Beep" scheme entry, which follows the user's
choice in Sound Settings rather than synthesizing a fixed tone at the
speaker the way Beep() does. The Win32 metadata files MessageBeep under
Diagnostics::Debug despite it being a user32 export, hence the extra
windows-sys feature; no new crate and no dbghelp.
Linux is left on the flash fallback on purpose: libcanberra and PipeWire
are runtime links away and XBell does nothing under Wayland.
Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* feat(windows): taskbar status overlay per window (#199)
Stamp a colored status dot on each window's taskbar button using the
same palette as the in-window agent dots:
- blue while a shell command or agent is working,
- amber when an agent is waiting on the user,
- green when work finishes while the window is unfocused (cleared on activation).
Adds a `taskbar_status_icon` setting (default on, Windows only) and a
Settings -> Window & Tabs row. The overlay is updated by a foreground
poll that aggregates agent status and shell busy state across each
window's panes, diffing against the current taskbar badge and only
calling ITaskbarList3::SetOverlayIcon when the badge changes.
Includes unit tests for overlay priority and the done-while-unfocused
edge tracking.
* fix(taskbar): retry a failed overlay instead of caching it as drawn
Four fixes on top of the overlay:
- A failed SetOverlayIcon was still recorded in `shown`, so a badge the
taskbar never took was remembered as drawn and never retried. Stamp now
reports success, and a failure drops the interface so the next tick
re-creates it — which is also what an Explorer restart needs.
- `create_failed` was a permanent latch: one CoCreateInstance failure
killed the badge for the whole process, though Explorer may simply not
be up yet when the first window opens. Use the tray's attempts/cooldown
backoff instead, which this module otherwise copies.
- The overlay's accessibility description was hard-coded English in an
app that localizes everything else. Reuse the panel and tray strings.
- Render the dot at 32px, not 16. SetOverlayIcon wants 16x16 at 96 dpi,
so at 150%/200% scaling the shell upscaled a 16px icon; `tray::icon`
already renders at 32 off macOS for the same reason.
Also drops the Win32_Graphics_Gdi feature: CreateIcon, DestroyIcon and
HICON all live in Win32_UI_WindowsAndMessaging, and the build and the
taskbar tests pass without it.
Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
* feat(bell): add combined visual + audible terminal bell mode
Currently BellMode only offers None, Visual, and Audible. Audible falls
back to Visual if the system bell cannot be rung, but there is no way to
intentionally get both at once.
Add a `Both` variant that rings the system bell *and* flashes the pane,
exposing it as a fourth option in Settings -> Terminal -> Bell. Existing
`none`/`visual`/`audible` values remain backward compatible.
Updates config serialization tests and i18n keys/translations/test list.
* fix(bell): align the settings copy and picker with the new Both mode
The bell description still enumerated three outcomes and the settings
search keywords omitted both, so the new mode was invisible to search
and contradicted by the row that labels it. Drop the picker's catch-all
onto the default instead of Both, which is the shape the PR just fixed.
Claude-Session: https://claude.ai/code/session_01H9QqEZ6JH3dGS6atEcf6ab
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Windows hands every process a private copy of the environment block at
`CreateProcess` time and never updates it. A tty7 daemon that has been up
since before an installer edited `HKCU\Environment` therefore gave a
brand-new pane its startup `PATH`, and the freshly installed command was
unresolvable until tty7 restarted (#333) — while a Windows Terminal
launched from Explorer found it, because Explorer rebuilds its own block
when it sees `WM_SETTINGCHANGE`.
Rather than chase broadcast messages, `daemon::windows_env` re-reads the
two hives Windows itself composes a process environment from — the
machine `Session Manager\Environment` and `HKCU\Environment` — at the
moment a pane is spawned, and pins the merge onto the pane's command.
The merge is a pure function over (machine, user, process, configured
overrides), so every semantic that matters is unit-testable without a
registry:
- Names are keyed case-insensitively, so a `Path` from the process block
and a `PATH` from a hive collapse into one variable instead of reaching
the child as two.
- `PATH` and `PSModulePath` are *combined* — machine value first, user
value appended — which is what Windows does and what keeps a per-user
install from shadowing the system half. Nothing is ever written back to
the user hive; that would bake the machine half into it permanently.
- `REG_EXPAND_SZ` values are expanded against the merged map, so a user
value naming a machine value naming a process value resolves. A
reference that resolves to nothing is left verbatim, as Windows leaves
it, and the chain is depth-bounded so a self-referential value cannot
hang the spawn path.
- The machine hive's `USERNAME=SYSTEM` is dropped, as Windows drops it.
- Directories the daemon's own `PATH` held that neither hive lists stay
reachable, appended behind the registry entries: freshening `PATH`
should only ever add resolvable commands, never take one away.
- Configured `env` overrides are applied last and win outright.
Non-Windows builds are untouched: only the registry reader and the spawn
wiring are `cfg(windows)`, while `cfg(test)` keeps the pure merge
compiling everywhere so its tests run on every platform.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A pane configured to run fish still advertised the login shell, because
pane_environment() injected TERM, the TTY7_* markers and TERM_PROGRAM
but never touched SHELL -- so the pane inherited the GUI session's
login-time snapshot of it. Everything that spawns "the user's shell"
read that: tmux's default-shell started zsh inside a fish pane, and so
did sudo -s, an editor's shell escape, and any coding agent picking a
quoting dialect from $SHELL. The failure is silent -- fish rejects the
bash line, the agent's sentinel file never appears, and the rejected
text stays in the line editor to concatenate onto the next send.
Inject SHELL alongside the other markers, set to the absolute path of
the program the pane is about to exec. That program is read off argv
rather than off the shell tty7 resolved: an argv-replacing integration
injection and the parent-shell override both rewrite argv, while
CommandBuilder::get_shell() keeps answering the passwd entry.
Only an absolute path is ever written. A configured command may be bare
(the inventory keeps it bare so PATH decides which install wins), and
consumers exec $SHELL under a PATH of their own, so a bare name is
resolved against the PATH the pane will inherit -- the user's env-block
PATH when they set one -- and skipped when that finds nothing. A stale
login shell beats a name that resolves somewhere else.
An explicit SHELL in the user's env block still wins, the same
precedence TERM_PROGRAM has: tty7 describes the pane, the user's config
gets the last word.
Windows is deliberately left out. Neither cmd nor PowerShell reads
SHELL; the tools that do are the POSIX emulations (MSYS/Git Bash,
Cygwin, WSL), and they want a POSIX path, not the Windows one this
would have to give them. That also leaves the WSL pane alone, where the
pane program is wsl.exe and the distro's own login shell is the right
answer.
Native SSH panes are unaffected: they never build a local command, and
the remote sshd sets SHELL from the remote passwd entry.
Closes#342
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
A TUI that turns mouse reporting on (vim with `set mouse=a`, lazygit,
tmux, …) draws its own right-button menus, and tty7 was delivering one
right-click to both consumers: `TerminalElement::register_mouse_handlers`
forwarded the press to the application, while the `.context_menu(…)` on
the terminal surface was attached unconditionally and popped tty7's own
menu over the top of it.
Gate the host menu on a single pure predicate, `should_show_context_menu`,
and call it from both sides so one click can only ever feed one consumer:
while reporting is active the unmodified right-click is the application's
alone, and Shift stays the escape hatch that reaches tty7 — the same
override Shift already provides for selection and for the wheel.
gpui-component's `ContextMenu` element owns the right mouse-down that
opens the popup: it wraps the terminal surface, so its listener fires
before any handler we can attach, and the builder closure it calls gets
no event to inspect. The verdict is therefore latched on our own right
mouse-down (safe, because the builder runs from a `window.defer` that
lands after the whole mouse dispatch has unwound) and a suppressed menu
is expressed as an item-less `PopupMenu`, which that element already
skips rendering.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
About had grown three sections that change system state and that nobody
looks for under "About": a PATH install, a registry write, and a daemon
restart. Two of them move out.
The `tty7` CLI goes to Agents. That page already describes tty7 <-> agent
integration in one direction (hooks reporting session status); the CLI is
the other direction, and its own description leads with "so scripts and
coding agents can drive tty7". The Loading and Unavailable arms there no
longer return early, since the CLI toggle is about this GUI's own host
rather than whichever machine the hook rows describe.
The Windows Explorer context menu goes to the installer, which is where
VS Code and Git for Windows put theirs: writing shell verbs is an
install-time decision, not a runtime preference. A task checkbox drives
new `--register-explorer-menu` / `--unregister-explorer-menu` flags, so
the key layout stays in core::explorer_context_menu instead of being
copied into the .iss. `status()` existed only to paint the settings UI
and goes with it. The uninstaller unregisters unconditionally: an install
that registered once and was later upgraded without the box ticked still
holds keys that would otherwise point at a deleted exe.
Server restart stays — it is about the app itself.
Also fixes localization the About section had skipped: eight hardcoded
English strings in the update block now have keys, and the orphaned
SettingsCheckUpdatesDesc key (which still claimed "tty7 never updates
itself", contradicted by the macOS in-app updater) is reused for a
one-line description in place of a 60-word account of the updater's
internals.
Finally, terminology in the Chinese UI. hook, agent, worktree, diff and
fork are read and spoken in English by Chinese developers, so translating
them lost more than it gained. Scrollback was worse than a style
question: 回滚 means rollback, the opposite direction. 窗格 for pane is
kept — that one is standard.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(install): prefer bundled server over release download for SSH remotes
SSH remote installs used , which only checks
and ignores the server binary already shipped next
to the Windows executable. WSL already uses
to find that bundled binary.
Add to auto-discover the bundled server and
fall back to the GitHub release download only when no matching local asset
exists. Switch and to use it.
This lets the Windows installer/zip (which already stages server binaries
under <exe>/server/) satisfy SSH remote installs without hitting the network.
The explicit path keeps its strict no-fallback behavior, and WSL
remains bundled-only.
Refs: future issue/PR for bundling server binaries into Windows releases.
* style(install): fix rustfmt formatting in bundled-server tests
* fix(ui): show remote server errors under their switcher group
Remote server restart/replace failures were reported through a global
modal dialog, which mixed errors from different machines together and
blocked the UI.
- Add to keep per-host error messages.
- Rename to ; when a
is available, store the error under that host's key and
expand its switcher group. Only fall back to a modal when no target is
known.
- Read when building switcher groups and surface the
message in the existing per-group error block.
- Add a Dismiss button to the group error block and clear stored errors
when the user retries or replaces the server.
Refs: #<issue-number>
* fix(ui): keep remote errors visible when the switcher is closed
The grouped error block is only on screen while the switcher is open, so
routing every failure into it silently swallowed the ones raised from the
window menu's restart-server command and from a mismatch hit mid-connect.
Fall back to the modal whenever there is no switcher to put the error in.
Also scope the Dismiss button to its own host: it retired whatever connect
flow happened to be in `self.connect`, including one still connecting to a
different machine. And clear a stored error when a fresh connect to that
host starts, so a later successful connect does not leave the group showing
a stale failure.
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* fix(install): prefer bundled server over release download for SSH remotes
SSH remote installs used , which only checks
and ignores the server binary already shipped next
to the Windows executable. WSL already uses
to find that bundled binary.
Add to auto-discover the bundled server and
fall back to the GitHub release download only when no matching local asset
exists. Switch and to use it.
This lets the Windows installer/zip (which already stages server binaries
under <exe>/server/) satisfy SSH remote installs without hitting the network.
The explicit path keeps its strict no-fallback behavior, and WSL
remains bundled-only.
Refs: future issue/PR for bundling server binaries into Windows releases.
* style(install): fix rustfmt formatting in bundled-server tests
* fix(ui): replace remote server binary from the mismatch dialog
The version/protocol mismatch dialog previously offered a 'Restart Server'
button that only restarted the existing daemon without replacing the
incompatible binary. This left users stuck on the same mismatch after the
restart.
- Change restart_mismatched_remote_server to call replace_remote_server
(replace binary + restart daemon) instead of restart_remote_server.
- Add a dedicated L10nKey::RemoteMismatchReplaceServer ('Update Server' /
'更新服务器端') and use it for the dialog's action button and detail text.
- Update the mismatch title/detail copy so it describes replacing the server
binary rather than restarting it.
Refs: #351
---------
Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
* fix(install): prefer bundled server over release download for SSH remotes
SSH remote installs used , which only checks
and ignores the server binary already shipped next
to the Windows executable. WSL already uses
to find that bundled binary.
Add to auto-discover the bundled server and
fall back to the GitHub release download only when no matching local asset
exists. Switch and to use it.
This lets the Windows installer/zip (which already stages server binaries
under <exe>/server/) satisfy SSH remote installs without hitting the network.
The explicit path keeps its strict no-fallback behavior, and WSL
remains bundled-only.
Refs: future issue/PR for bundling server binaries into Windows releases.
* style(install): fix rustfmt formatting in bundled-server tests
---------
Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
* feat(updater): add windows online updates
* feat(updater): support online updates for windows portable zip builds
f
* feat(updater): support online updates for nightly build
* fix(updater): strengthen post-download update verification
* feat(updater): support explicit stable and nightly channel switching
* fix(i18n): localize update settings ui
* fix(settings): prevent slider value labels from wrapping
* feat(updater): drop the nightly channel, refuse all-users Windows installs
Follow-up to the Windows updater work on this branch, applying maintainer
review.
Nightly is a build channel, not an update channel. The updater consults
`/releases/latest` again and nothing else, so it behaves on Windows exactly
as it already does on macOS: a Nightly build is offered the stable release
that supersedes it and graduates out of the prerelease, and no rolling
prerelease can become a source of code that gets executed on a user's
machine. Removed with it: the `UpdateChannel` enum and its version-string
inference, the `tags/nightly` query, the cross-channel version-ordering
bypass, the Settings → About channel row, the rolling-tag
`update-manifest.json` and the i18n keys that only served them.
`parse_version` and `is_update_available` are byte-identical to main again.
Nightly builds are untouched, and still carry tty7-updater plus the macOS
update archive — a Nightly user needs a working helper to reach the stable
release that replaces their build.
An all-users Windows installation is no longer updated in place. Running the
release Setup silently as the signed-in user cannot replace
`C:\Program Files\tty7`: Inno resolves `{autopf}` to `%LocalAppData%\Programs`
and installs a second copy beside the real one, or re-launches itself
elevated and puts a bare UAC prompt for an unsigned executable in `%TEMP%` in
front of a user whose GUI just vanished. tty7 declines both and points at the
release page. Detection reads Inno's own `HKLM` state for the frozen AppId and
independently probes whether the directory accepts writes, so a relocated or
pruned installation is caught too; the decision is a pure function with unit
tests, and it is re-checked before the download as well as during it.
Release and Nightly now verify the Windows packages they just built, mirroring
the macOS update-archive step: the install marker, tty7-updater.exe, the ZIP
layout the updater will accept and the PE versions it will demand. Every fact
the updater checks on the user's machine after downloading is checked here
instead, so a packaging mistake fails the build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(terminal): upload pasted images to the remote host in SSH panes (#337)
* fix(terminal): stage remote clipboard images in a private dir, off the UI thread
Review follow-up to the SSH image-paste upload.
`/tmp/tty7-clipboard-<user>` is a predictable name in a world-writable
directory: any local account on the remote host could pre-create it, and
the `Mkdir` result was discarded, so tty7 would have uploaded into a
directory someone else owned — readable by them, and swappable for
another image before the pane's agent opened the path. Screenshots are
exactly the payload that must not land there.
Images now stage in `$HOME/.cache/tty7/clipboard`, resolved from the
session's own `realpath .`, and the directory is verified before anything
is uploaded into it: a symlink is refused outright (`stat` would judge it
by its target), a `chmod 0700` the daemon watched succeed is the
ownership proof — POSIX only lets the owner change a mode — and a
following `stat` must report exactly `0700`. Any doubt is a hard failure
that falls back to pasting the local path rather than uploading. The
uploaded file is chmod'd `0600` once the transfer lands.
Only a verified directory is cached, so a preparation that failed is
retried on the next paste instead of latching a "ready" flag over a
directory that was never created.
All of it moves off the UI thread. Preparing the directory, starting the
transfer and polling it are blocking daemon+SSH round trips — the
workspace route gives up after 30s, the standalone-SSH route sets no read
timeout at all — and a keystroke handler must not make them. The pane
pastes from a background task instead, which is also what lets the upload
be watched to a terminal state: the SFTP panel's history only polls while
that panel is open and drops finished jobs after 30s, so a failed upload
used to leave a dangling remote path in the line and say nothing. Now it
notifies, once, naming the host and the reason.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(windows): brand toast notifications with a tty7 AUMID (#339)
* fix(windows): only write the toast shortcut where it is ours to write
The AUMID shortcut was rewritten on every launch, which broke two cases
the review caught on a real machine.
An elevated install owns `%ProgramData%\...\tty7.lnk`, so writing a
per-user copy listed "tty7" twice in the Start Menu and left an orphan
pointing at a deleted exe once the uninstaller had removed only its own.
And `cargo run` repointed the installed shortcut at `target\debug`,
permanently, for anyone who both installs tty7 and builds it.
So decide before writing. An all-users shortcut settles the question by
itself — branded if the installer stamped our AUMID on it, otherwise we
stay on the PowerShell identity, because the alternative is littering a
Start Menu we cannot clean up. Otherwise we refresh the single per-user
`tty7.lnk` Inno's default install owns anyway, and only when it is not
already ours, and never from a cargo build directory. A dev build still
brands the process for taskbar grouping, and still gets branded toasts
when an install left a stamped shortcut behind — Windows asks that the
AUMID be registered, not that it point at the process using it.
Reading a shortcut back needs `IShellLinkW::GetPath`, hence the
`Win32_Storage_FileSystem` feature; `SLGP_RAWPATH` keeps it from chasing
a moved target over the network.
Also close the window this opened. The shell indexes a new `.lnk`
asynchronously and, for an AUMID it has not seen, `Toast::show()`
reports success and drops the toast — measured, it does not return an
error. A shortcut we wrote seconds ago is therefore not yet proof of
anything, so toasts keep the PowerShell identity for half a minute after
we write one: ugly beats invisible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(terminal): eliminate seams between Powerline separators
* fix(terminal): skip the separator cover quad when the glyph is dim
The cover quad and the anti-aliased path overlap on the closing edge's
device pixel. With an opaque foreground that is a no-op, but a DIM cell
carries fg.a = 0.66, so the two compositing passes push that one column
to 1 - 0.34^2 = 0.884 alpha and tint the neighboring cell's background.
Emit the quad only for opaque separators.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(i18n): correct Chinese terminology and wording
The worst one collided two different concepts: a git worktree was
translated as 工作区, the same word tty7 uses for a workspace. "Remove
Worktree" therefore read as "delete this workspace" in a destructive
confirmation, and New Worktree Tab had three different names across the
menu bar, the palette, and its own dialog. A working tree is already 工作树
everywhere else, so worktree joins it and workspace keeps 工作区 to itself.
Also:
- Forget password means "clear the stored password", not "I forgot my
password" — 忘记密码 reads as password recovery.
- The SSH auth mode Agent (ssh-agent) was 代理, the same word as the proxy
fields right next to it.
- "The server holding your shells" became 保存 (stores), which is not what
the server does with them.
- Focus follows mouse had subject and object swapped.
- "over a week ago" lost its "ago".
- The shell help said to clear "Program" while the field above it is
labelled 程序; the sidebar-grouping help said "Scratch" while the header
itself reads 草稿.
- "Off closes straight away" was ambiguous about what closes.
- Mark Tab as Unread lost the tab in the palette.
- An SSH profile is a saved host, not a file on disk: 配置文件 → 主机配置.
- Punctuation: 帐户 → 账户, a halfwidth comma in Ln/Col, and em dashes
inside a sentence are now —— throughout instead of a spaced —.
* fix(i18n): close remaining zh terminology gaps
Review follow-up to the terminology pass on this branch.
- Finish the profile -> 主机配置 rename. The About blurb still said 配置文件,
and the Hosts search keywords still only matched the old term, so searching
settings for the words the UI now shows found nothing. Settings search is a
plain substring match over the whole keyword blob, so 主机配置 joins
配置文件 there and both still reach the section.
- Quote UI labels with the “” the file already uses for “显示更多选项”,
rather than the 「」 that had been introduced in two strings.
- 一周多以前 -> 一周多前, matching its four siblings: 刚刚, 分钟前, 小时前,
天前.
- 标记标签页为未读 -> 将标签页标记为未读.
- The remove-worktree dialog said 未提交的更改 while the changes panel, the
diff overlay and the palette all call git changes 变更. Its confirm button
(放弃更改并删除) is rendered from the same prompt, so the two moved together
and the dialog stays internally consistent.
The New Worktree Tab labels are left alone: 新X in the menu bar and 新建X in
the palette is the split the file already makes for New Workspace.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(windows): advertise terminal background to TUI apps
* refactor(windows): keep the background hint out of config.json
The daemon needs to know whether the window is light or dark when it
spawns a Windows pane, because ConPTY drops the child's OSC 11 query
before tty7's emulator can answer it. It was reading that from
`Config::theme` — a field nothing had written since it went dead — which
meant the GUI had to rewrite the user's `config.json` every time the
effective preset changed sides.
Move the hint to `appearance.json`, beside `machine.json` in the data
dir, and leave `Config::theme` exactly as it was. It is derived state:
written by the process that paints the window, read by the process that
has to describe it, and of no interest to the user. A file of its own
rather than a field on `Machine`, because the machine tree is owned by
the daemon and flushed on a timer, so a second writer would clobber the
workspaces and panes it had not seen. Absent, unreadable, and unparsable
all read as light — what the default preset is — so a daemon that starts
before the GUI has ever applied a theme describes the default window
instead of guessing.
Also silence the `unused variable` warning the hint parameter raised on
every non-Windows build, where the `COLORFGBG` block it feeds is
compiled out.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Accept a visible inline history suggestion while preserving end-of-line behavior when no suggestion is shown.
Refs l0ng-ai/tty7#315
Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
* feat(ui): add GUI localization for en and zh-Hans
* feat(ui): localize search placeholders and relative time
* feat(ui): localize palette, switcher, and sftp strings
* feat(ui): localize home shortcut labels
* feat(ui): localize tray, ssh prompt, and editor strings
* feat(ui): add plural/select i18n helpers and localize sftp/settings labels
* feat(ui): localize settings search, forwards panel, and file tree
* feat(ui): localize code editor and right panel
* feat(ui): localize stop/delete workspace confirmations with plural support
* feat(ui): localize diff overlay with plural-aware summary
* feat(ui): localize pending pane, worktree prompt, and home time strings
* feat(ui): localize app menus, tray, tab strip/sidebar, and remote status strings
* feat(ui): localize switcher, file_tree, machine_mirror fallback strings
* feat(ui): localize ssh prompts, theme presets, host error wrapper, and finish remote strings
* feat(ui): localize command palette strings
* feat(ui): localize app.rs notifications, prompts, placeholders, and parse errors
* feat(ui): localize remaining theme, switcher, settings, and sftp strings
* style: cargo fmt
* feat(ui): add language selector to settings
* fix(ui): refresh locales across windows
* refactor(ui): make GUI language selection explicit
* fix(ui): localize Explorer settings after merge
* fix(ui): keep persisted theme names out of the GUI locale
A theme's name is data, not chrome: it is written into the theme YAML and
matched back with `trim_end_matches(" (custom)")`. Translating it meant a
Chinese GUI forked "Nord" into "Nord(自定义)", the next fork stacked a second
suffix on it, and the name stayed Chinese after switching back to English. The
derived-name fallback had the same problem. Both are English again.
Also in this pass:
- Give each test thread its own locale override. The locale is process-wide and
tests run in parallel, so the two tests that switched to zh-CN could flip the
language out from under another thread's English assertions.
- Rebuild the menu bar when gui_language changes in config.json, the way the
in-app picker already does — otherwise the menus kept the old language.
- Document the values the setting actually accepts. The docs still described
`auto` and `zh-Hans`, which sanitize() resets to `en`.
- Put the English words back into the Chinese search keywords for the language
setting; the other 58 keyword sets keep them.
- Drop the unused is_zh_hans helper.
---------
Co-authored-by: thomas <thomas@gmail.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
* add CLI support for opening directories in new tabs
f
* feat(windows): add optional windows explorer context menus
f
* fix(gui): restore missing windows and reject lossy paths
* fix(windows): harden explorer menu registration and native path handling
* fix(cli): preserve native GUI paths on Windows
---------
Co-authored-by: thomas <thomas@gmail.com>
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>