fix(staged): hide the Rebase button while a rebase is queued or running - #938
Conversation
The header Rebase button stayed visible after a rebase was requested — the parent is still ahead until the pipeline lands, so it kept inviting repeat clicks the backend can only dedupe. Pending timeline rows now carry their session's pipeline kind, since the row subject is a display label an agent-pushed ACP title can replace mid-pipeline. BranchCard withholds the header Rebase button while an active (queued or running) rebase row is present; commandPipelinePending already covers the window between the click and the timeline reload that surfaces the row. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
…utton's disabled state Review of the previous commit found two loose ends and one wrong rationale. The `…` menu's Rebase item was still gated only on branchCommandDisabledReason, so it stayed clickable while a rebase ran and could enqueue a duplicate. It now takes rebaseAlreadyInFlight as a separate prop from rebaseSquashDisabled, so Squash — which queues behind the rebase harmlessly — is unaffected. BranchCardHeaderInfo's rebaseDisabled prop was unreachable: the button only renders under `onRebase`, which the caller withheld for the same conditions the prop was meant to disable. The header now hides the button only for an in-flight rebase and passes the reason string through rebaseDisabledReason (matching TimelineRow's convention), so a wrong branch or a command in progress surfaces as a disabled button with a tooltip instead of one that silently vanishes. The rebaseInFlight comment claimed the backend dedupes a repeat click anyway. That holds only for the queued case: queue_commit_pipeline_locked matches against find_queued_pipeline, which reads queued sessions only, so a second request against a *running* rebase inserts a fresh queued one that runs after the first lands. Corrected, so nobody reads the guard as cosmetic and drops it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4638df6f8c
ℹ️ 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".
| * plain commit session, since the subject is a display label an agent-pushed | ||
| * ACP title can replace mid-pipeline. | ||
| */ | ||
| pipelineKind?: PipelineKind | null; |
There was a problem hiding this comment.
Invalidate timelines cached before pipelineKind existed
In the web client, timelines survive deployments in both IndexedDB under the unchanged CACHE_SCHEMA_VERSION = 1 and the unversioned staged:boot:timelines localStorage snapshot. After upgrading while a rebase is active, its cached pending row therefore has pipelineKind === undefined, so rebaseInFlight returns false; a fresh cache can short-circuit revalidation for 10–30 seconds, leaving both Rebase entry points available and allowing a second request to queue behind a running rebase. Bump/version the response cache and invalidate or version the boot snapshot when introducing this field.
Useful? React with 👍 / 👎.
…elineKind field `pipelineKind` was added to `CommitTimelineItem` earlier on this branch to drive the `rebaseInFlight` guard, but neither web-mode layer that persists timeline responses across a deploy was versioned for it. After an upgrade that lands mid-rebase, cached pending rows read `pipelineKind === undefined`, the guard reads false, and both Rebase entry points come back while a rebase is queued or running — the duplicate-enqueue hole the guard exists to close, since the backend only dedupes *queued* rebases. CACHE_SCHEMA_VERSION goes to 2, so IndexedDB entries written by the previous build fail the version check and read as misses instead of short-circuiting the fetch inside their 30s TTL. The `staged:boot:timelines` localStorage snapshot was unversioned, and it is the worse of the two: it seeds the in-memory cache at module init, and an entry under TIMELINE_FRESH_MS makes getBranchTimelineWithRevalidation return `fresh: null` — no fetch at all to correct the shape. Its payload is now stamped with the same constant and discarded (and cleared) on mismatch; a pre-versioning snapshot is a bare record with no `schemaVersion`, so it fails the same check. Sharing one constant across both layers means the next cached-shape change needs exactly one bump. Tests cover both layers: a previous-version IDB entry is a miss in `cachedInvoke` and `cachedCommand`, an unversioned or mismatched snapshot is discarded and its key freed, and persist-then-seed round-trips under the current version. The snapshot tests mock the constant to an arbitrary value so they track the shared export rather than its current number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
The header Rebase button stayed visible after a rebase was requested — the parent is still ahead until the pipeline lands, so it kept inviting repeat clicks. The backend only dedupes the queued case (
queue_commit_pipeline_lockedmatches againstfind_queued_pipeline, which reads queued sessions only), so a second click against a running rebase inserts a fresh queued rebase that runs once the first lands.Changes
CommitTimelineItemgainspipeline_kind, set only on pending rows from the session's pipeline. The frontend needs a structured field here because the row subject is a display label that an agent-pushed ACP title can replace mid-pipeline (e.g. when a conflicted rebase hands off to an agent).rebaseInFlighthelper reads that field to detect a queued or running rebase on the branch, with unit tests.BranchCardwithholds the header Rebase button while a rebase is in flight;commandPipelinePendingalready covers the window between the click and the timeline reload that surfaces the row.…menu's Rebase item now disables onrebaseAlreadyInFlightas well, closing the second path to a duplicate rebase. It's a separate prop fromrebaseSquashDisabled, so Squash — which queues behind the rebase harmlessly — is unaffected.BranchCardHeaderInfoswaps the previously unreachablerebaseDisabledboolean for arebaseDisabledReasonstring (matchingTimelineRow's convention), so a wrong branch or a command in progress surfaces as a disabled button with a tooltip instead of one that silently vanishes.Testing
PipelineKind::Rebasewhile a plain commit session's does not.rebaseInFlight.test.tscovering queued, running, completed, non-rebase pipelines, and a missing timeline.