bell: move it left, and let its colour say what it will do

the bell sat on the right between the connection dot and the end-session plug,
which put a SETTING in among the STATUS. everything on the right reports what
the chat is doing now; the bell says what it will do later. it moves to the
left of the header, on its own.

and it is coloured by preference, so the setting is readable without opening
the menu: green for every end of turn, amber for input-needed, muted for none.

green is `green-500` rather than the `text-success` token deliberately — it
has to be the SAME green as the connection dot a few elements away, and a
semantic token that drifted from it would leave two greens in one header
meaning two different things. amber is `text-warning` (#f59e0b), which is the
conditional setting: it will speak, but only sometimes.

`none` keeps the struck-through bell as well as the muted colour. colour alone
is not a state anyone should have to learn, and it is the one setting where
being wrong is silent.
This commit is contained in:
2026-08-19 23:19:09 +00:00
parent 34e21317da
commit 9ca2287e50
2 changed files with 23 additions and 2 deletions
@@ -108,6 +108,11 @@ function DetailBar({
return ( return (
<div className="shrink-0 flex items-center px-4 py-2 border-b border-border bg-background/60"> <div className="shrink-0 flex items-center px-4 py-2 border-b border-border bg-background/60">
{/* Left, away from the connection dot and the end-session plug. It is a setting, not a status: it
says what this chat WILL do, while everything on the right reports what it is doing now. */}
<div className="flex items-center">
{sessionId && <NotifyMenu preference={notifyPreference ?? 'none'} onChange={commitNotifyPreference} />}
</div>
<div className="flex-1 min-w-0 px-3 text-center"> <div className="flex-1 min-w-0 px-3 text-center">
<EditableTitle <EditableTitle
value={sessionTitle ?? 'New chat'} value={sessionTitle ?? 'New chat'}
@@ -133,7 +138,6 @@ function DetailBar({
<span className="inline-block h-2 w-2 rounded-full bg-green-500" /> <span className="inline-block h-2 w-2 rounded-full bg-green-500" />
)} )}
<span>{!isConnected ? 'Disconnected' : isGenerating ? 'Working...' : ''}</span> <span>{!isConnected ? 'Disconnected' : isGenerating ? 'Working...' : ''}</span>
{sessionId && <NotifyMenu preference={notifyPreference ?? 'none'} onChange={commitNotifyPreference} />}
{isConnected && onDisconnect && ( {isConnected && onDisconnect && (
<button <button
type="button" type="button"
@@ -10,6 +10,23 @@ const OPTIONS: { value: NotifyPreference; label: string }[] = [
{ value: 'none', label: 'None' }, { value: 'none', label: 'None' },
]; ];
/**
* The bell says what it will do without being opened.
*
* Green is `green-500` on purpose rather than `text-success` — it has to be the SAME green as the
* connection dot two elements away, and a semantic token that drifted from it would leave two greens in
* one header meaning two different things.
*
* Amber for `input_needed` because it is the conditional setting: it will speak, but only sometimes.
* Muted for `none`, which is also the only state that draws a struck-through bell — colour alone is not
* a state anyone should have to learn.
*/
const BELL_TONE: Record<NotifyPreference, string> = {
every_turn: 'text-green-500',
input_needed: 'text-warning',
none: 'text-muted-foreground',
};
type NotifyMenuProps = { type NotifyMenuProps = {
preference: NotifyPreference; preference: NotifyPreference;
onChange: (preference: NotifyPreference) => void; onChange: (preference: NotifyPreference) => void;
@@ -40,7 +57,7 @@ export const NotifyMenu = ({ preference, onChange }: NotifyMenuProps) => {
type="button" type="button"
title={`Notifications: ${OPTIONS.find((o) => o.value === preference)?.label ?? 'None'}`} title={`Notifications: ${OPTIONS.find((o) => o.value === preference)?.label ?? 'None'}`}
aria-label="Notification preference" aria-label="Notification preference"
className="p-1 rounded hover:bg-muted transition-colors cursor-pointer" className={`rounded p-1 transition-colors hover:bg-muted cursor-pointer ${BELL_TONE[preference]}`}
> >
{preference === 'none' ? <BellOff className="h-3.5 w-3.5" /> : <Bell className="h-3.5 w-3.5" />} {preference === 'none' ? <BellOff className="h-3.5 w-3.5" /> : <Bell className="h-3.5 w-3.5" />}
</button> </button>