Skip to content

Run the workflow from the Execution tab with live node states - #93

Open
knihit wants to merge 13 commits into
wc/06b-run-panelfrom
wc/06-execution
Open

knihit wants to merge 13 commits into
wc/06b-run-panelfrom
wc/06-execution

Conversation

@knihit

@knihit knihit commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Part 3 of the Execution tab split. The first two parts are now their own PRs: #115 (part 1/2: API client, run core, node states, no UI) and #116 (part 2/2: the Execution tab, Run button, node badges, first screenshots). This PR keeps its number, head and tree. Its base is #116 (wc/06b-run-panel), so the diff shows only part 3.

What this PR still holds (10 commits, 22 files):

  • Follow a run that is already active (409 already_running), retry failed run reads (3 in a row, none for 401, 404 or an unreadable reply), and a 15 s call timeout with the "server did not answer" sentence.
  • Ignore a second start in the same tick, and document the read retries and the stale-graph limit.
  • Resume after a reload: run-memory.ts, use-execution.ts, App.tsx and main.tsx join a run that is still going.
  • README and CHANGELOG updates.

The description below covers the whole Execution tab, as before.

Description

The Execution tab of the workflow canvas now runs the workflow. It draws the same graph as the Designer, read-only, with a goal box and a Run button. Run starts a run on the API, then reads it once a second, and each node shows its live state until the run ends. The contract and the stub are unchanged, and the page has no stub-only code path: it calls the relative /api, so it works the same against the stub (npm run dev:stub) or a server named by WC_API_TARGET.

Execution tab (workflow-canvas/src/execution/):

  • Execution.tsx: the graph, a Goal box and Run. The goal starts as the outcome of the intent, because POST /api/run needs a goal of 1 to 2048 characters. Run and the goal box are off from the click until the run ends.
  • Each node shows a badge with a word and a colour from the theme: Queued (dashed outline), Running, Done, Failed. The end shows the result (Passed or Halted), and after a halt the stages behind the failed one say Not run. The state is in words and shapes as well as colour.
  • A line under the button says where the run is ("Running draft. 0 of 2 stages done."). It is a polite live region (role="status", aria-live="polite"), and the name of each node for a screen reader ends with its state.
  • An error from the API (the run does not start, or reads keep failing) shows in one alert with the sentence the server gave. The alert is inside a role="alert" region that is always on the page, so a screen reader reads the sentence. A failed stage is a result, not an error.
  • If POST /api/run answers 409 already_running (for example after a page reload in the middle of a run), the tab follows the run named in the answer, as the contract says a client should, and the line under the button says so. A read that fails is tried again after a second; the tab gives up after three in a row. Enter in the goal box runs.
  • The state lives in App, so a run goes on while the Designer tab is open.

Run core (src/execution/core/, no IO):

  • The state of the tab and its reducer, startAndFollow (starts a run or joins the active one, then reads it every 1000 ms until it is no longer running; a failed read is tried again up to three times in a row; a reading of another run ends it with an error), the state of each node, and the status sentences.
  • Its ports are RunPort (start and read a run) and Scheduler (wait for some milliseconds). timerScheduler is the browser adapter; tests use a fake that they move by hand, so no test sleeps.
  • test-support/execution-purity.test.ts holds the core to the same rule as the graph core: it imports only its own modules, the graph core and the wire types, and reads no clock, randomness, environment or network.

API client (src/api/):

  • WorkflowApi gains startRun and getRun. createHttpApi sends the goal as { "params": { "goal": ... } } and reads run_id, and reads the run into an ordered list of stages. A status the contract does not name is a bad reply. The run id is encoded in the path.
  • ApiError carries the run_id of an error body. createHttpApi ends a call after a timeout (15 s) with the "server did not answer" sentence.
  • loadWorkflow ties the run calls to the session token, so only those calls hold it, and turns the 409 already_running into the core's RunAlreadyActive.

Shared pieces:

  • WorkflowGraphCard (the card with the graph) and WorkflowGate (loading, error and empty states) are used by both tabs; designer/states.tsx moved to workflow/states.tsx. buildGraph is unchanged; withStatuses in the graph core adds a status to the nodes it names.
  • Colours of the states are CSS variables in canvas.css, and a test checks that they match the theme module and reach WCAG 2 AA in both modes.

Docs: workflow-canvas/README.md has an Execution tab section, and CHANGELOG has an entry under Unreleased. VERSION is untouched.

How to run

cd workflow-canvas
npm ci
npm run dev:stub

Open the address it prints, open the Execution tab and press Run. The stub fakes a run: the draft stage runs for 3 seconds, then review, then the end shows Passed.

Fixes from the hands-on QA run

  • A reload in the middle of a run showed "Ready". The tab now keeps the id of the latest run in sessionStorage (the id is no secret; the session has its own memory since Draw the workflow on the Designer tab with React Flow #83). When the page opens again it reads that run once, and a run that is still going is followed as a joined run, without a click on Run. A run that has ended, one that cannot be read (for instance after the server started again), or a browser that blocks storage leaves the tab ready for a new run.
  • Code: resumeRun in the run core, RunMemory port with sessionRunMemory as its adapter, and useExecution takes the memory; App takes an optional runMemory and main.tsx passes the real one.
  • Tests: unit tests of resumeRun (still running, ended, unreadable, another run, aborted, a failure while following), of the hook and of the memory (including blocked storage); end-to-end test "shows the run that is still going when the page is opened again", which failed before the change, and one that a finished run leaves the tab ready.
  • Checked on this head: npm test (58 files, 995 tests), npm run test:e2e (68 tests) and npm run screenshots pass; lint and typecheck are clean.

Type of Change

  • Documentation
  • Other: the Execution tab of the workflow canvas, in workflow-canvas/

Testing

  • Root npm test (tests/scripts/**/*.test.js): not run; this PR changes only workflow-canvas/ and the root CHANGELOG.md.
  • If cli/ changed: not applicable
  • Smoke tested affected agent(s): not applicable, no agent changed
  • If an agent's or skill's behavior changed: not applicable
  • New or changed code files carry the required SPDX header (see AGENTS.md's License headers rule); formats without comment syntax (e.g. JSON) are exempt

Checked in workflow-canvas/: npm run lint, npm run typecheck (3 projects), npm test (58 files, 995 tests), npm run build and npm run test:e2e (68 tests, against the stub) all pass. npm run screenshots gives byte-identical files on a second run. The build prints only the existing chunk-size warning.

Tests added:

  • Unit, run core: the reducer (every state, an event that does not fit is ignored, a reading of another run is ignored, a joined run is remembered); startAndFollow with a fake scheduler (polls every 1000 ms, no wait before the first read, stops on passed and on halted, retries a failed read and counts only failures in a row, joins the active run, one failed event for a refused start, repeated failed reads or a reading of another run, nothing is emitted after an abort); the node states for a running, passed and halted run; the status sentences; the goal check (empty, 2048 and 2049 characters, characters outside the basic plane); the no-IO rule of the core.
  • Unit, UI and client: getRun and startRun (request shape, token, encoded id, parsing, bad replies, error sentences); the timer adapter with fake timers; withStatuses; the tab through App with a fake server and scheduler (idle, Run disabled while in flight and from the click, each node going queued -> running -> done, a failed run, one alert for a refused start and for a failed read, the alert cleared by a new run and announced through its region, a joined run, a read that fails once, Enter in the goal box, the run continuing on the Designer tab, reading stops when the page closes, loading and failed-load states); the state colours (contrast and match with the theme module).
  • e2e (e2e/tests/execution.spec.ts): the tab before a run; a real run on the stub that clicks Run and sees draft go Running -> Done, then review Running -> Done, then the end Passed, in that order, with Run off in between; the live region and node names; a failed stage, a refused run, a joined run (409 with the run id) and a dropped read (the stub never fails a stage and runs one run at a time, so these answer the run calls in the test); the run kept across tabs.
  • e2e (e2e/tests/accessibility.spec.ts): axe on the Execution tab with a stage running, a failed stage and a passed run, and with a failed stage in the dark mode.

Screenshots

Execution tab, full page, 1280x800 (regenerated by npm run screenshots):

Idle Mid-run (first stage running)
Execution tab, before a run Execution tab, first stage running
Finished (passed) Mid-run, dark theme
Execution tab, run passed Execution tab, first stage running, dark theme

Checklist

  • My code follows the style guidelines of this project (see AGENTS.md's Code Style & Conventions)
  • I have performed a self-review of my own changes
  • I have commented my code where necessary
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings — build is clean
  • I have added or updated tests that prove my change works (see Testing above for the exact commands)

Notes for Reviewers

Fixes from the second adversarial review

  • The look for a remembered run now tells errors apart. A server that will not give the run stays quiet; a lookup that fails for another reason (no answer, 5xx) shows the alert for a run that may be going. The first reading is tried again like any other.
  • Pressing Run while the look is pending stops the look and starts a run, instead of doing nothing.
  • A start with no answer, or accepted in a reply the page cannot read, says "The run may not have started" and that Run joins it.
  • The launch code call has no timeout, as the code works once.
  • While the tab follows a run that was already going, the Goal box says "Not known" instead of showing another goal, and the status line keeps saying the run was already in progress after it ends.
  • The README says a plain reload resumes, since the page keeps its session in the tab (Draw the workflow on the Designer tab with React Flow #83). A new e2e test starts a run on a stub whose launch code works once, reloads during the run, and checks that the run is followed to the end with no request to POST /api/session. It fails when the session memory is switched off. A restarted server makes a reload ask for a fresh launch address.
  • Rebased onto the new wc/06b-run-panel.

Fixes from the third adversarial review

Fixes from the second hands-on QA run

  • The run id kept for a reload is forgotten when another workflow opens, so a reload does not look for it on another server. The test that kept a joined run when the runs changed now covers a repeated render; a new test covers the change of workflow. README line.
  • Restacked on Draw the run on the Execution tab with a Run button and live node states #116, which lets go of the run on a change of workflow.

Round 6

Rebased on #116. New e2e test: a 401 on the run start shows the lost-session message and leaves the tab storage empty (it failed with the guard removed).

Round 9: rebased on #116. No change of its own. npm test (1089 tests), lint and typecheck pass.

@knihit
knihit added this pull request to stack #79 October 26, 2026 04:49
@knihit
knihit marked this pull request as ready for review October 26, 2026 21:03
@knihit
knihit removed this pull request from stack #79 October 27, 2026 02:47
@knihit
knihit added this pull request to stack #101 October 27, 2026 02:47
@knihit
knihit removed this pull request from stack #101 October 27, 2026 02:48
@knihit
knihit added this pull request to stack #102 October 27, 2026 02:48
@knihit
knihit force-pushed the wc/06-execution branch 5 times, most recently from 23ab802 to 9b7eaee Compare October 27, 2026 21:25
@knihit
knihit force-pushed the wc/06-execution branch 2 times, most recently from f1e444e to e3ad08c Compare October 27, 2026 22:23
@knihit
knihit removed this pull request from stack #102 October 27, 2026 22:40
@knihit
knihit changed the base branch from wc/05b-theme to wc/06b-run-panel October 27, 2026 22:41
knihit added 13 commits October 29, 2026 02:43
…calls

A 409 already_running names the active run, so the Execution tab follows it
instead of ending in a dead end, for example after a page reload. A failed
read is tried again before the tab gives up, a reading for another run ends
the follow with an error, and a call that gets no answer ends after a
timeout. The error sentence is announced through an alert region, and Enter
in the goal box runs.
canRetryStatus in api/api.ts holds the rule next to the "0 = no answer"
convention: no answer or a 5xx can be tried again, any 4xx cannot. The
README now says any 4xx, as the code does.
…reload

After a reload the Execution tab said Ready although a run was going. The tab
now keeps the id of the latest run in sessionStorage and, when the page opens
again, reads that run once. A run that is still running is followed as a joined
run. A run that has ended, cannot be read, or a blocked storage leaves the tab
ready.
- Run pressed while the page is still looking for a run to join stops the
  look and starts the run, instead of doing nothing.
- The first reading of the remembered run is tried again like any other.
- The launch code call has no timeout: the code works once, so giving up on a
  slow answer would leave no way back in.
…o answer from one refused

- The look for the remembered run reports a run that may be going when the
  readings fail, and stays quiet only when the server will not give the run.
- A start that got no answer, or was accepted in a reply that could not be
  read, says the run may not have started and that Run joins it.
- While the tab follows a run that was already going, the Goal box says the
  goal is not known instead of showing another goal; the status line still
  says the run was already in progress once it ends.
- The README says resume after a reload works only where the launch code can
  be traded again, and a test shows what a reload does on a server that
  follows the contract.
…in the code timeout

The remembered run id is dropped once the run ends or turns out to be gone, so
a failed look at page open does not name a run that finished long ago. A run
that a reload joined is no longer cut off when the page gets new runs. The test
for the launch code call without a timeout now ends the call on abort, so it
fails when the timeout comes back. The README says what a reload does.
The run id kept for a reload belongs to the session that was left. It is
dropped with the run, so a later reload does not look for it on another
server. A run that a reload joined is still not cut off by a repeated
render, only by another workflow.
The page keeps its session in the tab, so a reload no longer ends in no launch code. The end-to-end test reloads during a run on a stub that takes the code once and checks that the run is followed again.
Drives the page: the start is answered 401, the run error says the server lost the session and where to get a new address, and the tab storage is empty.

This branch has not been deployed

No deployments
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