mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
f0ae0837dd382dbd5e9ba04ead9c5d44f686a2da
5061
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2b057eb21e |
fix(terminal): end a url at CJK punctuation instead of swallowing it (#15240)
Terminal URL detection treated every non-ASCII character as part of the url, so Japanese or Chinese text written straight after one was absorbed into the link. A line like PR: https://github.com/org/repo/pull/12345(作成済み・マージ待ち) underlined the whole run and opened the annotation percent-encoded onto the end of the url, which 404s. The body terminator listed only ASCII codes, so nothing at or above 0x80 could end a url. Terminating on all non-ASCII is the obvious repair and is wrong: a url path may legitimately carry unencoded CJK, and the wrapped-url tests cover exactly that - a real multi-line url whose continuation row is a Chinese path segment. That repair was written first and broke four of them. The distinction is punctuation, not ASCII. Non-ASCII punctuation, symbols and separators end a url; letters do not. Full-width brackets, an ideographic space, a full-width comma and the katakana middle dot are prose; 文档 in a path is not. Adds the extraction test file the module never had, covering the reporter's three cases, the CJK-path case that must keep working, and ASCII behaviour as a regression guard. Closes #10571 |
||
|
|
d143922561 |
fix(terminal): deliver an IME commit the deferred textarea diff missed (#15198)
Picking a single Chinese character from the candidate window with a number key loses it. The character flashes and disappears. Picking the same candidate with the mouse works, and picking multi-character words with number keys works. Two paths can deliver an IME commit, and this falls between them. A keydown the input method consumed routes into a setTimeout(0) diff of the helper textarea, and that diff is what normally delivers the commit; xterm's _keyDownSeen guard exists to defer to it. When the commit arrives after that timer has already run, neither path delivers. Mouse selection works because no key is down, and a real composition session works because it takes a different path entirely. That narrows it to an input method whose commit round-trips asynchronously and which shows no in-application preedit. Track that a consumed keydown still owes its commit, and deliver only when the diff did not. The upstream guard and its single read site are untouched, which is what keeps the duplicate-commit behaviour it was added for sealed. Not doing the obvious repairs deliberately: clearing the flag, skipping it for keyCode 229, or setting it after the composition short-circuit each unblock the input path without retiring the diff, and all three were measured emitting the character twice. The patch and the lockfile hash here are generated. Review config/patches/xterm-src/@xterm__xterm@6.1.0-beta.287.src.patch, which is the hand-written source of the change; the shipped patch and both minified bundles are the regenerator's output from the pinned upstream build, so nothing in this change was hand-transcribed into a bundle. Refs xtermjs/xterm.js#6036 Closes #12099 |
||
|
|
cdd3aabdd8 |
Display occupant agent icons in tab search results (#15134)
* Display occupant agent icons in tab search results When searching for or viewing open tabs, terminal tabs now show the icon of their occupant agent (e.g., grok) instead of a generic terminal icon. This makes it clearer which tabs have agents actively running. The occupant resolution reuses the same logic as the tab-strip agent identity system, including support for launchAgent, hook status, sleeping sessions, and OSC title parsing. * Simplify tab occupant agent resolution to use unified label only Remove the recordTitle parameter and rely solely on the unified title, which already carries live OSC titles. The terminal record's own title can stay stale (e.g., "Terminal N") while the unified label reflects the current state, eliminating duplication and simplifying the contract. * Add occupantAgent field to workspace tab helper |
||
|
|
314b02ba2d |
Redesign artifacts page as full-width table with drawer (#15233)
* refactor(artifacts): redesign as full-width table with detail drawer - Artifacts list displays as a compact data table with columns (Name, Type, Size, Updated, Expires) - Selected artifact opens in a right-side drawer instead of inline preview - Search and refresh consolidated in top toolbar - Better space utilization for browsing the artifact list * refactor(artifacts,automations): extract shared list-table layout - Extract common list-table styles (container, header, row) to @/lib for consistency across artifacts and automations tables - Move row interaction utilities to @/lib/list-row-interaction for reuse - Fix drawer width to calc(100vw-80px) to avoid macOS traffic-light controls - Extract WINDOW_CONTROLS_WIDTH/HEIGHT constants so portaled surfaces avoid the Windows/Linux overlay without hardcoding pixels - Clamp artifact search query to 2KB to prevent multi-MB pastes from pinning renderer memory - Remove unused artifact list visual mock * Extract shared artifact row actions and use CSS var for traffic lights - Unify dropdown and context menu actions via artifactRowActions() to prevent them from diverging during future maintenance. - Replace hardcoded 80px with platform-aware CSS variable (--mac-traffic-lights-width) so only macOS reserves space for traffic lights; Windows and Linux controls sit on the right edge instead. |
||
|
|
f48bdf8f59 |
fix(new-workspace): keep workspace creation reachable with zero projects (#15234)
The sidebar +, the landing Create button, the board lane +, the palette create row, and the tour CTA all disabled themselves when repos was empty — a dead end, since the composer's project field can add the first project inline and auto-select it. Drop the repo-count gate from every entry point. Submitting without a project still shows the inline "Choose or add a project" error. |
||
|
|
19ba83d496 | docs(mobile): add Android APK install guidance (#14978) | ||
|
|
598ba5d276 |
fix(terminal): stop the macOS IME forwarder from running on iPadOS (#15218)
* fix(terminal): stop the macOS IME forwarder from running on iPadOS Korean typed into a terminal from an iPad web client arrives as loose jamo instead of composed syllables. The native-text forwarder is a macOS workaround: it claims a printable keydown and delivers the input method's substituted text from the input event alone. It stands aside for IME composition by checking isComposing and compositionstart. Touch iOS/iPadOS is unreliable about firing those for hardware-keyboard CJK input, so each jamo keydown is claimed as its own one-shot substitution rather than deferred to xterm's composition handling. It runs there at all because every iOS user agent contains "Mac" - "Macintosh" in iPad desktop mode, the default since iPadOS 13, and "like Mac OS X" in mobile mode. maxTouchPoints is the only signal a real Mac never sets. The Linux branch immediately below already makes the mirror-image exclusion for Android and CrOS; the Mac branch never got the same treatment. Gate the forwarder install only. isMac keeps its other five consumers - the Ctrl+C interrupt, clipboard bypass, JIS yen input and the standalone 229 keydown policy - which intentionally still treat an iPad with a hardware keyboard like macOS, matching iPadOS shortcut conventions. Without the forwarder, xterm's own composition path plus the deferred textarea diff is the sole delivery mechanism, which is already the arrangement on Linux. Not verified on hardware: the claim that composition events are absent on iPadOS is the mechanism the code supports and the only one matching the reported symptom, but no on-device capture confirms it. The platform detection stands on its own regardless. terminal-ime-input-context-refresh.ts has the same user-agent collision for its NSTextInputContext refresh. Narrower trigger surface, left for a follow-up. Refs #13345 * fix(terminal): require more than one touch point before skipping the forwarder The gate used maxTouchPoints > 0, which is looser than the idiom already in this repo. isIOSWebView in mobile/src/terminal/terminal-webview-html.ts requires more than one, because a Mac with a touch-capable peripheral can report exactly one, and such a Mac must keep the forwarder: it is a real Mac running the input method this workaround exists for. Under the old threshold that Mac silently lost native text substitution - a macOS regression introduced by a fix aimed at iPadOS. A captured iPad reports five, so the stricter bound costs nothing on the device this targets. The check stays user-agent based rather than adopting that helper's platform check, because an iPhone reports platform "iPhone" rather than "MacIntel" and would slip through. |
||
|
|
604169f4af |
Filter automations list by agents (#15224)
* Add agent filter to automation list Allows filtering automations by one or more agents with search support. Status and last-run filters are reorganized into submenus. External automation entries are excluded from agent filtering scope. * Fix translation keys for agent filter in automation list Move agent search text from AgentCombobox keys to component-specific AutomationListFilterMenu keys. Adds translations across all locales. |
||
|
|
3697d68f21 |
fix(cmd-j): decline a GitLab iid match when the repo remote names a different project (STA-4450) (#15193)
* fix(cmd-j): decline a GitLab iid match when the repo remote names a different project (STA-4450) `repoMatchesGitLabSlug` laundered a definite project-path mismatch into `'unknown'` whenever the resolved identity came from a remote named `upstream`, and `worktreeMatchesGitLabUrl` treats `'unknown'` as permission to accept a bare iid. Since `deriveGitRemoteIdentity` ranks `upstream` above `origin`, any repo whose top-ranked remote is `upstream` lost GitLab project gating entirely, so an exact URL for an unrelated project could surface that workspace. Return the `matchGitRemoteKeyParts` verdict directly. Resolved identities are re-probed on a 6h TTL, so a remote naming a different project is current evidence. `'unknown'` now means only "no identity" or "unexpanded SSH host alias", both of which stay permissive as before. * docs(cmd-j): correct the identity-freshness comments and drop a duplicate test The GitLab why-comment implied resolved identities refresh unconditionally. They only refresh when a repo/project list sweep finds one past its ~6h TTL (`selectEnrichmentCandidates` runs from `repos:list`/`projects:list`/ `projectHostSetups:list`, four refreshes per sweep, after a 5m startup delay); there is no background timer. State the accepted cost instead of implying the gate is loss-free. The GitHub-side comment still claimed the identity is "chosen when the repo was added and never re-probed" — the exact claim this PR disproves. Rewrite it to the reason that still holds (one stored remote hides a fork's `origin`). Behavior on the GitHub path is unchanged; it stays with the twin ticket. Delete `does not surface an upstream-identified repo for an unrelated project iid`: the inverted test above it already asserts both halves (mismatched project declines, the named project still matches) against the same upstream-derived identity. |
||
|
|
a54c27f00d |
Restructure automation editor dialog into three-column layout (#14803)
* Restructure automation editor dialog into three-column layout - Separate prompt editing from settings configuration - Add Monaco editor for prompt with find widget support - Extract settings into right sidebar for better organization - Move automation name into prompt section for context - Simplify header and footer to focus on key actions - Settings controls now smoothly collapse when switching between Orca and Hermes targets * Fix React Doctor leak on automation prompt Escape listener. Move addEventListener into a helper that returns cleanup so the changed-code quality gate can see the subscription is released. * Fix stale ref closures in automation prompt editor - Move `onDismissRef.current` update into useLayoutEffect with `[onDismiss]` dependency to prevent stale closures in event listeners - Move `contentRef.current` update into the layout effect that syncs it, ensuring editor has current value when effects reference it |
||
|
|
f8e728bb8b |
fix(watcher): watch the resolved worktree root so symlinked and differently-cased paths work (#15077)
* fix(watcher): keep macOS FSEvents paths under the subscribed worktree root
macOS FSEvents reports OS-canonical paths: symlinks resolved and every
directory in its on-disk spelling. Linux (inotify) and Windows both rebuild
event paths from the directory that was subscribed, so only macOS observes
the mismatch.
Orca's watcher contract is "event paths live under worktreePath". Consumers
derive a worktree-relative path with relativePathInsideRoot(), which returns
null when the event falls outside the root -- and a null relative path drops
the event silently. So on a Mac whose worktree or folder path traverses a
symlink (~/code -> /Volumes/..., anything under /tmp or /var), or is spelled
with different casing than disk on a case-insensitive volume, every watcher
event was discarded: the editor never reloaded an agent's edit, the File
Explorer never refreshed, and Source Control never re-ran status. Nothing
errored, which is why this looked like "the file watcher stopped working"
on some machines and not others.
Rewrite event paths back onto the subscribed root inside
subscribeThroughWatcherSupervisor -- the single boundary every desktop,
runtime-environment, and SSH-relay watch passes through -- so one change
covers all three transports.
The resolution runs alongside the subscribe rather than before it: an await
ahead of the subscribe call lets a caller's abort land in a window where no
watcher-process subscription exists to cancel, which hangs the existing
cancellation contracts. The subscribe promise settles only after the rewrite
is installed, and only after the subscription itself is recorded, so
teardown never waits on a realpath.
Matching folds per path segment (NFC + case) instead of by prefix length,
because both folds change length and a folded-prefix length would slice the
raw event path mid-character. Byte-exact fast paths run first, so unaliased
roots -- every Linux and Windows watch, and most macOS ones -- cost one
string comparison per event and allocate nothing.
* fix(watcher): watch the resolved root so symlinked worktrees work on Linux too
Verified on a real Linux host: @parcel/watcher passes IN_DONT_FOLLOW |
IN_ONLYDIR to inotify_add_watch, so a symlinked worktree root fails outright
with ENOTDIR ('Not a directory'). The watch never installs and Orca caches the
root in unwatchableRoots, so it is never retried for that session. That is a
worse symptom than the macOS path-spelling mismatch and hits Linux users of
symlinked checkouts on every machine.
Hand the backend the resolved directory instead of the caller's spelling, and
keep mapping delivered paths back. Resolving the root also lets
@parcel/watcher's own ignore paths match again on macOS, where they were
computed from the unresolved root and silently excluded nothing.
The resolve is synchronous on purpose. Every caller reserves and forks its
watcher child in the same tick as the subscribe call -- capacity accounting and
cancellation ordering both depend on it, and 30+ existing tests encode it -- so
an await here would open a window where a subscribe is issued but no
cancellable child exists.
* test(watcher): use a directory junction on Windows so the alias repro runs there
Creating a directory symlink on Windows needs elevation or Developer Mode, so
the alias tests failed with EPERM on a real Windows host. A junction needs
neither, is what users actually have (a junctioned C:\dev), and realpath
resolves it identically -- so one fixture now covers all three platforms and the
end-to-end repro no longer skips outside Linux and macOS.
* test(watcher): pin the fabricated-path failure modes of the root rewrite
A rewrite that returns a WRONG path is worse than no fix -- a consumer would
act on the wrong file -- so pin the cases that could produce one: sibling
directories that share a prefix with the root (POSIX and UNC), the root itself
versus a shorter path, drive-letter casing, a root-only canonical path, and a
script where toLowerCase changes length. Found by running the rewrite over an
adversarial table; all already passed, so these lock in behaviour rather than
fix it.
* docs(watcher): drop an unverified claim about ignore paths
I claimed resolving the root also repairs @parcel/watcher's ignore-path
matching for aliased roots. Probing it on macOS shows the node_modules write is
excluded either way: FSEvents resolves symlinks in its own exclusion paths, so
the daemon filters at the source regardless of which spelling we subscribe with.
On Linux the exclusions are userspace globs relative to the watched directory
and there was no watch at all before this change, so there is nothing to
compare. Removing the claim rather than leaving a plausible-but-wrong rationale
in the module header.
* refactor(watcher): simplify root path rewriter
* test(palette): build searchable fixture documents
|
||
|
|
0e96b82e44 |
fix(mobile): keep phone tab selection across host snapshots
* fix(mobile): keep phone tab selection across host snapshots Preserve device-owned tab focus across ordinary host republications while explicit follow navigation remains authoritative. Retire closed selections across clients so stale snapshots cannot resurrect tabs. * fix(mobile): acknowledge session tab closes * fix(mobile): avoid tombstones for uncommitted closes * fix(web): implement session close IPC stubs * refactor: simplify mobile tab close flow * fix: bound session tab close confirmation |
||
|
|
9b8e9dc226 |
Prevent tab search results from jumping while typing (#15133)
* Prevent tab search results from jumping while typing - Retain deferred results that still match the current query - Add retainOpenTabResultsForQuery utility with query matching logic - Refactor TabBarCreateEntry to use useTabCreateEntrySearchResults hook * Re-check retained tab search rows with full search engines Instead of checking if row text contains the query, retention now re-runs the search engines on deferred results. This respects all matching rules (type aliases, paths, workspace labels, agent snippets) and ensures stale or mismatched rows don't linger on screen as the user types. |
||
|
|
24e662adc1 |
feat(ssh): verify host keys, and restore panes correctly across a reconnect (#14844)
* docs(ssh): design for real host key verification (STA-4319)
Today's ssh2 verifier records a fingerprint and returns true — every host key is
accepted, with no known_hosts consult and no change detection anywhere in
src/main/ssh/. Scope is per-connection, so exec, SFTP, port forwarding, the
watcher and relay deploy all ride that one unverified handshake, and the
ProxyJump path puts the final hop — the topology most likely to cross untrusted
network — on ssh2 specifically.
Decisions worth calling out:
- Read the user's known_hosts as a trust source but NEVER write to it. That file
is shared with every other SSH tool on the machine; appending means line
endings, permissions, concurrent writers and a corruption blast radius well
beyond us. Accepted keys go to our own per-target store. Reading theirs is also
the entire migration story: most developers already have their hosts there.
- Mismatch is scoped to the SAME key type. A host with only an RSA entry that
presents ed25519 is unknown, not changed. ssh2 negotiates ed25519 first, so
without this we would fire a change-of-key alarm at nearly every existing user
on their first upgraded connect — training them to dismiss the one warning that
is supposed to mean something. Flagged in review as the decision I am least
sure of; a downgrade-vector argument against it is being tested.
- Changed key hard-fails with no override button; recovery is a separate explicit
action, offered only when OUR store is what disagreed, because forgetting our
record cannot unblock a known_hosts conflict.
- Background reconnects deny rather than prompt. A dialog the user cannot place
in context only teaches click-through.
Two traps are documented because either would make the fix silently do nothing:
an async verifier returns a Promise, which ssh2 reads as truthy and accepts
immediately; and the existing test mock invokes hostVerifier with one argument
and ignores the return, so it would pass against a verifier that never decides.
Design only — no behaviour change. The doc is added to the tracked-reference
allowlist in .gitignore alongside the other docs/reference entries.
* docs(ssh): revise the host key design after security and migration review
Three things the reviews changed, kept visible rather than quietly edited out.
THREAT MODEL WAS WRONG IN THREE PLACES. Jump hosts are not the worst case — they
are already safe: shouldUseSystemSshTransport branches on exactly the inputs
resolveEffectiveProxy does, and attemptConnect returns after the system probe, so
ProxyJump goes through OpenSSH and is verified. Agent forwarding was overstated
(gated on the user's ForwardAgent). Credential theft was understated: any auth
error counts as agent fallback, so a MITM walks the user to the password AND
private-key passphrase prompts, and cachedPassword replays without prompting. The
relay claim was backwards — the attacker owns their own machine; the real impact
is the return direction, where they become the host our workspace trusts.
TYPE SCOPING IS A DOWNGRADE VECTOR WITHOUT ALGORITHM ORDERING. This was the
decision I flagged as least certain and asked to have argued both ways. OpenSSH
is safe only because order_hostkeyalgs() puts known types first and RFC 4253
gives the client's order priority. ssh2 negotiates ed25519 first regardless, so
an attacker who cannot forge the RSA key on file just presents ed25519 and gets a
friendly first-contact prompt instead of a hard failure. Keep scoping, but set
algorithms.serverHostKey to lead with the types on file — and add a sixth
outcome for 'unknown type, known host', which must never read as first contact.
SHIP THE DEFENCE BEFORE THE DIALOG. Startup restore fires eager connects for all
targets in parallel with a 15s timeout while a prompt would live 120s; ephemeral
VM targets present a new key every launch; paired-web connects run on the host
desktop, so the dialog opens on someone else's screen. Phase 1 is therefore no
modal at all: consult known_hosts and our store, match connects, unknown persists
with accept-new semantics, mismatch and revoked hard-fail. That is the whole MITM
defence with none of the migration risk.
Also folded in, verified live against OpenSSH 10.2p1: the without-port fallback
(bracketed lookup first, then bare, where the second pass can only yield match or
unknown — otherwise a bare line plus a non-default port produces a spurious
prompt); hashed entries hash the candidate form; multiple files union; a
cert-authority line does not match a plain key. IPv6 and bracket parsing moved
INTO scope — that is a parser requirement, not a scope call, and getting it wrong
produces the prompt-training harm the design exists to avoid.
* feat(ssh): parse and match OpenSSH known_hosts
The matcher half of STA-4319. No behaviour change yet — nothing calls this.
Hand-rolled because no maintained JS implementation exists, and written against
behaviour observed from OpenSSH 10.2p1 rather than inferred from the man page.
Three of those behaviours a reasonable reading gets wrong:
- A non-default port is TWO ordered lookups, not one candidate set: '[host]:port'
first, then bare host ('checking without port identifier' in ssh -v). The
fallback pass can only yield match or unknown — OpenSSH downgrades a wrong key
there rather than reporting a change. Collapse them and anyone holding a bare
line who connects off-port gets a spurious first-contact result; treat the
fallback as authoritative and they get a false change-of-key alarm.
- Revocation resolves in its own pass so the verdict cannot depend on line order.
Verified both orderings.
- A cert-authority line never matches a plain host key; it only validates
certificates. A normal line alongside it still decides.
Mismatch is scoped to the same key type, and a host known by a DIFFERENT type
returns unknown-type-known-host rather than plain unknown — an attacker who
cannot forge the key on file must not get a friendly first-contact result by
presenting another type. That outcome is only half the defence; the other half
(leading serverHostKey with known types) lands with the wiring.
47 tests from vectors executed against real sshd, including ssh-keygen -H hashed
entries. Each of six mutations reddens it: collapsing the passes, letting the
fallback report mismatch, dropping type scoping, resolving revocation in line
order, honouring an unrecognised marker, and skipping the blob/type agreement
check.
* feat(ssh): decide what to do with a presented host key
The policy half of STA-4319, kept separate from the ssh2 wiring so it is testable
without a handshake and injected rather than importing its sources, so a test
states its own trust state instead of writing files.
Phase 1 ships no dialog — a test asserts the decision is never 'prompt'. Startup
restore opens every previously-active target at once, ephemeral VM targets would
ask every launch, and paired-web connects run on the host desktop where the
dialog would appear on someone else's screen.
Ordering that matters: revocation outranks everything including
StrictHostKeyChecking=no, because a revoked key is a statement that this key is
known-bad rather than merely unrecognised. known_hosts is named before our own
store on a change, because its remedy (ssh-keygen -R) is the one that also
unblocks ssh and git — pointing at a remedy that cannot work is worse than none.
Two carve-outs with reasons: an ephemeral runtime target accepts WITHOUT
recording, since a fresh VM presents a new key every launch and a stored record
would accumulate per launch and eventually read as a spurious change; and when
ssh -G ran on the HOME-divergent path that suppresses /etc/ssh/ssh_config, an
unknown host is denied, because a site-wide policy may forbid it and being laxer
than ssh is the one outcome that is never acceptable.
Rejection text deliberately avoids 'authentication failed' and 'permission
denied': the reconnect ladder classifies on those substrings, so a denial phrased
that way is retried forever against a decision that will never change. Pinned by
a test.
* feat(ssh): build the host key verifier and the algorithm order that makes it safe
Still not wired into the handshake — that lands next. This is the piece that
turns a decision into an ssh2 callback, plus the half of the design that is easy
to forget because it lives in a different config field.
The verifier MUST be a plain function returning undefined. ssh2 does
'const ret = verifier(key, verify); if (ret !== undefined) verify(ret)', so an
async function returns a Promise — neither undefined nor falsy — and ssh2 accepts
the key immediately while ignoring whatever the callback later decides. Making
this async would silently restore exactly the accept-everything behaviour the
module exists to remove, so a test asserts the return value is undefined.
orderServerHostKeyAlgorithms is what makes type-scoped matching safe rather than
a downgrade. RFC 4253 gives the client's algorithm order priority, so leading
with the types we already hold for a host denies a server the choice of
presenting some other type to convert a hard failure into first contact. Without
it, an attacker who cannot forge the key on file just offers a different
algorithm. Revoked entries never contribute to that order.
Also fails closed on two paths that would otherwise hang or over-trust: a key
whose own length-prefixed header cannot be read is refused rather than reasoned
about, and a throw from any dependency denies, because ssh2 may not catch an
exception raised inside the verifier and the handshake would hang instead of
failing.
18 tests. Includes the two negative cases that matter — first-contact keys are
recorded, but keys we already know, rejected keys, ephemeral runtime targets and
a lax StrictHostKeyChecking are not.
* fix(ssh): promote every RSA signature algorithm for a known ssh-rsa key
A known_hosts entry names the KEY type, which is not the negotiated ALGORITHM
name. One ssh-rsa key is offered as rsa-sha2-512, rsa-sha2-256 or ssh-rsa
depending on the signature algorithm, so matching the literal name only would
leave a host we know by RSA ordered behind ed25519 — precisely the ordering this
function exists to prevent, and precisely the population (RSA-era known_hosts
entries) it was written for.
Verified from ssh2's own negotiation while wiring this: kex.js iterates the
CLIENT list and takes the first entry the server also offers, so client order
does decide, as RFC 4253 says. ssh2's default order leads with ed25519 and places
the RSA algorithms fifth through seventh.
* fix(ssh): verify host keys instead of accepting every one (STA-4319)
The actual fix. ssh-connection's verifier recorded a fingerprint and returned
true, so every ssh2 connection accepted every host key — no known_hosts consult,
no change detection. It now consults the user's known_hosts plus our own store
and refuses a changed, revoked or unverifiable key.
Phase 1 by design: no dialog. Unknown hosts are accepted and recorded
(accept-new semantics), because startup restore opens every previously-active
target at once, ephemeral VM targets present a new key each launch, and
paired-web connects run on the host desktop where a prompt would appear on
someone else's screen. The MITM defence lands now; the prompt is Phase 2.
Also sets algorithms.serverHostKey to lead with the types already known for the
host. Without it the type-scoped matching is a downgrade — an attacker who cannot
forge the key on file just presents another type and turns a hard failure into
first contact. Verified from ssh2's kex.js that the client list decides.
Denial replaces ssh2's generic handshake error with the specific reason, because
the reconnect ladder cannot distinguish a generic failure from a transient fault
and would retry forever against a decision that will never change.
An unreadable trust store degrades to known_hosts only rather than failing the
connect: a changed key is still refused, and a host trusted only by us falls back
to first contact and is re-recorded, reaching the same decision.
The ssh2 mock now uses the callback form and aborts the handshake on denial. As
written it called hostVerifier(key) with one argument and ignored the result, so
it would have passed against a verifier that never decides — flagged in the
design as a mock that had to change, not a test to quietly rewrite. Two new tests
pin the wiring rather than the module: an unidentifiable blob is refused, and a
well-formed key is accepted.
Note for review: commit
|
||
|
|
9f4ea42493 |
fix(agent-status): say what an OpenCode permission request is waiting on (STA-3160) (#14614)
* fix(agent-status): say what an OpenCode permission request is waiting on (STA-3160)
A permission.asked arrives as hook_event_name PermissionRequest, but
extractOpenCodeToolFields had no branch for it, so the pane reported a bare
{state:'waiting'} with no tool or command. The user could see that OpenCode was
blocked but not on what.
Read the fields @opencode-ai/sdk fixes for EventPermissionAsked: 'permission'
names the request, and metadata/patterns carry the command or paths it covers.
The normalizer is shared with mimo-code, so both are covered.
* fix(agent-status): show the OpenCode permission on the row, and retire it after (STA-3160)
Live validation against opencode 1.18.18 showed the original change populated
toolName/toolInput on a `waiting` entry that no surface rendered, while leaving
the answered permission cached for the rest of the pane's session.
Retire the tool fields on every OpenCode-family event except PermissionRequest.
isNewTurnEvent is false for this family, so resolveToolState otherwise inherits
one answered permission onto every later frame and the row reads a resolved
command as the live tool. Reproduced end to end: after approving `rm -rf build/`,
an unrelated later turn still reported it.
Read `filepath` from permission metadata. The SDK types metadata as
Record<string, unknown>, so its keys come from each tool; a live opencode 1.18.18
sends `filepath` (one word) for `edit`, which the previous key list missed. The
fallback to `patterns` covered it by accident, and the test that claimed to cover
it used `metadata: {}` — a shape OpenCode never emits. Tests now use captured
payloads for bash, edit and webfetch.
Show tool fields on `waiting` as well as `working`. All three consumers gated on
`working`, so a permission request rendered nothing at all; before/after of the
sidebar was pixel-identical. The rule now lives in one place (showsAgentToolPreview)
because a gate duplicated across three surfaces is a gate that drifts.
|
||
|
|
64de8dd637 |
fix(workspaces): delete on the confirmed host, and make both hosts' rows selectable (STA-4343) (#15013)
* fix(workspaces): host-qualified workspace deletion (STA-4343, STA-4448) Squashed integration of PR #14606 + the codex review-loop output, replayed onto current main. Granular history preserved on brennanb2025/sta-4343-review-full. Fixes the regression from #13413: a workspace id is repoId::path with no host component, so the same repo at the same path on two hosts published one id for two workspaces, and deletion routed by that id landed on whichever host routing preferred - usually the ACTIVE one, not the row the user confirmed. - removeWorktree takes a REQUIRED host-qualified WorktreeRemovalTarget; omitting the host is a type error. All destructive callers migrated. - Projections dedup on (host, id), so two hosts render as two selectable rows while the createWorktree/fetchWorktrees race duplicate still collapses. - Ephemeral VM cleanup is host-scoped. It matched on bare workspaceId, so the host-scoped delete path destroyed the SURVIVING host's VM and its unpushed filesystem - a leak fix that had become data destruction. - Selection, keyboard routing, lineage grouping and Space rows carry host identity end to end; fixing the executor dedupe alone would have turned one-row intent into deleting both hosts. Files split to stay under max-lines rather than raising any cap. * refactor: split files that crossed max-lines The review-loop commits used --no-verify, so the pre-commit hook never enforced the caps. Extracted cohesive units rather than raising any limit: renderer teardown, delete-with-toast, pinned-group rows, host-scope helpers, workspace-kind predicates, filter actions, kanban drag selection, the renderer removal result type, and the native-chat persistence tests. * refactor(workspaces): extract cleanup deletion-phase selector Clears the last max-lines violation and the import-type side effect the changed-code gate flagged. * refactor(sidebar): track the delete-dialog extraction modules * fix(workspaces): preserve host identity across remaining surfaces * fix(sidebar): re-carry host through the rewritten palette result model #15170 replaced PaletteSearchResult while this PR was open. Re-applied the host qualification on top of the new model instead of taking either side: results carry worktreeHostId again, and the board filter keys its matched set on host identity rather than the bare id. Known gap, documented in the board test rather than deleted: searchWorktrees resolves evidence through a `documents` map keyed by BARE worktree id, so two same-id host rows collapse before this code sees them. Closing that belongs with the palette work. * test(cmd-j): pin the palette collision gap instead of asserting the old model The palette collision test asserted two host-qualified rows, which #15170's rewrite made unreachable: item ids are bare again and worktreeMap is id-keyed. Rewritten to assert what holds — activation always names a host — and to pin the defect it exposes: two same-id rows render on ONE command value, so React sees duplicate keys and a click on the first row activates the second row's host. That reproduces on main, so it is pre-existing, not from this PR. Pinned rather than deleted so fixing it must update this test. --------- Co-authored-by: QA <qa@local> |
||
|
|
1412ae2d91 |
Revert "fix(terminal): inset the grid inside the xterm surface (#14583)" (#15181)
This reverts commit
|
||
|
|
1a04d292b6 |
fix(agents): lift the pane retirement fence when a live PTY re-attaches (STA-4114) (#14624)
* fix(agents): lift the pane retirement fence when a live PTY re-attaches (STA-4114) A detach/reattach cycle retires the pane on both sides — the main hook server's closedAgentStatusPaneKeys and the renderer's recentlyRetiredAgentStatusPaneKeys — and nothing ever cleared either one. The pane then rejected every later working/done event for the rest of its life while Pi kept running normally in the same PTY. Bind the fence to the fact it asserts: retirement claims the pane is gone, and binding a live PTY to that exact pane disproves it. Clear both tombstones at the spawn/attach chokepoint and at the daemon-backed reattach path, so recovery does not depend on the agent starting another turn — a pane re-attached mid-turn only has agent_end left to report, and one re-attached while idle emits nothing at all. Closed-tab tombstones are a separate, stronger claim and are deliberately left standing. * test(agent-hooks): re-arm the idle re-attach test against a turn-boundary fix The idle re-attach assertion posted only before_agent_start, which #14626 turns into a fence-lifting turn boundary. Under that change the test passes whether or not restorePaneAuthority runs, so it stops pinning this PR's mechanism. Assert first on agent_end — a non-turn event — so the test proves the fence was already down when the hook arrived. Verified: with restorePaneAuthority neutered AND before_agent_start added to the restart predicate, the old assertion passes and the new one fails. * fix(agents): lift a retired pane's whole fence, aliases included (STA-4114) Retirement fences the pane, its resolved owner, and every alias of it, then deletes those aliases. Restoring only the key handed to us left the rest standing — and a detached pane's process keeps posting the key it launched under (server.ts:1614), so the canonical re-attach case stayed suppressed with the fence apparently lifted. Verified against the real omp binary: the row came back under the stale launch pane instead of the detached owner. Record what each retirement fenced and replay it as a unit, rebuilding the aliases it deleted. Keys and aliases belonging to a closed tab are skipped, so the stronger claim survives and a live process is never routed back into a closed tab. The record is indexed by every fenced key and bounded at 1024 like the maps it mirrors; an evicted record degrades to the old behaviour. Also records why the renderer's restore IPC is deliberately unguarded: that map is not a mirror of main's (retirePtyAgentLaunchAuthority fences main directly on command-finished and PTY exit, and nothing pushes it back), and it is per-window and non-persisted, so gating the send on a local tombstone reintroduces this bug for exactly those panes. |
||
|
|
a963a7f462 | test(folder-workspaces): await routed update admission (#15179) | ||
|
|
a3a2c44edf |
Split browser pane (#14861)
* refactor: split BrowserPane.tsx under 400 lines * rm plan * refactor(browser-pane): reorganize into lifecycle folders Cut/paste + import rewrites only; no intentional behavior change. - annotate/, assemble-chrome/, host-guest/, navigate/, stream-remote/, describe-page/ (foundation sink, zero outgoing edges) - BrowserPane.tsx is now a pure re-export barrel; its component body moved verbatim to assemble-chrome/browser-workspace-pane.tsx so no dest file imports the barrel - browser-runtime.ts -> describe-page/live-browser-url-registry.ts (banned name; relocating the contract collapsed the host-guest/navigate mutual pair) - repath browser-pane test paths in config/reliability-gates.jsonc * refactor: sync addressBarValueRef with useEffect Move ref synchronization into useEffect hook with proper dependency tracking to ensure the ref updates are handled through React's lifecycle. Consolidate related imports from browser-page-types. * refactor(browser-pane): fix React lifecycle and external store patterns - Replace local state + effects with useSyncExternalStore for external subscriptions (draw hint, address bar, slot viewport) - Fix React StrictMode double-invoke issues in pointer handlers and state updates - Add keyboard navigation to context menu (arrows, Home, End, Escape) with focus management - Improve error handling for mobile driver reclaim and grab action IPC failures - Add test coverage for BrowserFind session flags, keyboard behavior, viewport lifecycle - Remove react-doctor/no-adjust-state-on-prop-change lint disables (root causes now fixed) * i18n: extract grab and download UI messages Move hardcoded toast notifications and error messages to translation system for both grab annotations and file drop handling. Also apply lazy initialization to address bar value and remove duplicate event recording. * fix(browser-pane): stop mutating refs during render React Doctor fails static analysis when refs are written in render. Mirror latest values in useLayoutEffect, and read the current page id from the latest grab callbacks. * fix(browser-pane): drop unused grab-mode exit dependency exit already reads the page id from a ref, so listing browserPageId trips the changed-code exhaustive-deps gate. * test(e2e): hide the window when Linux minimize is a no-op Xvfb has no window manager, so BrowserWindow.minimize() never sets isMinimized() on the frameless Linux CI window. Hide still occludes the guest compositor so restore coverage can run. |
||
|
|
2aebcfe288 |
Improve cmd j search keyword match (#15170)
* Implement multi-keyword palette matching with evidence-based ranking Replaces the single-match-per-field scoring with a comprehensive matcher that: - Validates token coverage across multiple query keywords - Normalizes Unicode text consistently across all sections - Classifies matches by quality for cross-section leadership - Supports evidence-based matching with hidden supporting fields - Includes typo matching for letter-only words - Performance-gated against a synthetic corpus of 800+ candidates Result structure now carries match ranges per field (not per row), quality class, and document rank so sections can compare relative strength. This enables worktree/open-tab/intent section ordering based on match intent rather than hardcoded defaults. * Improve cmd-j palette selection after deferred query commits Instead of clearing selection when the deferred query commits, intelligently select the next available item using the standard selection logic. Also remove unnecessary array index from React key generation to prevent spurious re-renders. |
||
|
|
32ee3b0536 |
reland(browser): route every cookie-import write through CDP identities, and never clear what it will not write back (#15030)
* reland(browser): restore CDP-identity cookie-import writes (#14729) Reverts the revert |
||
|
|
4a6de51ad8 |
fix(native-chat): enforce each pending send's own boundary in glue matching (STA-4477) (#14935)
* fix(native-chat): enforce each pending send's own boundary in glue matching Glue matching filtered candidate rows against the OLDEST still-open send and then matched the entire open queue against them. A prompt queued after a glued row landed could therefore be judged "already delivered" by that older row and pruned — the queued prompt disappeared with no bubble and no transcript turn. Each send now carries its own transcript boundary into the match: `gluedCandidateRows` tags every candidate row with the set of pending indices it actually landed after, and the matcher stops a run at the first send the row predates rather than skipping over it (adjacency is what makes a row glue). Exact single matches still belong to the occurrence path, unchanged. `native-chat-pending.ts` sat at 299 of its 300 effective-line budget, so the slash-command marker cache — a separate rule that never took part in pending pruning — moves verbatim to `native-chat-command-marker.ts`. Pure move: no behavior change, imports only. (max-lines is never bumped or disabled.) Refs STA-4477. Original PR #14663. * test(native-chat): cover the glue adjacency break and unmask the render path The `break` on a send the row cannot represent is the fix's central semantic choice, and swapping it for `continue` was passing the whole suite: nothing exercised a queue whose middle send is unrepresentable. Add that case. The mixed-age case also asserted both call sites in one `it`, so a prune-path failure masked the render-path assertion — and the render path is the one that makes a queued bubble visually vanish. Split it. Skip the per-send boundary scans when fewer than two sends are open: the glue matcher already returns nothing there, so a lone queued echo was walking the transcript twice per render for a discarded result. * fix(native-chat): migrate the live-session benchmark off the renamed glue exports Renaming the glue matcher's exports left this caller behind, and it crashed at runtime after printing six result rows: TypeError: matchingNativeChatUserTexts is not a function No gate caught it. config/scripts/** is in no tsconfig include and the file is not a *.test.ts, so neither typecheck nor vitest ever loads it. The empty-pending arm passes no pending sends, so the matcher takes its empty-queue exit without ever reading the rows — which is also why the renderer skips candidate-row construction entirely in that case. Escaping the row scan directly keeps what this arm actually measures identical to before, rather than fabricating per-row boundary sets that no production path builds. |
||
|
|
646e9b692b | fix(mobile): render pairing QR at scanner-safe scale (#15058) | ||
|
|
a1cd7eaa7e |
refactor(terminal): redesign quick command dialog with expanded layout (#15011)
* refactor(terminal): redesign quick command dialog with expanded layout a - Enlarge dialog to 52rem width and add scrollable content area for improved editing experience - Restructure textarea with header (label, status badge) and footer (hints, controls) - Compact action toggle to use shorter labels with grid layout - Move append enter switch to textarea footer (compact mode) for terminal commands - Add scope summary to Advanced toggle when collapsed for quick reference - Update dialog description and add contextual hints for user guidance * fix(terminal): restore defaultAdvancedOpen on quick command dialog Settings still opens the Advanced section from settings; the redesign dropped the prop and failed typecheck. * Improve accessibility of terminal quick command dialog - Add inert attribute to prevent keyboard/screen reader access to hidden advanced section - Add aria-label to textarea for clearer form field labeling - Extract label text to variable to avoid duplication and ensure consistency * refactor: remove status badge from quick command header Simplifies the dialog header layout by removing the "sent to agent" / "runs in terminal" badge and its associated translation strings. |
||
|
|
7ae6aedc02 |
fix(codex): stop a transient filesystem error from logging out the active account (#15046)
* fix(codex): stop a transient filesystem error from logging out the active account A single unreadable read of a managed Codex home's ownership marker cleared the user's active account selection, permanently. On Windows any exclusive lock — Defender real-time scanning, a backup agent, a sync client — makes every read of that marker fail with EBUSY, and the background rate-limit poll runs every 15 minutes plus once at every app start. Root cause: the ownership gate answered two very different questions through one channel. "This home is not ours" (a successful observation that failed a trust check) and "we could not read it" both surfaced as a throw, which the caller flattened to null, which three call sites took as proof the home was untrustworthy and wrote activeCodexManagedAccountId: null. Refusing to USE an unverified home is correct. Erasing the user's account selection because a file was briefly locked is not. The gate now returns a tri-state verdict. `untrusted` comes only from a proven trust failure or a definitive ENOENT/ENOTDIR where absence is itself the verdict; every other filesystem exception is `indeterminate`. Only `untrusted` may touch persisted state. Because `null` already meant "fall through to the system default" on both the launch and poll paths, not-clearing on its own would have run a DIFFERENT account behind a UI still showing the selected one. So the refusal needed real channels rather than a sentinel: - the poll returns an explicit skip; returning null would not have skipped at all, since the fetcher maps null to ~/.codex and would have spawned a token-refreshing app-server inside the user's real credential home - pane launch throws a typed temporary-unavailability error that both PTY implementations convert into a clean refusal with a retry message, including the re-resolution after the async auth-readiness wait - automatic session resume resolves the selected home eagerly, so an unreadable account can no longer be silently replaced by another one in the ranking - config-sync status reports a distinct managed-home-unavailable stall instead of "synced", with a bounded renderer retry so it clears on its own Also fixes the ticket's second symptom. The status bar's Sign in button called a re-auth that captured the selection before login and restored it after, so re-authenticating a deselected account restored `null` — a successful login that left the account inactive, with no success toast to distinguish it from failure. It now activates the account it just signed in, but only when the pre-login selection was empty, so it cannot silently switch accounts for multi-account users, and it runs the same restart prompt an explicit switch does. No retry or grace window inside the synchronous gate: it runs on the Electron main process in a loop over accounts, so a sleep there would freeze the UI. Recovery is simply the next readable evaluation. The WSL lane has the same class of defect, including one path that deletes a credential mirror. It is pre-existing, unreachable from these host code paths, and deliberately left for its own change; the host clearing sites cannot reach a WSL account because getSelfContainedManagedHostAccount excludes them. Fixes STA-4422 * test(codex): cover pending reset home ownership |
||
|
|
47a53b694b |
fix(ui): ignore IME composition Enter in the comment composer and replace field (#15057)
The keydown that exits a CJK composition carries isComposing: true (UI Events 3.6.5, row 5) and, when the IME is processing key input, keyCode 229 (7.3.1). Two handlers acted on it: - right-panel-comment-composer: Cmd/Ctrl+Enter posted the comment while the last syllable was still composing, so it went out truncated. - RichMarkdownSearchBar: Enter ran replace-current, mutating the document from a keystroke aimed at the candidate list; Escape closed the bar instead of letting the IME cancel the composition. Same in the find field. Guard all three with the existing isImeCompositionKeyDown helper, matching the rename and title inputs already on it. The held modifier does not change ownership: a composing Ctrl+Enter is still the IME's keydown, which is why useImeEnterGestureOwnership also owns it and only lets the post-compositionend redispatch chord through. Co-authored-by: BaeTab <bhwoo48@gmail.com> |
||
|
|
876e5b88a4 |
fix(ime): scope the composition route and deferred newline to owned sessions (#15056)
Two ways a terminal composition went wrong, both from state that was not scoped to the session it belonged to. The route called preventDefault() before checking whether it owned the session. The patched xterm treats that cancellation as "someone else will deliver this" and skips its own triggerDataEvent, so a route installed mid-composition — the connection effect re-runs, a reconnect swaps the transport, StrictMode remounts — suppressed the insertion and then returned without delivering anything. The commit vanished. Ownership is now decided first; preventDefault stays for sessions the route does own, including the ones it deliberately drops after a transport swap, since that drop is its decision to make. Pending-composition state was a bare per-element count, so a waiter could only ask "is anything composing", not "is what I was waiting for still composing". A composition the user starts after pressing Enter is behind that Enter, not in front of it, but it kept the count non-zero and held the newline anyway — `한` Enter `글` reaching the terminal as `한글\n`. The count is now reference-counted per session id, and the deferred send snapshots the sessions open when it starts and waits only for those. Reference counts rather than a set, so two overlapping routes owning the same session cannot clear each other's pending state. Co-authored-by: yeongjunyoo <47925973+yeongjunyoo@users.noreply.github.com> |
||
|
|
6387e5b8d3 |
fix(folder-workspaces): keep the broken-folder marker when a host sends a new reason (#15027)
FolderWorkspacePathStatus is cast, not decoded, off the runtime RPC wire -- runtime-rpc-envelope declares result: z.unknown(), so unwrapRuntimeRpcResult hands back whatever the host sent. Both title and description switch on status.reason with no runtime guard, so a newer host publishing a fifth reason matched nothing and returned undefined. FolderPathStatusIndicator's `!title` check then dropped the whole indicator, and a broken folder workspace rendered as healthy -- worse than the blank toast #15002 fixed, because there the warning was empty and here it is gone. Guard before each switch, the shape #15002 landed. A default: arm is not available: the type-aware config sets allowDefaultCaseForExhaustiveSwitch:false and rejects one with switch-exhaustiveness-check. Extract that guard into isHandledWireDiscriminant instead of hand-writing a third and fourth copy, and move #15002's two bespoke guards onto it. It takes unknown and checks typeof before Object.hasOwn -- hasOwn coerces its key, so a host that widened the field to an array sends ['missing'], which a hasOwn-only guard admits before the switch drops it straight back out. That was the P1 found in review on #15002; one implementation makes it structural instead of tribal. An unrecognized reason gets its own copy rather than reusing 'unavailable'. The unavailable remedy -- "Check the runtime or SSH connection and try again" -- is a false lead here: the host did check and reported the folder unusable, so retrying and inspecting a healthy connection wastes the user's time. Update Orca is the real remedy. Adding a fifth reason still fails typecheck in two places: TS2741 on the Record and TS2366 plus switch-exhaustiveness-check on both switches. |
||
|
|
7c798907c5 | fix(skills): harden cross-host bundle installs (#15000) | ||
|
|
453237cc57 |
fix(terminal): render the row tail the IME preedit overlay covers (#15014)
* fix(terminal): render the covered row tail inside the IME preedit overlay Closes #12545. Composing mid-line hid the character at the cursor for the whole composition. The preedit overlay is an opaque box anchored to the cursor cell, and nothing reaches the pty while composing, so those cells still held their characters — the box simply covered them. `CompositionHelper` now draws the rest of the row after the preedit inside the view, so the composition reads as inserted text pushing the tail right. Four details come with it: - The view is start-anchored while it carries a tail, so the preedit stays put and the pushed tail clips at the right edge; alone, `rtl` still keeps a long preedit's end in view. - It is themed from `options.theme` instead of the stock `#000`/`#FFF`, with any alpha dropped — the view masks the cells it draws over, so a see-through background would re-expose the very characters the tail stands in for. - The helper textarea syncs to the preedit's own bounds, so IME candidate dialogs anchor to the composing text rather than past the rendered tail. - A TUI can repaint the row under an open composition, so `updateCompositionElements` — which already runs on every render — re-reads the remainder and re-renders on change. A string compare adds no layout read. The tail is read with an explicit end column: the cacheable form of `translateToString` arms the line string cache's self-renewing idle-clear timer, and the composition path must own no timers. Geometry is not the cause. Two mature reference terminal implementations compose marked text into the grid rather than into a floating box, and both still blank the cells under it — one of them literally substitutes the marked characters into the row's character array before rasterizing. Moving off the overlay would not have fixed this report; rendering the covered tail is what does. The e2e arm asserts the invariant an opaque overlay owes the grid: it must render every committed cell its bounding rect covers. That is measured from the real rect against the real cell grid, so it fails on the unfixed build with `covers "하" / renders "가"`. Known limitation: the rendered tail is plain-styled while composing (theme foreground on theme background, no per-cell colors); colors return on commit. This is inherent to the overlay, and drawing the preedit into the cell renderer instead would be a far larger change. Co-authored-by: rayim <rayim@fxy.global> * test(e2e): assert the occlusion invariant, not the runner's cell width CI covered four columns where this machine covers two — 34.4px over an 8.43px grid against 12.3px over an 8px grid — so pinning the covered text verbatim pinned the font metrics rather than the behaviour. Assert instead that every committed cell the overlay covers appears in what it draws, which is the actual invariant and holds at any cell width. Still fails against main: covers "하" / renders "가". * fix(terminal): keep the rendered tail's spacing on the grid The composition view is white-space: nowrap, which collapses runs of spaces exactly like normal — it only suppresses wrapping. So a committed tail carrying padding drew its trailing glyph cells left of where the grid has them: measured in Chromium with xterm's own rule, twenty spaces plus a border rendered two cells wide instead of twenty-one. The visible case is Orca's most common IME context — composing inside an agent TUI input box, where the row is a prompt, padding, then a real border glyph the trim cannot drop. A stray border appeared a cell after the preedit while the real one stayed put. xterm sets white-space: pre on its grid rows for this reason; the view was only nowrap-safe while it held preedit text alone. The existing fixtures are all space-free, and the e2e invariant is that the overlay renders everything it covers — collapsing makes it cover less, so both stayed green. Pinned with a padded-row fixture. --------- Co-authored-by: rayim <rayim@fxy.global> |
||
|
|
b279f66c96 |
Include descendants in Pinned section when parent is pinned (#15035)
Descendants of pinned parents now appear in the Pinned section without individual pins. When an ancestor's pin state changes, reveal the active descendant to maintain focus and visibility. |
||
|
|
3bb87ff93b |
reland(shell): one portable Unix startup dialect, with both revert causes fixed (#15018)
* reland: portable startup-shell dialect, with the two revert causes fixed Relands #14863 (reverted by #14975) with fixes for both regressions the revert cited. 1. History GC deleted folder-workspace shell history. The live set was built from `getAllWorktreeMeta()` alone, but a folder workspace's PTY carries `folder:<id>` as its worktree id, so every live folder workspace looked orphaned. `getKnownWorktreeIdsForHistoryGc` now unions in `getFolderWorkspaces()`. Both consumers — the history-directory prune and the fish-history sweep — read that one set, so the fix covers bash, zsh and fish history alike. The directory prune had this gap since #1524; #14863 only widened its blast radius to fish files. 2. A copied Codex resume command aborted under `set -u`. Its leading clear statement has to test `$fish_pid`, and that unbound expansion takes the whole line — including the agent launch — down with it. Copied text runs in a shell Orca never spawned, so nothing can seed that variable first. The removal now rides on the agent itself as `env -u`, which needs no shell syntax and no expansion. Verified byte-identical under `set -u` in sh, bash, zsh, dash, ksh and fish. `env` cannot run the `cd` builtin, and a child `cd` would not move the agent, so the prefix is placed on the agent rather than on the whole `cd … && agent` chain. cmd and PowerShell have no nounset hazard and keep their clear ahead of the `cd`, which preserves `cd … && agent` — a failed `cd` still cannot launch the agent in the wrong directory. * fix(history-gc): stop three more paths from deleting live shell history Found by adversarial review of the reland. All three are the same class as the bug that caused the revert: a live set that is missing a category of real workspace, so the GC reads it as orphaned. 1. Profiles. The history root is `userData/terminal-history`, which has no profile segment, but the Store the GC consults is per-profile. So after a profile switch the live set condemned every other profile's history — and fish history, which lands in the user's own fish data dir, is shared by every profile on the machine. The live set now unions in the inactive profiles' worktrees and folder workspaces, read from their data files. A profile whose ids cannot be read reports the empty set rather than one that condemns real history. 2. No empty-set guard on the tree scan. `sweepOrphanedFishHistoryFiles` refuses an empty live set because it cannot be told apart from a store that failed to hydrate; the directory scan, which deletes more, had no such guard. A store that fell back to default state would have taken every worktree's bash and zsh history with it, across all roots including WSL. Four existing tests passed `new Set()` and relied on "empty means everything is orphaned" — exactly the behavior being removed — so they now pass a real live set. 3. Relay fish history. The relay isolates its history tree under its own root but wrote fish history into the shared fish data dir under the desktop naming, keyed by the CLIENT's worktree ids. On a machine running both Orca and a relay host, the desktop sweep deleted remote sessions' history once it went stale. Relay files are now `orca_relay_<hash>`, which the sweep's pattern deliberately does not match; the relay still deletes them by exact name when the worktree goes away. * fix(resume): enforce the env-removal invariants instead of documenting them Both found by adversarial review; both were unreachable from today's callers and silent if reached, which is exactly how they would survive to a caller that does reach them. - A pinned CODEX_HOME and the removal named the same variable, and `env -u` strips what the assignment just set — so the agent would have resumed against the real home and not found the session. The removal list now excludes any name the prefix pins, keeping the assignment authoritative as the old `clear…; CODEX_HOME=x agent` ordering did. Same fix in the git-bash twin. The PowerShell branch already clears before it assigns, so it was never affected. - Placement was keyed on the platform while the grammar it selects is keyed on the shell, so `platform: 'linux'` with `shell: 'powershell'` emitted POSIX `env -u` into a PowerShell line. PowerShell now routes to the PowerShell builder whatever the host, and the POSIX/cmd split below asks the shell rather than the platform. |
||
|
|
cc74436b3c |
fix(skills): keep an unanswered skill root's last known skills instead of reporting none (#15015)
* fix(skills): keep an unanswered skill root's last known skills instead of reporting none
An aborted or shed root scan degraded to `{skills: [], unavailable: true}`. Every
consumer derives "installed" from the skill list, so an unreadable root read as
proof the skill was gone and an already-installed skill offered Install again.
- discovery: retain the last answered scan per root (5 min, LRU-bounded, dropped
on install/update invalidation and when a root answers as absent) and serve it
when a later scan goes unavailable.
- source inventory: an unanswered root reports `exists: true`, so check
`skippedReason` first rather than presenting it as a successful scan.
- useInstalledAgentSkills: when an in-scope root did not answer and the skill was
not found, say the status may be incomplete instead of a bare "Not installed".
* fix(skills): do not let install verification accept an unanswered root's retained scan
Retention makes discovery serve a stalled root's last completed scan, which is the
state before an install wrote. Verification read that as proof, so reinstalling a
skill into a root that stalled could report success without reading what it wrote —
turning a retryable false negative into a false positive.
A match now counts only when a root that actually answered reached the skill, so a
symlinked placement co-owned by a healthy root still verifies.
|
||
|
|
2fdaa10fd1 |
fix(terminal): give the composing-chord deferral an owner and a ceiling (#15017)
The chord held for a live composition waits with `fallbackMs: null`, so nothing but compositionend can end it. `sendTerminalInputAfterComposition` returned void, so nobody could stop it either: the two listeners it puts on the terminal element outlived the pane, and a later composition on that element flushed the stale chord. Return a disposer from the helper and put a sender in front of it that owns every pending chord, so blur and pane teardown drop them the way the Enter path already clears its state. The sender also bounds the wait — generous enough for a conversion candidate window, and it discards rather than sends, because a chord arriving mid-preedit is the corruption the wait exists to prevent. The Enter path needs none of this: its 200 ms fallback always runs, so its listeners cannot outlive it. Pinned so that stays true. Fixes STA-4476 |
||
|
|
d21f63e6fc |
fix(browser): keep cookie-import warnings readable from newer hosts (#15002)
* fix(browser): keep cookie-import warnings readable from newer hosts The import summary is cast, not decoded, coming off the runtime RPC wire, so a newer host can publish a warning code or undecryptable reason this build has never seen. Both switches then matched nothing and returned undefined, which rendered as a blank toast.warning(undefined). #14683 replaced the absorbing default: arm with case 'unknown' to satisfy switch-exhaustiveness-check, which forbids default: on an exhaustive switch. Guard before each switch instead: narrow the wire value against the handled set and route anything else to a generic message. The handled sets are keyed by the unions themselves, so a fourth member still fails typecheck here. * fix(browser): reject non-string cookie-import warning discriminants Object.hasOwn coerces its key, so the membership guards admitted any value whose toString() matched a handled variant. A host that widened reason to an array sends ['unknown'], which passed the guard and then fell straight back out of the switch -- the same undefined-to-toast.warning blank the guards exist to prevent. Take unknown and check typeof first, matching isTopLevelView and isTuiAgent. The Record membership sets still fail typecheck on a new union member. |
||
|
|
8ca4ed945e |
feat(terminal): report execution host and listing scope in terminal list (#14973)
* feat(terminal): report execution host and listing scope in terminal list `orca terminal list` returned rows with no host identity and no statement of what the listing covered, so a scoped listing that saw nothing read as "nothing exists anywhere" — an agent reported a live remote worker dead. Each row now carries an optional `executionHostId` derived from the PTY id (SSH and paired-runtime ids embed their owner), and the result carries an optional `hostScope` naming the hosts covered and the known hosts skipped. Both are surfaced in `--json` and in the human-readable CLI output, where an absent field renders as `unknown` rather than `local`. Both row builders route through one resolver, so the rule lives in one place. * fix(terminal): preserve unverifiable host scope * fix(terminal): fail closed on unverifiable hosts * test(terminal): name unverifiable scope explicitly * perf(terminal): keep graph hydration host scans narrow * fix(terminal): reject blank foreign host owners * fix(terminal): validate inferred inventory hosts * fix(terminal): preserve paired folder host scope * fix(terminal): keep inventory host inference typed * fix(terminal): disclose paired folder hosts |
||
|
|
fed6a7d4fd |
Rethrow update errors after lineage recovery attempt (#15008)
Previously silent failures are now rethrown after recovery attempts. Co-authored-by: m4air <m4air@m4airs-Air.localdomain> |
||
|
|
84784f5393 |
Split pull request page (#14853)
* refactor: split PullRequestPage.tsx under 400 lines Move the 5888-line PR page into nested domain modules under src/renderer/src/components/pull-request-page/ and leave a thin public barrel. No intentional behavior change. * rm plan * Improve React stability and remove manual ref caching - Stabilize React keys in CheckDetailsPanel using content fields instead of array indices to prevent unnecessary remounting - Remove manual ref-based entries cache in PRFilesCombinedDiffViewer, rely on useMemo dependency (diffEntrySignature) instead - Move sectionsRef assignment to useLayoutEffect to avoid render-phase ref writes - Refactor usePRFileSectionLoader to destructure args for readability * Improve PR page stability: add error handling and fix race conditions - Add error handling with user feedback (toast notifications) for comment submission, review comments, diff loading, and file view syncing - Internationalize hardcoded strings for PR state labels and error messages - Fix race condition in reviewer submission by using a ref-based guard instead of render-time state - Fix scroll restoration to avoid overwriting target positions with intermediate clamp values - Add effectiveRepoId parameter for proper repo context in review operations - Disable reviewer picker during submission to prevent concurrent requests * Improve PR page stability: add timeout and stable list keys - Add 45s timeout for diff loading to prevent indefinite hangs - Fix React list key generation for annotations/jobs using content-based keys with occurrence tracking - Refactor scroll position caching to properly handle mid-restore teardown - Replace interpolated error messages with full locale-specific strings for close/reopen actions * Fix PR diff viewer cache isolation and list key collisions - Changed list key generation from string concatenation to JSON serialization to avoid collisions with actual content keys - Added host-aware scoping to diff view caches so local and remote execution don't share entries - Optimized virtualizer keys to use lightweight revision counter instead of full serialized signature * Extract PR file state into entry-scoped hooks Replace manual state resets with custom hooks that automatically clear section heights and active section when switching PR entries. This prevents state leakage between files and simplifies the diff viewer component. Also validates the active section key exists before passing it to child components. * Improve PR page error messages, accessibility, and stability - Show actual error messages from failed operations instead of generic fallbacks - Add aria attributes for combobox/listbox patterns and proper option identifiers - Consolidate duplicate label/assignee update logic and fix event listener passive mode - Memoize GitHub source runtime to prevent stale closure in checks callbacks - Extract filled state badge tone for reuse and fix workspace attachment type - Guard textarea shortcuts against concurrent saves and add mention query test * Add explicit PR file content cache eviction Extract PRFileContentRequestArgs type and create evictPRFileContentRequest function to handle cache eviction explicitly. Call eviction on load timeout so retries fetch fresh content. Adds tests for cache behavior. |
||
|
|
8b6d0231b2 | feat(i18n): localize Automations settings and navigation to Korean (#15004) | ||
|
|
226cf88ba6 |
fix(terminal): inset the grid inside the xterm surface (#14583)
* fix(terminal): inset the grid inside the xterm surface (#13252) Padding X/Y was applied as start-edge container margin, so the cell grid stayed flush on the trailing edges and a fractional background opacity stacked a darker gutter around the viewport. Put the setting on .xterm so FitAddon insets both axes and the themed background fills the pad. * fix(terminal): normalize padding before fit * fix(terminal): align stored and fitted padding * test(terminal): lock padding before fit * fix terminal padding opacity compositing * fix live terminal padding backgrounds * fix(terminal): preserve source-over alpha blending * fix(terminal): restore WebGL alpha blending * test(terminal): complete hidden retention pane fixture |
||
|
|
5e9e38fa75 |
fix(agent-status): announce Claude turn complete while background work runs (#14580)
* fix(agent-status): announce Claude turn complete while background work runs Lead Stop/StopFailure already ends the turn, but resolveClaudePaneState keeps the pane working for subagents, background shells, and session crons. That erases the working→done edge that mints the completion banner: subagent turns notify late with a stale body, and shells/crons never notify at all. Stamp turnCompletedAt on the gated lead Stop, announce immediately from that row, and pair the later all-clear done to the same end time so it cannot double-fire or collapse consecutive turns onto the pinned stateStartedAt. Fixes #13245 * fix(agent-status): suppress stamped turn replays * fix(agent-status): notify paired clients at turn end * Fix late-paired completion notification arming * test(agent-status): make the notification-id test fail on the pre-fix ordering The stored row inherited the helper's default codex agentType while the event named claude, so agentSnapshotMatchesExplicitTitle dropped it and freshStoredAgentStatus was undefined — the assertion held under either side of the `??`. Name the stored row's agent so the pinned working row survives and the snapshot-first precedence is what the test actually pins. * Suppress stamped completion tail replays * Prevent cross-coordinator title replays * Preserve stamped tails across fallback signals * Keep remount replay state while sibling lives * Scope OSC turn stamp preservation * Forward paired host completion stamps * Deduplicate paired completion tails * Bind paired completion tails to their turn * Preserve paired completion tail ownership * Keep paired tail replay state across remounts * Seed paired recovery without replaying completions * Seed startup replay and release stale fallback dedupe * fix(notifications): preserve stamped OSC repaints * fix(notifications): retain paired client turn boundary * fix notifications module import safety * chore: keep main integration focused * fix: preserve completion re-enable boundary |
||
|
|
886dec1d2a |
fix(browser): report cookies an import could not decrypt (#14683)
* fix(browser): report cookies an import could not decrypt Supersedes #13193, which reported only the Windows v20 case. Nothing distinguished "decryption failed" from "no cookies present". A row that would not decrypt was folded into the generic `skipped` counter, and a profile whose rows all failed returned ok:true with importedCookies:0 and no warning — a green "Imported 0 cookies from Google Chrome." The two situations produce opposite result shapes and the worse one reported success. Attribute the cause at the point of failure, while the version prefix is still in hand, and surface it as one `cookies-undecryptable` warning carrying the reason. Covers all three known causes rather than one prefix: - app-bound-encryption: Chrome/Edge 140+ on Windows write `v20`, which only the writing browser can unwrap. The version gate is a FORMAT check (`/^v\d\d$/`), so v20 passed it and failed inside AES like corruption. - linux-keyring-unavailable: getLinuxEncryptionKey derived the v11 key from an empty password when both secret-tool lookups failed, so it never returned null and the "Could not access encryption key" guard was unreachable on Linux. - unknown: any other cause still warns instead of reporting success. Deliberately not a hard failure on Linux: Chrome falls back to the "peanuts" v10 key precisely when no keyring exists, so those profiles still import. Pinned by a regression test. Refs #13192, #14181 * fix(browser): attribute decrypt failures exactly and gate CBC by version Review-loop findings on the initial commit, all fixed here. - CORRECTNESS: v11 rows were attempted with the v10 key when the keyring was unavailable. AES-128-CBC is unauthenticated, so a wrong key that yields valid PKCS#7 padding was accepted — roughly 1 in 256 per row. Garbage values were written into the jar as real cookies, and because those rows counted as successes the warning this PR adds could never fire. Key eligibility is now explicit per version rather than implicit in key ordering. - CORRECTNESS: the CBC path returned an empty Buffer for a prefix-only value BEFORE checking eligibility. An empty Buffer is truthy, so an ineligible row counted as imported and reached the live-jar clear. Eligibility now precedes that branch and empty CBC ciphertext is rejected as malformed. - ACCURACY: a named cause reported the TOTAL failure count, so one v20 row plus one corrupt row claimed both failed to app-bound encryption. Counts are now exact per cause, with the remainder reported separately and a tie falling back to 'unknown'. Exact-count approach carried over from #13193. - The app-bound copy no longer dead-ends. It names the existing in-app file import without describing how to produce the file — Chrome has no native decrypted-cookie export, so concrete guidance would send users to an extension that can read their whole session jar. - Direct prefix edge tests carried forward from #13193. Repo-wide search found no second multi-key unauthenticated-CBC first-success site, so this pattern was one occurrence rather than a class. Co-authored-by: manuaudio <manuaudio@users.noreply.github.com> * fix(pr): preserve split worktree slice Remove the unrelated rollback of the worktree-slice split and its forbidden max-lines baseline addition from this cookie-import PR. * fix(browser): match the unknown decrypt reason explicitly CI's type-aware code-quality gate flagged the reason switch as non-exhaustive: the 'unknown' member was handled by `default:` rather than matched. Matching it explicitly keeps the behaviour identical today and makes the gate enforce the thing that matters — adding a new reason to the union now fails the switch instead of falling silently into a generic message that would not describe it. This gate is separate from `oxlint` and is not covered by running oxlint on the changed files, which is why it only surfaced in CI. --------- Co-authored-by: manuaudio <manuaudio@users.noreply.github.com> |
||
|
|
5e189d6081 |
feat(browser): add a WebAuthn account picker (#14687)
* fix(browser): prompt for WebAuthn account selection * fix(browser): scope WebAuthn cancellation to session * fix(renderer): keep WebAuthn render phase pure |
||
|
|
96b03bab4f | fix(terminal): align Cursor Agent IME preedit anchor (#14982) | ||
|
|
a324ee20d4 |
Reset terminal SGR state around restored output (#14700)
* fix(terminal): reset SGR around restored output * fix(terminal): preserve live replay styling * fix(terminal): ground dead reattach fallback |
||
|
|
85565a9302 |
reland(workspace): set project location from the create-worktree host picker (#14965)
* feat(workspace): reland set project location from the create-worktree host picker Relands #14868 (reverted by #14912) with a fix for the regression that caused the revert: setting a project location could change the path before Orca used it. The retarget-after-setup path read the raw store record to find a just-created setup, because the memoized picker options had not refreshed yet: useAppStore.getState().projectHostSetups.find( (candidate) => candidate.id === setupId && candidate.setupState === 'ready' ) That hand-rolls a second selection path that skips every rule the option builder applies — repo eligibility, ephemeral-VM and runtime-owned SSH host exclusion, and the one-setup-per-host dedupe whose own comment notes that resolveWorkspaceCreationTarget takes the first project+host match and ignores the rest. So the composer could be retargeted at a setup other than the canonical one for that host, pointing creation at a different location than the one chosen. Resolves through buildProjectHostSetupOptions against fresh store state instead, so the fallback and the steady-state picker agree by construction. STA-4547 * fix(workspace): sanitize the clone prefill and drop an abandoned set-location Review follow-ups on this PR. The "Clone from URL" prefill seeded the field with the verbatim `git remote` URL, which can embed a PAT (`https://x-access-token:ghp_...@github.com/...`). The clone then runs on the *target* host, writing that token into its .git/config — a credential the user never typed into this flow, now readable by anyone on a shared host. Strip it with the same sanitizer `getProvisionedRootRecipeRepoUrl` already applies to the ephemeral-VM recipe URL. Extracted to resolveProjectCloneUrlPrefill so the rule is directly testable. The dialog also stays dismissable while a submit is in flight, and an SSH clone is unbounded. A clone the user backed out of minutes earlier still called onReady, silently moving the run target and resetting start-from under a form they had since pointed at another host. Drop the result if the dialog went away. * fix(workspace): re-arm the abandoned guard on mount StrictMode runs mount/cleanup/mount, so latching `abandoned` on the first cleanup left it true for the rest of the session and permanently suppressed onReady — the app wraps its root in StrictMode. Reset it on mount. |
||
|
|
f070033156 |
Revert "refactor(shell): one portable Unix startup dialect instead of shell d…" (#14975)
This reverts commit
|
||
|
|
9c4627d1c6 |
Refactor: split GitHubItemDialog into lifecycle-organized modules (#14931)
* refactor: split GitHubItemDialog.tsx under 400 lines No intentional behavior change. * refactor: group github-item-dialog into lifecycle folders Reorganize the 50 flat files under src/renderer/src/components/ github-item-dialog/ into six lifecycle folders: load-item-details/ shared types, both caches, fetch/settle, state badge open-dialog/ dialog shell, headers, body, tabs, link copy discuss-item/ conversation tab, comments, composer, timeline edit-item-fields/ GH edit section, labels, assignees, status inspect-pull-request/ combined diff viewer, checks tab land-pull-request/ PR actions, merge menu, reviewers No intentional behavior change. All 50 files moved verbatim; the only edits are relative-import specifiers (sibling paths plus a depth bump for ../../../../shared) and the hardcoded module paths in the two source-boundary tests. Import graph stays acyclic: zero mutual folder pairs, no file importing 4+ sibling folders, no dest file importing the public barrel, and no per-folder index barrels. * refactor: split item references and improve diff-viewer remount logic - Break down full `GitHubWorkItem` props into discrete `itemId`, `itemNumber`, and `itemRepoId` in mutation and action functions to prevent over-memoization of callbacks and improve dependency clarity. - Extract `getPRFilesCombinedDiffSignature()` and use it as a component key to safely remount the diff viewer when the PR revision changes, replacing generationRef tracking. - Add `getKeyedCheckAnnotations()` and `getKeyedCheckJobs()` to generate stable, collision-resistant keys for check arrays that may contain duplicates. - Consolidate interpreter timeouts into a single `SPAWNED_INTERPRETER_TIMEOUT_MS` constant and apply it via describe options rather than per-test values. * refactor: improve github-item-dialog repo context and i18n coverage - Add repoId prop to ConversationTab for explicit repo context override - Internationalize UI strings in diff viewer and PR action components - Improve error handling with cache rollback and guard cleanup on sync failure - Enhance cache key validation for cross-window invalidation by repoPath - Add repository access validation before rendering diff viewer - Fix cross-platform issues: skip symlink test on Windows, normalize CRLF in test assertions * Refactor check button i18n key and update text - Replace hash-based key with semantic name for maintainability - Change button label to "Open in browser" for broader context |
||
|
|
17ef6ccce6 |
fix(terminal): clear the preedit overlay when an IME cancels a composition (#14758)
Backspacing over the last radical of a Cangjie composition empties the IME's marked text without reaching compositionend, and the vendored xterm CompositionHelper only dropped the overlay's `active` class there. The box stayed painted with whatever glyph it last held (#11951). Clear on the state rather than on the key, as native terminals do: an empty `compositionupdate` now hides the overlay instead of only ever showing it, and a key the IME swallows re-derives the preedit from the textarea once it settles so a composition emptied with no composition event at all is cancelled too. |