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.
This commit is contained in:
1 parent
06fa8fc07f
commit
f61046b7a4
5 files changed
+99
-12
No files matched your search
@@ -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",
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -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);
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [openThreadId, setOpenThreadId] = useState<string | null>(null);
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [attachmentFileId, setAttachmentFileId] = useState<string>("");
|
||||
const [threadAttachmentFileId, setThreadAttachmentFileId] = useState<string>("");
|
||||
const composerRef = useRef<HTMLTextAreaElement | null>(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<HTMLFormElement>) => {
|
||||
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<HTMLFormElement>) => {
|
||||
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<HTMLFormElement>) => {
|
||||
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) {
|
||||
<ChatConversation
|
||||
space={activeSpace}
|
||||
participants={participants}
|
||||
rootMessages={rootMessages}
|
||||
messagesInSpace={messagesInSpace}
|
||||
threadsInSpace={threadsInSpace}
|
||||
pendingApprovals={pendingApprovalsInSpace}
|
||||
@@ -213,7 +273,7 @@ export function ChatPage({ snapshot, actions }: PageProps) {
|
||||
currentEmployeeId={employee.id}
|
||||
onAttachmentChange={setAttachmentFileId}
|
||||
onAttach={attachFile}
|
||||
onOpenThread={() => {}}
|
||||
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 && (
|
||||
<ChatThreadPanel
|
||||
thread={selectedThread}
|
||||
rootMessage={messagesAll.find((m) => 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}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Dialog
|
||||
title="New space"
|
||||
open={createOpen}
|
||||
|
||||
@@ -17,7 +17,6 @@ import type { MessageContext } from "./chat-helpers";
|
||||
export function ChatConversation({
|
||||
space,
|
||||
participants,
|
||||
rootMessages,
|
||||
messagesInSpace,
|
||||
threadsInSpace,
|
||||
pendingApprovals,
|
||||
@@ -41,7 +40,6 @@ export function ChatConversation({
|
||||
}: {
|
||||
space: ChatSpaceRecord | null;
|
||||
participants: string[];
|
||||
rootMessages: ChatMessageRecord[];
|
||||
messagesInSpace: ChatMessageRecord[];
|
||||
threadsInSpace: ChatThreadRecord[];
|
||||
pendingApprovals: ApprovalRecord[];
|
||||
@@ -74,7 +72,6 @@ export function ChatConversation({
|
||||
<div style={{ flex: 1, overflow: "auto", padding: "20px 24px 8px" }}>
|
||||
{space && (
|
||||
<ChatTimeline
|
||||
rootMessages={[]}
|
||||
messagesInSpace={filtered}
|
||||
threadsInSpace={threadsInSpace}
|
||||
pendingApprovals={pendingApprovals}
|
||||
|
||||
@@ -13,7 +13,6 @@ import { ChatMessageRow } from "./ChatMessageRow";
|
||||
import { formatDay, type MessageContext } from "./chat-helpers";
|
||||
|
||||
export function ChatTimeline({
|
||||
rootMessages,
|
||||
messagesInSpace,
|
||||
threadsInSpace,
|
||||
pendingApprovals,
|
||||
@@ -30,7 +29,6 @@ export function ChatTimeline({
|
||||
onDeleteMessage,
|
||||
onOpenFile,
|
||||
}: {
|
||||
rootMessages: ChatMessageRecord[];
|
||||
messagesInSpace: ChatMessageRecord[];
|
||||
threadsInSpace: ChatThreadRecord[];
|
||||
pendingApprovals: ApprovalRecord[];
|
||||
@@ -55,6 +53,10 @@ export function ChatTimeline({
|
||||
lastDay = day;
|
||||
elements.push(<DayDivider key={`day-${day}`} label={day} />);
|
||||
}
|
||||
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(
|
||||
<ChatMessageRow
|
||||
key={message.id}
|
||||
@@ -63,8 +65,8 @@ export function ChatTimeline({
|
||||
mentions={mentions}
|
||||
slashInvocations={slashInvocations}
|
||||
files={files}
|
||||
replyCount={0}
|
||||
onOpenThread={null}
|
||||
replyCount={replyCount}
|
||||
onOpenThread={thread ? () => onOpenThread(thread.id) : null}
|
||||
context={context}
|
||||
canModify={message.senderType === "employee" && message.senderId === currentEmployeeId && !message.deletedAt}
|
||||
onEdit={(bodyMd) => onEditMessage(message.id, bodyMd)}
|
||||
|
||||
Reference in new issue
Block a user