Skip to content

Commit 2a20757

Browse files
authored
Overlay the composer in fullscreen preview mode (#500)
- Keep the chat composer visible over fullscreen preview - Hide thread controls and add preview status text - Preserve input interaction while preview stays active
1 parent 5daa68c commit 2a20757

1 file changed

Lines changed: 131 additions & 77 deletions

File tree

apps/web/src/components/ChatView.tsx

Lines changed: 131 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -4914,6 +4914,16 @@ export default function ChatView({
49144914
</Suspense>
49154915
</div>
49164916
) : null;
4917+
const isFullscreenPreviewMode = Boolean(
4918+
previewOpen && activeProject && previewLayoutMode === "fullscreen",
4919+
);
4920+
const floatingComposerStatus = isTurnActive
4921+
? `Working for ${formatElapsed(activeWorkStartedAt, nowIso)}`
4922+
: showPlanFollowUpPrompt && activeProposedPlan
4923+
? "Plan ready for follow-up"
4924+
: phase === "disconnected"
4925+
? "Chat offline"
4926+
: "Ready for follow-up";
49174927

49184928
return (
49194929
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-x-hidden bg-background">
@@ -4982,7 +4992,7 @@ export default function ChatView({
49824992
<div
49834993
ref={previewSplitRef}
49844994
className={cn(
4985-
"flex min-h-0 min-w-0 flex-1",
4995+
"relative flex min-h-0 min-w-0 flex-1",
49864996
previewOpen && activeProject && previewLayoutMode === "top"
49874997
? "flex-col"
49884998
: previewOpen && activeProject && previewLayoutMode === "side"
@@ -5051,14 +5061,15 @@ export default function ChatView({
50515061
</div>
50525062
) : null}
50535063

5054-
{/* Chat column — hidden in fullscreen preview mode */}
5064+
{/* Chat column */}
50555065
<div
50565066
className={cn(
50575067
"flex min-h-0 min-w-0 flex-1 flex-col",
5058-
previewOpen && activeProject && previewLayoutMode === "fullscreen" && "hidden",
5068+
isFullscreenPreviewMode &&
5069+
"pointer-events-none absolute inset-x-0 bottom-0 z-20 min-h-0 flex-none",
50595070
)}
50605071
>
5061-
{isMobileCompanion ? (
5072+
{!isFullscreenPreviewMode && isMobileCompanion ? (
50625073
<div className="mx-auto w-full max-w-7xl px-3 pt-3 sm:px-5">
50635074
<MobileThreadAttentionBar
50645075
activePendingApproval={activePendingApproval}
@@ -5073,75 +5084,87 @@ export default function ChatView({
50735084
</div>
50745085
) : null}
50755086
{/* Messages Wrapper */}
5076-
<div className="relative flex min-h-0 flex-1 flex-col">
5077-
{/* Messages */}
5078-
<div
5079-
ref={setMessagesScrollContainerRef}
5080-
className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-y-contain px-3 py-3 sm:px-5 sm:py-4"
5081-
onScroll={onMessagesScroll}
5082-
onClickCapture={onMessagesClickCapture}
5083-
onWheel={onMessagesWheel}
5084-
onPointerDown={onMessagesPointerDown}
5085-
onPointerUp={onMessagesPointerUp}
5086-
onPointerCancel={onMessagesPointerCancel}
5087-
onTouchStart={onMessagesTouchStart}
5088-
onTouchMove={onMessagesTouchMove}
5089-
onTouchEnd={onMessagesTouchEnd}
5090-
onTouchCancel={onMessagesTouchEnd}
5091-
>
5092-
<MessagesTimeline
5093-
threadId={activeThread.id}
5094-
key={activeThread.id}
5095-
hasMessages={timelineEntries.length > 0}
5096-
isWorking={isWorking}
5097-
activeTurnInProgress={isWorking || !latestTurnSettled}
5098-
activeTurnStartedAt={activeWorkStartedAt}
5099-
scrollContainer={messagesScrollElement}
5100-
timelineEntries={timelineEntries}
5101-
completionDividerBeforeEntryId={completionDividerBeforeEntryId}
5102-
completionSummary={completionSummary}
5103-
turnDiffSummaryByAssistantMessageId={turnDiffSummaryByAssistantMessageId}
5104-
nowIso={nowIso}
5105-
expandedWorkGroups={expandedWorkGroups}
5106-
onToggleWorkGroup={onToggleWorkGroup}
5107-
revertTurnCountByUserMessageId={revertTurnCountByUserMessageId}
5108-
onRevertUserMessage={onRevertUserMessage}
5109-
isRevertingCheckpoint={isRevertingCheckpoint}
5110-
onImageExpand={onExpandTimelineImage}
5111-
markdownCwd={gitCwd ?? undefined}
5112-
resolvedTheme={resolvedTheme}
5113-
showReasoningContent={showReasoningContent}
5114-
timestampFormat={timestampFormat}
5115-
workspaceRoot={activeProject?.cwd ?? undefined}
5116-
shortcutGuides={chatShortcutGuides}
5117-
onRemoveQueuedMessage={onRemoveQueuedMessage}
5118-
onOpenSettings={() => void navigate({ to: "/settings" })}
5119-
onOpenTurnDiff={handleOpenTurnDiff}
5120-
/>
5121-
<div ref={messagesBottomRef} aria-hidden="true" className="h-px w-full" />
5122-
</div>
5123-
5124-
{/* scroll to bottom pill — shown when user has scrolled away from the bottom */}
5125-
{showScrollToBottom && (
5126-
<div className="pointer-events-none absolute bottom-1 left-1/2 z-30 flex -translate-x-1/2 justify-center py-1.5">
5127-
<button
5128-
type="button"
5129-
onClick={() => scrollMessagesToBottom("smooth")}
5130-
className="pointer-events-auto flex items-center gap-1.5 rounded-full border border-border/60 bg-card px-3 py-1 text-muted-foreground text-xs shadow-sm transition-colors hover:border-border hover:text-foreground hover:cursor-pointer"
5131-
>
5132-
<ChevronDownIcon className="size-3.5" />
5133-
Scroll to bottom
5134-
</button>
5087+
{!isFullscreenPreviewMode ? (
5088+
<div className="relative flex min-h-0 flex-1 flex-col">
5089+
{/* Messages */}
5090+
<div
5091+
ref={setMessagesScrollContainerRef}
5092+
className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-y-contain px-3 py-3 sm:px-5 sm:py-4"
5093+
onScroll={onMessagesScroll}
5094+
onClickCapture={onMessagesClickCapture}
5095+
onWheel={onMessagesWheel}
5096+
onPointerDown={onMessagesPointerDown}
5097+
onPointerUp={onMessagesPointerUp}
5098+
onPointerCancel={onMessagesPointerCancel}
5099+
onTouchStart={onMessagesTouchStart}
5100+
onTouchMove={onMessagesTouchMove}
5101+
onTouchEnd={onMessagesTouchEnd}
5102+
onTouchCancel={onMessagesTouchEnd}
5103+
>
5104+
<MessagesTimeline
5105+
threadId={activeThread.id}
5106+
key={activeThread.id}
5107+
hasMessages={timelineEntries.length > 0}
5108+
isWorking={isWorking}
5109+
activeTurnInProgress={isWorking || !latestTurnSettled}
5110+
activeTurnStartedAt={activeWorkStartedAt}
5111+
scrollContainer={messagesScrollElement}
5112+
timelineEntries={timelineEntries}
5113+
completionDividerBeforeEntryId={completionDividerBeforeEntryId}
5114+
completionSummary={completionSummary}
5115+
turnDiffSummaryByAssistantMessageId={turnDiffSummaryByAssistantMessageId}
5116+
nowIso={nowIso}
5117+
expandedWorkGroups={expandedWorkGroups}
5118+
onToggleWorkGroup={onToggleWorkGroup}
5119+
revertTurnCountByUserMessageId={revertTurnCountByUserMessageId}
5120+
onRevertUserMessage={onRevertUserMessage}
5121+
isRevertingCheckpoint={isRevertingCheckpoint}
5122+
onImageExpand={onExpandTimelineImage}
5123+
markdownCwd={gitCwd ?? undefined}
5124+
resolvedTheme={resolvedTheme}
5125+
showReasoningContent={showReasoningContent}
5126+
timestampFormat={timestampFormat}
5127+
workspaceRoot={activeProject?.cwd ?? undefined}
5128+
shortcutGuides={chatShortcutGuides}
5129+
onRemoveQueuedMessage={onRemoveQueuedMessage}
5130+
onOpenSettings={() => void navigate({ to: "/settings" })}
5131+
onOpenTurnDiff={handleOpenTurnDiff}
5132+
/>
5133+
<div ref={messagesBottomRef} aria-hidden="true" className="h-px w-full" />
51355134
</div>
5136-
)}
5137-
</div>
5135+
5136+
{/* scroll to bottom pill — shown when user has scrolled away from the bottom */}
5137+
{showScrollToBottom && (
5138+
<div className="pointer-events-none absolute bottom-1 left-1/2 z-30 flex -translate-x-1/2 justify-center py-1.5">
5139+
<button
5140+
type="button"
5141+
onClick={() => scrollMessagesToBottom("smooth")}
5142+
className="pointer-events-auto flex items-center gap-1.5 rounded-full border border-border/60 bg-card px-3 py-1 text-muted-foreground text-xs shadow-sm transition-colors hover:border-border hover:text-foreground hover:cursor-pointer"
5143+
>
5144+
<ChevronDownIcon className="size-3.5" />
5145+
Scroll to bottom
5146+
</button>
5147+
</div>
5148+
)}
5149+
</div>
5150+
) : null}
51385151

51395152
{/* Input bar */}
5140-
<div className={cn("px-3 pt-1.5 sm:px-5 sm:pt-2", isGitRepo ? "pb-1" : "pb-3 sm:pb-4")}>
5153+
<div
5154+
className={cn(
5155+
isFullscreenPreviewMode
5156+
? "pointer-events-auto px-4 pb-4 sm:px-6 sm:pb-6"
5157+
: "px-3 pt-1.5 sm:px-5 sm:pt-2",
5158+
!isFullscreenPreviewMode && (isGitRepo ? "pb-1" : "pb-3 sm:pb-4"),
5159+
)}
5160+
>
51415161
<form
51425162
ref={composerFormRef}
51435163
onSubmit={onSend}
5144-
className="mx-auto w-full min-w-0 max-w-7xl"
5164+
className={cn(
5165+
"w-full min-w-0",
5166+
isFullscreenPreviewMode ? "mx-auto max-w-4xl" : "mx-auto max-w-7xl",
5167+
)}
51455168
data-chat-composer-form="true"
51465169
>
51475170
<input
@@ -5154,8 +5177,10 @@ export default function ChatView({
51545177
/>
51555178
<div
51565179
className={cn(
5157-
"group relative rounded-[22px] p-px transition-colors duration-200",
5158-
composerProviderState.composerFrameClassName,
5180+
"group relative p-px transition-colors duration-200",
5181+
isFullscreenPreviewMode
5182+
? "overflow-hidden rounded-[30px] border border-white/10 bg-[rgba(28,28,32,0.92)] text-white shadow-[0_24px_80px_rgba(0,0,0,0.45)] backdrop-blur-2xl"
5183+
: ["rounded-[22px]", composerProviderState.composerFrameClassName],
51595184
)}
51605185
onDragEnter={onComposerDragEnter}
51615186
onDragOver={onComposerDragOver}
@@ -5179,22 +5204,46 @@ export default function ChatView({
51795204
</div>
51805205
</div>
51815206
)}
5207+
{isFullscreenPreviewMode ? (
5208+
<div className="flex items-center justify-between gap-3 border-white/10 border-b px-4 py-3 text-[12px] text-white/70">
5209+
<span className="truncate font-medium text-white/82">
5210+
{floatingComposerStatus}
5211+
</span>
5212+
<span className="shrink-0 text-white/45">Preview</span>
5213+
</div>
5214+
) : null}
51825215
<div
51835216
className={cn(
5184-
"rounded-[20px] border bg-card transition-colors duration-200 focus-within:border-ring/45",
5185-
isDragOverComposer ? "border-primary/70 bg-accent/30" : "border-border",
5186-
composerProviderState.composerSurfaceClassName,
5217+
isFullscreenPreviewMode
5218+
? "bg-transparent"
5219+
: "rounded-[20px] border bg-card transition-colors duration-200 focus-within:border-ring/45",
5220+
isFullscreenPreviewMode
5221+
? null
5222+
: isDragOverComposer
5223+
? "border-primary/70 bg-accent/30"
5224+
: "border-border",
5225+
!isFullscreenPreviewMode && composerProviderState.composerSurfaceClassName,
51875226
)}
51885227
>
51895228
{activePendingApproval ? (
5190-
<div className="rounded-t-[19px] border-b border-border/65 bg-muted/20">
5229+
<div
5230+
className={cn(
5231+
"border-b border-border/65 bg-muted/20",
5232+
!isFullscreenPreviewMode && "rounded-t-[19px]",
5233+
)}
5234+
>
51915235
<ComposerPendingApprovalPanel
51925236
approval={activePendingApproval}
51935237
pendingCount={pendingApprovals.length}
51945238
/>
51955239
</div>
51965240
) : pendingUserInputs.length > 0 ? (
5197-
<div className="rounded-t-[19px] border-b border-border/65 bg-muted/20">
5241+
<div
5242+
className={cn(
5243+
"border-b border-border/65 bg-muted/20",
5244+
!isFullscreenPreviewMode && "rounded-t-[19px]",
5245+
)}
5246+
>
51985247
<ComposerPendingUserInputPanel
51995248
pendingUserInputs={pendingUserInputs}
52005249
respondingRequestIds={respondingRequestIds}
@@ -5205,7 +5254,12 @@ export default function ChatView({
52055254
/>
52065255
</div>
52075256
) : showPlanFollowUpPrompt && activeProposedPlan ? (
5208-
<div className="rounded-t-[19px] border-b border-border/65 bg-muted/20">
5257+
<div
5258+
className={cn(
5259+
"border-b border-border/65 bg-muted/20",
5260+
!isFullscreenPreviewMode && "rounded-t-[19px]",
5261+
)}
5262+
>
52095263
<ComposerPlanFollowUpBanner
52105264
key={activeProposedPlan.id}
52115265
planTitle={proposedPlanTitle(activeProposedPlan.planMarkdown) ?? null}
@@ -5866,7 +5920,7 @@ export default function ChatView({
58665920
</form>
58675921
</div>
58685922

5869-
{isGitRepo && (
5923+
{!isFullscreenPreviewMode && isGitRepo && (
58705924
<BranchToolbar
58715925
threadId={activeThread.id}
58725926
onEnvModeChange={onEnvModeChange}
@@ -5877,7 +5931,7 @@ export default function ChatView({
58775931
: {})}
58785932
/>
58795933
)}
5880-
{pullRequestDialogState ? (
5934+
{!isFullscreenPreviewMode && pullRequestDialogState ? (
58815935
<PullRequestThreadDialog
58825936
key={pullRequestDialogState.key}
58835937
open

0 commit comments

Comments
 (0)