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
13 changes: 8 additions & 5 deletions src/browser/components/ChatPane/ChatInputDecoration.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { ReactNode } from "react";
import { ChevronDown, ChevronRight } from "lucide-react";
import { cn } from "@/common/lib/utils";
import { CHAT_DOCK_GUTTER_CLASS } from "@/constants/layout";
import { useChatDockColumnWidthClass } from "./chatDockColumn";

interface ChatInputDecorationProps {
expanded: boolean;
Expand All @@ -27,9 +29,11 @@ interface ChatInputDecorationProps {
// drifting again as individual decorations evolve, while `renderExpanded`
// keeps large hidden detail trees out of collapsed rerenders.
export function ChatInputDecoration(props: ChatInputDecorationProps) {
const columnWidthClass = useChatDockColumnWidthClass();

return (
<div
className={cn("bg-surface-primary px-4", props.className)}
className={cn("bg-surface-primary", CHAT_DOCK_GUTTER_CLASS, props.className)}
data-component={props.dataComponent}
>
<button
Expand All @@ -38,7 +42,8 @@ export function ChatInputDecoration(props: ChatInputDecorationProps) {
className={cn(
// Use a fixed collapsed row height so every decoration reads with the
// same top/bottom breathing room regardless of icon/text mix.
"group mx-auto flex h-6 w-full max-w-4xl items-center gap-2 text-xs leading-none transition-colors",
"group flex h-6 items-center gap-2 text-xs leading-none transition-colors",
columnWidthClass,
props.summaryClassName
)}
>
Expand All @@ -53,9 +58,7 @@ export function ChatInputDecoration(props: ChatInputDecorationProps) {
</div>
</button>
{props.expanded && props.renderExpanded && (
<div className={cn("mx-auto max-w-4xl", props.contentClassName)}>
{props.renderExpanded()}
</div>
<div className={cn(columnWidthClass, props.contentClassName)}>{props.renderExpanded()}</div>
)}
</div>
);
Expand Down
186 changes: 102 additions & 84 deletions src/browser/components/ChatPane/ChatPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ import { useSendMessageOptions } from "@/browser/hooks/useSendMessageOptions";
import type { TerminalSessionCreateOptions } from "@/browser/utils/terminal";
import { useAPI } from "@/browser/contexts/API";
import { useChatTranscriptFullWidth } from "@/browser/hooks/useChatTranscriptFullWidth";
import { CHAT_DOCK_GUTTER_CLASS } from "@/constants/layout";
import {
ChatDockColumnProvider,
ChatDockSurface,
useChatDockColumnWidthClass,
} from "./chatDockColumn";
import { useTranscriptDensity } from "@/browser/hooks/useTranscriptDensity";
import { useReviews } from "@/browser/hooks/useReviews";
import { ReviewsBanner } from "../ReviewsBanner/ReviewsBanner";
Expand Down Expand Up @@ -1756,73 +1762,75 @@ const ChatPaneContent: React.FC<ChatPaneContentProps> = (props) => {
of the scrollport never changes with dock height (send-flash
invariant). `bg-surface-primary` keeps transcript content from
showing through gaps between decoration banners. */}
<div
ref={composerDockRef}
data-testid="chat-composer-dock"
className="bg-surface-primary sticky bottom-0 z-10 mx-[-15px] break-normal whitespace-normal"
style={COMPOSER_DOCK_STYLE}
>
{!autoScroll && (
<button
onClick={handleJumpToBottom}
type="button"
// Sit just above the composer dock (8px gap), tracking its live
// height through normal layout instead of a measured offset.
className="assistant-chip font-primary text-foreground hover:assistant-chip-hover absolute bottom-full left-1/2 z-20 mb-2 -translate-x-1/2 cursor-pointer rounded-[20px] px-2 py-1 text-xs font-medium shadow-[0_4px_12px_rgba(0,0,0,0.3)] backdrop-blur-[1px] transition-transform duration-200 hover:scale-105 active:scale-95"
>
Jump to bottom{" "}
<span className="mobile-hide-shortcut-hints">
({formatKeybind(KEYBINDS.JUMP_TO_BOTTOM)})
</span>
</button>
)}
{transcriptOnly ? (
// Transcript-only workspaces keep their historical transcript, but the whole
// composer surface is replaced with a single read-only notice.
<TranscriptOnlyNoticePane />
) : (
<ChatInputPane
kind={meta?.kind}
workspaceId={workspaceId}
projectName={projectName}
workspaceName={workspaceName}
revealDecorations={revealDecorations}
isStreamStarting={isStreamStarting}
isTranscriptCaughtUp={isTranscriptCaughtUp}
runtimeConfig={runtimeConfig}
isPreStreamAgentTask={isPreStreamAgentTask}
preStreamAgentTaskStatus={
meta?.taskStatus === "starting" ? "starting" : "queued"
}
isCompacting={isCompacting}
shouldShowPinnedTodoList={shouldShowPinnedTodoList}
shouldShowReviewsBanner={shouldShowReviewsBanner}
concurrentLocalStreamingWorkspaceName={concurrentLocalStreamingWorkspaceName}
canInterrupt={canInterrupt}
autoCompactionResult={autoCompactionResult}
shouldShowCompactionWarning={shouldShowCompactionWarning}
contextSwitchWarning={contextSwitchWarning}
onContextSwitchCompact={handleContextSwitchCompact}
onContextSwitchDismiss={handleContextSwitchDismiss}
onModelChange={handleModelChange}
onMessageSendStarted={handleMessageSendStarted}
onMessageSent={handleMessageSent}
onResetContext={handleResetContext}
onTruncateHistory={handleClearHistory}
editingMessage={editingMessage}
onCancelEdit={handleCancelEdit}
onEditLastUserMessage={handleEditLastUserMessageClick}
onChatInputReady={handleChatInputReady}
queuedMessage={workspaceState?.queuedMessage ?? null}
onEditQueuedMessage={() => void handleEditQueuedMessage()}
onSendQueuedImmediately={
workspaceState?.canInterrupt ? handleSendQueuedImmediately : undefined
}
reviews={reviews}
onCheckReviews={handleCheckReviews}
/>
)}
</div>
<ChatDockColumnProvider value={chatTranscriptFullWidth}>
<div
ref={composerDockRef}
data-testid="chat-composer-dock"
className="bg-surface-primary sticky bottom-0 z-10 mx-[-15px] break-normal whitespace-normal"
style={COMPOSER_DOCK_STYLE}
>
{!autoScroll && (
<button
onClick={handleJumpToBottom}
type="button"
// Sit just above the composer dock (8px gap), tracking its live
// height through normal layout instead of a measured offset.
className="assistant-chip font-primary text-foreground hover:assistant-chip-hover absolute bottom-full left-1/2 z-20 mb-2 -translate-x-1/2 cursor-pointer rounded-[20px] px-2 py-1 text-xs font-medium shadow-[0_4px_12px_rgba(0,0,0,0.3)] backdrop-blur-[1px] transition-transform duration-200 hover:scale-105 active:scale-95"
>
Jump to bottom{" "}
<span className="mobile-hide-shortcut-hints">
({formatKeybind(KEYBINDS.JUMP_TO_BOTTOM)})
</span>
</button>
)}
{transcriptOnly ? (
// Transcript-only workspaces keep their historical transcript, but the whole
// composer surface is replaced with a single read-only notice.
<TranscriptOnlyNoticePane />
) : (
<ChatInputPane
kind={meta?.kind}
workspaceId={workspaceId}
projectName={projectName}
workspaceName={workspaceName}
revealDecorations={revealDecorations}
isStreamStarting={isStreamStarting}
isTranscriptCaughtUp={isTranscriptCaughtUp}
runtimeConfig={runtimeConfig}
isPreStreamAgentTask={isPreStreamAgentTask}
preStreamAgentTaskStatus={
meta?.taskStatus === "starting" ? "starting" : "queued"
}
isCompacting={isCompacting}
shouldShowPinnedTodoList={shouldShowPinnedTodoList}
shouldShowReviewsBanner={shouldShowReviewsBanner}
concurrentLocalStreamingWorkspaceName={concurrentLocalStreamingWorkspaceName}
canInterrupt={canInterrupt}
autoCompactionResult={autoCompactionResult}
shouldShowCompactionWarning={shouldShowCompactionWarning}
contextSwitchWarning={contextSwitchWarning}
onContextSwitchCompact={handleContextSwitchCompact}
onContextSwitchDismiss={handleContextSwitchDismiss}
onModelChange={handleModelChange}
onMessageSendStarted={handleMessageSendStarted}
onMessageSent={handleMessageSent}
onResetContext={handleResetContext}
onTruncateHistory={handleClearHistory}
editingMessage={editingMessage}
onCancelEdit={handleCancelEdit}
onEditLastUserMessage={handleEditLastUserMessageClick}
onChatInputReady={handleChatInputReady}
queuedMessage={workspaceState?.queuedMessage ?? null}
onEditQueuedMessage={() => void handleEditQueuedMessage()}
onSendQueuedImmediately={
workspaceState?.canInterrupt ? handleSendQueuedImmediately : undefined
}
reviews={reviews}
onCheckReviews={handleCheckReviews}
/>
)}
</div>
</ChatDockColumnProvider>
</PerfRenderMarker>
</div>
{transcriptContextMenu.menu}
Expand All @@ -1833,9 +1841,13 @@ const ChatPaneContent: React.FC<ChatPaneContentProps> = (props) => {
};

const TranscriptOnlyNoticePane: React.FC = () => {
const columnWidthClass = useChatDockColumnWidthClass();

return (
<div className="bg-surface-primary border-border-light border-t px-4 pb-2">
<div className="mx-auto max-w-4xl py-4">
<div
className={cn("bg-surface-primary border-border-light border-t pb-2", CHAT_DOCK_GUTTER_CLASS)}
>
<div className={cn("py-4", columnWidthClass)}>
<p role="note" className="text-muted text-sm leading-6">
{TRANSCRIPT_ONLY_NOTICE}
</p>
Expand Down Expand Up @@ -1901,23 +1913,27 @@ const ChatInputPane: React.FC<ChatInputPaneProps> = (props) => {
addDecorationEntry({
key: "compaction-warning",
node: (
<CompactionWarning
usagePercentage={props.autoCompactionResult.usagePercentage}
thresholdPercentage={props.autoCompactionResult.thresholdPercentage}
isStreaming={props.canInterrupt}
/>
<ChatDockSurface>
<CompactionWarning
usagePercentage={props.autoCompactionResult.usagePercentage}
thresholdPercentage={props.autoCompactionResult.thresholdPercentage}
isStreaming={props.canInterrupt}
/>
</ChatDockSurface>
),
});
}
if (props.contextSwitchWarning) {
addDecorationEntry({
key: "context-switch-warning",
node: (
<ContextSwitchWarningBanner
warning={props.contextSwitchWarning}
onCompact={props.onContextSwitchCompact}
onDismiss={props.onContextSwitchDismiss}
/>
<ChatDockSurface>
<ContextSwitchWarningBanner
warning={props.contextSwitchWarning}
onCompact={props.onContextSwitchCompact}
onDismiss={props.onContextSwitchDismiss}
/>
</ChatDockSurface>
),
});
}
Expand Down Expand Up @@ -1975,11 +1991,13 @@ const ChatInputPane: React.FC<ChatInputPaneProps> = (props) => {
addDecorationEntry({
key: "pre-stream-agent-task",
node: (
<div className="border-border-medium bg-background-secondary text-muted rounded-md border px-3 py-2 text-xs">
{props.preStreamAgentTaskStatus === "starting"
? "This agent task is starting and will become editable after launch accepts the initial prompt."
: "This agent task is queued and will start automatically when a parallel slot is available."}
</div>
<ChatDockSurface>
<div className="border-border-medium bg-background-secondary text-muted rounded-md border px-3 py-2 text-xs">
{props.preStreamAgentTaskStatus === "starting"
? "This agent task is starting and will become editable after launch accepts the initial prompt."
: "This agent task is queued and will start automatically when a parallel slot is available."}
</div>
</ChatDockSurface>
),
});
}
Expand Down
26 changes: 26 additions & 0 deletions src/browser/components/ChatPane/chatDockColumn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createContext, useContext, type ReactNode } from "react";
import { CHAT_DOCK_GUTTER_CLASS } from "@/constants/layout";

// Read from context rather than useChatTranscriptFullWidth because that hook also syncs the backend
// config, and one fetch plus subscription per docked surface would be wasteful.
const ChatDockFullWidthContext = createContext(false);

export const ChatDockColumnProvider = ChatDockFullWidthContext.Provider;

/** Lines a docked surface up with the transcript column in whichever width mode is active. */
export function useChatDockColumnWidthClass(): string {
return useContext(ChatDockFullWidthContext) ? "w-full" : "mx-auto w-full max-w-4xl";
}

/** Gutter plus column for docked surfaces that carry no padding of their own. */
export function ChatDockSurface(props: { children: ReactNode }) {
const columnWidthClass = useChatDockColumnWidthClass();

return (
<div className={CHAT_DOCK_GUTTER_CLASS}>
<div className={columnWidthClass} data-component="ChatDockSurface">
{props.children}
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const CompactionWarning: React.FC<{

return (
<div
className={`mx-4 text-right text-[10px] ${
className={`text-right text-[10px] ${
isUrgent ? "text-plan-mode font-semibold" : "text-muted"
}`}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import React, { useMemo, useSyncExternalStore } from "react";
import { AlertTriangle } from "lucide-react";
import { useWorkspaceContext } from "@/browser/contexts/WorkspaceContext";
import { useWorkspaceStoreRaw } from "@/browser/stores/WorkspaceStore";
import { CHAT_DOCK_GUTTER_CLASS } from "@/constants/layout";
import { useChatDockColumnWidthClass } from "@/browser/components/ChatPane/chatDockColumn";
import { cn } from "@/common/lib/utils";
import { isLocalProjectRuntime } from "@/common/types/runtime";
import type { RuntimeConfig } from "@/common/types/runtime";
Expand Down Expand Up @@ -85,11 +87,13 @@ export const ConcurrentLocalWarningView: React.FC<ConcurrentLocalWarningViewProp
export const ConcurrentLocalWarningDecoration: React.FC<ConcurrentLocalWarningViewProps> = (
props
) => {
const columnWidthClass = useChatDockColumnWidthClass();

return (
<div className="bg-surface-primary px-4 py-1.5">
<div className={cn("bg-surface-primary py-1.5", CHAT_DOCK_GUTTER_CLASS)}>
<ConcurrentLocalWarningView
streamingWorkspaceName={props.streamingWorkspaceName}
className={cn("mx-auto max-w-4xl", props.className)}
className={cn(columnWidthClass, props.className)}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const ContextSwitchWarning: React.FC<Props> = (props) => {
return (
<div
data-testid="context-switch-warning"
className="bg-background-secondary border-plan-mode mx-4 rounded-md border px-4 py-3"
className="bg-background-secondary border-plan-mode rounded-md border px-4 py-3"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
Expand Down
11 changes: 0 additions & 11 deletions src/browser/components/ProjectPage/ProjectPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -290,17 +290,6 @@ export const ProjectPage: React.FC<ProjectPageProps> = ({
{isNonGitRepo && (
<GitInitBanner projectPath={projectPath} onSuccess={handleGitInitSuccess} />
)}
{/* Keep the heading outside the provider branch to prevent layout shifts during setup. */}
<h1
className={cn(
"text-foreground px-1 text-[32px] leading-10 font-semibold",
// Narrow screens have no vertical room to spend on a decorative headline, but it
// is the page's only h1, so keep it for assistive tech instead of removing it.
"[@media(max-width:768px)]:sr-only"
)}
>
Let&rsquo;s get building.
</h1>
{/* Show configure prompt when no providers, otherwise show ChatInput */}
{!providersLoading && !hasProviders ? (
<ConfigureProvidersPrompt />
Expand Down
Loading
Loading