Skip to content

Commit 5c33d5c

Browse files
committed
feat(web): collapse mobile composer by default
Collapse the chat composer into a compact mobile preview until the user focuses or taps it, while preserving quick-send behavior for existing prompt content. Signed-off-by: Hiram Chirino <hiram@hiramchirino.com>
1 parent 9e29c9d commit 5c33d5c

1 file changed

Lines changed: 52 additions & 4 deletions

File tree

apps/web/src/components/ChatView.tsx

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ import {
142142
type TerminalContextSelection,
143143
} from "../lib/terminalContext";
144144
import { shouldUseCompactComposerFooter } from "./composerFooterLayout";
145+
import { useMediaQuery } from "../hooks/useMediaQuery";
145146
import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";
146147
import { ComposerPromptEditor, type ComposerPromptEditorHandle } from "./ComposerPromptEditor";
147148
import { PullRequestThreadDialog } from "./PullRequestThreadDialog";
@@ -336,6 +337,9 @@ export default function ChatView({ threadId }: ChatViewProps) {
336337
const [expandedWorkGroups, setExpandedWorkGroups] = useState<Record<string, boolean>>({});
337338
const [planSidebarOpen, setPlanSidebarOpen] = useState(false);
338339
const [isComposerFooterCompact, setIsComposerFooterCompact] = useState(false);
340+
const [isComposerFocused, setIsComposerFocused] = useState(false);
341+
const isMobileViewport = useMediaQuery("max-sm");
342+
const isComposerCollapsedMobile = isMobileViewport && !isComposerFocused;
339343
// Tracks whether the user explicitly dismissed the sidebar for the active turn.
340344
const planSidebarDismissedForTurnRef = useRef<string | null>(null);
341345
// When set, the thread-change reset effect will open the sidebar instead of closing it.
@@ -3586,8 +3590,24 @@ export default function ChatView({ threadId }: ChatViewProps) {
35863590
isDragOverComposer ? "border-primary/70 bg-accent/30" : "border-border",
35873591
composerProviderState.composerSurfaceClassName,
35883592
)}
3593+
onFocusCapture={() => setIsComposerFocused(true)}
3594+
onBlurCapture={(e) => {
3595+
// Only collapse if focus leaves the composer entirely
3596+
if (!e.currentTarget.contains(e.relatedTarget as Node)) {
3597+
setIsComposerFocused(false);
3598+
}
3599+
}}
3600+
onClick={() => {
3601+
if (isComposerCollapsedMobile) {
3602+
// First expand the composer, then focus the editor after it renders
3603+
setIsComposerFocused(true);
3604+
requestAnimationFrame(() => {
3605+
composerEditorRef.current?.focusAtEnd();
3606+
});
3607+
}
3608+
}}
35893609
>
3590-
{activePendingApproval ? (
3610+
{!isComposerCollapsedMobile && (activePendingApproval ? (
35913611
<div className="rounded-t-[19px] border-b border-border/65 bg-muted/20">
35923612
<ComposerPendingApprovalPanel
35933613
approval={activePendingApproval}
@@ -3612,11 +3632,38 @@ export default function ChatView({ threadId }: ChatViewProps) {
36123632
planTitle={proposedPlanTitle(activeProposedPlan.planMarkdown) ?? null}
36133633
/>
36143634
</div>
3615-
) : null}
3635+
) : null)}
3636+
{isComposerCollapsedMobile && (
3637+
<div className="flex items-center justify-between gap-2 px-3 py-2">
3638+
<span className={cn(
3639+
"min-w-0 truncate text-[14px]",
3640+
(activePendingProgress ? activePendingProgress.customAnswer : prompt.trim())
3641+
? "text-foreground"
3642+
: "text-muted-foreground/35",
3643+
)}>
3644+
{activePendingProgress
3645+
? (activePendingProgress.customAnswer || "Type your own answer, or leave this blank to use the selected option")
3646+
: (prompt.trim() || "Ask anything...")}
3647+
</span>
3648+
<button
3649+
type="submit"
3650+
className="flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/90 text-primary-foreground disabled:opacity-30"
3651+
disabled={
3652+
isSendBusy || isConnecting || !composerSendState.hasSendableContent
3653+
}
3654+
aria-label="Send message"
3655+
>
3656+
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
3657+
<path d="M8 3L8 13M8 3L4 7M8 3L12 7" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
3658+
</svg>
3659+
</button>
3660+
</div>
3661+
)}
36163662
<div
36173663
className={cn(
36183664
"relative px-3 pb-2 sm:px-4",
36193665
hasComposerHeader ? "pt-2.5 sm:pt-3" : "pt-3.5 sm:pt-4",
3666+
isComposerCollapsedMobile && "hidden",
36203667
)}
36213668
>
36223669
{composerMenuOpen && !isComposerApprovalState && (
@@ -3633,7 +3680,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
36333680
</div>
36343681
)}
36353682

3636-
{!isComposerApprovalState &&
3683+
{!isComposerCollapsedMobile &&
3684+
!isComposerApprovalState &&
36373685
pendingUserInputs.length === 0 &&
36383686
composerImages.length > 0 && (
36393687
<div className="mb-3 flex flex-wrap gap-2">
@@ -3738,7 +3786,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
37383786
</div>
37393787

37403788
{/* Bottom toolbar */}
3741-
{activePendingApproval ? (
3789+
{isComposerCollapsedMobile ? null : activePendingApproval ? (
37423790
<div className="flex items-center justify-end gap-2 px-2.5 pb-2.5 sm:px-3 sm:pb-3">
37433791
<ComposerPendingApprovalActions
37443792
requestId={activePendingApproval.requestId}

0 commit comments

Comments
 (0)