Repository navigation
Let a Bot's owner choose its colour and draw it a new face - #914
Draft
jerelvelarde wants to merge 18 commits into
Draft
jerelvelarde wants to merge 18 commits into
jerelvelarde wants to merge 18 commits into
Conversation
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.
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.
This was referenced Oct 31, 2026
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. A new look would reach the Bot's pages and the Bots list, never a conversation.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 it in 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 looks of the Bots OpenBot ships with so that stays true. A tenant package'savatar_seedcan name a colour the same way.violet:names none, since violet is OpenBot's own. The colours, the parse and the spelling 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 nobody can change the look of a Bot a tenant package owns. A new look is named on thebot.updatedaudit row; it is a picture, not a secret. Duplicating a Bot still copies its seed, so a copy keeps its source's look.The Setup page has a Look section. Beside Identity, for whoever may edit the Bot: its face at 72px and alive, the eight colours as a native radio group named Orange to Grey, and New face, which keeps the colour and draws a new face. A choice saves and shows at once; one that can't be saved goes back and says so. The radios stay live while a choice saves, so the arrow keys can walk the colours, and the newest choice wins. Another colour keeps the face, so going back finds it.
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 Postgres returned an edited Bot wherever its updated row landed: a new look could move it from first to last, and change which Bot is the default, since that 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
Verification
typecheck, lint, format:check and the assertion-shape guard are clean. The full app suite is 2201 pass, 0 fail, against 2190 on #913 and 2129 on upstream
main.shared/bot-look.test.ts,server/tests/agent-routes.test.tsandserver/tests/bot-lifecycle-audit.test.ts, run on this tip, are 93 pass, 0 fail.shared/bot-look.test.ts: a seed names one of the eight and never violet; the server refuses anything else in a plain sentence; another colour keeps the face.app/tests/bot-look.test.tsx: a colour saves and shows at once and every avatar follows; New face keeps the colour; the newest of several choices wins; a failed save goes back; nobody who can't edit sees the section; a conversation's face waits for the list.app/tests/bot-orb.test.tsx, extended: every Bot that never chose keeps its face; a named colour is honoured, violet and unknown ones are not.server/tests/agent-profile-store.integration.test.ts, extended: an edit changes the look only when it brings one, only for whoever may manage the Bot, and never for a tenant package's Bot; an edited Bot keeps its place in the list.server/tests/agent-routes.test.tsandserver/tests/bot-lifecycle-audit.test.ts, extended: the route takes or refuses a look, and the audit row names a new one.Merge notes
This and #916 both change
app/src/components/channels/avatar.tsxandapp/tests/user-preferences.test.tsx. Whichever merges second combines the two avatars, and updatesapp/tests/channel-avatar.test.tsxand the pane-title test inapp/tests/computer-pane.test.tsxto hold the Bots list, since a face now waits for it. The combined change is small and already written for whichever lands second.