Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions apps/web/src/components/ChatView.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
29 changes: 24 additions & 5 deletions apps/web/src/components/chat/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -3033,28 +3042,38 @@ 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 ? (
<div className="absolute inset-x-0 bottom-full z-20 mb-1 flex px-1">
<div className="absolute inset-x-0 bottom-full z-20 mb-1.5 flex px-2">
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
data-prompt-suggestion="true"
className="inline-flex max-w-[calc(50%-2rem)] cursor-pointer items-center gap-2 rounded-md border border-border/55 bg-card px-2.5 py-1.5 text-left text-muted-foreground text-xs shadow-sm shadow-black/5 transition-colors hover:border-border hover:bg-card hover:text-foreground focus-ring"
className="group inline-flex max-w-[calc(50%-2rem)] cursor-pointer items-center gap-2 rounded-sm px-1 py-0.5 text-left text-muted-foreground text-xs transition-colors hover:text-foreground focus-ring"
aria-label={`Use Claude suggested prompt: ${latestPromptSuggestion}`}
onClick={() => applyPromptSuggestion(latestPromptSuggestion)}
>
<SparklesIcon className="size-3.5 shrink-0 text-muted-foreground/65" />
<SparklesIcon className="size-3.5 shrink-0 text-muted-foreground/65 transition-colors group-hover:text-foreground/80" />
<span
ref={promptSuggestionOverflow.elementRef}
data-prompt-suggestion-text="true"
className="truncate"
>
{latestPromptSuggestionDisplayText}
</span>
{/* Tab hint stays hidden until hover or keyboard focus so the
resting state is just the sparkle and the words. */}
<Kbd
aria-hidden="true"
className="h-4 shrink-0 rounded-sm px-1 text-[10px] opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100"
>
Tab
</Kbd>
</button>
}
/>
Expand Down
Loading