Skip to content

Save the workflow from the Designer, with checks while typing and conflict handling - #100

Draft
knihit wants to merge 486 commits into
wc/07b-propertiesfrom
wc/07c-save
Draft

knihit wants to merge 486 commits into
wc/07b-propertiesfrom
wc/07c-save

Conversation

@knihit

@knihit knihit commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Description

Layer 3 of 4 of the editing work in the Designer. It adds saving: the page can now write the edited stages to the file, check them while you type, and handle the file changing under it. A stage is still only a name and an agent, and the page writes no other key. The API contract and the stub are unchanged, and the page has no stub-only code path.

Designer:

  • Save and a save line (No changes, Unsaved changes, Saving, Saved, a polite live region) sit in the header of the graph card. Nothing is written until Save. Save sends PUT /api/workflow with the base_hash of the file you loaded.
  • Checking: 400 ms after the last change the text goes to POST /api/validate. What it finds shows under the field it is about, and in one alert above the graph with the stage name in front. The findings of the last check stay on show while the next one is pending, so the alert does not flicker. Only an error stops a save (Save is off, with the reason in a tooltip); a warning or a note shows as a note and Save stays on. A 422 on save shows its findings the same way.
  • Conflict: a 409 shows an alert (The file changed on disk) with Load the file again, which replaces the draft; focus then moves to the name field.
  • A save whose answer is lost (no answer, or a server error) may still have written the file, so the page reads the file again: if it holds the text that was sent, the save counts as done; otherwise the failure is shown.
  • A file whose stages are not a block gets an info note before the first save, saying that a save writes the whole file in the plain layout.
  • Execution tab: it draws the saved workflow (a run uses the saved file), so it shows what a save wrote.

Core (designer/core/, no IO): ports.ts (EditPort, SaveConflict, SaveRefused, SaveLost), run-check.ts (checkAfterPause), run-save.ts (saveDraft, reloadWorkflow) and the save status line selector. Tests move time by hand with the Scheduler of the run core, so none sleeps.

API client: saveWorkflow and checkWorkflow; ApiError carries the diagnostics of an error body; loadWorkflow ties the edit calls to the session token and turns a 409 conflict and a 422 invalid_workflow into the core's errors. messageOf is shared in api/message-of.ts by the run and edit code.

Tests and docs: unit tests for the loops, the client, the edit port and the Designer's save, check, conflict and failure flows; stub-api/http/ui-wire.test.ts checks that the text the page writes is accepted by the stub and read back, and that a stale hash is 409; test-support/editing-contract.test.ts pins the limits and calls to api/openapi.yaml. The README section covers saving, checking and the conflict. The capture spec gains the saved, problems and conflict screenshots.

How to review

  • Review the diff against Edit stages in the Designer: Properties, agent list, add, rename and delete #99 (the GitHub diff of this PR is already against it).
  • Start with core/run-save.ts and core/run-check.ts with their tests (the rules), then use-editor.ts (how the loops are wired), then load-workflow.ts and http-api.ts, then EditAlerts.tsx and SaveControls.tsx.
  • Run it: cd workflow-canvas && npm ci && npm run dev:stub, change an agent, press Save, and reload to see it kept until the stub stops.

What the next layer adds

PR #97: the editing Playwright project (the whole flow against the stub, with axe in four states), the README paragraph on it, and the CHANGELOG entry.

Fixes from the hands-on QA run

  • A repeated name in the list of problems: the list leaves out the copy that marks the earlier stage's field (see Add the editing core of the Designer: draft, stage text, findings and editor state #98), and a test drives it through the page.
  • An empty name read ": name must be ..." in the list. A stage with no name is now called by its place in the list ("Stage 1: ..."). Test: DesignerEditor.test.tsx, which failed before the change.
  • A stopped server or a lost session gave no next step ("The server answered 502.", "The session token is missing or wrong."). messageOf now adds what to do: for no answer, or a 5xx that is not the API's, "If it stopped, start it again and get a fresh launch address from the server."; for the API's 401, "Get a fresh launch address from the server." The lost-save and refused-read errors use it too. Tests: message-of.test.ts, load-workflow.test.ts, and end-to-end e2e/tests/server-gone.spec.ts (load, run, session lost, save), which failed before the change.
  • Checked on this head: npm test (78 files, 1398 tests) and npm run test:e2e (83 tests) pass; lint and typecheck are clean.

Type of Change

  • Other: saving on the Designer tab of the workflow canvas, in workflow-canvas/

Testing

  • Root npm test (tests/scripts/**/*.test.js): not applicable; this PR changes only workflow-canvas/.
  • If cli/ changed: not applicable, cli/ is untouched
  • Smoke tested affected agent(s): not applicable, no agent changed
  • If an agent's or skill's behavior changed: not applicable, none changed
  • 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/ on this head: npm run lint, npm run typecheck (3 projects), npm test (78 files, 1398 tests), npm run test:stub (20 files, 359 tests), npm run api:lint, npm run build and npm run test:e2e (83 tests) all pass. npm run screenshots was run and its files are committed; a second run gives the same files.

Screenshots

Designer tab, full page, 1280x800 (regenerated by npm run screenshots; the links are pinned to the head commit of this PR):

Unsaved edits (agent changed, stage renamed) After a save
Designer tab with unsaved edits Designer tab after a save
A problem the server found The file changed on disk
Designer tab with a duplicate stage name Designer tab with a conflict
Unsaved edits, dark theme Designer on open, with the save line
Designer tab with unsaved edits, dark theme Designer tab on open

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

  • A conflict offers Copy my version (clipboard, or a box if the page may not), and keeps it after the file is loaded again, so the draft is never lost silently. The stages cannot be changed while the file loads.
  • A conflict whose file already holds the text that was sent counts as saved; the 409 current_hash tells a deleted file from a changed one without a read.
  • A 200 with an unreadable body is a lost answer; a lost answer with an unreadable file says the save may or may not have gone through ("The save did not finish").
  • A refusal with no error among its findings shows the sentence of the server. A lost session at a save warns that the draft is lost. The "saving rewrites this file" note shows only when the workflow can be edited. The e2e helpers fail on a refused call.
  • This branch takes the new wc/07b-properties by merge (a rebase conflicted in ten files for each of five commits), so its history is kept and no force push was needed.
  • Rejected: moving message-of out of the cores and the single source of truth for "a save is in flight" (refactors with no behaviour change); the wall-clock test noted in the review is a separate fix.

Fixes from the third adversarial review

  • The note with Copy my version that follows a reload stays while the person goes on to edit, until the file is loaded again or a save works (before, the first key dropped it).
  • A 401 from a proxy counts as a lost session like the API's own, at a save and at a check. A check that meets a lost session says so at once ("The session is lost"), because no save can work.
  • The test for the "rewrite" note on a read-only workflow now fails when the guard is removed. The e2e helper currentWorkflow fails on a refused call. The comments of the purity tests name the message helper.
  • Pressing a key in the Execution tab no longer redraws its canvas: the saved result is one object until the saved file changes.
  • Rebased onto the new wc/07b-properties by merge (the history is append-only, as before); the merge also takes the new layouts of the list from Add the editing core of the Designer: draft, stage text, findings and editor state #98.
  • Rejected: a save id on the answers of a save (see Add the editing core of the Designer: draft, stage text, findings and editor state #98); the Copy box inside the alert region (it shows only when the clipboard is refused, and no screen reader test shows a problem); keeping the "save did not finish" note across an edit (Copy my version covers it).
  • Blocked on a decision: a way to keep your version over the new file on a conflict.

Fixes from the second hands-on QA run

  • The problems of the check sit under Properties, not above the graph, so one that appears while the person types no longer pushes the name field and the graph down by 120 px (an e2e test measures both; it failed before). The conflict, reload and failed-save alerts stay on top. The problems screenshot is retaken.
  • The names in the list of problems are shown as code, without backticks.
  • Takes the new Edit stages in the Designer: Properties, agent list, add, rename and delete #99 by merge.

Round 6

Takes #99 by merge. A 401 on a save, a check or the reload of the editor now clears the kept session (the guard from #83) and shows the shared lost-session sentence, then the note that the draft is lost. The server-gone e2e test for a run start now also checks the message and the empty storage. Unit tests for the three calls failed with the guard removed.

Round 7: the lost-draft sentence now says "when you open a fresh address", because the lost-session message ends with "An address works once." and "when you do" no longer points at one thing.

Round 8: takes #99 by merge, with the old #99 head as the merge base. Three tests of the pending session in load-workflow.test.ts match the start of the error, because this layer adds advice after the server's message. npm test (81 files, 1482 tests), lint and typecheck pass.

Round 9: takes #99 by merge, with the old #99 head as the merge base. A test of the pending session matches the start of the error, because this layer adds advice. The <code> font fix and the re-shot designer-problems.png are kept. npm test (81 files, 1493 tests), lint and typecheck pass.

@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/07c-save branch 5 times, most recently from 8628432 to c14dd6f Compare October 27, 2026 21:25
@knihit
knihit removed this pull request from stack #102 October 27, 2026 22:40
@knihit
knihit added this pull request to stack #117 October 27, 2026 22:42
@knihit
knihit force-pushed the wc/07c-save branch 2 times, most recently from 0d652a5 to c1a344b Compare October 28, 2026 14:31
knihit added 26 commits October 29, 2026 01:42
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.
… editor state

The IO-free part of editing a workflow in the Designer, with no UI yet:
- the draft of the stages, where each stage keeps an id while it is renamed
- the text a save writes, which replaces only the block of stages
- the findings of a check, put on the stage and field they are about
- the state of the editor and its reducer, with the selectors on it

The wire types gain the hash and read-only flag of a workflow and the path
and severity of a finding, and the page reads them.
The server reports a repeated stage name on the later stage only. A person who
renames the first stage to a name the second one has saw no mark at the field
they were editing. The problems of the draft now hold a copy of such a name
problem for the earlier stages with that name. A list of problems leaves the
copies out, and a message that several stages repeat is joined once.
… ignore late answers

- A save keeps the lines of every stage that is unchanged, with the comments
  in front of it and the comments after the last stage. Line endings come
  from the stages line, a lone CR, a BOM and a quoted key are handled, and
  names that YAML 1.1 reads as booleans are quoted.
- The revision of the draft only grows, also across open and reload, and an
  answer to a save or a reload counts only while that save or reload is on.
- Deleting a stage that is not in the draft is not a change, and a name used
  three times marks the first stage once.
- A failed save can say that it may have been written.
- A test ties the stage limit of the draft to the contract.
…out with the validator

A stage that is new or changed is written with its dashes where the file's own
items have them, so a list with four spaces, three, or none stays one list.
A list that cannot take items, such as a flow list on the line below stages,
is written whole, which the notice already warns about. A kept last stage keeps
its own line ending in a file with mixed endings.
…, add, rename, delete

The Designer tab gets Properties for the selected stage, with a name field,
a list of the agents Konductor ships, and Delete stage. Add stage adds the
next agent-N. A stage is chosen by a click, or by Enter or Space on its node,
and focus moves to its name. The draft is kept in App, so it survives a
switch of tab, and the Execution tab says when it holds changes that are not
saved. Nothing is written to the file yet.

The graph names a node by the id of its stage, so two stages that share a
name for a moment still have two nodes, and the canvas fits the graph again
when a stage is added or deleted.
…aved

The draft lives in the page only, so a reload or a closed tab threw it away
without a word. While the draft differs from the saved file, a beforeunload
handler now makes the browser ask whether to leave.
…r when read-only

- Enter or Space on the stage that is already selected moves focus to its
  name, and Escape on a node no longer throws focus away.
- The Complete and Needs a fix mark shows only for a draft that a check has
  answered for; before that it says nothing.
- A polite live region says that a stage was added, deleted or selected.
- A read-only workflow gets a note that says why, and the card header no
  longer asks to select a stage to edit it.
- An agent that the saved file names stays on offer after another is chosen.
- Two stages with one name say their place in the list to a screen reader.
- The name box turns off spell check and autocomplete; the strong text is a
  Cloudscape strong; the agents test skips without the repository.
The note above the graph spans both columns of the Designer grid, so the
graph keeps the wide column. An add names the new stage, and each
announcement is a new element, so a repeat is read again. Escape is kept from
React Flow only on a canvas that can select. Browser tests cover the note's
layout and Enter, Space and Escape on the selected stage.
Focus went to the name, but the text stayed unselected, so typing was added to
agent-1. The name is now selected.
…d run

Pasting another launch address replaced the workflow and dropped the draft
without a word. The page now asks first. Cancel keeps the page as it is; the
address can be pasted again. The question also names a run being followed,
which is let go when another workflow opens.
At this layer a failed load adds advice after the server's message, so the tests that pin the error of a pending or fallback session match its start.
The code element fell back to the generic monospace family, which the
host resolves to different fonts, so the problems screenshot differed
between machines. Apply the bundled JetBrains Mono and re-shoot it.

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