* fix(i18n): give each reused localization key its own sentence
Thirteen keys were shared by call sites that say different things. The
catalog holds one value per key, so the second site rendered the first
site's text: "Move X left" on the right-move button, "Use X repo color"
on the icon picker, "lines above" on the expand-below control, "Open X
issues" on the open-in-Linear action, and similar for status icons,
reclaimable workspaces, remote uploads, automation destination notices,
the add-project button and MiniMax credential errors.
Split every divergent site onto its own key with translations in all
locales, sync the runtime-required catalog, and make
verify-localization-catalog reject a key reused with different fallback
text so the collision cannot come back.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(i18n): pluralize the remote upload toast with i18next count
The toast passed an English "s" as {{value1}}, so ja/ko/zh rendered
"ファイルs", "파일s" and "文件s". The catalog gate requires every locale to
keep the source placeholders, so dropping {{value1}} per locale is not an
option; use count plurals (_one/_other) like nativeDropTempCopyFailed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* docs: add one-line docstrings to the functions this change touches
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(i18n): reject reused fallback keys before --fix writes en.json
A missing key used with two different fallback sentences had its first
sentence written to en.json before the reused-fallback check failed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Matheus Breguêz <matbrgz@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Hosted macos-15 fails afterPack's packaged x64 terminal host boot check
(hourly run 40608643418: timed out waiting for the PTY), so no
latest-mac.yml is published. Windows, template qualification and routing
jobs stay on hosted runners.
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
* ci: narrow native, launcher and packaging cache keys and stop PR-local saves
* ci: hash Rust crate sources, not build outputs, in launcher and hook keys
* ci: fail closed on unlocked native packages and cover transitive key inputs
* ci: resolve the Windows Rust cache scope with a script, not a nested local action
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(i18n): render revised UI copy and fail on fallback drift
en.json kept stale values for 47 translate() keys whose source fallback was
rewritten, so the revised copy never rendered in any language.
- Adopt the newer source copy into en.json and retranslate zh/ko/ja/es/fr/pt.
- Move copy whose meaning changed to new keys (artifacts sign-in, remote host
access link / name, ssh config picker, jump palette automation label, Claude
subagent navigation hint) so stale translations cannot survive.
- Split five keys shared by different sentences at the sites the extractor
flagged (activity group-by, automation destination notices, Linear open
action, workspace options label).
- Fix catalog/source mistakes: onboarding "notify you know" typo, crash report
double dot and missing space, mobile search keyword "app", remote server
restart warning repeating "live tabs", and stale source fallbacks where the
catalog was right (Ghostty import, Skills button, too-many-changes banner,
card style and chat UI descriptions).
- verify-localization-extraction now fails on fallback drift and prints the
en.json and source values with how to fix it.
Closes#25572
* fix(i18n): translate es live tab/pane counts used in server restart warning
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(i18n): select whole count sentences at twelve more call sites
Twelve strings were a noun plus a plural ending passed as an
interpolation value, so every non-English locale got a Latin "s" glued
to its own noun whenever the count was not one. Each call site now
chooses between two whole sentences with a single {{count}}, the same
Singular/Plural pattern #27907 uses for three Settings strings.
ja, ko and zh drop the stray ending. es and fr keep what they rendered
for one and get grammatical plural sentences. The "commits ahead"
tooltip in the compare summary takes the wording the catalogs already
have for the identical sentence, because three translations treated the
ending as an object. Legacy keys stay in the catalogs.
Fixes#27973
* fix(i18n): name commits in Korean in the compare-summary tooltip
The Korean pair for the "commits ahead" tooltip was copied from
compareBaseCommitsAheadOne/Other, which keep the English "commit" and
"commits". Both sentences now say 커밋 with the 개 counter, the form
#27907 uses for its Korean count sentences, so the tooltip no longer
carries an English plural.
The locale test also requires ja, ko and zh to use one sentence for
both counts. It fails on the previous Korean pair.
* fix(i18n): take the Korean count sentences from the audit review
Eleven of the twelve Korean pairs still put the number before the noun
with no counter ("3 라벨"), the order left over from dropping the
English plural ending. They now read noun, number, counter
("라벨 3개", "멤버 3명"), the wording abruption supplied in review of
#27974. The "commits ahead" pair already had that form and is unchanged.
Three sentences are also reworded to follow the English: a failed
discard no longer says the files could not be deleted, the group toast
says how many of the contained projects remained, and "restricted key"
is spelled out as an API key with restricted access.
The 22 values are recorded in locale-ko-key-overrides.json, next to the
other reviewed Korean strings.
* chore(i18n): credit the Korean count sentences to their reviewer
The Korean wording applied in f6bbcaf341 was supplied by abruption as a
patch in review of #27974 and went in byte for byte. That commit could
not carry a co-author trailer, so this empty one does.
Co-authored-by: YEONGUK LEE <contact@abruption.dev>
* fix(i18n): drop the twelve keys the count sentences replaced
No call site reads the twelve noun-plus-ending keys any more, so they
leave the six catalogs, as #28185 did for the keys it replaced. With
them go the three Korean override entries for those keys and the one
value ko-ui-semantic-mistranslations.test.ts pinned for the old
sshImportSynced text; the sentence it guarded is now the
sshImportSyncedSingular/Plural pair, covered by
count-sentence-locales.test.ts. en-runtime-required.json is
regenerated.
* fix(i18n): give remote drop uploads their own count keys; drop low-value tests
- Remote terminal drop uploads used the runtime keys with a different
fallback ('to remote…'); split them into uploadingFileToRemote{Singular,Plural}
with translations in all catalogs.
- Remove mock-translate tests and the catalog-pinning count-sentence test;
verify:localization-* already enforces key and placeholder parity.
- Trim the 'why whole sentences' comments.
- Regenerate en-runtime-required.json.
* fix(i18n): natural zh/ja count strings and es host-import wording
---------
Co-authored-by: YEONGUK LEE <contact@abruption.dev>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(i18n): interpolate the settings-search empty-state query
The empty state rendered three sibling JSX nodes: a translated prefix
`No settings found for "`, the raw query, then a translated closing `"`.
English reads correctly only because English puts the quoted term after
the verb. Korean and Chinese put it before, so translators given the
leading fragment in isolation localized it as a complete clause and the
query was concatenated after the sentence had already ended:
ko: "에 대한 설정을 찾을 수 없습니다.screen reader"
zh: 找不到“的设置screen reader”
No value for the prefix key can fix this. A fixed prefix/suffix split
hard-codes one word order into the code, and the position where a
language needs the interpolated value is exactly what differs between
languages — so any single split is wrong for some locale by
construction. The message has to be one string with a placeholder, which
moves the decision of where the query goes from the JSX into each
locale's catalog entry.
Replace the fragments with one `{{value0}}` message under a new key.
The old keys are retired rather than reused: their translations are
clause fragments and would be wrong under the new shape. Korean and
Chinese get the natural order; es/fr/ja reassemble their own existing
fragments around the placeholder. ko/zh are also pinned in the
cross-locale key overrides next to the existing precedent for this same
fragment-concatenation class, so a catalog regeneration cannot
reintroduce the clause form.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(i18n): drop assertions the expected-message table already makes
Three of the file's assertions restated what the per-locale exact-match
table proves on its own, and one setup step was dead:
- "carries the query placeholder in every locale" — an exact match on the
rendered sentence cannot pass if a catalog value lost `{{value0}}`,
since the query would be missing from the output. Its `undefined` skip
branch was also unreachable: the catalogs it iterates are the same six
the table covers.
- "closes the ko/zh clause after the query" — `toBe` on the full string
already fixes where the clause sits relative to the query.
- `beforeEach(changeLanguage('en'))` — the remaining table test sets its
own language per case, and the catalog test reads the JSON directly and
never consults i18n.
Verified by mutation rather than assumed. Each of these makes the
trimmed file fail, so nothing the deleted assertions guarded is now
unguarded:
ko value loses {{value0}} 1 failed / 6 passed
es value loses {{value0}} 1 failed / 6 passed
ko puts the clause before the query 1 failed / 6 passed
zh ends with the query 1 failed / 6 passed
a retired fragment key returns in ja 1 failed / 6 passed
Clean: 7 passed. With the rest of the change reverted to main and this
file kept: 6 failed / 1 passed. The docblock keeps the reason the shape
changed and loses the retelling of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(i18n): use whole count sentences instead of appending English 's' in JSX
* fix(i18n): drop unrelated reformatting in McpConfigSection
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* 한국어 비활성 상태 라벨을 문맥에 맞게 수정
사용량 통계의 비활성 상태는 비활성으로, 터미널 색상 대비의 Off 옵션은 끔으로 수정했습니다. 활성 상태의 기존 활성화됨 표현은 유지했습니다.
번역 재생성 시에도 두 표현이 유지되도록 한국어 키별 보정 설정에 반영했습니다.
* fix(i18n): pair Korean usage badge enabled label with 비활성
* fix(i18n): translate remaining Korean runtime RPC startup failure strings
Carried over from #14044.
Co-authored-by: JeongSu <zz1996zz@naver.com>
---------
Co-authored-by: Willy <willy@Willy-MacBookPro.local>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: JeongSu <zz1996zz@naver.com>
Correct 22 Korean labels to match the workspace or worktree named by
their English source. Pin the same values in the Korean key overrides
and cover catalog values and repair-policy stability with regression tests.
Fixes#11727
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
resolveCommand only checks what it resolves, so a file that also builds its
own wsl.exe exec no longer passes the import check on a resolveCommand call
alone. Note editor --remote wsl+<distro> launches as out of scope.
* feat(wsl): internal distro allowlist for WSL execs on shared test boxes
ORCA_INTERNAL_WSL_DISTRO_ALLOWLIST (comma-separated distro names) keeps every
wsl.exe call out of distros not on the list: process-host's resolveSpawn,
the main and daemon node-pty spawns, and the two codex-accounts calls that
bypass process-host. Pure list/status/version calls stay allowed; any other
form must name an allowlisted distro, and --shutdown or a default-distro exec
is refused. A malformed value refuses every distro and logs once. Unset, the
check reads the variable and returns, so behavior is identical to today.
* fix(wsl): cover every direct wsl.exe exec with the internal distro allowlist
- Restore #34525's worktree-create payload and captured-host test, reverted by
accident in the first commit.
- Check the allowlist in execFileCapture, the choke point for WSL Git, gh and
worker-thread captures, and before the WSL git-read probe, the Codex hook
path canonicalization and the CLI login spawn. The WSL watcher now spawns
through process-host.
- Stop scanning wsl.exe argv at the first positional argument, which wsl.exe
runs in the default distro.
- check-process-host-imports fails on any direct wsl.exe exec outside
process-host that does not call assertWslDistroAllowed.
* fix(wsl): check the distro allowlist where resolveCommand produces the wsl.exe form
Every Git, gh and glab path into a distro resolves through resolveCommand, so
gitExecFileSync, gitSpawn, git stream reads and wslAwareSpawn are covered too;
the execFileCapture check is now redundant and removed. The import check also
treats buildWslExecArgs as a wsl.exe exec and accepts resolveCommand as covered.
* ci: move secondary, scheduled and contract workflows to GitHub-hosted runners
Public-repo standard hosted runners are free under Enterprise. Release, build,
benchmark, e2e and unit workflows stay on Blacksmith. ci-xterm-cache seeds both
cache stores since its PR consumer's pool is undecided.
* test(pi): resolve the smoke bundle's external packages from the repo
#34148 pulled zod into the commit-message planner graph; the bundle in the
OS temp dir could not resolve it on any runner.
* ci(unit): use four workers on the hosted four-core x64 runner
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Hourly and daily builds publish unvetted dev channels on a schedule, so their
latency is not user-facing. Move the macOS build to hosted macos-15, the
Windows build to hosted windows-2022 (the release path), template
qualification to hosted lanes, and the small routing/publish jobs to
ubuntu-latest. Adhoc and release builds stay on Blacksmith.
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
* test(wsl-host): native chat on a WSL host through the fake transport, including a stopped distro
* test(wsl-host): type the chat spec's focus host id
* test(wsl-host): harden the WSL host chat spec and re-run fake-connector specs on change
* test(wsl-host): scope the chat reply check to the transcript and test the fake's route
Find the prompt only in the transcript so the composer can't stand in for it,
re-check the boot-free stop window after unfocusing, right before the focus,
give the fake's distros.json temp file its own name so it can't collide with the
spec's, and test that the fake connector routes to both WSL host specs.
* ci: warm hosted runner caches and drop the serve-switch installer cache
* ci: seed only restored caches from hosted warm legs
* docs(ci): hosted dev builds and no larger runners
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Keep preflight and the unit-test matrix (the PR critical path) on Blacksmith; move routing, aggregation, contract, packaging and mobile checks to hosted standard runners, which are free for public repos under Enterprise concurrency.
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Removes 134 redundant test registrations across native-chat projection, sidebar ordering, and palette matching. Deletes two entire suites: notice appearance mapping and ordering scenarios whose test-owned Host supplies the behavior being asserted.
Retains 154 registrations covering state transitions, competing inputs, FIFO tool pairing, sidebar feed application, and search ranking. Production code is unchanged. Removes the two deleted suites’ timing entries and lowers the wall-clock baseline only for the deleted Date.now assertion.
Validation: isolated static registration/body preservation, formatting, lint, discovery, runtime routing, and metadata checks. Full PR CI must verify 275 expanded cases across 17 owning test roots, including the transcript-contract aggregator; 185 cases belong to retained selected sources and required counterparts. No local runtime tests were run.
* feat(mobile): light and dark themes with desktop's palettes
Settings → Appearance offers System, Light and Dark. The shell passes the
resolved scheme to over-the-air pages in its init frame.
Fixes#22184
* fix(mobile): draw your chat messages in desktop's user bubble
Desktop uses a faint surface with a hairline border in both themes; the inverted fill went black in light.
* test(mobile): re-measure the page script sweep after the shared theme hooks
Every route now imports the theme hooks while the entry does not, so they emit one shared script (96 for the tree). Re-swept every route prefix; the worst route step is now 15.
* test(mobile): read themed stylesheets in the text-input font-size census
The census only unwrapped StyleSheet.create({...}); inputs styled through themedStyleSheet(() => ({...})) came back unread.
Read git ls-files and git diff --name-only with -z so non-ASCII paths are
not C-quoted and still match ast-grep's real filenames; otherwise new
flaky-test findings in such files were silently dropped.
Fixes#34395
Co-authored-by: Neil <neil@stably.ai>
* refactor(mobile): share the queued-card projection with desktop
Image-only queued drafts showed a blank card on the phone.
* fix(mobile): honor the host's Queue follow-ups setting
With it off, a mid-turn send from the phone still queued while desktop sent it into the running turn.
* fix(mobile): keep a queued-card edit when the card leaves the queue
The phone closed the editor and dropped the typing; it now goes to the chat box as on desktop.
* fix(mobile): hold /clear and /compact for background work and sends
The phone fired the command where desktop holds it, showed start failures twice, and left the agent unnamed in failures.
* fix(mobile): hand a queued edit back to the chat it came from
* fix(mobile): scope a command's send hold to its own chat
* fix(mobile): read a published null queue as empty
* refactor(mobile): share the returned queued-card caption rule
* test(mobile): drop queued-card tests the shared rules already own
* refactor(native-chat): share the needs-attention rule and drop leftover wrappers
* fix(mobile): type the structured sends mock, drop the moved queued-cards path and ship queued edit copy at boot
* fix(native-chat): drop the queued card's unused image-label import after the rebase
* fix(mobile): order the phone's queue reorder preview by position, as the shared cards sort
* test(mobile): keep the queued-messages test within its line budget
* fix(mobile): settle terminal chat working state and turn timing with desktop's rules
Move desktop's live-status merge, Stop suppression, hook-state resolver and
terminal turn timing to src/shared/native-chat. Mobile reads the transcript
lifecycle the host already sends, so a provider turn end settles Working the
same way desktop does. Terminal chats now get the turn bar, timer and folding,
and the streaming preview uses the shared deriveNativeChatStreamingText.
* fix(mobile): send desktop's terminal Stop keys, a second Escape only for OpenCode
Desktop sends one Escape on Stop. It adds a paced second Escape only for
OpenCode, which confirms an interrupt that way. Claude Code and Codex read
Esc-Esc at an idle prompt as rewind. The rule moves to
native-chat-agent-support, and the phone now paces the second Escape at
desktop's answer step instead of 80ms.
* fix(mobile): word Orca-stop and usage-limit notices like desktop
Move desktop's Orca-stop sentences to src/shared with an English table that
desktop translates. The phone names the Orca host it pairs with, and when
that host is unnamed it says "the remote server". Usage-limit rows rebuild
desktop's sentence from the failure fact. The usageLimitLineText fallback,
which only the phone used, is deleted.
* test(mobile): pass the overlay's host name in the prompt controller harness
* test(mobile): re-record Stop goldens at desktop's step, split status-notice tests, pin the turn clock and ship notice copy at boot
* fix(mobile): offer structured Stop while a send is out and read queue-sends-next as working
Move desktop's structured Stop rule, its Stop-joining helpers and the
queue-sends-next rule to src/shared. Both sides now use them. The phone
counts its in-flight sends, so a conversation-stop host offers Stop before
the turn opens. Between queued turns the phone reads as working, and
commands that can't wait in line hold like they do on desktop. Untargeted
Stops are no longer joined on legacy hosts.
* fix(mobile): offer terminal Stop again when its Escape went nowhere
Stop hides Working on the phone like it does on desktop. The phone's Escape
can fail outright, though, while desktop writes to a local PTY. When no Escape
could have landed, the phone now drops that suppression. The streaming preview
also treats a pending echo as the transcript's tail, as desktop does, so an
earlier reply can't hide a new preview.
* fix(mobile): hold structured Stop while a send it covers is in flight
On desktop, Stop's held and Stopping state depends on whether the chat can
stop, and that includes a send still in flight. The phone now does the same.
Once Stop is pressed before the turn opens, the button holds and repeated
presses send no more untargeted cancels.
* fix(mobile): end the terminal Stop hold when a new message is sent
Desktop clears Stop's working suppression on an optimistic send, so a new turn
that keeps the same hook epoch (Claude with working subagents) still offers
Stop. The phone now does the same when a new pending echo appears.
* refactor(mobile): trim chat turn wiring after the desktop rule move
Drop the controller's unread nativeChatAgentWorking field. Re-render only when
a chat starts or stops having a send out. Keep the structured turn indicator
stable across stream frames. Type the Stop test table instead of casting.
* refactor(mobile): track structured sends in flight once for Stop and command holds
* test(mobile): re-record Stop goldens now that Stop settles with its Escape delivery
* fix(mobile): escape terminal image paths like desktop drops
* fix(mobile): keep launch-draft prefill over older user turns
* fix(mobile): rank @ file matches with desktop's fuzzy Quick Open search
* fix(mobile): use desktop's composer triggers for mid-prompt / and $ picks
Picked @ files are now quoted when the path has spaces.
* fix(mobile): send picked skill paths on structured chats
* fix(mobile): list the host's disk-scanned skills in the / and $ menus
The phone names the workspace and the host resolves its folder, refusing SSH
workspaces as desktop does; hosts without skills.discover-workspace.v1 list none.
* fix(mobile): forget a picked skill once its token is deleted
* fix(mobile): drop a picked skill as soon as its token is deleted, and retry failed skill scans
* fix(mobile): treat a picked leading skill as chat and stop failed skill scans from refiring
* test(mobile): drop the menu ranking case the shared picker tests own
* refactor(mobile): keep skill picks on the draft record and reuse shared send rules
A rejected send now keeps its picks and leading-skill origin on both send paths.
* fix(mobile): replace a re-picked skill's file and restore picks with a rejected send
* fix(mobile): read the chat agent for the skill catalog from the active resolution
* style(native-chat): format the composer autocomplete files
* fix(native-chat): import the picker's apply and send-classify rules from the shared composer module
* refactor(mobile): import the composer's draft-store hooks once
* style(mobile): format the notice row's style import