Q15: show which messages each agent actually read

Runs record a context manifest once a model turn succeeds; the chat
marks each human message with the agents that read its current version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 04:21:57 -04:00
1 parent 592ce69219
commit 469f2f2c35
14 files changed
+144 -10

No files matched your search

+1
View File
@@ -27,6 +27,7 @@ export const zh: Record<string, string> = {
"Advance": "推进",
"Advanced": "高级",
"Integrations": "集成密钥",
"Read by {names}": "已读:{names}",
"Keys are stored on this device, encrypted by the operating system, and shared by every company.": "密钥保存在本机,由操作系统加密,所有公司共用。",
"Composio API key": "Composio API 密钥",
"Powers the Connectors page and agent connector tools. Create one at platform.composio.dev.": "用于连接器页面和 Agent 的连接器工具。在 platform.composio.dev 创建。",
+6 -1
View File
@@ -8,6 +8,7 @@ import {
buildConversations,
collectParticipants,
filterApprovalsForSpace,
messageReaders,
} from "./chat/chat-helpers";
import type { PageProps } from "./types";
import { activeOrgId, requireActiveEmployee } from "./types";
@@ -27,6 +28,10 @@ export function ChatPage({ snapshot, actions }: PageProps) {
const agents = useMemo(() => snapshot.agents.filter((item) => item.orgId === orgId), [snapshot.agents, orgId]);
const files = useMemo(() => snapshot.files.filter((item) => item.orgId === orgId), [snapshot.files, orgId]);
const messagesAll = useMemo(() => snapshot.chatMessages.filter((m) => m.orgId === orgId), [snapshot.chatMessages, orgId]);
const readersByMessage = useMemo(
() => messageReaders(snapshot.taskRuns.filter((run) => run.orgId === orgId), agents, messagesAll),
[snapshot.taskRuns, orgId, agents, messagesAll],
);
const messageIds = useMemo(() => new Set(messagesAll.map((message) => message.id)), [messagesAll]);
const mentionsAll = useMemo(
() => snapshot.chatMentions.filter((mention) => messageIds.has(mention.messageId)),
@@ -202,7 +207,7 @@ export function ChatPage({ snapshot, actions }: PageProps) {
mentions={mentionsAll}
slashInvocations={slashInvocationsAll}
files={files}
context={{ employees, agents }}
context={{ employees, agents, readersByMessage }}
attachmentFileId={attachmentFileId}
currentEmployeeId={employee.id}
onAttachmentChange={setAttachmentFileId}
@@ -41,6 +41,7 @@ export function ChatMessageRow({
const messageFiles = deleted ? [] : messageAttachments(message.id, attachments, files);
const messageMentions = mentions.filter((mention) => mention.messageId === message.id);
const slashInvocation = deleted ? null : slashInvocations.find((invocation) => invocation.messageId === message.id) ?? null;
const readers = bot || deleted ? [] : context.readersByMessage.get(message.id) ?? [];
const submitEdit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const next = new FormData(event.currentTarget).get("body");
@@ -99,6 +100,11 @@ export function ChatMessageRow({
})}
</div>
)}
{readers.length > 0 && (
<div style={{ fontSize: 12, color: "var(--text-3)", marginTop: 4 }}>
{t("Read by {names}", { names: readers.join(", ") })}
</div>
)}
</div>
</div>
);
+25 -2
View File
@@ -8,15 +8,38 @@ import type {
ChatSpaceRecord,
EmployeeRecord,
FileObjectRecord,
TaskRunRecord,
} from "../../../../shared/domain";
import { t } from "../../i18n/i18n";
export interface MessageContext {
employees: EmployeeRecord[];
agents: AgentRecord[];
/** Agent names whose runs read each message. */
readersByMessage: Map<string, string[]>;
}
export function senderName(message: ChatMessageRecord, context: MessageContext): string {
/** Which agents' runs read the current version of each message. */
export function messageReaders(taskRuns: TaskRunRecord[], agents: AgentRecord[], messages: ChatMessageRecord[]): Map<string, string[]> {
const currentVersion = new Map(messages.map((message) => [message.id, message.editedAt ?? message.createdAt]));
const readers = new Map<string, string[]>();
for (const run of taskRuns) {
if (!run.contextJson || !run.agentId) continue;
const agent = agents.find((item) => item.id === run.agentId);
if (!agent) continue;
type Entry = { id: string; version: string };
const manifest = JSON.parse(run.contextJson) as { history: Entry[]; fresh: Entry[]; midRun: Entry[] };
for (const entry of [...manifest.history, ...manifest.fresh, ...manifest.midRun]) {
if (currentVersion.get(entry.id) !== entry.version) continue;
const names = readers.get(entry.id) ?? [];
if (!names.includes(agent.name)) names.push(agent.name);
readers.set(entry.id, names);
}
}
return readers;
}
export function senderName(message: ChatMessageRecord, context: Pick<MessageContext, "employees" | "agents">): string {
// A message whose principal no longer resolves must degrade to a visible
// placeholder, not throw: one orphaned row would otherwise blank the whole
// Messages page mid-render.
@@ -128,7 +151,7 @@ export function buildConversations(input: {
if (group) group.push(membership);
else membersBySpace.set(membership.spaceId, [membership]);
}
const context: MessageContext = { employees: input.employees, agents: input.agents };
const context = { employees: input.employees, agents: input.agents };
const items = input.spaces.map((space) => {
const last = lastBySpace.get(space.id) ?? null;