Skip to content
Open
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
61 changes: 57 additions & 4 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ import {
type TerminalContextSelection,
} from "../lib/terminalContext";
import { shouldUseCompactComposerFooter } from "./composerFooterLayout";
import { useMediaQuery } from "../hooks/useMediaQuery";
import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";
import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "./ComposerPromptEditor";
import { PullRequestThreadDialog } from "./PullRequestThreadDialog";
Expand Down Expand Up @@ -336,6 +337,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>({});
const [planSidebarOpen, setPlanSidebarOpen] = useState(false);
const [isComposerFooterCompact, setIsComposerFooterCompact] = useState(false);
const [isComposerFocused, setIsComposerFocused] = useState(false);
const isMobileViewport = useMediaQuery("max-sm");
const isComposerCollapsedMobile = isMobileViewport && !isComposerFocused;
// Tracks whether the user explicitly dismissed the sidebar for the active turn.
const planSidebarDismissedForTurnRef = useRef<string | null>(null);
// When set, the thread-change reset effect will open the sidebar instead of closing it.
Expand Down Expand Up @@ -3586,8 +3590,24 @@ export default function ChatView({ threadId }: ChatViewProps) {
isDragOverComposer ? "border-primary/70 bg-accent/30" : "border-border",
composerProviderState.composerSurfaceClassName,
)}
onFocusCapture={() => setIsComposerFocused(true)}
onBlurCapture={(e) => {
// Only collapse if focus leaves the composer entirely
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
setIsComposerFocused(false);
}
}}
onClick={() => {
if (isComposerCollapsedMobile) {
// First expand the composer, then focus the editor after it renders
setIsComposerFocused(true);
requestAnimationFrame(() => {
composerEditorRef.current?.focusAtEnd();
});
}
}}
>
{activePendingApproval ? (
{!isComposerCollapsedMobile && (activePendingApproval ? (
<div className="rounded-t-[19px] border-b border-border/65 bg-muted/20">
<ComposerPendingApprovalPanel
approval={activePendingApproval}
Expand All @@ -3612,11 +3632,43 @@ export default function ChatView({ threadId }: ChatViewProps) {
planTitle={proposedPlanTitle(activeProposedPlan.planMarkdown) ?? null}
/>
</div>
) : null}
) : null)}
{isComposerCollapsedMobile && (
<div className="flex items-center justify-between gap-2 px-3 py-2">
<span className={cn(
"min-w-0 truncate text-[14px]",
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
? "text-foreground"
: "text-muted-foreground/35",
)}>
{activePendingProgress
? (activePendingProgress.customAnswer || "Type your own answer, or leave this blank to use the selected option")
: (prompt.trim() || "Ask anything...")}
</span>
<button
type="button"
className="flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/90 text-primary-foreground disabled:opacity-30"
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Medium components/ChatView.tsx:3650

When showPlanFollowUpPrompt is true with an empty prompt, the collapsed mobile send button is incorrectly disabled because !composerSendState.hasSendableContent is true. However, in the expanded view at lines 4015-4047, an empty prompt with showPlanFollowUpPrompt correctly shows an enabled 'Implement' button. The collapsed view's disabled logic doesn't account for this valid state where sending without prompt content is permitted.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/components/ChatView.tsx around line 3650:

When `showPlanFollowUpPrompt` is true with an empty prompt, the collapsed mobile send button is incorrectly disabled because `!composerSendState.hasSendableContent` is `true`. However, in the expanded view at lines 4015-4047, an empty prompt with `showPlanFollowUpPrompt` correctly shows an enabled 'Implement' button. The collapsed view's disabled logic doesn't account for this valid state where sending without prompt content is permitted.

Evidence trail:
- Collapsed mobile send button disabled logic at line 3652: apps/web/src/components/ChatView.tsx (view_range [3640, 3670])
- Expanded view 'Implement' button with showPlanFollowUpPrompt=true and empty prompt at lines 4015-4022: apps/web/src/components/ChatView.tsx (view_range [3995, 4025])
- Expanded view regular send button at line 4049: apps/web/src/components/ChatView.tsx (view_range [4043, 4070])
- grep for 'hasSendableContent' showing only 4 usages, confirming collapsed view doesn't have special handling

disabled={
isSendBusy || isConnecting || !composerSendState.hasSendableContent
}
aria-label="Send message"
onPointerDown={(e) => e.preventDefault()}
onClick={(e) => {
e.stopPropagation();
onSend();
}}
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M8 3L8 13M8 3L4 7M8 3L12 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
</div>
)}
<div
className={cn(
"relative px-3 pb-2 sm:px-4",
hasComposerHeader ? "pt-2.5 sm:pt-3" : "pt-3.5 sm:pt-4",
isComposerCollapsedMobile && "hidden",
)}
>
{composerMenuOpen && !isComposerApprovalState && (
Expand All @@ -3633,7 +3685,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
</div>
)}

{!isComposerApprovalState &&
{!isComposerCollapsedMobile &&
!isComposerApprovalState &&
pendingUserInputs.length === 0 &&
composerImages.length > 0 && (
<div className="mb-3 flex flex-wrap gap-2">
Expand Down Expand Up @@ -3738,7 +3791,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
</div>

{/* Bottom toolbar */}
{activePendingApproval ? (
{isComposerCollapsedMobile ? null : activePendingApproval ? (
<div className="flex items-center justify-end gap-2 px-2.5 pb-2.5 sm:px-3 sm:pb-3">
<ComposerPendingApprovalActions
requestId={activePendingApproval.requestId}
Expand Down
Loading