Repository navigation
Let a Bot's owner choose its colour and draw it a new face - #914
Open
jerelvelarde wants to merge 34 commits into
Open
jerelvelarde wants to merge 34 commits into
jerelvelarde wants to merge 34 commits into
Conversation
This was referenced Oct 31, 2026
The last two places still drawing boring-avatars' gradient circles now draw the orb in the colour the Bot's id picks, so a Bot looks the same in the sidebar, the conversation header and its profile. Nothing imports boring-avatars any more, so it leaves the app's dependencies.
…ll when asked A Bot's orb is a block, so a group conversation's 12px faces no longer sit on the header's text baseline and drop half out of their circles. It resizes its canvas when the screen's pixel density changes, so dragging a window to another screen or zooming no longer leaves it soft, and it follows the reduced-motion setting live rather than only when it first draws. The changelog and the stylesheet now describe what this step draws, not what later steps will.
The skins import the orb's state type from agent-orb now that ai-core is gone, OpenBot's mark keeps its agent-orb class for them, and their tests check the orb's colour rather than boring-avatars' SVG. The Appearance test's preferences carry the new skin field.
AgentOrb read "100%" as 100px, so with no Bot yet the header drew a 100px orb in a 44 or 52px slot. With a Bot, the classes that used to rescale boring-avatars' SVG squeezed the avatar's circle to 44px below 900px wide while the orb inside stayed 52px, clipped and off centre. The frame now picks 44 or 52px from the window's width and draws the face at that size, the selectors that matched only the old SVG are gone here and from OpenDots' Bot rows, and AgentOrb takes its size only in px and refuses anything else.
Every orb on screen redrew on every animation frame, uncapped, so a sidebar of conversations kept a few dozen canvases repainting at the screen's full rate while the app sat idle: a small orb's colour still drifted, so no two frames were ever the same. A small orb's colour now holds still, an orb skips any frame that would draw what its canvas already shows in the same theme and at the same density, and a small orb draws at most about thirty frames a second. A face at rest with the pointer still now draws only to blink.
…violet A Bot's face was one of nine colours and nothing else, so a team of four had about an even chance of two identical faces. In the Bots OpenBot ships with, three were the same sky blue, and release notes wore violet, OpenBot's own mark. A Bot is now drawn from its seed: one of the eight colours that aren't violet, with its colour masses turned, its hue nudged and its eyes spaced by the same hash, so two Bots in one colour still look apart. BotOrb takes the seed rather than a colour and draws OpenBot's violet when it has none. The OpenDots avatar test checked one colour in nine, so it passed or failed on the hash; it now checks that the seed reaches the orb.
… Bots apart At 28px an orb reads as the average of its colours, and several averages sat side by side: coral beside ember, sky beside ocean, berry beside violet. The two Bots every deployment ships, general-assistant and knowledge, drew as coral and ember and looked alike, and sun read bronze at 72px. The eight colours keep their names but are re-tuned to read as red, orange, yellow, green, cyan, blue, pink and grey, with lightness keeping neighbours apart. orbSwatch gives the colour an orb reads as when small, and a test holds every pair, violet included, at least 0.08 apart in OKLab. The colour now takes the hash's top byte, so the shipped Bots draw yellow, red, cyan and grey; a test pins that. Every Bot's face changes once, before this merges.
The outline every element falls back on, and OpenMuse's header and tab focus rings, drew the violet ring at half strength with no border beside it: about 2.4:1 on the light background and 2.8:1 in the dark, under WCAG's 3:1 for a focus indicator. They now draw it at full strength. The visual-system test blends each ring at the strength its class gives it over the background and a card, in both themes, and fails under 3:1.
…hem all Every orb blinked about every four seconds and watched the pointer forever; only the system's reduced motion stopped it, so a list of Bots never sat still (WCAG 2.2.2). An orb under 40px now never follows the pointer and is still at rest: it moves only while it listens, works or lands, and holds where it was when it comes back to rest. A Bot faces choice, kept in this browser like Appearance, can make every orb move only while busy, the same way, or never; reduced motion still stills every face. The small-orb tests now check that a resting face draws nothing for ten seconds with the pointer circling it, and new tests cover each choice and reduced motion.
…y what they do Appearance was a select whose description read the same whichever theme was chosen. It is now a radio group drawn as a segmented control, so all three choices are in view, and its description says what the chosen one does. Bot faces sits beside it the same way, with Animated, Only while working and Still, and says when the device's reduced motion is stilling faces anyway. Arrow keys move through the choices and choose, and Tab leaves the group.
type-nav was 18px and type-body 17px: one pixel apart, which no one sees as a step. type-nav is now 20/24, so every step of the scale is at least 2px from the next, and a test holds that. Nothing on this branch uses type-nav yet; keeping both names leaves the branches built on this one working.
…ead of throwing AgentOrb threw when its size wasn't in px, so a style slip or a cast past the type took down the whole route around it. The type still takes only px, but at run time a size it can't read now draws the mark at its default 192px and warns once for that size. The test renders "100%" twice and checks the default size and the single warning.
…tops no other A touch that turns into a scroll ends in pointercancel, not pointerup, and nothing listened for it, so every orb kept looking at where the finger had been. The shared pointer now lets go on a cancel. Every orb also draws in one shared frame, and a callback that threw ended that frame and every frame after it, stopping every orb on the page; each one is now reported and skipped. A test for each.
Onboarding's roster drew each Bot's face, announced by name, beside a heading with the same name, so a screen reader said every Bot twice. AbstractAvatar takes decorative for a face beside a heading that already names its Bot, and the roster passes it. The test checks that a decorative face has no role or name and that the roster uses it.
The small-orb test played ten seconds of frames, then changed the orb to thinking at the real clock, a few milliseconds after mounting. The thinking motion had long settled by the frames that followed, so the test passed only when a blink, timed by how many orbs the process had drawn before, fell in that window: it failed in the full suite and on reruns. become now changes the state at the time the frames reached.
jerelvelarde
force-pushed
the
jerel/openbot-bot-look
branch
from
November 2, 2026 17:19
b821e6c to
5325fee
Compare
…n as on Retina The orb draws under a transform scaled by the screen's density, and shadowBlur is the one length a canvas transform never scales. So on a 1x screen the shadow under the eye-lights and the check was twice as soft against them as on a Retina one. The draw now takes the density and scales the blur by it.
A seed that starts with one of the eight Bot colours and a colon, as moss:k2p9x7aq, is drawn in that colour; the pattern's turn, the hue nudge and the eyes still come from the hash of the whole seed. Any other seed, violet: and unknown colours included, draws exactly as before, and the default team's looks are pinned so that stays true. The colours, the parse, the spelling a seed may have and the builders for a new look live in shared/bot-look.ts, so the server checks a look by the same rules the orb draws it by.
PATCH /api/agents/:agentId takes an optional avatarSeed and stores it in the existing agent_profiles.avatar_seed, so no migration. It is checked by the shared rules the orb draws by: 1 to 64 letters, numbers, hyphens, underscores or colons, and a colour prefix only when it is one a Bot can have. A violet: seed or an unknown colour is a 400 with a plain sentence. Whoever may rename the Bot may restyle it: the store's existing manage check covers both, so a package's Bots stay as the package drew them. A new look is named on the bot.updated audit row; an edit without one leaves the face, and the trail, as they were.
A Bot's Setup page has a Look section beside Identity: the Bot's face, large and alive, the eight colours as a radio group named Orange to Grey, and New face, which keeps the colour and draws a new one. A choice saves at once through the Bot's update, shows at once, and goes back with one sentence if it can't be saved; the newest of several choices wins, so the arrow keys can walk the colours while each one saves. Only people who may edit the Bot see it. Conversation faces, which know only a Bot's id, now draw the seed the Bots list holds for it, so a new look reaches the sidebar, a conversation's header and its messages, not only the Bot's own pages. They wait for the list rather than draw a face that is about to change. The OpenDuet skin's conversation rows, which looked seeds up themselves, pass ids again. Two tests whose fixtures did not answer the Bots list now do: one counted its requests, and one left a refused lookup retrying in the app's query client for the next file.
The tenant package reference says what avatar_seed does, that it may start with one of the eight Bot colours, and that violet: names none. The changelog says a Bot's owner can now choose its look, how the choice is stored, and what the API takes.
…defined key The test now expects the Bots list request the preview row's face makes, and its expected literal held body: undefined, a key toEqual does not check, on the assertion-shape allow-list. It compares each request's method and path and checks every body is absent, and its allow-list entry goes with the fix.
The Bots list query had no ORDER BY, so Postgres returned rows in whatever order they lay in, and an update writes a new row version: restyling or renaming a Bot moved it, and the first Bot in Yours jumped to last. The list is now ordered by when each Bot was made, with the id breaking a tie, because a tenant package writes all of its Bots in one transaction and they share a timestamp. Creation order is what the list showed before anything was edited. Nothing in the app re-sorts it: the Bots page splits it into sections in this order, and the app takes the first Bot as the default when no harness is picked, so an edit could also change that default.
After the palette re-tune, sky draws cyan and coral draws red, so the Look section's radios now say Cyan and Red instead of Light blue and Coral. The other six already named what they draw.
The swatches drew their focus ring at half strength, about 2.4:1 on a card. They now draw it at full strength, and the theme test that holds every lone focus ring to 3:1 reads the Look section too.
Arrowing through the colours saved each one it passed, so every colour was a PATCH and a bot.updated row, and restyled the Bot for everyone while the owner was still looking. The face now previews each colour, and the one the owner stops on saves after a short pause, when focus leaves the row, or when the page goes away. Arrowing back to the Bot's own colour gives back its own seed and saves nothing. New face and a saved colour show an Undo beside New face that saves the look from before. Undo while a colour is still in its pause takes it back without a save. A polite live region on the row says what changed, as "Blue, new face", "Green, saved" or "Blue, look undone".
After the palette re-tune the ten catalog Bots drew in seven colours: three pairs shared one, and none was red. On-call Handover, which drew green beside Interview Notes, now names red in its avatar_seed, so the ten use all eight colours and only two are shared. The four shipped Bots already draw yellow, red, cyan and grey, and keep their seeds.
With Bot faces set to still, the Look section's preview is a still, and the conversation list's face is one at rest. The test picks a colour and checks the preview is painted again in it at once, and that every other face of the Bot draws it once it is saved. Nothing needed changing.
WebKit's Tab passes over a radio that has no tabindex of its own, so in Safari and in the macOS desktop app's window Tab never reached the colours. The chosen colour now carries tabindex 0 and the others -1, so the group is still one stop and the arrow keys move through it.
jerelvelarde
force-pushed
the
jerel/openbot-bot-look
branch
from
November 2, 2026 19:32
5325fee to
befebc8
Compare
jerelvelarde
marked this pull request as ready for review
November 2, 2026 19:33
jerelvelarde
requested review from
MikeRyanDev,
davidmckayv,
dustywusty,
guidovizoso,
mxmzb,
samjulien and
tylerslaton
as code owners
November 2, 2026 19:33
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Depends on #913. Its own change is the commits after that one: compare.
The problem
With #913, every Bot is an orb whose colour and face come from a hash of its avatar seed, and nobody can change the seed.
agent_profiles.avatar_seedis set when a Bot is created or copied, and the edit route never reads it. A person whose two Bots landed in the same orange, or who wants the Bot that books travel to be blue, has no way to say so. The only lever is a tenant package'savatar_seed, which reaches only the package's own Bots.Conversation faces have a second gap.
ChannelAvatarknows only participant ids and draws each face from the id, which matches the seed only until somebody changes it.The approach
The colour travels in the seed, so nothing is migrated. A seed may start with one of the eight Bot colours and a colon, as
moss:k2p9x7aq; the orb draws that colour and takes the rest of its look from the hash of the whole seed. Every other seed draws exactly as before, and a test pins the shipped Bots' looks. The catalog's On-call Handover now names one (coral:oncall-handover), so the ten catalog Bots use all eight.violet:names none. The colours and the parse live inshared/bot-look.ts, which the server and the orb both read, so the server never accepts a look the orb would draw differently.The edit route takes it.
PATCH /api/agents/:agentIdtakes an optionalavatarSeed: 1 to 64 letters, digits, hyphens, underscores or colons, with a colour prefix only when a Bot can have that colour. A refusal is a 400 with one sentence, such as "Violet is kept for OpenBot. Pick another colour." Absent leaves the face as it is, so an edit to anything else never restyles a Bot. The store's existing manage check covers it, so only someone who can rename the Bot can change its look, and never for a Bot a tenant package owns. A new look is named on thebot.updatedaudit row; it is a picture, not a secret.The Setup page has a Look section for whoever may edit the Bot: its face at 72px, the eight colours as radios named for what each draws (Orange to Grey, with Cyan and Red), and New face, which keeps the colour. Arrowing through the colours previews each on the face; the one the owner stops on saves once, 700ms later, or when focus leaves the row or the page goes away. Browsing writes one
PATCHand one audit row, not one per colour passed. Undo puts back the look from before, and a polite live region says what changed: "Blue, new face", "Green, saved". A failed save goes back and says so. The swatches' focus ring is full strength, to clear 3:1.Conversation faces read the seed from the Bots list.
useBotSeedsmaps each id to the seed in the list the app already holds, so a new look reaches the sidebar, a conversation's header and its messages. Until the list arrives the circle holds no face, rather than one about to change.An edited Bot keeps its place. The Bots list had no order, so an edit could move a Bot from first to last and change the default, which falls back to the first.
list()now orders by creation, with the id breaking ties, because a tenant package writes all its Bots in one transaction.The cost: a conversation's faces now wait on
GET /api/agents, and a seed carries meaning in its spelling, so anything that writes one should go throughshared/bot-look.ts.What is not covered
coral:draws Red andsky:Cyan.Verification
typecheck, lint, format:check and the assertion-shape guard are clean. The full app suite is 2226 pass, 0 fail, against 2206 on #913 and 2129 on upstream
main. CI's startup job passes on macOS and Windows. In Chromium, WebKit and Firefox, at 1280 and 390 wide, every changed screen loads with no page error and no sideways scroll.shared/bot-look.test.ts: a seed names one of the eight and never violet; the server refuses anything else in a plain sentence.app/tests/bot-look.test.tsx: arrowing previews and saves only the colour it stops on, once; arrowing back saves nothing; a colour in its pause saves when the page goes away; Undo before and after a save; a failed save goes back; with faces Still a pick still shows; a conversation's face waits for the list.app/tests/bot-orb.test.tsx, extended: a Bot that never chose keeps its face; violet and unknown colours are refused; the catalog shares as few colours as it can.app/tests/visual-system.test.ts, extended: the swatches' ring clears 3:1.server/tests/agent-profile-store.integration.test.ts, extended: a look changes only when an edit brings one, only for whoever may manage the Bot; an edited Bot keeps its place.server/tests/agent-routes.test.ts,bot-lifecycle-audit.test.ts, extended: the route and the audit row.app/tests/bot-look.test.tsxalso: the swatches are one Tab stop, the chosen one, so WebKit reaches them.Merge notes
This and #916 both still change
app/src/components/channels/avatar.tsxandapp/tests/user-preferences.test.tsx, and a trial merge conflicts in both. Whichever merges second keeps this branch'suseBotSeedswith #916'sstateand status dot, and givesapp/tests/channel-avatar.test.tsxand the pane-title test inapp/tests/computer-pane.test.tsxa query client holding the Bots list, since a face now waits for it.