Show live status for parallel branches - #660
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds durable identity for parallel branches across the Rust run projection, OpenAPI contract, generated clients, and the web UI so the parallel-stage view can render accurate live branch rows (names/status/links) and stable per-branch stats even when branches are missing projections or have legacy/completed fallbacks.
Changes:
- Persist
parallel_branch_idinStageProjectionand deriveparallel_group_id+parallel_branch_indexforRunStageresponses. - Extend the OpenAPI schema + generated TypeScript client models to expose the new parallel branch identity fields.
- Update the web parallel-stage renderer and tests to show live per-branch rows/links and compute status counts from live branch stages when the parallel stage isn’t complete.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/packages/fabro-api-client/src/models/stage-projection.ts | Adds parallel_branch_id to generated StageProjection client model. |
| lib/packages/fabro-api-client/src/models/run-stage.ts | Adds parallel_group_id and parallel_branch_index to RunStage client model. |
| lib/foundation/fabro-types/src/run_projection.rs | Persists parallel_branch_id in the Rust run projection stage model. |
| lib/foundation/fabro-api/tests/stage_projection_round_trip.rs | Validates JSON round-trip for parallel_branch_id via ParallelBranchId. |
| lib/components/fabro-store/src/run_state.rs | Projects/stabilizes branch identity into stage projection during parallel branch start events. |
| lib/apps/fabro-server/src/server/tests.rs | Adds server test ensuring /runs/{id}/stages exposes parallel branch identity fields. |
| lib/apps/fabro-server/src/server/handler/billing.rs | Populates parallel_group_id/parallel_branch_index in RunStage from parallel_branch_id. |
| lib/apps/fabro-server/src/demo/mod.rs | Updates demo RunStage construction to include the new parallel fields. |
| docs/public/api-reference/fabro-api.yaml | Extends OpenAPI schemas for parallel_branch_id, parallel_group_id, parallel_branch_index. |
| apps/fabro-web/app/lib/stage-sidebar.ts | Extends sidebar stage model + mapping to carry parallel identity fields. |
| apps/fabro-web/app/lib/stage-sidebar.test.ts | Adds unit coverage for sidebar mapping of parallel identity fields. |
| apps/fabro-web/app/components/stage-renderers/parallel-children.tsx | Renders parallel branches using parallelGroupId + parallelBranchIndex and live status counts/links. |
| apps/fabro-web/app/components/stage-renderers/parallel-children.test.tsx | Expands UI tests for loops, duplicates, missing stages, completed fallbacks, and skipped/partial handling. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Branches bypass the engine's stage.started/stage.completed lifecycle, so no SWR key invalidated the stages list while a fork ran. The new live branch rows stayed frozen at their first observed state until an incidental refetch. Map parallel.* events to the stages list, run events, and graph keys. Also: - Label branch rows with formatStageLabel so a re-entered branch renders as `review_glm@2`, matching the sidebar and waterfall. - Build branch rows in one pass and count live outcomes in one loop. - Name ParallelBranchId in the OpenAPI spec and reuse fabro_types:: ParallelBranchId, replacing two copies of an inline string format. - Hoist makeStage and textContent into lib/test-utils so widening Stage cannot leave per-file fixtures stale (tests are excluded from typecheck, so the two component-test copies had already gone stale). - Query stat tiles by data-stat instead of an exact Tailwind class. - Reuse append_scoped_stage_event's body via append_event_with_scope and add test_branch_event instead of poking envelope fields. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Ran a review pass over this branch (reuse / quality / efficiency) and pushed fixes in 690ddd2. One real bug: parallel branches bypass the engine's Also fixed:
Left alone deliberately: the Verified: |
Branch indexes are sparse. A branch queued behind max_parallel reserves no stage identity until it acquires the semaphore, tokio task order is not index order, and a branch cancelled while queued never reserves one at all. Sizing the row list by `stagesByBranchIndex.size` treated an entry count as a dense index range, so a running branch at index 2 with nothing at 0 or 1 rendered as a single "pending" placeholder and the running branch disappeared. Size from the highest index observed. Also: - Derive the Succeeded/Failed tiles from the rendered rows instead of the completed-event rollup, so the tiles cannot contradict the list. This drops the isComplete fork and both live counters. - Show the fallback branch count in the Branches tile, which previously read "-" above N rows in exactly the case the fallback exists for. - BranchRow carries `label` and `stageId`; ChildRow owns the route it links to. `id` had become a display label on one path and a raw node id on the other, and a view model should not hold a URL. - Drop `branchIndex`, which only ever served as the React key and always equalled the array index. - Cover the sparse-index and pending-placeholder paths, and derive the rollup counts in `completedEvent` instead of passing contradictory ones. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Follow-up in 6ad8ba2 — the quality review came back after my first pass and found a second real bug. Late-starting branches could disappear. Branch indexes are sparse: Also:
Not taken, deliberately: collapsing Verified: full workspace tests, nightly fmt + clippy, |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (2)
apps/fabro-web/app/components/stage-renderers/parallel-children.tsx:161
- The "Branches" stat shows "—" whenever
overview.branchCountis null, even though this component already infers abranchCountfromstagesByBranchIndex.size(and uses that to render branch rows). This can lead to the stat disagreeing with the rendered rows when events haven’t loaded yet or for older runs that don’t includeparallel.started.
<StatItem
label="Failed"
value={failureCount}
lib/packages/fabro-api-client/src/models/run-stage.ts:63
- The doc comment for
parallel_group_idduplicates theresumed_from_stage_iddescription ("Canonical stage execution identifier...").parallel_group_idis the parent parallel stage’s StageId, so this comment is misleading for client users.
/**
* Canonical stage execution identifier in `node_id@visit` form.
*/
'parallel_group_id'?: string | null;
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
apps/fabro-web/app/lib/test-utils.tsx:65
textContent()assumes every non-string child is aReactTestInstance, butreact-test-rendererchildren can also be numbers, null, or booleans. As written,textContent(123 as any)will recurse and throw, making the helper brittle for components that render numeric text nodes (there are other test helpers in the repo that explicitly handle numbers).
/** Flatten a rendered subtree to its visible text. */
export function textContent(node: TestRenderer.ReactTestInstance): string {
return node.children
.map((child) => (typeof child === "string" ? child : textContent(child)))
.join("");
}
Conflicts were between this branch's parallel-branch identity work and main's stage billing, review targets, and live stage timing. - Stage fixtures: main added `billing` to each per-file `makeStage`; this branch had hoisted one builder into `lib/test-utils`. Kept the hoisted builder and gave it `billing: makeBilledTokenCounts()`, so both intents hold and the field list stays in one place. `stage-sidebar.test.ts` also builds raw `RunStage` wire payloads, so it keeps importing `makeBilledTokenCounts` directly. - Import lists (`run_projection.rs`, `fabro-api/src/lib.rs`, `run_state.rs`, `stage_projection_round_trip.rs`): unioned both sides — `ParallelBranchId` alongside `timing`, `ReviewTarget`, `ReviewTargetKind`, `AttrValue`, `Node`, and `StageToolBatchProjection`. - `fabro-server` tests: git interleaved two unrelated new tests into one body. Split them back into `list_run_stages_exposes_parallel_branch_identity` and `run_billing_includes_live_stage_timing_in_rows_and_totals`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (3)
docs/public/api-reference/fabro-api.yaml:10663
StageProjection.parallel_branch_idis introduced as aoneOfref but has no property-leveldescription, unlike otheroneOffields in this schema (e.g.provider_used). Adding it improves the generated docs and client model comments.
parallel_branch_id:
oneOf:
- $ref: "#/components/schemas/ParallelBranchId"
- type: "null"
lib/packages/fabro-api-client/src/models/run-stage.ts:66
- The JSDoc for
parallel_group_idis copy/pasted fromStageIdand doesn’t explain that this is specifically the parent parallel stage’s StageId. This is the only client-side documentation for many consumers, so it should match the OpenAPI description.
/**
* Canonical stage execution identifier in `node_id@visit` form.
*/
'parallel_group_id'?: string | null;
docs/public/api-reference/fabro-api.yaml:9962
RunEvent.parallel_branch_idswitched to aoneOfref, but the property-leveldescriptionwas dropped. This makes the event payload docs less self-describing (and some generators ignore the referenced schema description for property docs). Add an explicit description here, consistent with nearbystage_id/parallel_group_id.
This issue also appears on line 10660 of the same file.
parallel_branch_id:
oneOf:
- $ref: "#/components/schemas/ParallelBranchId"
- type: "null"
`parallel_group_id` used `oneOf: [$ref StageId, null]`, and the generator drops a sibling description in that position, so the TypeScript client documented the field as "Canonical stage execution identifier in `node_id@visit` form" — the shared StageId text, which says nothing about what this field means. Switching to `allOf` lets the field's own description through. Dropping `type: "null"` also makes the contract match the server, which omits both fields rather than sending null (`skip_serializing_if` on `Option`, pinned by list_run_stages_exposes_parallel_branch_identity). The Rust types are unchanged — still `Option<StageId>` and `Option<u32>`, which accept an explicit null on input either way — so this only narrows what clients are told to expect on the wire. Wording updated to match, and reworded to avoid an apostrophe the generator escapes into the JSDoc. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 22 out of 22 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
apps/fabro-web/app/lib/run-events.ts:222
PARALLEL_EVENTSinvalidation doesn’t includequeryKeys.runs.billing(runId). Parallel branches bypass the normalSTAGE_EVENTSlifecycle, so on the billing page (useRunBilling) the stage list/totals can stay stale while branches start/complete until some unrelated event triggers a billing invalidation (or the run ends). Consider invalidating billing for these parallel lifecycle events as well (and updating the corresponding run-events tests).
const keys: Key[] = [
queryKeys.runs.stages(runId),
queryKeys.runs.events(runId, 1000),
queryKeys.runs.graph(runId, "LR"),
queryKeys.runs.graph(runId, "TB"),
Summary
Screenshot
Live parallel-stage status while one branch is still running.
Testing