Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ interface DraggableTabProps {
isActive: boolean;
index: number;
closeable?: boolean;
isPreview?: boolean;
onSelect: () => void;
onClose?: () => void;
onCloseOthers?: () => void;
onCloseToRight?: () => void;
onKeep?: () => void;
icon?: React.ReactNode;
badge?: React.ReactNode;
hasUnsavedChanges?: boolean;
Expand All @@ -31,10 +33,12 @@ export const DraggableTab: React.FC<DraggableTabProps> = ({
isActive,
index,
closeable = true,
isPreview,
onSelect,
onClose,
onCloseOthers,
onCloseToRight,
onKeep,
icon,
badge,
hasUnsavedChanges,
Expand All @@ -50,6 +54,12 @@ export const DraggableTab: React.FC<DraggableTabProps> = ({
data: { tabId, panelId, type: "tab" },
});

const handleDoubleClick = useCallback(() => {
if (isPreview) {
onKeep?.();
}
}, [isPreview, onKeep]);

const handleContextMenu = useCallback(
async (e: React.MouseEvent) => {
e.preventDefault();
Expand Down Expand Up @@ -112,6 +122,7 @@ export const DraggableTab: React.FC<DraggableTabProps> = ({
minWidth: "60px",
}}
onClick={onSelect}
onDoubleClick={handleDoubleClick}
onContextMenu={handleContextMenu}
onMouseEnter={(e) => {
if (!isActive) {
Expand All @@ -130,6 +141,10 @@ export const DraggableTab: React.FC<DraggableTabProps> = ({
<Text
size="1"
className="max-w-[200px] select-none overflow-hidden text-ellipsis whitespace-nowrap"
style={{
fontStyle: isPreview ? "italic" : "normal",
opacity: isPreview ? 0.7 : 1,
}}
>
{label}
</Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface LeafNodeRendererProps {
closeTab: (taskId: string, panelId: string, tabId: string) => void;
closeOtherTabs: (panelId: string, tabId: string) => void;
closeTabsToRight: (panelId: string, tabId: string) => void;
keepTab: (panelId: string, tabId: string) => void;
draggingTabId: string | null;
draggingTabPanelId: string | null;
onActiveTabChange: (panelId: string, tabId: string) => void;
Expand All @@ -28,6 +29,7 @@ export const LeafNodeRenderer: React.FC<LeafNodeRendererProps> = ({
closeTab,
closeOtherTabs,
closeTabsToRight,
keepTab,
draggingTabId,
draggingTabPanelId,
onActiveTabChange,
Expand Down Expand Up @@ -56,6 +58,7 @@ export const LeafNodeRenderer: React.FC<LeafNodeRendererProps> = ({
onActiveTabChange={onActiveTabChange}
onCloseOtherTabs={closeOtherTabs}
onCloseTabsToRight={closeTabsToRight}
onKeepTab={keepTab}
onPanelFocus={onPanelFocus}
draggingTabId={draggingTabId}
draggingTabPanelId={draggingTabPanelId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ const PanelLayoutRenderer: React.FC<{
[layoutState, taskId],
);

const handleKeepTab = useCallback(
(panelId: string, tabId: string) => {
layoutState.keepTab(taskId, panelId, tabId);
},
[layoutState, taskId],
);

const handlePanelFocus = useCallback(
(panelId: string) => {
layoutState.setFocusedPanel(taskId, panelId);
Expand Down Expand Up @@ -116,6 +123,7 @@ const PanelLayoutRenderer: React.FC<{
closeTab={layoutState.closeTab}
closeOtherTabs={handleCloseOtherTabs}
closeTabsToRight={handleCloseTabsToRight}
keepTab={handleKeepTab}
draggingTabId={layoutState.draggingTabId}
draggingTabPanelId={layoutState.draggingTabPanelId}
onActiveTabChange={handleSetActiveTab}
Expand Down Expand Up @@ -147,6 +155,7 @@ const PanelLayoutRenderer: React.FC<{
handleSetActiveTab,
handleCloseOtherTabs,
handleCloseTabsToRight,
handleKeepTab,
handlePanelFocus,
handleAddTerminal,
handleSplitPanel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ interface PanelTabProps {
index: number;
draggable?: boolean;
closeable?: boolean;
isPreview?: boolean;
onSelect: () => void;
onClose?: () => void;
onCloseOthers?: () => void;
onCloseToRight?: () => void;
onKeep?: () => void;
icon?: React.ReactNode;
badge?: React.ReactNode;
hasUnsavedChanges?: boolean;
Expand All @@ -30,10 +32,12 @@ export const PanelTab: React.FC<PanelTabProps> = ({
index,
draggable = true,
closeable = true,
isPreview,
onSelect,
onClose,
onCloseOthers,
onCloseToRight,
onKeep,
icon,
badge,
hasUnsavedChanges,
Expand All @@ -60,10 +64,12 @@ export const PanelTab: React.FC<PanelTabProps> = ({
isActive={isActive}
index={index}
closeable={closeable}
isPreview={isPreview}
onSelect={onSelect}
onClose={onClose}
onCloseOthers={onCloseOthers}
onCloseToRight={onCloseToRight}
onKeep={onKeep}
icon={icon}
badge={badge}
hasUnsavedChanges={hasUnsavedChanges}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ interface TabbedPanelProps {
onActiveTabChange?: (panelId: string, tabId: string) => void;
onCloseOtherTabs?: (panelId: string, tabId: string) => void;
onCloseTabsToRight?: (panelId: string, tabId: string) => void;
onKeepTab?: (panelId: string, tabId: string) => void;
onPanelFocus?: (panelId: string) => void;
draggingTabId?: string | null;
draggingTabPanelId?: string | null;
Expand All @@ -62,6 +63,7 @@ export const TabbedPanel: React.FC<TabbedPanelProps> = ({
onActiveTabChange,
onCloseOtherTabs,
onCloseTabsToRight,
onKeepTab,
onPanelFocus,
draggingTabId = null,
draggingTabPanelId = null,
Expand Down Expand Up @@ -163,6 +165,7 @@ export const TabbedPanel: React.FC<TabbedPanelProps> = ({
index={index}
draggable={tab.draggable}
closeable={tab.closeable !== false}
isPreview={tab.isPreview}
onSelect={() => {
onActiveTabChange?.(panelId, tab.id);
onPanelFocus?.(panelId);
Expand All @@ -175,6 +178,7 @@ export const TabbedPanel: React.FC<TabbedPanelProps> = ({
}
onCloseOthers={() => onCloseOtherTabs?.(panelId, tab.id)}
onCloseToRight={() => onCloseTabsToRight?.(panelId, tab.id)}
onKeep={() => onKeepTab?.(panelId, tab.id)}
icon={tab.icon}
hasUnsavedChanges={tab.hasUnsavedChanges}
badge={tab.badge}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface PanelLayoutState {
closeTab: (taskId: string, panelId: string, tabId: string) => void;
closeOtherTabs: (taskId: string, panelId: string, tabId: string) => void;
closeTabsToRight: (taskId: string, panelId: string, tabId: string) => void;
keepTab: (taskId: string, panelId: string, tabId: string) => void;
setFocusedPanel: (taskId: string, panelId: string) => void;
addTerminalTab: (taskId: string, panelId: string) => void;
splitPanel: (
Expand All @@ -41,6 +42,7 @@ export function usePanelLayoutState(taskId: string): PanelLayoutState {
closeTab: state.closeTab,
closeOtherTabs: state.closeOtherTabs,
closeTabsToRight: state.closeTabsToRight,
keepTab: state.keepTab,
setFocusedPanel: state.setFocusedPanel,
addTerminalTab: state.addTerminalTab,
splitPanel: state.splitPanel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,4 +513,119 @@ describe("panelLayoutStore", () => {
expect(updatedMainPanel.type).toBe("leaf");
});
});

describe("preview tabs", () => {
beforeEach(() => {
usePanelLayoutStore.getState().initializeTask("task-1");
});

it("creates preview tab by default when opening a file", () => {
usePanelLayoutStore.getState().openFile("task-1", "src/App.tsx");

const panel = findPanelById(getPanelTree("task-1"), "main-panel");
const fileTab = panel?.content.tabs.find(
(t: { id: string }) => t.id === "file-src/App.tsx",
);
expect(fileTab?.isPreview).toBe(true);
});

it("replaces existing preview tab when opening another file", () => {
usePanelLayoutStore.getState().openFile("task-1", "src/App.tsx");
usePanelLayoutStore.getState().openFile("task-1", "src/Other.tsx");

const panel = findPanelById(getPanelTree("task-1"), "main-panel");
const fileTabs = panel?.content.tabs.filter((t: { id: string }) =>
t.id.startsWith("file-"),
);
expect(fileTabs).toHaveLength(1);
expect(fileTabs?.[0].id).toBe("file-src/Other.tsx");
expect(fileTabs?.[0].isPreview).toBe(true);
});

it("creates permanent tab when asPreview is false", () => {
usePanelLayoutStore.getState().openFile("task-1", "src/App.tsx", false);

const panel = findPanelById(getPanelTree("task-1"), "main-panel");
const fileTab = panel?.content.tabs.find(
(t: { id: string }) => t.id === "file-src/App.tsx",
);
expect(fileTab?.isPreview).toBe(false);
});

it("keeps preview tab as preview when re-clicking same file", () => {
usePanelLayoutStore.getState().openFile("task-1", "src/App.tsx");
usePanelLayoutStore.getState().openFile("task-1", "src/App.tsx");

const panel = findPanelById(getPanelTree("task-1"), "main-panel");
const fileTab = panel?.content.tabs.find(
(t: { id: string }) => t.id === "file-src/App.tsx",
);
expect(fileTab?.isPreview).toBe(true);
});

it("pins preview tab when double-clicking (asPreview=false)", () => {
usePanelLayoutStore.getState().openFile("task-1", "src/App.tsx");
usePanelLayoutStore.getState().openFile("task-1", "src/App.tsx", false);

const panel = findPanelById(getPanelTree("task-1"), "main-panel");
const fileTab = panel?.content.tabs.find(
(t: { id: string }) => t.id === "file-src/App.tsx",
);
expect(fileTab?.isPreview).toBe(false);
});

it("keepTab sets isPreview to false", () => {
usePanelLayoutStore.getState().openFile("task-1", "src/App.tsx");
usePanelLayoutStore
.getState()
.keepTab("task-1", "main-panel", "file-src/App.tsx");

const panel = findPanelById(getPanelTree("task-1"), "main-panel");
const fileTab = panel?.content.tabs.find(
(t: { id: string }) => t.id === "file-src/App.tsx",
);
expect(fileTab?.isPreview).toBe(false);
});

it("does not replace non-preview tabs when opening preview", () => {
usePanelLayoutStore.getState().openFile("task-1", "src/App.tsx", false);
usePanelLayoutStore.getState().openFile("task-1", "src/Other.tsx");

const panel = findPanelById(getPanelTree("task-1"), "main-panel");
const fileTabs = panel?.content.tabs.filter((t: { id: string }) =>
t.id.startsWith("file-"),
);
expect(fileTabs).toHaveLength(2);
expect(
fileTabs?.find((t) => t.id === "file-src/App.tsx")?.isPreview,
).toBe(false);
expect(
fileTabs?.find((t) => t.id === "file-src/Other.tsx")?.isPreview,
).toBe(true);
});

it("openDiff creates preview tab by default", () => {
usePanelLayoutStore
.getState()
.openDiff("task-1", "src/App.tsx", "modified");

const panel = findPanelById(getPanelTree("task-1"), "main-panel");
const diffTab = panel?.content.tabs.find((t: { id: string }) =>
t.id.startsWith("diff-"),
);
expect(diffTab?.isPreview).toBe(true);
});

it("openDiff creates permanent tab when asPreview is false", () => {
usePanelLayoutStore
.getState()
.openDiff("task-1", "src/App.tsx", "modified", false);

const panel = findPanelById(getPanelTree("task-1"), "main-panel");
const diffTab = panel?.content.tabs.find((t: { id: string }) =>
t.id.startsWith("diff-"),
);
expect(diffTab?.isPreview).toBe(false);
});
});
});
Loading