* 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.
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!
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.
* 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.
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
* Resolve and fetch the review target branch (compareBaseRef) during PR and MR worktree creation.
* Persist this ref on worktree metadata instead of pinning the head SHA.
* Dynamically repair existing worktrees with stale commit SHA compare bases in the Source Control UI using linked review metadata.
Merged pull requests are historical branch matches and should not be
treated as implicit review context unless the worktree has an explicit
PR link. Showing them as implicit review context leaves nothing
meaningful to unlink after the branch has been rebased or merged.
- Retrieve and propagate `autoMergeAllowed` from GitHub repo metadata.
- Suppress auto-merge action in UI if disallowed by the repository.
- Support specifying the merge method (e.g., squash) when auto-merging.
* Route task PR queries by upstream source
Implements the routing described in docs/tasks-pr-upstream-source.md so task PR and issue queries stay scoped to the selected source.
* rm design doc
Creating a worktree from a cross-repository (fork) PR previously named the
local branch with the maintainer's branch prefix (e.g. `me/866`) and pushed to
origin instead of the contributor's fork, so maintainer edits never reached the
PR. Fork PRs now adopt the contributor's branch name (matching same-repo PRs)
and resolve a fork push target, with a non-blocking warning when the PR
disables maintainer edits and an indicator showing where a push will land.
- pr-start-point: return branchNameOverride/headSha/maintainerCanModify for
cross-repo PRs (previously only same-repo PRs received these)
- github client: surface maintainer_can_modify alongside the fork push target
- composer: warn (but still allow) when "Allow edits from maintainers" is off
- source control: show the fork push target (owner:branch) before pushing
- extract fork-remote cleanup and setup into dedicated, unit-tested modules
Adds unit suites for the cleanup multi-fork matrix, fork-remote setup,
push-target resolution, the warning decision, and the push-target label.
Note: pre-commit react-doctor hook bypassed — its warnings in useComposerState
are pre-existing (identical count on base) and not enforced by CI.
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
* Default repo avatars to the GitHub upstream owner and flag forks
Make the GitHub owner avatar the default repo icon and surface it as the
primary choice in the icon picker (the "Image" tab becomes "Avatar",
moves first, and opens by default). For forks, resolve the upstream/
parent owner so the avatar reflects the source repo instead of the
personal fork, and show a fork indicator (GitFork glyph + "Fork of
owner/repo" tooltip) next to the repo in the sidebar and settings.
Fork detection prefers the offline `upstream` remote, falling back to a
`gh repo view --json isFork,parent` lookup; the resolved upstream is
stored on the repo. Existing repos self-correct via a one-time startup
backfill (local repos) and a lazy backfill when their icon settings open
(SSH repos); new repos resolve at add-time. Reset now restores the
default (re-detecting the upstream) instead of clearing to the Folder
icon.
* Harden repo upstream avatar handling
---------
Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
- Use fallback PR numbers after branch lookup misses, including detached HEAD
- Preserve review cards for forked or deleted-head PRs across manual refreshes
- Clear stale GitHub PR cache entries when unlinking worktree review metadata