* 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>
Three defects in the inline completion menu, all of which produced something
wrong rather than merely unhelpful.
A candidate was inserted into the command line verbatim. A directory named
`My Documents` completed to `cd My Documents/`, which the shell resplits into
two arguments and the command breaks. `shell_escape_path` already existed for
drag-and-drop paths; completion never reached for it. `escape_candidate` wraps
it and keeps a leading `~/` unescaped, since that prefix is the user's own text
and escaping it would stop the home expansion it was typed for.
The same escape decides whether a common-prefix step is safe to write. The
prefix shared by `My Documents` and `My Music` is `My ` — writing it raw both
breaks the line and puts a space inside the open word, which closes the menu on
the next keystroke and leaves the user worse off than before the Tab. A prefix
that needs escaping now steps through the candidates instead.
A menu fed only by generators stayed armed forever when nothing matched.
`git ckout<Tab>` matches no subcommand, but git's alias generator is in flight,
so the session opens empty and waits — and the callback that would have closed
it returned early on an empty result, so the menu never learned the generator
was done. An armed empty menu swallows every later Tab instead of handing the
line to the shell. Sessions now count their generators, and the last one to
answer closes a menu that still has nothing in it.
Command completion scanned this machine's PATH in a remote pane. The remote
isolation added in 08ca3a3 covered paths and generators but deliberately left
command completion running, which was right for the builtins half and wrong for
the PATH half: `system_prof<Tab>` over SSH to Linux offered macOS's
`system_profiler`. Worse, it failed inconsistently — with no local match the
position falls through to the remote's own compsys and answers correctly, so
the bug only appeared when this machine happened to have a match. Builtins are
true on any POSIX shell and still go out; the PATH scan is now local-only.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
The rcfile tty7 hands to bash replays the login-shell startup chain, but
sourced ~/.bashrc unconditionally after it. A login shell never does that
on its own — ~/.bashrc arrives only because the profile that won the chain
forwarded to it, which is how nearly every ~/.bash_profile is written. The
result was the user's whole ~/.bashrc running twice per pane: banners
printed twice, completions were sourced twice, and appends to
PROMPT_COMMAND stacked up.
Move ~/.bashrc into the same first-match-wins chain. That fixes the double
source and still keeps the fallback for a $HOME with no profile at all.
The existing test only asserted the rcfile mentions ~/.bashrc, which the
buggy version satisfied too. Add one that runs real bash against a
throwaway $HOME whose .bash_profile forwards, and counts the sourcings.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
$SHELL is a snapshot the session inherits at login, so chsh never moves
it -- a GUI launch keeps reporting the shell that was current when the
user logged in, and goes on doing so until they log out. The window's
shell menu marked the wrong entry "default" for that whole stretch.
Read the passwd entry instead, via getpwuid_r -- the reentrant form,
since getpwuid returns a pointer into a shared static another thread's
lookup can overwrite. $SHELL stays as the fallback for the rare case
where the lookup fails. The three callers that each reached for the
variable on their own -- the default-name lookup, the PATH enrichment
that runs the login shell at startup, and the shell-integration kind
probe -- now share the one function.
Same commit fixes who wins a name in the menu. Candidates were login
shell, then /etc/shells, then $PATH, and dedupe keeps the first -- so
on a machine with a Homebrew bash, /etc/shells listing /bin/bash first
handed the entry to macOS's 3.2 from 2007, old enough that
bash-completion 2.x will not load against it. Probe $PATH before
/etc/shells and widen the probe list to the POSIX shells, so the menu's
"bash" is the binary typing bash would reach; /etc/shells still catches
anything installed off $PATH.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
When a pane inherits no locale at all — the usual case for a GUI-launched
process on macOS — we derive an installed UTF-8 locale and inject it. But
we injected it as LC_CTYPE, which backs only character handling. Collation,
time and numbers stayed at C, and a shell that asks setlocale(cat, "") per
category finds no variable for the rest: bash warns
setlocale: LC_COLLATE: cannot change locale ()
once per category on every launch. zsh and fish swallow the failure, so
they merely look fine while being just as half-configured.
LANG backs every category and still loses to any LC_* the user's own rc
files set afterwards, which is what a fallback should do. LC_ALL would also
cover everything but would override those.
Co-authored-by: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com>
Some Windows shell brokers enforce `ProcessRedirectionTrustPolicy` on what
they launch. The daemon inherited it, every ConPTY shell under the daemon
inherited it in turn, and PowerShell could then no longer traverse a
user-created junction — which is exactly what Scoop's `current` links are.
`oh-my-posh` and `fzf` died with `Shim: Could not determine if target is a
GUI app`. Windows Terminal was unaffected because its process tree never
picked the policy up.
The policy cannot be relaxed once enabled, so the fix is to not inherit it:
when tty7 detects the enforcing bit, it creates the daemon with
`STARTUPINFOEXW` and `PROC_THREAD_ATTRIBUTE_PARENT_PROCESS` naming the
interactive desktop shell, which supplies the ordinary desktop token, device
map, and mitigation policy. The Win32 code stays isolated in
`daemon/spawn/windows.rs`, and the ordinary path still runs whenever the
policy is absent — or whenever the desktop shell cannot be borrowed, in
which case tty7 logs a warning and starts degraded rather than not at all.
Because naming a logical parent makes handle inheritance follow that
process, the daemon starts with no standard handles. `daemon::server` and
the pane reader's trace line now write to stderr in a way that tolerates
that, instead of `eprintln!`, which panics on a failed write.
ConPTY exit ordering: the process-exit monitor could observe a short-lived
shell exiting before the reader had delivered its final frame, so `Exited`
reached clients ahead of the output that preceded it. The monitor now
releases the pseudoconsole and lets the reader — which reports only after
forwarding everything up to EOF — announce the death, with a bounded window
behind it for the case where EOF never arrives because a grandchild holds
the ConPTY output pipe open.
Note this changes the daemon's token on the clean-parent path: it derives
from Explorer, so an elevated tty7 starts a medium-integrity daemon.
Co-authored-by: ARNO <ArnoChenFx@users.noreply.github.com>
capture_plain_returns_text_not_escapes gated its byte-level asserts on
the marker reaching the rendered capture, then asserted the raw capture
already carried a CR. The two captures are separate snapshots taken in
sequence, and on Windows ConPTY re-emits the echoed command in
escape-laden bursts: the marker can render (from the typed input line)
while the slightly earlier raw snapshot has yet to see a single CR —
Enter's CRLF only arrives with the command's execution. CI hit exactly
that window on x86_64-pc-windows-msvc.
Make the CR part of the settle condition the loop polls for, alongside
the marker, and name both in the timeout message so a genuine
CR-stripping regression still reads as one.
Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
Opens an Unreleased section for what landed after 26.8.1. The PR body
these came in under described a session CLI that had already shipped in
26.8.1 (#274) — `spawn`, `kill`, wire changes to `PaneInfo`, new control
kinds — none of which is what actually merged, so the changelog is
written from the diff instead.
`wait` is documented in its final shape, `--changed` included: the status
is a level rather than an event, so a wait issued right after a `send`
would otherwise answer with the previous turn's state. That is the part
a reader has to know to use the verb correctly, not a footnote.
The taught loop started its worker with `claude -p`, but headless print
mode never stops to ask, so the `waiting` state steps 3-4 are built on
could not arrive. Step 2 now launches interactively.
Every wait after a send passes `--changed`, with the reason spelled out:
without it the loop re-reads the state it just walked in on. The exit-1
"worker died" branch is documented too, and both guards are asserted in
the skill-content test.
Also gives the install round-trip test a Drop guard, so a panic cannot
leave `CLAUDE_CONFIG_DIR` set for whatever runs next in the process.
`installed()` was called from inside `render_settings_agents`, so the
Agents page did a `read_to_string` every frame it was on screen. The hook
rows it was modelled on cache into `SettingsState` precisely to avoid
that; the skill's presence now does the same, read when the page opens
and after a change.
The switch also swallowed its outcome into `log::warn!`. The one error a
user actually hits — uninstall refusing a `tty7-orchestration` file tty7
did not write — left the switch springing back with no explanation. The
result now lands in `orchestration_skill_note` beside the switch, the way
`agent_hooks_note` does, and the install/uninstall itself runs off the UI
thread.
Three holes in the wait primitive, all of which make a delegation loop
answer with something other than what it asked for.
The agent status the server keeps is a level, not an event: `done` stands
until the next turn begins, `waiting` until the agent moves again. A wait
issued right after a `send` therefore returned last turn's state before
the worker had even read the input, and a second task in the same pane
matched `--until done` instantly. `--changed` snapshots the position the
wait arrived at — status plus the activity counter, which ticks even when
the status letter does not — and refuses to match it; the JSON carries
`stale` so a plain wait can tell whether the answer might be a leftover.
`exit` was unreachable for any pane that had ever had an agent: the
snapshot has no liveness in it, and the daemon keeps a dead pane
registered until it is closed. A worker that crashed mid-turn reported
`working` until the timeout. Liveness is now re-checked from the tree
every few polls; the fast path where the first poll already answers still
costs exactly one request.
The "pane exited before reaching the awaited state" branch built its JSON
and then threw it away on an anyhow error, leaving `--json` with nothing
to read and the exit indistinguishable from an unreachable daemon. It now
exits 1 with its report.
Also: the wire spelling of a state is written out instead of derived from
the variant name, the sleep no longer overshoots a near deadline, an
absurd `--timeout` cannot overflow, and `--interval` is range-checked at
parse time rather than silently clamped.
* feat(cli): `tty7 wait` + the agent-coordination note
The two pieces of the original session-CLI PR that main's own CLI
doesn't cover, rebuilt as a minimal delta against it.
`tty7 wait %N --until waiting,done --timeout 600` blocks until a pane's
agent reaches a requested state — the orchestration primitive that lets
one agent sleep until its peer blocks on a permission prompt or
finishes a turn, instead of screen-scraping. A poll of `AgentStates`
rather than an `events` subscription on purpose: a one-shot stateless
question composes into scripts, survives a server restart mid-wait, and
needs no cursor management. Agentless-but-live panes read as idle via
the machine tree; a dead or vanished pane reads as exit, which ends
every wait (matched only when asked for). Timeout exits 124, the
`timeout(1)` convention.
The coordination note is discovery for the whole CLI: a marked,
idempotent block describing the verbs, installed into
~/.claude/CLAUDE.md (always; CLAUDE_CONFIG_DIR honored) and
~/.codex/AGENTS.md (only when ~/.codex exists). A one-time "Let your
agents coordinate?" prompt fires the first time a pane detects a coding
agent; a Settings → Agents switch drives the same install/remove, with
state read from the files themselves. Uninstall strips exactly the
marked block; an unterminated block is left alone rather than truncated
at a guess.
* feat(agents): replace the global note with an orchestration skill
Per review: global instructions tax every session's context and hand
every agent — workers included — the ambient authority to orchestrate
its neighbours. The common shape is primary → workers: one agent owns
decomposition, dispatch, waiting and aggregation; workers just do
bounded tasks.
A Claude Code skill fits that exactly. `core::orchestration_skill`
installs ~/.claude/skills/tty7-orchestration/SKILL.md — only its
one-line description rides in context until the user or the primary
agent explicitly invokes it, and workers never see it. The body can
therefore afford the full delegation loop (tab new → send → wait →
answer-or-capture → pane close) instead of a token-starved cheat
sheet.
The file is wholly tty7-owned: install is a plain overwrite (also the
version-refresh path), and uninstall keys on an ownership marker so a
user's hand-written skill under the same name is refused, not deleted.
Gone with the global note: the first-agent-detected prompt, its config
flag, and the CLAUDE.md/AGENTS.md writers — the Settings → Agents
switch now drives the skill install instead.
---------
Co-authored-by: l0ng-ai <ysdpk123@gmail.com>
Replace the unbounded decode channel with a bounded, latest-frame inbox so full-window browser frames cannot queue faster than they decode. Keep deletes ordered with in-flight work and discard superseded frames before decoding them.
Only retire images that reached the sprite atlas, and evict remaining atlas entries when a pane closes. This keeps hidden terminal-browser tabs and repeated pane lifecycles from retaining one decoded frame per repaint.
26.8.1 panics on launch under Wayland on a VMware Ubuntu guest:
gpui_linux/src/linux/wayland/client.rs:924: RefCell already borrowed
The xdg-desktop-portal event source notified windows of the initial
color-scheme and button-layout replies while still holding
`client.borrow_mut()`, and those callbacks re-enter GPUI, which reaches
the same `RefCell` through `with_common`. Whether it fires depends on
whether the portal reply beats window creation, so a slow VM loses that
race every time.
Fixed in the fork (l0ng-ai/zed@3a4acfd) for both the Wayland and X11
clients by collecting the window pointers and dropping the borrow before
notifying. Windows and macOS never compile that crate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Cursor-hiding TUIs (Kimi CLI, Ink apps) draw their caret as a reverse-video
cell and leave the real cursor wherever the frame's last write ended — for
Kimi that is the input box's right border, and the IME candidate list was
stranded there. When the cursor is hidden and its row holds exactly one
caret-sized inverse run, snap the IME anchor (and the marked-text preview)
to that run; rendering is untouched.
The gpui side (bumped here) now also answers IMR_QUERYCHARPOSITION — the
query the Windows 11 Microsoft Pinyin IME uses instead of CANDIDATEFORM —
and re-anchors the candidate window on every WM_IME_COMPOSITION.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pane_title_of() picked any non-empty PaneRecord.title, but an idle
terminal's foreground process is just the shell itself (zsh, bash, ...).
That made almost every idle-shell workspace show up as "zsh" in the
sidebar instead of the far more useful cwd/repo-derived name, which
defeats the "cwd as final fallback" intent of this change.
Skip bare shell process names when picking a pane title, so the
process-name fallback only kicks in for genuinely distinctive
foreground processes (nvim, an agent, ...).
Restarting the local server clears the window's tabs and then pulls the
layout back from the machine tree. The pull went out on the control link
we held, which pointed at the server we had just killed: `is_connected`
only flips once the reader sees EOF, so for a moment the dead link still
classifies as Ready. The call on it failed, `finish_hydration` logged a
warning and gave up, and the window stayed empty on the home page.
Drop the local link before resyncing so the pull waits for the new
server instead of racing a dead socket, and record a failed hydration as
a debt the next sync retries. The debt also stops the empty window from
diffing into "close every tab" and wiping the layout off the machine
once the link is back; a `Replace` retry is abandoned if the user has
filled the window in the meantime.