* fix(github-project): make linked PR numbers clickable
* fix: open linked pull requests at their exact URLs
* test: assert complete project relation cell text
* fix: clarify linked pull request controls and overflow
* Fix linked pull request focus clipping and overflow label
* Keep linked pull request overflow outside clipped values
* fix(github-project): center plain project cells and say what +N means
Drop the clickable linked PR numbers: the numbers stay plain text as
before. Plain text cells (repository, parent issue, linked pull requests,
sub-issue progress) now center vertically like the other cells, and the
linked PR overflow reads "+N more" instead of a bare "+N".
* fix(github-project): keep the linked PR "+N more" visible when numbers truncate
* fix(browser): close client-hosted Find on full navigation
* test(browser): trim client-hosted Find navigation coverage
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(emulator): match the requested AVD while waiting for boot
* test(emulator): trim AVD boot identity test to the regression case
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
A successful read of the default /mnt/ root stored nothing, so every confirmed running-distro
probe (e.g. the 2 s WSL transcript observer) launched another wsl.exe guest read per distro.
Any successful read is now remembered until the distro stops; a failed read still retries, and a
stop during a read leaves the restart to read again.
* perf(wsl): a held WSL spawn reads the running list once
The provider-level holds (daemon adapter, daemon-off local provider) accept a running-list read started within a 5 s lease, so they reuse the startup-target hold's probe instead of running wsl --list --running again. A noted stop still forces a fresh read; a read older than the lease probes again.
* perf(wsl): a held spawn's provider hold reuses only that spawn's own running read
Replaces the 5 s lease: the startup-target hold records a read anchor on the spawn's options and reads after it; the provider hold reads after the same anchor, so it reuses that spawn's read and nothing older. No anchor means a fresh read; a noted stop or wake still forces one.
* fix(wsl): cap how old a spawn's own anchored read may be at the provider hold
A daemon respawn or a same-session history lock can sit between the two holds, so the provider hold reads after max(anchor, now - 2 s).
The WSL rows kept for a transient running-list failure were stored per
scope key in a module-global map with no limit, so every visited scope's
rows stayed alive until a history delete or a restart. The map now keeps
the 4 most recently used scopes (carrying a scope over counts as use) and
spends no slot on scopes without WSL rows. Explicit deletes still clear it.
* fix(ci): drop duplicate grab-annotations type and split chat subscription perf test
* feat(browser): send compact annotation feedback; page details are opt-in
Annotations sent or copied to an agent now carry the page, element label,
intent, selector, source file, text and feedback. Tab id, viewport, bounds,
classes, nearby context, computed styles, DOM path and HTML move behind an
'Include page details' checkbox in the Send menu, which also drives Copy.
* refactor(browser): share the annotation Copy tooltip and simplify the details toggle
* refactor(browser): keep tray types with page types
---------
Co-authored-by: Neil <neil@stably.ai>
Fixes#34312. Takes over naufaldi/orca#1 by @naufaldi, with the main.css part dropped because #34932 already covers it.
- SshTargetCard and the reasoning chevron now use the scoped `[[class~='group/x']:has(:focus-visible)_&]` shape. Tailwind's `group-has-*` compiles to a non-subject `:has()` whose subject is `*`, and that rule made Blink restyle whole subtrees on every mutation. This change is the entire measured win: 19.4 → 1.6 ms, 12.5 → 3.6 ms and 8.0 → 2.4 ms per mutation.
- rich-markdown-editor.css drops a redundant `:has()` on the TOC header, as defence in depth.
- Adds a guard, check-has-restyle-scope.mjs. It compiles the renderer's Tailwind CSS, adds plain stylesheets and the listed vendor stylesheets, and fails on any non-subject `:has()` whose subject has no class, id or attribute. Three pdf.js rules are baselined after being measured as harmless. It runs in lint, check:changed and PR CI in about 1.5 s.
Co-authored-by: Naufaldi <naufaldi.rafif@gmail.com>
* perf(sidebar): scope live sidebar width to the drawers that read it
Writing --workspace-sidebar-live-width on :root every drag frame restyled the
whole document. Only the workspace board and agent dashboard sheets read it,
so write it to their SheetContent elements instead; a newly mounted drawer
picks up the current live width so opening mid-drag stays aligned.
* perf(sidebar): keep the live-width registry across HMR and stop it inheriting into drawer subtrees
- Registrations survive module re-evaluation; Radix keeps open drawers' refs stable, so they never re-register.
- @property inherits:false: only the drawer element itself reads the width, so its subtree no longer restyles per drag frame.
- Refresh the plugin-panel theme comment, which still described root writes during drags.
The 20ms real-timer assertion flaked under CI load (21ms on #35949). The
fake-timer test above it already proves the urgent write publishes within one
microtask while a scan is held, so the timing copy added no coverage.
* Support Windows native Antigravity account credentials
* Preserve Antigravity prompt arguments and sanitize Windows file failures
* Preserve Windows credential bytes and distinguish native read failures
* Import Buffer type in Windows security declarations
* Expose native credential bytes without a Node type dependency
* Let Windows agy launches keep shell ownership
* fix: declare Windows account messages and cover owner scope
* fix: resolve Windows account authority before snapshot projection
* fix: share Windows credential parsing with saved account reader
* test: type saved account identity lookup mock
* test: isolate native account fixture host detection
* test: reuse filesystem mock for container marker control
* test: preserve native credential contract failure diagnostics
* test: normalize private read expectation and cover Windows boundary
* fix(sidebar): keep agent rows ordered by their session start once history wraps
Sidebar agent rows are ordered by the session's first reported state, read as
`stateHistory[0].startedAt`. `stateHistory` is a rolling window capped at
AGENT_STATE_HISTORY_MAX that trims oldest-first, so past ~20 transitions that
row is a recent turn: the row's identity clock silently becomes an activity
clock and busy agents re-sort themselves on every turn.
Latch the session origin in `firstStateStartedAt` when the entry is built and
read it through `agentEntrySessionStartedAt`, keeping the history lookup as a
fallback for entries that predate the field.
* test(sidebar): cover row order through activity and retention
* fix(sidebar): preserve session row order through activity and retention
* test: complete paired terminal status fixtures
* fix: carry accepted session origins across paired rows
* test: cover paired origin work and mobile action parity
* test(mobile): expect sessionLive in the phone chat identity after main's start-a-chat change
* refactor(agents): extract the title name-anchoring vocabulary
Pure move, no behaviour change. The name tokens, display labels and the
bare-name/whole-title resolvers come out of agent-title-evidence.ts into
agent-title-name-anchoring.ts so more than one predicate can resolve a name to
an agent through the same implementation rather than by convention.
Nothing is added or reworded on the way out; the extracted functions are
unchanged and simply exported. Verified by the existing suites: the title,
identity and sidebar tests all pass untouched.
The identity ratchet moves with it — titleHasAgentName's two
parser-implementation callsites now live in the new module — so this commit is
green on its own rather than leaving the ratchet red until the next one.
* fix(agents): stop a foreign agent name in task text from taking a pane
A Claude pane's WORKING title is a bare braille spinner plus task text, so any
agent name inside that text resolved the title to a committed foreign identity.
The guard that fixed the mirror-image bug was written for the Claude label only,
so every other name short-circuited above it and took the pane from its owner —
the tab icon in use-tab-agent.ts, and the sidebar/dashboard row in
worktree-title-derived-agent-rows.ts, which never consulted the owner at all.
Make the rule agent-neutral in both directions: a title may only take a pane
from a known owner when it PRESENTS that agent, never when the name merely
occurs in free-form task text.
titlePresentsAgent sits beside collectAgentTitleEvidence and resolves names
through the vocabulary they now share. It differs from the parser in one
respect: the parser asks who a title names given no owner, so it must refuse
anything a task sentence could forge; this predicate is told which agent to
check, so it also accepts that one name in an identity POSITION — the whole
undecorated remainder, or the head of a `Name:` frame. The colon is what
separates `Codex: fix cursor offsets` from `Claude Code compare Opencode`.
resolveTabAgentFromSignals exists twice, already drifted in owner handling.
useTabAgent calls the copy in use-tab-agent.ts, which is what paints the tab
icon; every existing test imports the copy in tab-agent-from-signals.ts. Both
are fixed, and the new suite runs its whole contract through both so a future
divergence fails instead of hiding.
* fix(agents): let the title label map admit a missing label
Narrowing the map's value type to TuiAgent surfaced an inaccuracy that was
always there: noUncheckedIndexedAccess is off, so indexing Record<string, _>
types every lookup as a hit, and `label` is arbitrary text parsed off a terminal
title. The compiler concluded the `?? 'unknown'` fallback was unreachable and
the unknown-label comparison impossible.
The runtime disagrees — the map carries a fixed set of ~10 labels and misses
everything else. Deleting the branch would return undefined where the function
returns 'unknown' today, so the type is what is wrong. Partial<Record<...>>
makes absence expressible and leaves behaviour unchanged.
Pinned by a test, since nothing else forced the map to be honest about this.
* fix(agents): let an agent's own sigil still present it against an owner
titlePresentsAgent read only anchoredNames, so it missed the evidence class the
parser ranks highest: a vendor marker, the sigil an agent emits itself and task
text cannot forge. Gemini's four glyphs and Cursor's native literal are markers,
not anchored names, so every Gemini working, idle and permission title — and
Cursor's literal — failed the gate and the pane stayed on its previous owner.
Run Claude, let it finish, run gemini in the same pane: tab and sidebar both
stayed Claude. The bug this change exists to fix, mirrored.
Admitting the whole vendor class would have reintroduced #8940 the other way,
because Claude's '. ', '* ' and idle sigil are generic — OpenCode emits them
too — so they prove activity, not identity. hasGenericClaudeStatusPrefix already
modelled that distinction inside terminal-title-agent-type.ts; it moves to
agent-title-core.ts so both callers share the one definition.
Not isGenericClaudeStatusClaim, which is that model plus a clause for a
different question: it treats a 'claude' token as upgrading a generic prefix
into a real claim, which is right when asking whether a title may MINT Claude
identity and wrong when asking whether it PRESENTS Claude. Applied here it would
have let ". port the claude prompt" reclaim an OpenCode pane.
The predicate is now one memoized set per title rather than a memoized first
line and an uncached scan, so the common path — a title presenting nobody — no
longer re-runs the name matchers over every wrapper segment on each render.
* fix(agents): gate on one identity-frame grammar, ratcheted against the corpus
Two review rounds found the same class of defect: the gate was a second model of
"does this title present an agent", poorer than the model its callers use. Round
1 missed an evidence class, round 2 missed four whole ROUTES in a different
function. A third patch would have bought the fifth route.
getAgentLabel mints identity through routes the evidence parser cannot express,
because titleHasAgentName asks whether a name occurs ANYWHERE and never where.
One Codex branch therefore serves both 'codex working' and the mid-sentence
mention this change exists to reject. Route identity cannot separate them; the
gate genuinely needs position information the resolver discards.
So the models cannot be collapsed. What they can be is one GRAMMAR plus an
enforced agreement:
agentForIdentityFrame generalizes what isClaudeIdentityFrameSegment has always
described — a name, an optional status word, an optional '- action required' —
which was agent-neutral in shape and applied only to Claude. That was the same
one-way-guard mistake as the original bug, one level down. Generalized, it
covers every minting route: bare names, 'Codex: ...', em-dash frames, name plus
status, Windows launcher forms, '|'-headed titles and Pi's native format. The
positional and vendor-marker rules fold into it rather than stacking beside it.
terminal-title-pane-claim-corpus.test.ts is the enforcement. It sweeps the whole
classification corpus, and any title getAgentLabel still mints an identity from
must remain able to claim a pane. Shared code cannot enforce this because the
two models legitimately differ; the corpus can, and it names the offending title
when a future route is added.
Corpus: 14 lost claims before this commit, 0 after. One intended gain,
'claude.bat working', listed and explained in the ratchet: the old gate accepted
'aider.ps1 ready' for aider and refused the identical form for Claude purely
because the guard was Claude-only.
* fix(agents): stop reading a wrapper prefix as an identity frame
The head grammar treated '|' as a separator an agent puts after its own name.
Wrappers PREFIX the pane's title and the innermost title comes LAST
(terminal-title-wrapper-segments.ts), so a name before a '|' is a tmux window or
an ssh host, never the pane. `claude | zsh` on a Codex-owned pane therefore
handed it to Claude through all three consumers — this change's own bug,
inverted. Wrapper-joined titles already reach the real pane title through
getEvidenceTitleSegments' suffix splitting, which is what actually resolves
`ssh host | opencode ready`; the separator was not carrying that case.
'>' goes too. Pi's native `π > session` is matched before the head grammar runs,
so the only titles it affected were `name > text` breadcrumbs, which are closer
to task text than to an identity frame. That narrows `⠋ gemini > next steps` and
`⠋ codex > plan` relative to the previous gate; both are pinned as intended
rejections, since refusing leaves the pane with its known owner, which is the
safe direction on ambiguous evidence.
The corpus gain assertion is renamed to say it is corpus-scoped. Over every
string literal in the repo the change gains more than the one title this 67-title
corpus can see, mostly the same Claude symmetry, and the test should not read as
a global guarantee. Near-misses the grammar deliberately accepts are pinned too,
so the boundary is visible and moving it requires editing a list.
* fix(agents): admit identity frames before trusting terminal titles
* fix(agents): separate title identity from readiness policy
* Preserve title frame boundaries and normalization identity
* Preserve activity token whitespace at title wrapper boundaries
* test: expose typed mailbox idle evidence source
* test: scope title projection work contract to renderer
* test: distinguish task activity from agent identity
---------
Co-authored-by: Merge Sim <sim@local>
* Keep a chat's accepted pick in what it shows and sends
A pick held while a new chat is created is committed into the chat's
active option record once accepted, through the same step a normal pick
takes, and older options reads are dropped. The host also publishes the
chat's saved effort, Fast and speed as optional status fields so another
device's pick moves the option stamp and re-reads options.
* Keep chat picks ordered and capture displayed send selection
* fix: keep chat choices in one controller with accepted input capture
* fix: pass captured Codex model input to the turn request
* Keep structured chat option pickers usable during turns
* Preserve logical selection ownership across runtime transitions
* Consolidate selection lifecycle contract coverage
* Consolidate mobile structured send imports
* Preserve legacy action outcomes and opening message identity
* Consolidate repeated selection test setup and assertions
* Fix option reset admission and preserve uncertain write notices
* fix(chat): complete option contract fixture types
* test(chat): place affinity integration in cross-layer suite
* chore(browser): upgrade bundled agent-browser to 0.39.4
0.39.4 keeps refs stable across snapshots and refuses stale or hidden
click targets. Its replies now carry daemon lifecycle bookkeeping, which
is dropped where the JSON is parsed, and its tarball binaries lose the
executable bit pnpm's skipped postinstall used to restore, so the orcad
dev resolver now restores it like the desktop one.
* fix(browser): keep orcad's Chromium unbounded and read 0.39's stale-ref error
* fix(browser): import the shared stale-ref rule in orcad and keep the parser typed
* fix(browser): keep workers and frames running after the agent-browser helper leaves
agent-browser 0.39 asks auto-attached targets to wait for it before
starting. The proxy forwards that onto the guest's shared debugger,
which outlives the helper, so after the helper's idle exit nothing
resumed new workers or frames.
* feat(browser): flash the page viewport size while the pane resizes
Resizing the built-in browser pane gave no sign of the page's size, so
landing on a CSS breakpoint meant guessing or reading innerWidth in
DevTools. The existing zoom badge now also flashes the guest's
innerWidth × innerHeight after each resize, so zoom and size share one
corner instead of stacking. The size comes from the host box, UI zoom
and the guest's live zoom level, because a UI zoom change overwrites
the guest's zoom behind the pane's remembered level.
Refs #31014
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(browser): cover no size flash on tab open and tab switch
The issue promises the size badge stays hidden when a tab opens or is
shown again, but the e2e only checked it after the 1.4s fade had
already passed, so a stray flash would go unnoticed. A renderer-side
counter now records every size flash, and the hook test covers a pane
that is hidden and shown again at a new size.
Refs #31014
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(browser): cover the badge shared by zoom and size feedback
Zoom and size feedback now share one label and one fade timer, so the
latest change wins, but nothing exercised that hook directly. The new
test checks that a size flash replaces the zoom label and restarts the
fade instead of letting the earlier timer hide it early.
Refs #31014
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(browser): keep the size badge exact under Chrome's zoom presets
After rebasing onto per-site zoom (#31621), zoom levels snap to Chrome
presets such as 90%, so the badge drifted a pixel from innerWidth: the
zoom factor snapped levels that UI zoom sets freely, and rounding did
not match Blink. The factor now uses the guest's level as is, and the
size mirrors Blink's AdjustForAbsoluteZoom::AdjustInt on the integer
DIP size, checked against the e2e's measured guest sizes.
Refs #31014
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(browser): never show a size flash that may be wrong or stale
Before dom-ready the guest's zoom cannot be read, and a saved site zoom
lands at that moment, so sizing it at the default zoom could flash a
wrong number. A flash left running when the page went inactive or was
parked at 0×0 could also come back stale, since nothing measures it
then. Both cases now skip or hide the flash, because no size is better
than a misleading one.
Refs #31014
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(browser): announce the first resize after a tab opens
Skipping measurements until the guest zoom is readable also dropped the
baseline, and a new tab's opening measurement usually arrives before
dom-ready, so the first resize afterwards only set a baseline and never
showed the badge. The baseline is now kept as the host box size and
converted at the zoom readable when the next measurement comes, so no
zoom is guessed and the first real resize is still announced.
Refs #31014
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* test(browser): drop the size format case that only copied the string
The case asserted that the label reads exactly `1024 × 768`, which
restates the format string rather than any behavior, and main now
treats such copied-string tests as low value (#33736, #33778).
Refs #31014
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(browser): fold the size readout into its hook and drop tests that don't prove it
Moves the guest-size conversion into the hook that uses it, inlines the
one-use label formatter and predicate, and drops the e2e spec, the badge
timer test and the predicate tests. The conversion keeps its test against
sizes measured from a real guest.
* fix(browser): keep the size flash off under viewport presets and out of the pane's render
A preset pins the page size, and mobile emulation's layout viewport can
differ from the host box, so the readout only runs on a free-sized pane.
The badge now owns its label and fade state behind a ref handle, so a
resize drag re-renders only the badge instead of the whole page pane.
* fix(browser): skip the size flash while a phone or remote viewer sets the page metrics
* fix(browser): fold the screen scale into the size readout's rounding
Blink rounds the guest viewport in device pixels with the screen scale
times page zoom as its factor, so ignoring the screen scale left the
badge 1px off at most non-100% zoom levels on 2x screens.
* fix(browser): port Blink's exact zoom rounding to the size readout
Chromium 150's AdjustForAbsoluteZoom::AdjustInt adds 0.5 in float math
before dividing when zoomed in and truncates with a 0.01 slack, so the
+1 rule showed the badge 1px wide at many zoomed-in sizes.
* fix(browser): size the readout's guest frame in whole DIPs before the screen scale
Chromium rounds a guest frame to whole DIPs and only then scales it to
device pixels, so a fractional split width read one device pixel short.
* fix(browser): round the readout's guest frame up to whole DIPs
A live sweep at 1.2 UI zoom on a 2x screen showed Chromium sizes the
guest frame as the ceiling of the box in DIPs, not the nearest DIP.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Kelvin Amoaba <97001695+AmoabaKelvin@users.noreply.github.com>
* fix(browser): every web link in Orca follows the open-links-in-Orca setting
* test(browser): pin where links from Orca UI open
* fix(browser): keep the link-routing escape modifier and owning workspace on UI links
Credential and star links stay in the system browser; shared markdown keeps its owner-aware routing.
* fix(browser): hover cards and review toasts open links in their own workspace
API-key and console pages stay in the system browser; non-web hrefs are ignored like the system bridge does.
* fix(browser): UI link wrappers pass the click so the escape modifier applies
Share to X stays in the system browser with the other account-action pages.
* fix(browser): folder workspace PR-check links open in the folder workspace
* test(browser): name UI link tests after the path they exercise
* chore(browser): keep the link-open ratchet's helpers private
* fix(browser): narrow the Linear project toast link before routing it
* fix(browser): UI links route with this desktop as their source owner
Links from Orca's own UI no longer read the Active Server setting; the opener routes by the host workspace.
* fix(browser): UI links open under the host workspace's own owner
A remote workspace's browser gets the original URL, not this desktop's localhost label.
* fix(native-chat): read browser route eligibility from its owner module
Importing it through workspace-browser-tab-open pulled the store into every link button and formed an import cycle.
* feat(browser): add a shortcut to open a browser tab in a split to the right
* fix(browser): keep new-browser handlers void so the focus-read ratchet passes
* feat(browser): paste images into annotation comments and send their paths to the agent
* fix(paste): hand an image-only clipboard to native paste so Cmd+V reaches the annotation field
* fix(browser): let an open image preview keep Escape instead of exiting Design Mode
* test(paste): drop the ownership-event test the menu-claim test already covers
* refactor(browser): reuse chat preview lifetimes and the terminal's paste host for annotation images
* fix(browser): release pasted edit images when the edited note disappears
* fix(browser): refuse a 21st page annotation with a visible limit message instead of dropping the oldest
The store evicted the oldest note once a page held 20, so feedback vanished unseen. It now refuses the add; picking an element on a full page shows a limit toast instead of opening a draft, and the annotate banner states the limit.
* fix(browser): name the actions that free an annotation slot
* fix(browser): skip the limit toast for a selection loading already cancelled
* refactor(browser): one isBrowserAnnotationPageFull rule for the store, hook and banner
* refactor(browser): move the annotation limit message out of the grab hook
* fix(i18n): name the annotation limit placeholder
* fix(notes): name each agent in the Send notes to menu the way its sidebar row does
* chore(ratchet): prune the notes-send menu from the host-list render baseline
* perf(mobile): skip line stats when loading Source Control status
Mobile Changes rows never render per-file line counts, so the default
git.status line-stats pass (up to two `git diff --numstat` subprocesses
plus untracked file reads) was wasted host work on every load. Hosts
that predate the param strip the unknown key and behave as before.
Co-authored-by: zhangxaochen <3416677+zhangxaochen@users.noreply.github.com>
* test(mobile): re-record Changes-load goldens for the no-line-stats status read
Co-authored-by: zhangxaochen <3416677+zhangxaochen@users.noreply.github.com>
---------
Co-authored-by: zhangxaochen <3416677+zhangxaochen@users.noreply.github.com>
* fix(wsl): hold main's background WSL start in the shared preflight; folder launch scope honours the user's act
Main's background start (no wslUserAct) is now held in resolvePtySpawnStartupTarget, before the
startup cwd probe and any provider. A runtime start the user asked for (CLI, window, phone, split)
checks a WSL folder workspace's path as their act, so it may start the distro; background work is
refused with the stopped-distro message instead of folder_workspace_path_unavailable.
* fix(wsl): agent-session create and user resume check a WSL folder as the user's act; background holds with Start
* fix(wsl): agent-session ensure and a failed start's kept pane pass the user's act to the WSL folder check
* test(e2e): a prepared terminal.create dedupes as the caller's act
* refactor: colocate Jira cache and project selection owners
* ci: refresh Jira refactor validation after main test fix
* ci: refresh Jira owner move after main validation fixes