diff --git a/desktop/src/features/messages/ui/MessageComposer.tsx b/desktop/src/features/messages/ui/MessageComposer.tsx index b1ae232cb3..a5047da42f 100644 --- a/desktop/src/features/messages/ui/MessageComposer.tsx +++ b/desktop/src/features/messages/ui/MessageComposer.tsx @@ -217,6 +217,7 @@ function MessageComposerImpl({ const syncAddressedAgentsFromTextRef = React.useRef<(text: string) => void>( () => {}, ); + const prepareMentionWakeRef = React.useRef<(text: string) => void>(() => {}); disabledRef.current = disabled; isSendingRef.current = isSending; isUploadingRef.current = media.isUploading; @@ -274,6 +275,7 @@ function MessageComposerImpl({ onUpdate: ({ cursor, linkPreviewContent, text }) => { trackAuthoredContent(text); contentRef.current = text; + prepareMentionWakeRef.current(text); setComposerContentFromText(text); setPreviewContent(linkPreviewContent); if (!isSubmitLockedRef.current && !editTargetRef.current) { @@ -373,6 +375,7 @@ function MessageComposerImpl({ restoreQueuedAttachments: media.restoreQueuedAttachments, setSpoileredAttachmentUrls, }); + prepareMentionWakeRef.current = mentionSendFlow.prepareMentionWake; React.useEffect(() => { onDeferredEditPendingChange?.(isDeferredEditPending); return () => onDeferredEditPendingChange?.(false); diff --git a/desktop/src/features/messages/ui/useMentionSendFlow.ts b/desktop/src/features/messages/ui/useMentionSendFlow.ts index 8b21b26a51..48cbe166db 100644 --- a/desktop/src/features/messages/ui/useMentionSendFlow.ts +++ b/desktop/src/features/messages/ui/useMentionSendFlow.ts @@ -116,9 +116,8 @@ export function useMentionSendFlow({ agents.map((agent) => [normalizePubkey(agent.pubkey), agent]), ); }, [managedAgentsQuery.refetch, queryClient]); - useMentionWakePreflight({ + const { prepareMentionWake } = useMentionWakePreflight({ channelId, - content: contentRef.current, contentRef, enabled: mentionWakeEnabled && @@ -984,6 +983,7 @@ export function useMentionSendFlow({ onInvite: handleInviteNonMembers, open: pendingNonMemberSend !== null, }, + prepareMentionWake, sendMessageWithMentionFlow, }; } diff --git a/desktop/src/features/messages/ui/useMentionWakePreflight.test.mjs b/desktop/src/features/messages/ui/useMentionWakePreflight.test.mjs index 7de2484320..eaa854f448 100644 --- a/desktop/src/features/messages/ui/useMentionWakePreflight.test.mjs +++ b/desktop/src/features/messages/ui/useMentionWakePreflight.test.mjs @@ -72,6 +72,37 @@ test("wake plan rejects non-member managed agents", () => { ); }); +test("editor updates arm a mention-first draft without a rerender", async (t) => { + t.mock.timers.enable({ apis: ["setTimeout"] }); + const { act, renderHook } = await import("@testing-library/react"); + let starts = 0; + const contentRef = { current: "@Fizz " }; + const view = renderHook(() => + useMentionWakePreflight({ + channelId: "general", + contentRef, + enabled: true, + expectedRelayUrl: "wss://relay.example", + expectedSignerPubkey: "c".repeat(64), + getDraftMentionRefs: () => [fizzRef], + getManagedAgentsByPubkey: async () => + new Map([[AGENT, { pubkey: AGENT, status: "stopped" }]]), + isManagedAgentPubkey: () => true, + memberPubkeys: new Set([AGENT]), + startManagedAgent: async () => { + starts += 1; + return { pubkey: AGENT, status: "running" }; + }, + }), + ); + + contentRef.current = "@Fizz please investigate"; + act(() => view.result.current.prepareMentionWake(contentRef.current)); + await act(async () => t.mock.timers.tick(MENTION_WAKE_DELAY_MS)); + + assert.equal(starts, 1); +}); + test("unmount prevents a wake after an in-flight lookup resolves", async (t) => { t.mock.timers.enable({ apis: ["setTimeout"] }); const { act, renderHook } = await import("@testing-library/react"); @@ -84,7 +115,6 @@ test("unmount prevents a wake after an in-flight lookup resolves", async (t) => const view = renderHook(() => useMentionWakePreflight({ channelId: "general", - content: contentRef.current, contentRef, enabled: true, expectedRelayUrl: "wss://relay.example", diff --git a/desktop/src/features/messages/ui/useMentionWakePreflight.ts b/desktop/src/features/messages/ui/useMentionWakePreflight.ts index f8a3c45097..a2acf3f7b8 100644 --- a/desktop/src/features/messages/ui/useMentionWakePreflight.ts +++ b/desktop/src/features/messages/ui/useMentionWakePreflight.ts @@ -68,7 +68,6 @@ export function buildMentionWakePlan({ export function useMentionWakePreflight({ channelId, - content, contentRef, enabled, expectedRelayUrl, @@ -80,7 +79,6 @@ export function useMentionWakePreflight({ startManagedAgent, }: { channelId: string | null; - content: string; contentRef: React.MutableRefObject; enabled: boolean; expectedRelayUrl?: string; @@ -196,10 +194,10 @@ export function useMentionWakePreflight({ // biome-ignore lint/correctness/useExhaustiveDependencies: contextRef keeps callbacks fresh; these inputs are plan invalidation signals. React.useEffect(() => { - prepareMentionWake(content); + prepareMentionWake(contentRef.current); }, [ channelId, - content, + contentRef, enabled, expectedRelayUrl, expectedSignerPubkey, @@ -210,4 +208,6 @@ export function useMentionWakePreflight({ ]); React.useEffect(() => cancelMentionWake, [cancelMentionWake]); + + return { prepareMentionWake }; } diff --git a/desktop/tests/e2e/mentions.spec.ts b/desktop/tests/e2e/mentions.spec.ts index a596669804..f3f2cb2633 100644 --- a/desktop/tests/e2e/mentions.spec.ts +++ b/desktop/tests/e2e/mentions.spec.ts @@ -2085,13 +2085,16 @@ test("substantive in-channel managed-agent mentions wake before send", async ({ "start_managed_agent", ); const input = page.getByTestId("message-input"); - await input.fill("Hey @fizz"); + await input.fill("@fizz"); const dropdown = autocomplete(page); await expect(dropdown.getByText("fizz")).toBeVisible(); await expect(dropdown.getByText("agent")).toBeVisible(); await input.press("Enter"); - await page.keyboard.type(" can you help?"); + await page.keyboard.type( + " Can you investigate this? Please report what you find.", + { delay: 75 }, + ); await expect .poll(