Skip to content

Build out the Builder: activity log, notifications, anchored placement, and a rebuilt launch dialog - #334

Merged
dovvnloading merged 6 commits into
mainfrom
feat/builder-buildout
Aug 17, 2026
Merged

Build out the Builder: activity log, notifications, anchored placement, and a rebuilt launch dialog#334
dovvnloading merged 6 commits into
mainfrom
feat/builder-buildout

Conversation

@dovvnloading

Copy link
Copy Markdown
Owner

Problem

The Builder's engine (planning, budgets, approval routing, undo, resume) is substantial, but its surface was a prototype: builds had no visible activity log, a landed build announced nothing unless the user happened to be looking, output could land anywhere on the canvas, the launch dialog was the last surface in the app using a bare native <select> with an input bug in its budget fields, and the feature was undocumented anywhere in the app.

Change

  • Activity log: every tool call a build makes is recorded on the plan node - tool name, outcome, timing - in a capped ring buffer, rendered as a collapsible disclosure. Deliberately untouched by undo: it's run telemetry, not document content.
  • Completion notifications: a build landing done, paused, or failed now shows a banner, so a build finishing off-screen isn't silent.
  • Anchored placement: a build's new nodes land near its plan node instead of scattering at the canvas origin; launching a build centers the viewport on it.
  • Launch dialog rebuild: the recipe picker is now the app's own CustomSelect; selecting a recipe previews its steps before launch; budgets are named presets (Quick / Standard / Extended) with the exact numbers behind an Advanced disclosure that clamps on blur (fixing a bug where typing 0, or clearing a field to retype it, silently reverted to the default); a saved recipe can now be deleted.
  • Documentation: a full Builder section in Help, one sentence in Onboarding.
  • Review-fix pass: an adversarial review surfaced 13 findings, all fixed - most notably, undo could silently erase activity rows logged after a mid-run replan (the replan's own snapshot-based undo command was restoring the whole node wholesale); a build's parentless creates and an explicit parent_id equal to the plan node's own id used two different sibling-counting mechanisms and could overlap. Also fixed: an uncapped tool-call name field, a delete-recipe flow that ignored whether the delete actually succeeded, and a stray CSS float.

Test plan

  • Full backend suite: 3020 passed, 17 skipped
  • Full frontend suite: 1944 passed, zero lint errors, build and bundle size clean
  • Every commit verified against a real running backend (a scripted provider driving an actual build through the real WS/document/wire path, including the copilot approval gate) before being committed - activity log content, completion notification, and anchored placement all confirmed against the real DOM and real node coordinates
  • The clamp-on-blur budget fix verified with genuine keyboard input (typed 0, tabbed away, confirmed it settles on the clamped minimum)
  • Regression tests added for both correctness fixes found in review (undo-preserves-activity, placement overlap), plus the other 11 findings

…d output placement

The Builder's tool-use loop had no durable record of what a build did:
every tool call and its result vanished into the loop's in-memory message
list. A build that paused or failed announced nothing unless the user
happened to be looking at the canvas. New output (parentless node
creations) landed near the canvas origin rather than near the build that
produced it.

This adds:

- An activity log on the plan node (PlanState.builder_activity): one row
  per tool call the loop invokes, written at its single registry.invoke()
  choke point, so every call - including the four in-band control tools -
  is recorded in order with its outcome and timing. A capped ring buffer
  bounds its growth; it rides the existing whole-node scene patch
  mechanism, so no new wire plumbing was needed. It is deliberately
  untouched by undo: this is run telemetry, not document content, and the
  record of what happened must survive reverting what happened. Persisted
  with the rest of the plan node's state so it survives an app restart.

- Notifications when a build lands done, paused, or failed (success/
  warning/error respectively). "stopped" is excluded - the user just
  clicked Stop and is necessarily present.

- Anchored placement: a build's parentless node creations (a note, a
  from-scratch chat or code node) now land near the plan node that
  launched them instead of scattering near the canvas origin, reusing the
  run context's existing plan-node reference rather than adding a new one.

- builder/deleteRecipe, rounding out the recipe lifecycle - saveRecipe
  already existed with no way to remove what it saved.

Every change here is confined to the loop, its wire contract, and session
persistence; the loop's control flow, budget enforcement, and approval
routing are unchanged.
The plan node had no way to show the activity log added in the previous
commit. This wires PlanState.builder_activity through SceneCanvas into
the plan node's data and renders it as a collapsed-by-default disclosure,
reusing ChatNodeView's existing "an assistant turn's tool calls" pattern
(.chat-node-tool-invocations) rather than a new widget, since the content
is the same shape - a tool name, an outcome, one block of detail text -
just scoped to a whole build instead of a single turn, with a bounded,
independently scrollable list to hold the larger row count. The log
auto-scrolls to its newest row while the build is running and the
disclosure is open.

Verified against a real running backend (a scripted provider driving a
real build through the real WS/document/wire path, approval gate
included): the activity count, error count, per-row tool name, outcome
tint, and summary text all matched the calls made; the completion
notification carried the build's own finish summary; and the two created
notes landed exactly where the previous commit's anchored-placement fan-
out predicts, relative to the plan node.
The launch dialog was the last surface in the app still using a bare
native <select>, and its three budget fields were raw, unlabeled numbers
with an input bug: typing 0, or clearing a field to retype it, silently
snapped back to the default on every keystroke because the coercion used
`Number(value) || DEFAULT`, and 0 is falsy.

- The recipe picker is now CustomSelect, matching every other dropdown in
  the app.
- Selecting a recipe previews its own steps before launch, not just its
  description - the backend payload already carried them; nothing
  rendered them.
- The three budgets are now named presets (Quick / Standard / Extended)
  with a plain-language summary line, with the exact numbers moved behind
  an "Advanced" disclosure for anyone who wants them. Those fields now
  clamp to their valid range on blur instead of coercing while typing.
- builder/deleteRecipe (added in the first commit of this change) now has
  a UI: a saved (non-built-in) recipe can be removed from the picker.
- Launching a build - from scratch or from a recipe - now centers the
  viewport on the new plan node. The launcher has no canvas anchor of its
  own, so the node could previously land anywhere the scene's extent
  happened to place it, often off the visible viewport entirely.

Verified against a real running backend end to end, including the exact
input sequence that reproduces the old 0-coercion bug (typed "0" into Max
steps, tabbed away, confirmed it settles on 1 - the clamped minimum - not
back on the default).
The Builder had no documentation anywhere in the app - it appeared in
neither the Help Center nor the first-run onboarding wizard, so its only
self-explanation was a single placeholder sentence in its launch
dialog's goal field. For the most capable feature in the app, that made
it the least explained.

Adds a full "Builder" section to the Help Center (how a build works: the
goal-plan-build flow, what it can actually do, the two oversight modes,
and budgets; the plan node: watching a build run, pause/stop/resume,
undoing a build, and recipes) and one sentence to onboarding's first
step naming the Builder and what it does, alongside the existing
branching explanation.
…iew findings

An adversarial review pass across the Builder build-out branch surfaced
13 confirmed findings; this fixes all of them.

Correctness:
- Undo/"Undo this build" could silently erase activity rows and step
  completions logged after a mid-run builder.replan call. A replan
  records a command that snapshots the whole plan node at that instant
  (the app's existing, deliberate whole-node undo model); undoing that
  command restored the node wholesale, discarding everything logged
  afterward - directly contradicting the activity log's own documented
  "survives an undo" guarantee. Fixed the same way the existing
  builder_status review-fix handles the identical class of hazard:
  restoring a snapshot now always carries the CURRENT activity log
  forward rather than trusting the snapshot's stale copy.
- A build's parentless node creations (anchored near the plan node) and
  an explicit parent_id equal to the plan node's own id (which the
  executor prompt hands the model directly) used two different
  sibling-counting mechanisms for the same row - one by position, one by
  graph edge - so interleaving the two could place two nodes on top of
  each other. Unified to the position-based count specifically when
  parent_id is the anchor; an ordinary parent is unaffected.

Robustness:
- A tool call's name has no upstream length validation and was stored in
  the activity log uncapped, unlike every other field on that row -
  defeating the size-bounding the ring buffer and summary cap exist for.
  Now truncated the same way.
- Restoring a saved session's activity log crashed on a non-numeric
  elapsedMs instead of degrading to 0 the way every other field on that
  row already tolerates malformed input.

Also fixed: the launch dialog's recipe-delete flow ignored whether the
delete actually succeeded, silently treating a backend refusal or a
network failure as success; a CSS float in the activity log's layout
replaced with the flexbox idiom used everywhere else in the app,
incidentally making a previously-inert CSS class real.

Coverage added for all of the above, plus the ring buffer's exact trim
boundary, summary/tool-name truncation, malformed session data, the
plan node's running-and-open-only auto-scroll, an empty recipe step
list, and the budget preset control's behavior once its fields have
been hand-edited away from any preset.

Full local suites verified: 3020 backend tests, 1944 frontend tests,
zero lint errors, build and bundle size clean.
test_activity_tool_name_and_summary_are_both_truncated_when_over_cap
referenced ToolResult without importing it. At runtime the resulting
NameError was silently swallowed by ToolRegistry.invoke's own generic
exception handler and converted into an error result - whose error text
happens to also be long and gets truncated the same way a real success
result would, so the test passed without ever exercising the success
path it was meant to cover. Caught by CI's ruff check (F821, undefined
name), not by the test itself.

Fixed the import and added an explicit outcome == "ok" assertion so a
future regression back to the error path fails loudly instead of
silently passing for the wrong reason.
@dovvnloading
dovvnloading merged commit 827ade5 into main Aug 17, 2026
4 checks passed
@dovvnloading
dovvnloading deleted the feat/builder-buildout branch August 17, 2026 00:14
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