Preserve agent p-tags on focused sends

This commit is contained in:
klopez4212
2026-06-30 13:56:39 +01:00
parent f915433df3
commit a82c36dab2
3 changed files with 150 additions and 3 deletions
@@ -4,6 +4,7 @@ import test from "node:test";
import {
canOpenAgentConversationInChannel,
getDmTaskAgentPubkeys,
getThreadTaskAgentPubkeys,
mergeAutoRouteMentionPubkeys,
} from "./ChannelPane.helpers.ts";
@@ -114,3 +115,61 @@ test("DM task agent inference requires exactly one other known agent", () => {
[],
);
});
test("thread task agent inference requires exactly one known agent and one human", () => {
const knownAgentPubkeys = new Set(["agent-one", "agent-two"]);
assert.deepEqual(
getThreadTaskAgentPubkeys({
currentPubkey: "human",
knownAgentPubkeys,
messages: [
{
pubkey: "human",
tags: [["p", "agent-one"]],
},
{
pubkey: "agent-one",
tags: [["p", "human"]],
},
],
}),
["agent-one"],
);
assert.deepEqual(
getThreadTaskAgentPubkeys({
currentPubkey: "human",
knownAgentPubkeys,
messages: [
{
pubkey: "human",
tags: [["p", "agent-one"]],
},
{
pubkey: "other-human",
tags: [["p", "human"]],
},
],
}),
[],
);
assert.deepEqual(
getThreadTaskAgentPubkeys({
currentPubkey: "human",
knownAgentPubkeys,
messages: [
{
pubkey: "human",
tags: [["p", "agent-one"]],
},
{
pubkey: "agent-two",
tags: [["p", "human"]],
},
],
}),
[],
);
});
@@ -3,6 +3,7 @@ import type { TimelineMessage } from "@/features/messages/types";
import type { Channel } from "@/shared/api/types";
import { KIND_SYSTEM_MESSAGE } from "@/shared/constants/kinds";
import { normalizePubkey } from "@/shared/lib/pubkey";
import { getMentionTagPubkey } from "@/shared/lib/resolveMentionNames";
export function getChannelIntroKind(channel: Channel): string {
const isPrivate = channel.visibility === "private";
@@ -120,6 +121,53 @@ export function getDmTaskAgentPubkeys({
);
}
export function getThreadTaskAgentPubkeys({
currentPubkey,
knownAgentPubkeys,
messages,
}: {
currentPubkey?: string;
knownAgentPubkeys: ReadonlySet<string>;
messages: readonly TimelineMessage[];
}) {
const normalizedCurrentPubkey = currentPubkey
? normalizePubkey(currentPubkey)
: null;
const participants = new Map<string, string>();
const addParticipant = (pubkey: string | null | undefined) => {
if (!pubkey) {
return;
}
const normalized = normalizePubkey(pubkey);
if (!normalized || participants.has(normalized)) {
return;
}
participants.set(normalized, pubkey);
};
for (const message of messages) {
addParticipant(message.pubkey);
for (const tag of message.tags ?? []) {
addParticipant(getMentionTagPubkey(tag));
}
}
const agentPubkeys = new Map<string, string>();
for (const [normalized, pubkey] of participants) {
if (normalizedCurrentPubkey && normalized === normalizedCurrentPubkey) {
continue;
}
if (!knownAgentPubkeys.has(normalized)) {
return [];
}
agentPubkeys.set(normalized, pubkey);
}
return agentPubkeys.size === 1 ? [...agentPubkeys.values()] : [];
}
export function mergeAutoRouteMentionPubkeys({
autoRouteAgentPubkeys,
mentionPubkeys,
@@ -148,3 +196,20 @@ export function mergeAutoRouteMentionPubkeys({
return merged;
}
export function mergeTaskAgentMentionPubkeys({
agentPubkeys,
mentionPubkeys,
}: {
agentPubkeys: readonly string[];
mentionPubkeys: string[];
}) {
if (agentPubkeys.length === 0) {
return mentionPubkeys;
}
return mergeAutoRouteMentionPubkeys({
autoRouteAgentPubkeys: agentPubkeys,
mentionPubkeys,
});
}
@@ -44,7 +44,9 @@ import {
getChannelIntroDescription,
getChannelIntroKind,
getDmTaskAgentPubkeys,
getThreadTaskAgentPubkeys,
isWelcomeSetupSystemMessage,
mergeTaskAgentMentionPubkeys,
mentionsKnownAgent,
} from "@/features/channels/ui/ChannelPane.helpers";
import type { ChannelPaneProps } from "@/features/channels/ui/ChannelPane.types";
@@ -421,13 +423,17 @@ export const ChannelPane = React.memo(function ChannelPane({
mentionPubkeys: string[],
mediaTags?: string[][],
) => {
const sendMentionPubkeys = mergeTaskAgentMentionPubkeys({
agentPubkeys: dmTaskAgentPubkeys,
mentionPubkeys,
});
const shouldCompleteWelcomeBanner =
isActiveWelcomeChannel &&
(containsWelcomePersonaMention(content) ||
mentionsKnownAgent(mentionPubkeys, knownAgentPubkeys));
mentionsKnownAgent(sendMentionPubkeys, knownAgentPubkeys));
messageTimelineRef.current?.scrollToBottomOnNextUpdate();
await onSendMessage(content, mentionPubkeys, mediaTags);
await onSendMessage(content, sendMentionPubkeys, mediaTags);
if (shouldCompleteWelcomeBanner) {
completeWelcomeComposerBanner();
@@ -435,6 +441,7 @@ export const ChannelPane = React.memo(function ChannelPane({
},
[
completeWelcomeComposerBanner,
dmTaskAgentPubkeys,
isActiveWelcomeChannel,
knownAgentPubkeys,
onSendMessage,
@@ -764,6 +771,22 @@ export const ChannelPane = React.memo(function ChannelPane({
...threadMessages.map((entry) => entry.message),
];
}, [threadHeadMessage, threadMessages]);
const threadTaskAgentPubkeys = getThreadTaskAgentPubkeys({
currentPubkey,
knownAgentPubkeys,
messages: threadSourceMessages,
});
const handleSendThreadReply = React.useCallback(
(content: string, mentionPubkeys: string[], mediaTags?: string[][]) => {
const sendMentionPubkeys = mergeTaskAgentMentionPubkeys({
agentPubkeys: threadTaskAgentPubkeys,
mentionPubkeys,
});
return onSendThreadReply(content, sendMentionPubkeys, mediaTags);
},
[onSendThreadReply, threadTaskAgentPubkeys],
);
const hiddenAgentConversationMessageIds = React.useMemo(() => {
if (!enableAgentConversations) {
return new Set<string>();
@@ -1194,7 +1217,7 @@ export const ChannelPane = React.memo(function ChannelPane({
: undefined
}
onSelectReplyTarget={onSelectThreadReplyTarget}
onSend={onSendThreadReply}
onSend={handleSendThreadReply}
onScrollTargetResolved={onThreadScrollTargetResolved}
onToggleReaction={onToggleReaction}
onUnfollowThread={onUnfollowThread}