Show agent presence on direct-message avatars

chat_presence was recorded (active, waiting_approval, thinking) but never
surfaced. Map each status to a colour and render a presence dot on the
counterpart's avatar in the direct-message list, matching the design.
This commit is contained in:
ZacharyZhang-NY committed 2026-05-28 19:50:27 -04:00
1 parent b4056502ec
commit 0299ebbade
3 files changed
+46 -2

No files matched your search

@@ -214,6 +214,7 @@ export function ChatPage({ snapshot, actions }: PageProps) {
directs={directs}
agents={agents}
messages={messagesAll}
presence={snapshot.chatPresence}
activeSpaceId={activeSpaceId}
onSelect={(spaceId) => {
setSelectedSpaceId(spaceId);
@@ -1,13 +1,29 @@
import type { ReactNode } from "react";
import { Avatar, Icon, IconButton } from "../../shared/ui";
import type { AgentRecord, ChatMessageRecord, ChatSpaceRecord } from "../../../../shared/domain";
import type { AgentRecord, ChatMessageRecord, ChatPresenceRecord, ChatSpaceRecord } from "../../../../shared/domain";
import { countMessagesIn } from "./chat-helpers";
function presenceColor(status: string | undefined): string | null {
switch (status) {
case "active":
case "online":
return "var(--green)";
case "waiting_approval":
case "busy":
return "var(--amber)";
case "thinking":
return "var(--accent)";
default:
return null;
}
}
export function ChatSidebar({
channels,
directs,
agents,
messages,
presence,
activeSpaceId,
onSelect,
onCreateSpace,
@@ -16,10 +32,12 @@ export function ChatSidebar({
directs: ChatSpaceRecord[];
agents: AgentRecord[];
messages: ChatMessageRecord[];
presence: ChatPresenceRecord[];
activeSpaceId: string | null;
onSelect: (spaceId: string) => void;
onCreateSpace: () => void;
}) {
const presenceByPrincipal = new Map(presence.map((entry) => [entry.principalId, entry.status]));
return (
<aside className="secondary-pane">
<div className="row" style={{ padding: "4px 8px 12px" }}>
@@ -47,6 +65,7 @@ export function ChatSidebar({
key={space.id}
space={space}
agents={agents}
presenceByPrincipal={presenceByPrincipal}
active={space.id === activeSpaceId}
count={countMessagesIn(space.id, messages)}
onClick={() => onSelect(space.id)}
@@ -95,20 +114,30 @@ function ChannelItem({
function DirectItem({
space,
agents,
presenceByPrincipal,
active,
count,
onClick,
}: {
space: ChatSpaceRecord;
agents: AgentRecord[];
presenceByPrincipal: Map<string, string>;
active: boolean;
count: number;
onClick: () => void;
}) {
const counterpart = agents.find((agent) => space.name.includes(agent.name));
const dotColor = counterpart ? presenceColor(presenceByPrincipal.get(counterpart.id)) : null;
return (
<button type="button" className={`nav-item ${active ? "active" : ""}`.trim()} onClick={onClick}>
{counterpart ? <Avatar name={counterpart.name} size="sm" bot /> : <Icon name="user" size={13} />}
{counterpart ? (
<span className="presence">
<Avatar name={counterpart.name} size="sm" bot />
{dotColor && <span className="presence-dot" style={{ background: dotColor }} />}
</span>
) : (
<Icon name="user" size={13} />
)}
<span>{space.name}</span>
{count > 0 && <span className="nav-badge">{count}</span>}
</button>
@@ -311,6 +311,20 @@ a { color: inherit; }
min-height: 0;
overflow-y: auto;
}
.presence {
position: relative;
display: inline-flex;
flex-shrink: 0;
}
.presence-dot {
position: absolute;
right: -1px;
bottom: -1px;
width: 8px;
height: 8px;
border-radius: 50%;
border: 1.5px solid var(--bg);
}
.page { max-width: 1100px; }
.page-wide { width: 100%; }
.page-h1 {