diff --git a/packages/ui/src/features/mcp-apps/components/McpToolView.tsx b/packages/ui/src/features/mcp-apps/components/McpToolView.tsx index fb460f2b3..94057696b 100644 --- a/packages/ui/src/features/mcp-apps/components/McpToolView.tsx +++ b/packages/ui/src/features/mcp-apps/components/McpToolView.tsx @@ -1,17 +1,14 @@ import { Plugs } from "@phosphor-icons/react"; -import { Box, Flex } from "@radix-ui/themes"; -import { useState } from "react"; import { getPostHogExecDisplay, isPostHogExecTool, } from "../../posthog-mcp/utils/posthog-exec-display"; +import { ToolRow } from "../../sessions/components/session-update/ToolRow"; import { + ContentPre, compactInput, - ExpandableIcon, - ExpandedContentBox, formatInput, getContentText, - StatusIndicators, stripCodeFences, ToolTitle, type ToolViewProps, @@ -33,7 +30,6 @@ export function McpToolView({ mcpToolName, expanded = false, }: McpToolViewProps) { - const [isExpanded, setIsExpanded] = useState(expanded); const { status, rawInput, content } = toolCall; const { isLoading, isFailed, wasCancelled, isComplete } = useToolCallStatus( status, @@ -60,52 +56,40 @@ export function McpToolView({ const output = stripCodeFences(getContentText(content) ?? ""); const hasOutput = output.trim().length > 0; - const isExpandable = !!fullInput || hasOutput; + const showOutput = isComplete && hasOutput; - const handleClick = () => { - if (isExpandable) { - setIsExpanded(!isExpanded); - } - }; + const body = + fullInput || showOutput ? ( + <> + {fullInput && {fullInput}} + {showOutput && ( +
+ {output} +
+ )} + + ) : undefined; return ( - - - - - - - - {serverName} - {" - "} - {toolName} - {" (MCP)"} - - {inputPreview && ( - - {inputPreview} - - )} - - - - - {isExpanded && ( - <> - {fullInput && {fullInput}} - {isComplete && hasOutput && ( - {output} - )} - + + {serverName} + {" - "} + {toolName} + {" (MCP)"} + + {inputPreview && ( + + {inputPreview} + )} - + ); } diff --git a/packages/ui/src/features/mcp-servers/components/parts/ToolRow.tsx b/packages/ui/src/features/mcp-servers/components/parts/ToolRow.tsx index 13a743ac4..7ef5cefbd 100644 --- a/packages/ui/src/features/mcp-servers/components/parts/ToolRow.tsx +++ b/packages/ui/src/features/mcp-servers/components/parts/ToolRow.tsx @@ -18,11 +18,7 @@ export function ToolRow({ tool, onChange }: ToolRowProps) { const removed = !!tool.removed_at; return ( -
+
- - {isExpanded && hasDiff && ( - + + ) : undefined + } + > + {filePath && } + {diffStats && ( + + +{diffStats.added}{" "} + -{diffStats.removed} + )} - + ); } diff --git a/packages/ui/src/features/sessions/components/session-update/ExecuteToolView.tsx b/packages/ui/src/features/sessions/components/session-update/ExecuteToolView.tsx index 93451de5a..2217f02bd 100644 --- a/packages/ui/src/features/sessions/components/session-update/ExecuteToolView.tsx +++ b/packages/ui/src/features/sessions/components/session-update/ExecuteToolView.tsx @@ -1,12 +1,9 @@ import { Terminal } from "@phosphor-icons/react"; import { compactHomePath } from "@posthog/shared"; -import { Box, Flex } from "@radix-ui/themes"; -import { useState } from "react"; +import { ToolRow } from "./ToolRow"; import { - ExpandableIcon, - ExpandedContentBox, + ContentPre, getContentText, - StatusIndicators, stripCodeFences, ToolTitle, type ToolViewProps, @@ -28,7 +25,6 @@ export function ExecuteToolView({ turnComplete, expanded = false, }: ToolViewProps) { - const [isExpanded, setIsExpanded] = useState(expanded); const { status, rawInput, content, title } = toolCall; const { isLoading, isFailed, wasCancelled } = useToolCallStatus( status, @@ -46,45 +42,27 @@ export function ExecuteToolView({ "", ); const hasOutput = output.trim().length > 0; - const isExpandable = hasOutput; - - const handleClick = () => { - if (isExpandable) { - setIsExpanded(!isExpanded); - } - }; return ( - - - - - - - {description && {description}} - {command && ( - - - {truncateText(compactHomePath(command), MAX_COMMAND_LENGTH)} - - - )} - - - - - {isExpanded && hasOutput && ( - {output} + {output} : undefined} + > + {description && {description}} + {command && ( + + + {truncateText(compactHomePath(command), MAX_COMMAND_LENGTH)} + + )} - + ); } diff --git a/packages/ui/src/features/sessions/components/session-update/FetchToolView.tsx b/packages/ui/src/features/sessions/components/session-update/FetchToolView.tsx index e8d48c271..6b4a278a5 100644 --- a/packages/ui/src/features/sessions/components/session-update/FetchToolView.tsx +++ b/packages/ui/src/features/sessions/components/session-update/FetchToolView.tsx @@ -1,12 +1,10 @@ import { Globe } from "@phosphor-icons/react"; -import { Box, Flex, Link } from "@radix-ui/themes"; -import { useState } from "react"; +import { Link } from "@radix-ui/themes"; +import { ToolRow } from "./ToolRow"; import { ContentPre, - ExpandableIcon, findResourceLink, getContentText, - StatusIndicators, ToolTitle, type ToolViewProps, truncateText, @@ -20,7 +18,6 @@ export function FetchToolView({ turnCancelled, turnComplete, }: ToolViewProps) { - const [isExpanded, setIsExpanded] = useState(false); const { status, content, title } = toolCall; const { isLoading, isFailed, wasCancelled } = useToolCallStatus( status, @@ -33,61 +30,48 @@ export function FetchToolView({ const hasContent = fetchedContent.trim().length > 0; const url = resourceLink?.uri ?? ""; - const isExpandable = hasContent || url.length > MAX_URL_LENGTH; + const showUrl = url.length > MAX_URL_LENGTH; + const hasBody = hasContent || showUrl; - const handleClick = () => { - if (isExpandable) { - setIsExpanded(!isExpanded); - } - }; + const body = hasBody ? ( + <> + {showUrl && ( +
+ e.stopPropagation()} + > + {url} + +
+ )} + {hasContent && {fetchedContent}} + + ) : undefined; return ( - - - - {title || "Fetch"} - {url && ( - - - {truncateText(url, MAX_URL_LENGTH)} - - - )} - - - - {isExpanded && ( - - {url.length > MAX_URL_LENGTH && ( - - e.stopPropagation()} - > - {url} - - - )} - {hasContent && {fetchedContent}} - + + {title || "Fetch"} + {url && ( + + + {truncateText(url, MAX_URL_LENGTH)} + + )} - + ); } diff --git a/packages/ui/src/features/sessions/components/session-update/FileMentionChip.tsx b/packages/ui/src/features/sessions/components/session-update/FileMentionChip.tsx index 98fcb927a..9da40b2ca 100644 --- a/packages/ui/src/features/sessions/components/session-update/FileMentionChip.tsx +++ b/packages/ui/src/features/sessions/components/session-update/FileMentionChip.tsx @@ -85,7 +85,7 @@ export const FileMentionChip = memo(function FileMentionChip({ - + {filename} {directory && ( diff --git a/packages/ui/src/features/sessions/components/session-update/MoveToolView.tsx b/packages/ui/src/features/sessions/components/session-update/MoveToolView.tsx index 5db42204b..b64d1d4f9 100644 --- a/packages/ui/src/features/sessions/components/session-update/MoveToolView.tsx +++ b/packages/ui/src/features/sessions/components/session-update/MoveToolView.tsx @@ -2,6 +2,7 @@ import { ArrowsLeftRight } from "@phosphor-icons/react"; import { ToolRow } from "./ToolRow"; import { getFilename, + ToolTitle, type ToolViewProps, useToolCallStatus, } from "./toolCallUtils"; @@ -28,15 +29,17 @@ export function MoveToolView({ isFailed={isFailed} wasCancelled={wasCancelled} > - {title || - (sourcePath && destPath ? ( - <> - Move {getFilename(sourcePath)} →{" "} - {getFilename(destPath)} - - ) : ( - "Move file" - ))} + + {title || + (sourcePath && destPath ? ( + <> + Move {getFilename(sourcePath)}{" "} + → {getFilename(destPath)} + + ) : ( + "Move file" + ))} + ); } diff --git a/packages/ui/src/features/sessions/components/session-update/QuestionToolView.tsx b/packages/ui/src/features/sessions/components/session-update/QuestionToolView.tsx index a66110b9f..1649b5909 100644 --- a/packages/ui/src/features/sessions/components/session-update/QuestionToolView.tsx +++ b/packages/ui/src/features/sessions/components/session-update/QuestionToolView.tsx @@ -1,5 +1,5 @@ import { ChatCircle, CheckCircle } from "@phosphor-icons/react"; -import { Box, Flex, Text } from "@radix-ui/themes"; +import { Text } from "@radix-ui/themes"; import { ToolRow } from "./ToolRow"; import { getContentText, @@ -20,33 +20,25 @@ export function QuestionToolView({ ); const answerText = getContentText(content); - - if (!isComplete || !answerText) { - return ( - - {title || "Question"} - - ); - } + const showAnswer = isComplete && !!answerText; return ( - - - - {title || "Question"} - - - - - - {answerText} - - - + + + {answerText} +
+ ) : undefined + } + > + {title || "Question"} + ); } diff --git a/packages/ui/src/features/sessions/components/session-update/ReadToolView.tsx b/packages/ui/src/features/sessions/components/session-update/ReadToolView.tsx index 86a5b8619..744d23edd 100644 --- a/packages/ui/src/features/sessions/components/session-update/ReadToolView.tsx +++ b/packages/ui/src/features/sessions/components/session-update/ReadToolView.tsx @@ -1,14 +1,11 @@ import { FileText } from "@phosphor-icons/react"; import { SafeImagePreview } from "@posthog/ui/primitives/SafeImagePreview"; -import { Box, Flex } from "@radix-ui/themes"; -import { useState } from "react"; import { CodePreview } from "./CodePreview"; import { FileMentionChip } from "./FileMentionChip"; +import { ToolRow } from "./ToolRow"; import { - ExpandableIcon, getContentImage, getReadToolContent, - StatusIndicators, ToolTitle, type ToolViewProps, useToolCallStatus, @@ -19,7 +16,6 @@ export function ReadToolView({ turnCancelled, turnComplete, }: ToolViewProps) { - const [isExpanded, setIsExpanded] = useState(false); const { status, locations, content } = toolCall; const { isLoading, isFailed, wasCancelled } = useToolCallStatus( status, @@ -32,62 +28,39 @@ export function ReadToolView({ const imageContent = getContentImage(content); const fileContent = imageContent ? undefined : getReadToolContent(content); const lineCount = fileContent ? fileContent.split("\n").length : null; - const isExpandable = !!fileContent || !!imageContent; const firstLineNumber = startLine + 1; - const handleClick = () => { - if (isExpandable) { - setIsExpanded(!isExpanded); - } - }; + const body = imageContent ? ( +
+ +
+ ) : fileContent ? ( + + ) : undefined; return ( - - - - - {imageContent - ? "Read image in" - : `Read${lineCount !== null ? ` ${lineCount} lines in` : ""}`} - - {filePath && } - - - - {isExpanded && imageContent && ( - - - - - - )} - - {isExpanded && fileContent && ( - - - - - - )} - + + + {imageContent + ? "Read image in" + : `Read${lineCount !== null ? ` ${lineCount} lines in` : ""}`} + + {filePath && } + ); } diff --git a/packages/ui/src/features/sessions/components/session-update/SearchToolView.tsx b/packages/ui/src/features/sessions/components/session-update/SearchToolView.tsx index d96194ab6..d40d51b25 100644 --- a/packages/ui/src/features/sessions/components/session-update/SearchToolView.tsx +++ b/packages/ui/src/features/sessions/components/session-update/SearchToolView.tsx @@ -1,12 +1,8 @@ import { MagnifyingGlass } from "@phosphor-icons/react"; -import { Box, Flex } from "@radix-ui/themes"; -import { useState } from "react"; import { ToolRow } from "./ToolRow"; import { - ExpandableIcon, - ExpandedContentBox, + ContentPre, getContentText, - StatusIndicators, ToolTitle, type ToolViewProps, useToolCallStatus, @@ -17,7 +13,6 @@ export function SearchToolView({ turnCancelled, turnComplete, }: ToolViewProps) { - const [isExpanded, setIsExpanded] = useState(false); const { status, content, title } = toolCall; const { isLoading, isFailed, wasCancelled } = useToolCallStatus( status, @@ -27,52 +22,28 @@ export function SearchToolView({ const searchResults = getContentText(content) ?? ""; const hasResults = searchResults.trim().length > 0; - const resultLines = hasResults - ? searchResults.split("\n").filter((line) => line.trim().length > 0) - : []; - const resultCount = resultLines.length; - - if (!hasResults) { - return ( - - {title || "Search"} - - ); - } - - const handleClick = () => { - setIsExpanded(!isExpanded); - }; + const resultCount = hasResults + ? searchResults.split("\n").filter((line) => line.trim().length > 0).length + : 0; return ( - - - - - {title || "Search"} - + {searchResults} : undefined + } + > + + {title || "Search"} + + {hasResults && ( {resultCount} {resultCount === 1 ? "result" : "results"} - - - - {isExpanded && {searchResults}} - + )} + ); } diff --git a/packages/ui/src/features/sessions/components/session-update/ThinkToolView.tsx b/packages/ui/src/features/sessions/components/session-update/ThinkToolView.tsx index ae2f33e66..8dd6f666e 100644 --- a/packages/ui/src/features/sessions/components/session-update/ThinkToolView.tsx +++ b/packages/ui/src/features/sessions/components/session-update/ThinkToolView.tsx @@ -1,26 +1,17 @@ -import { - ArrowsInSimpleIcon, - ArrowsOutSimpleIcon, - Brain, -} from "@phosphor-icons/react"; -import { Box, Flex, IconButton, Text } from "@radix-ui/themes"; -import { useState } from "react"; +import { Brain } from "@phosphor-icons/react"; import { ToolRow } from "./ToolRow"; import { + ContentPre, getContentText, - LoadingIcon, type ToolViewProps, useToolCallStatus, } from "./toolCallUtils"; -const COLLAPSED_LINE_COUNT = 5; - export function ThinkToolView({ toolCall, turnCancelled, turnComplete, }: ToolViewProps) { - const [isExpanded, setIsExpanded] = useState(false); const { status, content, title } = toolCall; const { isLoading, isFailed, wasCancelled } = useToolCallStatus( status, @@ -30,71 +21,18 @@ export function ThinkToolView({ const thinkingContent = getContentText(content) ?? ""; const hasContent = thinkingContent.trim().length > 0; - const contentLines = thinkingContent.split("\n"); - const isCollapsible = contentLines.length > COLLAPSED_LINE_COUNT; - const hiddenLineCount = contentLines.length - COLLAPSED_LINE_COUNT; - const displayedContent = isExpanded - ? thinkingContent - : contentLines.slice(0, COLLAPSED_LINE_COUNT).join("\n"); - - if (!hasContent) { - return ( - - {title || "Thinking"} - - ); - } return ( - - - - - - {title || "Thinking"} - - - {isCollapsible && ( - setIsExpanded(!isExpanded)} - > - {isExpanded ? ( - - ) : ( - - )} - - )} - - - - -
-            {displayedContent}
-          
-
- {isCollapsible && !isExpanded && ( - - )} -
-
+ {thinkingContent} : undefined + } + > + {title || "Thinking"} + ); } diff --git a/packages/ui/src/features/sessions/components/session-update/ThoughtView.tsx b/packages/ui/src/features/sessions/components/session-update/ThoughtView.tsx index ae51e476b..cb6252512 100644 --- a/packages/ui/src/features/sessions/components/session-update/ThoughtView.tsx +++ b/packages/ui/src/features/sessions/components/session-update/ThoughtView.tsx @@ -1,67 +1,28 @@ import { Brain } from "@phosphor-icons/react"; -import { Box, Text } from "@radix-ui/themes"; -import { memo, useState } from "react"; -import { ExpandableIcon } from "./toolCallUtils"; +import { memo } from "react"; +import { ToolRow } from "./ToolRow"; +import { ContentPre } from "./toolCallUtils"; interface ThoughtViewProps { content: string; isLoading: boolean; } -const COLLAPSED_LINE_COUNT = 5; - export const ThoughtView = memo(function ThoughtView({ content, isLoading, }: ThoughtViewProps) { - const [isExpanded, setIsExpanded] = useState(false); - const hasContent = content.trim().length > 0; - const contentLines = content.split("\n"); - const isCollapsible = - hasContent && contentLines.length > COLLAPSED_LINE_COUNT; - const hiddenLineCount = contentLines.length - COLLAPSED_LINE_COUNT; - const displayedContent = isExpanded - ? content - : contentLines.slice(0, COLLAPSED_LINE_COUNT).join("\n"); return ( - - - {isExpanded && hasContent && ( - - - -
-                {displayedContent}
-              
-
- {isCollapsible && !isExpanded && ( - - )} -
-
- )} -
+ Thinking + +
); }); diff --git a/packages/ui/src/features/sessions/components/session-update/ToolCallView.tsx b/packages/ui/src/features/sessions/components/session-update/ToolCallView.tsx index 3627d4638..8b00fa482 100644 --- a/packages/ui/src/features/sessions/components/session-update/ToolCallView.tsx +++ b/packages/ui/src/features/sessions/components/session-update/ToolCallView.tsx @@ -15,16 +15,13 @@ import { } from "@phosphor-icons/react"; import { compactHomePath } from "@posthog/shared"; import type { CodeToolKind } from "@posthog/ui/features/sessions/types"; -import { Box, Flex } from "@radix-ui/themes"; -import { useState } from "react"; +import { ToolRow } from "./ToolRow"; import { + ContentPre, compactInput, - ExpandableIcon, - ExpandedContentBox, formatInput, getContentText, getFilename, - StatusIndicators, stripCodeFences, ToolTitle, type ToolViewProps, @@ -69,7 +66,6 @@ export function ToolCallView({ agentToolName, expanded = false, }: ToolCallViewProps) { - const [isExpanded, setIsExpanded] = useState(expanded); const { title, kind, status, locations, content, rawInput } = toolCall; const { isLoading, isFailed, wasCancelled, isComplete } = useToolCallStatus( status, @@ -107,49 +103,36 @@ export function ToolCallView({ const output = stripCodeFences(getContentText(content) ?? ""); const hasOutput = output.trim().length > 0; - const isExpandable = !!fullInput || hasOutput; + const showOutput = isComplete && hasOutput; - const handleClick = () => { - if (isExpandable) { - setIsExpanded(!isExpanded); - } - }; + const body = + fullInput || showOutput ? ( + <> + {fullInput && {fullInput}} + {showOutput && ( +
+ {output} +
+ )} + + ) : undefined; return ( - - - - - - - {displayText} - {inputPreview && ( - - {inputPreview} - - )} - {specialDisplay && {specialDisplay.suffix}} - - - - - {isExpanded && ( - <> - {fullInput && {fullInput}} - {isComplete && hasOutput && ( - {output} - )} - + + {displayText && {displayText}} + {inputPreview && ( + + {inputPreview} + )} - + {specialDisplay && {specialDisplay.suffix}} + ); } diff --git a/packages/ui/src/features/sessions/components/session-update/ToolRow.tsx b/packages/ui/src/features/sessions/components/session-update/ToolRow.tsx index 23015b7a2..8ee9b832d 100644 --- a/packages/ui/src/features/sessions/components/session-update/ToolRow.tsx +++ b/packages/ui/src/features/sessions/components/session-update/ToolRow.tsx @@ -1,28 +1,132 @@ -import type { Icon } from "@phosphor-icons/react"; -import { Flex } from "@radix-ui/themes"; -import type { ReactNode } from "react"; -import { LoadingIcon, StatusIndicators, ToolTitle } from "./toolCallUtils"; +import { Collapsible } from "@base-ui/react/collapsible"; +import { type Icon, WrenchIcon } from "@phosphor-icons/react"; +import { cn } from "@posthog/quill"; +import { type ReactNode, useState } from "react"; +import { + ExpandableIcon, + LoadingIcon, + StatusIndicators, + ToolTitle, +} from "./toolCallUtils"; interface ToolRowProps { - icon: Icon; - isLoading: boolean; + /** Leading tool icon. Ignored when `leading` is provided. */ + icon?: Icon; + isLoading?: boolean; isFailed?: boolean; wasCancelled?: boolean; + /** + * Header content beside the icon. A plain string is wrapped in a ToolTitle; + * pass nodes directly for richer headers (chips, mono spans, stats). + */ children: ReactNode; + /** Collapsible body. When present the row becomes a collapsible trigger. */ + content?: ReactNode; + /** Start expanded (uncontrolled). */ + defaultOpen?: boolean; + /** Controlled open state. Provide together with `onOpenChange`. */ + open?: boolean; + onOpenChange?: (open: boolean) => void; + /** + * Force the collapsible trigger even when `content` is lazily omitted while + * closed (used by the tool-call group, which only renders children open). + */ + collapsible?: boolean; + /** Wrap the content in the standard bordered box. Default true. */ + boxed?: boolean; + /** Override the leading icon slot entirely (e.g. a caret for a group). */ + leading?: ReactNode; + /** Extra header content after the title (e.g. a summary icon strip). */ + trailing?: ReactNode; } +/** + * The single wrapping element for every tool call: a header (icon + text), and + * — when there's a body — a base-ui Collapsible whose content sits in a + * left-padded box. Every tool view and the tool-call group render through this + * so MCP, execute, read, edit, etc. are structurally identical. + */ export function ToolRow({ icon, - isLoading, + isLoading = false, isFailed, wasCancelled, children, + content, + defaultOpen = false, + open, + onOpenChange, + collapsible, + boxed = true, + leading, + trailing, }: ToolRowProps) { - return ( - - - {children} + const [internalOpen, setInternalOpen] = useState(defaultOpen); + const isControlled = open !== undefined; + const isOpen = isControlled ? open : internalOpen; + const setOpen = (next: boolean) => { + if (!isControlled) setInternalOpen(next); + onOpenChange?.(next); + }; + + const isCollapsible = collapsible || content != null; + + const leadingNode = leading ?? ( + + {isCollapsible ? ( + + ) : ( + + )} + + ); + + const header = ( + + {typeof children === "string" ? ( + {children} + ) : ( + children + )} - + {trailing} + + ); + + if (!isCollapsible) { + return ( +
+ {leadingNode} + {header} +
+ ); + } + + return ( + + + {leadingNode} + {header} + + + {content != null && ( +
+ {content} +
+ )} +
+
); } diff --git a/packages/ui/src/features/sessions/components/session-update/toolCallUtils.tsx b/packages/ui/src/features/sessions/components/session-update/toolCallUtils.tsx index 4570e797e..ab24add53 100644 --- a/packages/ui/src/features/sessions/components/session-update/toolCallUtils.tsx +++ b/packages/ui/src/features/sessions/components/session-update/toolCallUtils.tsx @@ -245,7 +245,7 @@ export function ExpandableIcon({ export function ContentPre({ children }: { children: React.ReactNode }) { return ( - +
           {children}
diff --git a/packages/ui/src/features/sessions/components/session-update/useCodePreviewExtensions.ts b/packages/ui/src/features/sessions/components/session-update/useCodePreviewExtensions.ts
index 737418545..5700948da 100644
--- a/packages/ui/src/features/sessions/components/session-update/useCodePreviewExtensions.ts
+++ b/packages/ui/src/features/sessions/components/session-update/useCodePreviewExtensions.ts
@@ -32,7 +32,6 @@ export function useCodePreviewExtensions(
 
 export const CODE_PREVIEW_CONTAINER_STYLE: React.CSSProperties = {
   overflow: "hidden",
-  borderTop: "1px solid var(--gray-6)",
   "--color-background": "transparent",
 } as React.CSSProperties;
 
diff --git a/packages/ui/src/features/sessions/sessionViewStore.ts b/packages/ui/src/features/sessions/sessionViewStore.ts
index 807c5a62e..aa082eecd 100644
--- a/packages/ui/src/features/sessions/sessionViewStore.ts
+++ b/packages/ui/src/features/sessions/sessionViewStore.ts
@@ -4,12 +4,20 @@ interface SessionViewState {
   showRawLogs: boolean;
   searchQuery: string;
   showSearch: boolean;
+  /**
+   * Ephemeral per-tool-group expand overrides for the new thread, keyed by
+   * group id. `true` = expanded, `false` = collapsed, absent = follow the
+   * global collapse mode. Not persisted; wiped when the global mode changes.
+   */
+  groupOverrides: Record;
 }
 
 interface SessionViewActions {
   setShowRawLogs: (show: boolean) => void;
   setSearchQuery: (query: string) => void;
   toggleSearch: () => void;
+  setGroupOverride: (id: string, expanded: boolean) => void;
+  clearGroupOverrides: () => void;
 }
 
 type SessionViewStore = SessionViewState & { actions: SessionViewActions };
@@ -18,6 +26,7 @@ const useStore = create((set) => ({
   showRawLogs: false,
   searchQuery: "",
   showSearch: false,
+  groupOverrides: {},
   actions: {
     setShowRawLogs: (show) => set({ showRawLogs: show }),
     setSearchQuery: (query) => set({ searchQuery: query }),
@@ -26,10 +35,21 @@ const useStore = create((set) => ({
         showSearch: !state.showSearch,
         searchQuery: state.showSearch ? "" : state.searchQuery,
       })),
+    setGroupOverride: (id, expanded) =>
+      set((state) => ({
+        groupOverrides: { ...state.groupOverrides, [id]: expanded },
+      })),
+    clearGroupOverrides: () =>
+      set((state) =>
+        Object.keys(state.groupOverrides).length === 0
+          ? state
+          : { groupOverrides: {} },
+      ),
   },
 }));
 
 export const useShowRawLogs = () => useStore((s) => s.showRawLogs);
 export const useSearchQuery = () => useStore((s) => s.searchQuery);
 export const useShowSearch = () => useStore((s) => s.showSearch);
+export const useGroupOverrides = () => useStore((s) => s.groupOverrides);
 export const useSessionViewActions = () => useStore((s) => s.actions);
diff --git a/packages/ui/src/features/settings/sections/GeneralSettings.tsx b/packages/ui/src/features/settings/sections/GeneralSettings.tsx
index a1abbf40b..6fe05d71a 100644
--- a/packages/ui/src/features/settings/sections/GeneralSettings.tsx
+++ b/packages/ui/src/features/settings/sections/GeneralSettings.tsx
@@ -3,6 +3,10 @@ import { buildPostHogUrl } from "@posthog/core/settings/posthogUrl";
 import { useHostTRPC } from "@posthog/host-router/react";
 import { ANALYTICS_EVENTS } from "@posthog/shared";
 import { useAuthStateValue } from "@posthog/ui/features/auth/store";
+import {
+  COLLAPSE_MODE_OPTIONS,
+  type CollapseMode,
+} from "@posthog/ui/features/sessions/components/new-thread/conversationThreadConfig";
 import { SettingRow } from "@posthog/ui/features/settings/SettingRow";
 import {
   type AutoConvertLongText,
@@ -81,6 +85,7 @@ export function GeneralSettings() {
     defaultReasoningEffort,
     diffOpenMode,
     sendMessagesWith,
+    conversationCollapseMode,
     hedgehogMode,
     setDesktopNotifications,
     setDockBadgeNotifications,
@@ -92,6 +97,7 @@ export function GeneralSettings() {
     setDefaultReasoningEffort,
     setDiffOpenMode,
     setSendMessagesWith,
+    setConversationCollapseMode,
     setHedgehogMode,
   } = useSettingsStore();
 
@@ -205,6 +211,18 @@ export function GeneralSettings() {
     [defaultReasoningEffort, setDefaultReasoningEffort],
   );
 
+  const handleConversationCollapseModeChange = useCallback(
+    (value: CollapseMode) => {
+      track(ANALYTICS_EVENTS.SETTING_CHANGED, {
+        setting_name: "conversation_collapse_mode",
+        new_value: value,
+        old_value: conversationCollapseMode,
+      });
+      setConversationCollapseMode(value);
+    },
+    [conversationCollapseMode, setConversationCollapseMode],
+  );
+
   const handleSendMessagesWithChange = useCallback(
     (value: SendMessagesWith) => {
       track(ANALYTICS_EVENTS.SETTING_CHANGED, {
@@ -493,6 +511,34 @@ export function GeneralSettings() {
         
       
 
+      {/* Conversation */}
+      
+        Conversation
+      
+
+      
+        
+            handleConversationCollapseModeChange(value as CollapseMode)
+          }
+          size="1"
+        >
+          
+          
+            {COLLAPSE_MODE_OPTIONS.map((opt) => (
+              
+                {opt.label}
+              
+            ))}
+          
+        
+      
+
       {/* Power */}
       
         Power
diff --git a/packages/ui/src/features/settings/settingsStore.ts b/packages/ui/src/features/settings/settingsStore.ts
index 4e2e496e5..ae26076e3 100644
--- a/packages/ui/src/features/settings/settingsStore.ts
+++ b/packages/ui/src/features/settings/settingsStore.ts
@@ -1,4 +1,8 @@
 import type { ExecutionMode, WorkspaceMode } from "@posthog/shared";
+import {
+  COLLAPSE_MODE_DEFAULT,
+  type CollapseMode,
+} from "@posthog/ui/features/sessions/components/new-thread/conversationThreadConfig";
 import { electronStorage } from "@posthog/ui/shell/rendererStorage";
 import { create } from "zustand";
 import { persist } from "zustand/middleware";
@@ -119,6 +123,10 @@ interface SettingsStore {
   setTerminalFont: (font: TerminalFont) => void;
   setTerminalCustomFontFamily: (value: string) => void;
 
+  // Conversation thread (new-thread)
+  conversationCollapseMode: CollapseMode;
+  setConversationCollapseMode: (mode: CollapseMode) => void;
+
   // Experimental / misc
   hedgehogMode: boolean;
   mcpAppsDisabledServers: string[];
@@ -228,6 +236,11 @@ export const useSettingsStore = create()(
       setTerminalCustomFontFamily: (value) =>
         set({ terminalCustomFontFamily: value }),
 
+      // Conversation thread (new-thread)
+      conversationCollapseMode: COLLAPSE_MODE_DEFAULT,
+      setConversationCollapseMode: (mode) =>
+        set({ conversationCollapseMode: mode }),
+
       // Experimental / misc
       hedgehogMode: false,
       mcpAppsDisabledServers: [],
@@ -308,6 +321,9 @@ export const useSettingsStore = create()(
         terminalFont: state.terminalFont,
         terminalCustomFontFamily: state.terminalCustomFontFamily,
 
+        // Conversation thread (new-thread)
+        conversationCollapseMode: state.conversationCollapseMode,
+
         // Experimental / misc
         hedgehogMode: state.hedgehogMode,
         mcpAppsDisabledServers: state.mcpAppsDisabledServers,