20 KiB
Source Control Push Failure AI Recovery
Problem
Push, force-push, publish, and sync push-stage failures flow through the shared
remote operation formatter in src/renderer/src/lib/source-control-remote-error.ts:94
and the editor-store toast paths in src/renderer/src/store/slices/editor.ts:3716
and src/renderer/src/store/slices/editor.ts:3821. A local pre-push hook or
lint hook is not an auth, protected-branch, or transport problem, so generic
remote guidance sends the user in the wrong direction.
PR #7787 proves the behavior is useful, but the prototype needs tightening before it is maintainable:
src/renderer/src/components/right-sidebar/SourceControl.tsx:1938andsrc/renderer/src/components/right-sidebar/SourceControl.tsx:5799repeat push-failure predicates and feed live branch/file state into a prompt that says "at failure time".src/renderer/src/components/right-sidebar/use-source-control-recovery-ai.ts:63imports push detection again instead of consuming one derived recovery model.src/renderer/src/components/right-sidebar/source-control-ai-commit-failure-launch.ts:24andsrc/renderer/src/components/right-sidebar/source-control-ai-push-failure-launch.ts:24are copied launch flows with action-specific copy.src/shared/source-control-commit-failure-agent-command.ts:6andsrc/shared/source-control-push-failure-agent-command.ts:6are identical command-template builders.src/renderer/src/components/right-sidebar/source-control-fix-split-button.tsx:21already contains a reusable split button, whileSourceControl.tsx:6415carries a second inline version.src/renderer/src/lib/source-control-remote-error.ts:148currently treats anyisSyncerror as push-like for hook detection.syncBranchcalls the formatter for fetch, upstream-status, pull, and push failures, so the renderer cannot prove a sync error came from the push stage without an explicit marker.src/renderer/src/components/right-sidebar/SourceControl.push-failure-recovery.test.ts:2imports prompt helpers through the large React module instead of shared prompt modules.
Goal
Add first-class recovery for pre-push hook failures without growing a parallel push-only subsystem:
- Toasts for push, force-push, publish, and sync push-stage failures say "blocked" for detected pre-push or lint-hook output.
- Source Control shows a concise Push blocked panel with Details and AI Fix only for a failure snapshot that is both push-like and hook-like.
- AI Fix launches the configured
fixPushFailureSource Control action with a safe, provider-neutral prompt. - Commit-failure recovery and push-failure recovery share launch, command-template, split-button, and dense error-panel primitives.
Non-goals
- Do not fix server-side pre-receive hooks, hosted CI failures, or provider-side protected-branch failures from this entry point.
- Do not bypass hooks, add
--no-verify, push from the launched agent, create a PR, or assume GitHub-specific terminology. - Do not add app-wide persistence for this transient failure panel.
- Do not redesign the Source Control panel or the Source Control AI settings model.
- Do not fold unrelated launch surfaces such as checks recovery into this change unless a tiny shared helper is already needed for commit/push recovery.
Design
-
Keep push-hook detection and prompt building in shared code.
src/shared/source-control-push-failure.tsowns normalization, bounded scanning, summary text, Details eligibility, prompt truncation, prompt rules, and changed-file list bounding.- Detection must stay conservative: explicit
pre-push/prepush,hook declined to push, hook-runner output in push context, or lint output in push context. Auth, non-fast-forward, protected branch, pre-receive, submodule, and generic transport failures stay on the existing remote-error path. - Keep the output scan bounded at 64 KiB and the prompt failure-output section bounded. Also cap prompt file lines to a small constant with an omitted-count line; do not rely on the git-status cap, which is far too high for prompt context.
-
Make remote-error formatting sync-stage aware.
- Extend
RemoteOperationErrorOptionswith an explicit push-stage flag for sync, for exampleisSyncPushStage. - Gate push-hook classification on
isPush,isForcePush,publish, orisSyncPushStage; do not use bareisSyncas evidence of a push failure. - In
syncBranch, pass the push-stage flag only inside the two innerpushRuntimeGitcatch blocks. The outer fetch, upstream-status, and pull catches still passisSyncfor Sync-shaped generic copy but must not render blocked hook copy. - Carry the same push-stage fact back to the renderer, either by tagging the
thrown
Errorwith a narrow exported marker or by wrapping it in a typed error that preserves the original message/cause.SourceControlmust setsyncPushStageonly from that marker, not fromkind === 'sync'plus hook-like stderr. - Keep submodule push messages before hook detection so submodule guidance remains more specific than AI recovery.
- Extend
-
Replace one-off command builders with one shared recovery command builder.
- Add a shared builder that accepts
{ actionId, promptOverride, commandInputTemplate, basePrompt }and renders the existing Source Control AI command template for launch actions. - Keep compatibility exports for commit and push helpers if nearby tests or call sites still use those names, but make them wrappers over the generic builder.
- Move prompt-builder tests to import from shared modules, not
SourceControl.tsx.
- Add a shared builder that accepts
-
Capture and derive push recovery state once.
- Extend
SourceControlActionErrorwith the raw error, a push-stage marker for sync failures, the branch name at failure time, a bounded status-entry snapshot at failure time, and a per-worktree sequence token. runRemoteActionclears the worktree's previous error and increments its sequence before starting. A catch may write an error only if its sequence still owns that worktree, so a slow failure cannot overwrite a newer retry or success.- Add a focused renderer helper that accepts the captured
SourceControlActionErrorand the current branch name, then returns eithernullor a model with raw/sanitized detail text, summary, details flag, kind label, and AI prompt. - The helper returns
nullunless the operation ispush,force_push,publish, orsyncwith the sync push-stage marker andisPushHookFailure(rawError)passes. - If the current branch is known and differs from the captured branch, hide or clear the model instead of launching an agent with wrong branch context.
- Pass that single model to
useSourceControlAiandCommitArea; neither prop construction norCommitAreashould repeat the push-hook predicate.
- Extend
-
Use one generic recovery launcher for commit and push failures.
- Replace the separate commit and push launch files with
source-control-ai-recovery-launch.ts, or keep the old files as one-line wrappers if import stability is cheaper. - The generic launcher owns connection resolution, SSH/local agent discovery, saved-agent validation, agent-args validation, agent selection, terminal launch, focus, and success/failure toasts.
- Resolve connection without collapsing local and unresolved states: read
const worktreeConnectionId = getConnectionId(worktreeId), use it when it is a string ornull, and only fall back tosourceRepoConnectionIdwhen the worktree lookup returnsundefined. If the fallback is alsoundefined, show the workspace-connection error instead of launching locally.nullmeans proven local; a string means SSH/remote. - Validate saved CLI args before agent detection or terminal creation. On
Windows, keep using PowerShell-safe planning through
planAgentCliArgsSuffix(..., 'powershell'). - Action-specific data is limited to action id, base prompt, empty-prompt copy, unavailable-agent copy, and success copy.
- Replace the separate commit and push launch files with
-
Keep the recovery hook thin.
use-source-control-recovery-ai.tsbuilds the commit prompt, accepts the already-derived push recovery prompt/model, keeps independent loading flags, and calls the generic launcher.- It should not import push-hook detection, resolve agents, or duplicate launch plumbing.
-
Extract and reuse the recovery UI.
- Reuse or replace the existing
src/renderer/src/components/right-sidebar/source-control-fix-split-button.tsxrather than adding another split-button component. - Move the dense recovery notice and Details dialog into focused renderer
modules named for source-control recovery. Avoid
helpers,utils, and new max-lines suppressions. - Reuse the same notice/dialog component for commit and push recovery with action-specific labels, summary, details, prompt, saved recipe, and launch callback.
- Keep normal remote errors filtered out when a push recovery model is rendered, so the user does not see both Push blocked and generic remote error copy for the same failure.
- Reuse or replace the existing
-
Retain
fixPushFailurein the existing Source Control AI action model.- The prototype already adds
fixPushFailuretosrc/shared/source-control-ai-actions.ts; keep it as a launch action likefixCommitFailure. - Verify default settings, normalizers, global settings rows, repository override rows, labels, descriptions, and variable chips all include Push failure fixes.
- Do not create a push-specific settings store or migration. Existing
normalizeSourceControlAiSettingshydrates missing action defaults.
- The prototype already adds
Data Flow
- User clicks Push, Force Push, Publish Branch, or Sync.
runRemoteActionclears that worktree's previous remote error, records a new sequence token, and starts the editor-store remote action.- Git push fails and throws.
pushBranchor sync's inner push-stage catch callsresolveRemoteOperationErrorMessagewith push-like options. Sync fetch, pull, and upstream-status failures do not pass the push-stage flag.- Sync's inner push-stage catch marks the rethrown error as push-stage so
runRemoteActioncan preservesyncPushStagein the captured snapshot. - The formatter classifies hook output before auth/transport fallbacks only for push-like options and shows blocked toast copy.
SourceControlcatches the same error, and if the sequence still owns the worktree, stores{ kind, rawError, message, syncPushStage, branchName, entriesSnapshot }.- The push recovery helper derives one
pushRecoverymodel from that snapshot. CommitArearenders normal remote errors or the Push blocked recovery notice.- AI Fix builds the
fixPushFailurecommand input and launches the selected agent in the owning local or SSH runtime.
Edge Cases
- Auth, missing repo, protected-branch, pre-receive, non-fast-forward, and transport failures must not show the Push blocked panel.
- Submodule push errors keep their existing specialized messages before push-hook detection.
- Sync failures from fetch, upstream-status, or pull stages must not show Push blocked, even if stderr contains words like "lint" or "hook".
- Create PR intent remote failures use the same
runRemoteActionplumbing; if CommitArea is not visible, blocked toast copy is still required but no hidden panel work is needed. - A newer remote operation for the same worktree must clear stale recovery state immediately and prevent an older in-flight failure from writing after it.
- Switching worktrees or branches must not show another worktree or branch's push failure.
- External edits or another Orca window may make the failure stale. Do not add cross-window persistence for this feature; rely on local retry/remount/branch mismatch clearing and list this as residual risk.
- Source Control AI hidden for a repo still shows the blocked notice and Details; it hides AI Fix.
- SSH worktrees must detect and launch agents on the owning connection, not a local fallback.
- Windows launch planning must continue to use PowerShell-safe agent args.
- Very large hook output and very large changed-file sets must be bounded before prompt generation.
- ANSI/control output must not leak into summaries or Details comparison logic. Details may show sanitized text while preserving enough line breaks for debugging.
- Empty custom command templates must stay empty so the launcher rejects them with a clear settings error.
- Prompt text must treat file paths, branch names, and hook output as data, not instructions.
Test Plan
- Shared unit tests:
src/shared/source-control-push-failure.test.tscovers detection positives, auth/protected/pre-receive/non-fast-forward negatives, ANSI/control stripping, bounded scanning, details comparison, prompt output truncation, prompt file-list capping, and provider-neutral prompt rules.- Shared command-template tests cover the generic recovery builder plus commit/push wrappers, including empty templates and prompt overrides.
src/shared/source-control-ai-actions.test.tscoversfixPushFailurelabel, default template, variables, normalization, and action-list inclusion.
- Remote formatter and store tests:
src/renderer/src/lib/source-control-remote-error.test.tscovers push, force-push, publish, and sync push-stage blocked copy plus sync non-push stage negatives, auth/non-fast-forward/protected/pre-receive/submodule regressions.src/renderer/src/store/slices/editor.test.tscovers thepushBranchtoast path and both sync push-stage and sync non-push-stage toasts.
- Renderer model and UI tests:
- A focused push recovery derivation test covers operation kind, sync push-stage marker, branch mismatch, stale sequence ownership, snapshot file entries, summary, details flag, prompt contents, and ordinary remote errors.
CommitAreatests cover the Push blocked notice, Details dialog, AI Fix visible/hidden, ordinary remote errors, no duplicate remote error when a push model exists, and unchanged commit-failure recovery rendering.- Settings tests cover global and repository Source Control AI action rows showing Push failure fixes.
- Launcher tests:
- Generic recovery launcher tests cover commit and push action ids, invalid CLI args before detection, empty template rejection, unavailable saved agent, local versus SSH agent detection, successful launch/focus, and success copy.
- Checks:
- Run targeted vitest files above,
pnpm typecheck,pnpm lint, andpnpm check:max-lines-ratchet.
- Run targeted vitest files above,
UI Quality Bar
The Source Control recovery UI must match docs/STYLEGUIDE.md: monochrome,
quiet, dense, token-based, and consistent with adjacent commit-failure recovery.
Use existing shadcn Button, DropdownMenu, and Dialog primitives, lucide
icons, card/border/destructive/muted tokens, and the existing action
recipe row patterns. The Push blocked notice should keep the summary readable at
narrow right-sidebar widths, avoid nested cards, avoid new color values, show
stable split-button geometry while launching, and keep Details as progressive
disclosure for long hook output. Dialogs and dropdowns must remain usable in
light/dark mode and under SSH latency.
Review Screenshots
- Source Control after a simulated pre-push hook failure: Push blocked notice with summary, AI Fix, and Details.
- Push failure Details dialog showing hook output and Fix with AI.
- AI Fix customize-launch dialog for
fixPushFailure. - Ordinary push auth/protected/remote failure state: regular remote error, no Push blocked notice.
- Sync fetch/pull-stage failure state: Sync-shaped remote error, no Push blocked notice.
- Source Control AI hidden for the repo: Push blocked notice and Details, no AI Fix.
- Source Control AI settings showing the Push failure fixes launch recipe.
- Adjacent commit-failure recovery notice still rendering correctly after the shared UI extraction.
Rollout
- Tighten shared push-failure classifier/prompt bounds and add the generic recovery command builder.
- Make remote formatter/store sync-stage aware and update toast tests.
- Add the
SourceControlActionErrorsnapshot/sequence data and one push recovery derivation helper. - Replace duplicated commit/push launchers with the generic recovery launcher and thin hook wiring.
- Extract/reuse source-control recovery UI components, including the existing split-button module.
- Verify
fixPushFailuresettings/default/normalizer coverage and i18n labels. - Update targeted tests and run typecheck, lint, and max-lines ratchet.
- Validate rendered Electron states with screenshots before opening the PR.
Lightweight Eng Review
- Scope: Reduced from PR #7787's push-specific parallel path to a shared recovery layer. Keep the feature limited to local pre-push/lint-hook detection, blocked copy, an inline recovery notice, Details, and AI launch. No persistence or provider-specific recovery.
- Architecture/data flow: Shared code owns classification, prompt text, and
command-template rendering; the editor store owns stage-aware toast formatting;
SourceControlowns transient worktree-scoped failure snapshots and sequence invalidation; the generic launcher owns local/SSH agent discovery and terminal launch. - Failure modes covered:
- Sync stage ambiguity: only the inner push stage may classify as hook-blocked.
- Auth/protected/pre-receive/non-fast-forward/submodule/transport errors stay out of push recovery.
- Branch or worktree switches suppress stale recovery models.
- Per-worktree sequence tokens prevent older in-flight failures from overwriting newer retries or successes.
- Empty custom templates, invalid CLI args, unavailable saved agents, missing worktree context, SSH host detection, and Windows shell planning fail before terminal launch.
- Large or hostile hook output and file lists are bounded and treated as data in prompts.
- Test coverage required:
- Shared classifier/prompt/command/action-model tests in
src/shared. - Remote formatter and editor-store tests for push, force-push, publish, sync push-stage, and sync non-push-stage paths.
- Renderer derivation and
CommitAreatests for Push blocked, Details, AI Fix, hidden AI actions, stale branch/sequence, ordinary remote errors, and existing commit failure recovery. - Generic launcher tests for local/SSH, invalid args, saved-agent unavailable, empty template, and success launch.
- Settings tests for global and repo Push failure fixes rows.
- Electron validation for all required screenshots; no separate E2E is required if renderer tests cover the model and Electron validates the real UI.
- Shared classifier/prompt/command/action-model tests in
- Performance/blast radius: No polling, watchers, migrations, or startup work. Runtime cost is one bounded classifier pass over at most 64 KiB, bounded prompt output, and a bounded file snapshot only when a remote action fails. Largest blast radius is the shared recovery UI and launcher used by existing commit failures, so commit recovery needs tests and a screenshot.
- UI quality bar: Match
docs/STYLEGUIDE.mdand adjacent Source Control density: token-based error state, stable split-button geometry, progressive Details disclosure, no nested cards, concise copy, light/dark compatibility, and visible disabled/loading state under SSH latency. - Required review screenshots:
- Push blocked notice after pre-push hook failure.
- Push failure Details dialog with Fix with AI.
fixPushFailurecustomize-launch dialog.- Ordinary push auth/protected/remote failure with no Push blocked panel.
- Sync non-push-stage failure with no Push blocked panel.
- Source Control AI hidden: Push blocked without AI Fix.
- Source Control AI settings showing Push failure fixes.
- Existing commit-failure recovery notice after shared UI extraction.
- Residual risks: Classifier false positives remain possible for unusual local hook-runner output that mentions push context; keep tests biased toward preserving auth, protected-branch, and non-fast-forward behavior. Failure panels are renderer-local, so another Orca window or an external terminal can fix the problem while the old panel remains visible until local retry, remount, or branch/worktree change. Electron validation may need a mocked or temporary repo scenario to trigger a real pre-push hook without mutating user data.