fix(site/src/pages/AgentsPage): prevent planning pill overlap

This commit is contained in:
Jaayden Halko
2026-06-02 13:52:10 +00:00
parent 858ba11bf7
commit ab6b4d1f3f
2 changed files with 110 additions and 15 deletions
@@ -842,6 +842,76 @@ export const PlanningIndicator: Story = {
},
};
const narrowPlanningContextUsage: AgentContextUsage = {
usedTokens: 100_000,
contextLimitTokens: 200_000,
};
const narrowPlanningModelOptions = [
{
id: "long-model-name",
provider: "anthropic",
model: "claude-sonnet-4-5-long-name",
displayName: "Claude Sonnet 4.5 Extended Thinking",
},
] as const;
export const PlanningIndicatorNarrow: Story = {
args: {
planModeEnabled: true,
onPlanModeToggle: fn(),
contextUsage: narrowPlanningContextUsage,
selectedModel: narrowPlanningModelOptions[0].id,
modelOptions: [...narrowPlanningModelOptions],
},
decorators: [
(Story) => (
<div style={{ width: 360 }}>
<Story />
</div>
),
],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const composer = await canvas.findByTestId("chat-composer");
const sendButton = canvas.getByRole("button", { name: "Send" });
const contextUsageButton = canvas.getByRole("button", {
name: /Context usage/,
});
const planningBadge = canvasElement.querySelector<HTMLElement>(
"[data-testid='planning-badge']",
);
const isVisible = (element: HTMLElement) => {
const style = getComputedStyle(element);
const rect = element.getBoundingClientRect();
return (
style.display !== "none" &&
style.visibility !== "hidden" &&
rect.width > 0 &&
rect.height > 0
);
};
await waitFor(() => {
const composerRect = composer.getBoundingClientRect();
const sendButtonRect = sendButton.getBoundingClientRect();
const contextUsageRect = contextUsageButton.getBoundingClientRect();
expect(contextUsageRect.left).toBeGreaterThanOrEqual(composerRect.left);
expect(sendButtonRect.right).toBeLessThanOrEqual(composerRect.right);
if (planningBadge && isVisible(planningBadge)) {
expect(planningBadge.getBoundingClientRect().right).toBeLessThanOrEqual(
contextUsageRect.left + 1,
);
return;
}
expect(canvas.getByRole("button", { name: "1 more item" })).toBeVisible();
});
},
};
export const DisablePlanModeFromBadge: Story = {
args: {
planModeEnabled: true,
@@ -196,7 +196,8 @@ export interface AttachedWorkspaceInfo {
type ToolBadgeData =
| { kind: "workspace"; name: string }
| ({ kind: "attached-workspace" } & AttachedWorkspaceInfo)
| { kind: "mcp"; server: TypesGen.MCPServerConfig };
| { kind: "mcp"; server: TypesGen.MCPServerConfig }
| { kind: "planning" };
// Small `X` button rendered inside pill-style badges (attached
// workspace, MCP server, planning indicator) to dismiss or disable
@@ -224,13 +225,38 @@ const ToolBadge: FC<{
badge: ToolBadgeData;
onRemoveWorkspace?: () => void;
onRemoveMcp?: (serverId: string) => void;
onRemovePlanning?: () => void;
isDisabled?: boolean;
className?: string;
}> = ({ badge, onRemoveWorkspace, onRemoveMcp, className }) => {
}> = ({
badge,
onRemoveWorkspace,
onRemoveMcp,
onRemovePlanning,
isDisabled,
className,
}) => {
const badgeCls = cn(
"inline-flex shrink-0 items-center gap-1 rounded-full bg-surface-secondary px-2 py-0.5 text-xs font-medium text-content-secondary",
className,
);
if (badge.kind === "planning") {
return (
<span data-testid="planning-badge" className={badgeCls}>
<PencilIcon className="size-3" />
Planning
{onRemovePlanning && (
<BadgeDismissButton
onClick={onRemovePlanning}
ariaLabel="Disable plan mode"
isDisabled={isDisabled}
/>
)}
</span>
);
}
if (badge.kind === "attached-workspace") {
return (
<Tooltip>
@@ -525,6 +551,9 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
// Ordered list of active tool badge data so we can determine
// which ones ended up in the overflow popover.
const allBadges: ToolBadgeData[] = [];
if (planModeEnabled) {
allBadges.push({ kind: "planning" });
}
// When workspace data is available, WorkspacePill handles
// the display (including app dropdown). Otherwise fall back
// to the simple attached-workspace ToolBadge.
@@ -1339,24 +1368,12 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
disabled={isDisabled}
placeholder={modelSelectorPlaceholder}
formatProviderLabel={formatProviderLabel}
className="md:shrink"
dropdownSide="top"
dropdownAlign="center"
enableMobileFullWidthDropdown
/>
)}
{planModeEnabled && (
<span className="hidden shrink-0 items-center gap-1 rounded-full bg-surface-secondary px-2 py-0.5 text-xs font-medium text-content-secondary sm:inline-flex">
<PencilIcon className="size-3" />
Planning
{onPlanModeToggle && (
<BadgeDismissButton
onClick={handleDisablePlanMode}
ariaLabel="Disable plan mode"
isDisabled={isDisabled}
/>
)}
</span>
)}{" "}
{/* Badge row; all badges and the pill always
* render so the DOM structure never changes.
* Overflow badges use invisible + order-1 to
@@ -1387,6 +1404,10 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
badge={badge}
onRemoveWorkspace={removeWorkspaceHandler}
onRemoveMcp={handleRemoveMcp}
onRemovePlanning={
onPlanModeToggle ? handleDisablePlanMode : undefined
}
isDisabled={isDisabled}
className={isOverflow ? "invisible order-1" : undefined}
/>
);
@@ -1427,6 +1448,10 @@ export const AgentChatInput: FC<AgentChatInputProps> = ({
badge={badge}
onRemoveWorkspace={removeWorkspaceHandler}
onRemoveMcp={handleRemoveMcp}
onRemovePlanning={
onPlanModeToggle ? handleDisablePlanMode : undefined
}
isDisabled={isDisabled}
/>
))}
</PopoverContent>