Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
a2af80b
Let each person pick a skin for OpenBot in Settings
davidmckayv Oct 27, 2026
7392bf6
Give OpenDots a real skin: its rail, Dots sidebar, top bar and comput…
davidmckayv Oct 27, 2026
da5cefc
Show OpenDots' coworker home on a new conversation with a chosen Bot
davidmckayv Oct 27, 2026
76e3703
Dress a conversation's own computer pane as OpenDots' right pane
davidmckayv Oct 27, 2026
32e09aa
Add the OpenDuet skin: a phone-width frame and a Tasks screen on Open…
davidmckayv Oct 27, 2026
8e3db37
Let the OpenDuet orb wake only when the composer starts or stops reco…
davidmckayv Oct 27, 2026
e75538a
Set OpenDots' composer on the canvas colour, as OpenDots draws it in …
davidmckayv Oct 27, 2026
af74be7
Fuse the OpenDots skin
davidmckayv Oct 27, 2026
5ea0e24
Fix what OpenDuet's live pass found: the orb, the approval card's gly…
davidmckayv Oct 27, 2026
a73c22a
Fuse the OpenDuet skin
davidmckayv Oct 27, 2026
881b18f
Build the OpenMuse skin: one agent, a phone-shaped column, five tabs
davidmckayv Oct 27, 2026
87493c5
Fuse the OpenMuse skin
davidmckayv Oct 27, 2026
225cbfe
Clear a skin this build no longer ships from the page on load
davidmckayv Oct 27, 2026
41425f3
Note the skins in the changelog
davidmckayv Oct 27, 2026
a77cd94
Give the OpenDuet skin OpenBot's identity: the deployment's name, Ope…
davidmckayv Oct 27, 2026
2b21efd
Fuse the OpenDuet skin's OpenBot branding
davidmckayv Oct 27, 2026
62c9cda
Wear the deployment's brand in the OpenDots skin, keeping only OpenDo…
davidmckayv Oct 27, 2026
b10ebe9
Fuse the OpenDots skin's OpenBot branding
davidmckayv Oct 27, 2026
8f97bd0
Wear the deployment's brand in the OpenMuse skin
davidmckayv Oct 27, 2026
5537c8a
Fuse the OpenMuse skin's OpenBot branding
davidmckayv Oct 27, 2026
69b744d
Read the Anthropic key from either header in the AG2 test
davidmckayv Oct 27, 2026
7816cb0
Merge main into the skins
davidmckayv Oct 27, 2026
912e6c3
Build OpenDuet's Tasks from OpenBot's own lists, and give pages OpenB…
davidmckayv Oct 27, 2026
50d1373
Make the OpenDots skin pass AA contrast, still for reduced motion, an…
davidmckayv Oct 27, 2026
34227cf
Merge main into the skins
davidmckayv Oct 28, 2026
f1547d4
Save the skin on the account, share one navigation list, and give ski…
davidmckayv Oct 28, 2026
6afb431
Fuse the core review fixes: account skin, shared navigation, data-slo…
davidmckayv Oct 28, 2026
972cf18
Bring in the core review fixes
davidmckayv Oct 28, 2026
8465123
Bring in the core review fixes
davidmckayv Oct 28, 2026
9838a71
Build the OpenMuse skin's tabs from OpenBot's own components
davidmckayv Oct 28, 2026
b92a7bc
Bring in the core review fixes
davidmckayv Oct 28, 2026
68401f0
Draw OpenDuet's menu from OpenBot's navigation list and dress pages b…
davidmckayv Oct 28, 2026
ca5852c
Bring in the dictation hooks
davidmckayv Oct 28, 2026
bb77ec6
Give the composer's dictation buttons their own hooks
davidmckayv Oct 28, 2026
234ca49
Build OpenDots' destinations from OpenBot's navigation list and style…
davidmckayv Oct 28, 2026
ac7daab
Find the composer's dictation controls by their hooks in the OpenDuet…
davidmckayv Oct 28, 2026
bf4b831
Hook the OpenMuse skin onto OpenBot's navigation list and data-slots
davidmckayv Oct 28, 2026
cf810a0
Fuse the openduet skin's review fixes
davidmckayv Oct 28, 2026
7817424
Fuse the opendots skin's review fixes
davidmckayv Oct 28, 2026
43da9e5
Fuse the openmuse skin's review fixes
davidmckayv Oct 28, 2026
918e96c
Say in the changelog that the skin follows the account
davidmckayv Oct 28, 2026
bb7e567
Read the Anthropic key and API version the 2.x SDK sends in the LangG…
davidmckayv Oct 28, 2026
4d23c1f
Launch the Chromium build CI installs in the enter-text test
davidmckayv Oct 28, 2026
0e3788a
Record isBrokered's new home in the duplicate-definition guard
davidmckayv Oct 28, 2026
a698bad
Merge main into the skins
davidmckayv Oct 28, 2026
dcdf57e
Merge main
davidmckayv Oct 28, 2026
605925a
Wait for home's composer, not its greeting, when a skin switch lands …
davidmckayv Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,18 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.
`computers/<Bot id>` on the workspace volume (below). Files saved before the upgrade stay at the
root of the volume, where no Bot sees them; move them into a Bot's folder to give them back.

### Pick a skin: OpenDuet, OpenDots or OpenMuse

Settings → Preferences → Skin changes how OpenBot looks for you, and nothing about what it does.
Three skins draw the open-source templates' looks over OpenBot's own features: OpenDuet (one
phone-width column with Chat and Tasks), OpenDots (a rail, a sidebar of Bots with their avatars, and
the Bot's computer beside the chat) and OpenMuse (one Bot at the centre, with Chat, Activity, Ideas,
Goals and Apps). Every skin keeps your deployment's name from `brand.yaml`, OpenBot's own mark and
each Bot's own avatar; only the look changes. The choice is saved on your account, so it follows
you to every device, and changes nothing for anybody else. The templates' own features that
OpenBot has no server for, such as OpenDots' Spaces, OpenDuet's morning brief and OpenMuse's mail and
calendar, are not part of the skins.

### The audit log reads like a log

Admin → Audit fills the window and runs the table across the whole pane, with the title, the views
Expand Down
2 changes: 2 additions & 0 deletions app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
const dark = window.localStorage.getItem("openbot-theme") === "dark";
document.documentElement.classList.toggle("dark", dark);
document.documentElement.style.colorScheme = dark ? "dark" : "light";
const skin = window.localStorage.getItem("openbot-skin");
if (skin && skin !== "openbot") document.documentElement.dataset.skin = skin;
} catch {}
</script>
</head>
Expand Down
129 changes: 38 additions & 91 deletions app/src/components/app-sidebar/app-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
import {
IconBolt,
IconBox,
IconLogout,
IconPlug,
IconPlus,
IconSearch,
IconSettings,
IconShieldLock,
} from "@tabler/icons-react";
import { IconLogout, IconPlus, IconSearch } from "@tabler/icons-react";
import {
useInfiniteQuery,
useMutation,
Expand Down Expand Up @@ -54,6 +45,7 @@ import {
import { useChannelEvents } from "@/lib/channels/use-channel-events";
import { appConfig } from "@/lib/generated/application-config";
import { EASE_OUT, ENTRANCE_SECONDS } from "@/lib/motion";
import { navigationFor } from "@/lib/navigation";
import { relativeTime } from "@/lib/relative-time";
import {
type MessageListEmphasis,
Expand All @@ -66,8 +58,6 @@ import { Channel } from "./channel";
import { ChannelPagination } from "./channel-pagination";

const appLinkOptions = { to: "/" } satisfies LinkOptions;
const adminLinkOptions = { to: "/admin" } satisfies LinkOptions;
const settingsLinkOptions = { to: "/settings" } satisfies LinkOptions;

const userMenuItemClassName = "gap-2 px-2 py-1.5";

Expand Down Expand Up @@ -238,6 +228,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
*/
const animateOrder =
!searching && (channels.data?.length ?? 0) <= MAX_ANIMATED_ROWS;
const navigation = navigationFor(currentUser?.role);

const handleSignOut = async () => {
await signOut.mutateAsync();
Expand Down Expand Up @@ -349,68 +340,29 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
</SidebarContent>
<SidebarFooter>
<SidebarMenu className="gap-px">
<SidebarMenuItem>
<SidebarMenuButton
className="hover:bg-foreground/5 h-10"
render={(props) => (
<Link
{...props}
to="/bots"
activeProps={{ className: "bg-foreground/5" }}
/>
)}
>
<div className="size-[28px] flex items-center justify-center">
<IconBolt />
</div>
<span className="text-sm">Bots</span>
{/* What waits on you across every Bot; Bots lists them first. */}
<BotsNavBadge />
</SidebarMenuButton>
</SidebarMenuItem>
{/*
* Beside Bots rather than inside Admin: each is somebody's own. Capabilities are your skills
* and what your Bots remember about you; Connections are the apps a Bot reaches as you.
* Capabilities links to `/capabilities` rather than a tab, so it stays lit on both tabs.
*/}
<SidebarMenuItem>
<SidebarMenuButton
className="hover:bg-foreground/5 h-10"
render={(props) => (
<Link
{...props}
to="/capabilities"
activeProps={{
className: "bg-foreground/5",
}}
/>
)}
>
<div className="size-[28px] flex items-center justify-center">
<IconBox />
</div>
<span className="text-sm trackint-tight">Capabilities</span>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton
className="hover:bg-foreground/5 h-10"
render={(props) => (
<Link
{...props}
to="/connections"
activeProps={{
className: "bg-foreground/5",
}}
/>
)}
>
<div className="size-[28px] flex items-center justify-center">
<IconPlug />
</div>
<span className="text-sm trackint-tight">Connections</span>
</SidebarMenuButton>
</SidebarMenuItem>
{navigation
.filter((item) => item.section === "primary")
.map((item) => (
<SidebarMenuItem key={item.id}>
<SidebarMenuButton
className="hover:bg-foreground/5 h-10"
render={(props) => (
<Link
{...props}
to={item.to}
activeProps={{ className: "bg-foreground/5" }}
/>
)}
>
<div className="size-[28px] flex items-center justify-center">
<item.icon />
</div>
<span className="text-sm">{item.label}</span>
{/* What waits on you across every Bot; Bots lists them first. */}
{item.id === "bots" ? <BotsNavBadge /> : null}
</SidebarMenuButton>
</SidebarMenuItem>
))}
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger
Expand All @@ -429,23 +381,18 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
side="top"
sideOffset={8}
>
{/* Admin routes are server-guarded; hide the entry for users who cannot open them. */}
{currentUser?.role === "admin" ? (
<DropdownMenuItem
className={userMenuItemClassName}
render={<Link {...adminLinkOptions} />}
>
<IconShieldLock />
Admin
</DropdownMenuItem>
) : null}
<DropdownMenuItem
className={userMenuItemClassName}
render={<Link {...settingsLinkOptions} />}
>
<IconSettings />
Settings
</DropdownMenuItem>
{navigation
.filter((item) => item.section === "account")
.map((item) => (
<DropdownMenuItem
className={userMenuItemClassName}
key={item.id}
render={<Link to={item.to} />}
>
<item.icon />
{item.label}
</DropdownMenuItem>
))}
<DropdownMenuItem
className={userMenuItemClassName}
disabled={signOut.isPending}
Expand Down
79 changes: 45 additions & 34 deletions app/src/components/channels/composer/composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1345,6 +1345,7 @@ export function Composer({
<Button
aria-label="Stop the Bot"
className={actionClassName}
data-slot="composer-stop"
data-testid="composer-stop"
onClick={onStop}
size="icon"
Expand Down Expand Up @@ -1374,6 +1375,7 @@ export function Composer({
<Button
aria-label={sendLabel}
className={actionClassName}
data-slot="composer-send"
disabled={!canSend}
size="icon"
type="submit"
Expand Down Expand Up @@ -1479,6 +1481,7 @@ export function Composer({
{tooManyStagedNotice}
<form
aria-busy={isBusy}
data-slot="composer"
className={cn(
/*
* `py-3` IS LOAD-BEARING ONCE THE TEXT WRAPS. On one line `min-h-14` and the controls
Expand Down Expand Up @@ -1520,6 +1523,7 @@ export function Composer({
<Button
aria-label="Attach a file"
className="self-end"
data-slot="composer-attach"
// The button stays, and stays labelled: a conversation that cannot take another
// message has not lost the ability to attach, it has lost the message. Swapping it
// for the "unavailable" placeholder would say the wrong thing about why.
Expand All @@ -1543,23 +1547,26 @@ export function Composer({
<IconPlus className="size-5" />
</Button>
)}
<PromptArea
aria-label="Message"
className={cn(
"min-w-0 flex-1 border-0 bg-transparent p-0 text-sm shadow-none",
editorClassName,
)}
disabled={disabled}
maxHeight={COMPACT_MAX_HEIGHT_PX}
minHeight={COMPACT_MIN_HEIGHT_PX}
onChange={handleChange}
onImagePaste={canAttach ? stagePastedImage : undefined}
onSubmit={submitDraft}
placeholder="Ask anything"
ref={promptAreaRef}
triggers={triggers}
value={value}
/>
{/* The slot is ours: `PromptArea` passes no attribute through to its own box. */}
<div className="min-w-0 flex-1" data-slot="composer-input">
<PromptArea
aria-label="Message"
className={cn(
"w-full border-0 bg-transparent p-0 text-sm shadow-none",
editorClassName,
)}
disabled={disabled}
maxHeight={COMPACT_MAX_HEIGHT_PX}
minHeight={COMPACT_MIN_HEIGHT_PX}
onChange={handleChange}
onImagePaste={canAttach ? stagePastedImage : undefined}
onSubmit={submitDraft}
placeholder="Ask anything"
ref={promptAreaRef}
triggers={triggers}
value={value}
/>
</div>
<DictationButton
dictation={dictation}
disabled={disabled || voiceCall?.active === true}
Expand All @@ -1585,6 +1592,7 @@ export function Composer({
<form
aria-busy={isBusy}
className="overflow-hidden rounded-2xl border border-border bg-card"
data-slot="composer"
onSubmit={handleFormSubmit}
>
<DictationSurface
Expand All @@ -1600,30 +1608,33 @@ export function Composer({
images={images}
onRemove={discardAttachment}
/>
<PromptArea
aria-label="Message"
autoGrow
className={cn(
"w-full border-0 bg-transparent p-0 text-sm shadow-none",
editorClassName,
)}
disabled={disabled}
maxHeight={MAX_HEIGHT_PX}
onChange={handleChange}
onImagePaste={canAttach ? stagePastedImage : undefined}
onSubmit={submitDraft}
placeholder="Ask anything"
ref={promptAreaRef}
triggers={triggers}
value={value}
/>
<div data-slot="composer-input">
<PromptArea
aria-label="Message"
autoGrow
className={cn(
"w-full border-0 bg-transparent p-0 text-sm shadow-none",
editorClassName,
)}
disabled={disabled}
maxHeight={MAX_HEIGHT_PX}
onChange={handleChange}
onImagePaste={canAttach ? stagePastedImage : undefined}
onSubmit={submitDraft}
placeholder="Ask anything"
ref={promptAreaRef}
triggers={triggers}
value={value}
/>
</div>
</div>

<div className="flex items-center justify-between">
{attachmentsEnabled ? (
<Button
aria-label="Attach a file"
className="size-7 rounded-full p-0"
data-slot="composer-attach"
disabled={disabled}
onClick={() => fileInputRef.current?.click()}
type="button"
Expand Down
3 changes: 3 additions & 0 deletions app/src/components/channels/composer/dictation-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function DictationButton({
return (
<Button
aria-label="Dictate a message"
data-slot="composer-dictate"
title={
dictation.supported
? "Dictate a message"
Expand Down Expand Up @@ -119,6 +120,7 @@ function RecordingControls({
<Button
ref={cancel}
aria-label="Cancel dictation"
data-slot="composer-dictate-cancel"
title="Cancel dictation"
size="icon"
type="button"
Expand Down Expand Up @@ -190,6 +192,7 @@ function RecordingControls({
<div className="flex shrink-0 items-center gap-2">
<Button
aria-label="Stop and transcribe"
data-slot="composer-dictate-stop"
title="Stop and add to draft"
disabled={!recording}
size="icon"
Expand Down
5 changes: 4 additions & 1 deletion app/src/components/channels/conversation-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,10 @@ export function ConversationView({
working={working}
/>
</div>
<div className="max-w-2xl mx-auto w-full px-0 pb-4 shrink-0">
<div
className="max-w-2xl mx-auto w-full px-0 pb-4 shrink-0"
data-slot="composer-column"
>
{notice}
<Composer
agents={agents}
Expand Down
11 changes: 9 additions & 2 deletions app/src/components/layout/detail-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,18 @@ export function DetailPanel({
>
<div
className="flex h-full flex-col bg-sidebar border-l border-border"
data-slot="detail-pane"
style={{ width: detailWidth }}
>
{/* Rendered for the whole animation, so the way out is available immediately. */}
<div className="h-12 shrink-0 sticky top-0 flex flex-row items-center justify-between px-2 gap-2">
<div className="flex min-w-0 w-full items-center gap-1.5">
<div
className="h-12 shrink-0 sticky top-0 flex flex-row items-center justify-between px-2 gap-2"
data-slot="detail-pane-header"
>
<div
className="flex min-w-0 w-full items-center gap-1.5"
data-slot="detail-pane-title"
>
{title}
</div>
<div className="flex flex-row gap-1.5">
Expand Down
Loading
Loading