dwarves — the Forge(live) run-picker
A follow-up deferred in
3e082eb(theGetRunReport+dwarves-embedlive tab): the live pane rendered only the newest run (or the one pinned via$DWARVES_RUN_ID). This adds a run-picker — a dropdown of recent runs — so a viewer chooses which historized run to render, live, from the UI. No proto change (theHistoryRPC already carried the run list), additive (unpinned still ⇒ newest), and it reuses the sameGetRunReport→ForgePipelineAdapter→PipelineViewpath.
The one-call fetch (dwarves-embed::fetch_forge_split)
The pane's fetch returns both halves in a SINGLE connection, with the two legs split:
fetch_forge_split(endpoint, token, run_id, history_limit) -> Result<ForgeFetch>— oneHistory(limit)call feeds both the picker list and newest-run selection (whenrun_idis unpinned), then oneGetRunReport(id)fetches the selected run.ForgeFetch { runs: Vec<RunRow>, report: Result<RunReport> }keeps the legs apart: the outerErris the LIST leg failing (couldn't connect /Historyerrored ⇒ genuinely unavailable); areport: Err(…)is only the SELECTED run's report failing (a bad pin / a run that vanished) while the run list is fresh.fetch_forge(…) -> (Vec<RunRow>, RunReport)folds the split back to the strict all-or-nothing pair (a report error ⇒ overallErr);fetch_run_reportis a thin wrapper over that. No twin — three functions, one fetch body.
Report-error resilience — a bad pin never blanks the picker
When History succeeds but the pinned run's GetRunReport fails, the pane lands in a distinct report_error Conn state instead of collapsing to unavailable:
apply_fetch(Result<ForgeFetch>)(split out ofrefreshso the transitions are drivable headless) repopulatesrunsfrom the freshHistorylist before looking at the report — so the dropdown stays usable to switch to a working run. Only when the forge has no runs at all does a report error stay the plainunavailable/no-runs state.- The control strip shows an amber "run <id> unavailable — pick another run" (the error on hover);
state_json.status == "report_error"withrun_countintact. Picking a good run re-fetches and clears the error. Previously the whole result was discarded on a report error, throwing the freshly-fetched list away — the fix that closed the last run-picker deferral.
The pure mapping (run_picks_from_wire)
RunPick { run_id, pipeline, status }— one dropdown entry,Serializeforstate_json.run_picks_from_wire(&[RunRow]) -> Vec<RunPick>— pure, order-preserving (newest-first, as the server returns), so the picker list is unit-testable with no server.
The pane (ForgePanel)
- Two new fields:
runs: Vec<RunPick>(the offered list) +history_limit: u32(DEFAULT_HISTORY_LIMIT = 20).refresh()→apply_fetch()repopulatesrunsfrom the freshHistorylist whenever the list leg succeeds; a dead-server fetch error leaves the list as-is (a blip doesn't blank the dropdown), and a report-only error keeps the fresh list (thereport_errorstate above). - UI: an
egui::ComboBoxin the control strip —● newestplus one entry per run (run_id · pipeline · status). Selecting an entry pins itsrun_idand re-fetches;● newestclears the pin. The click drives the exact same seam a robot uses. state_jsongainsselected_run_id(the pin;null⇒ newest),runs,run_count,history_limit— the robot-observable picker surface.update_jsonalready pinned{"run_id": "…"}(""⇒ newest); now also{"history_limit": N}resizes the list. Both re-fetch.
Why this shape
- Reuse, no twin (L5).
fetch_run_reportbecomes a wrapper overfetch_forge; the render path (GetRunReport→ForgePipelineAdapter::from_report→PipelineView) is untouched. No new RPC, no new viewer. - Additive (L2). The default (unpinned ⇒ newest, auto-poll) is byte-for-byte the prior behaviour; the picker is purely additional surface.
- Robot-tested (L4). The picker (list + pin + clear) and the
report_errorrecovery drive headless overstate_json(apply_fetch→state_json), closing the coverage gap on the newest UI surface (the live-forge tab was mounted but not yet robot-ledgered).
Deferred
- The picker lists
History(limit)runs (default 20) — no paging / infinite scroll. - No live-search/filter box over the list yet (fine at 20; add a filter if
limitgrows). - Selection is by
run_idonly; a richer label (project + finished-at) is cosmetic.