Skip to content

Name the frameworks in the New Bot wizard, ask visibility last, and share one harness catalogue - #799

Merged
guidovizoso merged 12 commits into
mainfrom
guido/bots-new-bot-ui
Oct 17, 2026
Merged

guidovizoso merged 12 commits into
mainfrom
guido/bots-new-bot-ui

Conversation

@guidovizoso

Copy link
Copy Markdown
Collaborator

The New Bot wizard asked a person who had just decided what they wanted a Bot
to do to choose between two radio buttons labelled "Built-in" and "Managed".
Both are a kind of Bot and neither word says which one is theirs. This rebuilds
that step as a list they can recognise themselves in, moves visibility to the
end, and gives the dialog a steadier shape.

What changed

Where does it run is buttons now, led by the Bot this deployment runs
itself, followed by the frameworks somebody might already have running — by
name, by their own mark, and with the one-line summary each already had. The
whole list is open and scrolls; the question above it and Back below it stay
put. Pressing one both answers and moves on, so there is no Continue confirming
a decision already taken.

The address moved to its own step, reached only by the frameworks, so a Bot
that runs here is never shown a field it has no use for.

Visibility is last. It is the one answer that does not change what the
person is making, and asked second it arrived before they had anything to go on.

The step counter is gone. The wizard is three questions for a Bot that runs
here and four for one at an address, and a counter whose total changes under
the reader reads as the thing having got longer.

"Role" became "What it does". Beside "Title" it read as a second job title
when it is the standing instruction the Bot carries into every channel. The
per-channel promise moved from above all three fields onto the one field it is
true of. The dialog also holds a floor of 70% of the viewport so it stops
resizing between questions.

The structural part

The twelve frameworks and their marks lived in desktop/src-tauri/src/harness.rs
and desktop/src/marks/, reachable only through the Tauri command the setup
window calls. The web app needs the same names, summaries and artwork, and a
second copy would drift — which matters more than for most lists, because this
one carries other companies' trade marks and the rules about what may be drawn
for them.

They now live in shared/harnesses.json and shared/marks/, following the
shared/model-providers.json precedent. Rust reads the JSON with include_str!
(the desktop is not in the bun workspace, so it takes the file by path); the web
app imports the module. Harness keeps its snake_case wire format for
HarnessPicker.tsx — a private camelCase row struct does the renaming — so the
Tauri contract is unchanged, and all 24 Rust harness tests and 139 desktop TS
tests pass against the moved files.

Two things to decide before merge

1. Twelve vendor marks now appear in the hosted web app.
shared/marks/README.md scopes those vendored SVGs to "a picker" in a locally
run installer. This is the same identifying use, but it is a different surface —
multi-tenant and served to signed-in customers. Worth a look from whoever owns
that call; reversing it after launch is worse than delaying it.

2. The framework choice is recognition only. Picking Google ADK rather than
CrewAI changes nothing: both lead to the same generic endpoint field and are
stored identically, so "which did I pick" is unanswerable after the dialog
closes. That follows from keeping the API unchanged, and it was raised and
accepted deliberately — but it is the thing most likely to want revisiting.

Mastra is deliberately absent from the web list. The server reaches it as
remote_mastra through the @ag-ui/mastra bridge and needs a roster id to say
which agent it means; the create endpoint can send neither, so a Mastra row here
would make a Bot that appears, accepts a grant, and answers nothing. The
catalogue carries that as webCreate: false, which also lets the web and
installer lists diverge later without a second file.

Three frameworks still show their name alone — Agno, AG2 and Langroid have
no mark in any maintained set (all 404 on @lobehub/icons-static-svg), and the
README forbids inventing one. Their rows reserve the tile's width so the list
does not read ragged.

Also in here

  • New Bot on /bots is a small ghost button rather than a large filled one.
    Not in the original scope; flagging it because it is now the least prominent
    control on a page whose purpose is making Bots.
  • .github/workflows/desktop.yml watches shared/harnesses.json and
    shared/marks/**. catalogue() parses that JSON at runtime with an expect,
    so a bad row would pass lint, typecheck and the TS suite and then panic the
    setup window — and the desktop jobs are the only thing that would catch it.
  • biome.json skips .superpowers, the git-ignored scratch directory.

Known gaps

  • The scroller's offset resets on Back, so a framework chosen low in the list
    comes back selected but off-screen. One scrollIntoView if it matters.
  • aria-pressed announces "toggle button" for a control that navigates, and the
    set-of-N semantics the radiogroup gave screen readers are gone.
  • shared/harnesses.ts asserts the JSON's shape rather than validating the
    credential / maintainer unions, so a typo there surfaces as a Rust panic
    rather than a type error.

Testing

1221 app tests, 242 shared, 149 desktop TS, 24 Rust harness tests, and
server/tests/agent-routes.test.ts all pass; lint, format:check, typecheck
and build are clean. A repo-wide bun test additionally needs a provisioned
openbot_test database — the failures without one are environmental and
unrelated to this branch, whose entire server footprint is a single changed line.

Design and plan: docs/superpowers/specs/2026-10-16-bot-creation-dialog-design.md,
docs/superpowers/plans/2026-10-16-bot-creation-dialog.md.

A floor of 70% of the viewport, under the 85% ceiling the popup already
carried, so three of the four states land on one height instead of the
dialog resizing between every question. The buttons pin to the bottom, and
the pane keeps slack under it so the animated frame never shaves the last
line of a step while the spring settles.

"Role" beside "Title" read as a second job title when it is the standing
instruction the Bot carries into every channel. It is now "What it does",
and the per-channel promise moved from above all three fields onto the one
field it is true of. The validation messages and the Bot's own screens
follow the same name.
The twelve frameworks and the address row lived in Rust, reachable only
through the Tauri command the desktop setup window calls. The web app's New
Bot dialog needs the same names, summaries and marks to say "the thing you
already run is one of these", and a second copy would drift — which matters
more here than for most lists, because this one carries other companies'
trade marks and the rules about what may be drawn for them.

The desktop is not in the root bun workspace, so Rust takes the JSON by path
with include_str! rather than importing the module beside it. The file is
camelCase, which is what the web app reads; `Harness` stays snake_case,
which is what the Tauri command has always sent HarnessPicker.tsx, so a
private row struct does the renaming and the wire format is unchanged.

Two fields are new and the desktop ignores both. `webCreate` is false for
Mastra alone: the server reaches it as remote_mastra through the @ag-ui/mastra
bridge and needs a roster id, and the web create API can send neither, so a
Mastra row in that dialog would make a Bot that appears, accepts a grant, and
answers nothing. `webSummary` overrides the installer's sentence on the two
rows where it is false on a surface that installs nothing.
The marks sat under desktop/src, reachable only by the setup window. The web
app's New Bot dialog draws the same eleven, and copying the artwork would
have meant two sets of another company's trade mark to keep in step. They
now live in shared/marks/, globbed from both sides, with the README that
carries the rules moved with them.

Both globs resolve a file to its id by stripping everything up to the marks
directory rather than a fixed prefix, so neither breaks again if the path
moves. The Rust guard that every named mark is vendored follows to the new
location.

A glob that matches nothing fails silently here: every row would simply show
its name, which is also what a brand with no mark legitimately looks like.
`every named mark is vendored where both apps look for it` is the assertion
that tells those two apart, and it was proved to fail by removing a mark
before it was trusted.
Visibility was the second question, which put it before the person had
decided anything that makes private or public mean something. It is the one
answer that does not change what they are making, so it goes last.

The counter goes with the reorder rather than despite it. The wizard is three
questions for a Bot that runs here and four for one at an address somebody
hosts, and a counter whose total changes under the reader mid-way reads as
the thing having got longer. Each step already says what it is asking in its
own heading, which is what was carrying the reader.

Two tests used the counter as their signal that a step had moved; they now
read the heading, which says the same thing and survives the next reorder.
The first version of the test forbidding the counter passed against the
counter it was written to forbid — `Step {n} of {total}` renders as four text
nodes and a regex matcher walks them one at a time — so it asserts against
the popup's whole text instead.
Two radios asked a person who had just described a role to tell Built-in from
Managed. Both are a kind of Bot and neither word says which one is theirs. The
step now leads with the Bot this deployment runs itself and lists the
frameworks somebody might already have running, by name and by their own mark,
so the answer is recognised instead of deduced.

The press is the answer, so there is no Continue beside them: a button that
confirmed a decision already taken was the second press this path used to cost.
Three frameworks show and the rest open behind a disclosure, because twelve
names at once is a wall on the step somebody reaches knowing only that they
want a Bot.

The address moves onto a step of its own, reached only by the frameworks, so a
Bot that runs here is never shown a field it has no use for. It is generic: no
heading names the framework that led to it, because nothing about it differs by
framework. That is also the honest limit of the list — picking Google ADK
rather than CrewAI changes nothing and is stored nowhere.

Mastra is absent, and shared/harnesses.json says why: the server reaches it
through its own bridge and needs a roster id this create path cannot send.

Tasks 4 and 5 of the plan are one commit because they could not be two.
Click-to-advance leaves the step the moment a framework is pressed, so the
endpoint fields could not stay inline for one commit and move in the next
without shipping a state where a hosted Bot had nowhere to type an address.

biome.json now skips .superpowers, the git-ignored scratch directory these
plans and their mockups live in; HTML written to be thrown away was failing
`bun run lint`.
"Show 9 more" made somebody who did not find their own in the first three
press a control before they could even look. The list is open, and scrolls:
scrolling is the gesture people already try on a list, and the scroller is
bounded so it is the list that moves and not the step — the question above it
and the Back button below it stay where they were drawn.

"Run it here" was describing the deployment rather than the person's choice:
"On this deployment's own Bot. Nothing to host or connect." Neither
"deployment" nor "host or connect" means anything to somebody who has just
written down what they want a Bot to do. It now says OpenBot runs it for you
and there is nothing to set up. The unavailable case stops naming the plumbing
too: it says the Bot can be switched on and that whoever set OpenBot up is the
one to do it, which is true and is addressed to the right person.
Four server tests still asserted "Role description must be text between 1 and
1000 characters.", which the rename changed two weeks of commits ago. CI runs
the repo-wide suite, so the branch was red. This is the second half of a miss
already recorded once: the app half was fixed when it was found, the server
half was never looked for, because both times the rename was verified against
a subset of the suite rather than all of it.

`app/tests/composing-enter.test.tsx` and `create-agent-steps.test.tsx` passed
only because a `mock.module` in a different file leaked process-wide. Both
failed when run alone, and the plan's own step command for one of them was
broken. The stub moves to app/tests/preload.ts, which exists for exactly this —
its comment is about a test that "passed on its own and failed in the suite",
and this was the same bug pointing the other way. A file that needs a mark
drawn still overrides it.

The desktop CI filter watched desktop/** only, and the catalogue and the marks
it guards now live in shared/. A row with a bad `credential` would have passed
lint, typecheck and the TS suite and then panicked the setup window, because
`catalogue()` parses that file at runtime. The filter now watches
shared/harnesses.json and shared/marks/**.

Unmarked rows no longer collapse their tile. Four of the twelve have no mark,
and their names began a tile's width left of the rest, which read as a ragged
list rather than four brands without artwork. Reserving the column draws
nothing and invents nothing; it only keeps the names in line.
`overflow-y-auto` computes `overflow-x` to `auto` as well, so the box clipped
sideways as much as it did downward — and the inset was on the right only. The
row under the pointer had its left border shaved off against that edge, which
is what made the component look cut off. The padding is now on both sides, and
the negative margin hands it back so the rows keep the width of the button
above them.

The height was a constant, chosen before seeing it in a dialog that is itself
70% of the viewport: the list stopped at 240px and left 139px of the step
empty below it. In `svh` it tracks the popup it sits in — 336px of list on a
laptop, and the gap down to Back is 45px instead of 139px.
The real cause of the cut-off left edge, which the last commit guessed at and
missed. The frame that animates each step's height hides its overflow on both
axes. The negative margin added to keep a row's border off the clip edge made
the scroller three pixels wider than that frame, and focusing the step scrolled
the frame four pixels sideways to bring them into view. Every child then drew
four pixels left of where it belonged — which is why the heading lost the left
of its W and each row lost its left border, and why insetting on both sides did
not help: the inset was the thing causing it.

The scroller is flush now, and nothing inside the step is wider than the frame.
The rows keep their border off the clip edge by having a focus ring drawn
inside rather than outside — which they needed anyway, since they had no focus
style at all and keyboard focus fell back to an outline the scroller would have
clipped.

The test asserts the frame's scrollWidth against its clientWidth, which is the
property that actually broke. The one it replaces asserted a Tailwind class on
a wrapper two levels up, and would have passed throughout.
@guidovizoso
guidovizoso merged commit 12c3a69 into main Oct 17, 2026
24 of 25 checks passed
@guidovizoso
guidovizoso deleted the guido/bots-new-bot-ui branch October 17, 2026 00:48
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