From 7a85f73dbbca48228e18a0b541f9c2f93f41fe13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Tue, 30 Jun 2026 22:27:11 -0400 Subject: [PATCH] Show threads as compact rows instead of inline full messages The channel timeline now renders each thread as a single compact row with sender avatar, message preview (truncated to one line), reply count with participant avatars, and last reply time. Clicking a row opens the thread in the right panel where the full conversation lives. This matches Slack's thread-list pattern instead of dumping every message body into the channel view. --- .../renderer/src/pages/chat/ChatTimeline.tsx | 148 ++++++++++++++---- 1 file changed, 115 insertions(+), 33 deletions(-) diff --git a/AgentCompany-New/src/renderer/src/pages/chat/ChatTimeline.tsx b/AgentCompany-New/src/renderer/src/pages/chat/ChatTimeline.tsx index f1801db9..56c5a666 100644 --- a/AgentCompany-New/src/renderer/src/pages/chat/ChatTimeline.tsx +++ b/AgentCompany-New/src/renderer/src/pages/chat/ChatTimeline.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from "react"; -import { Button, Icon, Pill } from "../../shared/ui"; +import { Avatar, Button, Icon, Pill } from "../../shared/ui"; import type { ApprovalRecord, ChatAttachmentRecord, @@ -9,8 +9,7 @@ import type { ChatThreadRecord, FileObjectRecord, } from "../../../../shared/domain"; -import { ChatMessageRow } from "./ChatMessageRow"; -import { formatDay, type MessageContext } from "./chat-helpers"; +import { formatDay, senderName, type MessageContext } from "./chat-helpers"; export function ChatTimeline({ rootMessages, @@ -49,35 +48,32 @@ export function ChatTimeline({ }) { const elements: ReactNode[] = []; let lastDay = ""; - rootMessages.forEach((message) => { + for (const message of rootMessages) { const day = formatDay(message.createdAt); if (day !== lastDay) { lastDay = day; elements.push(); } const thread = threadsInSpace.find((t) => t.rootMessageId === message.id) ?? null; - const replyCount = thread - ? messagesInSpace.filter((m) => m.threadId === thread.id && m.id !== thread.rootMessageId).length - : 0; + const replies = thread + ? messagesInSpace.filter((m) => m.threadId === thread.id && m.id !== thread.rootMessageId) + : []; + const lastReply = replies.length > 0 ? replies[replies.length - 1] : null; + const participantNames = threadParticipants(replies, context); elements.push( - onOpenThread(thread.id) : null} + thread={thread} + replyCount={replies.length} + lastReply={lastReply} + participantNames={participantNames} context={context} - canModify={message.senderType === "employee" && message.senderId === currentEmployeeId && !message.deletedAt} - onEdit={(bodyMd) => onEditMessage(message.id, bodyMd)} - onDelete={() => onDeleteMessage(message.id)} - onOpenFile={onOpenFile} + onOpen={thread ? () => onOpenThread(thread.id) : null} />, ); - }); - pendingApprovals.forEach((approval) => { + } + for (const approval of pendingApprovals) { elements.push( onDecline(approval.id)} />, ); - }); + } return <>{elements}; } +function ThreadRow({ + message, + thread, + replyCount, + lastReply, + participantNames, + context, + onOpen, +}: { + message: ChatMessageRecord; + thread: ChatThreadRecord | null; + replyCount: number; + lastReply: ChatMessageRecord | null; + participantNames: string[]; + context: MessageContext; + onOpen: (() => void) | null; +}) { + const name = senderName(message, context); + const bot = message.senderType === "agent"; + const preview = message.bodyMd.replace(/\n+/g, " ").slice(0, 120); + const time = formatTime(message.createdAt); + return ( + + ); +} + +function threadParticipants(replies: ChatMessageRecord[], context: MessageContext): string[] { + const seen = new Set(); + const names: string[] = []; + for (const r of replies) { + const key = `${r.senderType}:${r.senderId}`; + if (seen.has(key)) continue; + seen.add(key); + names.push(senderName(r, context)); + } + return names; +} + +function formatTime(iso: string): string { + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return ""; + return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", hour12: false }); +} + function DayDivider({ label }: { label: string }) { return ( -
+
{label}
@@ -110,19 +201,10 @@ function ApprovalInline({ onDecline: () => void; }) { return ( -
+
- Approval needed + Approval needed {approval.risk}
{approval.title}