From f9ec757c64b7698e12bd6bf6b32c4b89aee79dfd Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 2 Dec 2025 19:23:05 +0000 Subject: [PATCH] refactor message stream layout --- dev-docs/architecture.md | 2 +- .../ui/src/components/message-block-list.tsx | 105 +++ ...essage-stream-v2.tsx => message-block.tsx} | 601 +----------------- .../ui/src/components/message-list-header.tsx | 62 ++ .../ui/src/components/message-section.tsx | 412 ++++++++++++ .../src/components/session/session-view.tsx | 4 +- packages/ui/src/styles/messaging.css | 58 +- .../styles/messaging/message-block-list.css | 35 + ...message-stream.css => message-section.css} | 37 -- 9 files changed, 635 insertions(+), 681 deletions(-) create mode 100644 packages/ui/src/components/message-block-list.tsx rename packages/ui/src/components/{message-stream-v2.tsx => message-block.tsx} (54%) create mode 100644 packages/ui/src/components/message-list-header.tsx create mode 100644 packages/ui/src/components/message-section.tsx create mode 100644 packages/ui/src/styles/messaging/message-block-list.css rename packages/ui/src/styles/messaging/{message-stream.css => message-section.css} (78%) diff --git a/dev-docs/architecture.md b/dev-docs/architecture.md index b13f121a..21f94654 100644 --- a/dev-docs/architecture.md +++ b/dev-docs/architecture.md @@ -35,7 +35,7 @@ CodeNomad is a cross-platform desktop application built with Electron that provi │ │ │ UI Components │ │ │ │ │ │ - InstanceTabs │ │ │ │ │ │ - SessionTabs │ │ │ -│ │ │ - MessageStreamV2 │ │ │ +│ │ │ - MessageSection │ │ │ │ │ │ - PromptInput │ │ │ │ │ └────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────┘ │ diff --git a/packages/ui/src/components/message-block-list.tsx b/packages/ui/src/components/message-block-list.tsx new file mode 100644 index 00000000..d6b002f9 --- /dev/null +++ b/packages/ui/src/components/message-block-list.tsx @@ -0,0 +1,105 @@ +import { Index, createEffect, createSignal, type Accessor } from "solid-js" +import VirtualItem from "./virtual-item" +import MessageBlock from "./message-block" +import type { InstanceMessageStore } from "../stores/message-v2/instance-store" + +const VIRTUAL_ITEM_MARGIN_PX = 800 +const ESTIMATED_MESSAGE_HEIGHT = 320 +const INITIAL_FORCE_MIN_ITEMS = 12 +const INITIAL_FORCE_OVERSCAN = 6 + +interface MessageBlockListProps { + instanceId: string + sessionId: string + store: () => InstanceMessageStore + messageIds: () => string[] + messageIndexMap: () => Map + lastAssistantIndex: () => number + showThinking: () => boolean + thinkingDefaultExpanded: () => boolean + showUsageMetrics: () => boolean + scrollContainer: Accessor + loading?: boolean + onRevert?: (messageId: string) => void + onFork?: (messageId?: string) => void + onContentRendered?: () => void + setBottomSentinel: (element: HTMLDivElement | null) => void +} + +export default function MessageBlockList(props: MessageBlockListProps) { + const [initialForceActive, setInitialForceActive] = createSignal(true) + const [initialForceInitialized, setInitialForceInitialized] = createSignal(false) + const [initialForceStartIndex, setInitialForceStartIndex] = createSignal(0) + const [, setInitialForceRemaining] = createSignal(0) + + createEffect(() => { + props.instanceId + props.sessionId + setInitialForceActive(true) + setInitialForceInitialized(false) + setInitialForceStartIndex(0) + setInitialForceRemaining(0) + }) + + createEffect(() => { + if (!initialForceActive() || initialForceInitialized()) return + const ids = props.messageIds() + if (ids.length === 0) return + const viewportHeight = props.scrollContainer()?.clientHeight ?? (typeof window !== "undefined" ? window.innerHeight : 800) + const estimatedCount = Math.min( + ids.length, + Math.max(INITIAL_FORCE_MIN_ITEMS, Math.ceil(viewportHeight / ESTIMATED_MESSAGE_HEIGHT) + INITIAL_FORCE_OVERSCAN), + ) + setInitialForceStartIndex(Math.max(0, ids.length - estimatedCount)) + setInitialForceRemaining(estimatedCount) + setInitialForceInitialized(true) + }) + + return ( + <> + + {(messageId) => { + const messageIndex = () => props.messageIndexMap().get(messageId()) ?? 0 + const forceVisible = () => initialForceActive() && messageIndex() >= initialForceStartIndex() + const handleMeasured = () => { + if (!forceVisible()) return + setInitialForceRemaining((value) => { + const next = value > 0 ? value - 1 : 0 + if (next === 0) { + setInitialForceActive(false) + } + return next + }) + } + return ( + !props.loading} + forceVisible={forceVisible} + onMeasured={handleMeasured} + > + + + ) + }} + +