* fix(azure-devops): retry with -preview api-version and keep project-level Git base for on-prem Server (STA-3494)
Azure DevOps Server rejects api-version=7.1 with 400
VssInvalidPreviewVersionException unless the -preview suffix is supplied,
so auth and every Git endpoint failed. Retry once with -preview on that
rejection and remember the requirement per origin. Also stop letting a
same-origin ORCA_AZURE_DEVOPS_API_BASE_URL (collection-level, needed only
for the connectionData auth probe) override the project-level base derived
from the remote for Git endpoints; cross-origin (proxy) overrides keep
working.
* fix(azure-devops): constrain preview retry and base override
#12065 started passing an AbortSignal as a third argument to provider.exec
in readRemoteUrl, and updated the Gitea, GitHub and GitLab assertions to
match. Azure DevOps and Bitbucket were missed, so main is red on three
test files.
Same expect.any(AbortSignal) shape #12065 used for the other providers.
Co-authored-by: Orca <help@stably.ai>
* fix(P1-D): coalesce remote-ref probes, TTL negatives, and bound unsettled keys
Keep forge resolution from stampeding git under worktree fan-out, let
remotes added mid-session be discovered without a restart, and refuse
pathological new-branch waves once the unsettled map is full.
* fix(P1-D): stop abandoned probes publishing, and split capacity refusals
A coalesced probe abandoned as stale kept running and still wrote its answer
to the cache, so a late permanent miss could land over the successor's fresher
one. Probes now publish only while they still own the in-flight key.
The hosted-review capacity refusal told brand-new branches that an earlier
attempt of their own never answered when the refusal was really the unsettled
map or the process-wide detached cap; each cap now says what it is.
Also caches stable "no such remote" SSH misses under the negative TTL instead
of re-spawning the probe on every poll.
Co-authored-by: Orca <help@stably.ai>
* Bound SSH remote URL probe with deadline to prevent hangs
The SSH branch of remote URL probes was unbounded — the relay's bounds
are per-phase and reset on every frame, so a relay dribbling output would
outlive them. Pass AbortSignal.timeout to the SSH provider's exec call to
enforce the same 30s deadline as local probes.
Treat AbortError as a transient probe error: it signals unavailable
infrastructure (deadline or cancellation), not a negative answer about
the remote.
---------
Co-authored-by: Orca <help@stably.ai>
* fix(P1-D): bound hosted-review lookups with a detachable deadline
The `inflight` map in the hosted-review branch cache was only ever cleared
when the lookup settled, and nothing bounded how long that took. One wedged
provider call pinned its branch for the life of the process: every later poll
joined the same dead promise, so the card loaded forever with no in-session
recovery.
Each lookup now runs under a 120s deadline. Nothing below the funnel can be
cancelled, so the deadline detaches instead: the record is released, the
callers get the last known review (or a timeout error), and the branch enters
the existing failure backoff. The lookup keeps running and its answer is still
adopted if it lands, so a slow-but-alive host converges rather than failing
forever. A token identity keeps a detached lookup from evicting the record
that replaced it, and a wall-clock sweep expires records whose timer never
fired — main's timers are suspended across system sleep. `inflight` is capped
independently of the completed cache.
The failure backoff moves to its own module: it has a different lifetime from
the answer cache and is what a deadline records against.
* fix(P1-D): bound `git remote get-url` on the local/WSL path
`getRemoteUrlForRepo` ran the git child with no timeout, which is the one
unbounded step under the hosted-review lookup funnel: `git/runner.ts` only
arms its kill path when a timeout is passed, so a dead network mount or a
stalled WSL interop hangs the call and everything above it. The SSH branch is
already bounded by the relay mux's 30s request timeout, so it is unchanged.
* rm review doc
* rm review doc
* test(P1-D): add probe tests and transient-failure recovery verification
Add tests for coalesced-probe and remote-url-probe infrastructure. Add integration test verifying that transient Bitbucket API failures don't cache as a definitive no-review result, allowing recovery after cache TTL expiration.
* fix(P1-D): track lookups from start, prevent stale scope adoption
- Count unsettled lookups when they start, not after deadline expires: prevents multiple concurrent lookups for the same branch.
- Add evicted generation floor: prevents adopting stale results when scope is invalidated and evicted from the map.
- Consolidate duplicate repository reference cache logic into createRemoteRefProbeCache utility.
- Fix deadline wrapper in git config signature lookup: bound the caller's deadline only, not the coalesced probe itself.
* feat(P1-D): add remote-ref-probe-cache utility
Cache successful remote URL probes per repo/runtime to avoid duplicate work.
Skip caching transient errors and SSH failures so providers can retry on
reconnect, preventing stale scope adoption during the session.
* Stop attaching stale closed PRs/MRs to default-branch checkouts
On the repo default branch, the implicit head-branch PR lookup (state=all)
could attach a historical closed/merged PR whose head ref was the default
branch name and show its wrong diffs and checks (#9171).
Add a shared default-branch guard: an implicit branch-name match on the
repository's default branch never surfaces a non-open review. Applied at
the branch-lookup choke point of all five provider clients (GitHub,
GitLab, Bitbucket, Azure DevOps, Gitea). Explicitly linked reviews are
exempt; open reviews from the trunk stay visible; resolution is lazy
(zero git calls unless a non-open candidate appears), TTL-cached,
transport-aware (local/WSL/SSH), probe-time-bounded, and fails open.
* Treat stuck-locked GitLab MRs as non-open in the default-branch guard
Three code-review lanes flagged (one reproduced) that 'locked' — normally
a seconds-long merge transition, but a known GitLab wedge state — leaked
past the closed/merged-only check and would re-create the #9171 symptom
for a stuck-locked MR whose source branch is the trunk.
* Bound default-branch lookup to one refresh budget
* Coalesce default-branch resolution probes
* 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".
Replace the hand-rolled `AbortController` + `setTimeout(() => controller.abort())`
+ `clearTimeout` in `finally` pattern with `AbortSignal.timeout(ms)` across the
main-process fetchers, updaters, and hosted-provider clients. This removes a
timer-leak footgun (a thrown/early-returned path that skips the finally leaks the
timer) and ~3-4 lines of bookkeeping per site. `AbortSignal.timeout` is Node
17.3+ (Electron main is Node 22+).
Two sites compose a caller-cancel signal with the timeout via `AbortSignal.any`
(Node 20.3+) instead of a manual abort listener:
- git/fork-sync.ts: also fixes a latent bug — the caller's `options.signal` was
spread into the git options then immediately clobbered by `signal:
controller.signal`, so caller cancellation was silently dropped. `AbortSignal.any`
restores it.
- rate-limits/claude-fetcher.ts (fetchViaOAuth external signal).
hosted-review-api-request.ts: `AbortSignal.timeout()` rejects with a
`TimeoutError`, not an `AbortError`, so the timeout-detection branch is updated
(otherwise `timedOut` would never be set).
minimax-fetcher.test.ts: its timeout test drove the abort with fake timers, which
cannot advance `AbortSignal.timeout`'s internal timer. Rewritten to fire the
timeout with an already-aborted signal so it genuinely exercises the abort path.
Deliberately NOT migrated:
- src/relay/git-handler.ts: the relay targets Node 18 (`build-relay.mjs`,
MIN_NODE_MAJOR = 18); `AbortSignal.any` needs Node 20.3+, and timeout-only would
drop the request context signal.
- ipc/feedback.ts: its timeout-driven fallback is verified with fake timers, which
can't advance `AbortSignal.timeout`; kept on the manual pattern.
* 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>
* Add Source Control Create PR intent flow
Implements the Source Control Create PR flow described in docs/source-control-create-pr-flow.md.
* Keep Commit visible beside Create PR
* Fix Create PR partial staging action band
* Integrate hosted review creation into Create PR intent flow
- Automatically create the pull or merge request on GitHub/GitLab after
successfully staging, committing, and pushing in the intent flow.
- Introduce a unified `updateCommitDrafts` helper to keep React state and
its ref synchronized, preventing draft-overwrite race conditions.
- Split primary action tests into focused files to satisfy the ESLint
`max-lines` rule.
- Replace hardcoded "Local Mac" strings with dynamic host labels.
* Support Azure DevOps and Gitea PR creation and limit large diffs
Implement automated pull request creation for Azure DevOps and Gitea
repositories. This includes REST API integration, credential checks via
environment variables, template support, and error classification.
Additionally, introduce limits on large diff payloads in git status
extraction to prevent renderer-freezing performance bottlenecks when
loading extremely large files.
* Skip source control refetches when PR creation intent is in flight
Avoid recomputing branch eligibility while isCreatePrIntentInFlight is true.
This prevents tearing down the PR composer or rotating dropdown hints
prematurely if ahead/behind or dirty states are temporarily perturbed
temporarily perturbed mid-flow.
* Expose manual prerequisite actions next to Create PR button
Previously, the Create PR intent only supported "Stage All" as a
sibling action. This expands prerequisite resolution to handle other
intermediate steps such as committing, publishing, and pushing
(including force pushing).
This ensures the edit-commit-push-review loop remains streamlined
directly within the CommitArea by displaying the specific required
next action beside the primary Create PR button.
* Move PR creation actions from CommitArea to sidebar header
- Decouples PR creation and PR intent actions from the local commit area
primary button, ensuring local/remote git actions remain primary.
- Renders a dedicated PR creation button in the source control header
beside the hosted review status.
- Simplifies CommitArea by removing prerequisite split-button rendering
and review composer logic.
* Delete source control create PR flow design document
Remove the design document for the source control create PR flow as the feature has been successfully implemented.
* Display PR creation errors in inline notice
Unify PR/review creation error reporting by replacing the duplicate
createPrErrors state with the shared createPrIntentNotice. Validation
and API errors are now shown directly within the visible inline alert
notice to improve layout consistency and visibility.
Also refactor the execution host platform label lookup to use simple
if statements instead of a switch block.
* Improve Create PR intent flow safety and provider awareness
- Integrate the hosted review composer directly into the Source Control
panel when a direct review creation action is available.
- Abort the in-flight PR creation intent flow early if the current git
branch changes to prevent staging or committing on the wrong target.
- Keep in-flight action labels provider-aware (e.g., "Create MR" on GitLab)
by passing hosted review inputs to the action resolver.
- Omit large diff text payloads from git status responses when line counts
exceed safe rendering limits to avoid UI performance degradation.
- Ensure field generation does not retarget the base branch of a PR/MR without
explicit user confirmation.
* Preserve PR and MR templates in AI pull request generation
- Preload templates (including GitLab merge requests) into the AI
context before generation to prevent bypassing provider-side fallbacks.
- Instruct the AI generator to fill out and preserve existing template
headings, required sections, and checklists instead of deleting them.
- Pass provider and template settings from the renderer to the backend
RPC and runtime handlers.
* Mock DropdownMenuShortcut in tab-title-tooltip test
Add a mock for the DropdownMenuShortcut component in the dropdown menu
mock to prevent test failures.