From f6da005b5062c2cd7eea2a860a290b6f9722291b Mon Sep 17 00:00:00 2001 From: HeavyGee <133152184+heavygee@users.noreply.github.com> Date: Tue, 4 Aug 2026 04:18:21 +0100 Subject: [PATCH] feat(web): FUE + composer hint for session @-mentions (#1274) * feat(web): FUE + placeholder for rich composer session @-mentions Discover session @-mentions via composer-grounded FUE and always-on placeholder copy when rich composer is active (#1273). Co-authored-by: Cursor * fix(web): notify onFocus after programmatic rich-composer autofocus Playwright headless (and some engines) skip the DOM focus event for element.focus(), so FUE engage never ran. Call the onFocus prop after autofocus so discovery still works. Co-authored-by: Cursor * fix(web): engage rich-composer FUE on mount DOM focus events are unreliable for programmatic autofocus (and in Playwright). Treat the live rich composer as the affordance and open the callout when the rich path mounts. Co-authored-by: Cursor * fix(web): measure FueCallout height; ellipsis rich placeholder Bot review on #1274: position from real panel height (ResizeObserver) so multi-line FUE bodies clear the composer, and keep long mention placeholders on one ellipsized line in the input row. Co-authored-by: Cursor * fix(web): Escape dismisses rich-composer FUE; keep expand flex chain Escape while the mention FUE is engaging only dismisses the callout (no abort/collapse). FUE anchor is a flex container so expanded RichComposerInput still fills height. Mock resolveComposerPlaceholderKey in sendError tests. Co-authored-by: Cursor --------- Co-authored-by: Cursor --- .../HappyComposer.sendError.test.tsx | 6 +- .../AssistantChat/HappyComposer.tsx | 93 +++++++++++++++---- .../AssistantChat/RichComposerInput.tsx | 21 ++++- web/src/components/Fue.test.ts | 13 +++ web/src/components/Fue.tsx | 28 ++++-- web/src/lib/composerSegments.test.ts | 24 +++++ web/src/lib/composerSegments.ts | 13 +++ web/src/lib/locales/en.ts | 3 + web/src/lib/locales/zh-CN.ts | 3 + 9 files changed, 176 insertions(+), 28 deletions(-) diff --git a/web/src/components/AssistantChat/HappyComposer.sendError.test.tsx b/web/src/components/AssistantChat/HappyComposer.sendError.test.tsx index ccddfb1b..068afcbd 100644 --- a/web/src/components/AssistantChat/HappyComposer.sendError.test.tsx +++ b/web/src/components/AssistantChat/HappyComposer.sendError.test.tsx @@ -83,7 +83,11 @@ vi.mock('@assistant-ui/react', async () => { } }) -vi.mock('@/lib/composerSegments', () => ({ isRichComposerMentionsEnabled: () => false })) +vi.mock('@/lib/composerSegments', () => ({ + isRichComposerMentionsEnabled: () => false, + resolveComposerPlaceholderKey: ({ showContinueHint }: { showContinueHint: boolean }) => + showContinueHint ? 'misc.typeMessage' : 'misc.typeAMessage', +})) vi.mock('@/hooks/useComposerDraft', () => ({ useComposerDraft: (sessionId: string | undefined) => ({ sessionId, complete: true, restoredAny: false }), })) diff --git a/web/src/components/AssistantChat/HappyComposer.tsx b/web/src/components/AssistantChat/HappyComposer.tsx index 04276b20..8c025942 100644 --- a/web/src/components/AssistantChat/HappyComposer.tsx +++ b/web/src/components/AssistantChat/HappyComposer.tsx @@ -19,12 +19,14 @@ import { useRef, useState } from 'react' -import { isRichComposerMentionsEnabled } from '@/lib/composerSegments' +import { isRichComposerMentionsEnabled, resolveComposerPlaceholderKey } from '@/lib/composerSegments' import type { SessionMentionResolveResult } from '@/components/AssistantChat/RichComposerInput' import { RichComposerInput, type RichComposerInputHandle, } from '@/components/AssistantChat/RichComposerInput' +import { useFue } from '@/lib/use-fue' +import { FueCallout, FueDot } from '@/components/Fue' import type { AgentState, CodexCollaborationMode, PermissionMode, PiModelSummary } from '@/types/api' import type { Suggestion } from '@/hooks/useActiveSuggestions' import type { ConversationStatus } from '@/realtime/types' @@ -522,6 +524,7 @@ export function HappyComposer(props: { const textareaRef = useRef(null) const richInputRef = useRef(null) + const richComposerFueAnchorRef = useRef(null) // `composer.text === ''` alone is not enough to identify the empty state // created by a send. A user can type and delete a fresh draft before the // failed mutation reports back. Keep monotonic interaction generations so @@ -538,8 +541,21 @@ export function HappyComposer(props: { // Kill-switch only (?richMentions=0 / localStorage=0 / VITE=false). Mount-time // read — hard reload required, so no per-keystroke localStorage/URL parse. const [richMentionsEnabled] = useState(() => isRichComposerMentionsEnabled()) + const { + status: richComposerFueStatus, + engage: engageRichComposerFue, + dismiss: dismissRichComposerFue, + } = useFue('rich-composer-mentions') const prevControlledByUser = useRef(controlledByUser) + // Composer itself is the affordance: open the FUE callout once the rich + // path is live. Relying on DOM focus alone is flaky (programmatic + // autofocus / Playwright headless often skip the focus event). + useEffect(() => { + if (!richMentionsEnabled) return + engageRichComposerFue() + }, [richMentionsEnabled, engageRichComposerFue]) + const recordUserEdit = useCallback(() => { userEditGenerationRef.current += 1 }, []) @@ -1178,6 +1194,14 @@ export function HappyComposer(props: { } if (key === 'Escape') { + // FUE callout also listens on window; dismiss it first so Escape + // does not also abort a running thread or collapse the editor. + if (richComposerFueStatus === 'engaging') { + e.preventDefault() + e.stopPropagation() + dismissRichComposerFue() + return + } const action = getComposerEscapeAction({ hasSuggestions: suggestions.length > 0, threadIsRunning, @@ -1217,6 +1241,8 @@ export function HappyComposer(props: { haptic, composerEnterBehavior, richMentionsEnabled, + richComposerFueStatus, + dismissRichComposerFue, flushAndSend, canQueueSend, isExpanded, @@ -2058,20 +2084,36 @@ export function HappyComposer(props: { isExpanded ? 'min-h-0 flex-1 items-stretch' : 'items-center' }`}> {richMentionsEnabled ? ( - +
+ engageRichComposerFue()} + resolveSessionMentionTooltip={resolveSessionMentionTooltip} + onEdit={handleRichEdit} + className={editorClassName} + /> + {richComposerFueStatus !== 'acknowledged' ? ( + + ) : null} +
) : isExpanded ? (