From f8b324dde0ba25e93dcb7a0182be1aa9a05458c0 Mon Sep 17 00:00:00 2001 From: Dylan Martin Date: Tue, 28 Jul 2026 19:37:14 -0700 Subject: [PATCH 1/6] Clarify diff view controls Generated-By: PostHog Code Task-Id: 4a2adf0e-b339-400e-a390-5e08ceab8265 --- packages/ui/src/features/command/CommandMenu.tsx | 2 +- packages/ui/src/features/command/keyboard-shortcuts.ts | 2 +- .../src/features/task-detail/components/TaskHeaderActions.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/features/command/CommandMenu.tsx b/packages/ui/src/features/command/CommandMenu.tsx index bae485110f..e6f082a7a8 100644 --- a/packages/ui/src/features/command/CommandMenu.tsx +++ b/packages/ui/src/features/command/CommandMenu.tsx @@ -340,7 +340,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { ? [ { id: "open-review-panel", - label: "Open review panel", + label: "Open diff view", icon: ( ), diff --git a/packages/ui/src/features/command/keyboard-shortcuts.ts b/packages/ui/src/features/command/keyboard-shortcuts.ts index ddc82d1172..55ad5d9458 100644 --- a/packages/ui/src/features/command/keyboard-shortcuts.ts +++ b/packages/ui/src/features/command/keyboard-shortcuts.ts @@ -194,7 +194,7 @@ export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [ { id: "toggle-review-panel", keys: SHORTCUTS.TOGGLE_REVIEW_PANEL, - description: "Toggle review panel", + description: "Toggle diff view", category: "navigation", }, { diff --git a/packages/ui/src/features/task-detail/components/TaskHeaderActions.tsx b/packages/ui/src/features/task-detail/components/TaskHeaderActions.tsx index d69a198bd9..b1b6eb4000 100644 --- a/packages/ui/src/features/task-detail/components/TaskHeaderActions.tsx +++ b/packages/ui/src/features/task-detail/components/TaskHeaderActions.tsx @@ -113,7 +113,7 @@ function TaskDiffStatsBadge({ task }: { task: Task }) { useDiffStatsToggle(task, "split"); return ( From 84101b30ad13d600a49d4c1e2a66779721595144 Mon Sep 17 00:00:00 2001 From: Dylan Martin Date: Tue, 28 Jul 2026 19:38:56 -0700 Subject: [PATCH 2/6] Allow closing empty cloud panels Generated-By: PostHog Code Task-Id: 4a2adf0e-b339-400e-a390-5e08ceab8265 --- .../panels/components/LeafNodeRenderer.tsx | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx b/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx index 7da0b54eb8..501d5b371a 100644 --- a/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx +++ b/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx @@ -1,6 +1,14 @@ import { Cloud as CloudIcon } from "@phosphor-icons/react"; +import { + Button, + Empty, + EmptyContent, + EmptyDescription, + EmptyHeader, + EmptyMedia, + EmptyTitle, +} from "@posthog/quill"; import type { Task } from "@posthog/shared/domain-types"; -import { Flex, Text } from "@radix-ui/themes"; import type React from "react"; import { useMemo } from "react"; import { useHostCapabilities } from "../../../shell/useHostCapabilities"; @@ -56,25 +64,45 @@ export const LeafNodeRenderer: React.FC = ({ const activeTabId = tabs.some((t) => t.id === node.content.activeTabId) ? node.content.activeTabId : (tabs[0]?.id ?? node.content.activeTabId); + const hiddenTabIds = useMemo( + () => + node.content.tabs + .filter((tab) => !tabs.some((visibleTab) => visibleTab.id === tab.id)) + .map((tab) => tab.id), + [node.content.tabs, tabs], + ); const cloudEmptyState = useMemo( () => isCloud ? ( - - - - - Cloud runs are read-only - - - + + + + + + Cloud runs are read-only + + Local workspace tools are unavailable for this run. + + + {hiddenTabIds.length > 0 && ( + + + + )} + ) : undefined, - [isCloud], + [closeTab, hiddenTabIds, isCloud, node.id, taskId], ); const contentWithComponents = { From 9cfa8422ca377b88fae3a20a4b4d463a9c581c63 Mon Sep 17 00:00:00 2001 From: Dylan Martin Date: Tue, 28 Jul 2026 21:32:15 -0700 Subject: [PATCH 3/6] Prevent unusable panel splits Generated-By: PostHog Code Task-Id: 4a2adf0e-b339-400e-a390-5e08ceab8265 --- .../panels/components/LeafNodeRenderer.tsx | 34 ++++++++----------- .../panels/components/TabbedPanel.tsx | 17 ++++++++-- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx b/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx index 501d5b371a..689c2da0fb 100644 --- a/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx +++ b/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx @@ -1,8 +1,6 @@ import { Cloud as CloudIcon } from "@phosphor-icons/react"; import { - Button, Empty, - EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, @@ -85,24 +83,9 @@ export const LeafNodeRenderer: React.FC = ({ Local workspace tools are unavailable for this run. - {hiddenTabIds.length > 0 && ( - - - - )} ) : undefined, - [closeTab, hiddenTabIds, isCloud, node.id, taskId], + [isCloud], ); const contentWithComponents = { @@ -124,7 +107,20 @@ export const LeafNodeRenderer: React.FC = ({ draggingTabId={draggingTabId} draggingTabPanelId={draggingTabPanelId} onAddTerminal={hideTerminal ? undefined : () => onAddTerminal(node.id)} - onSplitPanel={(direction) => onSplitPanel(node.id, direction)} + onSplitPanel={ + tabs.length > 1 + ? (direction) => onSplitPanel(node.id, direction) + : undefined + } + onClosePanel={ + tabs.length === 0 && hiddenTabIds.length > 0 + ? () => { + for (const tabId of hiddenTabIds) { + closeTab(taskId, node.id, tabId); + } + } + : undefined + } emptyState={cloudEmptyState} /> ); diff --git a/packages/ui/src/features/panels/components/TabbedPanel.tsx b/packages/ui/src/features/panels/components/TabbedPanel.tsx index d34ad048ed..7694868340 100644 --- a/packages/ui/src/features/panels/components/TabbedPanel.tsx +++ b/packages/ui/src/features/panels/components/TabbedPanel.tsx @@ -1,5 +1,5 @@ import { useDroppable } from "@dnd-kit/react"; -import { Plus, SquareSplitHorizontalIcon } from "@phosphor-icons/react"; +import { Plus, SquareSplitHorizontalIcon, X } from "@phosphor-icons/react"; import { useHostTRPCClient } from "@posthog/host-router/react"; import { PanelDropZones } from "@posthog/ui/features/panels/components/PanelDropZones"; import type { SplitDirection } from "@posthog/ui/features/panels/panelLayoutStore"; @@ -67,6 +67,7 @@ interface TabbedPanelProps { draggingTabPanelId?: string | null; onAddTerminal?: () => void; onSplitPanel?: (direction: SplitDirection) => void; + onClosePanel?: () => void; rightContent?: React.ReactNode; emptyState?: React.ReactNode; } @@ -84,6 +85,7 @@ export const TabbedPanel: React.FC = ({ draggingTabPanelId = null, onAddTerminal, onSplitPanel, + onClosePanel, rightContent, emptyState, }) => { @@ -230,12 +232,23 @@ export const TabbedPanel: React.FC = ({ )} - {(rightContent || (content.droppable && onSplitPanel)) && ( + {(rightContent || onClosePanel || + (content.droppable && onSplitPanel)) && ( {rightContent} + {onClosePanel && ( + + + + + + )} {content.droppable && onSplitPanel && ( Date: Tue, 28 Jul 2026 21:42:55 -0700 Subject: [PATCH 4/6] Keep panel splitting available for local runs Generated-By: PostHog Code Task-Id: 4a2adf0e-b339-400e-a390-5e08ceab8265 --- .../ui/src/features/panels/components/LeafNodeRenderer.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx b/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx index 689c2da0fb..12d6979817 100644 --- a/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx +++ b/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx @@ -108,9 +108,7 @@ export const LeafNodeRenderer: React.FC = ({ draggingTabPanelId={draggingTabPanelId} onAddTerminal={hideTerminal ? undefined : () => onAddTerminal(node.id)} onSplitPanel={ - tabs.length > 1 - ? (direction) => onSplitPanel(node.id, direction) - : undefined + isCloud ? undefined : (direction) => onSplitPanel(node.id, direction) } onClosePanel={ tabs.length === 0 && hiddenTabIds.length > 0 From 5f052dcae6ed3d9e3b606f1fb921e5709d308ee9 Mon Sep 17 00:00:00 2001 From: Dylan Martin Date: Tue, 28 Jul 2026 21:50:56 -0700 Subject: [PATCH 5/6] Format panel close control Generated-By: PostHog Code Task-Id: 4a2adf0e-b339-400e-a390-5e08ceab8265 --- .../ui/src/features/panels/components/TabbedPanel.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/features/panels/components/TabbedPanel.tsx b/packages/ui/src/features/panels/components/TabbedPanel.tsx index 7694868340..fbb00aa51e 100644 --- a/packages/ui/src/features/panels/components/TabbedPanel.tsx +++ b/packages/ui/src/features/panels/components/TabbedPanel.tsx @@ -232,7 +232,8 @@ export const TabbedPanel: React.FC = ({ )} - {(rightContent || onClosePanel || + {(rightContent || + onClosePanel || (content.droppable && onSplitPanel)) && ( = ({ {rightContent} {onClosePanel && ( - + From aeef48ae6737d43c8f6f991a1b7c1ade9525cdbe Mon Sep 17 00:00:00 2001 From: Dylan Martin Date: Tue, 28 Jul 2026 22:20:56 -0700 Subject: [PATCH 6/6] Disable cloud panel drag splitting Generated-By: PostHog Code Task-Id: 4a2adf0e-b339-400e-a390-5e08ceab8265 --- .../panels/components/LeafNodeRenderer.tsx | 16 +++++++----- .../panels/components/TabbedPanel.test.tsx | 26 ++++++++++++++++++- .../panels/components/TabbedPanel.tsx | 9 ++++--- 3 files changed, 40 insertions(+), 11 deletions(-) diff --git a/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx b/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx index 12d6979817..50c42f887c 100644 --- a/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx +++ b/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx @@ -62,13 +62,14 @@ export const LeafNodeRenderer: React.FC = ({ const activeTabId = tabs.some((t) => t.id === node.content.activeTabId) ? node.content.activeTabId : (tabs[0]?.id ?? node.content.activeTabId); - const hiddenTabIds = useMemo( - () => - node.content.tabs - .filter((tab) => !tabs.some((visibleTab) => visibleTab.id === tab.id)) - .map((tab) => tab.id), - [node.content.tabs, tabs], - ); + const hiddenTabIds = useMemo(() => { + const visibleTabIds = new Set(tabs.map((tab) => tab.id)); + const hiddenIds: string[] = []; + for (const tab of node.content.tabs) { + if (!visibleTabIds.has(tab.id)) hiddenIds.push(tab.id); + } + return hiddenIds; + }, [node.content.tabs, tabs]); const cloudEmptyState = useMemo( () => @@ -106,6 +107,7 @@ export const LeafNodeRenderer: React.FC = ({ onPanelFocus={onPanelFocus} draggingTabId={draggingTabId} draggingTabPanelId={draggingTabPanelId} + allowPanelSplit={!isCloud} onAddTerminal={hideTerminal ? undefined : () => onAddTerminal(node.id)} onSplitPanel={ isCloud ? undefined : (direction) => onSplitPanel(node.id, direction) diff --git a/packages/ui/src/features/panels/components/TabbedPanel.test.tsx b/packages/ui/src/features/panels/components/TabbedPanel.test.tsx index 9ba602d936..33ad0313a2 100644 --- a/packages/ui/src/features/panels/components/TabbedPanel.test.tsx +++ b/packages/ui/src/features/panels/components/TabbedPanel.test.tsx @@ -3,6 +3,10 @@ import { render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import type { PanelContent } from "../panelTypes"; +const panelDropZonesSpy = vi.hoisted(() => + vi.fn((_props: { allowSplit?: boolean }) => null), +); + vi.mock("@dnd-kit/react", () => ({ useDroppable: () => ({ ref: vi.fn() }), })); @@ -16,7 +20,7 @@ vi.mock("@posthog/host-router/react", () => ({ })); vi.mock("./PanelDropZones", () => ({ - PanelDropZones: () => null, + PanelDropZones: panelDropZonesSpy, })); vi.mock("./PanelTab", () => ({ @@ -52,6 +56,26 @@ function content(activeTabId: string): PanelContent { } describe("TabbedPanel", () => { + it("disables split drop zones when panel splitting is unavailable", () => { + const droppableContent = { ...content("logs"), droppable: true }; + + render( + + + , + ); + + expect(panelDropZonesSpy.mock.lastCall?.[0]).toEqual( + expect.objectContaining({ allowSplit: false }), + ); + }); + it("retains visited tabs within a task and resets them for another task", () => { const { rerender } = render( diff --git a/packages/ui/src/features/panels/components/TabbedPanel.tsx b/packages/ui/src/features/panels/components/TabbedPanel.tsx index fbb00aa51e..3c096ccb88 100644 --- a/packages/ui/src/features/panels/components/TabbedPanel.tsx +++ b/packages/ui/src/features/panels/components/TabbedPanel.tsx @@ -65,6 +65,7 @@ interface TabbedPanelProps { onPanelFocus?: (panelId: string) => void; draggingTabId?: string | null; draggingTabPanelId?: string | null; + allowPanelSplit?: boolean; onAddTerminal?: () => void; onSplitPanel?: (direction: SplitDirection) => void; onClosePanel?: () => void; @@ -83,6 +84,7 @@ export const TabbedPanel: React.FC = ({ onPanelFocus, draggingTabId = null, draggingTabPanelId = null, + allowPanelSplit = true, onAddTerminal, onSplitPanel, onClosePanel, @@ -307,11 +309,12 @@ export const TabbedPanel: React.FC = ({ panelId={panelId} isDragging={!!draggingTabId} allowSplit={ - // Allow split if: + allowPanelSplit && + // Within a splittable layout, allow the edge drop zones if: // 1. Current panel has > 1 tab (same-panel split), OR // 2. Dragging from a different panel (cross-panel split) - content.tabs.length > 1 || - (draggingTabPanelId !== null && draggingTabPanelId !== panelId) + (content.tabs.length > 1 || + (draggingTabPanelId !== null && draggingTabPanelId !== panelId)) } /> )}