mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 16:03:20 +00:00
421c8a43ca6a9de93d2875d2699fd820eb555ef8
7482
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
421c8a43ca |
fix(mobile): stop serving a pre-write host-list snapshot to loads issued after the write
removeHost/persistHost await hostListMutation, but the in-flight loadHosts() de-dupe handed back a pass that started BEFORE the write committed, so a load issued after removal repainted the removed host card (#8791). Every durable write now drops the shared pass via host-list-load-sharing.ts so the next caller reads fresh; concurrent loads with no write between them still share one Keychain pass. Also extracts the host action sheet into host-list-action-sheet-actions.ts to pin closeBeforePress on Edit host + Remove (the freeze half of #8791, already fixed by #8536). |
||
|
|
55947a3557 |
fix(mobile): exclude proxy fake-ip addresses from pairing QR (#10498)
* fix(mobile): exclude proxy fake-ip addresses from pairing QR Clash/mihomo TUN interfaces in 198.18.0.0/15 were enumerated as pairing candidates and could become the default QR endpoint. Phones then retry an unroutable address forever. Drop those addresses from the pickable list (#10404). * refactor(mobile): keep fake-ip filtering local * test(mobile): cover fake-ip range boundaries --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
6d4e335001 |
feat(worktrees): support project-level worktree.sharedDirectories in orca.yaml (#10459)
* feat(worktrees): support project-level worktree.sharedDirectories in orca.yaml Follow-up to #7549: `.worktreeinclude` copies gitignored paths into each new worktree, which is right for `.env`/`.vscode/` but wrong for large rebuildable directories. Copying `node_modules` per worktree is slow and duplicates disk, and each worktree's install then diverges. Adds `worktree.sharedDirectories` to `orca.yaml` — a versioned, in-repo list of gitignored directories that are symlinked (shared) into every new local worktree, so one install serves them all. Adds to, never replaces, the per-user Worktree Shared Paths setting. `createWorktreeSharedPaths` uses a new 'share' materialization mode that always symlinks. The existing 'link' mode APFS clone-copies on macOS, which would give each worktree an independent node_modules and defeat the point; 'link' and 'copy' behavior are unchanged. Entries must exist as gitignored directories in the primary checkout; absolute paths, `..` traversal, and `.git` are rejected. Resolution never throws, so a malformed orca.yaml cannot block worktree creation. Remote (SSH) creation skips this, as it does symlink paths and `.worktreeinclude`. Closes #10451 * fix(worktrees): keep worktrees deletable after sharing a directory A directory-only ignore rule (`node_modules/`, the common spelling) matches the primary checkout's real directory, so the shared directory resolves and gets symlinked — but it never matches the worktree's symlink, so Git reports that link as untracked. Deletion only tolerated the per-user shared paths, so every worktree in such a repo became permanently dirty: the clean preflight threw "uncommitted or untracked changes" and `git worktree remove` refused without --force. Feed the configured `orca.yaml` shared directories into the same tolerate-and-unlink machinery the per-user shared paths already use, at both deletion call sites. The names are read unfiltered, since the create-time resolver drops exactly the entry deletion needs most. * test(worktrees): register createWorktreeSharedPaths in the runtime symlink mock orca-runtime.ts imports createWorktreeSharedPaths, but the vi.mock factory for ../ipc/worktree-symlinks never listed it. Vitest resolves omitted exports lazily, so this only stays green because no runtime test configures a repo with worktree.sharedDirectories — the first one that does would fail on a mock resolution error rather than on its own assertion. * fix(source-control): don't count shared symlinks as uncommitted changes A directory-only ignore rule (`node_modules/`) matches the primary checkout's real directory but never the worktree's symlink, so Git reports the shared link as untracked for the life of the worktree. That made every affected worktree read as dirty: a phantom row in the diff view, and Create PR blocked with `blockedReason: 'dirty'` telling the user to commit an entry they cannot commit, because it is a symlink Orca created. Status and the review-creation preflight now drop untracked entries that are both declared shared (per-user shared paths or orca.yaml sharedDirectories) and actually symlinks on disk. Both conditions are required, so a regular file at a declared name, or a symlink nobody declared, still counts as user work. The decision fails closed: anything not positively identified stays dirty. The preflight moves to `--porcelain -z` so paths with spaces or non-ASCII bytes are compared raw rather than C-quoted, with a parser that consumes the origin field a rename emits instead of reading it as its own record. Symlink detection moves to a leaf module: importing it from ipc/worktree-symlinks would pull APFS cloning, and its child_process dependency, into the status graph. SSH is unaffected and left alone — remote worktree creation skips the symlink and shared-directory passes, so a remote worktree never has one. * fix(source-control): wire shared links into local status * fix(worktrees): resolve the status repo once and reject uncollapsed shared paths `git:status` resolved the registered worktree's repo twice per call — once inside `getLocalGitOptionsForRegisteredWorktree` and again for the shared-link lookup — walking every repo's worktree meta on a polling path. `apps/./web` also survived `sharedDirectories` normalization: `resolve()` collapses it when the symlink is created but Git reports the collapsed path, so every later comparison misses and the link reads as permanent untracked work. Also stop resolving shared links for SSH repos in review creation: `repo.path` names a path on the remote host. Adds the missing wiring coverage for review creation and runtime status, plus the untracked-only conjunct in both filters — all four were mutation-verified to leave the suite green before these tests. * test(worktrees): pin the resolver-to-status seam for shared directories The resolver's output and the status filter were only tested apart — status used a hardcoded `['node_modules']`. Feed the resolved directories back through `getWorktreeSharedLinkPaths` into a real `getStatus` so a resolver that ever returned a differently-spelled path can no longer leave the link showing as a phantom untracked row. * fix(worktrees): try a directory junction before a symlink on Windows A plain `fs.symlink` needs Developer Mode or admin on Windows, so an ordinary Windows user got EPERM, the per-path catch logged and continued, and the worktree came up with no shared directory and no signal. A directory junction needs no privilege, and the rest of the codebase already uses one for win32 directory links. The symlink stays as a fallback rather than being replaced: a junction cannot target a UNC path, and a WSL project's repo lives behind one, so replacing it outright would trade the local-volume bug for a WSL regression. Safe for the removal path either way — Windows reports a junction as both a symlink and a directory, so the `isSymbolicLink()` unlink that runs before `git worktree remove` still fires and still refuses to follow it. * fix(worktrees): keep NUL bytes and tolerated links out of the removal error The removal preflight switches to `git status --porcelain -z` whenever it has shared links to tolerate, then attached that raw stdout to the error. `.trim()` does not strip interior NULs, so the message reached the user as `?? node_modules<NUL>?? precious.txt<NUL>` — raw control bytes, and it named the shared link, the one entry that is not the user's work and cannot be committed away. Parse the NUL-delimited output once and use it for both the clean verdict and the error text, so the two can never disagree about what blocks removal. The `-z` switch stays: it is what keeps paths with spaces or non-ASCII names comparable against the configured entry. * chore(worktrees): drop stray reformatting and note why the SSH guard exists Committing the merge staged 792 files, so lint-staged ran the formatter across all of them and rewrapped three renderer files that were already unformatted on main. Nothing was lost — they were byte-identical to main ignoring whitespace — but they showed up in the pull request as unrelated changed files. Restored to main's exact bytes. Committed with --no-verify on purpose: the pre-commit formatter is what introduced the rewrapping, so letting it run again would simply reapply it. Every check it would have run was run by hand instead — lint, typecheck, and the IPC and source-control suites all pass, and the three restored files are expected to fail a format check because that is main's current state. Also records why the connection guard on the shared-link lookup is not dead code: the remote dirty check ignores those paths, so the guard's only effect is avoiding a stray local read and the bad cache entry it would leave behind. * refactor(source-control): drop a scan-everything guard and freeze the cached list The dirty check built a filtered array only to read its length, so it always scanned every status record; asking whether any record is untracked stops at the first one and reads the same either way. The cached shared-directory list was also handhanded out by reference, so a caller that mutated it would corrupt every read for the rest of the cache window. Marking the return readonly prevents that at compile time; copying on return would work too but would allocate on the status-polling path, and there is exactly one caller, which only spreads it. |
||
|
|
70c81c4b32 |
fix: pr-bug-scan validated finding from #6471 (#6512)
* fix: address pr-bug-scan validated finding from #6471 stripTags now removes real markup (closing/self-closing/attributed/custom tags) outside the allow-list; only bare identifier-glued generics like Array<string> are kept. Blocks svg/center/custom leak a * fix: address pr-bug-scan validated finding from #6471 stripTags now removes real markup (closing/self-closing/attributed/custom tags) outside the allow-list; only bare identifier-glued generics like Array<string> are kept. Blocks svg/center/custom leak a * fix(mobile): harden markdown preview tag stripping * fix(mobile): preserve angle-bracket prose while stripping tags --------- Co-authored-by: orca-bug-scan-bot <orca-bug-scan-bot@stably.ai> Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
b41e813cb5 |
fix(native-chat): surface draft launch context in desktop and mobile chat composers (#9802)
* fix(native-chat): surface draft launch context in chat composers
Creating a workspace from a GitHub issue delivers the issue link only into
the agent TUI's input buffer (argv prefill or startup paste), so the chat
view showed no trace of it on desktop or mobile.
Desktop: draft launches now seed an in-memory launch draft keyed by tab id
(direct work-item launches, background GitHub work-item creates, quick-create
composer, and new-tab draft deliveries). The chat composer adopts the seed
once as its editable draft, declines permanently if the composer already has
text, and drops an untouched copy when any user turn lands (the one-line TUI
input means the prefill was submitted or deliberately cleared) or on its own
send, whose existing input pre-clear retires the TUI copy.
Mobile: the host publishes the draft as an optional launchDraft field on the
mobile terminal tab snapshot (additive, no protocol bump) and the mobile
composer adopts it with the same once-only/decline/resolve semantics. Mobile
chat sends now also pre-clear the TUI input line (Ctrl+U, desktop parity) so
a pending prefill cannot concatenate with the sent message.
Completion seeding resolves the launch tab from the synced store tabs when
the backend spawned the terminal and activation reports no primaryTabId.
Split the Windows shell-quoting tests into their own file to stay within the
max-lines budget.
* revert(mobile): drop incidental pnpm-lock churn from the launch-draft branch
The libc binding fields and the @typescript-eslint peer re-resolution came from
a local install, not from this change; mobile/package.json is untouched.
* fix(native-chat): resolve launch drafts without trusting cross-host clocks
The rule required a user turn stamped at or after the seed. Grok omits row
timestamps, so a Grok launch draft never resolved; and the seed time is a
renderer clock while the stamp comes from the executing host's JSONL, so a
remote workspace whose clock trailed never resolved either. Both left the
composer adopting an already-submitted prefill, which re-sends it as a
duplicate turn.
Resolve on any user turn that is not PROVABLY older than the seed (a launch
draft's session starts with zero user turns), with the existing cross-host
skew slack, plus a timestamp-free backstop for wider skew: a new tail user
turn since the draft was first observed. "Load earlier" prepends, so it
cannot move the tail and cannot over-resolve.
Split out of native-chat-pending.ts to stay under the max-lines ratchet.
* fix(worktrees): seed the launch draft on the agent's own tab, never on tabs[0]
Two defects in the completion seed:
- The tab was resolved by array position. buildStartupOpt returns undefined on
the backend-spawn path, so applyDefaultTerminalTabs stamps launchAgent on no
tab and the launchAgent guard was dead there. A repo with default terminal
tabs ("dev server", "logs", ...) got the draft on a tab that runs no agent,
and then published it to mobile as THAT tab's launchDraft. Correlate on the
backend startup tab, then on a launchAgent-stamped tab, then on primaryTabId
(which is the agent tab whenever the renderer owns startup); never tabs[0].
- Runtime-owned worktrees mirror their session tabs async, so tabsByWorktree
was empty at seed time and the seed was silently dropped for that whole host
class. Defer to the first mirrored tab via the existing delayed-delivery
queue, which now holds every pending delivery for a worktree instead of one
(setup/issue commands and the seed both wait on the same first tab).
* fix(store): evict nativeChatLaunchDraftByTabId on every teardown path
The new map was absent from all four paths its sibling
nativeChatLaunchPromptByTabId participates in: tab close, the orphan terminal
sweep, the bulk worktree purge, and the removeWorktree teardown. A stranded
entry is worse than a plain leak here because sync-runtime-graph keeps
publishing it to mobile as that tab's launchDraft.
* fix(native-chat): only seed single-line unsubmitted launch drafts
The unsubmitted-delivery branch seeded on every draft delivery, which also
caught the agent-session-fork path whose prompt is multi-line scraped context.
The chat send pre-clears the TUI with Ctrl+U (kill-to-start-of-LINE), so a
multi-line prefill cannot be fully cleared and its earlier lines would glue
onto the next message. The GitHub work-item draft this feature targets is a
bare issue URL, so narrowing costs it nothing.
Also assert the composer retires the seed after a send — deleting that call
previously failed no test.
* fix(mobile): stop the chat pre-clear from wiping a just-pasted image
The text write set clearInputFirst unconditionally. On the image path that
Ctrl+U lands AFTER pasteMobileNativeChatImagePaths already pasted the image,
so the agent receives the text alone while acceptSend still renders the
thumbnail on the sent bubble — silent image loss.
Desktop's image path clears exactly once, before the paste, and never again;
mobile now matches: pre-clear only when nothing was deliberately pasted first.
The image paste already leads with its own Ctrl+U, so a launch-draft prefill
parked on the input line still cannot glue onto the message.
Pinned at both levels: the controller test drives the real send hook and
asserts clearInputFirst per branch, and the send module asserts the wire text
carries no leading \x15. The image-attachments test injects its own baseSend,
so it structurally could not observe this.
* fix(mobile): hold the launch-draft prefill until the transcript settles
session.tabs delivers launchDraft before the transcript read resolves, so the
seed effect could run against an empty in-flight message list and miss the
user-turn decline. Launching from an issue, submitting the prefill in the TUI,
and never opening desktop chat (nothing else clears the host seed) then
prefilled the mobile composer with the already-sent issue link — a send tapped
before it retracted duplicated it to the agent.
Thread the session's loading state through and skip the seed while the read is
in flight. idle/waiting-session still seed: no session means no user turns.
* fix(runtime): publish a launch draft to mobile only for the tab's own agent
The publish had no agent check while the desktop consumer declines on
mismatch. The seed is keyed by tab id, which survives a pane's agent switch, so
mobile could adopt a draft desktop refuses — seed for claude, never open
desktop chat, switch the pane to Codex, and mobile prefills the Codex chat with
the Claude-era issue link. Align publish with the consumer.
* fix(native-chat): take the launch-draft baseline only after the transcript loads
The timestamp-free backstop snapshotted the transcript's user turns on first
observation of the draft, which can happen while the read is still in flight and
`messages` is []. A pane bound to a session that already had user turns then
backfilled above that zero baseline with a different tail id, so clause 2
resolved and silently dropped the seed — the launch context never appeared, and
the feature no-oped for exactly the panes it was meant to serve. Clause 1 was
already correct there (that history is provably older than the seed).
Gate baseline capture and resolution on the transcript read settling, the same
shape mobile's drafts hook uses. Clause 1 is unchanged; while loading the merged
list is empty anyway, and a pane with live appends is never reported 'loading'.
Also restore clause 1's short-circuit: it scans with .some() again and only
allocates the user-turn list when falling through to the backstop.
NativeChatView sat at exactly the 400-line cap, so the composer's two
launch-draft props are now spread from the hook result they already mirror.
* fix(native-chat): reject multi-line launch drafts inside the seed helper
The single-line guard lived in deliverLaunchPromptToAgentTab, so the two
other seeding entry points (worktree create, direct work-item launch)
bypassed it — and every Linear launch is multi-line by construction
("Linked Linear issue: STA-…" + url). The chat send pre-clears the TUI
with Ctrl+U, which kills to start of LINE, so those earlier lines stay
parked to glue onto the next message.
* fix(worktrees): keep the deferred agent seed off ambiguous mirrored tabs
The runtime-owned deferred path fell back to tabs[0], which the module's
own docstring forbids: with repo default tabs ("dev server", "logs") the
seed lands on a tab running no agent, where mobile withholds it and
desktop's agent check ignores it — the feature is silently dead for that
create and the entry leaks until tab close.
The queue entry is consumed before delivery, so there is no retry to fall
back on; accept the first mirrored tab only when it is the worktree's
only one and so unambiguously the agent's.
* fix(mobile): treat a launch-draft-only session-tab frame as a change
mobileSessionTabEqual's terminal branch never compared launchDraft, and
the route keeps `prev` when tabs compare equal — so a publish whose only
delta is the draft appearing or retracting was discarded and never
reached the composer. Live QA passed only because agentStatus happened to
change in the same frame.
MobileSessionTab's terminal variant did not declare the field either
(the controller read it through the structurally wider
MobileNativeChatTab), which is why TypeScript never flagged it.
* fix(mobile): judge a launch prefill only from its own settled transcript
Two ways the drafts hook was reading a transcript that was not the active
chat's:
- transcriptLoading came from `status`, a plain useState written by a
passive effect declared before the drafts hook. On the commit where the
tab identity changes it still holds the previous tab's value, so the
guard was off on exactly the render that seeds: first entry saw
status 'idle' with an empty list and seeded an already-submitted link,
and a tab switch declined the new tab's prefill from the old tab's
turns. The session hook now tracks the identity its messages describe
and reports transcriptLoading until they agree; the retire effect gates
on it too.
- Leaving chat view nulled launchDraft while draftKey stayed the same,
which the hook could not tell from a host retraction — it declined the
prefill permanently, so peeking at the terminal dropped the context.
The controller now passes the raw field plus an explicit chatActive
flag, and both effects hold their state when the tab is not on chat.
The controller wiring was previously unasserted: replacing both props
with constants left all 795 mobile session tests green.
* fix(native-chat): keep the launch-draft baseline across a transcript reload
baselineKey went null whenever the transcript was loading, and the null
branch DISCARDED an already-valid baseline taken from a settled read. It
was then re-taken from the fuller list, swallowing the very user turn
that resolves the draft — so a stale prefill gets re-adopted as a
duplicate turn. Key the baseline on draft identity alone and gate only
the capture.
session.status is also not a truthful read-in-flight signal: a live
'working' hook outranks 'loading', so the guard could be off over an
in-flight empty list. Expose the read phase itself and gate on that.
* test: cover the launch-draft reducers and the sync-key skip gate
Every consumer test injects the three launch-draft reducers as bare
vi.fn()s, so reducing markNativeChatLaunchDraftAdopted to a no-op left
2609 tests green — while in the app the composer would resurrect the
prefill after every manual clear.
canSkipRuntimeMobileSessionSyncKeyBuild had no launch-draft case either:
when it skips, the sync key is never even built, so the existing
getRuntimeMobileSessionSyncKey case cannot catch its removal.
* fix(native-chat): hold the launch-draft baseline in state, not a render-mutated ref
react-compiler rejects reading or writing a ref during render. Adjust the held
baseline with the sanctioned render-time setState instead, keeping the local
copy so the render that first sees a settled transcript resolves against it.
* fix(mobile): carry the transcript identity in the session read state
react-doctor flags the separate loadedIdentity state as an extra render for a
derivable value. Hold status alongside the identity it describes in one state
written by the subscription effect, so transcriptLoading derives from it.
* test(native-chat): assert the readPhase contract without the hook-status race
The test asserted status === 'working', which depends on liveStatusOverride
winning over ambient transcript state — green locally, red under CI load. The
contract is that readPhase stays 'loading' once live content unmasks status,
so assert exactly that; it still fails if readPhase derives from status.
* fix(mobile): derive pre-read chat status instead of writing it from the effect
react-doctor's no-derived-state-effect flags idle/waiting-session/loading being
set in the subscription effect: all three are pure functions of the props. Derive
them during render and keep state only for the genuinely async outcome, tagged
with the identity it describes.
The tag now gates `messages` too, so a just-switched tab never sees the previous
tab's transcript at all rather than seeing it behind a loading flag.
* fix(mobile): drop a settled chat read once its subscription is torn down
The settled outcome was only ever replaced by a newly arriving frame, so any
effect re-run that landed back on an already-settled identity resurfaced it over
a list the same effect had just cleared: 'ready' with no messages and
transcriptLoading false. Toggling out of chat view and back hit this every time
(the agent goes null, then returns), flashing the "start a chat" empty state over
a real conversation and opening the launch-draft seed's decline check on an empty
transcript. A reconnect did the same via the client dep.
Identity and client are the effect's only inputs, so tagging the read with both
and dropping it during render when either moves covers every re-run.
|
||
|
|
0388319a32 |
Improve translations for resource manager and related UI elements (#11205)
* Improve translations for resource manager and related UI elements
Standardize terminology ("daemon" vs "service"), complete missing translations, and refine wording across Spanish, Japanese, Korean, and Chinese locales for consistency and clarity.
* Improve translations for resource manager and related UI elements
* fix(test): update zh name-mode label expectation after translation fix
The resource-manager translation pass correctly changed the Chinese
"Name" label from 姓名 to 名称; update the localized options unit test
to match so CI passes.
|
||
|
|
a40183389b |
feat: bound direct SSH reconnect fan-out and recovery (#11003)
* docs: design for direct SSH reconnect fan-out Capture the implementation-ready plan for host-qualified, epoch-fenced SSH reconnect recovery after two rounds of multi-model LLM counsel review. * docs: reconcile SSH reconnect fan-out design * docs: close reconnect design consistency gaps * feat: implement bounded direct SSH reconnect recovery * fix: bound direct SSH retry settlement * fix: harden direct SSH reconnect authority * fix: preserve split SSH retry ownership * fix: preserve SSH split continuation authority * docs: record final SSH reconnect validation * fix: preserve SSH authority through retained and detached state * fix: retain SSH authority across delayed split mounts * fix: close SSH authority recovery gaps * fix: fence stale SSH transport replacement * fix: serialize SSH target teardown * fix: settle SSH teardown failures before reconnect * fix: retire failed SSH reset sessions * test: reconcile current main E2E contracts * fix: close direct SSH reconnect review gaps * fix: fence stale SSH reconnect side effects * fix: close final SSH reconnect lifecycle gaps * test: stabilize current-main reliability gates * test: prove plugin navigation containment * test: make plugin navigation oracle authoritative * test: make plugin navigation oracle deterministic * ci: allow sharded e2e suite to finish * test: wait for runtime pane publication * test: classify pane readiness by error code * test: select close persistence terminal by tab identity * docs: mark reconnect implementation validated --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
6fc05df985 | feat(dictation): add stop button and shortcut hint to Listening indicator (#11152) | ||
|
|
9e49708c07 |
fix(codex): re-confirm spurious shell readings before skipping the restart card (#11076)
An account switch decides pane eligibility from a single cached inspectProcess read. When that read reports the pane's shell for a live Codex session, the pane silently loses its restart card - no error, no retry. Re-check shell readings on Orca-launched Codex panes with the existing fresh-scan confirmForegroundProcess before trusting them; only an affirmative codex answer flips the decision, so a genuine exit to the shell stays uncarded and unsupported providers keep today's behavior. |
||
|
|
bd8640db06 | chore: remove orchestration structured-output draft design doc from repo root | ||
|
|
2d23217166 |
feat: add Trae CLI as a supported TUI agent (#10763)
* feat: [AI-GEN] add Trae CLI as a supported TUI agent Closes #10579. Wire trae-cli into the desktop and mobile agent catalogs following the same integration pattern as other CLI agents (e.g. Ante, Devin): - src/shared/types.ts, tui-agent-config.ts: register 'trae' with detectCmdAliases (traecli/trae-agent) and argv prompt injection, matching trae-cli's `trae-cli [prompt]` contract. The CLI's own third documented alias `ta` is intentionally excluded — too generic a 2-letter name to use as a PATH-existence detection signal without false-positiving on unrelated tools. - src/shared/trae-headless-command.ts: recognize `--print`/`-p` and `--output-format json|stream-json` as one-shot headless invocations (same shape as claude-headless-command.ts) so they aren't mistaken for a live interactive session. - agent-kind.ts, telemetry-events.ts, agent-status-types.ts, agent-type-label.ts, tui-agent-display-names.ts, tui-agent-permissions.ts (YOLO via trae-cli's own --yolo flag), tui-agent-selection.ts: standard per-agent registrations. - agent-catalog.tsx, agent-favicon-assets.ts, mobile/src/tasks/mobile-tui-agents.ts, mobile/src/components/mobile-agent-icon-assets.ts: catalog entries and bundled favicon (fetched from docs.trae.cn, required by mobile's offline-icon invariant test). - i18n: add the "Trae" label to all five locale catalogs (en/es/ja/ko/zh). - Tests: agent-process-recognition, agent-status, tui-agent-startup. Verified with `pnpm typecheck` (desktop + mobile), the relevant vitest suites (869 tests across 12 files, all green), oxlint (clean), and a real end-to-end launch of the actual trae-cli binary through Orca's pty.spawn IPC path (confirmed via the OS process table). * fix: [AI-GEN] point Trae catalog entry at the real CLI quick-start doc docs.trae.cn/cli (what the installed CLI's own --help text prints as its "User manual" link) soft-404s — the docs site restructured and the working page is docs.trae.cn/cli_get-started-with-trae-cli (confirmed by HTTP fetch: real page title "TRAE CLI 快速开始" vs the old path's "404 - 页面不存在"). Addresses CodeRabbit's homepageUrl review comment. * fix: [AI-GEN] detect Trae on traecli, not the ambiguous trae-cli name Per @AmethystLiang's review: the open-source bytedance/trae-agent project (MIT, ~12k stars) registers its own console script as `trae-cli` (pyproject.toml: `trae-cli = "trae_agent.cli:main"`), an entirely unrelated CLI with a different contract (`trae-cli run "task"`, `-p` short for `--provider`). Detecting on bare `trae-cli` would false-positive on that project's installs and break launch for anyone who has it instead of the actual TRAE CN CLI. - tui-agent-config.ts: detectCmd/launchCmd/expectedProcess -> `traecli` (TRAE CN's own installer symlinks this alias too, but the other project does not ship it). Dropped the `trae-agent` alias entirely — it's the colliding project's literal repo name, the highest false-positive string available. - agent-catalog.tsx: cmd -> `traecli` to match; faviconDomain -> `www.trae.cn` (bare `trae.cn` 404s on Google's favicon service; `www.trae.cn` is the product-root domain that actually resolves). - mobile-tui-agents.ts: faviconDomain -> `www.trae.cn` to match. - Tests updated: agent-process-recognition now asserts `trae-cli` and `trae-agent` are NOT recognized as Trae (regression guard against reintroducing the collision); tui-agent-startup updated for the new launch command. promptInjectionMode stays `argv` and the headless-command file stays as-is — both verified against the real TRAE CN CLI's actual --help output (pasted in the PR review thread), not assumptions. * refactor: [AI-GEN] share one print-mode headless matcher across agents trae-headless-command.ts was a rename-only fork of claude-headless-command.ts, and ante-headless-command.ts carried a third copy of optionName. Collapse both print-mode files into print-mode-headless-command.ts, dispatch from a Partial<Record<TuiAgent, ...>> table instead of an if-chain, and compress the Trae comments to the repo's one-line style. * fix: [AI-GEN] terminate Trae flag parsing before the positional prompt `traecli` is a Cobra CLI with subcommands, so an argv prompt starting with `help`, `config`, `-…` was dispatched as a subcommand or flag instead of being run as the task. Add `argvPromptSeparator: '--'` (same reason Grok has it), and stop the shared print-mode headless matcher at `--` so a prompt that reads like `--print` no longer drops the pane out of agent recognition. * docs: [AI-GEN] name both Trae CLIs explicitly in the detect-name comment Co-authored-by: Orca <help@stably.ai> * docs: [AI-GEN] drop the vendor tag from the Trae union comment Co-authored-by: Orca <help@stably.ai> * fix: [AI-GEN] guard the nullable startup plan in the Trae separator test Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: 陈泽榜 <chenzebang@jianzhikeji.com> Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Co-authored-by: Orca <help@stably.ai> |
||
|
|
1bd80931bd | fix(diff): stop file-tree navigation remounting combined diffs; make tree resizable (#11088) | ||
|
|
21dee21a6d |
test(cli): lock CLI-compatible timeout parse contract (#11206)
parsePositiveSafeIntegerNumericText mirrors the CLI's own Number() coercion on purpose: text like `600000.000000000000001` is the budget the CLI will actually wait on, so rejecting it here would leave the relay and SSH kill timers shorter than the CLI's and cut the request short. Document that and pin it with regression cases. |
||
|
|
0404f27b3f |
chore(i18n): drop orphaned cadence catalog keys (#11154)
Removes the lowercase hourly/weekly/custom entries from the AutomationSchedulePicker catalog node in all five locales. They were superseded by keys derived from the rendered labels in #11068, and the extractor only adds keys, so they stayed behind unreferenced. Follow-up to #11068. |
||
|
|
16dcf865ed |
fix(i18n): translate automation cadence labels (#11068)
The cadence picker now resolves Hourly, Daily, Weekdays, Weekly, and Custom cron through the renderer i18n catalog, with corrected zh/ko/es translations. Supersedes #10044 (thanks @innocarpe) and #10045 (thanks @fsdwen). Fixes #10043 |
||
|
|
dca0db38c4 |
fix(orchestration): repair version-skewed run schemas (#11150)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
1fa9ffb5ea |
ci(pr): run E2E when a PR touches tests/e2e paths (advisory) (#11131)
* ci(pr): run E2E when a PR touches tests/e2e paths Regression specs under tests/e2e never ran on PR CI — only schedule and release called e2e.yml — so a red regression test could merge green. Path-filter and workflow_call the E2E suite when E2E-relevant files change. Use merge-base diffs so base-branch drift does not false-trigger E2E, fail the detector when git diff cannot compute the PR range, and pin least-privilege contents:read on both the detector and reusable E2E workflow. Closes #10518 Co-authored-by: Wooseong Kim <innocarpe@gmail.com> Co-authored-by: Orca <help@stably.ai> * ci(pr): make the E2E path gate actually block, and match the real config path Two fixes to the new path-filtered E2E job. The gate did not gate. pr.yml's `verify` job is the required check, and it enumerates its dependencies explicitly — `e2e` was in neither `needs` nor the result list, so a failing shard left `verify` green. That reproduces the exact hole this job exists to close: a red spec merges green, just with a red box further down the page. Add `e2e` to both. Because the job is path-filtered, `skipped` is the normal result on a PR that touches no E2E files and has to keep passing. That allowance is checked after the strict loop rather than inside it, so it can never leak to the six jobs that are always required. The `playwright.` pattern matched nothing. The config is tests/playwright.config.ts — beside tests/e2e/, not inside it — so no tracked file starts with `playwright.` and editing the runner config would silently skip E2E. Anchor it at `tests/playwright.`. Adds a contract test alongside the existing release-e2e one. Verified it fails when either fix is reverted, and simulated the gate across success/skipped/failure/cancelled plus the skip-must-not-mask-a-real-failure case. * test(ci): close two gaps in the E2E gate contract CodeRabbit was right on both counts — verified by reverting each and watching the contract stay green. The path filter was unasserted, so `e2e` could lose its `if:` and run on every PR — the cost the filter exists to avoid — without failing anything. The strict-loop check hardcoded four of the six required jobs, so dropping GIT_COMPATIBILITY or SHELL_CONTRACTS left them unenforced while the contract passed. Derive the list from verify.needs instead, so a newly added required job that misses the loop fails here rather than silently going unchecked. * ci(pr): land the E2E path gate advisory instead of blocking The E2E suite is currently failing every scheduled run on main — 22 of the last 22 — so making verify depend on it would block any PR touching tests/e2e/**, including the PRs that fix the suite. This PR's own run reproduced that: 3 of 12 shards failed on specs unrelated to it (agent-session resume, Jira linking, plugin containment, terminal artifacts). So the job runs and reports on E2E-path PRs but is left out of verify.needs for now. The detector, the tests/playwright. path fix, and the contract tests are unaffected — those stand on their own and were the substance of the review. Flipping to blocking is a three-line change once the suite is green; the exact wiring, including why the skipped allowance must sit outside the strict loop, is recorded on verify's Require-successful-checks step. The contract test pins the advisory choice so it reads as deliberate rather than as the unwired-gate bug it originally caught, and still fails if the path filter, the strict-loop coverage, or the config path regress. --------- Co-authored-by: Wooseong Kim <innocarpe@gmail.com> Co-authored-by: Orca <help@stably.ai> |
||
|
|
0d6f9195d8 |
fix(orchestration): reveal worker terminals reliably (#11142)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
de162c632b | fix(memory): retune image and orca.yaml ceilings that rejected valid input (#10815) | ||
|
|
380034edf9 |
fix(macos): avoid scene deadlock on app reactivation (#11055)
* fix(macos): avoid redundant focus on app activation * test(macos): cover passive app activation --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
d548641f1d |
fix(sidebar): restore legible selected-workspace fill in dark mode (#11139)
#8321 mixed the selected card's wash into the opaque --worktree-sidebar surface, lifting dark mode to 16% (#4b4b4b). Card text lost too much contrast against it. Return both modes to a translucent wash (light 8%, dark 10%) so the brighter selection border added by #8321 carries the selected state instead of the fill. Co-authored-by: Orca <help@stably.ai> |
||
|
|
efcc015d69 |
docs: add WeChat group 6 QR with overflow guidance
Show group 5 and group 6 QR codes side by side so people can join group 6 if group 5 is full. |
||
|
|
89f32a121b | fix(lint): restore nested config discovery in changed-code gate (#11130) | ||
|
|
77d4c64f7a |
Improve orchestration migration safety for live legacy workers (#11107)
* fix(orchestration): clarify legacy migration safety * fix(cli): sanitize legacy formatted messages * test(runtime): allow near-cap fuzz under shard load --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> |
||
|
|
df9f55990b |
fix(browser): keep the address bar editable in a narrow toolbar (#11098)
* fix(browser): keep the address bar editable in a narrow toolbar Every other browser toolbar control is shrink-0, so the address bar was the only flexible item and absorbed the entire squeeze: below roughly 420px of pane width it collapsed to the leading globe icon with a zero-width input. Clicking it only opened the suggestion dropdown, which inherits `--radix-popover-trigger-width` and so rendered at icon width — there was no way to type or edit a URL in that tab. Focusing a squeezed bar now lifts the form out of the toolbar flow and overlays the row edge to edge, giving a full-width editable field that navigates on Enter (and a full-width suggestion list for free). A measured slot stays in flow so the overlay cannot feed back into its own width, and the slot keeps a min width so the globe remains a real hit target instead of being overlapped by neighbouring buttons. Fixes #11090 Claude-Session: https://claude.ai/code/session_01Mx53f7erbtw5NraS8HdXKE * fix(browser): use the documented floating shadow for the expanded bar STYLEGUIDE.md defines exactly three elevation levels and forbids a fourth; shadow-md was not one of them. The overlaid address bar is a floating surface, so it takes the documented floating shadow already used by the other floating surfaces in this pane. Claude-Session: https://claude.ai/code/session_01Mx53f7erbtw5NraS8HdXKE * test(browser): make the narrow-toolbar regression deterministic The spec passed only from a clean profile. Two preconditions it set once are actively undone by the app: - BrowserPane re-focuses a blank tab's address bar across several animation frames plus the blank-url did-finish-load handler, so a single blur() was reverted and the bar never reached its squeezed resting state. - Startup paths re-open the right sidebar. At a fixed 700px window that leaves the pane ~70px, so the overlay had nowhere to go and the field measured 0px. Settling these separately let whichever settled first drift back while the next one ran. Re-assert them in one loop until they hold simultaneously, and size the window from the chrome actually measured instead of assuming a fixed 700px. Verified 8/8 green, and still fails at the overlay assertion when the fix is disabled, so the regression coverage stays real. --------- Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com> |
||
|
|
e551d3ec0d |
perf(lint): consolidate code-quality gates into Oxlint (#11117)
Consolidate standalone code-quality scanners into Oxlint, preserve focused native/type-aware enforcement, add custom plugin coverage, and harden deferred PTY test cleanup. |
||
|
|
7a3df87994 |
fix(skills): stop offering an update the CLI provably cannot perform (#11110)
* Revert "fix(skills): stop reporting a failed update when the CLI succeeded (#11105)" This reverts commit |
||
|
|
48e31b3fc0 |
fix(agent-status): show Codex v2 subagents (#11059)
* fix(agent-status): track Codex rollout subagents * fix(agent-status): resolve cross-day Codex child rollouts and unblock CI gate Codex files each rollout under its own local start date, so a session that runs past midnight spawns children into a sibling day directory. Scanning only the parent's directory left 13% of real subagent spawns (48/371 across local rollouts) permanently unresolved, which pinned a phantom "working" row and re-ran readdirSync every poll tick forever. Resolve the child's own day directory from occurred_at_ms, and time-box a child whose rollout stays unreadable so a deleted or never-written file can't leak a working row. Also make the hook HTTP handler return void: the changed-code quality gate keys findings by span overlap, so this PR's added line inside the pre-existing async createServer callback resurfaced no-misused-promises as a new finding. Tests cover cross-day resolution, grace-period retirement, and that the poll re-arms across successive roster changes (the prior tests passed even when the poll died after its first change). * fix(agent-status): keep the Codex subagent poll alive across nested hooks A nested non-codex CLI inherits its parent's ORCA_PANE_KEY, so its hook POST reached scheduleCodexSubagentPoll and tore the timer down before the source guard, silently ending polling while a rollout child was still live. |
||
|
|
54ed8c2311 | fix(opencode): harden lifecycle status delivery (#11017) | ||
|
|
c25d85cc4c |
perf(terminal): eliminate adverse control and frame-gate cases (#11045)
* perf(terminal): eliminate dense control and frame gate regressions * test(terminal): keep gate labels in valid expect shape * test(terminal): expose the surviving sub-threshold control-density case The only adverse strip fixture sat at 50% control density, which is exactly where the fallback fires and wins. A shape at 31 controls per 64-unit block evades the trigger and still loses to the per-character legacy (0.67x), so the benchmark structurally could not show it. Add that fixture, pin both density literals in the staleness guard so a retune fails loudly instead of silently measuring a boundary that moved, and export the probe constant the equivalence test was hardcoding. |
||
|
|
a8660839ee |
fix(skills): stop reporting a failed update when the CLI succeeded (#11105)
The Update skills dialog showed "The update didn't finish" / "Some skills could not be updated" with an armed Retry, directly above the runner's own log line saying "All global skills are up to date" — on a clean exit 0. `skills update` compares its lock's recorded hash against the source and never reads disk (dist/cli.mjs: `latestHash !== entry.skillFolderHash`). Once the lock has advanced past the installed bytes it prints up-to-date, exits 0 and writes nothing. The copy left behind is a recognised older revision, so the post-run re-scan sees `outdated`, skillUpdateFailedNames counted that as a failed run, and skill-update-run settled to state 'error'. Retry re-ran the same command, which no-op'd again — a closed loop. Reclassify `outdated` as "the command did not converge this", not "the run failed". The freshness badge still marks the copy not-current, so nothing is hidden; the run just stops being blamed for it. A botched write is still caught: a half-written bundle hashes to `unrecognized`, a wholly-degraded or removed copy leaves no convergent placement, and process-level failure still surfaces via the spawn error. Reachable by anyone who updated during the stub conversion window — every bundled skill has a stub -> full -> stub oscillation in its last three registry revisions. |
||
|
|
700cde83e0 |
i18n: translate orchestration page and workflow messages (#11097)
Add translations for OrchestrationPage coordinator and child PR names, plus agent workflow status messages (initial states and progress beats) across all supported languages (English, Spanish, Japanese, Korean, Simplified Chinese). |
||
|
|
3f5098a0f2 | fix(workspaces): hide false repo error for remote servers (#11050) | ||
|
|
9de4519c82 |
fix(terminal): gate the developer menu behind Option and unblock manual parking (#11091)
The Developer submenu shipped visible on every worktree right-click, and its Park terminal action always refused with "These terminals cannot be parked safely." - reveal the Developer submenu only when Option/Alt is held at right-click, captured at open time so it can't shift rows mid-menu - stop a settled pendingActivationSpawn tag from refusing a manual park: first activation stamps it on every tab and only a fresh updateTabPtyId consumes it, so a reattached tab kept it forever - resolve the single leaf of a rootless layout for parked watcher coverage, so a workspace whose panes never mounted is no longer permanently uncoverable - restore the !isVisible park guard dropped in #11016, which let the workspace being viewed unmount its own terminals - split manual-park eligibility out of the automatic cold-park policy module |
||
|
|
49fbe5231d | feat(workspaces): add emoji shortcode picker | ||
|
|
038fd7a50c | feat(workspaces): derive readable emoji identifiers | ||
|
|
84b335f80c | fix(workspaces): support emoji-only names | ||
|
|
e73b1a1dd0 |
feat(new-workspace): type-ahead Project and Run-on pickers (#11062)
* feat(new-workspace): make the project picker a type-ahead field The Create-worktree Project slot read as bulky and unpolished: a label row, an add-project icon, a 36px outline trigger and a chevron, all spent before choosing anything — then a popover carrying its own *second* search box, two-line rows, and a footer that scrolled out of reach. The field is now the search. Typing filters in place, so the nested search box is gone. Exactly one row is armed at any time and Enter takes it; hovering arms, so pointer and keyboard drive one cursor rather than two competing highlights. Armed is tracked by row key, not index, so a list arriving late over SSH cannot slide a different project under a keypress the user already aimed. Rows are single-line at 28px with an on-row Enter cap that takes space only while armed, and "Add a new project" is pinned to the popover edge so it survives every state — scrolled, filtered to nothing, or no projects at all. Long names and deep paths degrade deliberately: the name keeps up to half the row and the path elides from its middle, so two monorepo siblings stay distinguishable as …/services/checkout-api vs -web where a flat truncate rendered both identically. Recency is derived from when each project last had a workspace created, which is the action this picker is about to repeat — no new store field. The shell keeps data-project-combobox-root + role=combobox and stays focusable, so the composer's initial-focus and project-required handlers still land on it. * fix(new-workspace): align, scroll and loosen the project picker Five fixes to the type-ahead picker, three reported and two found while checking for related breakage. Alignment: the name and its smaller detail line were centred as boxes, so the 12px path sat visibly high against the 14px name. Both now share a baseline, in the committed field and in every row. The dot mark and the Enter cap are chips rather than text, so they stay centred on the row. Scrolling: the mouse wheel did nothing over the list. The composer is a Radix Dialog, and react-remove-scroll cancels wheel events for portaled content outside the dialog's DOM tree — the scrollbar dragged fine but the wheel was dead. The old cmdk list carried a shim for exactly this; the plain scroll pane that replaced it did not, so it has its own now. Density: rows go 28px -> 32px, row text 13px -> 14px and detail 11px -> 12px, with a taller Add row and more air above section headings. Escape stranded a query: with the list closed but text still typed, Escape was ignored (it was gated on the list being open), leaving the field showing text that matched nothing and hid the committed project. Escape now always restores the committed display, and only bubbles when there is nothing to undo. Listbox ownership: options sat inside unroled section and scroll wrappers, which breaks the listbox -> option relationship assistive tech relies on. Sections are groups carrying the heading as their label, and the scroll pane is presentational. Both new behaviours are covered by tests verified to fail without the fix. * chore(tools): keep the project-picker design lab The exploration harness behind the picker rewrite: 16 interactive design variants rendered against the app's real tokens and shadcn primitives, so a prototype is a drop-in ProjectCombobox rather than a mockup. Worth keeping because the frames encode bugs that only reproduce in context. DialogFrame renders the picker inside a real Radix Dialog, which is the only way the react-remove-scroll wheel bug shows up; the fixtures carry duplicate display names and deep sibling paths that a naive truncate renders identically. Run with: npx vite --config tools/wt-picker-lab/vite.config.ts * fix(new-workspace): stop the project list flashing open, shrink its empty state Opening the picker read as a double flash. The shared popover surface is translucent and fades 0 -> 1, which is right over the app canvas but wrong here: this popover lands directly on the composer dialog, so for the length of the fade the Name field underneath showed straight through the list and you saw two layers at once. The list now uses an opaque surface and zooms without fading, so it is solid from the first frame. Every other popover keeps the blur and fade. The "No projects match your search." state was a 60px centred block sitting next to 32px rows, which read as a different kind of surface and made an empty result feel like an error. It is now sized and aligned like a row. The lab's dialog frame focused whatever Radix picked first, which popped the Add-project tooltip on open and masked the real problem; it now focuses the name field the way the real composer does. * fix(new-workspace): square mark, centred empty state, and keep the list open on tab-focus Four fixes, three reported and one found while sweeping for others. Square mark: the option dot had a `rounded-full` override, so a project read as a circle here and a square everywhere else (jump palette, sidebar). Drop the override and use RepoBadgeMark's own shape. Centred empty state: "No projects match your search." was left-aligned after being shrunk to row height; centre it. Tab-focus blinked the list shut: the field lives in the popover's anchor, not inside its content, so Radix's dismissable layer saw focus land "outside" and closed the list the instant you tabbed in. Focus and pointer events within this control no longer dismiss it; genuine outside events still do. Junk text could strand the field: typing a query that matched nothing and then clicking away left the text sitting there with the list closed, showing no project and no error. A query only means something while the list is open, so closing without committing now clears it. On pressing Create with no project: no change needed. The create gate has not depended on project selection since #4991, and both submit paths already call showProjectRequiredError(), which sets the inline message and turns the field red via aria-invalid. Verified end to end: the button is pressable, the press paints the field destructive, and the message appears beneath it. * feat(new-workspace): rebuild the Run-on picker to match the project picker "Run on" was the last composer field still built the old way: an outline trigger wrapping a cmdk list, two-line rows, and no way to search. It now matches the project picker, so the two fields in the same form read as one control. The field is the search — type to filter hosts, paths and recipes with no nested search box. Exactly one row is armed at a time and Enter takes it; hovering arms, so pointer and keyboard drive one cursor. Rows are 32px with the label and its path on a shared baseline, the path eliding from its middle so two deep sibling paths stay distinguishable. The popover surface is opaque and unfaded because it lands on the composer dialog, where a translucent fade shows the form underneath. Two behaviours the project picker doesn't have are preserved. Disconnected hosts keep their inline Connect action, tracked per host so one stalled connect never blocks the others, and the list stays open so the connecting state is visible. Two rows open nested lists rather than committing: VM recipes, and "Add host" pinned to the popover edge so it survives every state — scrolled, filtered to nothing, or with no hosts at all. Enter and ArrowRight open a submenu; Escape backs out one layer at a time. Extracted from NewWorkspaceComposerCard (-563 lines) into files that each stay under the line limit without a suppression. Tests: the run-target cases asserted cmdk internals (`[cmdk-item]`, aria-disabled, cmdk-separator) that no longer exist. Rewritten against behaviour and the listbox roles instead. All 22 composer tests pass, plus a live sweep of 11 interactions in a real dialog. * fix(new-workspace): drop the Enter cap, fix submenu hover, match the Add rows Three follow-ups on the two composer pickers. The ↵ cap on the hovered row is gone from both. On a run-target row it sat next to the Connect action and read as a second, competing affordance; the highlight already says what Enter will take. Submenu rows never highlighted under the pointer. They passed a hardcoded `armed={false}`, so the recipe list and the Add-host choices were the only rows in either picker with no hover state. They now track their own hover. "Add a new project" used a chunky FolderPlus where "Add host" uses a plain Plus. Both rows were already the same height and type, so matching the glyph is the whole difference. * fix(new-workspace): restore the folder glyph, two-line Add-host cards, quiet Connect rows Three follow-ups. "Add a new project" goes back to FolderPlus — matching "Add host"'s plain Plus made the two consistent but lost the glyph that says which kind of thing is being added. A disconnected host row no longer repeats its status. The Connect button already says the host isn't connected, so "Connect this host to set up projects" beside it was saying it twice. Rows without a Connect action keep their detail, since there it explains why the host can't run. The Add-host choices go back to two-line cards. Their descriptions explain what you're picking ("Use an existing machine over SSH" vs "Pair another Orca runtime"), unlike a host row's detail, which just labels a host you already recognise. RunTargetRow grows a `stacked` variant for that rather than making the single-line row do both jobs. * fix(new-workspace): give Run on the same vertical rhythm as the other fields Run on is nested inside the Project block so the two share its error and empty states, which also put it on that block's 4px internal spacing. It reads as its own field, so it sat noticeably tighter than the 16px gap every other field in the composer gets. Pad it to match. * refactor(new-workspace): share the type-ahead machinery between both pickers Project and Run on were built one after the other, so each grew its own copy of the same mechanics: query and open state, arming by row key, the arrow-key walk, scroll-the-armed-row-into-view, the react-remove-scroll wheel shim, and the closes-drops-the-query rule. Two copies of subtle behaviour is two places for it to drift. useTypeAheadCombobox now owns all of it. Callers pass a function that turns a query into row keys and get back the query, the armed key, and the movement helpers. Run on layers its submenu state on top by wrapping `close`, which is the only part that isn't shared. The two long class strings both files repeated verbatim — the field shell and the opaque unfaded popover surface — are named constants now, so the reason they differ from the stock popover recipe is written down once instead of implied by a duplicated literal. No behaviour change: 16,456 renderer tests pass, plus the 22-check live interaction sweep across both pickers in a real dialog. * fix(new-workspace): drop aria-expanded from option rows, remove the design lab `aria-expanded` isn't a supported prop on `role="option"`, so the submenu rows were claiming a state screen readers can't interpret there. `aria-haspopup` alone already says the row opens a menu. Removes tools/wt-picker-lab. It was the harness for exploring this redesign — 12 interactive variants — and it did its job, but the 11 that lost are dead code, and its prototypes were the only thing failing the react-doctor gate (5 errors, all in throwaway variants; the shipped pickers had none). |
||
|
|
89968a1061 |
fix(preflight): refresh Windows PATH on forced CLI checks (#10091)
Refresh the persisted Windows PATH during preflight without blocking Electron's main thread. Bound and deduplicate registry reads, preserve the last good cache on failure, skip host refresh for WSL, and add Windows regression coverage. |
||
|
|
badf91101b |
fix(quality): enforce performance-safe lint baseline (#11074)
* fix(quality): clear safe existing lint findings * fix(quality): keep lint cleanup allocation-free * fix(quality): enforce performance-safe baseline * test(terminal): drain deferred confirmation cleanup |
||
|
|
9a3b348e82 | release: v1.4.160-rc.3 | ||
|
|
e104010593 |
fix(skills): require updater registration (#11051)
* fix(skills): require updater registration * fix(skills): reject incomplete updater registrations --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
f10b6de2c7 |
fix(relay): tolerate wall-clock skew in host-proof validation (#10474)
* fix(relay): tolerate wall-clock skew in host-proof validation A few seconds of local clock lag made challenge issuedAt appear in the future, so host-proof rejected every handshake and Mobile Relay looped connecting forever. Allow ±30s skew while keeping the 10s challenge window (#10401). * fix(relay): preserve host-proof challenge bounds --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
ee7ec43149 |
fix(codex): keep a host account switch inside the host lane (#10992)
* fix(codex): keep a host account switch inside the host lane markLiveCodexSessionsForRestart walked every tab's PTYs and carded any pane whose foreground looked like Codex. There was no lane check anywhere in that path, so a host account switch raised a restart notice on live SSH/relay panes — and a notice mutes the pane, so the user's remote terminal went deaf. The notice was provably spurious: a remote spawn carries a connectionId, so isDaemonHostSpawn is false and no CODEX_HOME is ever injected. The remote Codex uses the remote machine's own credentials; a local selection cannot reach it. Scope marking by lane instead. A pane's lane is (machine, runtime): `host`, `wsl:<distro>`, `env:<id>` for a relay environment, or an SSH connection that no managed selection can name. A switch made while a runtime environment is active still cards that environment's panes, which is the case that made the old "mark everything" behaviour look right. WSL was the same defect, not a separate one. A Windows run saw a WSL pane correctly escape a host switch, but only because its foreground read `wsl.exe`, which fails the Codex-foreground test — the Win32 process table cannot see into a WSL2 VM. That is incidental: `codex`, `node` and `python3` foregrounds are all eligible today, so a WSL pane that surfaces one (WSL1 pico-processes are in Win32_Process) would be carded by a host switch. The lane is now what decides. Also stop queueing remote and SSH panes into the bind-driven stale sweep at all. recordCodexPaneAccountForSpawn bails on anything that is not a daemon host spawn, so listStalePanes can never report one stale, yet each pane still spent every rung on a 15s-timeout remote RPC — ~75s per pane since the ladder widened to five rungs. The lane vocabulary moves to shared/ so the renderer keys panes exactly as a launch does rather than growing a third copy of the rules. Refs #10757 * fix(codex): key a WSL pane by the distro its launch actually used The lane guard derived a pane's WSL distro from the workspace UNC path alone. A launch does not: pty.ts hands getCodexSelectionTargetForPty a third argument, the resolved runtime's distro, so a wsl.exe pane on an ordinary Windows-path worktree launches under `wsl:Ubuntu`. The renderer keyed that same pane `wsl:__default__`, so the Ubuntu switch never reached it — the pane kept the old account with no notice, which is #10757 returning by a new route on the exact platform the issue was reported from. Resolve the distro the way the spawn does: the project execution runtime first, then terminalWindowsWslDistro. Both are already in renderer state. Also match a distro-less WSL switch against the whole `wsl:` family. Two mutations reach the renderer as `{runtime:'wsl', wslDistro:null}` while writing concrete distro slots: selecting the system default clears EVERY wsl slot (setSelectedCodexAccountIdForTarget), and `add` stores the distro it discovered from the machine. Keying those to `__default__` missed the very panes they re-pointed. The residual cost is over-marking a sibling distro after an add, bounded to this machine's WSL panes and far cheaper than a stranded pane. An owner-less remote pane colliding with the host lane was untested — that collision is what would mute a working remote terminal, so pin the disjointness rather than the literal key. Refs #10757 * fix(codex): resolve a pane's lane the way its launch resolved it Three more places where the renderer's lane and the launch's lane disagreed. Each disagreement is silent: too narrow and a stranded pane never gets its notice (#10757 returns), too wide and a healthy pane is muted, because a notice makes onData drop every keystroke. Shell: main runs the request through resolveLocalWindowsTerminalRuntimeOptions, so an unset shellOverride still lands on WSL when that is the Windows default. Reading tab.shellOverride alone called such a pane `host` — a host switch would have muted a working WSL terminal. Gate on the renderer platform, as pty.ts gates on process.platform. Cwd: a terminal's startup cwd is deliberately not constrained to the worktree (resolveTerminalStartupCwd, #7685), and main keys the lane off that cwd. Follow it through the same shared call instead of reading the workspace root, so a pane split after `cd \\wsl.localhost\...` is keyed where it actually runs. The comment claiming a pane can never start outside its workspace was simply wrong. Family match: narrow the previous commit. setSelectedCodexAccountIdForTarget only nulls every WSL slot when the account is null AND no distro is named; any other write lands in one slot. So claim the family only when the change actually cleared them all, and let `add` pass the created account's concrete target rather than the row's "WSL default". Both call sites already knew which case they were in. Refs #10757 * fix(codex): derive the pane's project runtime the way main does The previous commit reached for getLocalProjectExecutionRuntimeContext as a stand-in for main's resolveLocalProjectRuntimeForWorktreeId. They are not the same function, and the differences both produce wrong lanes: - It falls back to `state.activeRepoId` when the worktree is not a git worktree, so a folder-workspace pane inherited whichever repo happened to be selected. That is not a property of the pane at all — the lane moved when the sidebar selection moved. On a WSL project it both muted a healthy host pane and hid the notice a host switch owed it. - It synthesizes a runtime from `inherit-global` where main returns undefined, and its host branch rewrites an explicit `wsl.exe` to powershell.exe, keying a live WSL pane `host`. Walk repo -> project directly instead, which is what resolveLocalProjectRuntimeForRepo does, and use it only to supply a distro — never to downgrade a shell. That also drops the throwing call out of this path entirely; the lane runs outside scanCodexPanes' inspection guard, so a throw there would have lost the notice for every pane in the batch, not just one. Also find the added account by diffing the roster. Reading it back through the row's active id returns null once two distro slots are filled, which sent the notice to `wsl:__default__` while `add` had written a concrete distro. Refs #10757 * fix(codex): key the lane off the runtime the renderer actually shipped Reverses the project-runtime half of the previous commit. That commit assumed main resolved the project runtime itself, so it re-derived one by hand. It does not: for a local pane the RENDERER computes it with getLocalProjectExecutionRuntimeContext and ships it with the spawn (pty-connection.ts), and pty.ts feeds that straight to getCodexSelectionTargetForPty. So the helper is not an approximation to be improved on — it is the launch. The hand walk dropped the global Windows runtime default, which is what turns an `inherit-global` project preference into WSL. A user who set their runtime default to WSL but left terminalWindowsShell alone would have had every live WSL pane keyed `host`: muted by a host switch, and missed by their own. It also disagreed on folder workspaces, where the launch really does resolve through the active repo. Keep the repair-required early return: that call throws, and it sits outside the scan's per-pane failure guard, so a throw would lose the notice for every pane in the batch rather than one. Separately, floating terminals have no workspace root, so their startup cwd is used verbatim (resolveTerminalStartupCwdForWorkspace). Resolving one against a root that does not exist yielded no cwd at all, keying a floating Codex pane on a WSL filesystem as `host`. Read its cwd directly. Require exactly one new account before trusting the roster diff — an unloaded prior roster makes every account look new, and Add Account is not gated on it. Refs #10757 * fix(codex): stop claiming a floating-terminal cwd the tab never has The floating-terminal branch read tab.startupCwd, which no floating creation path ever sets (FloatingTerminalPanel, FloatingTerminalWindowControls, floating-workspace-tab-creation all pass none). Its cwd is resolved over IPC from settings.floatingTerminalCwd and handed to the transport as a prop, so it never reaches the store at all. The branch was inert and its comment described main's handling of args.cwd rather than what the code read. Say what is actually true: a floating pane is keyed by its shell, and the configured-WSL-cwd-under-a-host-shell case is a known gap. Guessing from the unresolved setting would risk the mute direction, which is the expensive one. Also pin the repair-required early return. resolveLocalWindowsTerminalRuntimeOptions throws there, and the lane runs outside scanCodexPanes' per-pane failure guard, so without it Promise.all rejects and every pane in the batch loses its notice. That guard had no coverage; removing it now fails with the spawn error. Refs #10757 * fix(codex): trust the lane main recorded at spawn over a re-derived one The switch path re-derived each pane's Codex lane from current state while main had already written the resolved shell, cwd and distro at spawn. Four review rounds each found another divergence between the two, and the derivation still answers for a launch that never happened once the user edits a runtime preference. Prefer the recorded lane where one exists; keep the derivation for the panes main never records — pre-feature panes, LocalPtyProvider spawns and remote ids — and log when the two disagree. * refactor(codex): drop a redundant guard around the recorded-lane lookup |
||
|
|
2cf91b8e69 |
fix(skills): complete plugin-cache scans without false attention (#10865)
Fixes the P0 where skill cards showed an unclearable amber "Needs attention" while the Details dialog reported everything up to date. Root cause: when the plugin-cache scan tripped one of its own bounds it recorded an incomplete path, and inventorySkillFreshness expanded that into one fabricated placement per manifest skill at a path it never stat'ed. Those synthetic "inaccessible" copies lit the pill, were filtered out of the dialog, and could never be cleared because plugin-cache is not an updatable topology. - Removes the fabrication; reports typed scan issues instead. - Requires readable SKILL.md evidence before promoting a directory to a candidate, so a same-named foreign plugin (Codex's own computer-use) no longer flags. - Prunes skill payloads and node_modules so ordinary vendor caches stop tripping the depth and entry bounds. - Partitions scan reasons: only a real read failure raises a pill; bounds that ended the walk block an all-clear claim; the rest are Details-only. Fixes #10633. Refs #10659, #10904, #10918, #10775, #10791, #10813. |
||
|
|
025ceec04e | test(e2e): harden release latch and teardown coverage (#11036) | ||
|
|
e4b113b11d | fix(native-chat): keep known sessions loading until flush (#11032) | ||
|
|
dbbeae3512 | fix(terminal): clear retained overlay for editor tabs (#11030) | ||
|
|
84c70d6e29 |
fix(mobile): heal a stale input line before sending diff-review notes (#11035)
* fix(mobile): heal a stale input line before sending diff-review notes The stale-input marker is keyed by terminal handle, not by surface, so a paste orphaned on a terminal by native chat is still marked when the user sends diff-review notes to that same terminal — and those notes were submitted on top of it. Gate the send on the heal, as the native-chat answer send already does; when the clear fails, surface the error instead of dropping the note silently. Completes a follow-up deliberately deferred by #10480. * test(mobile): assert the post-heal send carries the notes, not another clear * style(mobile): trim the stale-heal comment to its non-obvious why Keeps the terminal-handle keying and the #10228 link (why a NativeChat-named helper runs in diff review) and the deviceToken rationale; drops the clause that restated the call. Addresses CodeRabbit review feedback. |
||
|
|
12ef12c55b |
chore(quality): ratchet Oxlint, React Doctor, and Zustand performance (#11034)
* chore(quality): ratchet lint and Zustand performance * fix(ci): stabilize React peer lock snapshot * fix(ci): isolate PR diff and React Doctor CLI |