Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8df3768
Add tests for Corbit agent avatar and pastel human initials fallback
cursoragent Sep 4, 2026
54fbb09
Add Corbit agent avatar and light pastel fallback for people without …
cursoragent Sep 4, 2026
4052947
Update docs: document Corbit agent avatar and pastel human fallback
cursoragent Sep 4, 2026
d5427fd
Fix error-sink mock in inbox test to include error envelope helpers
cursoragent Sep 4, 2026
c93e887
Consolidate avatar rendering and Tailwind styles
pratikbuilds Sep 4, 2026
06d295e
Fix check:ui-vocabulary: drop retired chat- prefix from avatar class …
TheGreatAxios Sep 4, 2026
a3bea22
Format timeline.tsx after shortened className rename
TheGreatAxios Sep 4, 2026
f638363
Fix stale chat-member-avatar selectors after ui-vocabulary rename
TheGreatAxios Sep 4, 2026
c1e334d
Format team-avatar-stack test after selector rename
TheGreatAxios Sep 4, 2026
997d3de
Fix stale sender-avatar selectors after ui-vocabulary rename
TheGreatAxios Sep 4, 2026
b8effa6
Merge branch 'main' into cursor/corbit-avatar-pastel-fallback-92f0
pratikbuilds Sep 5, 2026
036c935
Simplify generated avatar classes
pratikbuilds Sep 4, 2026
71c0ed0
Merge branch 'main' into cursor/corbit-avatar-pastel-fallback-92f0
pratikbuilds Sep 5, 2026
2c66817
Merge branch 'main' into cursor/corbit-avatar-pastel-fallback-92f0
pratikbuilds Sep 6, 2026
a7fe866
Merge branch 'main' into cursor/corbit-avatar-pastel-fallback-92f0
TheGreatAxios Sep 6, 2026
7cf2679
chat-ui: drop the avatar class-shape test
TheGreatAxios Sep 6, 2026
778d3b2
chat-ui: format avatar.test.tsx
TheGreatAxios Sep 6, 2026
e3c87c0
Merge branch 'main' into cursor/corbit-avatar-pastel-fallback-92f0
TheGreatAxios Sep 6, 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
15 changes: 7 additions & 8 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,14 +153,13 @@ and the rest of its semantic palette. Never hardcode a hex value or an
arbitrary Tailwind color class in product code; if a needed token doesn't
exist yet, add it in react-ui, not locally.

**Generated identity color** is the one deliberate exception: a person's
fallback avatar (no explicit picture) needs a color per principal, not a
handful of shared tokens, so it's the same `colorForPrincipal` hash
already shipped for presence cursors (`@corbits/presence/color`), paired
with a computed black/white initials color for contrast
(`@corbits/chat-ui`'s `generatedAvatarStyle`). Agents keep react-ui's
`Avatar` tone system (solid `--primary`/`--accent`/`--success`) so the two
identity kinds stay visually distinct at a glance.
**Avatar identity** is the one deliberate color exception. A person without
an explicit image receives deterministic initials from the approved pastel
palette. `@corbits/chat-ui` owns the finite Tailwind background/text class
map and resolves a principal to one class pair; consumers never use CSS
variables or inline color styles. Agents use `CorbitAvatar`: a circular
field with a dark lower visor and sensor glint. The two forms stay distinct
at a glance in every collaborative surface.

**Type.** Red Hat Display for sans (UI text, headings), Space Mono for
monospace (code, IDs, numeric/tabular contexts). Both are declared once in
Expand Down
49 changes: 1 addition & 48 deletions apps/web/src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -1329,12 +1329,7 @@ select:disabled,
outline-offset: -1px;
}

/* `Avatar`'s own root span carries `role="img"`, not a `data-slot` — this
used to target a `[data-slot="avatar"]` that react-ui's `Avatar` has
never actually rendered, so the account avatar silently fell back to
react-ui's own default sizing/shape. Geometry only: background/text
color come from the generated per-person fill
(`.avatar-identity-generated`, lower specificity, so it still wins). */
/* `Avatar`'s own root span carries `role="img"`, not a `data-slot`. */
.shell-sidebar-account-btn [role="img"] {
flex-shrink: 0;
width: 2.1rem;
Expand Down Expand Up @@ -2992,48 +2987,6 @@ tr.insights-row-clickable:hover {
color: var(--muted-foreground);
}

/* Create-agent panel's identity row (`create-agent-panel.tsx`): a live
Avatar preview beside a row of tone swatches. Preview-only — see the
component's own doc comment on why no color is persisted today. */
.create-agent-identity {
display: flex;
align-items: center;
gap: 0.75rem;
}

.create-agent-tone-row {
display: flex;
gap: 0.4rem;
}

.create-agent-tone-swatch {
width: 1.5rem;
height: 1.5rem;
border: 2px solid transparent;
cursor: pointer;
padding: 0;
}

.create-agent-tone-swatch[aria-pressed="true"] {
border-color: var(--foreground);
}

.create-agent-tone-neutral {
background: var(--muted);
}

.create-agent-tone-agent {
background: var(--primary);
}

.create-agent-tone-agent2 {
background: var(--accent);
}

.create-agent-tone-agent3 {
background: var(--success);
}

/* The optional "what should this agent do?" field: visually quieter than
the required Name field above it — a smaller label, no bold-required
asterisk, since a name alone is the supported happy path. */
Expand Down
59 changes: 22 additions & 37 deletions apps/web/src/pages/create-agent-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
// lane).

import {
Avatar,
Button,
Dialog,
DialogBody,
Expand All @@ -51,8 +50,15 @@ import {
IntakeForm,
Textarea,
} from "@corbits/react-ui";
import type { AvatarTone, IntakeField } from "@corbits/react-ui";
import type { IntakeField } from "@corbits/react-ui";
import { useEffect, useState } from "react";
import {
AVATAR_COLORS,
avatarColorClass,
CORBIT_DEFAULT_COLOR,
CorbitAvatar,
} from "@corbits/chat-ui";
import type { AvatarColor } from "@corbits/chat-ui";

import { ApiQueryError } from "@corbits/api-query";

Expand Down Expand Up @@ -83,23 +89,6 @@ function submitErrorFromCause(
};
}

function initialsFromName(name: string): string {
const [first, second] = name.trim().split(/\s+/).filter(Boolean);
if (first === undefined) return "?";
if (second === undefined) return first.slice(0, 2);
return `${first[0]}${second[0]}`;
}

const AVATAR_TONES: readonly {
readonly tone: AvatarTone;
readonly label: string;
}[] = [
{ tone: "neutral", label: "Grey" },
{ tone: "agent", label: "Orange" },
{ tone: "agent2", label: "Blue" },
{ tone: "agent3", label: "Green" },
];

type FormValues = {
readonly name: string;
readonly purpose: string;
Expand Down Expand Up @@ -248,7 +237,8 @@ export function CreateAgentPanel({
readonly onCreated: (definition: AgentDefinition) => void;
}) {
const [values, setValues] = useState<FormValues>(EMPTY_VALUES);
const [tone, setTone] = useState<AvatarTone>("agent");
const [avatarColor, setAvatarColor] =
useState<AvatarColor>(CORBIT_DEFAULT_COLOR);
const [handleTouched, setHandleTouched] = useState(false);
const [advancedOpen, setAdvancedOpen] = useState(false);
const [draftFailed, setDraftFailed] = useState(false);
Expand Down Expand Up @@ -284,7 +274,7 @@ export function CreateAgentPanel({

function reset() {
setValues(EMPTY_VALUES);
setTone("agent");
setAvatarColor(CORBIT_DEFAULT_COLOR);
setHandleTouched(false);
setAdvancedOpen(false);
setDraftFailed(false);
Expand Down Expand Up @@ -437,27 +427,22 @@ export function CreateAgentPanel({
</p>
)}

<div className="create-agent-identity">
<Avatar
initials={initialsFromName(values.name)}
label={values.name.trim() === "" ? "New agent" : values.name}
tone={tone}
<div className="flex items-center gap-3">
<CorbitAvatar
ariaLabel={values.name.trim() === "" ? "New agent" : values.name}
color={avatarColor}
size="lg"
/>
<div
role="group"
aria-label="Agent color"
className="create-agent-tone-row"
>
{AVATAR_TONES.map((entry) => (
<div role="group" aria-label="Agent color" className="flex gap-1.5">
{AVATAR_COLORS.map((color) => (
<button
key={entry.tone}
key={color}
type="button"
aria-label={entry.label}
aria-pressed={tone === entry.tone}
className={`create-agent-tone-swatch create-agent-tone-${entry.tone}`}
aria-label={`Choose ${color} avatar color`}
aria-pressed={avatarColor === color}
className={`size-6 cursor-pointer rounded-full border-2 border-transparent p-0 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-foreground aria-pressed:border-foreground ${avatarColorClass[color]}`}
disabled={submitting}
onClick={() => setTone(entry.tone)}
onClick={() => setAvatarColor(color)}
/>
))}
</div>
Expand Down
10 changes: 2 additions & 8 deletions apps/web/src/shell/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,9 @@ import {
SlidersHorizontal,
SquaresFour,
} from "@corbits/icons";
import type { CSSProperties } from "react";
import { useMemo } from "react";

import {
AVATAR_IDENTITY_CLASS,
CHAT_STRINGS,
generatedAvatarStyle,
} from "@corbits/chat-ui";
import { CHAT_STRINGS, avatarClassForPrincipal } from "@corbits/chat-ui";
import {
createInsightsWindow,
usageChromeLabel,
Expand Down Expand Up @@ -278,14 +273,13 @@ export function Sidebar({
aria-label={`${user.name} · Account menu`}
title={user.name}
data-ctx-account=""
style={generatedAvatarStyle(user.id) as CSSProperties}
>
<Avatar
initials={initialsOf(user.name)}
label={user.name}
size="sm"
tone="neutral"
className={AVATAR_IDENTITY_CLASS}
className={avatarClassForPrincipal(user.id)}
/>
<span className="shell-sidebar-account-name">{user.name}</span>
</button>
Expand Down
21 changes: 17 additions & 4 deletions apps/web/src/shell/workbench-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
import { toast } from "@corbits/react-ui";
import {
CHAT_STRINGS,
CorbitAvatar,
displayWorkbenchTitle,
workbenchesQueryKeyPrefix,
patchWorkbenchSettings,
Expand Down Expand Up @@ -313,11 +314,23 @@ function WorkbenchRow({
data-active={active ? "true" : undefined}
onClick={onSelect}
>
{/* The workbench's own initial, not its agent's — a DM and a
channel can share an agent, so an agent monogram would
collapse both rows into the same letter. */}
{/* The workbench's own initial for multi-party channels, or
the Corbit avatar for agent DM conversations. */}
<span className="shell-ch-stack" aria-hidden="true">
<span>{displayTitle.slice(0, 1).toUpperCase()}</span>
{workbench.kind === "chat" ? (
<span
data-agent="true"
className="!overflow-hidden !border-[1.5px] !border-background !bg-transparent !p-0"
>
<CorbitAvatar
size="sm"
ariaLabel={displayTitle}
className="!static !size-full"
/>
</span>
) : (
<span>{displayTitle.slice(0, 1).toUpperCase()}</span>
)}
</span>
<span className="shell-ch-meta">
<span className="shell-ch-name-row">
Expand Down
81 changes: 0 additions & 81 deletions packages/chat-ui/src/avatar-identity.test.ts

This file was deleted.

Loading
Loading