From f61046b7a44bf9915731ca1806431d80727c516d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 6 Jul 2026 21:39:03 -0400 Subject: [PATCH] Restore thread panel wiring lost in the flat-timeline refactor The flat-messages refactor left ChatThreadPanel orphaned: ChatPage passed a no-op onOpenThread and ChatTimeline hardcoded onOpenThread={null}, so threads (including /escalate board flows) were unreachable from the UI and the desktop e2e failed on main. Reconnect open/close/reply/board-intervention wiring around the flat timeline, show reply counts on thread roots, and clear the lint debt that severed feature left behind. --- AgentCompany-New/eslint.config.mjs | 6 ++ .../src/main/services/chat-service.ts | 2 +- .../src/renderer/src/pages/ChatPage.tsx | 90 ++++++++++++++++++- .../src/pages/chat/ChatConversation.tsx | 3 - .../renderer/src/pages/chat/ChatTimeline.tsx | 10 ++- 5 files changed, 99 insertions(+), 12 deletions(-) diff --git a/AgentCompany-New/eslint.config.mjs b/AgentCompany-New/eslint.config.mjs index 524004c1..d353b11d 100644 --- a/AgentCompany-New/eslint.config.mjs +++ b/AgentCompany-New/eslint.config.mjs @@ -24,4 +24,10 @@ export default tseslint.config( "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], }, }, + { + files: ["scripts/**/*.cjs"], + rules: { + "@typescript-eslint/no-require-imports": "off", + }, + }, ); diff --git a/AgentCompany-New/src/main/services/chat-service.ts b/AgentCompany-New/src/main/services/chat-service.ts index d10aba17..1aed299b 100644 --- a/AgentCompany-New/src/main/services/chat-service.ts +++ b/AgentCompany-New/src/main/services/chat-service.ts @@ -219,7 +219,7 @@ export class ChatService { this.db.prepare( "update chat_mentions set wake_state = 'woken' where message_id = ? and target_type = 'agent' and target_id = ?", ).run(messageId, mention.target.id); - } catch (error) { + } catch { this.db.prepare( "update chat_mentions set wake_state = 'failed' where message_id = ? and target_type = 'agent' and target_id = ?", ).run(messageId, mention.target.id); diff --git a/AgentCompany-New/src/renderer/src/pages/ChatPage.tsx b/AgentCompany-New/src/renderer/src/pages/ChatPage.tsx index aadacfd5..5c88bc31 100644 --- a/AgentCompany-New/src/renderer/src/pages/ChatPage.tsx +++ b/AgentCompany-New/src/renderer/src/pages/ChatPage.tsx @@ -3,10 +3,10 @@ import { Field } from "../shared/ui"; import { Dialog } from "../shared/Dialog"; import { ChatSidebar } from "./chat/ChatSidebar"; import { ChatConversation } from "./chat/ChatConversation"; +import { ChatThreadPanel } from "./chat/ChatThreadPanel"; import { collectParticipants, filterApprovalsForSpace, - isRootMessage, } from "./chat/chat-helpers"; import { ROLE_KEY } from "../../../shared/domain"; import type { PageProps } from "./types"; @@ -60,8 +60,10 @@ export function ChatPage({ snapshot, actions }: PageProps) { ); const [selectedSpaceId, setSelectedSpaceId] = useState(null); + const [openThreadId, setOpenThreadId] = useState(null); const [createOpen, setCreateOpen] = useState(false); const [attachmentFileId, setAttachmentFileId] = useState(""); + const [threadAttachmentFileId, setThreadAttachmentFileId] = useState(""); const composerRef = useRef(null); const firstAvailableSpaceId = channels[0]?.id ?? directs[0]?.id ?? null; @@ -93,13 +95,70 @@ export function ChatPage({ snapshot, actions }: PageProps) { () => (activeSpace ? threadsAll.filter((t) => t.spaceId === activeSpace.id) : []), [threadsAll, activeSpace], ); - const rootMessages = messagesInSpace.filter((message) => isRootMessage(message, threadsInSpace)); + const selectedThread = threadsInSpace.find((thread) => thread.id === openThreadId) ?? null; + const threadMessages = selectedThread + ? messagesInSpace.filter((m) => m.threadId === selectedThread.id && m.id !== selectedThread.rootMessageId) + : []; + const selectedBoardInterventions = useMemo( + () => (selectedThread ? boardInterventionsAll.filter((item) => item.threadId === selectedThread.id) : []), + [boardInterventionsAll, selectedThread], + ); const participants = useMemo( () => collectParticipants(messagesInSpace, employees, agents), [messagesInSpace, employees, agents], ); const pendingApprovalsInSpace = filterApprovalsForSpace(approvalsAll, activeSpace); + const openThread = (threadId: string) => { + setOpenThreadId(threadId); + setThreadAttachmentFileId(""); + }; + + const closeThread = () => { + setOpenThreadId(null); + setThreadAttachmentFileId(""); + }; + + const sendInThread = (event: FormEvent) => { + event.preventDefault(); + if (!activeSpace || !selectedThread) return; + const form = new FormData(event.currentTarget); + const body = value(form, "message"); + if (!body) return; + event.currentTarget.reset(); + setThreadAttachmentFileId(""); + void actions.run(() => window.agentCompany.sendMessage({ + orgId, + spaceId: activeSpace.id, + threadId: selectedThread.id, + senderType: "employee", + senderId: employee.id, + bodyMd: body, + attachmentFileIds: threadAttachmentFileId ? [threadAttachmentFileId] : [], + })); + }; + + const sendBoardIntervention = (event: FormEvent) => { + event.preventDefault(); + if (!activeSpace || !selectedThread) return; + const form = new FormData(event.currentTarget); + const action = value(form, "action"); + const target = value(form, "target"); + const details = value(form, "details"); + if (!action || !details) return; + event.currentTarget.reset(); + const targetMention = action === "takeover" && target && target !== "board" ? ` @${target}` : ""; + void actions.run(() => window.agentCompany.sendMessage({ + orgId, + spaceId: activeSpace.id, + threadId: selectedThread.id, + senderType: "board", + senderId: employee.id, + bodyMd: `/board ${action}${targetMention} ${details}`, + attachmentFileIds: [], + })); + }; + const sendInChannel = (event: FormEvent) => { event.preventDefault(); if (!activeSpace) return; @@ -136,6 +195,7 @@ export function ChatPage({ snapshot, actions }: PageProps) { const created = result.chatSpaces.find((space) => space.orgId === orgId && !existingSpaceIds.has(space.id)); if (!created) throw new Error("Created chat space is missing from snapshot."); setSelectedSpaceId(created.id); + closeThread(); setCreateOpen(false); }); }; @@ -193,6 +253,7 @@ export function ChatPage({ snapshot, actions }: PageProps) { activeSpaceId={activeSpaceId} onSelect={(spaceId) => { setSelectedSpaceId(spaceId); + closeThread(); }} onCreateSpace={() => setCreateOpen(true)} /> @@ -200,7 +261,6 @@ export function ChatPage({ snapshot, actions }: PageProps) { {}} + onOpenThread={openThread} onApprove={(approvalId) => decideApproval(approvalId, "approved")} onDecline={(approvalId) => decideApproval(approvalId, "declined")} onEditMessage={editChatMessage} @@ -223,6 +283,28 @@ export function ChatPage({ snapshot, actions }: PageProps) { composerRef={composerRef} /> + {selectedThread && ( + m.id === selectedThread.rootMessageId) ?? null} + messages={threadMessages} + attachments={snapshot.chatAttachments} + mentions={mentionsAll} + slashInvocations={slashInvocationsAll} + boardInterventions={selectedBoardInterventions} + files={files} + context={{ employees, agents }} + canIntervene={canBoardIntervene} + attachmentFileId={threadAttachmentFileId} + onClose={closeThread} + onAttachmentChange={setThreadAttachmentFileId} + onAttach={() => importAttachment(setThreadAttachmentFileId)} + onSubmit={sendInThread} + onBoardSubmit={sendBoardIntervention} + onOpenFile={openFile} + /> + )} + {space && ( ); } + const thread = threadsInSpace.find((item) => item.rootMessageId === message.id) ?? null; + const replyCount = thread + ? messagesInSpace.filter((m) => m.threadId === thread.id && m.id !== thread.rootMessageId).length + : 0; elements.push( onOpenThread(thread.id) : null} context={context} canModify={message.senderType === "employee" && message.senderId === currentEmployeeId && !message.deletedAt} onEdit={(bodyMd) => onEditMessage(message.id, bodyMd)}