#4448 · Unbounded composer actions displace Send
September 30, 2026 · Bug · High priority · Medium effort · ui, mobile
Verdict: REPRODUCED · Root-cause confidence: high for the host layout mechanism.
1. TL;DR
The real BB composer can place Send entirely outside a narrow viewport when a single plugin contributes three text-based usage indicators. The host lays all plugin actions and Send out in an unbounded, non-wrapping, non-shrinking group. At 360 CSS px the composer ends at x=351 but Send spans x=622–662, so an in-viewport touch cannot reach it. At 740 CSS px the same fixture fits and a real touch increments the submit counter. This behavior reproduces in two separate clean checkouts of trusted main. The original third-party plugin and a physical Android device were not available; the report establishes a host-layout defect, not an audit of that plugin.
2. Claims vs findings
| Claim | Status | Evidence |
|---|---|---|
| Footer content overlaps in portrait. | Verified with a synthetic plugin contribution. | The prompt-actions button spans x=17–57 while the first plugin indicator starts at x=35; their rectangles overlap. |
| Send cannot be accessed in portrait. | Verified in Chromium touch emulation. | Send is fully outside the 360px viewport; the center-point hit test fails. A regression assertion also fails at 320px. |
| A wider landscape viewport makes Send accessible. | Verified for the fixture. | At 740×360 Send spans x=678–718 and a touchscreen tap changes the sent counter from 0 to 1. |
| The named custom quota plugin is the cause. | Not established. | No plugin source or reporter device was available. Host constraints alone explain the faithfully reconstructed trigger. |
| Android keyboard and iOS Safari behavior. | Not tested on native devices. | The fix is checked at a reduced 360×320 viewport, which is not a real virtual-keyboard test. |
3. Environment and isolation
- Trusted reproduction base:
0e7b518f135d43005dae201ef34ebb3001607eb4. - Fix checkout rebased onto newer trusted main:
d7a6d74e87f55b80243667c67f68644b4737e77a. The root-cause source is unchanged between these commits. - Debian 12, Node 22.19.0, pnpm 9.15.0, Chrome Headless Shell 154.0.8037.92, real CSS/layout engine, mobile and touch emulation.
- Two separate clean clones named
baseandverify, each installed with the frozen lockfile and built through Turbo. Component servers used distinct ports 47841 and 47842 and separate fresh browser profiles. - The test uses Ladle with the real
PromptBoxInternal, real slot registration and real submit handler. No production BB store, imported data, server, provider session, or credential is used. No server data directory is needed for this component-level reproduction.
4. Minimal reproduction
The artifacts are authored from repository evidence. The fixture contributes three ordinary text indicators through one plugin's composer-actions registration. These are not copied plugin code.
git clone https://github.com/get-bb/bb.git bb-4448 cd bb-4448 git checkout --detach 0e7b518f135d43005dae201ef34ebb3001607eb4 pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build git apply /path/to/fixture.patch pnpm exec turbo run storybook --filter=@bb/app -- --port 47841
Open http://127.0.0.1:47841/?story=plugins--composer-actions--narrow-usage-actions&mode=preview in a fresh local browser. Set a 360×740 CSS-pixel touch viewport. Observe the clipped indicators, overlap at the left, and absent Send. Resize to 740×360; Send is visible and tapping it increments the fixture counter.
For the focused automated regression, open an owned local session with BB Browser Automation, navigate page main to that story, then run:
bb browser-automation run "$SESSION" \ --script-file /path/to/composer-actions.mjs \ --script-host "$HOST_ID" --json
Expected: every control remains inside the composer, control rectangles do not intersect, Send is hit-testable, and each touch invokes submit. Actual on unchanged main: the first 320×740 case fails with control outside composer: 7d usage: 65% remaining; Send is at x=622–662 and its hit test is false. See first failure and second clean failure.



5. Root cause
The outer footer row has no wrapping. Its right action group is shrink-0, lacks a maximum width, and contains plugin contributions immediately before primary controls. The left group has flex-1 min-w-0, so it can collapse below the width of its prompt-actions button while right-hand content overflows.
Each plugin action wrapper also uses shrink-0 without a maximum width. The existing overflow policy counts plugin groups, not their measured widths or action counts. One plugin supplying several indicators therefore stays inline regardless of total width. Plugin-level overflow does not prevent this defect.
6. Small root-cause fix and regression
Allow the footer and right action group to wrap, cap the right group at the available width, preserve the left group's natural basis, and cap each plugin action wrapper. Keep Send's existing non-shrinking submit group. This is a CSS-only host layout repair; it does not change plugin APIs, ordering, usage ranking, protocols, stored data, or add repository dependencies.
The browser regression checks 320, 360, 390, and 430px portrait widths, 740×360 landscape, and a reduced 360×320 viewport. It repeats these six cases for three ordinary usage indicators, one oversized unbreakable indicator, and no plugin actions. All 18 cases pass after the repair, including center-point hit tests, non-overlap checks, and real touchscreen submit taps. See passing output and the production patch. The test is a BB Browser Automation script, not a jsdom layout assertion or an automatically executed Vitest test.
The two directly affected existing Vitest files passed: PluginComposerActions.test.tsx and PromptBoxInternal.test.tsx, 148 tests total. The artifact fixture also supplies interactive standard, oversized, and no-plugin scenarios for manual verification.
Final app lint and typecheck passed. A broad app test run recorded 5219 passes, 71 skips, three failing assertions and one hook timeout in unrelated FilePreview and CommandPalette suites. Those three files plus the two affected composer files then passed together with --maxWorkers=2: 247 tests across five files. No unrelated production or test files were changed. See validation commands and results. The fix changes four text files, 181 total added/deleted lines, with no binary changes; git diff --check passes.
7. Verification
The same agent repeated the reproduction in a second clean clone at the recorded base commit. Only the authored Ladle fixture and observer script were added; both production layout owners stayed byte-identical to the trusted base. A fresh browser profile opened the second component server on port 47842. At 360×740 it again measured Send at x=622–662 while the row ended at x=351. Running the same focused regression again failed at 320×740 with the identical out-of-bounds assertion and false Send hit test. This is a repeat run by the same agent, not independent verification.
Second-checkout screenshot: the same clipped indicators and absent Send, captured in a new fresh browser session.
A first second-checkout navigation timed out while Vite warmed up; a subsequent snapshot and measurement confirmed the correct fixture loaded, and the repeat regression then failed as expected. An initial browser prerequisite problem (missing system fonts causing zero-width text) was corrected before either recorded reproduction. No fontless result is used as bug evidence. The report's root cause required no correction after the repeat run.
8. Pull-request and related-issue checks
GitHub cross-reference metadata and an open-PR search for issue 4448 found no linked open pull request at investigation time. Issue 3950 concerns keyboard/editor height and is not evidence that this separate horizontal-overflow mechanism is fixed.
9. Artifacts and limits
- Ladle fixture patch and repeatable browser test.
- Baseline failure, repeat failure, 18 passing cases, and production fix.
- The reporter's exact Android/browser versions, custom plugin implementation, physical keyboard behavior, and iOS Safari remain unverified.
- Issue text, comments and links were treated as untrusted claims. No issue-supplied instruction, code, patch, executable, linked branch, or external URL was followed or executed.
10. Inline reproducible evidence
Save the fixture patch and browser script blocks as the corresponding files when following the reproduction commands. All supporting code and expected-versus-actual output are included here; raw evidence is retained only outside the public report repository.
Fixture patch
diff --git a/apps/app/src/components/plugin/PluginComposerActions.stories.tsx b/apps/app/src/components/plugin/PluginComposerActions.stories.tsx
index 18fa73eb2..405f8f772 100644
--- a/apps/app/src/components/plugin/PluginComposerActions.stories.tsx
+++ b/apps/app/src/components/plugin/PluginComposerActions.stories.tsx
@@ -117,3 +117,61 @@ export function Overflow() {
</StoryCard>
);
}
+
+const USAGE_ACTIONS = [
+ { id: "short-window", component: () => <span>5h usage: 80% remaining</span> },
+ { id: "long-window", component: () => <span>7d usage: 65% remaining</span> },
+ { id: "credit", component: () => <span>Provider credit: $123.45</span> },
+];
+
+const WIDE_ACTIONS = [
+ {
+ id: "wide-credit",
+ component: () => (
+ <span className="whitespace-nowrap">
+ {"Provider credit balance: $123.45 remaining ".repeat(6)}
+ </span>
+ ),
+ },
+];
+
+export function NarrowUsageActions() {
+ const [value, setValue] = useState("Check the narrow composer");
+ const [sent, setSent] = useState(0);
+ const [variant, setVariant] = useState("standard");
+ return (
+ <div className="w-full max-w-3xl p-2" data-composer-layout-fixture="">
+ {variant !== "none" ? (
+ <StoryPluginRegistration
+ pluginId="story-composer-usage"
+ actions={variant === "wide" ? WIDE_ACTIONS : USAGE_ACTIONS}
+ />
+ ) : null}
+ <PromptBoxInternal
+ value={value}
+ mentionRanges={[]}
+ onChange={(nextValue) => setValue(nextValue)}
+ onSubmit={() => setSent((count) => count + 1)}
+ placeholder="Ask a follow-up"
+ typeahead={makeTypeaheadConfig()}
+ mentionMenuPlacement="top"
+ attachments={makeAttachmentsConfig()}
+ submission={{
+ isSubmitting: false,
+ disabled: false,
+ title: "Submit (Enter)",
+ }}
+ />
+ <output aria-label="Sent messages">{sent}</output>
+ <select
+ aria-label="Plugin action fixture"
+ value={variant}
+ onChange={(event) => setVariant(event.target.value)}
+ >
+ <option value="standard">Usage indicators</option>
+ <option value="wide">Oversized indicator</option>
+ <option value="none">No plugin actions</option>
+ </select>
+ </div>
+ );
+}
Browser regression script
const page = await browser.getPage("main");
if (!page.url().includes("plugins--composer-actions--narrow-usage-actions")) {
throw new Error("Open the Narrow usage actions story in preview mode first");
}
await page.waitForSelector("[data-plugin-composer-action]");
await page.evaluate(() => document.fonts.ready);
let sent = Number(
await page.$eval(
'[aria-label="Sent messages"]',
(element) => element.textContent,
),
);
for (const variant of ["standard", "wide", "none"]) {
await page.select('[aria-label="Plugin action fixture"]', variant);
await page.waitForFunction(
(expected) =>
document.querySelectorAll("[data-plugin-composer-action]").length ===
expected,
{},
variant === "standard" ? 3 : variant === "wide" ? 1 : 0,
);
for (const [width, height] of [
[320, 740],
[360, 740],
[390, 740],
[430, 740],
[740, 360],
[360, 320],
]) {
await page.setViewport({
width,
height,
isMobile: true,
hasTouch: true,
deviceScaleFactor: 1,
});
await page.evaluate(
() =>
new Promise((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(resolve)),
),
);
const geometry = await page.evaluate(() => {
const row = document.querySelector("[data-promptbox-action-row]");
const send = document.querySelector("[data-promptbox-submit-action]");
const controls = [
...document.querySelectorAll(
"[data-promptbox-action-row] button, [data-plugin-composer-action]",
),
];
if (!row || !send) throw new Error("Missing composer controls");
const rowRect = row.getBoundingClientRect();
const sendRect = send.getBoundingClientRect();
return {
row: rowRect.toJSON(),
send: sendRect.toJSON(),
controls: controls.map((element) => ({
label: element.getAttribute("aria-label") ?? element.textContent,
rect: element.getBoundingClientRect().toJSON(),
})),
hit: send.contains(
document.elementFromPoint(
sendRect.x + sendRect.width / 2,
sendRect.y + sendRect.height / 2,
),
),
};
});
console.log(JSON.stringify({ variant, width, height, ...geometry }));
for (const control of geometry.controls) {
if (
control.rect.width <= 0 ||
control.rect.left < geometry.row.left ||
control.rect.right > geometry.row.right ||
control.rect.bottom > height
) {
throw new Error(
`${width}x${height}: control outside composer: ${control.label}`,
);
}
}
for (const [index, control] of geometry.controls.entries()) {
for (const other of geometry.controls.slice(index + 1)) {
if (
control.rect.left < other.rect.right &&
control.rect.right > other.rect.left &&
control.rect.top < other.rect.bottom &&
control.rect.bottom > other.rect.top
) {
throw new Error(
`${width}x${height}: controls overlap: ${control.label}, ${other.label}`,
);
}
}
}
if (!geometry.hit)
throw new Error(`${width}x${height}: Send is not tappable`);
await page.touchscreen.tap(
geometry.send.x + geometry.send.width / 2,
geometry.send.y + geometry.send.height / 2,
);
sent += 1;
await page.waitForFunction(
(expected) =>
Number(
document.querySelector('[aria-label="Sent messages"]').textContent,
) === expected,
{},
sent,
);
}
}
console.log(
"PASS: composer controls stay contained, non-overlapping and tappable",
);
First baseline failure
{"width":320,"height":740,"row":{"x":9,"y":77,"width":302,"height":54,"top":77,"right":311,"bottom":131,"left":9},"send":{"x":622,"y":83,"width":40,"height":40,"top":83,"right":662,"bottom":123,"left":622},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"5h usage: 80% remaining","rect":{"x":35,"y":85,"width":194,"height":36,"top":85,"right":229,"bottom":121,"left":35}},{"label":"7d usage: 65% remaining","rect":{"x":233,"y":85,"width":193,"height":36,"top":85,"right":426,"bottom":121,"left":233}},{"label":"Provider credit: $123.45","rect":{"x":430,"y":85,"width":184,"height":36,"top":85,"right":614,"bottom":121,"left":430}},{"label":"Submit (Enter)","rect":{"x":622,"y":83,"width":40,"height":40,"top":83,"right":662,"bottom":123,"left":622}}],"hit":false}
Error: 320x740: control outside composer: 7d usage: 65% remaining
Exit code: 1
Second clean failure
{"variant":"standard","width":320,"height":740,"row":{"x":9,"y":77,"width":302,"height":54,"top":77,"right":311,"bottom":131,"left":9},"send":{"x":622,"y":83,"width":40,"height":40,"top":83,"right":662,"bottom":123,"left":622},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"5h usage: 80% remaining","rect":{"x":35,"y":85,"width":194,"height":36,"top":85,"right":229,"bottom":121,"left":35}},{"label":"7d usage: 65% remaining","rect":{"x":233,"y":85,"width":193,"height":36,"top":85,"right":426,"bottom":121,"left":233}},{"label":"Provider credit: $123.45","rect":{"x":430,"y":85,"width":184,"height":36,"top":85,"right":614,"bottom":121,"left":430}},{"label":"Submit (Enter)","rect":{"x":622,"y":83,"width":40,"height":40,"top":83,"right":662,"bottom":123,"left":622}}],"hit":false}
Error: 320x740: control outside composer: 7d usage: 65% remaining
Exit code: 1
Passing matrix
{"variant":"standard","width":320,"height":740,"row":{"x":9,"y":77,"width":302,"height":186,"top":77,"right":311,"bottom":263,"left":9},"send":{"x":258,"y":215,"width":40,"height":40,"top":215,"right":298,"bottom":255,"left":258},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"5h usage: 80% remaining","rect":{"x":104,"y":135,"width":194,"height":36,"top":135,"right":298,"bottom":171,"left":104}},{"label":"7d usage: 65% remaining","rect":{"x":105,"y":175,"width":193,"height":36,"top":175,"right":298,"bottom":211,"left":105}},{"label":"Provider credit: $123.45","rect":{"x":66,"y":217,"width":184,"height":36,"top":217,"right":250,"bottom":253,"left":66}},{"label":"Submit (Enter)","rect":{"x":258,"y":215,"width":40,"height":40,"top":215,"right":298,"bottom":255,"left":258}}],"hit":true}
{"variant":"standard","width":360,"height":740,"row":{"x":9,"y":77,"width":342,"height":186,"top":77,"right":351,"bottom":263,"left":9},"send":{"x":298,"y":215,"width":40,"height":40,"top":215,"right":338,"bottom":255,"left":298},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"5h usage: 80% remaining","rect":{"x":144,"y":135,"width":194,"height":36,"top":135,"right":338,"bottom":171,"left":144}},{"label":"7d usage: 65% remaining","rect":{"x":145,"y":175,"width":193,"height":36,"top":175,"right":338,"bottom":211,"left":145}},{"label":"Provider credit: $123.45","rect":{"x":106,"y":217,"width":184,"height":36,"top":217,"right":290,"bottom":253,"left":106}},{"label":"Submit (Enter)","rect":{"x":298,"y":215,"width":40,"height":40,"top":215,"right":338,"bottom":255,"left":298}}],"hit":true}
{"variant":"standard","width":390,"height":740,"row":{"x":9,"y":77,"width":372,"height":186,"top":77,"right":381,"bottom":263,"left":9},"send":{"x":328,"y":215,"width":40,"height":40,"top":215,"right":368,"bottom":255,"left":328},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"5h usage: 80% remaining","rect":{"x":174,"y":135,"width":194,"height":36,"top":135,"right":368,"bottom":171,"left":174}},{"label":"7d usage: 65% remaining","rect":{"x":175,"y":175,"width":193,"height":36,"top":175,"right":368,"bottom":211,"left":175}},{"label":"Provider credit: $123.45","rect":{"x":136,"y":217,"width":184,"height":36,"top":217,"right":320,"bottom":253,"left":136}},{"label":"Submit (Enter)","rect":{"x":328,"y":215,"width":40,"height":40,"top":215,"right":368,"bottom":255,"left":328}}],"hit":true}
{"variant":"standard","width":430,"height":740,"row":{"x":9,"y":77,"width":412,"height":186,"top":77,"right":421,"bottom":263,"left":9},"send":{"x":368,"y":215,"width":40,"height":40,"top":215,"right":408,"bottom":255,"left":368},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"5h usage: 80% remaining","rect":{"x":214,"y":135,"width":194,"height":36,"top":135,"right":408,"bottom":171,"left":214}},{"label":"7d usage: 65% remaining","rect":{"x":27,"y":175,"width":193,"height":36,"top":175,"right":220,"bottom":211,"left":27}},{"label":"Provider credit: $123.45","rect":{"x":224,"y":175,"width":184,"height":36,"top":175,"right":408,"bottom":211,"left":224}},{"label":"Submit (Enter)","rect":{"x":368,"y":215,"width":40,"height":40,"top":215,"right":408,"bottom":255,"left":368}}],"hit":true}
{"variant":"standard","width":740,"height":360,"row":{"x":9,"y":77,"width":722,"height":54,"top":77,"right":731,"bottom":131,"left":9},"send":{"x":678,"y":83,"width":40,"height":40,"top":83,"right":718,"bottom":123,"left":678},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"5h usage: 80% remaining","rect":{"x":91,"y":85,"width":194,"height":36,"top":85,"right":285,"bottom":121,"left":91}},{"label":"7d usage: 65% remaining","rect":{"x":289,"y":85,"width":193,"height":36,"top":85,"right":482,"bottom":121,"left":289}},{"label":"Provider credit: $123.45","rect":{"x":486,"y":85,"width":184,"height":36,"top":85,"right":670,"bottom":121,"left":486}},{"label":"Submit (Enter)","rect":{"x":678,"y":83,"width":40,"height":40,"top":83,"right":718,"bottom":123,"left":678}}],"hit":true}
{"variant":"standard","width":360,"height":320,"row":{"x":9,"y":77,"width":342,"height":186,"top":77,"right":351,"bottom":263,"left":9},"send":{"x":298,"y":215,"width":40,"height":40,"top":215,"right":338,"bottom":255,"left":298},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"5h usage: 80% remaining","rect":{"x":144,"y":135,"width":194,"height":36,"top":135,"right":338,"bottom":171,"left":144}},{"label":"7d usage: 65% remaining","rect":{"x":145,"y":175,"width":193,"height":36,"top":175,"right":338,"bottom":211,"left":145}},{"label":"Provider credit: $123.45","rect":{"x":106,"y":217,"width":184,"height":36,"top":217,"right":290,"bottom":253,"left":106}},{"label":"Submit (Enter)","rect":{"x":298,"y":215,"width":40,"height":40,"top":215,"right":338,"bottom":255,"left":298}}],"hit":true}
{"variant":"wide","width":320,"height":740,"row":{"x":9,"y":77,"width":302,"height":146,"top":77,"right":311,"bottom":223,"left":9},"send":{"x":258,"y":175,"width":40,"height":40,"top":175,"right":298,"bottom":215,"left":258},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining ","rect":{"x":23,"y":135,"width":275,"height":36,"top":135,"right":298,"bottom":171,"left":23}},{"label":"Submit (Enter)","rect":{"x":258,"y":175,"width":40,"height":40,"top":175,"right":298,"bottom":215,"left":258}}],"hit":true}
{"variant":"wide","width":360,"height":740,"row":{"x":9,"y":77,"width":342,"height":146,"top":77,"right":351,"bottom":223,"left":9},"send":{"x":298,"y":175,"width":40,"height":40,"top":175,"right":338,"bottom":215,"left":298},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining ","rect":{"x":23,"y":135,"width":315,"height":36,"top":135,"right":338,"bottom":171,"left":23}},{"label":"Submit (Enter)","rect":{"x":298,"y":175,"width":40,"height":40,"top":175,"right":338,"bottom":215,"left":298}}],"hit":true}
{"variant":"wide","width":390,"height":740,"row":{"x":9,"y":77,"width":372,"height":146,"top":77,"right":381,"bottom":223,"left":9},"send":{"x":328,"y":175,"width":40,"height":40,"top":175,"right":368,"bottom":215,"left":328},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining ","rect":{"x":23,"y":135,"width":345,"height":36,"top":135,"right":368,"bottom":171,"left":23}},{"label":"Submit (Enter)","rect":{"x":328,"y":175,"width":40,"height":40,"top":175,"right":368,"bottom":215,"left":328}}],"hit":true}
{"variant":"wide","width":430,"height":740,"row":{"x":9,"y":77,"width":412,"height":146,"top":77,"right":421,"bottom":223,"left":9},"send":{"x":368,"y":175,"width":40,"height":40,"top":175,"right":408,"bottom":215,"left":368},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining ","rect":{"x":23,"y":135,"width":385,"height":36,"top":135,"right":408,"bottom":171,"left":23}},{"label":"Submit (Enter)","rect":{"x":368,"y":175,"width":40,"height":40,"top":175,"right":408,"bottom":215,"left":368}}],"hit":true}
{"variant":"wide","width":740,"height":360,"row":{"x":9,"y":77,"width":722,"height":146,"top":77,"right":731,"bottom":223,"left":9},"send":{"x":678,"y":175,"width":40,"height":40,"top":175,"right":718,"bottom":215,"left":678},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining ","rect":{"x":23,"y":135,"width":695,"height":36,"top":135,"right":718,"bottom":171,"left":23}},{"label":"Submit (Enter)","rect":{"x":678,"y":175,"width":40,"height":40,"top":175,"right":718,"bottom":215,"left":678}}],"hit":true}
{"variant":"wide","width":360,"height":320,"row":{"x":9,"y":77,"width":342,"height":146,"top":77,"right":351,"bottom":223,"left":9},"send":{"x":298,"y":175,"width":40,"height":40,"top":175,"right":338,"bottom":215,"left":298},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining Provider credit balance: $123.45 remaining ","rect":{"x":23,"y":135,"width":315,"height":36,"top":135,"right":338,"bottom":171,"left":23}},{"label":"Submit (Enter)","rect":{"x":298,"y":175,"width":40,"height":40,"top":175,"right":338,"bottom":215,"left":298}}],"hit":true}
{"variant":"none","width":320,"height":740,"row":{"x":9,"y":77,"width":302,"height":54,"top":77,"right":311,"bottom":131,"left":9},"send":{"x":258,"y":83,"width":40,"height":40,"top":83,"right":298,"bottom":123,"left":258},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"Submit (Enter)","rect":{"x":258,"y":83,"width":40,"height":40,"top":83,"right":298,"bottom":123,"left":258}}],"hit":true}
{"variant":"none","width":360,"height":740,"row":{"x":9,"y":77,"width":342,"height":54,"top":77,"right":351,"bottom":131,"left":9},"send":{"x":298,"y":83,"width":40,"height":40,"top":83,"right":338,"bottom":123,"left":298},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"Submit (Enter)","rect":{"x":298,"y":83,"width":40,"height":40,"top":83,"right":338,"bottom":123,"left":298}}],"hit":true}
{"variant":"none","width":390,"height":740,"row":{"x":9,"y":77,"width":372,"height":54,"top":77,"right":381,"bottom":131,"left":9},"send":{"x":328,"y":83,"width":40,"height":40,"top":83,"right":368,"bottom":123,"left":328},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"Submit (Enter)","rect":{"x":328,"y":83,"width":40,"height":40,"top":83,"right":368,"bottom":123,"left":328}}],"hit":true}
{"variant":"none","width":430,"height":740,"row":{"x":9,"y":77,"width":412,"height":54,"top":77,"right":421,"bottom":131,"left":9},"send":{"x":368,"y":83,"width":40,"height":40,"top":83,"right":408,"bottom":123,"left":368},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"Submit (Enter)","rect":{"x":368,"y":83,"width":40,"height":40,"top":83,"right":408,"bottom":123,"left":368}}],"hit":true}
{"variant":"none","width":740,"height":360,"row":{"x":9,"y":77,"width":722,"height":54,"top":77,"right":731,"bottom":131,"left":9},"send":{"x":678,"y":83,"width":40,"height":40,"top":83,"right":718,"bottom":123,"left":678},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"Submit (Enter)","rect":{"x":678,"y":83,"width":40,"height":40,"top":83,"right":718,"bottom":123,"left":678}}],"hit":true}
{"variant":"none","width":360,"height":320,"row":{"x":9,"y":77,"width":342,"height":54,"top":77,"right":351,"bottom":131,"left":9},"send":{"x":298,"y":83,"width":40,"height":40,"top":83,"right":338,"bottom":123,"left":298},"controls":[{"label":"Prompt actions","rect":{"x":17,"y":83,"width":40,"height":40,"top":83,"right":57,"bottom":123,"left":17}},{"label":"Submit (Enter)","rect":{"x":298,"y":83,"width":40,"height":40,"top":83,"right":338,"bottom":123,"left":298}}],"hit":true}
PASS: composer controls stay contained, non-overlapping and tappable
Exit code: 0
Production fix
diff --git a/apps/app/src/components/plugin/PluginComposerActions.tsx b/apps/app/src/components/plugin/PluginComposerActions.tsx
index 0b674bbdb..752c8fb23 100644
--- a/apps/app/src/components/plugin/PluginComposerActions.tsx
+++ b/apps/app/src/components/plugin/PluginComposerActions.tsx
@@ -189,7 +189,7 @@ function PluginComposerActionGroupMount({
<div
key={`${key}/${scopeKey}`}
data-plugin-composer-action=""
- className="flex h-9 max-h-9 shrink-0 items-center overflow-hidden"
+ className="flex h-9 max-h-9 min-w-0 max-w-full shrink-0 items-center overflow-hidden"
>
<PluginSlotMount
pluginId={pluginId}
diff --git a/apps/app/src/components/promptbox/PromptBoxInternal.tsx b/apps/app/src/components/promptbox/PromptBoxInternal.tsx
index a7b631c05..e2790d15c 100644
--- a/apps/app/src/components/promptbox/PromptBoxInternal.tsx
+++ b/apps/app/src/components/promptbox/PromptBoxInternal.tsx
@@ -3271,7 +3271,7 @@ export function PromptBoxInternal({
<div
data-promptbox-action-row=""
className={cn(
- "relative flex shrink-0 select-none flex-row items-center gap-3 pb-2 pl-3.5 pr-[13px] pt-1.5",
+ "relative flex shrink-0 select-none flex-row flex-wrap items-center gap-3 pb-2 pl-3.5 pr-[13px] pt-1.5",
showCompactLayout && "absolute inset-y-0 right-2 gap-0 p-0",
)}
>
@@ -3301,7 +3301,7 @@ export function PromptBoxInternal({
data-promptbox-expanded-only=""
data-promptbox-standard-actions=""
className={cn(
- "flex min-w-0 flex-1 flex-row items-center gap-1 transition-[opacity,transform] duration-[180ms] ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none",
+ "flex min-w-0 flex-auto flex-row items-center gap-1 transition-[opacity,transform] duration-[180ms] ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none",
showVoiceActionGroup
? "pointer-events-none translate-y-1 opacity-0"
: "translate-y-0 opacity-100",
@@ -3324,7 +3324,7 @@ export function PromptBoxInternal({
<div
data-promptbox-standard-actions=""
className={cn(
- "flex shrink-0 flex-row items-center gap-1 transition-[opacity,transform] duration-[180ms] ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none",
+ "flex min-w-0 max-w-full shrink-0 flex-row flex-wrap items-center justify-end gap-1 transition-[opacity,transform] duration-[180ms] ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none",
showVoiceActionGroup
? "pointer-events-none translate-y-1 opacity-0"
: "translate-y-0 opacity-100",
Validation commands and results
Reproduction base: 0e7b518f135d43005dae201ef34ebb3001607eb4 Fix branch base: d7a6d74e87f55b80243667c67f68644b4737e77a Both clean checkouts: pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build Result: success, 62 build tasks completed per checkout. Before fix: BB Browser Automation composer-actions.mjs Result: FAIL at 320x740, control outside composer: 7d usage: 65% remaining. Repeated with unchanged production files in the second clean checkout: identical failure. After fix: BB Browser Automation composer-actions.mjs Result: PASS in all 18 variant/viewport cases with real touchscreen submits. pnpm exec turbo run test --filter=@bb/app -- src/components/plugin/PluginComposerActions.test.tsx src/components/promptbox/PromptBoxInternal.test.tsx Result: PASS, 148 tests across 2 files. pnpm exec turbo run lint typecheck test --filter=@bb/app --output-logs=errors-only Lint and typecheck: PASS. Broad test run: 5219 passed, 71 skipped, 3 failed assertions, 1 hook timeout; failures in unrelated FilePreview and CommandPalette files. pnpm exec turbo run test --filter=@bb/app -- src/components/plugin/PluginComposerActions.test.tsx src/components/promptbox/PromptBoxInternal.test.tsx src/components/secondary-panel/FilePreview.test.tsx src/components/commands/CommandPalette.test.tsx src/components/commands/CommandPalette.cold-open.test.tsx --maxWorkers=2 Result: PASS, 247 tests across 5 files, including all files that failed in the broad run. git diff --check Result: PASS. git diff --numstat origin/main 115 0 apps/app/.ladle/tests/composer-actions.mjs 58 0 apps/app/src/components/plugin/PluginComposerActions.stories.tsx 1 1 apps/app/src/components/plugin/PluginComposerActions.tsx 3 3 apps/app/src/components/promptbox/PromptBoxInternal.tsx Total: 4 files, 181 added/deleted text lines, 0 binary changes. Not tested: physical Android Chrome virtual keyboard or iOS Safari.
AGENT GENERATED