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 ? (