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/panels/components/LeafNodeRenderer.tsx b/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx index 7da0b54eb8..50c42f887c 100644 --- a/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx +++ b/packages/ui/src/features/panels/components/LeafNodeRenderer.tsx @@ -1,6 +1,12 @@ import { Cloud as CloudIcon } from "@phosphor-icons/react"; +import { + Empty, + 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,23 +62,29 @@ 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(() => { + 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( () => isCloud ? ( - - - - - Cloud runs are read-only - - - + + + + + + Cloud runs are read-only + + Local workspace tools are unavailable for this run. + + + ) : undefined, [isCloud], ); @@ -95,8 +107,20 @@ export const LeafNodeRenderer: React.FC = ({ onPanelFocus={onPanelFocus} draggingTabId={draggingTabId} draggingTabPanelId={draggingTabPanelId} + allowPanelSplit={!isCloud} onAddTerminal={hideTerminal ? undefined : () => onAddTerminal(node.id)} - onSplitPanel={(direction) => onSplitPanel(node.id, direction)} + onSplitPanel={ + isCloud ? undefined : (direction) => onSplitPanel(node.id, direction) + } + 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.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 d34ad048ed..3c096ccb88 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"; @@ -65,8 +65,10 @@ interface TabbedPanelProps { onPanelFocus?: (panelId: string) => void; draggingTabId?: string | null; draggingTabPanelId?: string | null; + allowPanelSplit?: boolean; onAddTerminal?: () => void; onSplitPanel?: (direction: SplitDirection) => void; + onClosePanel?: () => void; rightContent?: React.ReactNode; emptyState?: React.ReactNode; } @@ -82,8 +84,10 @@ export const TabbedPanel: React.FC = ({ onPanelFocus, draggingTabId = null, draggingTabPanelId = null, + allowPanelSplit = true, onAddTerminal, onSplitPanel, + onClosePanel, rightContent, emptyState, }) => { @@ -230,12 +234,21 @@ export const TabbedPanel: React.FC = ({ )} - {(rightContent || (content.droppable && onSplitPanel)) && ( + {(rightContent || + onClosePanel || + (content.droppable && onSplitPanel)) && ( {rightContent} + {onClosePanel && ( + + + + + + )} {content.droppable && onSplitPanel && ( = ({ 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)) } /> )} 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 (