Skip to content

feat: Workflow builder rebuild + desktop fixes (the un-merged half of #140) - #142

Merged
hoangsnowy merged 8 commits into
mainfrom
feat/workflow-builder-rebuild
Aug 10, 2026
Merged

feat: Workflow builder rebuild + desktop fixes (the un-merged half of #140)#142
hoangsnowy merged 8 commits into
mainfrom
feat/workflow-builder-rebuild

Conversation

@hoangsnowy

Copy link
Copy Markdown
Owner

What & why

The RemoteAgent zero-cost path merged in #140, but that squash landed before the rest of the session's work was pushed. This PR brings the 8 commits that never reached main: the desktop-correctness fixes and the full Workflow app rebuild. Branched off current main and cherry-picked cleanly (no conflicts); Release build 0/0, phantom-CSS clean (569 classes), 847 tests pass.

Desktop / correctness fixes

  • Provider badges — every agent row (incl. the Orchestrator) names the provider the call actually uses; forcing RemoteAgent no longer renders "Azure".
  • Minimize destroyed appsWindowHost dropped minimized / off-workspace windows from the render tree, so Blazor disposed them (a running pipeline, typed input, produced artifacts — gone). Windows now stay mounted, hidden via CSS.

Workflow app rebuilt (n8n / Opal-class visual builder)

  • G1 — three-region layout: searchable node library · canvas + run drawer · docked inspector. Slim toolbar, bigger window.
  • G2 — ⌘K command palette + "+ Add", and auto-connect (adding while a node is selected wires the edge).
  • G3 — per-node run status dots + a Run section in the inspector (status / tokens / cost / failure reason).
  • G4 — session-scoped recent-runs list (replaces "history not persisted"); fit-to-view clamped so it never over-zooms.
  • Undo/redo — snapshot-based, ⌘Z / ⌘⇧Z + toolbar buttons.
  • G5 — RemoteAgent reports estimated tokens (the CLI gives none); the status bar shows the would-be metered-API cost next to the real $0 — "$0.0000 cost (≈ $0.0071 API)". Never presented as billed spend.

Each item was verified in the running app (standalone Web via Node Playwright, light + dark) during the session — screenshots covered the three-region layout, docked inspector, per-node run status, the ⌘K palette, auto-connect, recent-runs, and undo/redo counts (5→6→undo 5→redo 6). Preview card pushed to the Claude Design project (ui_kits/agentos/workflow.html).

Type of change

  • Bugfix
  • New feature (Workflow builder)

Test plan

  • dotnet build AgentOs.slnx -c Release — 0 warnings, 0 errors
  • dotnet test AgentOs.slnx -c Release — 847 passed, 14 skipped
  • check-classes.ps1 — no phantom classes (569 defined)

🤖 Generated with Claude Code

hoangsnowy and others added 8 commits August 10, 2026 20:07
PipelineStudio rendered a per-agent provider chip from a hardcoded map
(Requirement/Qa/Aggregate = anthropic, Coding/Testing = azure). A Force provider
overrides every agent's own setting — LlmClientFactory resolves the forced key,
not Agents:<Stage>:Provider — so while one is active that map does not merely go
stale, it contradicts the run. Forcing RemoteAgent still rendered "Azure" next to
Coding and Testing, telling the operator their work went to a cloud endpoint when
every token had been produced by the CLI on their own machine. On a run whose
entire point is "0 server tokens", that is the one label that must not lie.

ProvName now returns the effective Force provider when there is one (read from
the same LlmConfigView snapshot the "Live · provider" chip and the run itself
use), and falls back to the per-agent map otherwise.

Verified against the run that exposed it: with Force = RemoteAgent, the pipeline
completed as Requirement (7 FR, 2 entities) -> Coding (17 files) -> Testing
(21 tests, ~85% cov) -> QA (pass, 0.87) -> Aggregate at 0 tok / $0.0000 / 193.3s,
every call dispatched to the paired runner's claude CLI.

Release build 0 warnings / 0 errors; 847 tests pass, 14 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The leader row's chip was the literal "Claude" in markup, so after the previous
commit the five specialist rows correctly read RemoteAgent while the Orchestrator
above them still named Anthropic — and the leader row is the first thing a viewer
reads. The colour class was also still driven by the hardcoded per-agent map, so a
badge could read "RemoteAgent" while tinted as Azure.

Both now derive from the effective config: LeaderProv returns the Force provider
when one is set, and ProvCss maps any provider name onto the two badge classes
that actually exist in app.css (.prov.anthropic / .prov.azure) rather than
inventing a class no stylesheet defines — a phantom class renders unstyled, which
design-system.md calls out as a past cause of broken windows.

Verified on the running Aspire stack with Force = RemoteAgent: all six rows
(Orchestrator + the five specialists) read RemoteAgent, matching the
"RemoteAgent (forced)" toolbar chip. check-classes.ps1 reports no phantom classes
(514 defined, all static .razor tokens resolve). Release build 0 warnings /
0 errors; 847 tests pass, 14 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WindowHost rendered only `!w.Minimized && w.Workspace == ActiveWorkspace`. Blazor
disposes a component the moment it leaves the render tree, so minimizing a window
— or switching virtual workspace — threw away everything it held: a pipeline run
in flight, the story typed into it, the artifacts already produced. The
server-side work kept going with nowhere to land. On a desktop metaphor, minimize
must hide a window, not kill the app inside it.

The stylesheet had assumed this all along: `.appwin.minimized { display: none }`
has existed since the chrome was written, and was simply unreachable. WindowHost
now renders every open window and lets CSS hide it; AppFrame adds `off-workspace`
for a window parked on another workspace, with a matching rule next to the
minimized one.

Verified on the running Aspire stack: with a probe string typed into the Agents
window, clicking Minimize leaves the window mounted (`appwin focused minimized`,
`display: none`) with the textarea value intact — where it previously vanished.
check-classes.ps1 clean (515 defined, all static .razor tokens resolve); Release
build 0 warnings / 0 errors; 847 tests pass, 14 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Workflow editor put a 14-chip "Add step" strip across the top, floated the
inspector over the node it was editing, and stacked new nodes on top of each
other via a modulo formula — it read as a diagram toy, not a builder, and the
user could not tell what a run produced without opening the separate Pipeline
app. This restructures it into the three-region layout every serious builder
(n8n, Google Opal, OpenAI Agent Builder) uses, and surfaces run results in place.

Layout (G1):
- LEFT — a searchable node library, grouped Agents / Flow / Data / Control, each
  node a card with icon, name and a one-line hint, colour-coded to the category
  it will paint on the canvas. Replaces the always-on chip strip (progressive
  disclosure — the palette is there when you want a node, not in your face).
- CENTER — the canvas, with a proper zoom cluster (in / % / out / fit), the
  minimap, and an empty-state hint when there are no nodes.
- RIGHT — a docked, persistent inspector that pushes the canvas rather than
  covering it; shows an empty-state when nothing is selected.
- Toolbar slimmed to workflow-switch, name, live-provider chip, Save, Run, and an
  overflow "⋯" menu for Duplicate / panel toggle / Delete. Default window grown
  1080×660 → 1320×840 so the canvas is actually usable.

Run visibility (G3):
- Each node carries a live status dot in its head (spinner while running, ✓ done,
  ✕ failed, – skipped) so progress reads on the card, not only in the log.
- The docked inspector gains a Run section: selecting a node shows its status,
  its metric line (tokens / cost / elapsed) and, on failure, the reason — so
  "what did this node produce / why did it fail" is answerable without leaving
  the canvas. RunMessage is captured per node for that.

Node insertion no longer stacks: a new node lands to the right of the selected
(or rightmost) node on the same row, so it reads as the next step.

Verified on the standalone Web with Node Playwright, light + dark: the three
regions render correctly, the docked inspector populates on node-select, and a
run lights every node's status dot (green ✓) with per-node token/cost meta plus
a "RUN · DONE" section in the dock. Release build 0 warnings / 0 errors;
check-classes.ps1 clean (553 defined, all static .razor tokens resolve); 847
tests pass, 14 skipped. Pushed the matching preview card to the Claude Design
project (ui_kits/agentos/workflow.html).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eAgent path

Running on a paired dev machine spends the member's own flat subscription, so the
server cost is genuinely $0 — the whole point of the RemoteAgent provider. But the
CLI reports no usage, so every run read "0 tok · $0.0000", which looks broken and
hides the one number a demo wants: what this work would have cost on a metered API.

Two honest additions, neither of which claims a billed spend:

- RemoteAgentLlmClient now estimates token counts from the text it actually sent
  and received (~4 chars/token, the standard English heuristic) and reports them on
  the response + span. CostUsd stays 0m — the estimate is deliberately NOT folded
  into billed cost. "0 tok" becomes a real figure everywhere tokens are shown.
- The Agents status bar accumulates, per call, what those tokens WOULD cost priced
  by the model each agent used (CostCalculator). When nothing was billed but tokens
  were spent, it shows "$0.0000 cost (≈ $X API)" — the saving, clearly labelled,
  never as actual spend. For real cloud providers the estimate equals the billed
  cost, so the parenthetical only appears on the $0 path.

Verified on the standalone Web with Node Playwright: a pipeline run shows
"1964 tok (1532↑ / 432↓) · $0.0000 cost (≈ $0.0071 API)". Two RemoteAgent tests
that pinned the old "0 tokens" contract were updated to the new intent (zero cost,
estimated tokens > 0). Release build 0 warnings / 0 errors; check-classes.ps1
clean; 847 tests pass, 14 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Building a workflow still meant clicking a node in the rail, then dragging an edge
by hand — and there was no keyboard-first way to add a node at all. This adds the
two interactions that make n8n / Opal feel fluid.

Command palette (⌘K / Ctrl+K, or the toolbar "+ Add"):
- A centred quick-add over the canvas. Type to filter every node type by name or
  hint, ↑/↓ to move the highlight, Enter to add, Esc to close. The global key is a
  document-level listener (workflow-keys.js) routed back to the component through a
  JSInvokable; registration is best-effort and CircuitSafe, so a missing script
  degrades to the "+ Add" button rather than crashing the circuit.

Auto-connect:
- Adding a node while one is selected now also draws the edge from it to the new
  node — the "add the next step" gesture — so a chain is one action per step
  instead of add-then-drag. The edge is written to the graph the same way
  BuildDiagram materialises persisted ones, so it survives Save/reload. Combined
  with the earlier no-stack positioning, new nodes land to the right of the anchor
  and wire up automatically.

Verified on the standalone Web with Node Playwright: Ctrl+K opens the palette,
typing "eval" narrows it to Evaluator with the keyboard highlight and ↵ hint, and
adding a node while Testing Agent was selected wired Testing→Print with a visible
edge. Release build 0 warnings / 0 errors; check-classes.ps1 clean (564 defined);
847 tests pass, 14 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…w builder

Two canvas-polish items from the builder rebuild.

Recent runs:
- The Recent Runs tab showed a bare counter that reset every time the window
  closed, with the label "demo — history not persisted". It now records each run
  (workflow name, time, completed/stopped, a one-line outcome) in the
  OrchestrationStore, keyed per tenant and bounded to 25. The list is session-scoped
  and in-memory — it survives closing and reopening the Workflow window, which the
  old counter did not — and the label says exactly that ("this session — full
  history lives in the Pipeline run store") rather than pretending to be durable.
  Each row shows a green/red status dot, the name, the outcome and the timestamp.

Fit-to-view:
- "Fit" could zoom IN past 2.6× when the diagram library measured a small or
  just-added bounding box, which looked broken. It is now clamped so fit never
  exceeds 1:1 — it frames the whole graph or stops at 100%, never zooms in.

Verified on the standalone Web with Node Playwright: two runs produce two
timestamped rows in Recent Runs (badge "2", green dots), and Fit reframes the full
five-node graph at 56% instead of over-zooming. Release build 0 warnings / 0
errors; check-classes.ps1 clean (569 defined); 847 tests pass, 14 skipped.

Deferred: canvas undo/redo — the one remaining builder item, held back because a
correct command-history stack over the diagram is a larger, riskier change best
done on its own.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The one deferred builder item. Snapshot-based, not a command stack: each
structural edit deep-copies the whole graph (nodes + edges + positions) to JSON
and pushes it, so undo is "restore this snapshot + rebuild the canvas" with no
per-operation inverse to get wrong — the graph is already the serializable source
of truth. Covers the operations that go through the app: add node, delete node,
and the auto-connect that rides on an add. A manual canvas edge-draw or node drag
folds into the next snapshot rather than being individually undoable, which keeps
this free of re-entrant diagram-event coupling. History is bounded to 50.

Toolbar gains undo/redo icon buttons (disabled when their stack is empty) and the
global ⌘Z / ⌘⇧Z / ⌘Y shortcuts route through workflow-keys.js to OnUndoKey /
OnRedoKey.

One non-obvious fix this required: the DiagramCanvas was keyed by graph id alone,
so restoring the SAME graph after an undo left the canvas bound to the previous
BlazorDiagram instance and nothing re-rendered. The key now includes a version
counter bumped on every BuildDiagram, so an undo/redo of the current graph forces
a fresh canvas. Two undo/redo arrow icons added to the shared Icon component.

Verified on the standalone Web with Node Playwright: adding a node takes the graph
5→6, ⌘Z returns it to 5, ⌘⇧Z back to 6 — with the toolbar buttons and minimap
reflecting each step. Release build 0 warnings / 0 errors; check-classes.ps1 clean
(569 defined); 847 tests pass, 14 skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
<label>max iterations<input type="number" min="0" max="10" @bind="_selected.Data.MaxIterations" @bind:after="InspectorChanged" /></label>
</div>
@if (_selected.Data.Category == StepCategory.Evaluator)
@if (NodeLibrary.SelectMany(g => g.Types).Any(MatchesSearch) == false)
@hoangsnowy
hoangsnowy merged commit 30956a3 into main Aug 10, 2026
4 checks passed
@hoangsnowy
hoangsnowy deleted the feat/workflow-builder-rebuild branch August 10, 2026 13:13
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.

2 participants