diff --git a/apps/web/src/components/ChatView.browser.tsx b/apps/web/src/components/ChatView.browser.tsx index 9efba9ab..50b49e6b 100644 --- a/apps/web/src/components/ChatView.browser.tsx +++ b/apps/web/src/components/ChatView.browser.tsx @@ -10082,6 +10082,35 @@ describe("ChatView timeline estimator parity (full app)", () => { } }); + it("accepts the prompt suggestion with Tab from the focused composer", async () => { + const suggestion = "Commit this and open the PR"; + const { mounted } = await mountPromptSuggestionChip(suggestion); + try { + const composerEditor = await waitForComposerEditor(); + composerEditor.focus(); + composerEditor.dispatchEvent( + new KeyboardEvent("keydown", { key: "Tab", bubbles: true, cancelable: true }), + ); + + await waitForElement( + () => (composerEditor.textContent?.trim() === suggestion ? composerEditor : null), + () => + `Tab never moved the suggestion into the composer; it holds "${ + composerEditor.textContent ?? "" + }".`, + ); + // Accepting the suggestion consumes it, so the chip goes away. + await waitForElement( + () => (document.querySelector('[data-prompt-suggestion="true"]') ? null : document.body), + "The prompt suggestion chip stayed visible after Tab accepted it.", + ); + // Focus stays in the composer instead of tabbing out to the toolbar. + expect(document.activeElement).toBe(composerEditor); + } finally { + await mounted.cleanup(); + } + }); + it("keeps the slash-command menu visible above the composer", async () => { const mounted = await mountChatView({ viewport: DEFAULT_VIEWPORT, diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index 36adda4d..f44dee17 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -145,6 +145,7 @@ import { useProviderRateLimitResetCredit, } from "../ProviderRateLimitResetCredit"; import { CircleAlertIcon, FileTextIcon, SparklesIcon, XIcon } from "lucide-react"; +import { Kbd } from "../ui/kbd"; import { proposedPlanTitle } from "../../proposedPlan"; import { getProviderDisplayName, @@ -2590,6 +2591,14 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) return true; } } + // Tab accepts the Claude suggestion floating above the composer. The + // suggestion only exists while the composer is empty, so this never + // clobbers typed text, and the menu branch above already claimed Tab when + // a slash or mention menu is open. + if (key === "Tab" && latestPromptSuggestion) { + applyPromptSuggestion(latestPromptSuggestion); + return true; + } if (key === "Enter" && !event.shiftKey) { submitComposer(); return true; @@ -3033,21 +3042,23 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) > {/* Float the suggestion above the composer (like the scroll-to-bottom button) so it overlays the bottom of the message list instead of consuming input-bar height. - Width is capped below half the composer so it never reaches the centered - scroll-to-bottom button that shares this band. */} + It is bare text rather than a chip: no border, fill, or shadow, so it reads as + a quiet hint instead of a second card stacked on the composer. Width is capped + below half the composer so it never reaches the centered scroll-to-bottom + button that shares this band. */} {latestPromptSuggestion && latestPromptSuggestionDisplayText && !isComposerCollapsedMobile ? ( -