mirror of
https://github.com/block/buzz.git
synced 2026-11-02 23:41:08 +00:00
Fix task view reply and read scoping
This commit is contained in:
@@ -564,3 +564,96 @@ test("dedicated conversation view stops at the next task anchor", () => {
|
||||
|
||||
assert.deepEqual(visibleIds, ["root", "agent-reply", "first-task-reply"]);
|
||||
});
|
||||
|
||||
test("dedicated conversation view keeps older task descendant replies", () => {
|
||||
const root = message({
|
||||
body: "Can you look into the data model?",
|
||||
createdAt: 1,
|
||||
id: "root",
|
||||
});
|
||||
const firstAnchor = message({
|
||||
body: "I'll look into it.",
|
||||
createdAt: 2,
|
||||
id: "agent-reply",
|
||||
pubkey: "agent",
|
||||
});
|
||||
const firstTaskReply = message({
|
||||
body: "This belongs in the first task.",
|
||||
createdAt: 3,
|
||||
id: "first-task-reply",
|
||||
});
|
||||
const secondAnchor = message({
|
||||
body: "Let's split this into another task.",
|
||||
createdAt: 4,
|
||||
id: "second-anchor",
|
||||
pubkey: "agent",
|
||||
});
|
||||
const secondTaskReply = message({
|
||||
body: "This belongs in the second task.",
|
||||
createdAt: 5,
|
||||
id: "second-task-reply",
|
||||
});
|
||||
const firstTaskFollowup = {
|
||||
...message({
|
||||
body: "Continuing the first task after task two started.",
|
||||
createdAt: 6,
|
||||
id: "first-task-followup",
|
||||
}),
|
||||
parentId: "first-task-reply",
|
||||
rootId: "root",
|
||||
};
|
||||
const plainRootReply = message({
|
||||
body: "A plain root reply after task two should not join task one.",
|
||||
createdAt: 7,
|
||||
id: "plain-root-reply",
|
||||
});
|
||||
const messages = [
|
||||
root,
|
||||
firstAnchor,
|
||||
firstTaskReply,
|
||||
secondAnchor,
|
||||
secondTaskReply,
|
||||
firstTaskFollowup,
|
||||
plainRootReply,
|
||||
];
|
||||
const conversation = buildAgentConversation({
|
||||
agentName: "Fizz",
|
||||
agentPubkey: "agent",
|
||||
agentReply: firstAnchor,
|
||||
channel: { id: "channel", name: "general" },
|
||||
contextMessages: messages,
|
||||
parentMessage: root,
|
||||
threadRootMessage: root,
|
||||
});
|
||||
const firstMarker = parseAgentConversationMarker(
|
||||
markerEvent({ content: { startedAt: 2 }, createdAt: 2 }),
|
||||
);
|
||||
const secondMarker = parseAgentConversationMarker({
|
||||
...markerEvent({
|
||||
content: { agentReplyId: "second-anchor", startedAt: 4 },
|
||||
createdAt: 4,
|
||||
id: "second-marker",
|
||||
}),
|
||||
tags: [
|
||||
["h", "channel"],
|
||||
["e", "root", "", "root"],
|
||||
["e", "second-anchor", "", "agent-reply"],
|
||||
["p", "agent"],
|
||||
["title", "Second task"],
|
||||
],
|
||||
});
|
||||
const markers = [firstMarker, secondMarker].filter(Boolean);
|
||||
|
||||
const visibleIds = messages
|
||||
.filter((entry) =>
|
||||
isConversationMessage(entry, conversation, markers, messages),
|
||||
)
|
||||
.map((entry) => entry.id);
|
||||
|
||||
assert.deepEqual(visibleIds, [
|
||||
"root",
|
||||
"agent-reply",
|
||||
"first-task-reply",
|
||||
"first-task-followup",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -93,6 +93,16 @@ export function isConversationMessage(
|
||||
return false;
|
||||
}
|
||||
|
||||
const markerAnchorIds = new Set(
|
||||
markers
|
||||
.filter(
|
||||
(marker) =>
|
||||
marker.channelId === conversation.channelId &&
|
||||
marker.threadRootId === conversation.threadRootId &&
|
||||
marker.agentReplyId !== conversation.agentReply.id,
|
||||
)
|
||||
.map((marker) => marker.agentReplyId),
|
||||
);
|
||||
const orderedThreadMessages =
|
||||
messages.length > 0
|
||||
? messages.filter(
|
||||
@@ -133,7 +143,44 @@ export function isConversationMessage(
|
||||
}
|
||||
}
|
||||
|
||||
return messageIndex < nextAnchorIndex;
|
||||
if (messageIndex < nextAnchorIndex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const selectedTaskMessageIds = new Set<string>();
|
||||
for (const candidate of orderedThreadMessages) {
|
||||
const candidateIndex = messageIndexById.get(candidate.id);
|
||||
if (
|
||||
candidateIndex !== undefined &&
|
||||
candidateIndex >= anchorIndex &&
|
||||
candidateIndex < nextAnchorIndex
|
||||
) {
|
||||
selectedTaskMessageIds.add(candidate.id);
|
||||
}
|
||||
}
|
||||
selectedTaskMessageIds.delete(conversation.threadRootId);
|
||||
|
||||
const messageById = new Map(
|
||||
orderedThreadMessages.map((candidate) => [candidate.id, candidate]),
|
||||
);
|
||||
let parentId = message.parentId;
|
||||
const visited = new Set<string>([message.id]);
|
||||
while (parentId && !visited.has(parentId)) {
|
||||
if (selectedTaskMessageIds.has(parentId)) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
parentId === conversation.threadRootId ||
|
||||
markerAnchorIds.has(parentId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
visited.add(parentId);
|
||||
parentId = messageById.get(parentId)?.parentId ?? null;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentMarker =
|
||||
|
||||
@@ -135,7 +135,6 @@ export function AgentConversationScreen({
|
||||
const {
|
||||
getMessageReadAt,
|
||||
isThreadMuted,
|
||||
markThreadRead,
|
||||
markMessageRead,
|
||||
updateAgentConversationTitle,
|
||||
} = useAppShell();
|
||||
@@ -420,11 +419,6 @@ export function AgentConversationScreen({
|
||||
});
|
||||
}, [conversation, conversationSourceMessages, updateAgentConversationTitle]);
|
||||
React.useEffect(() => {
|
||||
const latestMessage = timelineMessages[timelineMessages.length - 1] ?? null;
|
||||
if (latestMessage) {
|
||||
markThreadRead(conversation.threadRootId, latestMessage.createdAt);
|
||||
}
|
||||
|
||||
if (isThreadMuted(conversation.threadRootId)) {
|
||||
return;
|
||||
}
|
||||
@@ -440,9 +434,19 @@ export function AgentConversationScreen({
|
||||
getMessageReadAt,
|
||||
isThreadMuted,
|
||||
markMessageRead,
|
||||
markThreadRead,
|
||||
timelineMessages,
|
||||
]);
|
||||
const replyParentEventId = React.useMemo(() => {
|
||||
const latestTaskMessage = [...timelineMessages]
|
||||
.reverse()
|
||||
.find((message) => message.id !== conversation.threadRootId);
|
||||
|
||||
return (
|
||||
latestTaskMessage?.id ??
|
||||
conversation.agentReply.id ??
|
||||
conversation.threadRootId
|
||||
);
|
||||
}, [conversation.agentReply.id, conversation.threadRootId, timelineMessages]);
|
||||
const routeableAgentPubkeys = React.useMemo(
|
||||
() =>
|
||||
agentParticipants
|
||||
@@ -539,10 +543,10 @@ export function AgentConversationScreen({
|
||||
autoRouteAgentPubkeys: autoRoutedAgentPubkeys,
|
||||
mentionPubkeys,
|
||||
}),
|
||||
parentEventId: conversation.threadRootId,
|
||||
parentEventId: replyParentEventId,
|
||||
});
|
||||
},
|
||||
[autoRoutedAgentPubkeys, conversation.threadRootId, sendMessageMutation],
|
||||
[autoRoutedAgentPubkeys, replyParentEventId, sendMessageMutation],
|
||||
);
|
||||
|
||||
const isComposerDisabled =
|
||||
@@ -734,6 +738,7 @@ export function AgentConversationScreen({
|
||||
emptyTitle="No conversation messages yet"
|
||||
hasComposerOverlay
|
||||
isLoading={messagesQuery.isLoading && timelineMessages.length === 0}
|
||||
layoutShiftKey={conversation.id}
|
||||
messageListPlacement="top"
|
||||
messages={timelineMessages}
|
||||
profiles={profiles}
|
||||
|
||||
Reference in New Issue
Block a user