Collapse multi-line explanatory comment blocks into single-line "why" statements
per AGENTS.md ("Document the Why, Briefly"): drop restatements of the code and
mechanism narration; keep the non-obvious reason, external refs, and directives.
Comments-only — verified no code changed via a Babel/esbuild comment-strip
token-equality gate against origin/main; typecheck and oxlint clean.
Area: main — git, source-control, providers & integrations. 40 files changed, 1432 insertions(+), 4473 deletions(-).
Co-authored-by: Orca <help@stably.ai>
* Clarify PR panel guidance: classify errors and confirm-only composer
Replace the ambiguous GitHub hosted-review boolean with a four-state evidence
model (found/positive_unresolved/not_found/unknown) so "No PR found" never
appears without an accepted lookup result. Classify GitHub refresh failures
into types (rate_limited, auth, network, permission, repo_unavailable,
gh_unavailable, unknown) for stable, honest copy. Confirmed-only composer:
preserve drafts across transient failures; hide Create during hard errors and
positive-unresolved evidence. Hard errors clear only when an eligibility
request starts after the error and returns an accepted outcome. Propagate
error types and unified retry schedule through the store. Sync mobile parity
with shouldOpenChecksPanelCreateComposer gating. Localize all new copy.
* Clarify PR panel guidance: classify errors and confirm-only composer
Add reviewLookupOutcome to hosted-review eligibility and thread it through
the panel so it never claims "No PR found" without accepted evidence. A
failed lookup is unavailable, not a settled no-PR. Fail closed on positive
unresolved evidence, hard refresh errors, and unavailable lookups. Add
structured GitHub refresh-error classification with Retry-After parsing.
Implement confirmed-only composer gating based on fresh, matching-context
eligibility with hard-error clearing. Mobile gates on reviewLookupOutcome
to prevent false Create claims. Surface throwOnFailure variants for each
provider so transport failures cross the RPC boundary instead of collapsing
to null. (Design success criteria 1–4; invariant 8.)
* Add exec-error helpers for subprocess error classification
Extracts stderr/stdout parsing and Retry-After detection into a
lightweight module that can be imported without pulling in the heavier
runner machinery. Supports PR-refresh error classification and proper
rate-limit handling for gh commands.
* test(mobile): include reviewLookupOutcome in create eligibility fixtures
Create / Push & Create now fails closed unless the lookup is not_found.
Update mobile test fixtures so accepted-no-PR cases can still proceed.
* Add OrThrow mock variants to forge-provider test mocks
forge-provider resolves branch reviews via the OrThrow variant so
lookup failures surface as unavailable instead of "no PR found".
On a fork checkout (origin=fork, upstream=parent) PR details failed to load because getOwnerRepo() resolved only the origin remote while GitHub PRs live on the upstream parent. Make getOwnerRepo() prefer upstream (mirroring getIssueOwnerRepo), and pin the call sites that genuinely need the checkout's own origin identity (getRepoSlug, getRepoUpstream, createGitHubPullRequest, resolvePrWorkItemSource) to the origin-only primitive.
Lands the fix community-identified in #7332 and hardened in #7513.
Closes#7331
Co-authored-by: fsdwen <1214772+fsdwen@users.noreply.github.com>
Co-authored-by: brennanb2025 <79079362+brennanb2025@users.noreply.github.com>
* fix(github): attribute GitHub API outages instead of blank/"failed" states
When GitHub's API is unreachable (5xx outage, network, or rate limit), Orca
showed no PR data with no explanation, so it read as an Orca bug rather than a
GitHub-side problem.
- Add a shared classifier (classifyGitHubUnavailable) reused by the main
process and the renderer so every surface attributes an outage identically.
A live outage returns HTTP 5xx, which the PR-refresh classifier previously
had no branch for (fell through to the un-attributed "refresh failed").
- Right-sidebar Checks panel: show GitHub-attributed copy in the error
empty-state, plus an inline banner over stale cached PR data so an outage
doesn't look like a normal (silently out-of-date) panel.
- Tasks/PR-list page: replace the vague "N of M projects failed to load" with
a GitHub-attributed banner when the failure is a reachability problem.
Copy names GitHub as the source and reassures it isn't an Orca problem, with no
status-page link. Stays GitHub-scoped so GitLab/other providers aren't
mislabeled.
* fix(github): keep outage attribution accurate
* fix(github): preserve outage attribution edge cases
* fix(github): preserve Tasks outage attribution
* fix(github): avoid false outage attribution
* fix(runtime): tolerate absent browser certificate state
* fix(ui): preserve exhaustive optional state handling
* fix(github): preserve outage attribution for combined queries
* fix(github): preserve runtime failure attribution
* chore: restore unrelated UI files to main (out of scope)
native-chat-session-option-labels.ts and skill-freshness-group.tsx switch
tweaks were unrelated to GitHub API outage attribution — they fix pre-existing
switch-exhaustiveness lint on main, which this PR's CI (oxlint) doesn't gate on.
Restore them to origin/main so this PR's diff stays focused; the exhaustiveness
cleanup belongs in its own change. (sync-runtime-graph.ts is already identical
to main, so no diff there to revert.)
* fix(github): drop Orca self-reference from outage copy
* fix(github): drop em-dashes from outage copy
* fix(issues): replace cursor-based pagination with page-number Search API
Problem
=======
Issue pagination (#8649) had two bugs:
1. Pages 6-16 were unreachable — clicking page 16 highlighted page 5;
clicking 6/7 did nothing. The old cursor-based approach
(updated:<CURSOR) broke with Search API's relevance sorting —
pages after the first few returned no items even though more
issues existed.
2. Issue numbers appeared out of order on loaded pages (e.g. #1082
between #1308 and #1499), because client-side sort used
updatedAt instead of issue number.
Root Cause
==========
The pagination used two separate GitHub API strategies:
- Initial page 0 load: REST endpoints (repos/:owner/:repo/issues,
repos/:owner/:repo/pulls) sorted by updatedAt
- Subsequent pages: Search API with cursor (updated:<DATE)
These two sources returned items in different orders, causing items
to go missing or appear on wrong pages across page boundaries.
Solution
========
1. Unified on GitHub Search API for all pages — initial load and
pagination both use search/issues?q=...&page=N, eliminating the
REST-vs-Search inconsistency.
2. Changed from cursor-based (update:<DATE) to page-number-based
pagination (page=N), which the Search API supports natively.
3. Switched client-side sort from updatedAt to issue number
(sortWorkItemsByNumber), matching GitHub's default Issues view.
4. Parallelized page fetches in handleLoadNextPage — clicking page
16 now fetches all intermediate pages concurrently (~2s) instead
of sequentially (~30s).
5. Cleaned up dead legacy gh issue list / gh pr list code path,
extracted quoteForSearch helper, shortened overlong comments.
Files changed: 11 files, +140/-127 lines
Closes#8649
* chore: remove unrelated merge formatting
---------
Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
* Fix PR checks sticking to a stale linked PR after a terminal branch switch
A worktree's linked PR is a branch-scoped hint, but two refresh paths race
when a terminal switches branches: the git-status identity path clears
branch-scoped review links, while the worktree-listing path rehydrates the
new branch together with the stale persisted link and clears nothing. When
the listing lands first (the common case — worktree listing is much faster
than git status), the identity path sees no branch change and the stale
link survives. Every subsequent refresh then re-fetches the linked PR by
exact number, which ignores the branch, so Checks stays pinned to the old
branch's PR and the Refresh button cannot recover.
Two-part fix:
- Prevention: listing refreshes now route observed branch switches through
updateWorktreeGitIdentity before merging, so the existing link clear and
tombstone machinery runs no matter which refresh path wins. Gated on the
entry still carrying branch-scoped review context so a stale listing row
cannot roll back a newer branch identity.
- Recovery: PRInfo now carries headRefName, and a fetch that returns the
linked OPEN PR whose head branch matches neither the current branch, the
worktree push target, nor the worktree HEAD clears the durable link and
re-resolves by branch. Wired into both fetchPRForBranch and the
background refresh coordinator, mirroring the merged-PR divergence clear.
This also heals wedged workspaces persisted by earlier builds.
* Harden stale PR recovery across refresh races
* Avoid duplicate PR recovery refresh work
* Index linked PR refresh aliases once
* fix(github): pin work-item list ordering to updated-desc so cursor pagination reaches every page
The Tasks page paginates work items with an updatedAt cursor
(updated:<oldest-item), but the underlying gh calls never pinned a sort:
'gh issue list' defaults to created-desc and '--search' defaults to
best-match. Items created long ago but updated recently therefore never
appeared on any page — page 0 (created order) skipped them and every
later page excluded them via the cursor — so the pager advertised pages
the fetch chain could never reach, clicks on them clamped to the last
real page, and cross-page ordering was scrambled.
Append sort:updated-desc to every list/search invocation so the fetch
order matches the cursor field on the first and all subsequent pages.
Verified against a live 588-issue repo: the cursor chain previously
died around page 5; it now traverses 585/588 unique issues (the
remainder is the pre-existing strict '<' boundary edge for items
sharing the cursor's exact timestamp).
Fixes#8649
* fix(github): make work-item cursor pagination lossless at updatedAt boundaries
Builds on the sort-pin fix: switch the pagination cursor from strict
'updated:<' to inclusive 'updated:<=' so items sharing the boundary row's
exact updatedAt are no longer skipped between pages (the residual 3/588 edge
in #8649).
The inclusive bound re-fetches the boundary rows, so dedupe them by repoId+id
(a bare item.id like 'issue:9' collides across repos). Extract the page
accumulation out of the 12k-line TaskPage component into a pure, unit-tested
helper (accumulateWorkItemPages) that dedupes and backfills: it accumulates
fresh rows across fetches and emits uniform pageSize pages, so deduped pages
never shrink below the size totalPages (count / effectivePageSize) assumes —
which would otherwise strand the tail items and break the no-count degraded
pager.
Also hoist the updated-desc ordering into a named WORK_ITEM_LIST_SORT_QUALIFIER
constant so the cursor's ordering contract has one home.
Tradeoff: when per-repo fetch size equals pageSize, the boundary dedupe costs
one extra fetch per page; acceptable for interactive pagination and bounded by
the gh rate-limit guard. Persisting the cursor/buffer across calls is a
possible follow-up.
---------
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
* fix(source-control): route GHES remotes to the GitHub provider for PR creation
A GitHub Enterprise Server user could not submit a PR — Orca demanded
ORCA_GITEA_TOKEN — while issue sync worked fine (#8312).
Root cause: GitHub owner/repo resolution (parseGitHubOwnerRepo) hard-rejects
any host that is not literally github.com. A GHES remote lives on a custom
host, so GitHub's forge resolveRepository returned null and provider detection
fell through the list to Gitea, whose KNOWN_NON_GITEA_HOSTS denylist cannot
enumerate arbitrary GHES domains. Issue sync was unaffected because gh
issue/pr list run with cwd=repoPath and let gh resolve the GHES host natively.
Fix mirrors GitLab self-hosted detection (getGlabKnownHosts): a new
getEnterpriseGitHubRepoSlug resolves a custom-host origin to owner/repo only
when gh is authenticated to that host — gh only ever manages GitHub/GHES
credentials, so a logged-in host is definitively GitHub. Wired into:
- forge-provider GitHub resolveRepository (fallback after github.com miss),
so detection claims GHES before Gitea is consulted;
- createGitHubPullRequest owner/repo resolution;
- isGitHubAuthenticated, which now probes the repo's real host instead of a
hardcoded --hostname github.com.
github.com repos keep the cached getRepoSlug fast path and never spawn the
extra gh auth probe.
* fix(github): host-qualify GHES gh commands and probe auth in the repo runtime
Addresses two correctness issues found in review of the #8312 fix.
1. GHES host was discarded before `gh pr create`. `--repo owner/repo` shorthand
resolves against gh's default host (usually github.com), so for a user
authed to both github.com and GHES it could target a same-named github.com
repo or fail — deterministic for SSH repos, which run gh with no cwd. Now
`createGitHubPullRequest` and the `findOpenPRByHeadBase` fallback pass a
host-qualified `HOST/owner/repo` for GHES (github.com keeps the shorthand).
Also generalize `parseCreatePRPayload`'s URL regex off github.com so a GHES
PR URL parses directly instead of limping through the list fallback.
2. GHES auth was probed on the wrong gh runtime. `getAuthenticatedGitHubHosts`
ran a global `gh auth status` with no cwd/WSL/SSH context and cached every
runtime under one "local" key, so a GHES login present only in the repo's
WSL distro was missed and the repo fell back to Gitea. Replaced with
`isGitHubHostAuthenticated`, which runs `gh auth status --hostname <host>`
with the repository's execution options (cwd/WSL distro, or SSH-local like
the create path) and caches per runtime+host — mirroring GitLab's
isGlabConfiguredForRemoteHost. This also honors GH_ENTERPRISE_TOKEN inferred
from repo context. Spawn failures stay indeterminate (uncached).
Adds createGitHubPullRequest-level tests asserting the actual gh `--repo`
arguments (create + fallback) and the WSL/SSH runtime of the auth probe.
* perf(source-control): drop redundant GHES gh auth probe in eligibility
Review follow-up. Detection only routes a GHES remote to the GitHub provider
after getEnterpriseGitHubRepoSlug has confirmed gh is authenticated to its
host, so isGitHubAuthenticated can trust a non-null slug as authenticated and
skip a second, rate-limited `gh auth status` spawn per eligibility poll.
Reaching the github.com probe now implies the remote is github.com. Tests
assert the enterprise path fires no redundant gh probe.
- gh file-fetch failures (rate limit, auth, unresolved remote) previously
returned an empty array, which the Files tab rendered as "No files
changed." — indistinguishable from a real empty PR
- getPRFiles now returns null on failure; work-item-details surfaces this
as filesUnavailable so GitHubItemDialog and PullRequestPage can show a
retry action instead of a misleading empty state
Here is a summary of how the sandbox behaves on your macOS system:
### ⚙️ How it Works
When `--sandbox` is enabled (either via the launch flag or the `enableTerminalSandbox` setting in your `settings.json`), terminal commands run inside a lightweight containment boundary:
- **macOS Native Isolation**: It utilizes macOS's native `sandbox-exec` utility to restrict system calls, network sockets, and directory access.
- **Secure File Boundaries**: File system writes are locked down to designated safe zones (such as your designated workspace or scratch directory). Access to critical system paths, private user data, and external network resources is restricted.
---
### 🛡️ Active Permissions for this Session
In this current session, the permission model is configured as follows:
| Action / Resource | Permission Status | Details / Paths |
| :--- | :--- | :--- |
| **Command Execution** | ✅ **Allowed** | Terminal command execution is enabled. |
| **File Reads (Allowed)** | ✅ **Allowed** | `/scratch`, `/browser_recordings`, `/html_artifacts`, `/knowledge`, `/worktrees`, `/skills`, `/builtin` |
| **File Writes (Allowed)**| ✅ **Allowed** | `/scratch`, `/browser_recordings`, `/html_artifacts`, `/knowledge`, `/worktrees` |
| **Sensitive Files** | ⚠️ **Ask** | `.env`, `.npmrc`, `.vscode`, `.git-credentials`, etc. |
| **Root/App Settings** | 🚫 **Denied** | Direct modifications to `/config` and main `.gemini` configurations |
---
### 🔧 Configuration and Management
* **Persistent Settings**:
To enable sandboxing by default for all future sessions, configure the `enableTerminalSandbox` setting in your `~/.gemini/antigravity-cli/settings.json`:
```json
{
"enableTerminalSandbox": true
}
```
* **Dynamic Adjustments**:
Within an active CLI (`agy`) session, you can run the `/permissions` slash command to view or modify your autonomy and sandboxing levels on the fly.
> [!NOTE]
> Running in sandbox mode provides an excellent balance of autonomy and security, allowing me to execute build commands, run test scripts, and manage project files safely without risk to your primary host environment.
Please let me know if you would like me to set up a new project workspace or run any specific tasks within this session!
110 files carried an eslint/oxlint-disable max-lines directive but are
already under the default max-lines budget (300 .ts / 400 .tsx / 600 .mjs
/ 800 test), so the suppression is dead. Removing it restores real
max-lines coverage on these files with zero behavior change.
Each removed directive had max-lines as its only rule; verified via a
full oxlint run (0 max-lines violations, 0 new errors). Diff is pure
deletions (200 lines, 0 additions) — no code touched.
Co-authored-by: Orca <help@stably.ai>
Enable three unicorn rules — one correctness, two performance — and fix every
existing violation repo-wide so the rules pass as errors.
prefer-number-properties (76 sites)
- parseInt/parseFloat/NaN -> Number.* : safe aliases (autofixed).
- isNaN -> Number.isNaN (12 sites, hand-converted): global isNaN coerces its
argument, Number.isNaN does not. Verified every call site already passes a
number (Number.parseInt results, number-typed fields, Date.getTime()), so the
conversion is behavior-preserving today and guards against a future non-numeric
argument silently coercing.
prefer-array-find (26 sites)
- .filter(pred)[0] -> .find(pred); .filter(pred).at(-1) / .pop() -> .findLast(pred).
Drops the intermediate array and short-circuits.
prefer-array-index-of (5 sites)
- .findIndex(x => x === v) -> .indexOf(v).
Verified: typecheck (node/cli/web) clean, 53 affected suites pass (1679 tests),
oxlint clean repo-wide. mobile/ uses findLast safely (already ships ES2023
.toReversed()); config scripts and e2e helpers run on Node 24.
- Resolve true work item identity (issue vs. PR) using the URL path to
override stale or incorrect cached payload types.
- Prevent invalid PR start point resolution when launching an issue
misclassified as a PR.
- Validate and reject mismatched URL types in the worktree metadata
dialog fields to avoid incorrect associations.
The OOM reports (F0BDMD16LJ2 and the taifunk many-worktree sessions)
show memory creeping over long sessions with churning worktrees. One
contributor: in pr-refresh-coordinator, many local worktrees that track
the same linked PR coalesce into a single queue entry whose 'aliases'
map keeps one entry per worktree. Aliases were only pruned when a
candidate was re-enqueued as invalid — never when a worktree was simply
removed/closed — so the maps grew unbounded across a session.
Add pruneWorktreePRRefreshAliases(worktreeId) and call it from
removeWorktreeMetadataAndTransientState (the existing central
worktree-removal cleanup, alongside removeWorktreeMeta /
forgetWorktree / deleteWorktreeHistoryDir). It drops the removed
worktree's aliases, deletes the queue entry when none remain, and
rebinds the representative candidate if the removed worktree owned it.
Covered by 3 new coordinator tests (accumulate-then-prune, keep-entry-
on-remaining-aliases with candidate rebind, no-op for unknown worktree)
plus two test-only inspection helpers.
Co-authored-by: Neil <neil@stably.ai>
* chore(lint): upgrade oxlint to 1.71 and enable 7 new rules
Upgrade oxlint 1.67.0 -> 1.71.0 (1.72 was blocked by the repo's 3-day
minimum-release-age supply-chain guard; nothing here needs it). The
bump is a no-op on the existing config.
Enable 3 error rules (backlog autofixed to zero in this commit) and
4 warn rules (surface signal without gating CI):
error (autofixed, behavior-preserving):
- unicorn/prefer-node-protocol (~1531 sites: bare builtin -> node:)
- typescript/no-import-type-side-effects (~36: all-inline-type -> import type)
- unicorn/no-array-reverse (19: copy-then-reverse -> toReversed)
warn (real signal, current fires are test-only/correct):
- unicorn/no-array-fill-with-reference-type (aliasing footgun guard)
- typescript/no-unsafe-function-type (bans bare Function type)
- unicorn/prefer-array-flat-map (map().flat() -> flatMap())
- unicorn/prefer-regexp-test (.match() in bool ctx -> .test())
mobile/.oxlintrc.json extends root, so it inherits all 7; the autofix
ran from root and covered mobile/ too.
Verification (all green): oxlint 0 errors (root+mobile+aux configs),
oxfmt clean, typecheck (node+cli+web), vitest 22795 passed / 0 failed,
builds (electron-vite + web + cli) succeed. node: rewrites confirmed to
skip embedded SSH/CLI string payloads (AST-only); all toReversed sites
verified to operate on fresh copies or write-once locals.
* chore(lint): bump mobile oxlint to 1.71 so inherited rules parse
mobile/ is a standalone pnpm project pinning its own oxlint@1.67, which
lacks unicorn/no-array-fill-with-reference-type (needs >=1.70). Since
mobile/.oxlintrc.json extends the root config, mobile CI's 'cd mobile &&
oxlint' failed to parse the new rule. Bump mobile to match root (1.71).
Verified in mobile/: oxlint 0 errors, oxfmt --check clean, tsc --noEmit
pass, vitest 978 passed / 0 failed.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
GitHub rejects enabling auto-merge on a PR that is already mergeable
with a "Pull request is in clean status" error.
* Suppress "Enable auto-merge" option in the UI when direct merge
is available, while retaining "Disable auto-merge".
* Translate the GitHub "clean status" GraphQL error into an actionable
message recommending direct merge.
* Surface GitHub check suites awaiting approval to unblock merge
- Query the check-suites API endpoint to find suites with an
"action_required" conclusion, which are often workflows awaiting
"Approve and run" and do not have any associated check runs.
- Map the "action_required" status distinctly instead of treating it as
a standard failure or omitting it entirely.
- Update the UI to render these suites with a warning icon, a dedicated
"Action required" label, and a localized hint explaining that manual
approval is required on GitHub.
- Count "action_required" checks as failed/blocking when deriving overall
PR and task statuses so the UI does not report all checks passing.
* Enhance visibility and handling of action-required PR check suites
* Include check suite IDs in pending approval check names and URLs to
allow navigating directly to the specific workflow run.
* Add an "action required" count badge to PR dialog and page checks tabs.
* Prioritize action-required checks in the checks preview summary.
* Use correct check run state for the action-required fallback hint in
the right sidebar details panel.
* Add translations for the new status across all supported locales.
Previously, transient errors during candidate branch discovery (such as
rate limits or network issues) were silently ignored, leading to a
false "no-pr" result and causing the sidebar PR state to flicker.
Now, track and return any pending error encountered during branch
lookups, propagating it as an upstream error if no PR is successfully
recovered.
- Centralize and align auto-merge eligibility logic across web and
mobile clients.
- Use the `enablePullRequestAutoMerge` GraphQL mutation instead of
`gh pr merge --auto` to prevent immediate merges on clean branches.
- Fall back to `gh pr merge --auto` when a merge queue is required on
the base branch.
- Hide the auto-merge control when only optional checks are pending.
* Style commit text area to match input tokens and fix disabled state
- Update borders and shadows to match standard input tokens.
- Style the disabled state explicitly to prevent Chromium's user-agent
styles from washing out the field outline.
- Add dark mode background styling overrides.
* Improve timeline items assertions in work-item-details test
Extract timelineItems to a local variable and add an explicit null check to prevent TypeScript compilation errors when calling the .at method.
* Add GitHub issue timeline activity feed to details dialog
Fetch and display issue timeline events (assignments, mentions, closes,
reopenings, and project board column moves) in a unified activity tab
within the GitHub item details dialog.
- Query the GitHub REST timeline endpoint up to a bounded 300 items.
- Merge comments and timeline events into a single sorted conversation.
- Render tailored icons, links, and localizable messages per event.
- Ensure issue reply targets are not incorrectly filtered by stale PR state.
- Correctly update local item state from details payload.
* Cap issue timeline pagination by supported activities
Count only mapped, supported timeline items instead of raw REST events
when checking the maximum item threshold. This prevents pagination from
stopping early when pages contain many unsupported event types.
When the hosting provider (GitHub) reports conflicts but a local merge
simulation is clean, we now mark the conflict summary as locally clean.
This state is surfaced in both the desktop and mobile sidebars with a
clear explanation and a copyable set of commands to trigger a remote
mergeability recalculation via an empty commit and push.
Ensure that when a visible fallback PR has been merged (e.g., outside
Orca with a deleted head branch), it is still accepted and refreshed by
branch lookup instead of being discarded as an implicit merged PR.
* Add `acceptMergedFallbackPR` option to GitHub branch lookups
* Enable this option during manual and background refreshes of fallback PRs
* Plumb the new option through preload APIs, IPC handlers, and RPC protocols