Skip to content

Commit 1c8321c

Browse files
tellahoRizzCarl
authored
fix(desktop): retain automatic mentions only in threads (#7144)
**Category:** fix **User Impact:** Top-level channel messages now notify only agents explicitly selected for that message, while thread replies visibly retain their addressed agents. **Problem:** Channel-root and thread composers presented the same automatic-mention model even though retained recipients are only predictable within an ongoing thread. That could make a new top-level message notify an agent the sender did not deliberately choose for that message. **Solution:** Make retained audiences a thread-only capability. Root messages remain explicit and one-shot; threads retain visible, removable agent recipients, with the automatic-mention setting exposed directly in the mention picker. <details> <summary>File changes</summary> **desktop/src/features/channels/ui/ChannelPane.tsx** Removes persistent audience state from the channel-root composer. **desktop/src/features/messages/ui/ComposerAddressControls.tsx** Uses the broader **Manage mentions** label because the picker includes people as well as automatic agent controls. **desktop/src/features/messages/ui/ComposerAddressControls.test.mjs** Locks the updated accessible label and active treatment. **desktop/src/features/messages/ui/MentionAutocomplete.tsx** Shows the right-aligned automatic-mention setting directly, uses thread-specific copy, preserves keyboard/focus behavior, and keeps the current mention when retention is unchecked. **desktop/src/features/messages/ui/MentionAutocomplete.test.mjs** Covers the always-visible setting, compact layout, copy, and thread-scoped agent actions. **desktop/src/features/messages/ui/MessageComposer.tsx** Separates unpinning an agent for future replies from removing its current draft mention. **desktop/src/features/messages/ui/MessageComposer.types.ts** Narrows retained audience contexts to threads. **desktop/src/features/messages/ui/persistentAgentAudienceHosts.test.mjs** Prevents channel-root and new-message hosts from opting back into retained audiences. **desktop/src/features/messages/ui/useAgentAddressLockPicker.ts** Splits unpin and current-mention removal semantics. **desktop/src/features/messages/ui/useAgentAddressLockPicker.test.mjs** Verifies unpinning retains the current draft mention. **desktop/src/features/settings/ui/AgentsSettingsPanel.tsx** Describes the preference as addressing selected agents in thread replies. **desktop/tests/e2e/persistent-agent-audience.spec.ts** Moves retained-audience lifecycle coverage to thread composers and adds root, settings, layout, focus, keyboard, unpin, and draft regressions. **desktop/src/features/messages/ui/MessageComposerAutocompletes.tsx** Preserves composer focus ownership while routing the thread-only controls. **desktop/src/features/messages/ui/useComposerFocusOwnership.ts** Keeps focus within the composer while interacting with its mention overlay controls. </details> ### Reproduction steps 1. Enable **Automatically mention agents** under agent settings. 2. In a channel root, select an agent and send a message. Confirm the agent is addressed once, no retained-recipient control appears, and the next root message has no agent recipient. 3. Open a thread and select an agent. Confirm the visible recipient persists into later replies. 4. Open **Manage mentions** in the thread composer. Confirm the automatic-mention setting is immediately visible, right-aligned, and labeled **Address selected agents in thread replies**. 5. Uncheck a selected agent. Confirm its current draft mention remains, while later replies no longer retain it automatically. --------- Signed-off-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
1 parent 560fea7 commit 1c8321c

19 files changed

Lines changed: 509 additions & 806 deletions

desktop/src/features/channels/ui/ChannelPane.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -754,7 +754,6 @@ export const ChannelPane = React.memo(function ChannelPane({
754754
) : null}
755755
<ComposerDockBackdrop gutterClassName="inset-x-5" />
756756
<MessageComposer
757-
audienceContext={{ type: "channel" }}
758757
channelId={activeChannel?.id ?? null}
759758
channelName={activeChannel?.name ?? "channel"}
760759
channelType={activeChannel?.channelType ?? null}

desktop/src/features/home/ui/InboxDetailPane.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,10 @@ function InboxMessageDetailPane({
495495
: null;
496496
const isThreadContext =
497497
!isDirectMessage && hasInboxThreadContext(item, messages);
498+
const threadRootTags = isThreadContext
499+
? (displayMessages.find((message) => message.id === item.conversationId)
500+
?.tags ?? [])
501+
: [];
498502
const contextLabel = isThreadContext
499503
? isDirectMessage
500504
? `Thread with ${item.senderLabel}`
@@ -804,7 +808,14 @@ function InboxMessageDetailPane({
804808
/>
805809
<div className="pointer-events-auto">
806810
<MessageComposer
807-
audienceContext={isDirectMessage ? null : { type: "thread" }}
811+
audienceContext={
812+
isDirectMessage
813+
? null
814+
: {
815+
type: "thread",
816+
rootTags: threadRootTags,
817+
}
818+
}
808819
channelId={item.item.channelId}
809820
channelName={item.channelLabel ?? "channel"}
810821
channelType={composerChannelType}

desktop/src/features/messages/lib/persistentAgentAudience.test.mjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,21 @@ test("explicitly excluded agents are not auto-promoted again", async () => {
219219
assert.deepEqual(currentAudiences(store), { [scope]: [agentA] });
220220
});
221221

222+
test("initialization preserves exclusions across thread remounts", async () => {
223+
const store = await loadStore(14);
224+
const scope = `${ownerA}:channel-a:thread-a`;
225+
226+
store.initializePersistentAgentAudience(scope, [agentA]);
227+
assert.deepEqual(currentAudiences(store), { [scope]: [agentA] });
228+
229+
store.excludePersistentAgentAudienceMember(scope, agentA);
230+
store.initializePersistentAgentAudience(scope, [agentA]);
231+
assert.deepEqual(currentAudiences(store), { [scope]: [] });
232+
233+
store.addPersistentAgentAudienceMember(scope, agentA);
234+
assert.deepEqual(currentAudiences(store), { [scope]: [agentA] });
235+
});
236+
222237
test("explicit re-selection reinstates an excluded agent", async () => {
223238
const store = await loadStore(13);
224239
const scope = `${ownerA}:channel-a:channel`;

desktop/src/features/messages/lib/persistentAgentAudience.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,23 @@ export function removePersistentAgentAudienceMembersIfUnchanged({
160160
return true;
161161
}
162162

163+
export function initializePersistentAgentAudience(
164+
scope: string,
165+
pubkeys: Iterable<string>,
166+
): void {
167+
if (!scope) return;
168+
const excluded = excludedPubkeysByScope.get(scope);
169+
const initialPubkeys = normalizePubkeys(pubkeys).filter(
170+
(pubkey) =>
171+
!(audiences[scope] ?? []).includes(pubkey) && !excluded?.has(pubkey),
172+
);
173+
if (initialPubkeys.length === 0) return;
174+
setPersistentAgentAudience(scope, [
175+
...(audiences[scope] ?? []),
176+
...initialPubkeys,
177+
]);
178+
}
179+
163180
export function addPersistentAgentAudienceMember(
164181
scope: string,
165182
pubkey: string,

desktop/src/features/messages/ui/ComposerAddressControls.test.mjs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ test("mention control expands with automatically mentioned agents", async () =>
7272
const avatar = view.getByTestId("composer-address-lock-agent-pubkey");
7373
assert.ok(avatar);
7474
const manage = view.getByRole("button", {
75-
name: "Manage automatic agent mentions",
75+
name: "Manage mentions",
7676
});
7777
assert.match(manage.className, /(?:^|\s)-ml-2(?:\s|$)/);
7878
assert.match(manage.className, /(?:^|\s)pl-2(?:\s|$)/);
@@ -82,18 +82,18 @@ test("mention control expands with automatically mentioned agents", async () =>
8282
/(?:^|\s)pr-1\.5(?:\s|$)/,
8383
);
8484
assert.match(
85-
view.getByRole("button", { name: "Manage automatic agent mentions" })
86-
.parentElement?.className ?? "",
85+
view.getByRole("button", { name: "Manage mentions" }).parentElement
86+
?.className ?? "",
8787
/(?:^|\s)bg-primary\/15(?:\s|$)/,
8888
);
8989
assert.match(
90-
view.getByRole("button", { name: "Manage automatic agent mentions" })
91-
.parentElement?.className ?? "",
90+
view.getByRole("button", { name: "Manage mentions" }).parentElement
91+
?.className ?? "",
9292
/(?:^|\s)text-primary(?:\s|$)/,
9393
);
9494
assert.doesNotMatch(
95-
view.getByRole("button", { name: "Manage automatic agent mentions" })
96-
.parentElement?.className ?? "",
95+
view.getByRole("button", { name: "Manage mentions" }).parentElement
96+
?.className ?? "",
9797
/(?:^|\s)bg-accent\/70(?:\s|$)/,
9898
);
9999
assert.doesNotMatch(
@@ -110,7 +110,13 @@ test("mention control expands with automatically mentioned agents", async () =>
110110
/scale\(0.8\)/,
111111
);
112112
}
113-
const remove = view.getByTestId("composer-address-lock-remove-agent-pubkey");
113+
const remove = view.getByRole("button", {
114+
name: "Don't automatically mention Agent Ada in this thread",
115+
});
116+
assert.equal(
117+
remove.getAttribute("aria-label")?.includes("conversation"),
118+
false,
119+
);
114120
const removeChrome = remove.querySelector("span.absolute");
115121
assert.match(
116122
removeChrome?.className ?? "",

desktop/src/features/messages/ui/ComposerAddressControls.tsx

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,7 @@ export function ComposerMentionButton({
180180
<Tooltip disableHoverableContent>
181181
<TooltipTrigger asChild>
182182
<button
183-
aria-label={
184-
hasAgents
185-
? "Manage automatic agent mentions"
186-
: "Mention someone"
187-
}
183+
aria-label={hasAgents ? "Manage mentions" : "Mention someone"}
188184
className={cn(
189185
"flex h-8 items-center justify-center rounded-lg focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
190186
showActiveChrome
@@ -205,9 +201,7 @@ export function ComposerMentionButton({
205201
</button>
206202
</TooltipTrigger>
207203
<TooltipContent>
208-
{hasAgents
209-
? "Manage automatic agent mentions"
210-
: "Mention someone"}
204+
{hasAgents ? "Manage mentions" : "Mention someone"}
211205
</TooltipContent>
212206
</Tooltip>
213207
<AnimatePresence
@@ -234,7 +228,7 @@ export function ComposerMentionButton({
234228
<Tooltip disableHoverableContent key={agent.pubkey}>
235229
<TooltipTrigger asChild>
236230
<motion.button
237-
aria-label={`Don't automatically mention ${agent.displayName} in this conversation`}
231+
aria-label={`Don't automatically mention ${agent.displayName} in this thread`}
238232
animate={{ opacity: 1, scale: 1 }}
239233
className="group/address relative rounded-full focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring"
240234
data-testid={`composer-address-lock-remove-${agent.pubkey}`}
@@ -276,7 +270,7 @@ export function ComposerMentionButton({
276270
</TooltipTrigger>
277271
<TooltipContent>
278272
Don't automatically mention {agent.displayName} in this
279-
conversation
273+
thread
280274
</TooltipContent>
281275
</Tooltip>
282276
))}

0 commit comments

Comments
 (0)