diff --git a/apps/app/src/components/promptbox/FollowUpPromptBox.test.tsx b/apps/app/src/components/promptbox/FollowUpPromptBox.test.tsx index b2ec85bebe..96dad2d6ce 100644 --- a/apps/app/src/components/promptbox/FollowUpPromptBox.test.tsx +++ b/apps/app/src/components/promptbox/FollowUpPromptBox.test.tsx @@ -447,6 +447,27 @@ describe("FollowUpPromptBox", () => { expect(queuedMessages.previousElementSibling).toBe(pluginHeaderRoot); }); + it("lays the banner stack on an explicit single-column track", () => { + // jsdom has no layout engine, so this guards the track declaration rather + // than the resulting geometry. A bare `grid gap-2` puts every card in an + // implicit `auto` column, whose base size is the widest card's min-content + // width — and a row built as `min-w-0 flex-1 truncate` still reports its + // full untruncated string there, because that guard only bites once the + // parent width is definite. One long queued message sized the column at + // ~1350px inside a 370px composer and stretched every other card off the + // viewport with it. + const props = createFollowUpPromptBoxProps({ kind: "ready" }); + render( + Queued messages} + />, + ); + + const stack = screen.getByTestId("queued-messages").parentElement; + expect(stack?.className).toContain("grid-cols-[minmax(0,1fr)]"); + }); + it("does not mount plugin banners for a retained inactive composer without a real scope", () => { setPluginSlotRegistrations("inactive-banner", { homepageSections: [], diff --git a/apps/app/src/components/promptbox/FollowUpPromptBox.tsx b/apps/app/src/components/promptbox/FollowUpPromptBox.tsx index 7bd8871707..1b961b491c 100644 --- a/apps/app/src/components/promptbox/FollowUpPromptBox.tsx +++ b/apps/app/src/components/promptbox/FollowUpPromptBox.tsx @@ -53,6 +53,7 @@ import { usePointerCoarse } from "@bb/shared-ui/hooks/use-pointer-coarse"; import { ThreadTimelineScrollToBottomButton } from "@/views/thread-detail/ThreadTimelineScrollToBottomButton"; import { useOptionalPaneContext } from "@/views/thread-detail/PaneContext"; import { ThreadContextWindowIndicator } from "@/components/thread/timeline"; +import { PROMPT_STACK_TRACK_CLASS } from "@/components/promptbox/banner/PromptStackCard"; import { THREAD_PROMPT_CONTEXT_BANNER_ROW_HEIGHT } from "@/components/promptbox/banner/ThreadPromptContextBanner"; import { isPlanModePrompt, @@ -279,7 +280,7 @@ function FollowUpPromptBoxStackOnly({
-
+
{composerScope ? ( {stack} ) : ( @@ -858,7 +859,10 @@ function DefaultFollowUpComposer({ data-promptbox-shell="" className="space-y-2" > -
+
{hasPluginComposerScope ? ( {stack} ) : ( diff --git a/apps/app/src/components/promptbox/NewThreadPromptBox.tsx b/apps/app/src/components/promptbox/NewThreadPromptBox.tsx index b8ece5a4c4..f509ed16de 100644 --- a/apps/app/src/components/promptbox/NewThreadPromptBox.tsx +++ b/apps/app/src/components/promptbox/NewThreadPromptBox.tsx @@ -13,6 +13,7 @@ import type { Host, ProjectSource, PromptTextMention } from "@bb/domain"; import type { ComposerView } from "@get-bb/plugin-sdk"; import type { ComposerTextEffectSource } from "@/lib/composer-text-effects"; import { ComposerBannersSlot } from "@/components/plugin/PluginComposerBanners"; +import { PROMPT_STACK_TRACK_CLASS } from "@/components/promptbox/banner/PromptStackCard"; import { type PluginComposerHost, usePluginComposerViewModel, @@ -385,7 +386,9 @@ const DefaultNewThreadComposer = memo(function DefaultNewThreadComposer({ data-promptbox-shell="" className="w-full" > -
+
{modeConfig.banner} diff --git a/apps/app/src/components/promptbox/banner/PromptStackCard.tsx b/apps/app/src/components/promptbox/banner/PromptStackCard.tsx index 5e8e2e1915..c3407586c3 100644 --- a/apps/app/src/components/promptbox/banner/PromptStackCard.tsx +++ b/apps/app/src/components/promptbox/banner/PromptStackCard.tsx @@ -16,6 +16,23 @@ const BASE_CHROME = cn( "border border-border bg-surface-raised-solid", ); +/** + * The track the stack itself lays cards on. + * + * A bare `grid gap-2` drops every card into an implicit `auto` column, and an + * auto column's base size is the widest card's MIN-CONTENT width. That is not + * the width the card renders at: a row built as `min-w-0 flex-1 truncate` + * still reports its full untruncated string as its min-content contribution, + * because the truncation only bites once the parent hands it a definite width. + * So one long queued message — or one plugin banner with a long rule in it — + * sized the column at ~1350px inside a 370px composer, and every other card + * stretched to match and left the viewport with it. + * + * `minmax(0, 1fr)` pins the track to the shell's width instead. Each card then + * gets a definite width and its own truncation does the rest. + */ +export const PROMPT_STACK_TRACK_CLASS = "grid-cols-[minmax(0,1fr)]"; + export interface PromptStackCardProps { children: ReactNode; /**