From fb8aa23e668fa42a6ea4fe2e5de4b367bfea9c8e Mon Sep 17 00:00:00 2001 From: iris Date: Sat, 24 Oct 2026 14:05:02 +0200 Subject: [PATCH] swarm-ui: a todo switched from clear to upsert drops its prefix flag --- docs/swarm/ui.md | 9 ++++++--- .../swarm-ui/src/pages/notifications/FlowDraft.ts | 14 ++++++++++++++ .../src/pages/notifications/FlowEditorPage.tsx | 2 +- .../src/pages/notifications/NodeInspector.tsx | 10 ++-------- .../packages/swarm-ui/test/flow-draft.test.mjs | 11 +++++++++++ 5 files changed, 34 insertions(+), 12 deletions(-) diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md index 7cdecf42..50067068 100644 --- a/docs/swarm/ui.md +++ b/docs/swarm/ui.md @@ -258,9 +258,12 @@ Edits stay in a draft until **save**, which sends the whole flows document with the revision the editor read. **discard edits** goes back to the saved flow and **delete flow** removes it, after a confirmation. If someone changed the flows elsewhere in the meantime, the controller turns the save down as stale, and the -editor offers to reload and drop your edits, or to keep them on top of the -latest. For any other refusal the editor shows the controller's own message. -The router runs a saved flow from its next event. +editor offers to reload and drop your edits, or to keep them and replace this +flow's latest version with them. For any other refusal the editor shows the +controller's own message. The router runs a saved flow from its next event. + +Closing or reloading the tab with unsaved edits asks first. Leaving through a +link in the app, or the browser's back button, drops the draft without asking. ### Sending a message to an agent diff --git a/frontend/packages/swarm-ui/src/pages/notifications/FlowDraft.ts b/frontend/packages/swarm-ui/src/pages/notifications/FlowDraft.ts index d7f0c45c..1af89858 100644 --- a/frontend/packages/swarm-ui/src/pages/notifications/FlowDraft.ts +++ b/frontend/packages/swarm-ui/src/pages/notifications/FlowDraft.ts @@ -200,6 +200,20 @@ export function reparent( }; } +// A todo with its op switched. A clear carries no text, and only a clear may +// be a prefix, so each is reset where the new op has no place for it. +export function withOp( + node: Extract, + op: "upsert" | "clear", +): Extract { + return { + ...node, + op, + text: op === "clear" ? "" : node.text, + prefix: op === "clear" ? node.prefix : false, + }; +} + // The node with `next.id` swapped for `next`. A node keeps its place and its // kind-class: the source stays the source, and nothing with children becomes // a todo. diff --git a/frontend/packages/swarm-ui/src/pages/notifications/FlowEditorPage.tsx b/frontend/packages/swarm-ui/src/pages/notifications/FlowEditorPage.tsx index 92b36ecf..e398f3c4 100644 --- a/frontend/packages/swarm-ui/src/pages/notifications/FlowEditorPage.tsx +++ b/frontend/packages/swarm-ui/src/pages/notifications/FlowEditorPage.tsx @@ -185,7 +185,7 @@ export function FlowEditorPage({ reload and drop my edits ) : null} diff --git a/frontend/packages/swarm-ui/src/pages/notifications/NodeInspector.tsx b/frontend/packages/swarm-ui/src/pages/notifications/NodeInspector.tsx index 2a1864df..3eec4668 100644 --- a/frontend/packages/swarm-ui/src/pages/notifications/NodeInspector.tsx +++ b/frontend/packages/swarm-ui/src/pages/notifications/NodeInspector.tsx @@ -16,6 +16,7 @@ import { replaceNode, type Edit, type Limits, + withOp, } from "./FlowDraft.js"; import { kindValues, @@ -237,14 +238,7 @@ function TodoFields({ label="op" value={node.op} options={opList} - onChange={(op) => - onChange({ - ...node, - op: op as TodoNode["op"], - // A clear carries no text. - text: op === "clear" ? "" : node.text, - }) - } + onChange={(op) => onChange(withOp(node, op as TodoNode["op"]))} /> ({ @@ -156,6 +157,16 @@ test("replaceNode keeps the place and the kind class", () => { assert.equal(replaceNode(f, todo("nope")).ok, false); }); +test("withOp drops what the new op has no place for", () => { + const clearing = { ...todo("t"), op: "clear", prefix: true, text: "" }; + const up = withOp(clearing, "upsert"); + assert.equal(up.op, "upsert"); + assert.equal(up.prefix, false, "only a clear may be a prefix"); + const back = withOp({ ...todo("t"), prefix: false }, "clear"); + assert.equal(back.text, "", "a clear carries no text"); + assert.equal(withOp(clearing, "clear").prefix, true, "kept on a clear"); +}); + test("new ids do not collide", () => { const f = flow(); assert.equal(newNodeId(f, "match"), "match1");