diff --git a/apps/code/src/renderer/components/GlobalEventHandlers.tsx b/apps/code/src/renderer/components/GlobalEventHandlers.tsx index 67a51367f..8fb59afc2 100644 --- a/apps/code/src/renderer/components/GlobalEventHandlers.tsx +++ b/apps/code/src/renderer/components/GlobalEventHandlers.tsx @@ -1,6 +1,6 @@ +import { useReviewNavigationStore } from "@features/code-review/stores/reviewNavigationStore"; import { useFolders } from "@features/folders/hooks/useFolders"; import { usePanelLayoutStore } from "@features/panels/store/panelLayoutStore"; -import { useRightSidebarStore } from "@features/right-sidebar"; import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore"; import { useSidebarData } from "@features/sidebar/hooks/useSidebarData"; import { useVisualTaskOrder } from "@features/sidebar/hooks/useVisualTaskOrder"; @@ -46,7 +46,12 @@ export function GlobalEventHandlers({ const { data: workspaces = {} } = useWorkspaces(); const clearAllLayouts = usePanelLayoutStore((state) => state.clearAllLayouts); const toggleLeftSidebar = useSidebarStore((state) => state.toggle); - const toggleRightSidebar = useRightSidebarStore((state) => state.toggle); + const setReviewMode = useReviewNavigationStore( + (state) => state.setReviewMode, + ); + const getReviewMode = useReviewNavigationStore( + (state) => state.getReviewMode, + ); const currentTaskId = view.type === "task-detail" ? view.data?.id : undefined; const { workspace: currentWorkspace, handleToggleFocus } = useFocusWorkspace( @@ -163,8 +168,14 @@ export function GlobalEventHandlers({ useHotkeys(SHORTCUTS.SETTINGS, handleOpenSettings, globalOptions); useHotkeys(SHORTCUTS.GO_BACK, goBack, globalOptions); useHotkeys(SHORTCUTS.GO_FORWARD, goForward, globalOptions); + const handleToggleReview = useCallback(() => { + if (!currentTaskId) return; + const mode = getReviewMode(currentTaskId); + setReviewMode(currentTaskId, mode === "closed" ? "split" : "closed"); + }, [currentTaskId, getReviewMode, setReviewMode]); + useHotkeys(SHORTCUTS.TOGGLE_LEFT_SIDEBAR, toggleLeftSidebar, globalOptions); - useHotkeys(SHORTCUTS.TOGGLE_RIGHT_SIDEBAR, toggleRightSidebar, globalOptions); + useHotkeys(SHORTCUTS.TOGGLE_REVIEW_PANEL, handleToggleReview, globalOptions); useHotkeys(SHORTCUTS.SHORTCUTS_SHEET, onToggleShortcutsSheet, globalOptions); useHotkeys(SHORTCUTS.INBOX, navigateToInbox, globalOptions); useHotkeys(SHORTCUTS.PREV_TASK, handlePrevTask, globalOptions, [ diff --git a/apps/code/src/renderer/components/HeaderRow.tsx b/apps/code/src/renderer/components/HeaderRow.tsx index 90df2ee26..d8ddeb943 100644 --- a/apps/code/src/renderer/components/HeaderRow.tsx +++ b/apps/code/src/renderer/components/HeaderRow.tsx @@ -1,7 +1,6 @@ +import { DiffStatsBadge } from "@features/code-review/components/DiffStatsBadge"; import { CloudGitInteractionHeader } from "@features/git-interaction/components/CloudGitInteractionHeader"; import { GitInteractionHeader } from "@features/git-interaction/components/GitInteractionHeader"; -import { RightSidebarTrigger } from "@features/right-sidebar/components/RightSidebarTrigger"; -import { useRightSidebarStore } from "@features/right-sidebar/stores/rightSidebarStore"; import { SidebarTrigger } from "@features/sidebar/components/SidebarTrigger"; import { useSidebarStore } from "@features/sidebar/stores/sidebarStore"; import { useWorkspace } from "@features/workspace/hooks/useWorkspace"; @@ -24,19 +23,9 @@ export function HeaderRow() { const isResizing = useSidebarStore((state) => state.isResizing); const setIsResizing = useSidebarStore((state) => state.setIsResizing); - const rightSidebarOpen = useRightSidebarStore((state) => state.open); - const rightSidebarWidth = useRightSidebarStore((state) => state.width); - const rightSidebarIsResizing = useRightSidebarStore( - (state) => state.isResizing, - ); - const setRightSidebarIsResizing = useRightSidebarStore( - (state) => state.setIsResizing, - ); - const activeTaskId = view.type === "task-detail" ? view.data?.id : undefined; const activeWorkspace = useWorkspace(activeTaskId); const isCloudTask = activeWorkspace?.mode === "cloud"; - const showRightSidebarSection = view.type === "task-detail"; const handleLeftSidebarMouseDown = (e: React.MouseEvent) => { e.preventDefault(); @@ -45,13 +34,6 @@ export function HeaderRow() { document.body.style.userSelect = "none"; }; - const handleRightSidebarMouseDown = (e: React.MouseEvent) => { - e.preventDefault(); - setRightSidebarIsResizing(true); - document.body.style.cursor = "col-resize"; - document.body.style.userSelect = "none"; - }; - return ( )} - {showRightSidebarSection && view.type === "task-detail" && view.data && ( + {view.type === "task-detail" && view.data && ( - - {rightSidebarOpen && - (isCloudTask ? ( + + {isCloudTask ? ( ) : ( - ))} - {rightSidebarOpen && ( - - )} + )} + + )} diff --git a/apps/code/src/renderer/components/MainLayout.tsx b/apps/code/src/renderer/components/MainLayout.tsx index 698574438..f49e76d66 100644 --- a/apps/code/src/renderer/components/MainLayout.tsx +++ b/apps/code/src/renderer/components/MainLayout.tsx @@ -7,7 +7,6 @@ import { ArchivedTasksView } from "@features/archive/components/ArchivedTasksVie import { CommandMenu } from "@features/command/components/CommandMenu"; import { CommandCenterView } from "@features/command-center/components/CommandCenterView"; import { InboxView } from "@features/inbox/components/InboxView"; -import { RightSidebar, RightSidebarContent } from "@features/right-sidebar"; import { FolderSettingsView } from "@features/settings/components/FolderSettingsView"; import { SettingsDialog } from "@features/settings/components/SettingsDialog"; import { MainSidebar } from "@features/sidebar/components/MainSidebar"; @@ -82,12 +81,6 @@ export function MainLayout() { {view.type === "skills" && } - - {view.type === "task-detail" && view.data && ( - - - - )} diff --git a/apps/code/src/renderer/constants/keyboard-shortcuts.ts b/apps/code/src/renderer/constants/keyboard-shortcuts.ts index 3ae4df768..26fb45464 100644 --- a/apps/code/src/renderer/constants/keyboard-shortcuts.ts +++ b/apps/code/src/renderer/constants/keyboard-shortcuts.ts @@ -8,7 +8,7 @@ export const SHORTCUTS = { GO_BACK: "mod+[", GO_FORWARD: "mod+]", TOGGLE_LEFT_SIDEBAR: "mod+b", - TOGGLE_RIGHT_SIDEBAR: "mod+shift+b", + TOGGLE_REVIEW_PANEL: "mod+shift+b", PREV_TASK: "mod+shift+[,ctrl+shift+tab", NEXT_TASK: "mod+shift+],ctrl+tab", CLOSE_TAB: "mod+w", @@ -112,9 +112,9 @@ export const KEYBOARD_SHORTCUTS: KeyboardShortcut[] = [ category: "navigation", }, { - id: "toggle-right-sidebar", - keys: SHORTCUTS.TOGGLE_RIGHT_SIDEBAR, - description: "Toggle right sidebar", + id: "toggle-review-panel", + keys: SHORTCUTS.TOGGLE_REVIEW_PANEL, + description: "Toggle review panel", category: "navigation", }, { diff --git a/apps/code/src/renderer/features/code-review/components/CloudReviewPage.tsx b/apps/code/src/renderer/features/code-review/components/CloudReviewPage.tsx index 7cb0ac7a8..a643941c6 100644 --- a/apps/code/src/renderer/features/code-review/components/CloudReviewPage.tsx +++ b/apps/code/src/renderer/features/code-review/components/CloudReviewPage.tsx @@ -15,11 +15,11 @@ import { } from "./ReviewShell"; interface CloudReviewPageProps { - taskId: string; task: Task; } -export function CloudReviewPage({ taskId, task }: CloudReviewPageProps) { +export function CloudReviewPage({ task }: CloudReviewPageProps) { + const taskId = task.id; const { effectiveBranch, prUrl, isRunActive, remoteFiles, isLoading } = useCloudChangedFiles(taskId, task); const onComment = useReviewComment(taskId); @@ -64,6 +64,7 @@ export function CloudReviewPage({ taskId, task }: CloudReviewPageProps) { return ( { + if (isCloud) { + const stats = computeDiffStats(cloudFiles); + return { + filesChanged: stats.filesChanged, + linesAdded: stats.linesAdded, + linesRemoved: stats.linesRemoved, + }; + } + return { + filesChanged: localDiffStats.filesChanged, + linesAdded: localDiffStats.linesAdded, + linesRemoved: localDiffStats.linesRemoved, + }; + }, [isCloud, cloudFiles, localDiffStats]); +} + +export function DiffStatsBadge({ task }: DiffStatsBadgeProps) { + const taskId = task.id; + const { filesChanged, linesAdded, linesRemoved } = useChangedFileStats(task); + const reviewMode = useReviewNavigationStore( + (s) => s.reviewModes[taskId] ?? "closed", + ); + const setReviewMode = useReviewNavigationStore((s) => s.setReviewMode); + + const hasChanges = filesChanged > 0; + + const isOpen = reviewMode !== "closed"; + + const handleClick = () => { + setReviewMode(taskId, isOpen ? "closed" : "split"); + }; + + return ( + + + + {hasChanges ? ( + + {linesAdded > 0 && ( + + +{linesAdded} + + )} + {linesRemoved > 0 && ( + + -{linesRemoved} + + )} + + ) : ( + 0 + )} + + + ); +} diff --git a/apps/code/src/renderer/features/code-review/components/ReviewPage.tsx b/apps/code/src/renderer/features/code-review/components/ReviewPage.tsx index b86df4a0f..1ffd21111 100644 --- a/apps/code/src/renderer/features/code-review/components/ReviewPage.tsx +++ b/apps/code/src/renderer/features/code-review/components/ReviewPage.tsx @@ -1,11 +1,11 @@ import { makeFileKey } from "@features/git-interaction/utils/fileKey"; import { usePanelLayoutStore } from "@features/panels/store/panelLayoutStore"; -import { isTabActiveInTree } from "@features/panels/store/panelStoreHelpers"; import { useCwd } from "@features/sidebar/hooks/useCwd"; import type { parsePatchFiles } from "@pierre/diffs"; import { Flex, Text } from "@radix-ui/themes"; +import { useReviewNavigationStore } from "@renderer/features/code-review/stores/reviewNavigationStore"; import { useTRPC } from "@renderer/trpc/client"; -import type { ChangedFile } from "@shared/types"; +import type { ChangedFile, Task } from "@shared/types"; import { useQuery } from "@tanstack/react-query"; import { useMemo } from "react"; import { useReviewComment } from "../hooks/useReviewComment"; @@ -22,17 +22,16 @@ import { } from "./ReviewShell"; interface ReviewPageProps { - taskId: string; + task: Task; } -export function ReviewPage({ taskId }: ReviewPageProps) { +export function ReviewPage({ task }: ReviewPageProps) { + const taskId = task.id; const repoPath = useCwd(taskId); const openFile = usePanelLayoutStore((s) => s.openFile); - const isReviewTabActive = usePanelLayoutStore((s) => { - const layout = s.getLayout(taskId); - if (!layout) return false; - return isTabActiveInTree(layout.panelTree, "review"); - }); + const isReviewOpen = useReviewNavigationStore( + (s) => (s.reviewModes[taskId] ?? "closed") !== "closed", + ); const onComment = useReviewComment(taskId); const { @@ -46,7 +45,7 @@ export function ReviewPage({ taskId }: ReviewPageProps) { allPaths, diffLoading, refetch, - } = useReviewDiffs(repoPath, isReviewTabActive); + } = useReviewDiffs(repoPath, isReviewOpen); const { diffOptions, @@ -86,6 +85,7 @@ export function ReviewPage({ taskId }: ReviewPageProps) { return ( { + e.preventDefault(); + isDragging.current = true; + const startX = e.clientX; + const startWidth = width; + + const handleMouseMove = (e: MouseEvent) => { + if (!isDragging.current) return; + const delta = startX - e.clientX; + const newWidth = Math.min( + SIDEBAR_MAX_WIDTH, + Math.max(SIDEBAR_MIN_WIDTH, startWidth + delta), + ); + setWidth(newWidth); + }; + + const handleMouseUp = () => { + isDragging.current = false; + document.removeEventListener("mousemove", handleMouseMove); + document.removeEventListener("mouseup", handleMouseUp); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + + document.addEventListener("mousemove", handleMouseMove); + document.addEventListener("mouseup", handleMouseUp); + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + }, + [width], + ); + + return ( + + { + e.currentTarget.style.background = "var(--accent-8)"; + }} + onMouseLeave={(e) => { + if (!isDragging.current) { + e.currentTarget.style.background = "transparent"; + } + }} + /> + + + + + ); +} + export interface ReviewShellProps { taskId: string; + task?: Task; fileCount: number; linesAdded: number; linesRemoved: number; @@ -225,6 +310,7 @@ export interface ReviewShellProps { export function ReviewShell({ taskId, + task, fileCount, linesAdded, linesRemoved, @@ -239,6 +325,11 @@ export function ReviewShell({ }: ReviewShellProps) { const scrollContainerRef = useRef(null); + const reviewMode = useReviewNavigationStore( + (s) => s.reviewModes[taskId] ?? "closed", + ); + const isExpanded = reviewMode === "expanded"; + const scrollRequest = useReviewNavigationStore( (s) => s.scrollRequests[taskId] ?? null, ); @@ -345,6 +436,7 @@ export function ReviewShell({ > - - {children} - + + + {children} + + + {isExpanded && task && ( + + )} + ); diff --git a/apps/code/src/renderer/features/code-review/components/ReviewToolbar.tsx b/apps/code/src/renderer/features/code-review/components/ReviewToolbar.tsx index 227a68d49..4da3b8b7b 100644 --- a/apps/code/src/renderer/features/code-review/components/ReviewToolbar.tsx +++ b/apps/code/src/renderer/features/code-review/components/ReviewToolbar.tsx @@ -5,13 +5,20 @@ import { ArrowsIn, ArrowsOut, Columns, + CornersIn, + CornersOut, Rows, } from "@phosphor-icons/react"; -import { Flex, IconButton, Text } from "@radix-ui/themes"; +import { Flex, IconButton, Separator, Text } from "@radix-ui/themes"; import { DiffSettingsMenu } from "@renderer/features/code-review/components/DiffSettingsMenu"; +import { + type ReviewMode, + useReviewNavigationStore, +} from "@renderer/features/code-review/stores/reviewNavigationStore"; import { memo } from "react"; interface ReviewToolbarProps { + taskId: string; fileCount: number; linesAdded: number; linesRemoved: number; @@ -22,6 +29,7 @@ interface ReviewToolbarProps { } export const ReviewToolbar = memo(function ReviewToolbar({ + taskId, fileCount, linesAdded, linesRemoved, @@ -32,6 +40,15 @@ export const ReviewToolbar = memo(function ReviewToolbar({ }: ReviewToolbarProps) { const viewMode = useDiffViewerStore((s) => s.viewMode); const toggleViewMode = useDiffViewerStore((s) => s.toggleViewMode); + const reviewMode = useReviewNavigationStore( + (s) => s.reviewModes[taskId] ?? "closed", + ); + const setReviewMode = useReviewNavigationStore((s) => s.setReviewMode); + + const handleToggleExpand = () => { + const next: ReviewMode = reviewMode === "expanded" ? "split" : "expanded"; + setReviewMode(taskId, next); + }; return ( )} + + + + + + + {reviewMode === "expanded" ? ( + + ) : ( + + )} + + ); diff --git a/apps/code/src/renderer/features/code-review/stores/reviewNavigationStore.ts b/apps/code/src/renderer/features/code-review/stores/reviewNavigationStore.ts index a08aacdcd..f74c14699 100644 --- a/apps/code/src/renderer/features/code-review/stores/reviewNavigationStore.ts +++ b/apps/code/src/renderer/features/code-review/stores/reviewNavigationStore.ts @@ -1,8 +1,11 @@ import { create } from "zustand"; +export type ReviewMode = "closed" | "split" | "expanded"; + interface ReviewNavigationStoreState { activeFilePaths: Record; scrollRequests: Record; + reviewModes: Record; } interface ReviewNavigationStoreActions { @@ -10,15 +13,18 @@ interface ReviewNavigationStoreActions { requestScrollToFile: (taskId: string, path: string) => void; clearScrollRequest: (taskId: string) => void; clearTask: (taskId: string) => void; + setReviewMode: (taskId: string, mode: ReviewMode) => void; + getReviewMode: (taskId: string) => ReviewMode; } type ReviewNavigationStore = ReviewNavigationStoreState & ReviewNavigationStoreActions; export const useReviewNavigationStore = create()( - (set) => ({ + (set, get) => ({ activeFilePaths: {}, scrollRequests: {}, + reviewModes: {}, setActiveFilePath: (taskId, path) => set((state) => ({ @@ -40,5 +46,12 @@ export const useReviewNavigationStore = create()( activeFilePaths: { ...state.activeFilePaths, [taskId]: null }, scrollRequests: { ...state.scrollRequests, [taskId]: null }, })), + + setReviewMode: (taskId, mode) => + set((state) => ({ + reviewModes: { ...state.reviewModes, [taskId]: mode }, + })), + + getReviewMode: (taskId) => get().reviewModes[taskId] ?? "closed", }), ); diff --git a/apps/code/src/renderer/features/command/components/CommandMenu.tsx b/apps/code/src/renderer/features/command/components/CommandMenu.tsx index d05bd40d2..8c37e391b 100644 --- a/apps/code/src/renderer/features/command/components/CommandMenu.tsx +++ b/apps/code/src/renderer/features/command/components/CommandMenu.tsx @@ -1,7 +1,7 @@ +import { useReviewNavigationStore } from "@features/code-review/stores/reviewNavigationStore"; import { Command } from "@features/command/components/Command"; import { CommandKeyHints } from "@features/command/components/CommandKeyHints"; import { useFolders } from "@features/folders/hooks/useFolders"; -import { useRightSidebarStore } from "@features/right-sidebar"; import { useSettingsDialogStore } from "@features/settings/stores/settingsDialogStore"; import { useSidebarStore } from "@features/sidebar/stores/sidebarStore"; import { @@ -35,7 +35,21 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { const { folders } = useFolders(); const { theme, cycleTheme } = useThemeStore(); const toggleLeftSidebar = useSidebarStore((state) => state.toggle); - const toggleRightSidebar = useRightSidebarStore((state) => state.toggle); + const view = useNavigationStore((state) => state.view); + const setReviewMode = useReviewNavigationStore( + (state) => state.setReviewMode, + ); + const getReviewMode = useReviewNavigationStore( + (state) => state.getReviewMode, + ); + const openReviewPanel = useCallback(() => { + const taskId = view.type === "task-detail" ? view.data?.id : undefined; + if (!taskId) return; + const mode = getReviewMode(taskId); + if (mode === "closed") { + setReviewMode(taskId, "split"); + } + }, [view, getReviewMode, setReviewMode]); const commandRef = useRef(null); const close = useCallback(() => onOpenChange(false), [onOpenChange]); @@ -170,14 +184,11 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { Toggle left sidebar - Toggle right sidebar + Open review panel ; } else if (tab.data.type === "logs") { icon = ; - } else if (tab.data.type === "review") { - icon = ; } else if (tab.data.type === "action") { icon = ; } diff --git a/apps/code/src/renderer/features/panels/store/panelLayoutStore.test.ts b/apps/code/src/renderer/features/panels/store/panelLayoutStore.test.ts index 6f63b48f4..15e88bcdc 100644 --- a/apps/code/src/renderer/features/panels/store/panelLayoutStore.test.ts +++ b/apps/code/src/renderer/features/panels/store/panelLayoutStore.test.ts @@ -49,7 +49,7 @@ describe("panelLayoutStore", () => { assertPanelLayout(tree, [ { panelId: "main-panel", - expectedTabs: ["logs", "review", "shell"], + expectedTabs: ["logs", "shell"], activeTab: "logs", }, ]); @@ -64,11 +64,11 @@ describe("panelLayoutStore", () => { it("adds file tab to main panel by default", () => { usePanelLayoutStore.getState().openFile("task-1", "src/App.tsx"); - assertTabCount(getPanelTree("task-1"), "main-panel", 4); + assertTabCount(getPanelTree("task-1"), "main-panel", 3); assertPanelLayout(getPanelTree("task-1"), [ { panelId: "main-panel", - expectedTabs: ["logs", "review", "shell", "file-src/App.tsx"], + expectedTabs: ["logs", "shell", "file-src/App.tsx"], }, ]); }); @@ -93,7 +93,7 @@ describe("panelLayoutStore", () => { activeTab: "file-src/App.tsx", }, ]); - assertTabCount(getPanelTree("task-1"), "main-panel", 2); + assertTabCount(getPanelTree("task-1"), "main-panel", 1); }); it("falls back to main panel if focused panel does not exist", () => { @@ -105,11 +105,11 @@ describe("panelLayoutStore", () => { usePanelLayoutStore.getState().openFile("task-1", "src/App.tsx"); // File should fall back to main-panel - assertTabCount(getPanelTree("task-1"), "main-panel", 4); + assertTabCount(getPanelTree("task-1"), "main-panel", 3); assertPanelLayout(getPanelTree("task-1"), [ { panelId: "main-panel", - expectedTabs: ["logs", "review", "shell", "file-src/App.tsx"], + expectedTabs: ["logs", "shell", "file-src/App.tsx"], }, ]); }); @@ -370,14 +370,14 @@ describe("panelLayoutStore", () => { }); it("reorders tabs within a panel", () => { - // tabs: [logs, review, shell, file-src/App.tsx, file-src/Other.tsx, file-src/Third.tsx] - // move index 3 to index 5 - usePanelLayoutStore.getState().reorderTabs("task-1", "main-panel", 3, 5); + // tabs: [logs, shell, file-src/App.tsx, file-src/Other.tsx, file-src/Third.tsx] + // move index 2 to index 4 + usePanelLayoutStore.getState().reorderTabs("task-1", "main-panel", 2, 4); const panel = findPanelById(getPanelTree("task-1"), "main-panel"); const tabIds = panel?.content.tabs.map((t: { id: string }) => t.id); - expect(tabIds?.[3]).toBe("file-src/Other.tsx"); - expect(tabIds?.[5]).toBe("file-src/App.tsx"); + expect(tabIds?.[2]).toBe("file-src/Other.tsx"); + expect(tabIds?.[4]).toBe("file-src/App.tsx"); }); it("preserves active tab after reorder", () => { diff --git a/apps/code/src/renderer/features/panels/store/panelLayoutStore.ts b/apps/code/src/renderer/features/panels/store/panelLayoutStore.ts index d1bf0a0df..47c08f67c 100644 --- a/apps/code/src/renderer/features/panels/store/panelLayoutStore.ts +++ b/apps/code/src/renderer/features/panels/store/panelLayoutStore.ts @@ -53,7 +53,6 @@ export interface PanelLayoutStore { filePath: string, asPreview?: boolean, ) => void; - openReview: (taskId: string) => void; keepTab: (taskId: string, panelId: string, tabId: string) => void; closeTab: (taskId: string, panelId: string, tabId: string) => void; closeOtherTabs: (taskId: string, panelId: string, tabId: string) => void; @@ -123,14 +122,6 @@ function createDefaultPanelTree(): PanelNode { closeable: false, draggable: true, }, - { - id: DEFAULT_TAB_IDS.REVIEW, - label: "Review", - data: { type: "review" as const }, - component: null, - closeable: false, - draggable: true, - }, { id: DEFAULT_TAB_IDS.SHELL, label: "Terminal", @@ -396,13 +387,6 @@ export const usePanelLayoutStore = createWithEqualityFn()( }); }, - openReview: (taskId) => { - set((state) => openTab(state, taskId, "review", false)); - track(ANALYTICS_EVENTS.REVIEW_PANEL_VIEWED, { - task_id: taskId, - }); - }, - keepTab: (taskId, panelId, tabId) => { set((state) => updateTaskLayout(state, taskId, (layout) => { @@ -922,7 +906,7 @@ export const usePanelLayoutStore = createWithEqualityFn()( { name: "panel-layout-store", // Bump this version when the default panel structure changes to reset all layouts - version: 9, + version: 10, migrate: () => ({ taskLayouts: {} }), }, ), diff --git a/apps/code/src/renderer/features/panels/store/panelStoreHelpers.ts b/apps/code/src/renderer/features/panels/store/panelStoreHelpers.ts index bf01a86cb..3b953ddfb 100644 --- a/apps/code/src/renderer/features/panels/store/panelStoreHelpers.ts +++ b/apps/code/src/renderer/features/panels/store/panelStoreHelpers.ts @@ -25,7 +25,6 @@ export function parseTabId(tabId: string): ParsedTabId & { status?: string } { } export function createTabLabel(tabId: string): string { - if (tabId === "review") return "Review"; const parsed = parseTabId(tabId); if (parsed.type === "file") { return parsed.value.split("/").pop() || parsed.value; @@ -121,9 +120,7 @@ export function createNewTab( }; break; case "system": - if (tabId === "review") { - data = { type: "review" }; - } else if (tabId === "logs") { + if (tabId === "logs") { data = { type: "logs" }; } else if (tabId.startsWith("shell")) { data = { diff --git a/apps/code/src/renderer/features/panels/store/panelTypes.ts b/apps/code/src/renderer/features/panels/store/panelTypes.ts index 6f9869634..bc7edfa58 100644 --- a/apps/code/src/renderer/features/panels/store/panelTypes.ts +++ b/apps/code/src/renderer/features/panels/store/panelTypes.ts @@ -13,9 +13,6 @@ export type TabData = absolutePath: string; repoPath: string; } - | { - type: "review"; - } | { type: "terminal"; terminalId: string; diff --git a/apps/code/src/renderer/features/right-sidebar/components/RightSidebar.tsx b/apps/code/src/renderer/features/right-sidebar/components/RightSidebar.tsx deleted file mode 100644 index 653de9151..000000000 --- a/apps/code/src/renderer/features/right-sidebar/components/RightSidebar.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { ResizableSidebar } from "@components/ResizableSidebar"; -import type React from "react"; -import { useRightSidebarStore } from "../stores/rightSidebarStore"; - -export const RightSidebar: React.FC<{ children: React.ReactNode }> = ({ - children, -}) => { - const open = useRightSidebarStore((state) => state.open); - const width = useRightSidebarStore((state) => state.width); - const setWidth = useRightSidebarStore((state) => state.setWidth); - const isResizing = useRightSidebarStore((state) => state.isResizing); - const setIsResizing = useRightSidebarStore((state) => state.setIsResizing); - - return ( - - {children} - - ); -}; diff --git a/apps/code/src/renderer/features/right-sidebar/components/RightSidebarContent.tsx b/apps/code/src/renderer/features/right-sidebar/components/RightSidebarContent.tsx deleted file mode 100644 index 2246e84ae..000000000 --- a/apps/code/src/renderer/features/right-sidebar/components/RightSidebarContent.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Panel } from "@features/panels/components/Panel"; -import { PanelGroup } from "@features/panels/components/PanelGroup"; -import type { Task } from "@shared/types"; -import { TopPanel } from "./TopPanel"; - -interface RightSidebarContentProps { - taskId: string; - task: Task; -} - -export function RightSidebarContent({ - taskId, - task, -}: RightSidebarContentProps) { - return ( - - - - - - ); -} diff --git a/apps/code/src/renderer/features/right-sidebar/components/RightSidebarTrigger.tsx b/apps/code/src/renderer/features/right-sidebar/components/RightSidebarTrigger.tsx deleted file mode 100644 index 1b80f0549..000000000 --- a/apps/code/src/renderer/features/right-sidebar/components/RightSidebarTrigger.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Tooltip } from "@components/ui/Tooltip"; -import { SidebarSimpleIcon } from "@phosphor-icons/react"; -import { IconButton } from "@radix-ui/themes"; -import { - formatHotkey, - SHORTCUTS, -} from "@renderer/constants/keyboard-shortcuts"; -import type React from "react"; -import { useRightSidebarStore } from "../stores/rightSidebarStore"; - -export const RightSidebarTrigger: React.FC = () => { - const toggle = useRightSidebarStore((state) => state.toggle); - - return ( - - - - - - ); -}; diff --git a/apps/code/src/renderer/features/right-sidebar/components/TopPanel.tsx b/apps/code/src/renderer/features/right-sidebar/components/TopPanel.tsx deleted file mode 100644 index 31d014762..000000000 --- a/apps/code/src/renderer/features/right-sidebar/components/TopPanel.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { TabbedPanel } from "@features/panels/components/TabbedPanel"; -import type { PanelContent } from "@features/panels/store/panelTypes"; -import { ChangesPanel } from "@features/task-detail/components/ChangesPanel"; -import { FileTreePanel } from "@features/task-detail/components/FileTreePanel"; -import { FolderSimple, GitDiff } from "@phosphor-icons/react"; -import type { Task } from "@shared/types"; -import { useMemo, useState } from "react"; - -interface TopPanelProps { - taskId: string; - task: Task; -} - -export function TopPanel({ taskId, task }: TopPanelProps) { - const [activeTabId, setActiveTabId] = useState("changes"); - - const content: PanelContent = useMemo( - () => ({ - id: `top-panel-${taskId}`, - activeTabId, - tabs: [ - { - id: "changes", - label: "Changes", - icon: , - data: { type: "other" }, - draggable: false, - closeable: false, - component: , - }, - { - id: "files", - label: "Files", - icon: , - data: { type: "other" }, - draggable: false, - closeable: false, - component: , - }, - ], - }), - [taskId, task, activeTabId], - ); - - return ( - setActiveTabId(tabId)} - /> - ); -} diff --git a/apps/code/src/renderer/features/right-sidebar/components/index.ts b/apps/code/src/renderer/features/right-sidebar/components/index.ts deleted file mode 100644 index dd3c289dd..000000000 --- a/apps/code/src/renderer/features/right-sidebar/components/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { RightSidebar } from "./RightSidebar"; -export { RightSidebarContent } from "./RightSidebarContent"; -export { RightSidebarTrigger } from "./RightSidebarTrigger"; diff --git a/apps/code/src/renderer/features/right-sidebar/index.ts b/apps/code/src/renderer/features/right-sidebar/index.ts deleted file mode 100644 index 5d34b910b..000000000 --- a/apps/code/src/renderer/features/right-sidebar/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { - RightSidebar, - RightSidebarContent, - RightSidebarTrigger, -} from "./components"; -export { useRightSidebarStore } from "./stores/rightSidebarStore"; diff --git a/apps/code/src/renderer/features/right-sidebar/stores/rightSidebarStore.ts b/apps/code/src/renderer/features/right-sidebar/stores/rightSidebarStore.ts deleted file mode 100644 index 683806d5c..000000000 --- a/apps/code/src/renderer/features/right-sidebar/stores/rightSidebarStore.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createSidebarStore } from "@stores/createSidebarStore"; - -export const useRightSidebarStore = createSidebarStore({ - name: "right-sidebar-storage", - defaultWidth: 300, -}); diff --git a/apps/code/src/renderer/features/skills/components/SkillDetailPanel.tsx b/apps/code/src/renderer/features/skills/components/SkillDetailPanel.tsx index 0fd8a1a9d..6b9f81f2f 100644 --- a/apps/code/src/renderer/features/skills/components/SkillDetailPanel.tsx +++ b/apps/code/src/renderer/features/skills/components/SkillDetailPanel.tsx @@ -69,7 +69,7 @@ export function SkillDetailPanel({ skill, onClose }: SkillDetailPanelProps) { )} {skill.source !== "bundled" && ( - + )} diff --git a/apps/code/src/renderer/features/task-detail/components/ChangesPanel.tsx b/apps/code/src/renderer/features/task-detail/components/ChangesPanel.tsx index 990b6e9cd..8d9572577 100644 --- a/apps/code/src/renderer/features/task-detail/components/ChangesPanel.tsx +++ b/apps/code/src/renderer/features/task-detail/components/ChangesPanel.tsx @@ -7,7 +7,6 @@ import { makeFileKey } from "@features/git-interaction/utils/fileKey"; import { invalidateGitWorkingTreeQueries } from "@features/git-interaction/utils/gitCacheKeys"; import { partitionByStaged } from "@features/git-interaction/utils/partitionByStaged"; import { updateGitCacheFromSnapshot } from "@features/git-interaction/utils/updateGitCache"; -import { usePanelLayoutStore } from "@features/panels/store/panelLayoutStore"; import { useCwd } from "@features/sidebar/hooks/useCwd"; import { useCloudChangedFiles } from "@features/task-detail/hooks/useCloudChangedFiles"; import { @@ -133,7 +132,6 @@ function ChangedFileItem({ onStageToggle, depth = 0, }: ChangedFileItemProps) { - const openReview = usePanelLayoutStore((state) => state.openReview); const requestScrollToFile = useReviewNavigationStore( (state) => state.requestScrollToFile, ); @@ -160,7 +158,6 @@ function ChangedFileItem({ task_id: taskId, }); requestScrollToFile(taskId, fileKey); - openReview(taskId); }; const workspaceContext = { diff --git a/apps/code/src/renderer/features/task-detail/components/ExternalAppsOpener.tsx b/apps/code/src/renderer/features/task-detail/components/ExternalAppsOpener.tsx index dd7ba8df7..a6e9ab486 100644 --- a/apps/code/src/renderer/features/task-detail/components/ExternalAppsOpener.tsx +++ b/apps/code/src/renderer/features/task-detail/components/ExternalAppsOpener.tsx @@ -1,7 +1,7 @@ import { useExternalApps } from "@features/external-apps/hooks/useExternalApps"; import { CodeIcon, CopyIcon } from "@phosphor-icons/react"; import { ChevronDownIcon } from "@radix-ui/react-icons"; -import { Button, DropdownMenu, Flex, Text } from "@radix-ui/themes"; +import { DropdownMenu, Flex, Text } from "@radix-ui/themes"; import { SHORTCUTS } from "@renderer/constants/keyboard-shortcuts"; import { handleExternalAppAction } from "@utils/handleExternalAppAction"; import { useCallback, useState } from "react"; @@ -12,13 +12,9 @@ const DROPDOWN_ICON_SIZE = 16; interface ExternalAppsOpenerProps { targetPath: string | null; - label?: string; } -export function ExternalAppsOpener({ - targetPath, - label = "Open", -}: ExternalAppsOpenerProps) { +export function ExternalAppsOpener({ targetPath }: ExternalAppsOpenerProps) { const { detectedApps, defaultApp, isLoading } = useExternalApps(); const [dropdownOpen, setDropdownOpen] = useState(false); @@ -83,25 +79,25 @@ export function ExternalAppsOpener({ return ( - {dropdownOpen && ( - - )} - - + {defaultApp?.icon ? ( )} - - {label}{" "} - - ⌘O - - - - + - - - + + diff --git a/apps/code/src/renderer/features/task-detail/components/FileTreePanel.tsx b/apps/code/src/renderer/features/task-detail/components/FileTreePanel.tsx index a5e3d64da..5fae22c5a 100644 --- a/apps/code/src/renderer/features/task-detail/components/FileTreePanel.tsx +++ b/apps/code/src/renderer/features/task-detail/components/FileTreePanel.tsx @@ -261,7 +261,7 @@ function LocalFileTreePanel({ taskId, task: _task }: FileTreePanelProps) { height="100%" py="2" style={{ - overflowY: "scroll", + overflowY: "auto", }} > diff --git a/apps/code/src/renderer/features/task-detail/components/TabContentRenderer.tsx b/apps/code/src/renderer/features/task-detail/components/TabContentRenderer.tsx index 32ad9e0d9..76113a56d 100644 --- a/apps/code/src/renderer/features/task-detail/components/TabContentRenderer.tsx +++ b/apps/code/src/renderer/features/task-detail/components/TabContentRenderer.tsx @@ -5,9 +5,6 @@ import { ChangesPanel } from "@features/task-detail/components/ChangesPanel"; import { FileTreePanel } from "@features/task-detail/components/FileTreePanel"; import { TaskLogsPanel } from "@features/task-detail/components/TaskLogsPanel"; import { TaskShellPanel } from "@features/task-detail/components/TaskShellPanel"; -import { useWorkspace } from "@features/workspace/hooks/useWorkspace"; -import { CloudReviewPage } from "@renderer/features/code-review/components/CloudReviewPage"; -import { ReviewPage } from "@renderer/features/code-review/components/ReviewPage"; import type { Task } from "@shared/types"; interface TabContentRendererProps { @@ -21,7 +18,6 @@ export function TabContentRenderer({ taskId, task, }: TabContentRendererProps) { - const workspace = useWorkspace(taskId); const { data } = tab; switch (data.type) { @@ -42,16 +38,6 @@ export function TabContentRenderer({ /> ); - case "review": { - const isCloud = - workspace?.mode === "cloud" || task.latest_run?.environment === "cloud"; - return isCloud ? ( - - ) : ( - - ); - } - case "action": return ( s.reviewModes[taskId] ?? "closed", + ); + const workspace = useWorkspace(taskId); + const isCloud = + workspace?.mode === "cloud" || task.latest_run?.environment === "cloud"; + + const isReviewOpen = reviewMode !== "closed"; + const isExpanded = reviewMode === "expanded"; + + const MIN_REVIEW_WIDTH = 300; + const containerRef = useRef(null); + const [reviewWidth, setReviewWidth] = useState(null); + const isDragging = useRef(false); + + const handleResizeStart = useCallback( + (e: React.MouseEvent) => { + e.preventDefault(); + isDragging.current = true; + + const startX = e.clientX; + const container = containerRef.current; + if (!container) return; + + const containerRect = container.getBoundingClientRect(); + const startWidth = reviewWidth ?? containerRect.width * 0.5; + + const onMouseMove = (moveEvent: MouseEvent) => { + const delta = startX - moveEvent.clientX; + const maxWidth = containerRect.width * 0.5; + const newWidth = Math.min( + maxWidth, + Math.max(MIN_REVIEW_WIDTH, startWidth + delta), + ); + setReviewWidth(newWidth); + }; + + const onMouseUp = () => { + isDragging.current = false; + document.removeEventListener("mousemove", onMouseMove); + document.removeEventListener("mouseup", onMouseUp); + document.body.style.cursor = ""; + document.body.style.userSelect = ""; + }; + + document.body.style.cursor = "col-resize"; + document.body.style.userSelect = "none"; + document.addEventListener("mousemove", onMouseMove); + document.addEventListener("mouseup", onMouseUp); + }, + [reviewWidth], + ); + return ( - - + + + {/* Main tabbed panels — hidden when review is expanded */} + + + + + {/* Resize handle */} + {isReviewOpen && !isExpanded && ( + + )} + + {/* Review panel — always mounted, kept in layout via visibility:hidden to avoid layout storms */} + + {isCloud ? ( + + ) : ( + + )} + +