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
56 changes: 52 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,38 @@ 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="submit"
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:3649

The collapsed mobile send button (lines 3649-3659) allows submission when activePendingProgress exists but canAdvance is false. It only checks composerSendState.hasSendableContent, which doesn't validate whether the user has selected an option or provided a valid custom answer. This lets users submit incomplete responses that would be blocked in the expanded view (lines 3951-3979). Consider disabling the button when activePendingProgress exists and !activePendingProgress.canAdvance.

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

The collapsed mobile send button (lines 3649-3659) allows submission when `activePendingProgress` exists but `canAdvance` is false. It only checks `composerSendState.hasSendableContent`, which doesn't validate whether the user has selected an option or provided a valid custom answer. This lets users submit incomplete responses that would be blocked in the expanded view (lines 3951-3979). Consider disabling the button when `activePendingProgress` exists and `!activePendingProgress.canAdvance`.

Evidence trail:
apps/web/src/components/ChatView.tsx:3649-3659 (collapsed mobile button disabled condition), apps/web/src/components/ChatView.tsx:3967-3971 (expanded view button disabled condition), apps/web/src/pendingUserInput.ts:121 (canAdvance definition), apps/web/src/components/ChatView.tsx:2809-2819 (onAdvanceActivePendingUserInput function), apps/web/src/components/ChatView.tsx:2353-2355 (onSend calls onAdvanceActivePendingUserInput when activePendingProgress exists)

className="flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/90 text-primary-foreground disabled:opacity-30"
disabled={
isSendBusy || isConnecting || !composerSendState.hasSendableContent
}
aria-label="Send message"
>
<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 +3680,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 +3786,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