Skip to content

feat(staged): add no-verify and deferred push options to the PR button menu - #1052

Merged
matt2e merged 7 commits into
mainfrom
pr-submenu
Nov 12, 2026
Merged

matt2e merged 7 commits into
mainfrom
pr-submenu

Conversation

@matt2e

@matt2e matt2e commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

Extends the PR button's chevron menu in Staged. It now appears in every PR state, not just Create PR, and gains push variants that skip the pre-push hook or wait for in-flight work to finish.

Menu

  • Push without verifying (git push --no-verify), plus force-push variants (Option-click skips the dialog). A PushVariant { force, no_verify } replaces the bare force flag along the whole push path, including the HTTPS fallback, the agent handoff prompts, queued-push dedupe and Retry/rejection flows. push_no_verify lives in the JSON pipeline column, so no migration is needed for it.
  • Push after current work (all four variants): arms a push that fires once the branch and its project have nothing running, disconnected or queued. The menu row and timeline footer show what the push is waiting on, and you can cancel it.
  • View PR #N / Copy PR URL when a PR exists.
  • Menu items come from a pure builder (prButtonMenu.ts). The force-push dialog title and text now reflect rejected vs. manual and verify vs. no-verify.

Backend (new fields, for review)

  • Migration 0042-add-branch-deferred-push: nullable branches.deferred_push (push / force / push-no-verify / force-no-verify) and deferred_push_requested_at (ms). These surface as Branch.deferred_push and on the wire as deferredPush / deferredPushRequestedAt.
  • Commands defer_branch_push / cancel_deferred_push, with matching web arms. Arming runs under the branch launch lock. If everything is already idle, it pushes right away instead.
  • Firing happens at the tail of drain_branch_queue. The flag is cleared before the push starts and re-armed if the push fails to start. When a project-note session ends, its project's armed branches are kicked (KickSource::ProjectSessionEnded).
  • A same-variant push that completed after the arm satisfies the deferred push, but only if nothing else ended on the branch after it. The completion time comes from pipeline step completed_at, not from updated_at, because transfers and branch moves rewrite updated_at.
  • A flag on branches is used instead of a queued push row. A queued row would block the branch FIFO, and the queue can't see project sessions.

Tests

Adds prButtonMenu.test.ts, plus store, migration, prs, session_queue and launch_resume tests covering arming, firing, stacking and the ordering edge cases.

🤖 Generated with Claude Code

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-11T01:35:46.397064Z 3981268 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3981268b1d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines 2080 to +2084
launch_resume::detect_at_launch(app.handle(), &store);
// Deferred pushes ("Push after current work") left armed by
// the last run. After the offer is known, so a branch the
// offer may put work back on waits for the answer.
launch_resume::kick_deferred_pushes_at_launch(app.handle(), &store);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Establish launch holds before queue recovery kicks

When a restart has an armed deferred push, a queued row, and an interrupted branch or project session awaiting the launch-resume decision, kick_all_with_queued_work schedules that branch before detect_at_launch records the hold. That already-scheduled drain does not consult deferred_push_holds, so after the queued row finishes it can fire or clear the deferred push while the interrupted session is still terminal, causing resumed work to remain unpushed. Detect and establish the launch holds before issuing any startup queue kicks, or make the drain explicitly honor those holds.

Useful? React with 👍 / 👎.

Comment thread apps/staged/src-tauri/src/prs.rs Outdated
Comment on lines +2755 to +2757
store
.set_branch_deferred_push(branch_id, None)
.map_err(|e| e.to_string())?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear only the deferred request that was checked

If new branch work starts after check_deferred_push reports idle and the user re-arms the branch with a different variant before this write, the unconditional clear deletes that newer request and the old variant is then queued behind the new work. The same lost-update race exists in the Satisfied arm above. Clear with a compare-and-swap on the checked variant/timestamp, or keep the check and clear under the branch launch lock, so re-arming cannot be overwritten.

Useful? React with 👍 / 👎.

Comment thread apps/staged/src-tauri/src/prs.rs Outdated
Comment on lines +2420 to +2424
// Deferred push ("Push after current work")
// =============================================================================
//
// A push armed on the branch row (`branches.deferred_push`, see
// `store::DeferredPush`) that fires once the branch and its project are idle.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Split deferred-push scheduling out of prs.rs

Move the deferred-push state machine, completion analysis, commands, and tests into a focused submodule rather than adding this large new responsibility to prs.rs; this change grows the file by roughly 1,450 lines to nearly 5,000 lines, making queue and push behavior substantially harder to navigate and maintain. The repository explicitly requires growing Rust modules to be split before they become difficult to navigate.

AGENTS.md reference: apps/staged/AGENTS.md:L28-L31

Useful? React with 👍 / 👎.

matt2e added a commit that referenced this pull request Nov 11, 2026
`src-tauri/src/prs.rs` had grown to nearly 5,000 lines on this branch. Per
AGENTS.md (split growing Rust modules by responsibility before they become
hard to navigate) and the PR #1052 review, it is now `src-tauri/src/prs/`,
split along every responsibility it held, not only the deferred push:

- `mod.rs`: module docs, declarations and the re-exports that keep
  `crate::prs::*` paths working for `web_server`, `session_commands`,
  `session_completion`, `pr_poll_scheduler`, `pipeline_dependency`,
  `branches`, `pr_detection` and `upstream_repair`.
- `context.rs`: resolving a branch to its repository and working directory
  (`resolve_branch_repo_and_subpath`, `BranchPipelineContext`,
  `resolve_branch_pipeline_context`, `base_branch_name`).
- `pipeline.rs`: the session-row plumbing every pipeline shares:
  `BranchPipelineResponse`, the prompt constants and `pipeline_prompt`,
  `find_queued_pipeline`, `branch_has_work_in_flight`,
  `RunningPipelineSession` + `launch_running_pipeline_session`, and
  `start_pipeline_for_branch`.
- `git_commands.rs`: the fetch/push shell commands with the HTTPS fallback
  and `push_refspec`.
- `commit.rs`: rebase and squash, including "Rebase and force push" and the
  queued-commit dequeue (`start_queued_commit_pipeline_for_branch`).
- `push.rs`: `PushVariant`, `build_push_pipeline_steps`, push queueing and
  per-variant dedupe, the follow-up force push row,
  `start_or_queue_push_pipeline_for_branch` and `has_unpushed_commits_impl`.
- `pull.rs`: the immediate fast-forward with its marker session, the queued
  pull, and `build_pull_pipeline_steps`.
- `git_pipeline.rs`: rebuilding a queued push or pull on dequeue
  (`rebuild_queued_git_pipeline`, `start_queued_git_pipeline_for_branch`).
- `deferred.rs`: "Push after current work": blockers, the satisfied-by-a-
  completed-push check, arm / cancel / fire.
- `create_pr.rs`: the create-PR pipeline and its handoff prompt.
- `pr_status.rs`: `PrStatusEvent`, single-branch and per-project refresh with
  the bounded pool, `BranchRefresh` collection, and clearing stale status.
- `commands.rs`: the thirteen `#[tauri::command]` wrappers and `get_store`,
  following `project_transfer::commands` / `actions::commands`.
- `tests.rs` + `tests/{commit,push,pull,git_pipeline,deferred,create_pr,
  pr_status}.rs`: the test module, with the shared fixtures in `tests.rs`
  and each test next to the module it covers (the `project_transfer`
  layout).

The move is behavior-neutral: no function body, prompt, command or event
changed, and no item was renamed. The only code edits are visibility
(`pub(super)` on items now shared between sibling modules or reached from
the tests), the per-module `use` lists, module docs, and seven intra-doc
links qualified with their new module path. Call-site changes outside the
tree: the `generate_handler!` list in `lib.rs` names
`prs::commands::<command>`, because tauri's `#[command]` macros are
`#[macro_export]`ed at the crate root and cannot be re-exported through
`prs/mod.rs`; the two source-text tests in `session_commands.rs` that
`include_str!` the dequeue paths now read `prs/commit.rs` and
`prs/git_pipeline.rs`; and a doc comment in `timeline.rs` names the new
command path.

Verified: `cargo check --lib --tests`; `cargo clippy --lib --tests` with no
warnings in the new tree (the five it reports are pre-existing in
`note_transfer.rs` and `orchestrator_tests.rs`); `cargo test --lib -- prs::
session_commands::tests::queued_start branches:: pipeline_dependency::
session_queue:: launch_resume:: session_completion::` (199 passed); the test
and function counts before and after are equal (66 tests, 161 fns), and a
line-level diff of the old file against the tree differs only in `use`
lines; `rustfmt --check` on the tree. Nothing frontend-visible changed, so
`pnpm -s check` was not run.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
matt2e and others added 7 commits November 12, 2026 14:27
…widen the menu to every PR state

The PR button's chevron menu only existed in the Create PR state. It now
shows whenever it has entries. Its items come from a pure builder
(`prButtonMenu.ts`); the component maps each item id to an icon and a
handler.

Push without verifying (`git push --no-verify`):
- A `PushVariant { force, no_verify }` replaces the bare `force` flag
  through the push path. `--no-verify` goes into both halves of the HTTPS
  fallback, and the force-push handoff template spells it out so the
  agent's retry skips the hook too.
- Two new prompts. Both contain the strings
  `infer_branch_resume_session_type` matches on, so `push-completed`
  still fires.
- `PipelineExecution.push_no_verify` is persisted in the JSON pipeline
  column (no migration). Queued pushes dedupe per variant, and dequeue
  rebuilds the steps with the flag through a new
  `rebuild_queued_git_pipeline`.
- `push_branch` and its web arm take `noVerify`. The frontend carries it
  through `pushStateStore`, untracked-push adoption, Retry and the
  rejection dialog.

Menu, by state:
- Local ahead: "Push without verifying" (with Option: "Force push
  without verifying", no dialog).
- Diverged: "Force push without verifying…", which opens the force-push
  dialog (Option skips it).
- With a PR: "View PR #N" (unless the button already opens it) and "Copy
  PR URL", through a shared `resolvePrUrl()`.
- Create PR state: unchanged.

The force-push dialog is now titled "Push Rejected" only after a
rejection and "Force Push" otherwise. When the push skips the hook, it
says so and its button reads "Force Push Without Verifying". Option is
read from the shared `trackAltKey()`, which resets on blur, instead of
the button's own listeners.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…R button menu

A push armed from the PR button's menu that fires once the branch *and* its
project have nothing running, disconnected or queued. All four variants are
offered: push, force push, and both without verifying (`--no-verify`).

New backend fields (for review, per AGENTS.md):
- Migration `0042-add-branch-deferred-push`: two nullable columns on
  `branches`, `deferred_push TEXT` (one of `push`, `force`, `push-no-verify`,
  `force-no-verify`) and `deferred_push_requested_at INTEGER` (ms). Existing
  rows read as not armed; `migration_tests.rs` covers both.
- `store::DeferredPush { force, no_verify, requested_at }` with
  `as_column_name()` / `parse()`, and `Branch.deferred_push:
  Option<DeferredPush>`. The store does not depend on `prs::PushVariant`;
  `prs` maps between the two.
- `BranchWithWorkdir.deferred_push: Option<&'static str>` and
  `deferred_push_requested_at: Option<i64>` on the wire; the frontend
  `Branch` gains `deferredPush` / `deferredPushRequestedAt`.
- `KickSource::ProjectSessionEnded`.

Why a flag on `branches` and not a queued push row: a queued row sits in the
branch FIFO (`drain_branch_queue` stops at it, so it would block work queued
behind it) and the queue cannot see project sessions. The fired push is an
ordinary push pipeline, so the per-variant dedupe, the prompts and the
`push-completed` handling all apply unchanged.

Backend:
- `set_branch_deferred_push` / `branch_ids_with_deferred_push(project_id)`
  on the store, publishing `Branch` so clients refetch (no new event), and
  `project_has_sessions_in_flight` (sessions joined to `project_notes`,
  running / disconnected / queued).
- Commands `defer_branch_push(branch_id, provider, force, no_verify)` and
  `cancel_deferred_push(branch_id)`, registered in `lib.rs` with
  `web_server.rs` arms. Arming takes the branch launch lock for the
  check-and-set; if the branch and project are already idle it pushes right
  away instead (`{ armed: false, push }`), closing the gap between the menu
  rendering and the click.
- Firing: the tail of `drain_branch_queue`, when nothing queued could start
  (`prs::fire_deferred_push_if_idle`). It inherits the drain's quitting /
  provisioning / transferring gates and runs on the kicker's task. The flag is
  cleared before the push starts so the push's own terminal-state kick cannot
  fire it twice; a start failure re-arms it. The fired push runs with
  `provider: None`, like a queued row.
- `drain_queued_after_terminal_state` no longer returns early for branchless
  sessions: a project-note session's end resolves its project and kicks that
  project's armed branches with `KickSource::ProjectSessionEnded`.
- Startup: `launch_resume::kick_deferred_pushes_at_launch` runs after
  `detect_at_launch`. Armed branches the launch-resume offer names (their own
  interrupted session, or an interrupted project session of their project)
  are held in `LaunchResumeState` and kicked when the offer is answered —
  immediately on a dismissal, after the run otherwise — so the push cannot
  fire ahead of a resumed session's commits. The rest are kicked with
  `KickSource::Startup`.

Menu (`prButtonMenu.ts`), a new section between the push-now rows and the PR
links, PR-gated like `pushItems` and shown only while `workInFlight`
(branch sessions, git pipelines, or any running session in the project via
`projectStateStore.hasRunningSessions`):
- Local ahead and in sync: "Push after current work" and "Push after current
  work without verifying"; Option swaps both to their force variants.
- Diverged: the force pair with "…", opening the force-push dialog (Option
  skips it). The dialog is titled "Force Push After Current Work" and its
  button says so.
- While armed, the section is a single "Cancel deferred push" row in every
  state (the flag can coexist with a queued or running push); its tooltip
  names the armed variant.

Timeline: a `git-deferred-push` footer row below the queued-pull row shows
the armed variant's label, "Waiting for branch and project sessions to
finish", and a Cancel button (`pushDeferred` on `TimelineRow`). The row is
the undo, which is why the plain deferred rows need no confirm.

Tests: the `it.each` table and separator invariant in `prButtonMenu.test.ts`
cover every state ± Option, armed and work-in-flight; `prs.rs` covers a
running branch session, running / queued / disconnected project sessions
with each of the four variants firing exactly once, immediate push when
already idle, waiting behind a queued push instead of stacking, re-arming on
a failed start, re-arm / cancel, and the store round trip;
`migration_tests.rs` covers 0042; `session_queue/tests.rs` asserts the
`ProjectSessionEnded` kicks with `BranchQueueKicker::recording()`;
`launch_resume/tests.rs` covers the startup hold.

Verified with `pnpm -s check` (0 errors), the affected vitest files, and
the new tests under `cargo test` in `src-tauri`. Full suites were not run:
the machine was under heavy load.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Svelte trims the leading whitespace of an `{#if}` block body, so the
newline before "once the current branch…" and before "The pre-push hook
won't run." vanished, and the description rendered as "local versiononce
the current…", "finish.The pre-push hook won't run." and, for the
immediate no-verify dialog, "version.The pre-push hook won't run."

The joining spaces are now explicit `{' '}` expressions at the start of
each block, so every deferred × noVerify combination renders with a single
space (the rejected title is unaffected). Verified by inspecting the
compiled client output: the static template is ` <!>.<!>` and both
conditional text nodes begin with a space. There is no component test for
this file (the card test stubs the button out), so none was added.
`pnpm -s check` passes with 0 errors.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…ork" feature

Four findings from the review of c5ed5ab, plus a stale test left by 2d29b03.

1. A deferred push no longer stacks on a same-variant queued push (prs.rs).
   When the drain tail finds the branch and project idle, it first looks for a
   push session of the same variant (same force / no_verify) that completed
   successfully on the branch at or after `deferred_push.requested_at`. If one
   exists the flag is cleared and logged instead of fired, so one intent ends
   as one push session. "Successfully" is the pipeline's own verdict
   (completed without the agent, or every step succeeded or skipped); a
   different variant, a cancelled push, or one that completed before the arm
   leaves the deferred push to fire. New store query
   `get_completed_sessions_for_branch_since(branch_id, since)`; `updated_at`
   stands in for a completion time. The firing step now goes through a
   `DeferredPushCheck` (NotArmed / Blocked / Satisfied / Ready).

2. A wait that never ends is now visible (store/sessions.rs, prs.rs, UI).
   Disconnected project sessions still hold the push (a disconnected remote
   session may still be working). `project_has_sessions_in_flight` became
   `project_session_ids_in_flight`, and when the drain tail declines to fire
   it logs the blocking branch and project session ids at info level.
   The `git-deferred-push` footer row also names what it waits on, e.g.
   "Waiting for 2 branch sessions and 1 project session to finish", falling
   back to the generic text when this client can see nothing (a queued project
   session, or a snapshot not loaded yet). This is computed on the frontend
   from live state (`deferredPushWaitingMeta`), so no wire field was added:
   a per-branch count on `BranchWithWorkdir` would only refresh on a
   `branch-changed` publish, which session ends do not trigger, and would have
   gone stale as sessions finished.

3. The frontend's `workInFlight` now matches the backend's idle definition
   (BranchCard.svelte). It counts this branch's timeline sessions and git
   actions plus the project's own sessions: running ones from
   `projectStateStore.getRunningProjectSessionIds` (running sessions the
   registry holds without a branch, i.e. project-note sessions) and
   disconnected ones from `disconnectedSessionsStore.projectSessionsForProject`.
   Sessions on sibling branches no longer offer a wait the backend would not
   perform. The menu tooltips say so. `countGitActionsInFlight` joins
   `isGitActionInFlight` in `branchCardHelpers.ts`.

4. A second selection during the `deferBranchPush` round trip can no longer
   arm twice or start two immediate pushes (BranchCardPrButton.svelte). A
   local `deferPushPending` guards `deferPush` and `cancelDeferredPush`, and
   `buildPrMenu` takes it as `deferPushPending`, marking the deferred rows
   `disabled` while it is set; the dropdown renders that.

Also: 2d29b03 added migration 0042 and its own test but left the sixteen
older `assert_eq!(version, 41)` assertions in `migration_tests.rs` at 41, and
the 0040 and 0041 fixtures had no `branches` table for 0042 to alter, so
`store::migration_tests` failed on this branch. The assertions now read 42 and
both fixtures create a minimal `branches` table.

Tests: `prs.rs` — the stacking test asserts the flag is cleared and exactly
one push session results; new tests cover a completed push of another variant
(three pairs) not satisfying the deferred one, and a cancelled or pre-arm
same-variant push still firing. `prButtonMenu.test.ts` covers the pending
flag disabling only the deferred rows and `deferredPushWaitingMeta`'s wording.
`branchCardHelpers.test.ts` covers the count. `disconnectedSessions.test.ts`
and a new `projectState.test.ts` cover the project-only queries.

Verified with `pnpm -s check` (0 errors), the affected vitest files
(prButtonMenu, branchCardHelpers, disconnectedSessions, projectState,
BranchCard.component), and `cargo test --lib -- prs::tests store::` plus the
`launch_resume` deferred-push tests in `src-tauri` (all passing). The full
suites were not run: the machine was under heavy load from another build.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…hen nothing ended after it

Addresses the review of ed9ef0b: the issue in prs.rs, the sessions.rs
warning about bulk-rewritten `updated_at`, and the BranchCard warning about
branchless sessions.

1. A same-variant push now satisfies the deferred push only when it is the
   last thing that ended on the branch (prs.rs, store/sessions.rs). The
   counterexample: push P queued behind the current work, "Push after current
   work" armed on top, P completes after the arm, then a session S queued
   behind P commits and completes. P could not have carried S's commits, yet
   the old check found P "completed since the arm", cleared the flag and
   never pushed them. `push_satisfying_deferred` (was
   `push_of_variant_completed_since`) now also asks the new store query
   `branch_session_ids_ended_since(branch_id, since, except_session_id)` —
   terminal sessions (`completed`, `error`, `cancelled`) other than the push
   whose row was written at or after the push finished — and treats any hit,
   including a same-millisecond tie, as "not satisfied". Of several
   qualifying pushes the latest is judged. Every terminal status counts
   because a cancelled or failed agent session may still have committed. The
   decision is logged.

2. The push's completion time is no longer the row's `updated_at`, which
   `project_transfer` and `branch_move` rewrite for every session on a branch
   (so an old push could suddenly look newer than the arm and clear the flag
   without pushing). It is now `PipelineExecution::finished_at()`: the latest
   step `completed_at`, written only by the pipeline runner and never
   rewritten after the pipeline ends. No new field or migration. For a
   pipeline that handed off to the agent this is the handoff time, earlier
   than the agent's finish, which only errs toward an extra push. The
   ordering check still has to read other sessions' `updated_at`, which later
   writes only ever move later; the doc comment on
   `get_completed_sessions_for_branch_since` now describes it as a pre-filter
   that over-selects rather than a completion time.

3. The frontend's project-side count (BranchCard.svelte,
   projectState.svelte.ts, disconnectedSessions.svelte.ts) is unchanged in
   code, with the invariant written down: a session only reaches either store
   with a project id, and the backend resolves a branchless session's project
   through `project_notes` alone — `project_active_session` (snapshot and
   Blox reconcile events), `get_project_id_for_session` (terminal emits) and
   the resume plan's event context fall back to the branch's project
   otherwise, and the one `running` emit that stamps a project id without a
   lookup is `start_project_session`, which creates the note. Pikchr diagram
   child sessions, the other branchless rows, resolve to no project and emit
   no status event, so they are never registered. "Has a project id, no
   branch id" is therefore exactly "joined to `project_notes`", matching
   `project_session_ids_in_flight` apart from queued project sessions, which
   only err toward not offering the wait.

Tests: `prs.rs` adds the counterexample (P completes after the arm, S
completes after P, idle → the deferred push fires exactly once) and the
rewritten-row case (an old same-variant push's `agent_id`/`updated_at`
bumped as a transfer does → still fires); `complete_push_successfully` now
records step finish times, and the stacking test lets the millisecond clock
advance between the note ending and the push completing, since a tie now
counts as "after". `store/tests.rs` covers the new query (every terminal
status, ties, the exclusion by id, other branches and running rows left out).

Verified with `pnpm -s check` (0 errors), prettier on the touched frontend
files, the affected vitest files (projectState, disconnectedSessions,
prButtonMenu, branchCardHelpers, BranchCard.component: 140 passing), and
`cargo test --lib -- prs::tests store::tests launch_resume session_queue
session_completion` (307 passing) plus `cargo clippy --lib --tests` with no
warnings in the touched files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
`src-tauri/src/prs.rs` had grown to nearly 5,000 lines on this branch. Per
AGENTS.md (split growing Rust modules by responsibility before they become
hard to navigate) and the PR #1052 review, it is now `src-tauri/src/prs/`,
split along every responsibility it held, not only the deferred push:

- `mod.rs`: module docs, declarations and the re-exports that keep
  `crate::prs::*` paths working for `web_server`, `session_commands`,
  `session_completion`, `pr_poll_scheduler`, `pipeline_dependency`,
  `branches`, `pr_detection` and `upstream_repair`.
- `context.rs`: resolving a branch to its repository and working directory
  (`resolve_branch_repo_and_subpath`, `BranchPipelineContext`,
  `resolve_branch_pipeline_context`, `base_branch_name`).
- `pipeline.rs`: the session-row plumbing every pipeline shares:
  `BranchPipelineResponse`, the prompt constants and `pipeline_prompt`,
  `find_queued_pipeline`, `branch_has_work_in_flight`,
  `RunningPipelineSession` + `launch_running_pipeline_session`, and
  `start_pipeline_for_branch`.
- `git_commands.rs`: the fetch/push shell commands with the HTTPS fallback
  and `push_refspec`.
- `commit.rs`: rebase and squash, including "Rebase and force push" and the
  queued-commit dequeue (`start_queued_commit_pipeline_for_branch`).
- `push.rs`: `PushVariant`, `build_push_pipeline_steps`, push queueing and
  per-variant dedupe, the follow-up force push row,
  `start_or_queue_push_pipeline_for_branch` and `has_unpushed_commits_impl`.
- `pull.rs`: the immediate fast-forward with its marker session, the queued
  pull, and `build_pull_pipeline_steps`.
- `git_pipeline.rs`: rebuilding a queued push or pull on dequeue
  (`rebuild_queued_git_pipeline`, `start_queued_git_pipeline_for_branch`).
- `deferred.rs`: "Push after current work": blockers, the satisfied-by-a-
  completed-push check, arm / cancel / fire.
- `create_pr.rs`: the create-PR pipeline and its handoff prompt.
- `pr_status.rs`: `PrStatusEvent`, single-branch and per-project refresh with
  the bounded pool, `BranchRefresh` collection, and clearing stale status.
- `commands.rs`: the thirteen `#[tauri::command]` wrappers and `get_store`,
  following `project_transfer::commands` / `actions::commands`.
- `tests.rs` + `tests/{commit,push,pull,git_pipeline,deferred,create_pr,
  pr_status}.rs`: the test module, with the shared fixtures in `tests.rs`
  and each test next to the module it covers (the `project_transfer`
  layout).

The move is behavior-neutral: no function body, prompt, command or event
changed, and no item was renamed. The only code edits are visibility
(`pub(super)` on items now shared between sibling modules or reached from
the tests), the per-module `use` lists, module docs, and seven intra-doc
links qualified with their new module path. Call-site changes outside the
tree: the `generate_handler!` list in `lib.rs` names
`prs::commands::<command>`, because tauri's `#[command]` macros are
`#[macro_export]`ed at the crate root and cannot be re-exported through
`prs/mod.rs`; the two source-text tests in `session_commands.rs` that
`include_str!` the dequeue paths now read `prs/commit.rs` and
`prs/git_pipeline.rs`; and a doc comment in `timeline.rs` names the new
command path.

Verified: `cargo check --lib --tests`; `cargo clippy --lib --tests` with no
warnings in the new tree (the five it reports are pre-existing in
`note_transfer.rs` and `orchestrator_tests.rs`); `cargo test --lib -- prs::
session_commands::tests::queued_start branches:: pipeline_dependency::
session_queue:: launch_resume:: session_completion::` (199 passed); the test
and function counts before and after are equal (66 tests, 161 fns), and a
line-level diff of the old file against the tree differs only in `use`
lines; `rustfmt --check` on the tree. Nothing frontend-visible changed, so
`pnpm -s check` was not run.

Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
…the module split

CI on PR #1052 failed in one test,
`web_server::blocking_guard_tests::async_commands_and_arms_keep_blocking_calls_off_the_runtime`.
Its `ALLOWED` table matches file paths exactly, and the entry excusing the
`pull_branch_ff_only_impl` closure (which `pull_or_queue_branch_with` runs
under `spawn_blocking`) still named `prs.rs`. Since 581eb84 moved
`pull_or_queue_branch_for_branch` into `src/prs/pull.rs`, the entry no
longer matched and the walk reported fourteen false offences on
`pull_or_queue_branch`.

The file key is now `prs/pull.rs`. No code outside the test changed.

Verified: `cargo test --lib -- web_server::blocking_guard_tests` passes
(1 passed) in `src-tauri`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
@matt2e
matt2e merged commit 88d24c5 into main Nov 12, 2026
8 checks passed
@matt2e
matt2e deleted the pr-submenu branch November 12, 2026 03:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant