Repository navigation
Give every Bot a face of its own, and follow the system's dark mode - #913
Open
jerelvelarde wants to merge 12 commits into
Open
jerelvelarde wants to merge 12 commits into
jerelvelarde wants to merge 12 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.
jerelvelarde
requested review from
davidmckayv,
guidovizoso and
tylerslaton
as code owners
October 31, 2026 00:19
jerelvelarde
requested review from
MikeRyanDev,
dustywusty,
mxmzb and
samjulien
as code owners
October 31, 2026 00:20
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.
The problem
A Bot is drawn as a gradient circle from
boring-avatars, picked from its seed. In a list, two Bots are told apart by a blur of colour with nothing to recognise, and OpenBot's own mark is a different drawing altogether, so the product and its Bots look as if they came from two places.The colours are shadcn's neutral defaults, down to a mid-grey focus ring. Text sits on a scale of its own, and the stylesheet admits it: "Text is sized on a 15px base while everything else is on Tailwind's 16px rem."
Dark mode is a switch that ignores the operating system, and the old code wrote
lightto storage on every load, so nothing can tell a person who chose light from one who never chose.The approach
The values go where every screen already reads them. The palette fills shadcn's own variables in
app/src/theme.css, so Bots, Capabilities, Connections, Settings and Admin change without touching each screen, and each skin still overrides them under:root[data-skin]. Light is cool greys, a near-black primary and one accent,#6430ab, kept for focus and selection; dark is near-black with the primary turned light. Text drops the 15px scale and follows the browser's own size, so it is larger everywhere. That reaches the browser extension's pages too, since they importapp/src/theme.css, and the recording notice copies the colours, with a test holding the copy equal.Dark mode follows the system. Settings → General → Appearance offers System, the default, Light and Dark, stored under a new
openbot-appearancekey and written only when somebody chooses. For anybody who never chose, the oldopenbot-themekey'sdarkstays Dark and itslightreads as System, since it was written whether or not anybody asked.index.htmlrepeats the rule before the first paint, and a test holds the two together.Every Bot is an orb, drawn on a canvas.
BotOrbdraws a glass sphere with two eyes that blink, follow the pointer, and show listening, working and done. A Bot's look comes from a hash of its seed: one of eight colours, and from the same hash a turn of its colour masses, a nudge to its hue and the spacing of its eyes, so two Bots in one colour still look apart. Violet is kept for OpenBot's own mark (AgentOrb), so no Bot is mistaken for the product. Nothing importsboring-avatarsany more, and it leaves the app's dependencies.A list of faces costs little. Every orb on the page draws on one shared animation frame, and none draws off screen or in a hidden tab. A small orb, under 40px, holds its colour still, only blinks and watches, skips any frame that would draw what its canvas already shows, and draws at most about 30 frames a second, so a sidebar at rest draws only blinks. With reduced motion an orb is one still picture.
The cost is real and worth naming: every Bot's colour changes on every deployment, and somebody who knew a Bot by its gradient learns it again. Larger text also means a little less fits on a dense screen.
What is not covered
--brandor the new status colours, and nothing reads the status, glass and shadow tokens yet.pointercancelis not listened for, so when the browser takes a touch over to scroll, the eyes stay on the last point.Verification
typecheck, lint, format:check and the assertion-shape guard are clean. The full app suite is 2190 pass, 0 fail, against 2129 pass, 0 fail on upstream
mainatab9855c1.app/tests/bot-orb.test.tsx: no Bot is violet; the Bots OpenBot ships with are drawn unlike each other, two in one colour included; fifty orbs share one frame; a small orb at rest draws nothing until the pointer moves, and about thirty frames a second on a 120Hz screen; nothing draws off screen or in a hidden tab;AgentOrbmaps every state and refuses a size that isn't px.app/tests/visual-system.test.ts: every light variable has a dark value; text roles clear 4.5:1 on every ground, and each status word on its soft fill, in both themes; the root font size is the browser's.app/tests/settings-appearance.test.tsx: Appearance offers System, Light and Dark, and Dark is drawn and remembered.app/tests/theme-preference.test.ts, extended: the pre-paint script and the provider follow the system live; only a choice is written; storage that throws still follows the system.app/tests/skins-openmuse.test.tsx, extended: that skin's header draws its face at its slot's size.Merge notes
orb/ai-core.tsxis gone andAIStateis exported fromagent-orb.AgentOrbtakes its size only in px, as"64px", and throws on anything else, because a canvas drawn at a share of its container has nothing to be a share of. A branch in flight that importsai-coreor passessize="100%"has to follow.