diff --git a/src/browser/components/ChatPane/ChatPane.tsx b/src/browser/components/ChatPane/ChatPane.tsx index 12a7b27cdc..df834a58de 100644 --- a/src/browser/components/ChatPane/ChatPane.tsx +++ b/src/browser/components/ChatPane/ChatPane.tsx @@ -20,8 +20,13 @@ import { EditCutoffBarrier } from "@/browser/features/Messages/ChatBarrier/EditC import { StreamingBarrier } from "@/browser/features/Messages/ChatBarrier/StreamingBarrier"; import { RetryBarrier } from "@/browser/features/Messages/ChatBarrier/RetryBarrier"; import { PinnedTodoList } from "../PinnedTodoList/PinnedTodoList"; -import { LayoutStackLane } from "./LayoutStackLane"; -import type { LayoutStackItem } from "./layoutStack"; +import { ChatInputDecorationStackLane, TranscriptTailStackLane } from "./LayoutStackLane"; +import { + createChatInputDecorationStackItem, + createTranscriptTailStackItem, + type ChatInputDecorationStackItem, + type TranscriptTailStackItem, +} from "./layoutStack"; import { VIM_ENABLED_KEY } from "@/common/constants/storage"; import { ChatInput, type ChatInputAPI } from "@/browser/features/ChatInput/index"; import type { QueueDispatchMode } from "@/browser/features/ChatInput/types"; @@ -912,42 +917,48 @@ const ChatPaneContent: React.FC = (props) => { interruptedBarrierMessageIds.add(message.id); } } - const transcriptTailItems: LayoutStackItem[] = []; + const transcriptTailItems: TranscriptTailStackItem[] = []; if (shouldMountRetryBarrier) { - transcriptTailItems.push({ - key: "retry-barrier", - node: , - }); + transcriptTailItems.push( + createTranscriptTailStackItem({ + key: "retry-barrier", + node: , + }) + ); } if (shouldShowStreamingBarrier) { - transcriptTailItems.push({ - key: "streaming-barrier", - node: ( - - ), - }); + transcriptTailItems.push( + createTranscriptTailStackItem({ + key: "streaming-barrier", + node: ( + + ), + }) + ); } if (shouldShowQueuedAgentTaskPrompt) { - transcriptTailItems.push({ - key: "queued-agent-prompt", - node: ( -
-
-
Queued
- + transcriptTailItems.push( + createTranscriptTailStackItem({ + key: "queued-agent-prompt", + node: ( +
+
+
Queued
+ +
-
- ), - }); + ), + }) + ); } const handleLoadOlderHistory = useCallback(() => { if (!shouldRenderLoadOlderMessagesButton || loadingOlderHistory) { @@ -1190,12 +1201,9 @@ const ChatPaneContent: React.FC = (props) => { )} -
@@ -1314,9 +1322,13 @@ const ChatInputPane: React.FC = (props) => { // Keep optional banners/warnings on one shared lane so the seam right above the textarea is // owned by a single component boundary. That lets hydration reserve only the volatile // workspace-specific decoration stack instead of the whole composer pane. - const decorationEntries: LayoutStackItem[] = []; + const decorationEntries: ChatInputDecorationStackItem[] = []; + const addDecorationEntry = (entry: { key: string; node: React.ReactNode }) => { + decorationEntries.push(createChatInputDecorationStackItem(entry)); + }; + if (props.shouldShowCompactionWarning) { - decorationEntries.push({ + addDecorationEntry({ key: "compaction-warning", node: ( = (props) => { }); } if (props.contextSwitchWarning) { - decorationEntries.push({ + addDecorationEntry({ key: "context-switch-warning", node: ( = (props) => { // visibly flashed while another local agent was active. Pin it with composer decorations // instead; new transcript rows no longer move the warning. if (props.concurrentLocalStreamingWorkspaceName) { - decorationEntries.push({ + addDecorationEntry({ key: "concurrent-local-warning", node: ( = (props) => { } if (props.shouldShowPinnedTodoList) { - decorationEntries.push({ + addDecorationEntry({ key: "pinned-todo-list", node: , }); } - decorationEntries.push({ + addDecorationEntry({ key: "background-processes", node: , }); // The Chat Instructions decoration is intentionally self-gating: it renders // nothing when the scratchpad is empty or disabled, so it can always be in // the decoration lane without affecting layout for users who don't use it. - decorationEntries.push({ + addDecorationEntry({ key: "chat-instructions", node: , }); if (props.shouldShowReviewsBanner) { - decorationEntries.push({ + addDecorationEntry({ key: "reviews-banner", node: , }); } if (props.queuedMessage) { - decorationEntries.push({ + addDecorationEntry({ key: "queued-message", node: ( = (props) => { }); } if (props.isQueuedAgentTask) { - decorationEntries.push({ + addDecorationEntry({ key: "queued-agent-task", node: (
@@ -1404,11 +1416,9 @@ const ChatInputPane: React.FC = (props) => { return ( <> - void) | null = null; let originalResizeObserver: typeof ResizeObserver | undefined; const resizeCallbacks = new Map(); +const COMPOSER_STACK_COMPONENT = "ChatInputDecorationStack"; +const TRANSCRIPT_TAIL_STACK_COMPONENT = "TranscriptTailStack"; class ResizeObserverMock implements ResizeObserver { public readonly callback: ResizeObserverCallback; @@ -99,12 +106,16 @@ async function waitForResizeObservation(target: Element): Promise { }); } -function createTextItem(key: string, text: string): LayoutStackItem { - return { key, node:
{text}
}; +function createTextItem(key: string, text: string): ChatInputDecorationStackItem { + return createChatInputDecorationStackItem({ key, node:
{text}
}); } -function createHiddenItem(key = "idle-decoration"): LayoutStackItem { - return { key, node: