diff --git a/AgentCompany-New/src/renderer/src/pages/ChatPage.tsx b/AgentCompany-New/src/renderer/src/pages/ChatPage.tsx index 7166f8f7..d75702bd 100644 --- a/AgentCompany-New/src/renderer/src/pages/ChatPage.tsx +++ b/AgentCompany-New/src/renderer/src/pages/ChatPage.tsx @@ -8,6 +8,7 @@ import { collectParticipants, filterApprovalsForSpace, isRootMessage, + replayEventsForThread, } from "./chat/chat-helpers"; import type { PageProps } from "./types"; import { activeOrgId, requireActiveEmployee } from "./types"; @@ -41,6 +42,10 @@ export function ChatPage({ snapshot, actions }: PageProps) { () => snapshot.approvals.filter((a) => a.orgId === orgId && a.state === "pending"), [snapshot.approvals, orgId], ); + const auditEventsAll = useMemo( + () => snapshot.auditEvents.filter((event) => event.orgId === orgId), + [snapshot.auditEvents, orgId], + ); const channels = useMemo(() => spaces.filter((space) => space.spaceType !== "direct"), [spaces]); const directs = useMemo(() => spaces.filter((space) => space.spaceType === "direct"), [spaces]); @@ -84,6 +89,10 @@ export function ChatPage({ snapshot, actions }: PageProps) { const threadMessages = selectedThread ? messagesInSpace.filter((m) => m.threadId === selectedThread.id && m.id !== selectedThread.rootMessageId) : []; + const threadAuditEvents = useMemo( + () => (selectedThread ? replayEventsForThread(auditEventsAll, selectedThread, messagesInSpace) : []), + [auditEventsAll, selectedThread, messagesInSpace], + ); const participants = useMemo( () => collectParticipants(messagesInSpace, employees, agents), [messagesInSpace, employees, agents], @@ -201,6 +210,7 @@ export function ChatPage({ snapshot, actions }: PageProps) { mentions={mentionsAll} slashInvocations={slashInvocationsAll} files={files} + auditEvents={threadAuditEvents} context={{ employees, agents }} onClose={() => setOpenThreadId(null)} onSubmit={sendInThread} diff --git a/AgentCompany-New/src/renderer/src/pages/chat/ChatMessageRow.tsx b/AgentCompany-New/src/renderer/src/pages/chat/ChatMessageRow.tsx index 7cbded99..36879eeb 100644 --- a/AgentCompany-New/src/renderer/src/pages/chat/ChatMessageRow.tsx +++ b/AgentCompany-New/src/renderer/src/pages/chat/ChatMessageRow.tsx @@ -57,9 +57,10 @@ export function ChatMessageRow({ {messageFiles.map((file) => )} )} - {replyCount > 0 && onOpenThread && ( + {onOpenThread && ( )} diff --git a/AgentCompany-New/src/renderer/src/pages/chat/ChatThreadPanel.tsx b/AgentCompany-New/src/renderer/src/pages/chat/ChatThreadPanel.tsx index 382c6a28..7be5a3c7 100644 --- a/AgentCompany-New/src/renderer/src/pages/chat/ChatThreadPanel.tsx +++ b/AgentCompany-New/src/renderer/src/pages/chat/ChatThreadPanel.tsx @@ -1,6 +1,7 @@ import type { FormEvent } from "react"; import { Button, Icon, IconButton } from "../../shared/ui"; import type { + AuditEventRecord, ChatAttachmentRecord, ChatMentionRecord, ChatMessageRecord, @@ -18,6 +19,7 @@ export function ChatThreadPanel({ mentions, slashInvocations, files, + auditEvents, context, onClose, onSubmit, @@ -28,6 +30,7 @@ export function ChatThreadPanel({ mentions: ChatMentionRecord[]; slashInvocations: ChatSlashInvocationRecord[]; files: FileObjectRecord[]; + auditEvents: AuditEventRecord[]; context: MessageContext; onClose: () => void; onSubmit: (event: FormEvent) => void; @@ -85,6 +88,7 @@ export function ChatThreadPanel({ compact /> ))} + {auditEvents.length > 0 && }
@@ -108,3 +112,33 @@ export function ChatThreadPanel({ ); } + +function AuditReplay({ events }: { events: AuditEventRecord[] }) { + return ( +
+
Audit
+
+ {events.map((event) => ( +
+ {formatTime(event.createdAt)} + + {event.entityType}.{event.action} + +
+ ))} +
+
+ ); +} diff --git a/AgentCompany-New/src/renderer/src/pages/chat/chat-helpers.ts b/AgentCompany-New/src/renderer/src/pages/chat/chat-helpers.ts index e5f088d7..77c1c7b1 100644 --- a/AgentCompany-New/src/renderer/src/pages/chat/chat-helpers.ts +++ b/AgentCompany-New/src/renderer/src/pages/chat/chat-helpers.ts @@ -1,6 +1,7 @@ import type { AgentRecord, ApprovalRecord, + AuditEventRecord, ChatAttachmentRecord, ChatMessageRecord, ChatSpaceRecord, @@ -80,6 +81,33 @@ export function countMessagesIn(spaceId: string, messages: ChatMessageRecord[]): return messages.filter((message) => message.spaceId === spaceId).length; } +export function replayEventsForThread( + events: AuditEventRecord[], + thread: ChatThreadRecord, + messages: ChatMessageRecord[], +): AuditEventRecord[] { + const messageIds = new Set( + messages + .filter((message) => message.threadId === thread.id || message.id === thread.rootMessageId) + .map((message) => message.id), + ); + return events + .filter((event) => { + if (event.entityType === "chat_message" && messageIds.has(event.entityId)) return true; + const payload = auditPayload(event); + return payload.threadId === thread.id || payload.sourceThreadId === thread.id; + }) + .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); +} + +function auditPayload(event: AuditEventRecord): Record { + const parsed = JSON.parse(event.payloadJson) as unknown; + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error("Audit event payload is invalid."); + } + return parsed as Record; +} + export function formatDay(iso: string): string { const date = new Date(iso); if (Number.isNaN(date.getTime())) return "—"; diff --git a/AgentCompany-New/tests/e2e/desktop.spec.ts b/AgentCompany-New/tests/e2e/desktop.spec.ts index 2a235872..96b1ba37 100644 --- a/AgentCompany-New/tests/e2e/desktop.spec.ts +++ b/AgentCompany-New/tests/e2e/desktop.spec.ts @@ -61,6 +61,10 @@ test("onboards, hires, and creates work in the Electron shell", async () => { await page.getByRole("button", { name: "Send" }).click(); await expect(page.getByText("@Board").first()).toBeVisible(); await expect(page.getByText("/escalate").first()).toBeVisible(); + await page.getByRole("button", { name: "Open thread" }).first().click(); + await expect(page.getByText("Audit")).toBeVisible(); + await expect(page.getByText("chat_message.sent").first()).toBeVisible(); + await expect(page.getByText("approval.requested").first()).toBeVisible(); await page.getByRole("button", { name: "Approvals" }).click(); await expect(page.getByText("Escalate release").first()).toBeVisible();