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
1 change: 1 addition & 0 deletions rules/git-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ The stashed changes will be automatically merged back after the rebase completes
- If both sides of a conflict have valid imports/hooks, keep both and remove any duplicate constant redefinitions
- When rebasing documentation/table conflicts (e.g., workflow README tables), prefer keeping **both** additions from HEAD and upstream - merge new rows/content from both branches rather than choosing one side
- **Complementary additions**: When both sides added new sections at the end of a file (e.g., both added different documentation tips), keep both sections rather than choosing one — they're not truly conflicting, just different additions
- **React component wrapper conflicts**: When rebasing UI changes that conflict on wrapper div classes (e.g., `flex items-start space-x-2` vs `flex items-end gap-1`), keep the newer styling from the incoming commit but preserve any functional components (like dialogs or modals) that exist in HEAD but not in the incoming change

## Rebasing with uncommitted changes

Expand Down
11 changes: 2 additions & 9 deletions src/components/ChatInputControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function ChatInputControls({
(settings?.selectedChatMode === "build" && enabledMcpServersCount > 0);

return (
<div className="flex">
<div className="flex items-center">
<ChatModeSelector />
{showMcpToolsPicker && (
<>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM | consistency / dead-code | Found by: All 3 reviewers (unanimous)

Leftover spacer div inconsistent with new gap approach

The PR adds gap-0.5 to the parent flex container and removes several spacer <div> elements, but leaves one <div className="w-1.5"></div> between the MCP tools section and ModelPicker. This spacer now stacks with the gap, producing inconsistent spacing: gap-0.5 (2px) between most items, but gap-0.5 + w-1.5 (2px + 6px = 8px) between McpToolsPicker and ModelPicker.

💡 Suggestion: Remove the remaining spacer div and let gap-0.5 handle all spacing uniformly (or increase gap if more spacing is desired between all items).

Expand All @@ -33,15 +33,8 @@ export function ChatInputControls({
)}
<div className="w-1.5"></div>
<ModelPicker />
<div className="w-1.5"></div>
<ProModeSelector />
<div className="w-1"></div>
{showContextFilesPicker && (
<>
<ContextFilesPicker />
<div className="w-0.5"></div>
</>
)}
{showContextFilesPicker && <ContextFilesPicker />}
</div>
);
}
61 changes: 47 additions & 14 deletions src/components/ChatModeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { toast } from "sonner";
import { LocalAgentNewChatToast } from "./LocalAgentNewChatToast";
import { useAtomValue } from "jotai";
import { chatMessagesByIdAtom } from "@/atoms/chatAtoms";
import { Hammer, Bot, MessageCircle, Lightbulb } from "lucide-react";

function NewBadge() {
return (
Expand Down Expand Up @@ -95,6 +96,22 @@ export function ChatModeSelector() {
return "Build";
}
};

const getModeIcon = (mode: ChatMode) => {
switch (mode) {
case "build":
case "agent":
return <Hammer size={14} />;
case "ask":
return <MessageCircle size={14} />;
case "local-agent":
return <Bot size={14} />;
case "plan":
return <Lightbulb size={14} />;
default:
return <Hammer size={14} />;
}
Comment on lines +100 to +113

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM | consistency / maintainability | Found by: Code Health, UX Wizard

getModeIcon duplicates the icon-to-mode mapping that's repeated in each SelectItem below

The icon for each mode is defined in two places:

  1. getModeIcon (lines 100–113) — used in the trigger
  2. Each SelectItem (lines 155–220) — with additional color classes

If a mode's icon changes, both must be updated. The default case also silently returns <Hammer />, masking any missing mapping for future modes.

Suggestion: Extract a single modeConfig record mapping each mode to { icon, displayName, color } and derive both the trigger icon and the dropdown items from it. For the default case, consider returning a generic icon so unknown modes are visually distinct.

};
const isMac = detectIsMac();

return (
Expand All @@ -109,18 +126,25 @@ export function ChatModeSelector() {
<MiniSelectTrigger
data-testid="chat-mode-selector"
className={cn(
"h-6 w-fit px-1.5 py-0 text-xs-sm font-medium shadow-none gap-0.5",
selectedMode === "build" ||
selectedMode === "local-agent" ||
selectedMode === "plan"
? "bg-background hover:bg-muted/50 focus:bg-muted/50"
: "bg-primary/10 hover:bg-primary/20 focus:bg-primary/20 text-primary border-primary/20 dark:bg-primary/20 dark:hover:bg-primary/30 dark:focus:bg-primary/30",
"cursor-pointer w-fit px-2 py-0 text-xs font-medium border-none shadow-none gap-1 rounded-lg transition-colors",
selectedMode === "build" || selectedMode === "local-agent"
? "text-foreground/80 hover:text-foreground hover:bg-muted/60"
: selectedMode === "ask"
? "bg-purple-500/10 text-purple-600 hover:bg-purple-500/15 dark:bg-purple-500/15 dark:text-purple-400 dark:hover:bg-purple-500/20"
: selectedMode === "plan"
? "bg-blue-500/10 text-blue-600 hover:bg-blue-500/15 dark:bg-blue-500/15 dark:text-blue-400 dark:hover:bg-blue-500/20"
: "text-foreground/80 hover:text-foreground hover:bg-muted/60",
)}
Comment on lines 128 to 137

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM | consistency / complexity | Found by: Code Health, UX

Build/Agent modes visually blend into toolbar; nested ternary is hard to maintain

Two related issues:

  1. Visual distinction: Build and Local Agent modes use text-muted-foreground hover:text-foreground hover:bg-muted/60 — identical styling to ModelPicker, McpToolsPicker, and other toolbar controls. The mode selector is the most important control, but in Build/Agent mode it has no visual weight to distinguish it. Ask (purple) and Plan (blue) get color accents, but the most commonly used modes don't.

  2. Code readability: The 4-level nested ternary (build/local-agentaskplan → default) is hard to scan, especially since the default case duplicates the first branch.

💡 Suggestion: Extract a getModeStyles(mode: ChatMode): string helper (matching the existing getModeDisplayName and getModeIcon pattern). Give Build/Agent at least text-foreground instead of text-muted-foreground to differentiate the active mode from surrounding controls.

size="sm"
/>
}
>
<SelectValue>{getModeDisplayName(selectedMode)}</SelectValue>
<SelectValue>
<span className="flex items-center gap-1.5">
{getModeIcon(selectedMode)}
{getModeDisplayName(selectedMode)}
</span>
</SelectValue>
</TooltipTrigger>
<TooltipContent>
{`Open mode menu (${isMac ? "\u2318 + ." : "Ctrl + ."} to toggle)`}
Expand All @@ -131,10 +155,11 @@ export function ChatModeSelector() {
<SelectItem value="local-agent">
<div className="flex flex-col items-start">
<div className="flex items-center gap-1.5">
<Bot size={14} className="text-muted-foreground" />
<span className="font-medium">Agent v2</span>
<NewBadge />
</div>
<span className="text-xs text-muted-foreground">
<span className="text-xs text-muted-foreground ml-[22px]">
Better at bigger tasks and debugging
</span>
</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM | consistency / maintainability | Found by: Code Health, Endorsed by: UX, Correctness

Brittle ml-[22px] magic number repeated 5 times

The description text uses ml-[22px] to indent below the icon+label row. This hardcoded pixel value appears 5 times and is tightly coupled to the icon size (14px) + gap (1.5 = 6px) + some rounding. If the icon size or gap changes, all 5 instances must be updated manually.

💡 Suggestion: Use pl-[calc(14px+0.375rem)] to make the coupling explicit, or better yet, wrap each item's layout in a shared component/pattern that handles the indent structurally (e.g., using a grid or a shared padding class).

Expand All @@ -143,10 +168,11 @@ export function ChatModeSelector() {
<SelectItem value="plan">
<div className="flex flex-col items-start">
<div className="flex items-center gap-1.5">
<Lightbulb size={14} className="text-blue-500" />
<span className="font-medium">Plan</span>
<NewBadge />
</div>
<span className="text-xs text-muted-foreground">
<span className="text-xs text-muted-foreground ml-[22px]">
Design before you build
</span>
</div>
Expand All @@ -155,13 +181,14 @@ export function ChatModeSelector() {
<SelectItem value="local-agent" disabled={isQuotaExceeded}>
<div className="flex flex-col items-start">
<div className="flex items-center gap-1.5">
<Bot size={14} className="text-muted-foreground" />
<span className="font-medium">Basic Agent</span>
<span className="text-xs text-muted-foreground">
({isQuotaExceeded ? "0" : messagesRemaining}/5 remaining for
today)
</span>
</div>
<span className="text-xs text-muted-foreground">
<span className="text-xs text-muted-foreground ml-[22px]">
{isQuotaExceeded
? "Daily limit reached"
: "Try our AI agent for free"}
Expand All @@ -171,16 +198,22 @@ export function ChatModeSelector() {
)}
<SelectItem value="build">
<div className="flex flex-col items-start">
<span className="font-medium">Build</span>
<span className="text-xs text-muted-foreground">
<div className="flex items-center gap-1.5">
<Hammer size={14} className="text-muted-foreground" />
<span className="font-medium">Build</span>
</div>
<span className="text-xs text-muted-foreground ml-[22px]">
Generate and edit code
</span>
</div>
</SelectItem>
<SelectItem value="ask">
<div className="flex flex-col items-start">
<span className="font-medium">Ask</span>
<span className="text-xs text-muted-foreground">
<div className="flex items-center gap-1.5">
<MessageCircle size={14} className="text-purple-500" />
<span className="font-medium">Ask</span>
</div>
<span className="text-xs text-muted-foreground ml-[22px]">
Ask questions about the app
</span>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/components/McpToolsPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ export function McpToolsPicker() {
return (
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger
className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground h-8 px-2"
className="inline-flex items-center justify-center whitespace-nowrap rounded-lg text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border-none bg-transparent shadow-none text-muted-foreground hover:text-foreground hover:bg-muted/60 h-7 px-1.5 cursor-pointer"
data-testid="mcp-tools-button"
title="Tools"
>
<Wrench className="size-4" />
<Wrench className="size-3.5" />
</PopoverTrigger>
<PopoverContent
className="w-120 max-h-[80vh] overflow-y-auto"
Expand Down
6 changes: 4 additions & 2 deletions src/components/ModelPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,14 +166,16 @@ export function ModelPicker() {
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger
className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground h-8 max-w-[130px] px-1.5 text-xs-sm gap-2"
className="inline-flex items-center justify-center whitespace-nowrap rounded-lg text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border-none bg-transparent shadow-none text-foreground/80 hover:text-foreground hover:bg-muted/60 h-7 max-w-[130px] px-2 gap-1.5 cursor-pointer"
data-testid="model-picker"
title={modelDisplayName}
>
<span className="truncate">
{modelDisplayName === "Auto" && (
<>
<span className="text-xs text-muted-foreground">Model:</span>{" "}
<span className="text-xs text-muted-foreground/70">
Model:
</span>{" "}
</>
)}
{modelDisplayName}
Expand Down
6 changes: 3 additions & 3 deletions src/components/ProModeSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,11 @@ export function ProModeSelector() {
<Tooltip>
<TooltipTrigger
render={
<PopoverTrigger className="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-primary/50 bg-background shadow-sm hover:bg-primary/10 h-8 px-1.5 gap-1.5 shadow-primary/10 hover:shadow-md hover:shadow-primary/15" />
<PopoverTrigger className="inline-flex items-center justify-center whitespace-nowrap rounded-lg text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border-none bg-transparent shadow-none text-primary/95 hover:text-primary hover:bg-primary/10 h-7 px-2 gap-1 cursor-pointer" />
}
>
<Sparkles className="h-4 w-4 text-primary" />
<span className="text-primary font-medium text-xs-sm">Pro</span>
<Sparkles className="h-3.5 w-3.5" />
<span className="font-medium">Pro</span>
</TooltipTrigger>
<TooltipContent>Configure Dyad Pro settings</TooltipContent>
</Tooltip>
Expand Down
2 changes: 1 addition & 1 deletion src/components/chat/AuxiliaryActionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export function AuxiliaryActionsMenu({
<>
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<DropdownMenuTrigger
className="inline-flex items-center justify-center whitespace-nowrap rounded-xl text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-muted bg-primary/10 text-primary cursor-pointer h-8 px-2"
className="inline-flex items-center justify-center whitespace-nowrap rounded-full text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 hover:bg-primary/20 hover:scale-105 bg-primary/10 text-primary cursor-pointer h-8 w-8 mb-1"
data-testid="auxiliary-actions-menu"
>
<Plus
Expand Down
20 changes: 12 additions & 8 deletions src/components/chat/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ import { useCountTokens } from "@/hooks/useCountTokens";
import { useChats } from "@/hooks/useChats";
import { useRouter } from "@tanstack/react-router";
import { showError as showErrorToast } from "@/lib/toast";
import { cn } from "@/lib/utils";

const showTokenBarAtom = atom(false);

Expand Down Expand Up @@ -424,7 +425,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
{t("errorLoadingProposal", { message: proposalError.message })}
</div>
)}
<div className="p-4" data-testid="chat-input-container">
<div className="p-2 pt-0" data-testid="chat-input-container">
{/* Show context limit banner above chat input for visibility */}
{showBanner && tokenCountResult && (
<ContextLimitBanner
Expand All @@ -433,9 +434,12 @@ export function ChatInput({ chatId }: { chatId?: number }) {
/>
)}
<div
className={`relative flex flex-col border border-border rounded-lg bg-(--background-lighter) shadow-sm ${
isDraggingOver ? "ring-2 ring-blue-500 border-blue-500" : ""
} ${showBanner ? "rounded-t-none border-t-0" : ""}`}
className={cn(
"relative flex flex-col border border-border rounded-2xl bg-(--background-lighter) transition-colors duration-200",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The Tailwind CSS class bg-(--background-lighter) uses invalid syntax for arbitrary values. It should be bg-[--background-lighter] to correctly reference the CSS variable. This will ensure it's parsed correctly by Tailwind and avoid potential issues with tooling or future updates.

Suggested change
"relative flex flex-col border border-border rounded-2xl bg-(--background-lighter) transition-colors duration-200",
"relative flex flex-col border border-border rounded-2xl bg-[--background-lighter] transition-colors duration-200",

"focus-within:border-primary/30 focus-within:ring-1 focus-within:ring-primary/20",
isDraggingOver && "ring-2 ring-blue-500 border-blue-500",
showBanner && "rounded-t-none border-t-0",
Comment on lines +438 to +441

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 CSS specificity: focus-within ring overrides drag-over ring indicator

When the chat input is focused (which is nearly always the case) and a user drags a file over the input, the drag-and-drop visual indicator (blue ring-2 + border-blue-500) is overridden by the focus-within ring styles because pseudo-class variants have higher CSS specificity than plain utility classes.

Root Cause

The new classes focus-within:border-primary/30 focus-within:ring-1 focus-within:ring-primary/20 are always active when the text input has focus. The drag indicator classes ring-2 ring-blue-500 border-blue-500 are conditionally applied when isDraggingOver is true. Because :focus-within pseudo-class variants have higher CSS specificity than bare utilities, focus-within:ring-1 wins over ring-2, and focus-within:border-primary/30 wins over border-blue-500.

The same issue exists in src/components/chat/HomeChatInput.tsx:89-93.

Previously (before this PR), there were no focus-within ring styles on the container, so the drag-over blue ring always displayed correctly.

Actual: During drag-over while focused, user sees a subtle 1px primary-colored ring instead of the intended prominent 2px blue ring.

Expected: The drag-over indicator (2px blue ring/border) should always be visible regardless of focus state, as it's a temporary high-priority visual signal.

Impact: Drag-and-drop file attachment visual feedback is degraded in the most common scenario (input is focused), making it harder for users to see the drop target.

Suggested change
"relative flex flex-col border border-border rounded-2xl bg-(--background-lighter) transition-colors duration-200",
"focus-within:border-primary/30 focus-within:ring-1 focus-within:ring-primary/20",
isDraggingOver && "ring-2 ring-blue-500 border-blue-500",
showBanner && "rounded-t-none border-t-0",
"relative flex flex-col border border-border rounded-2xl bg-(--background-lighter) transition-colors duration-200",
!isDraggingOver && "focus-within:border-primary/30 focus-within:ring-1 focus-within:ring-primary/20",
isDraggingOver && "ring-2 ring-blue-500 border-blue-500",
showBanner && "rounded-t-none border-t-0",
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

)}
Comment on lines +437 to +442

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM — CSS specificity conflict: focus-within ring vs drag-over ring

2/3 agents flagged this (both MEDIUM)

When isDraggingOver is true, ring-2 ring-blue-500 border-blue-500 is applied alongside the always-present focus-within:ring-1 focus-within:ring-primary/20. If the user is typing (input focused) and drags a file over, both ring styles activate simultaneously. The focus-within ring color (ring-primary/20) may bleed through or conflict with the drag-over blue ring, producing an inconsistent visual.

The same pattern exists in HomeChatInput.tsx.

Suggested fix: Conditionally omit the focus-within ring when dragging:

Suggested change
className={cn(
"relative flex flex-col border border-border rounded-2xl bg-(--background-lighter) transition-colors duration-200",
"focus-within:border-primary/30 focus-within:ring-1 focus-within:ring-primary/20",
isDraggingOver && "ring-2 ring-blue-500 border-blue-500",
showBanner && "rounded-t-none border-t-0",
)}
className={cn(
"relative flex flex-col border border-border rounded-2xl bg-(--background-lighter) transition-colors duration-200",
!isDraggingOver && "focus-within:border-primary/30 focus-within:ring-1 focus-within:ring-primary/20",
isDraggingOver && "ring-2 ring-blue-500 border-blue-500",
showBanner && "rounded-t-none border-t-0",
)}

onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
Expand Down Expand Up @@ -566,7 +570,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
onCancel={cancelPendingFiles}
/>

<div className="flex items-start space-x-2 ">
<div className="flex items-end gap-1">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM | interaction-design | Found by: UX, Endorsed by: Correctness

items-end alignment — verify send button aligns with single-line text

The input row changed from flex items-start space-x-2 to flex items-end gap-1, and buttons changed from mt-1 to mb-0.5. This anchors the send/cancel buttons to the bottom of the container, which is the right call for multi-line inputs (button stays near the last line). However, for a single-line input, the button icon center may not perfectly align with the text baseline.

💡 Suggestion: Verify visually with a single line of text that the send icon aligns cleanly. May need mb-1 or a small padding adjustment.

<LexicalChatInput
value={inputValue}
onChange={setInputValue}
Expand All @@ -585,7 +589,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
<button
onClick={handleCancel}
aria-label={t("cancelGeneration")}
className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg"
className="px-2 py-2 mb-0.5 mr-1 text-muted-foreground hover:text-destructive rounded-lg transition-colors duration-150 cursor-pointer"
/>
}
>
Expand All @@ -604,7 +608,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
disableSendButton
}
aria-label={t("sendMessage")}
className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50"
className="px-2 py-2 mb-0.5 mr-1 text-muted-foreground hover:text-primary rounded-lg transition-colors duration-150 disabled:opacity-30 disabled:hover:text-muted-foreground cursor-pointer disabled:cursor-default"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM | accessibility | Found by: UX Wizard, Endorsed by: Correctness, Code Health

disabled:opacity-30 may fail WCAG 2.1 contrast requirements

The disabled send button uses disabled:opacity-30 (down from disabled:opacity-50). Combined with the already-muted text-muted-foreground color, the resulting contrast ratio likely falls below the WCAG 2.1 SC 1.4.11 minimum of 3:1 for non-text UI components. Users may not realize the send button exists when disabled.

The same pattern appears in HomeChatInput.tsx.

💡 Suggestion: Use disabled:opacity-40 or disabled:opacity-50 to maintain sufficient visibility. Test contrast against WCAG 3:1 minimum on both light and dark themes.

/>
}
>
Expand All @@ -614,7 +618,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
</Tooltip>
)}
</div>
<div className="pl-2 pr-1 flex items-center justify-between pb-2">
<div className="px-2 flex items-center justify-between pb-0.5 pt-0.5">
<div className="flex items-center">
<ChatInputControls showContextFilesPicker={false} />
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/chat/ContextLimitBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export function ContextLimitBanner({

return (
<div
className="mx-auto max-w-3xl px-3 py-1.5 rounded-t-md border-t border-l border-r border-amber-500/30 bg-amber-500/10 flex items-center justify-between gap-3 text-xs text-amber-600 dark:text-amber-500"
className="mx-auto max-w-3xl px-3 py-1.5 rounded-t-2xl border-t border-l border-r border-amber-500/30 bg-amber-500/10 flex items-center justify-between gap-3 text-xs text-amber-600 dark:text-amber-500"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM | visual / layout | Found by: Correctness Expert, Endorsed by: UX Wizard

ContextLimitBanner width constraint mismatches ChatInput container

2/3 agents flagged this (1 MEDIUM, 1 LOW)

The ContextLimitBanner uses max-w-3xl mx-auto (768px, centered), but the ChatInput wrapper div below it has no max-w-3xl constraint. When the banner is shown, the ChatInput applies rounded-t-none border-t-0 to visually join with the banner above. However, if the parent container is wider than 768px, the banner will be narrower and centered while the input stretches full width — the side borders won't align, breaking the seamless visual connection.

The previous rounded-t-md had the same structural issue, but the larger rounded-t-2xl radius makes the misalignment more noticeable.

Additionally, the focus-within:ring-1 on the ChatInput will still render on the top edge even when border-t-0 is set, creating a subtle colored line where the banner and input should seamlessly join.

💡 Suggestion: Either add max-w-3xl mx-auto to the ChatInput wrapper when the banner is shown, or remove it from the ContextLimitBanner. Also consider conditionally disabling the focus-within ring on the top edge when showBanner is true.

data-testid="context-limit-banner"
>
<span className="flex items-center gap-1.5">
Expand Down
22 changes: 13 additions & 9 deletions src/components/chat/HomeChatInput.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SendIcon, StopCircleIcon } from "lucide-react";
import { SendHorizontalIcon, StopCircleIcon } from "lucide-react";
import {
Tooltip,
TooltipTrigger,
Expand All @@ -20,6 +20,7 @@ import { LexicalChatInput } from "./LexicalChatInput";
import { useChatModeToggle } from "@/hooks/useChatModeToggle";
import { useTypingPlaceholder } from "@/hooks/useTypingPlaceholder";
import { AuxiliaryActionsMenu } from "./AuxiliaryActionsMenu";
import { cn } from "@/lib/utils";

export function HomeChatInput({
onSubmit,
Expand Down Expand Up @@ -85,9 +86,12 @@ export function HomeChatInput({
<>
<div className="p-4" data-testid="home-chat-input-container">
<div
className={`relative flex flex-col space-y-2 border border-border rounded-lg bg-(--background-lighter) shadow-sm ${
isDraggingOver ? "ring-2 ring-blue-500 border-blue-500" : ""
}`}
className={cn(
"relative flex flex-col border border-border rounded-2xl bg-(--background-lighter) transition-colors duration-200",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The Tailwind CSS class bg-(--background-lighter) uses invalid syntax for arbitrary values. It should be bg-[--background-lighter] to correctly reference the CSS variable. This will ensure it's parsed correctly by Tailwind and avoid potential issues with tooling or future updates.

Suggested change
"relative flex flex-col border border-border rounded-2xl bg-(--background-lighter) transition-colors duration-200",
"relative flex flex-col border border-border rounded-2xl bg-[--background-lighter] transition-colors duration-200",

"hover:border-primary/30",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM | consistency | Found by: Code Health, Endorsed by: UX

hover:border-primary/30 present on HomeChatInput but missing from ChatInput

HomeChatInput adds a hover:border-primary/30 class on the container that ChatInput does not have. These two components provide the same chat input experience in different contexts (home page vs chat page), so users will see different hover behavior depending on where they are.

💡 Suggestion: Either add hover:border-primary/30 to ChatInput's container as well for consistency, or remove it from HomeChatInput. If the difference is intentional (home page input should be more discoverable), add a brief comment explaining why.

"focus-within:border-primary/30 focus-within:ring-1 focus-within:ring-primary/20",
isDraggingOver && "ring-2 ring-blue-500 border-blue-500",
)}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
Comment on lines 86 to 96

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM | correctness / visual regression | Found by: Correctness, Endorsed by: UX

space-y-2 removed without replacement

The old code used space-y-2 on the input container to create vertical spacing between the text area row and the controls row. The new code removes it without adding an equivalent gap, relying solely on the children's own padding. Verify this doesn't collapse the spacing between the input area and the controls bar — the ChatInput.tsx version doesn't have this issue because it never had space-y-2.

onDrop={handleDrop}
Expand All @@ -108,7 +112,7 @@ export function HomeChatInput({
onCancel={cancelPendingFiles}
/>

<div className="flex items-start space-x-2 ">
<div className="flex items-end gap-1">
<LexicalChatInput
value={inputValue}
onChange={setInputValue}
Expand All @@ -127,7 +131,7 @@ export function HomeChatInput({
render={
<button
aria-label="Cancel generation (unavailable here)"
className="px-2 py-2 mt-1 mr-1 text-(--sidebar-accent-fg) rounded-lg opacity-50 cursor-not-allowed"
className="px-2 py-2 mb-0.5 mr-1 text-muted-foreground rounded-lg opacity-50 cursor-not-allowed transition-colors duration-150"
/>
}
>
Expand All @@ -145,17 +149,17 @@ export function HomeChatInput({
onClick={handleCustomSubmit}
disabled={!inputValue.trim() && attachments.length === 0}
aria-label="Send message"
className="px-2 py-2 mt-1 mr-1 hover:bg-(--background-darkest) text-(--sidebar-accent-fg) rounded-lg disabled:opacity-50"
className="px-2 py-2 mb-0.5 mr-1 text-muted-foreground hover:text-primary rounded-lg transition-colors duration-150 disabled:opacity-30 disabled:hover:text-muted-foreground cursor-pointer disabled:cursor-default"
/>
}
>
<SendIcon size={20} />
<SendHorizontalIcon size={20} />
</TooltipTrigger>
<TooltipContent>Send message</TooltipContent>
</Tooltip>
)}
</div>
<div className="pl-2 pr-1 flex items-center justify-between pb-2">
<div className="px-2 flex items-center justify-between pb-0.5 pt-0.5">
<div className="flex items-center">
<ChatInputControls showContextFilesPicker={false} />
</div>
Expand Down
Loading
Loading