From 62d4c2a33fef9209b5659dbc41a86ceb297994f9 Mon Sep 17 00:00:00 2001 From: Ammar Date: Mon, 25 May 2026 16:17:23 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20refactor:=20encode=20chat=20layo?= =?UTF-8?q?ut=20lanes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Centralize transcript-tail and composer-decoration layout policy behind semantic lane wrappers. Require layout chrome entries to use lane-specific factories so future banners must explicitly choose whether they belong inside the transcript scrollport or in stable composer chrome. --- _Generated with `mux` • Model: `openai:gpt-5.5` • Thinking: `xhigh` • Cost: `$0.56`_ --- src/browser/components/ChatPane/ChatPane.tsx | 108 +++++++------ .../ChatPane/LayoutStackLane.test.tsx | 146 +++++++----------- .../components/ChatPane/LayoutStackLane.tsx | 90 +++++++---- .../components/ChatPane/layoutStack.ts | 34 +++- 4 files changed, 215 insertions(+), 163 deletions(-) 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: