From 016ae2590191c883961c5cd2c409607de8f54414 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:49:41 -0400 Subject: [PATCH] Open threads in-place in center pane and fix office crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chat: clicking a thread row now replaces the center pane content with the thread's full messages (back button to return to thread list). Removed the right-side thread panel entirely — threads navigate in place like Slack, not as a side panel overlay. Office: reduced frame rate to 24fps, skip rendering when canvas has zero dimensions, schedule next frame AFTER draw (not before) to prevent runaway frame buildup that caused the white screen crash. --- .../src/renderer/src/pages/ChatPage.tsx | 33 ++--- .../src/renderer/src/pages/OfficePage.tsx | 19 +-- .../src/pages/chat/ChatConversation.tsx | 119 ++++++++++++------ 3 files changed, 102 insertions(+), 69 deletions(-) diff --git a/AgentCompany-New/src/renderer/src/pages/ChatPage.tsx b/AgentCompany-New/src/renderer/src/pages/ChatPage.tsx index 95090736..24e3d308 100644 --- a/AgentCompany-New/src/renderer/src/pages/ChatPage.tsx +++ b/AgentCompany-New/src/renderer/src/pages/ChatPage.tsx @@ -276,42 +276,23 @@ export function ChatPage({ snapshot, actions }: PageProps) { slashInvocations={slashInvocationsAll} files={files} context={{ employees, agents }} - attachmentFileId={attachmentFileId} + attachmentFileId={selectedThread ? threadAttachmentFileId : attachmentFileId} currentEmployeeId={employee.id} - onAttachmentChange={setAttachmentFileId} - onAttach={attachFile} + onAttachmentChange={selectedThread ? setThreadAttachmentFileId : setAttachmentFileId} + onAttach={selectedThread ? attachThreadFile : attachFile} onOpenThread={openThread} onApprove={(approvalId) => decideApproval(approvalId, "approved")} onDecline={(approvalId) => decideApproval(approvalId, "declined")} onEditMessage={editChatMessage} onDeleteMessage={deleteChatMessage} onOpenFile={openFile} - onSubmit={sendInChannel} + onSubmit={selectedThread ? sendInThread : sendInChannel} composerRef={composerRef} + activeThread={selectedThread} + threadMessages={threadMessages} + onCloseThread={closeThread} /> - {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={attachThreadFile} - onSubmit={sendInThread} - onBoardSubmit={sendBoardIntervention} - onOpenFile={openFile} - /> - )} - { if (!running) return; - animRef.current = requestAnimationFrame(tick); const elapsed = now - lastFrameRef.current; - if (elapsed < FPS_INTERVAL) return; - lastFrameRef.current = now - (elapsed % FPS_INTERVAL); + if (elapsed < FPS_INTERVAL) { + animRef.current = requestAnimationFrame(tick); + return; + } + lastFrameRef.current = now; const dpr = window.devicePixelRatio || 1; const w = canvas.clientWidth; const h = canvas.clientHeight; - if (canvas.width !== w * dpr || canvas.height !== h * dpr) { - canvas.width = w * dpr; - canvas.height = h * dpr; - } + if (w === 0 || h === 0) { animRef.current = requestAnimationFrame(tick); return; } + const pw = Math.round(w * dpr); + const ph = Math.round(h * dpr); + if (canvas.width !== pw || canvas.height !== ph) { canvas.width = pw; canvas.height = ph; } ctx.setTransform(dpr, 0, 0, dpr, 0, 0); for (const agent of agentsRef.current) { @@ -90,6 +92,7 @@ export function OfficePage({ snapshot }: PageProps) { } drawScene(ctx, w, h, agentsRef.current, now); + animRef.current = requestAnimationFrame(tick); }; animRef.current = requestAnimationFrame(tick); diff --git a/AgentCompany-New/src/renderer/src/pages/chat/ChatConversation.tsx b/AgentCompany-New/src/renderer/src/pages/chat/ChatConversation.tsx index 62398dff..f231fe32 100644 --- a/AgentCompany-New/src/renderer/src/pages/chat/ChatConversation.tsx +++ b/AgentCompany-New/src/renderer/src/pages/chat/ChatConversation.tsx @@ -12,6 +12,7 @@ import type { } from "../../../../shared/domain"; import { ChatComposer } from "./ChatComposer"; import { ChatTimeline } from "./ChatTimeline"; +import { ChatMessageRow } from "./ChatMessageRow"; import type { MessageContext } from "./chat-helpers"; export function ChatConversation({ @@ -38,6 +39,9 @@ export function ChatConversation({ onOpenFile, onSubmit, composerRef, + activeThread, + threadMessages, + onCloseThread, }: { space: ChatSpaceRecord | null; participants: string[]; @@ -62,17 +66,25 @@ export function ChatConversation({ onOpenFile: (fileId: string) => void; onSubmit: (event: FormEvent) => void; composerRef: MutableRefObject; + activeThread: ChatThreadRecord | null; + threadMessages: ChatMessageRecord[]; + onCloseThread: () => void; }) { const [query, setQuery] = useState(""); const trimmed = query.trim().toLowerCase(); const visibleRoots = trimmed ? rootMessages.filter((message) => message.bodyMd.toLowerCase().includes(trimmed)) : rootMessages; + + const showThread = activeThread !== null; + return (
- {space && } + {space && !showThread && } + {space && showThread && } +
- {space && ( + {space && !showThread && ( )} + {space && showThread && ( + m.id === activeThread.rootMessageId) ?? null} + messages={threadMessages} + attachments={attachments} + mentions={mentions} + slashInvocations={slashInvocations} + files={files} + context={context} + currentEmployeeId={currentEmployeeId} + onEditMessage={onEditMessage} + onDeleteMessage={onDeleteMessage} + onOpenFile={onOpenFile} + /> + )}
{ - setSearchOpen((open) => { - if (open) onQueryChange(""); - return !open; - }); + setSearchOpen((open) => { if (open) onQueryChange(""); return !open; }); }; return (
{space.name} - + {space.topic}
{searchOpen && ( - onQueryChange(event.target.value)} - onKeyDown={(event) => { if (event.key === "Escape") toggleSearch(); }} - style={{ width: 200, height: 28, padding: "0 10px" }} - /> + onQueryChange(e.target.value)} + onKeyDown={(e) => { if (e.key === "Escape") toggleSearch(); }} style={{ width: 200, height: 28, padding: "0 10px" }} /> )} {!searchOpen && participants.length > 0 && } @@ -164,3 +164,52 @@ function ConversationHeader({
); } + +function ThreadHeader({ thread, onBack }: { thread: ChatThreadRecord; onBack: () => void }) { + return ( +
+ + + {thread.title} + +
+ ); +} + +function ThreadView({ thread, rootMessage, messages, attachments, mentions, slashInvocations, files, context, currentEmployeeId, onEditMessage, onDeleteMessage, onOpenFile }: { + thread: ChatThreadRecord; + rootMessage: ChatMessageRecord | null; + messages: ChatMessageRecord[]; + attachments: ChatAttachmentRecord[]; + mentions: ChatMentionRecord[]; + slashInvocations: ChatSlashInvocationRecord[]; + files: FileObjectRecord[]; + context: MessageContext; + currentEmployeeId: string; + onEditMessage: (messageId: string, bodyMd: string) => void; + onDeleteMessage: (messageId: string) => void; + onOpenFile: (fileId: string) => void; +}) { + const allMessages = rootMessage ? [rootMessage, ...messages] : messages; + return ( + <> + {allMessages.map((message) => ( + onEditMessage(message.id, bodyMd)} + onDelete={() => onDeleteMessage(message.id)} + onOpenFile={onOpenFile} + /> + ))} + + ); +}