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:
1 parent
592ce69219
commit
469f2f2c35
14 files changed
+144
-10
No files matched your search
@@ -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 创建。",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user