diff --git a/apps/desktop/electron.vite.config.ts b/apps/desktop/electron.vite.config.ts index f45a88842bf..b782939ab74 100644 --- a/apps/desktop/electron.vite.config.ts +++ b/apps/desktop/electron.vite.config.ts @@ -49,9 +49,6 @@ export default defineConfig({ define: { "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV), "process.platform": JSON.stringify(process.platform), - "import.meta.env.ENABLE_NEW_UI": JSON.stringify( - process.env.ENABLE_NEW_UI || "false", - ), "import.meta.env.DEV_SERVER_PORT": JSON.stringify(getPortSync()), }, diff --git a/apps/desktop/index.d.ts b/apps/desktop/index.d.ts index 59390fdba02..8ca9b1e2751 100644 --- a/apps/desktop/index.d.ts +++ b/apps/desktop/index.d.ts @@ -5,7 +5,6 @@ import type { WebviewTag } from "electron"; interface ImportMetaEnv { readonly DEV_SERVER_PORT?: string; - readonly ENABLE_NEW_UI?: string; } interface ImportMeta { diff --git a/apps/desktop/src/renderer/env.d.ts b/apps/desktop/src/renderer/env.d.ts index 5a4b0f4410a..22b0869ec4c 100644 --- a/apps/desktop/src/renderer/env.d.ts +++ b/apps/desktop/src/renderer/env.d.ts @@ -1,7 +1,7 @@ /// interface ImportMetaEnv { - readonly ENABLE_NEW_UI: string; + // Environment variables } interface ImportMeta { diff --git a/apps/desktop/src/renderer/screens/main/MainScreen.tsx b/apps/desktop/src/renderer/screens/main/MainScreen.tsx index 9e60ff27fb4..a9d82e15bfa 100644 --- a/apps/desktop/src/renderer/screens/main/MainScreen.tsx +++ b/apps/desktop/src/renderer/screens/main/MainScreen.tsx @@ -1,87 +1,35 @@ -import { - closestCenter, - DndContext, - type DragEndEvent, - type DragOverEvent, - DragOverlay, - type DragStartEvent, - KeyboardSensor, - PointerSensor, - useDroppable, - useSensor, - useSensors, -} from "@dnd-kit/core"; -import { arrayMove, sortableKeyboardCoordinates } from "@dnd-kit/sortable"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup, } from "@superset/ui/resizable"; +import type React from "react"; import { useEffect, useRef, useState } from "react"; import type { ImperativePanelHandle } from "react-resizable-panels"; -import type { MosaicNode, Tab, TabType, Workspace } from "shared/types"; -import { createShortcutHandler } from "../../lib/keyboard-shortcuts"; -import { - createTabShortcuts, - createWorkspaceShortcuts, -} from "../../lib/shortcuts"; +import type { Tab, Workspace, Worktree } from "shared/types"; import { AppFrame } from "./components/AppFrame"; import { Background } from "./components/Background"; import TabContent from "./components/MainContent/TabContent"; import TabGroup from "./components/MainContent/TabGroup"; -import { NewLayoutMain } from "./components/NewLayout/NewLayoutMain"; +import { findTabRecursive } from "./components/MainContent/utils"; import { PlaceholderState } from "./components/PlaceholderState"; +import { PlanView } from "./components/PlanView"; import { Sidebar } from "./components/Sidebar"; import { DiffTab } from "./components/TabContent/components/DiffTab"; -import { TopBar } from "./components/TopBar"; - -// Droppable wrapper for main content area -function DroppableMainContent({ - children, - isOver, -}: { - children: React.ReactNode; - isOver: boolean; -}) { - const { setNodeRef } = useDroppable({ - id: "main-content-drop-zone", - data: { - type: "main-content", - }, - }); - - return ( -
- {children} - {isOver && ( -
-
- Drop to add to split view -
-
- )} -
- ); -} +import { AddTaskModal } from "./components/AddTaskModal"; +import { MOCK_TASKS } from "./components/mock-data"; +import type { PendingWorktree, UITask } from "./components/types"; +import { enrichWorktreesWithTasks } from "./components/utils"; +import { TaskTabs } from "./components/TaskTabs"; +import type { TaskStatus } from "./components/StatusIndicator"; export function MainScreen() { - // Check if new UI is enabled - const enableNewUI = import.meta.env.ENABLE_NEW_UI === "true"; - - // If new UI is enabled, render the new layout - if (enableNewUI) { - return ; - } - - // Otherwise, render the original layout + const sidebarPanelRef = useRef(null); const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [showSidebarOverlay, setShowSidebarOverlay] = useState(false); - const sidebarPanelRef = useRef(null); + const [isAddTaskModalOpen, setIsAddTaskModalOpen] = useState(false); + + // Workspace state const [workspaces, setWorkspaces] = useState(null); const [currentWorkspace, setCurrentWorkspace] = useState( null, @@ -89,181 +37,104 @@ export function MainScreen() { const [selectedWorktreeId, setSelectedWorktreeId] = useState( null, ); - const [selectedTabId, setSelectedTabId] = useState(null); // Can be a group tab or any tab + const [selectedTabId, setSelectedTabId] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [mode, setMode] = useState<"plan" | "edit">("edit"); + const [pendingWorktrees, setPendingWorktrees] = useState( + [], + ); + + // Compute which tasks have worktrees (are "open") + const openTasks = MOCK_TASKS.filter((task) => + currentWorkspace?.worktrees?.some((wt) => wt.branch === task.branch), + ); - // Diff view state - const [showDiffView, setShowDiffView] = useState(false); - const [diffWorktreeId, setDiffWorktreeId] = useState(null); + const handleCollapseSidebar = () => { + const panel = sidebarPanelRef.current; + if (panel && !panel.isCollapsed()) { + panel.collapse(); + setIsSidebarOpen(false); + } + }; - // Drag and drop state - const [activeId, setActiveId] = useState(null); - const [isOverMainContent, setIsOverMainContent] = useState(false); + const handleExpandSidebar = () => { + const panel = sidebarPanelRef.current; + if (panel && panel.isCollapsed()) { + panel.expand(); + setIsSidebarOpen(true); + } + }; + // Get selected worktree const selectedWorktree = currentWorkspace?.worktrees?.find( (wt) => wt.id === selectedWorktreeId, ); - const diffWorktree = currentWorkspace?.worktrees?.find( - (wt) => wt.id === diffWorktreeId, - ); + // Get selected tab and its parent (if it's a sub-tab) + const tabResult = selectedWorktree?.tabs + ? findTabRecursive(selectedWorktree.tabs, selectedTabId ?? "") + : null; - // Helper: Create a new tab - const createTab = async ( - workspaceId: string, - worktreeId: string, - name: string, - type: TabType, - ) => { - const result = await window.ipcRenderer.invoke("tab-create", { - workspaceId, - worktreeId, - name, - type, - }); + const selectedTab = tabResult?.tab; + const parentGroupTab = tabResult?.parent; - if (!result.success || !result.tab) { - console.error("[MainScreen] Failed to create tab:", result.error); - return null; + // Load all workspaces + const loadAllWorkspaces = async () => { + try { + const allWorkspaces = await window.ipcRenderer.invoke("workspace-list"); + setWorkspaces(allWorkspaces); + } catch (error) { + console.error("Failed to load workspaces:", error); } - - return result.tab; }; - // Helper: Show diff view for a worktree - now creates a tab - const handleShowDiffView = async (worktreeId: string) => { + // Optimistically add a tab to the current workspace + const handleTabCreated = (worktreeId: string, tab: Tab) => { if (!currentWorkspace) return; - // Find the worktree - const worktree = currentWorkspace.worktrees?.find( - (wt) => wt.id === worktreeId, - ); - if (!worktree) return; - - // Check if a diff tab already exists for this worktree - const existingDiffTab = worktree.tabs?.find((tab) => tab.type === "diff"); - - if (existingDiffTab) { - // If a diff tab already exists, just select it - await window.ipcRenderer.invoke("workspace-set-active-selection", { - workspaceId: currentWorkspace.id, - worktreeId: worktreeId, - tabId: existingDiffTab.id, - }); - - // Reload the workspace to get the updated state - const updatedWorkspace = await window.ipcRenderer.invoke( - "workspace-get", - currentWorkspace.id, - ); - if (updatedWorkspace) { - setCurrentWorkspace(updatedWorkspace); + // Find the worktree and add the tab + const updatedWorktrees = currentWorkspace.worktrees.map((wt) => { + if (wt.id === worktreeId) { + return { + ...wt, + tabs: [...wt.tabs, tab], + }; } - - // Update the workspaces array - await loadAllWorkspaces(); - - // Set state to select the tab - setSelectedWorktreeId(worktreeId); - setSelectedTabId(existingDiffTab.id); - return; - } - - // Create a new diff tab - const result = await window.ipcRenderer.invoke("tab-create", { - workspaceId: currentWorkspace.id, - worktreeId: worktreeId, - name: `Changes – ${worktree.branch}`, - type: "diff", + return wt; }); - if (result.success && result.tab) { - // Set active selection in backend first - await window.ipcRenderer.invoke("workspace-set-active-selection", { - workspaceId: currentWorkspace.id, - worktreeId: worktreeId, - tabId: result.tab.id, - }); - - // Reload the workspace to get the updated state with the new tab - const updatedWorkspace = await window.ipcRenderer.invoke( - "workspace-get", - currentWorkspace.id, - ); - if (updatedWorkspace) { - setCurrentWorkspace(updatedWorkspace); - } - - // Update the workspaces array - await loadAllWorkspaces(); - - // Set state to select the new tab - setSelectedWorktreeId(worktreeId); - setSelectedTabId(result.tab.id); - } - }; - - // Helper: Close diff view (legacy - can be removed if not used elsewhere) - const handleCloseDiffView = () => { - setShowDiffView(false); - setDiffWorktreeId(null); - }; - - // Configure sensors for drag-and-drop - const sensors = useSensors( - useSensor(PointerSensor, { - activationConstraint: { - distance: 8, - }, - }), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }), - ); + const updatedWorkspace = { + ...currentWorkspace, + worktrees: updatedWorktrees, + activeWorktreeId: worktreeId, + activeTabId: tab.id, + }; - // Helper function to find a tab recursively (for finding sub-tabs inside groups) - const findTabRecursive = ( - tabs: Tab[] | undefined, - tabId: string, - ): { tab: Tab; parent?: Tab } | null => { - if (!tabs) return null; + setCurrentWorkspace(updatedWorkspace); - for (const tab of tabs) { - if (tab.id === tabId) { - return { tab }; - } - // Check if this tab is a group tab with children - if (tab.type === "group" && tab.tabs) { - for (const childTab of tab.tabs) { - if (childTab.id === tabId) { - return { tab: childTab, parent: tab }; - } - } - } + // Also update in workspaces array + if (workspaces) { + setWorkspaces( + workspaces.map((ws) => + ws.id === currentWorkspace.id ? updatedWorkspace : ws, + ), + ); } - return null; }; - // Get selected tab and its parent (if it's a sub-tab) - const tabResult = selectedWorktree?.tabs - ? findTabRecursive(selectedWorktree.tabs, selectedTabId ?? "") - : null; - - const selectedTab = tabResult?.tab; - const parentGroupTab = tabResult?.parent; - + // Handle tab selection const handleTabSelect = (worktreeId: string, tabId: string) => { setSelectedWorktreeId(worktreeId); setSelectedTabId(tabId); - // Save active selection and update workspace state + if (currentWorkspace) { window.ipcRenderer.invoke("workspace-set-active-selection", { workspaceId: currentWorkspace.id, worktreeId, tabId, }); - // Update the current workspace state to reflect the new active selection + setCurrentWorkspace({ ...currentWorkspace, activeWorktreeId: worktreeId, @@ -272,18 +143,18 @@ export function MainScreen() { } }; + // Handle tab focus (for terminals) const handleTabFocus = (tabId: string) => { - // When a terminal gets focus, update the selected tab if (!currentWorkspace || !selectedWorktreeId) return; setSelectedTabId(tabId); - // Save active selection and update workspace state + window.ipcRenderer.invoke("workspace-set-active-selection", { workspaceId: currentWorkspace.id, worktreeId: selectedWorktreeId, tabId, }); - // Update the current workspace state to reflect the new active selection + setCurrentWorkspace({ ...currentWorkspace, activeWorktreeId: selectedWorktreeId, @@ -291,6 +162,7 @@ export function MainScreen() { }); }; + // Handle workspace selection const handleWorkspaceSelect = async (workspaceId: string) => { try { const workspace = await window.ipcRenderer.invoke( @@ -300,12 +172,11 @@ export function MainScreen() { if (workspace) { setCurrentWorkspace(workspace); - // Persist the active workspace await window.ipcRenderer.invoke( "workspace-set-active-workspace-id", workspaceId, ); - // Restore the active selection for this workspace + const activeSelection = await window.ipcRenderer.invoke( "workspace-get-active-selection", workspaceId, @@ -315,7 +186,6 @@ export function MainScreen() { setSelectedWorktreeId(activeSelection.worktreeId); setSelectedTabId(activeSelection.tabId); } else { - // No saved selection, reset setSelectedWorktreeId(null); setSelectedTabId(null); } @@ -325,8 +195,8 @@ export function MainScreen() { } }; + // Handle worktree created const handleWorktreeCreated = async () => { - // Refresh workspace data after worktree creation if (!currentWorkspace) return; try { @@ -337,7 +207,6 @@ export function MainScreen() { if (refreshedWorkspace) { setCurrentWorkspace(refreshedWorkspace); - // Also refresh workspaces list await loadAllWorkspaces(); } } catch (error) { @@ -345,8 +214,11 @@ export function MainScreen() { } }; - const handleUpdateWorktree = (worktreeId: string, updatedWorktree: any) => { - // Optimistically update the worktree in the current workspace + // Handle worktree update + const handleUpdateWorktree = ( + worktreeId: string, + updatedWorktree: Worktree, + ) => { if (!currentWorkspace) return; const updatedWorktrees = currentWorkspace.worktrees.map((wt) => @@ -360,7 +232,6 @@ export function MainScreen() { setCurrentWorkspace(updatedCurrentWorkspace); - // Also update the workspaces array so the carousel renders the updated data if (workspaces) { setWorkspaces( workspaces.map((ws) => @@ -370,680 +241,246 @@ export function MainScreen() { } }; - const loadAllWorkspaces = async () => { - try { - const allWorkspaces = await window.ipcRenderer.invoke("workspace-list"); + // Handle show diff - creates a diff tab + const handleShowDiff = async (worktreeId: string) => { + if (!currentWorkspace) return; - setWorkspaces(allWorkspaces); - } catch (error) { - console.error("Failed to load workspaces:", error); - } - }; + // Find the worktree + const worktree = currentWorkspace.worktrees?.find( + (wt) => wt.id === worktreeId, + ); + if (!worktree) return; - // Scan for existing worktrees when workspace is opened - const scanWorktrees = async (workspaceId: string) => { - try { - const result = await window.ipcRenderer.invoke( - "workspace-scan-worktrees", - workspaceId, + // Check if a diff tab already exists for this worktree + const existingDiffTab = worktree.tabs?.find((tab) => tab.type === "diff"); + + if (existingDiffTab) { + // If a diff tab already exists, just select it + await window.ipcRenderer.invoke("workspace-set-active-selection", { + workspaceId: currentWorkspace.id, + worktreeId: worktreeId, + tabId: existingDiffTab.id, + }); + + // Reload the workspace to get the updated state + const updatedWorkspace = await window.ipcRenderer.invoke( + "workspace-get", + currentWorkspace.id, ); + if (updatedWorkspace) { + setCurrentWorkspace(updatedWorkspace); + } + + // Update the workspaces array + await loadAllWorkspaces(); + + // Set state to select the tab + setSelectedWorktreeId(worktreeId); + setSelectedTabId(existingDiffTab.id); + return; + } + + // Create a new diff tab + const result = await window.ipcRenderer.invoke("tab-create", { + workspaceId: currentWorkspace.id, + worktreeId: worktreeId, + name: `Changes – ${worktree.branch}`, + type: "diff", + }); + + if (result.success && result.tab) { + // Set active selection in backend first + await window.ipcRenderer.invoke("workspace-set-active-selection", { + workspaceId: currentWorkspace.id, + worktreeId: worktreeId, + tabId: result.tab.id, + }); - if (result.success && result.imported && result.imported > 0) { - console.log("[MainScreen] Imported worktrees:", result.imported); + // Reload the workspace to get the updated state with the new tab + const updatedWorkspace = await window.ipcRenderer.invoke( + "workspace-get", + currentWorkspace.id, + ); + if (updatedWorkspace) { + setCurrentWorkspace(updatedWorkspace); } - } catch (error) { - console.error("[MainScreen] Failed to scan worktrees:", error); + + // Update the workspaces array + await loadAllWorkspaces(); + + // Set state to select the new tab + setSelectedWorktreeId(worktreeId); + setSelectedTabId(result.tab.id); } }; - // Load active workspace and all workspaces on mount - useEffect(() => { - const loadActiveWorkspace = async () => { - try { - setLoading(true); - setError(null); + // Task handlers + const handleOpenAddTaskModal = () => { + setIsAddTaskModalOpen(true); + }; - // Load all workspaces - await loadAllWorkspaces(); + const handleCloseAddTaskModal = () => { + setIsAddTaskModalOpen(false); + }; - // Try to load the active workspace first, fall back to last opened - let workspaceId = await window.ipcRenderer.invoke( - "workspace-get-active-workspace-id", - ); + const handleSelectTask = (task: UITask) => { + if (!currentWorkspace) return; - // Fall back to last opened if no active workspace - if (!workspaceId) { - const lastOpenedWorkspace = await window.ipcRenderer.invoke( - "workspace-get-last-opened", - ); - workspaceId = lastOpenedWorkspace?.id ?? null; - } + // Find existing worktree for this task's branch + const existingWorktree = currentWorkspace.worktrees?.find( + (wt) => wt.branch === task.branch, + ); - if (workspaceId) { - const workspace = await window.ipcRenderer.invoke( - "workspace-get", - workspaceId, - ); + if (existingWorktree) { + // Worktree already exists - switch to it + setSelectedWorktreeId(existingWorktree.id); + if (existingWorktree.tabs && existingWorktree.tabs.length > 0) { + handleTabSelect(existingWorktree.id, existingWorktree.tabs[0].id); + } + handleCloseAddTaskModal(); + } else { + // Worktree doesn't exist - create it with optimistic update + const pendingId = `pending-${Date.now()}`; + const pendingWorktree: PendingWorktree = { + id: pendingId, + isPending: true, + title: task.name, + branch: task.branch, + description: task.description, + taskData: { + slug: task.slug, + name: task.name, + status: task.status, + }, + }; - if (workspace) { - setCurrentWorkspace(workspace); - // Scan for existing worktrees - await scanWorktrees(workspace.id); + // Add pending worktree immediately + setPendingWorktrees((prev) => [...prev, pendingWorktree]); + handleCloseAddTaskModal(); - // Restore active selection for this workspace - const activeSelection = await window.ipcRenderer.invoke( - "workspace-get-active-selection", - workspaceId, - ); + void (async () => { + try { + const result = await window.ipcRenderer.invoke("worktree-create", { + workspaceId: currentWorkspace.id, + title: task.name, + branch: task.branch, + createBranch: false, // Branch should already exist + description: task.description, + }); - if (activeSelection?.worktreeId && activeSelection?.tabId) { - setSelectedWorktreeId(activeSelection.worktreeId); - setSelectedTabId(activeSelection.tabId); + if (result.success && result.worktree) { + // Remove pending worktree + setPendingWorktrees((prev) => + prev.filter((wt) => wt.id !== pendingId), + ); + // Refresh workspace to get the real worktree + await handleWorktreeCreated(); + setSelectedWorktreeId(result.worktree.id); + if (result.worktree.tabs && result.worktree.tabs.length > 0) { + handleTabSelect(result.worktree.id, result.worktree.tabs[0].id); } + } else { + // Remove pending on failure + setPendingWorktrees((prev) => + prev.filter((wt) => wt.id !== pendingId), + ); } + } catch (error) { + console.error("Failed to create worktree for task:", error); + // Remove pending on error + setPendingWorktrees((prev) => + prev.filter((wt) => wt.id !== pendingId), + ); } - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setLoading(false); - } - }; - - loadActiveWorkspace(); - }, []); - - // Listen for workspace-opened event from menu - useEffect(() => { - const handler = async (workspace: Workspace) => { - console.log("[MainScreen] Workspace opened event received:", workspace); - setLoading(false); - // Persist the active workspace - await window.ipcRenderer.invoke( - "workspace-set-active-workspace-id", - workspace.id, - ); - // Scan for existing worktrees FIRST - await scanWorktrees(workspace.id); - // Refresh workspaces list (after scanning to get updated worktrees) - await loadAllWorkspaces(); - // Fetch the updated workspace after scanning (this includes imported worktrees) - const refreshedWorkspace = await window.ipcRenderer.invoke( - "workspace-get", - workspace.id, - ); - if (refreshedWorkspace) { - setCurrentWorkspace(refreshedWorkspace); - } - }; - - console.log("[MainScreen] Setting up workspace-opened listener"); - window.ipcRenderer.on("workspace-opened", handler); - return () => { - console.log("[MainScreen] Removing workspace-opened listener"); - window.ipcRenderer.off("workspace-opened", handler); - }; - }, []); - - // Listen for terminal exit events and auto-close the tab - useEffect(() => { - const handleTerminalExit = async (data: { - id: string; - exitCode: number; - }) => { - console.log( - `[MainScreen] Terminal ${data.id} exited with code ${data.exitCode}`, - ); - - if (!currentWorkspace || !selectedWorktreeId) return; - - // Find which tab contains this terminal - const worktree = currentWorkspace.worktrees.find( - (wt) => wt.id === selectedWorktreeId, - ); - if (!worktree) return; - - // Check if the exited terminal is the currently selected tab - const isCurrentTab = selectedTabId === data.id; - - // Find the tab to determine its context (top-level or in group) - const tabResult = findTabRecursive(worktree.tabs, data.id); - if (!tabResult) return; - - const parentGroup = tabResult.parent; - const isInGroup = !!parentGroup; - - // Get the tabs array (either from group or top-level) - const tabs = isInGroup ? parentGroup?.tabs || [] : worktree.tabs; - const currentIndex = tabs.findIndex((t) => t.id === data.id); - - // Update mosaic tree if in a group - if (isInGroup && parentGroup && parentGroup.mosaicTree) { - const updatedMosaicTree = removeTabFromMosaicTree( - parentGroup.mosaicTree as MosaicNode, - data.id, - ); - - await window.ipcRenderer.invoke("tab-update-mosaic-tree", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: parentGroup.id, - mosaicTree: updatedMosaicTree, - }); - } - - // Delete the tab - const result = await window.ipcRenderer.invoke("tab-delete", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: data.id, - }); - - if (!result.success) { - console.error("Failed to close exited terminal tab:", result.error); - return; - } - - // Temporarily clear selection to force unmount if it's the current tab - const savedTabId = selectedTabId; - const savedWorktreeId = selectedWorktreeId; - if (isCurrentTab) { - setSelectedTabId(null); - } - - // Refresh workspace - const refreshedWorkspace = await window.ipcRenderer.invoke( - "workspace-get", - currentWorkspace.id, - ); - - if (refreshedWorkspace) { - setCurrentWorkspace(refreshedWorkspace); - await loadAllWorkspaces(); - - // Wait for next tick to ensure state updates - await new Promise((resolve) => setTimeout(resolve, 0)); - - // Only select adjacent tab if the exited terminal was the current one - if (isCurrentTab) { - const updatedWorktree = refreshedWorkspace.worktrees.find( - (wt) => wt.id === savedWorktreeId, - ); - - if (updatedWorktree) { - if (isInGroup && parentGroup) { - // Select adjacent tab within the group - const updatedGroupTab = findTabById( - updatedWorktree.tabs, - parentGroup.id, - ); - if ( - updatedGroupTab && - updatedGroupTab.tabs && - updatedGroupTab.tabs.length > 0 - ) { - const newIndex = Math.min( - currentIndex, - updatedGroupTab.tabs.length - 1, - ); - handleTabSelect( - savedWorktreeId, - updatedGroupTab.tabs[newIndex].id, - ); - } else { - setSelectedTabId(null); - } - } else if (updatedWorktree.tabs.length > 0) { - // Select adjacent top-level tab - const newIndex = Math.min( - currentIndex, - updatedWorktree.tabs.length - 1, - ); - handleTabSelect( - savedWorktreeId, - updatedWorktree.tabs[newIndex].id, - ); - } else { - setSelectedTabId(null); - } - } - } - } - }; - - window.ipcRenderer.on("terminal-exited", handleTerminalExit); - return () => { - window.ipcRenderer.off("terminal-exited", handleTerminalExit); - }; - }, [currentWorkspace, selectedWorktreeId, selectedTabId]); - - // Helper: recursively find a tab by ID - const findTabById = (tabs: Tab[], tabId: string): Tab | null => { - for (const tab of tabs) { - if (tab.id === tabId) return tab; - if (tab.type === "group" && tab.tabs) { - const found = findTabById(tab.tabs, tabId); - if (found) return found; - } - } - return null; - }; - - // Helper: Remove tab ID from mosaic tree - const removeTabFromMosaicTree = ( - tree: MosaicNode, - tabId: string, - ): MosaicNode | null => { - if (typeof tree === "string") { - // If this is the tab to remove, return null - return tree === tabId ? null : tree; - } - - // Recursively remove from branches - const newFirst = removeTabFromMosaicTree(tree.first, tabId); - const newSecond = removeTabFromMosaicTree(tree.second, tabId); - - // If both branches are gone, return null - if (!newFirst && !newSecond) { - return null; - } - - // If one branch is gone, return the other - if (!newFirst) { - return newSecond; - } - if (!newSecond) { - return newFirst; + })(); } - - // Both branches exist, keep the structure - return { - ...tree, - first: newFirst, - second: newSecond, - }; }; - // Helper: Add tab ID to mosaic tree - const addTabToMosaicTree = ( - tree: MosaicNode | null | undefined, - tabId: string, - ): MosaicNode => { - if (!tree) { - return tabId; - } - - if (typeof tree === "string") { - // Prevent duplicate IDs - if the tree already contains this tab ID, just return the tree - if (tree === tabId) { - console.warn( - `[MainScreen] Attempted to add duplicate tab ID "${tabId}" to mosaic tree`, - ); - return tree; - } - - // Single tab - create a split - return { - direction: "row", - first: tree, - second: tabId, - splitPercentage: 50, - }; - } - - // Check if the tab ID already exists in the tree (recursively) - const containsTabId = (node: MosaicNode): boolean => { - if (typeof node === "string") { - return node === tabId; - } - return containsTabId(node.first) || containsTabId(node.second); - }; - - if (containsTabId(tree)) { - console.warn( - `[MainScreen] Tab ID "${tabId}" already exists in mosaic tree, skipping addition`, - ); - return tree; - } + const handleCreateTask = (taskData: { + name: string; + description: string; + status: TaskStatus; + assignee: string; + branch: string; + }) => { + if (!currentWorkspace) return; - // Tree node - add to the second branch - return { - ...tree, - second: addTabToMosaicTree(tree.second, tabId), + // Create pending worktree for optimistic update + const pendingId = `pending-${Date.now()}`; + const pendingWorktree: PendingWorktree = { + id: pendingId, + isPending: true, + title: taskData.name, + branch: taskData.branch, + description: taskData.description, + taskData: { + slug: "...", // Will be generated by backend + name: taskData.name, + status: taskData.status, + }, }; - }; - - // Drag and drop handlers - const handleDragStart = (event: DragStartEvent) => { - setActiveId(event.active.id as string); - setIsOverMainContent(false); - }; - - const handleDragOver = (event: DragOverEvent) => { - const overId = event.over?.id; - setIsOverMainContent(overId === "main-content-drop-zone"); - }; - - const handleDragEnd = async (event: DragEndEvent) => { - const { active, over } = event; - setActiveId(null); - setIsOverMainContent(false); - if (!over || active.id === over.id) return; - - const activeData = active.data.current; - const overData = over.data.current; - - // Only handle tab dragging - if (activeData?.type !== "tab") { - return; - } - - // Handle dropping onto the main content area - if (over.id === "main-content-drop-zone") { - const activeData = active.data.current; - const draggedTabId = active.id as string; - - // Only handle tab dragging - if (activeData?.type !== "tab") { - return; - } + // Add pending worktree immediately + setPendingWorktrees((prev) => [...prev, pendingWorktree]); + handleCloseAddTaskModal(); - const draggedWorktreeId = activeData.worktreeId as string; - - // Check if the dragged tab is from the same worktree as the currently selected tab - if (draggedWorktreeId !== selectedWorktreeId) { - console.log( - "[MainScreen] Cannot drop tab from different worktree onto main content", - ); - return; - } - - if (!currentWorkspace || !selectedWorktreeId) return; - - const worktree = currentWorkspace.worktrees.find( - (wt) => wt.id === selectedWorktreeId, - ); - if (!worktree) return; - - const draggedTab = findTabById(worktree.tabs, draggedTabId); - if (!draggedTab || draggedTab.type === "group") { - console.log("[MainScreen] Cannot drop group tabs onto main content"); - return; - } - - // Case 1: Currently viewing a group tab - add the dragged tab to that group - if (selectedTab?.type === "group") { - try { - const parentTabId = activeData.parentTabId; - - // Check if the dragged tab is already in this group - const isAlreadyInGroup = selectedTab.tabs?.some( - (t) => t.id === draggedTabId, - ); - if (isAlreadyInGroup) { - console.log( - "[MainScreen] Tab is already in this group - creating duplicate tab for split", - ); - - // Find the original tab to get its properties - const originalTab = findTabById(worktree.tabs, draggedTabId); - if (!originalTab) { - console.error( - "[MainScreen] Could not find original tab:", - draggedTabId, - ); - return; - } - - // Create a new duplicate tab - const newTab = await createTab( - currentWorkspace.id, - selectedWorktreeId, - originalTab.name, - originalTab.type, - ); - - if (!newTab) return; - - // Move the new tab into the group - const moveResult = await window.ipcRenderer.invoke("tab-move", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: newTab.id, - sourceParentTabId: undefined, - targetParentTabId: selectedTab.id, - targetIndex: selectedTab.tabs?.length || 0, - }); - - if (!moveResult.success) { - console.error( - "[MainScreen] Failed to move tab:", - moveResult.error, - ); - return; - } - - // Update the mosaic tree to include the new tab - const updatedMosaicTree = addTabToMosaicTree( - selectedTab.mosaicTree, - newTab.id, - ); - - await window.ipcRenderer.invoke("tab-update-mosaic-tree", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: selectedTab.id, - mosaicTree: updatedMosaicTree, - }); - - // Refresh workspace to show the updated structure - const refreshedWorkspace = await window.ipcRenderer.invoke( - "workspace-get", - currentWorkspace.id, - ); - if (refreshedWorkspace) { - setCurrentWorkspace(refreshedWorkspace); - } - return; - } - - // Move the tab into the group - const moveResult = await window.ipcRenderer.invoke("tab-move", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: draggedTabId, - sourceParentTabId: parentTabId, - targetParentTabId: selectedTab.id, - targetIndex: selectedTab.tabs?.length || 0, - }); - - if (!moveResult.success) { - console.error("[MainScreen] Failed to move tab:", moveResult.error); - return; - } - - // Update the mosaic tree to include the new tab - const updatedMosaicTree = addTabToMosaicTree( - selectedTab.mosaicTree, - draggedTabId, - ); - - await window.ipcRenderer.invoke("tab-update-mosaic-tree", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: selectedTab.id, - mosaicTree: updatedMosaicTree, - }); + void (async () => { + try { + // Create a worktree for this task + const result = await window.ipcRenderer.invoke("worktree-create", { + workspaceId: currentWorkspace.id, + title: taskData.name, + branch: taskData.branch, + createBranch: true, + description: taskData.description, + }); - // Refresh workspace to show the updated structure - const refreshedWorkspace = await window.ipcRenderer.invoke( - "workspace-get", - currentWorkspace.id, + if (result.success && result.worktree) { + // Remove pending worktree + setPendingWorktrees((prev) => + prev.filter((wt) => wt.id !== pendingId), ); - if (refreshedWorkspace) { - setCurrentWorkspace(refreshedWorkspace); - } - } catch (error) { - console.error("[MainScreen] Error adding tab to group:", error); - } - } - // Case 2: Currently viewing a single tab - create a new group with both tabs - else if (selectedTab) { - try { - // If dragging a tab onto itself, create a new duplicate tab for the split - let secondTabId = draggedTabId; - const parentTabId = activeData.parentTabId; - - if (draggedTabId === selectedTab.id) { - console.log( - "[MainScreen] Dragging tab onto itself - creating duplicate tab for split", - ); - // Create a new tab with the same type and name - const newTab = await createTab( - currentWorkspace.id, - selectedWorktreeId, - selectedTab.name, - selectedTab.type, - ); + // Reload workspace to get the new worktree + await handleWorktreeCreated(); - if (!newTab) return; + // Switch to the new worktree + setSelectedWorktreeId(result.worktree.id); - secondTabId = newTab.id; + // Select first tab if available + if (result.worktree.tabs && result.worktree.tabs.length > 0) { + handleTabSelect(result.worktree.id, result.worktree.tabs[0].id); } - - // Create a new group tab - const groupTab = await createTab( - currentWorkspace.id, - selectedWorktreeId, - "Tab Group", - "group", - ); - - if (!groupTab) return; - - const groupTabId = groupTab.id; - - // Move both tabs into the group - // First, move the currently selected tab - await window.ipcRenderer.invoke("tab-move", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: selectedTab.id, - sourceParentTabId: undefined, - targetParentTabId: groupTabId, - targetIndex: 0, - }); - - // Then, move the second tab (either the dragged tab or the newly created one) - await window.ipcRenderer.invoke("tab-move", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: secondTabId, - sourceParentTabId: - secondTabId === draggedTabId ? parentTabId : undefined, - targetParentTabId: groupTabId, - targetIndex: 1, - }); - - // Create a simple mosaic tree with both tabs - const mosaicTree: MosaicNode = { - direction: "row", - first: selectedTab.id, - second: secondTabId, - splitPercentage: 50, - }; - - await window.ipcRenderer.invoke("tab-update-mosaic-tree", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: groupTabId, - mosaicTree, - }); - - // Select the new group tab to show the mosaic - setSelectedTabId(groupTabId); - await window.ipcRenderer.invoke("workspace-set-active-selection", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: groupTabId, - }); - - // Refresh workspace to show the updated structure - const refreshedWorkspace = await window.ipcRenderer.invoke( - "workspace-get", - currentWorkspace.id, + } else { + // Remove pending on failure + setPendingWorktrees((prev) => + prev.filter((wt) => wt.id !== pendingId), ); - if (refreshedWorkspace) { - setCurrentWorkspace(refreshedWorkspace); - } - } catch (error) { - console.error("[MainScreen] Error creating tab group:", error); } + } catch (error) { + console.error("Failed to create task/worktree:", error); + // Remove pending on error + setPendingWorktrees((prev) => prev.filter((wt) => wt.id !== pendingId)); } - return; - } - - // Handle sidebar drag operations (reordering, moving between groups) - const draggedWorktreeId = activeData.worktreeId as string; - const draggedTabId = active.id as string; - const activeParentTabId = activeData.parentTabId; - const overParentTabId = overData?.parentTabId; + })(); + }; - if (!currentWorkspace || !draggedWorktreeId) return; + const handleCreatePR = async () => { + if (!currentWorkspace || !selectedWorktreeId) return; - const worktree = currentWorkspace.worktrees.find( - (wt) => wt.id === draggedWorktreeId, + const worktree = currentWorkspace.worktrees?.find( + (wt) => wt.id === selectedWorktreeId, ); if (!worktree) return; try { - // Dropping onto a group tab or group area - if (overData?.type === "group" || overData?.type === "group-area") { - const groupTabId = overData.groupTabId as string; - - // Don't allow dropping a tab onto its own parent - if (activeParentTabId === groupTabId) { - return; - } - - const draggedTab = findTabById(worktree.tabs, draggedTabId); - const groupTab = findTabById(worktree.tabs, groupTabId); - - if (!draggedTab || !groupTab || groupTab.type !== "group") { - console.error("[MainScreen] Invalid tab or group tab"); - return; - } - - // Move the tab into the group - const moveResult = await window.ipcRenderer.invoke("tab-move", { - workspaceId: currentWorkspace.id, - worktreeId: draggedWorktreeId, - tabId: draggedTabId, - sourceParentTabId: activeParentTabId, - targetParentTabId: groupTabId, - targetIndex: groupTab.tabs?.length || 0, - }); - - if (!moveResult.success) { - console.error("[MainScreen] Failed to move tab:", moveResult.error); - return; - } - - // Update the mosaic tree to include the new tab - const updatedMosaicTree = addTabToMosaicTree( - groupTab.mosaicTree, - draggedTabId, - ); - - await window.ipcRenderer.invoke("tab-update-mosaic-tree", { - workspaceId: currentWorkspace.id, - worktreeId: draggedWorktreeId, - tabId: groupTabId, - mosaicTree: updatedMosaicTree, - }); + const result = await window.ipcRenderer.invoke("worktree-create-pr", { + workspaceId: currentWorkspace.id, + worktreeId: selectedWorktreeId, + }); - // Refresh workspace + if (result.success) { + // Reload workspace to show updated PR state const refreshedWorkspace = await window.ipcRenderer.invoke( "workspace-get", currentWorkspace.id, @@ -1051,36 +488,39 @@ export function MainScreen() { if (refreshedWorkspace) { setCurrentWorkspace(refreshedWorkspace); } - return; - } - // Reordering within the same parent group - if (overData?.type === "tab" && activeParentTabId === overParentTabId) { - const parentTab = activeParentTabId - ? findTabById(worktree.tabs, activeParentTabId) - : null; - - const tabsArray = parentTab?.tabs || worktree.tabs; - const oldIndex = tabsArray.findIndex((t) => t.id === active.id); - const newIndex = tabsArray.findIndex((t) => t.id === over.id); + // Open PR URL in default browser only if we have a valid URL + // (--web mode opens browser automatically, so we don't need to open it again) + if (result.prUrl && result.prUrl.startsWith("http")) { + await window.ipcRenderer.invoke("open-external", result.prUrl); + } + } else { + // Show error as alert + alert(`Failed to create PR: ${result.error || "Unknown error"}`); + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + alert(`Failed to create PR: ${errorMessage}`); + } + }; - if (oldIndex === -1 || newIndex === -1) return; + const handleMergePR = async () => { + if (!currentWorkspace || !selectedWorktreeId) return; - // Save to backend - const reorderedTabs = arrayMove(tabsArray, oldIndex, newIndex); - const newOrder = reorderedTabs.map((t) => t.id); - const result = await window.ipcRenderer.invoke("tab-reorder", { - workspaceId: currentWorkspace.id, - worktreeId: draggedWorktreeId, - parentTabId: activeParentTabId, - tabIds: newOrder, - }); + const worktree = currentWorkspace.worktrees?.find( + (wt) => wt.id === selectedWorktreeId, + ); + if (!worktree) return; - if (!result.success) { - console.error("[MainScreen] Failed to reorder tabs:", result.error); - } + try { + const result = await window.ipcRenderer.invoke("worktree-merge-pr", { + workspaceId: currentWorkspace.id, + worktreeId: selectedWorktreeId, + }); - // Refresh workspace + if (result.success) { + // Reload workspace to show updated state const refreshedWorkspace = await window.ipcRenderer.invoke( "workspace-get", currentWorkspace.id, @@ -1088,706 +528,223 @@ export function MainScreen() { if (refreshedWorkspace) { setCurrentWorkspace(refreshedWorkspace); } - } - // Moving to a different parent group - else if ( - overData?.type === "tab" && - activeParentTabId !== overParentTabId - ) { - const targetParentTabId = overParentTabId; - - if (targetParentTabId) { - const draggedTab = findTabById(worktree.tabs, draggedTabId); - const targetGroupTab = findTabById(worktree.tabs, targetParentTabId); - - if ( - !draggedTab || - !targetGroupTab || - targetGroupTab.type !== "group" - ) { - console.error("[MainScreen] Invalid tab or target group"); - return; - } - - // Move the tab into the group - const moveResult = await window.ipcRenderer.invoke("tab-move", { - workspaceId: currentWorkspace.id, - worktreeId: draggedWorktreeId, - tabId: draggedTabId, - sourceParentTabId: activeParentTabId, - targetParentTabId: targetParentTabId, - targetIndex: targetGroupTab.tabs?.length || 0, - }); - - if (!moveResult.success) { - console.error("[MainScreen] Failed to move tab:", moveResult.error); - return; - } - - // Update the mosaic tree to include the new tab - const updatedMosaicTree = addTabToMosaicTree( - targetGroupTab.mosaicTree, - draggedTabId, - ); - - await window.ipcRenderer.invoke("tab-update-mosaic-tree", { - workspaceId: currentWorkspace.id, - worktreeId: draggedWorktreeId, - tabId: targetParentTabId, - mosaicTree: updatedMosaicTree, - }); - - // Refresh workspace - const refreshedWorkspace = await window.ipcRenderer.invoke( - "workspace-get", - currentWorkspace.id, - ); - if (refreshedWorkspace) { - setCurrentWorkspace(refreshedWorkspace); - } - } + alert("PR merged successfully!"); + } else { + // Show error as alert + alert(`Failed to merge PR: ${result.error || "Unknown error"}`); } } catch (error) { - console.error("[MainScreen] Error during sidebar drag operation:", error); + const errorMessage = + error instanceof Error ? error.message : String(error); + alert(`Failed to merge PR: ${errorMessage}`); } }; - // Get active item for drag overlay - const activeTab = - activeId && selectedWorktree - ? findTabById(selectedWorktree.tabs, activeId) - : null; - - // Set up keyboard shortcuts + // Load active workspace on mount useEffect(() => { - const workspaceShortcuts = createWorkspaceShortcuts({ - switchToPrevWorkspace: () => { - if (!workspaces || !currentWorkspace) return; - const currentIndex = workspaces.findIndex( - (ws) => ws.id === currentWorkspace.id, - ); - if (currentIndex > 0) { - handleWorkspaceSelect(workspaces[currentIndex - 1].id); - } - }, - switchToNextWorkspace: () => { - if (!workspaces || !currentWorkspace) return; - const currentIndex = workspaces.findIndex( - (ws) => ws.id === currentWorkspace.id, - ); - if (currentIndex < workspaces.length - 1) { - handleWorkspaceSelect(workspaces[currentIndex + 1].id); - } - }, - toggleSidebar: () => { - const panel = sidebarPanelRef.current; - if (!panel) return; - - if (panel.isCollapsed()) { - panel.expand(); - setIsSidebarOpen(true); - } else { - panel.collapse(); - setIsSidebarOpen(false); - } - }, - createSplitView: async () => { - // Create horizontal split - if (!currentWorkspace || !selectedWorktreeId || !selectedTab) return; - - // If we're inside a group (parentGroupTab exists), add to that group - if (parentGroupTab) { - const newTab = await createTab( - currentWorkspace.id, - selectedWorktreeId, - "Terminal", - "terminal", - ); - if (!newTab) return; - - // Move into the parent group - await window.ipcRenderer.invoke("tab-move", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: newTab.id, - sourceParentTabId: undefined, - targetParentTabId: parentGroupTab.id, - targetIndex: parentGroupTab.tabs?.length || 0, - }); - - // Update mosaic tree (horizontal split) - add to existing group's mosaic - const updatedMosaicTree = addTabToMosaicTree( - parentGroupTab.mosaicTree, - newTab.id, - ); - - await window.ipcRenderer.invoke("tab-update-mosaic-tree", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: parentGroupTab.id, - mosaicTree: updatedMosaicTree, - }); - - // Select the newly created terminal - setSelectedTabId(newTab.id); - await window.ipcRenderer.invoke("workspace-set-active-selection", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: newTab.id, - }); - } else { - // Create new group with horizontal split - const newTab = await createTab( - currentWorkspace.id, - selectedWorktreeId, - "Terminal", - "terminal", - ); - if (!newTab) return; - - const groupTab = await createTab( - currentWorkspace.id, - selectedWorktreeId, - "Tab Group", - "group", - ); - if (!groupTab) return; - - // Move both tabs into group - await window.ipcRenderer.invoke("tab-move", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: selectedTab.id, - sourceParentTabId: undefined, - targetParentTabId: groupTab.id, - targetIndex: 0, - }); - - await window.ipcRenderer.invoke("tab-move", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: newTab.id, - sourceParentTabId: undefined, - targetParentTabId: groupTab.id, - targetIndex: 1, - }); - - // Create horizontal mosaic tree - const mosaicTree: MosaicNode = { - direction: "row", - first: selectedTab.id, - second: newTab.id, - splitPercentage: 50, - }; - - await window.ipcRenderer.invoke("tab-update-mosaic-tree", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: groupTab.id, - mosaicTree, - }); + const loadActiveWorkspace = async () => { + try { + setLoading(true); + setError(null); - // Select the newly created terminal (not the group) - setSelectedTabId(newTab.id); - await window.ipcRenderer.invoke("workspace-set-active-selection", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: newTab.id, - }); - } + await loadAllWorkspaces(); - // Refresh workspace - const refreshedWorkspace = await window.ipcRenderer.invoke( - "workspace-get", - currentWorkspace.id, + let workspaceId = await window.ipcRenderer.invoke( + "workspace-get-active-workspace-id", ); - if (refreshedWorkspace) { - setCurrentWorkspace(refreshedWorkspace); - // Also refresh workspaces list for sidebar - await loadAllWorkspaces(); - } - }, - createVerticalSplit: async () => { - // Create vertical split - if (!currentWorkspace || !selectedWorktreeId || !selectedTab) return; - - // If we're inside a group (parentGroupTab exists), add to that group - if (parentGroupTab) { - const newTab = await createTab( - currentWorkspace.id, - selectedWorktreeId, - "Terminal", - "terminal", - ); - if (!newTab) return; - - // Move into the parent group - await window.ipcRenderer.invoke("tab-move", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: newTab.id, - sourceParentTabId: undefined, - targetParentTabId: parentGroupTab.id, - targetIndex: parentGroupTab.tabs?.length || 0, - }); - - const first = parentGroupTab.mosaicTree; - - if (!first) { - console.error( - "Failed to create vertical split: parentGroupTab.mosaicTree is undefined", - ); - return; - } - - // Update mosaic tree with column direction for vertical split - const updatedMosaicTree: MosaicNode = { - direction: "column", - first, - second: newTab.id, - splitPercentage: 50, - } satisfies MosaicNode; - - await window.ipcRenderer.invoke("tab-update-mosaic-tree", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: parentGroupTab.id, - mosaicTree: updatedMosaicTree, - }); - - // Select the newly created terminal - setSelectedTabId(newTab.id); - await window.ipcRenderer.invoke("workspace-set-active-selection", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: newTab.id, - }); - } else { - // Create new group with vertical split - const newTab = await createTab( - currentWorkspace.id, - selectedWorktreeId, - "Terminal", - "terminal", - ); - if (!newTab) return; - const groupTab = await createTab( - currentWorkspace.id, - selectedWorktreeId, - "Tab Group", - "group", + if (!workspaceId) { + const lastOpenedWorkspace = await window.ipcRenderer.invoke( + "workspace-get-last-opened", ); - if (!groupTab) return; - - // Move both tabs into group - await window.ipcRenderer.invoke("tab-move", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: selectedTab.id, - sourceParentTabId: undefined, - targetParentTabId: groupTab.id, - targetIndex: 0, - }); - - await window.ipcRenderer.invoke("tab-move", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: newTab.id, - sourceParentTabId: undefined, - targetParentTabId: groupTab.id, - targetIndex: 1, - }); - - // Create vertical mosaic tree - const mosaicTree: MosaicNode = { - direction: "column", - first: selectedTab.id, - second: newTab.id, - splitPercentage: 50, - }; - - await window.ipcRenderer.invoke("tab-update-mosaic-tree", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: groupTab.id, - mosaicTree, - }); - - // Select the newly created terminal (not the group) - setSelectedTabId(newTab.id); - await window.ipcRenderer.invoke("workspace-set-active-selection", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: newTab.id, - }); - } - - // Refresh workspace - const refreshedWorkspace = await window.ipcRenderer.invoke( - "workspace-get", - currentWorkspace.id, - ); - if (refreshedWorkspace) { - setCurrentWorkspace(refreshedWorkspace); - // Also refresh workspaces list for sidebar - await loadAllWorkspaces(); + workspaceId = lastOpenedWorkspace?.id ?? null; } - }, - }); - const tabShortcuts = createTabShortcuts({ - switchToPrevTab: () => { - if (!selectedWorktree || !selectedTabId) return; - - // If we're inside a group tab, navigate between group's children - if (parentGroupTab && parentGroupTab.tabs) { - const tabs = parentGroupTab.tabs; - const currentIndex = tabs.findIndex((t) => t.id === selectedTabId); - if (currentIndex > 0) { - handleTabSelect(selectedWorktree.id, tabs[currentIndex - 1].id); - } - } else { - // Navigate between top-level tabs - const tabs = selectedWorktree.tabs; - const currentIndex = tabs.findIndex((t) => t.id === selectedTabId); - if (currentIndex > 0) { - handleTabSelect(selectedWorktree.id, tabs[currentIndex - 1].id); - } - } - }, - switchToNextTab: () => { - if (!selectedWorktree || !selectedTabId) return; - - // If we're inside a group tab, navigate between group's children - if (parentGroupTab && parentGroupTab.tabs) { - const tabs = parentGroupTab.tabs; - const currentIndex = tabs.findIndex((t) => t.id === selectedTabId); - if (currentIndex < tabs.length - 1) { - handleTabSelect(selectedWorktree.id, tabs[currentIndex + 1].id); - } - } else { - // Navigate between top-level tabs - const tabs = selectedWorktree.tabs; - const currentIndex = tabs.findIndex((t) => t.id === selectedTabId); - if (currentIndex < tabs.length - 1) { - handleTabSelect(selectedWorktree.id, tabs[currentIndex + 1].id); - } - } - }, - newTab: async () => { - if (!currentWorkspace || !selectedWorktreeId) return; - - try { - const result = await window.ipcRenderer.invoke("tab-create", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - name: "New Terminal", - type: "terminal", - }); - - if (result.success && result.tab) { - const newTabId = result.tab.id; + if (workspaceId) { + const workspace = await window.ipcRenderer.invoke( + "workspace-get", + workspaceId, + ); - // Select the new tab first (matches sidebar button behavior) - handleTabSelect(selectedWorktreeId, newTabId); + if (workspace) { + setCurrentWorkspace(workspace); - // Then refresh workspace to get updated data - const refreshedWorkspace = await window.ipcRenderer.invoke( - "workspace-get", - currentWorkspace.id, + const activeSelection = await window.ipcRenderer.invoke( + "workspace-get-active-selection", + workspaceId, ); - if (refreshedWorkspace) { - setCurrentWorkspace(refreshedWorkspace); - // Also refresh workspaces list for sidebar - await loadAllWorkspaces(); + if (activeSelection?.worktreeId && activeSelection?.tabId) { + setSelectedWorktreeId(activeSelection.worktreeId); + setSelectedTabId(activeSelection.tabId); } - } else { - console.error("Failed to create tab:", result.error); } - } catch (error) { - console.error("Error creating new tab:", error); - } - }, - closeTab: async () => { - if (!currentWorkspace || !selectedWorktreeId || !selectedTabId) return; - - // Check if we're inside a group tab - const isInGroup = !!parentGroupTab; - const tabs = isInGroup - ? parentGroupTab?.tabs || [] - : selectedWorktree?.tabs || []; - const currentIndex = tabs.findIndex((t) => t.id === selectedTabId); - const tabToClose = selectedTabId; - - // Delete the tab first - const result = await window.ipcRenderer.invoke("tab-delete", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: tabToClose, - }); - - if (!result.success) { - console.error("Failed to close tab:", result.error); - return; - } - - // Then update mosaic tree if in a group (after deletion) - if (isInGroup && parentGroupTab && parentGroupTab.mosaicTree) { - const updatedMosaicTree = removeTabFromMosaicTree( - parentGroupTab.mosaicTree as MosaicNode, - tabToClose, - ); - - await window.ipcRenderer.invoke("tab-update-mosaic-tree", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId: parentGroupTab.id, - mosaicTree: updatedMosaicTree, - }); } + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } finally { + setLoading(false); + } + }; - // Refresh workspace to get updated tab list - const refreshedWorkspace = await window.ipcRenderer.invoke( - "workspace-get", - currentWorkspace.id, - ); - - if (refreshedWorkspace) { - // Force update workspaces list first for sidebar - await loadAllWorkspaces(); - - // Update workspace state with new object reference - setCurrentWorkspace(refreshedWorkspace); - - // Find the worktree and updated parent group if applicable - const updatedWorktree = refreshedWorkspace.worktrees.find( - (wt) => wt.id === selectedWorktreeId, - ); - - if (updatedWorktree) { - // Re-find the parent group from the refreshed workspace - const wasInGroup = isInGroup; - const oldParentId = parentGroupTab?.id; + loadActiveWorkspace(); + }, []); - if (wasInGroup && oldParentId) { - const updatedGroupTab = findTabById( - updatedWorktree.tabs, - oldParentId, - ); - if ( - updatedGroupTab && - updatedGroupTab.tabs && - updatedGroupTab.tabs.length > 0 - ) { - // Select adjacent tab within the group - const newIndex = Math.min( - currentIndex, - updatedGroupTab.tabs.length - 1, - ); - handleTabSelect( - selectedWorktreeId, - updatedGroupTab.tabs[newIndex].id, - ); - } else { - // Group is now empty, clear selection - setSelectedTabId(null); - } - } else if (updatedWorktree.tabs.length > 0) { - // Top-level tab - select adjacent top-level tab - const newIndex = Math.min( - currentIndex, - updatedWorktree.tabs.length - 1, - ); - handleTabSelect( - selectedWorktreeId, - updatedWorktree.tabs[newIndex].id, - ); - } else { - // No tabs left, clear selection - setSelectedTabId(null); - } - } - } - }, - reopenClosedTab: () => { - // TODO: implement reopen closed tab - console.log("Reopen closed tab"); - }, - jumpToTab: (index: number) => { - if (!selectedWorktree) return; - - // Flatten tabs: expand group children to number all actual terminals - const flattenTabs = (tabs: Tab[]): Tab[] => { - const result: Tab[] = []; - for (const tab of tabs) { - if (tab.type === "group" && tab.tabs && tab.tabs.length > 0) { - // Add all children of the group - result.push(...tab.tabs); - } else { - // Add non-group tabs - result.push(tab); - } - } - return result; - }; + // Listen for workspace-opened event + useEffect(() => { + const handler = async (workspace: Workspace) => { + console.log( + "[MainScreen] Workspace opened event received:", + workspace, + ); + setLoading(false); - const flatTabs = flattenTabs(selectedWorktree.tabs); - if (index > 0 && index <= flatTabs.length) { - handleTabSelect(selectedWorktree.id, flatTabs[index - 1].id); - } - }, - }); + await window.ipcRenderer.invoke( + "workspace-set-active-workspace-id", + workspace.id, + ); + await loadAllWorkspaces(); - const handleKeyDown = (event: KeyboardEvent) => { - // Try workspace shortcuts first - const workspaceHandler = createShortcutHandler( - workspaceShortcuts.shortcuts, + const refreshedWorkspace = await window.ipcRenderer.invoke( + "workspace-get", + workspace.id, ); - if (!workspaceHandler(event)) { - return; + if (refreshedWorkspace) { + setCurrentWorkspace(refreshedWorkspace); } - - // Then try tab shortcuts - const tabHandler = createShortcutHandler(tabShortcuts.shortcuts); - tabHandler(event); }; - // Use capture phase to intercept events before they reach terminal - window.addEventListener("keydown", handleKeyDown, true); + window.ipcRenderer.on("workspace-opened", handler); return () => { - window.removeEventListener("keydown", handleKeyDown, true); + window.ipcRenderer.off("workspace-opened", handler); }; - }, [ - workspaces, - currentWorkspace, - selectedWorktree, - selectedWorktreeId, - selectedTabId, - ]); + }, []); return ( - -
- - - {/* Hover trigger area when sidebar is hidden */} - {!isSidebarOpen && ( -
setShowSidebarOverlay(true)} + <> + + + {/* Hover trigger area when sidebar is hidden */} + {!isSidebarOpen && ( +
setShowSidebarOverlay(true)} + /> + )} + + {/* Sidebar overlay when hidden and hovering */} + {!isSidebarOpen && showSidebarOverlay && workspaces && ( +
setShowSidebarOverlay(false)} + > +
+ { + setShowSidebarOverlay(false); + }} + onShowDiff={handleShowDiff} + /> +
+
+ )} + + +
+ {/* Worktree tabs at the top - each tab represents a worktree */} + { + // Don't allow selecting pending worktrees + if (worktreeId.startsWith("pending-")) return; + + setSelectedWorktreeId(worktreeId); + // Select first tab in the worktree + const worktree = currentWorkspace?.worktrees?.find( + (wt) => wt.id === worktreeId, + ); + if (worktree && worktree.tabs && worktree.tabs.length > 0) { + handleTabSelect(worktreeId, worktree.tabs[0].id); + } + }} + mode={mode} + onModeChange={setMode} /> - )} - - {/* Sidebar overlay when hidden and hovering */} - {!isSidebarOpen && showSidebarOverlay && workspaces && ( -
setShowSidebarOverlay(false)} - > -
- + {mode === "plan" ? ( + // Plan mode - show kanban board + { - setShowSidebarOverlay(false); - }} - isDragging={!!activeId} - onShowDiff={handleShowDiffView} + onTabCreated={handleTabCreated} /> -
-
- )} - - {/* App Frame - continuous border + sidebar + topbar */} - - - setIsSidebarOpen(false)} - onExpand={() => setIsSidebarOpen(true)} - > - {isSidebarOpen && workspaces && ( - { - const panel = sidebarPanelRef.current; - if (panel && !panel.isCollapsed()) { - panel.collapse(); - } - }} - isDragging={!!activeId} - onShowDiff={handleShowDiffView} - /> - )} - - - {/* Main Content Area */} - -
- {/* Top Bar */} - { - const panel = sidebarPanelRef.current; - if (panel && panel.isCollapsed()) { - panel.expand(); - } - }} - workspaceName={currentWorkspace?.name} - currentBranch={currentWorkspace?.branch} - /> - - {/* Content Area */} - - {showDiffView && - diffWorktreeId && - diffWorktree && - currentWorkspace ? ( - // Show diff view -
- -
- ) : loading || - error || - !currentWorkspace || - !selectedTab || - !selectedWorktree ? ( + ) : ( + // Edit mode - show workspace/terminal view + + {/* Sidebar panel with full workspace/worktree management */} + setIsSidebarOpen(false)} + onExpand={() => setIsSidebarOpen(true)} + > + {isSidebarOpen && workspaces && ( + { + const panel = sidebarPanelRef.current; + if (panel && !panel.isCollapsed()) { + panel.collapse(); + } + }} + onShowDiff={handleShowDiff} + /> + )} + + + + + {/* Main content panel */} + + {loading || + error || + !currentWorkspace || + !selectedTab || + !selectedWorktree ? ( ) : selectedTab.type === "group" ? ( // Selected tab is a group tab → display its mosaic layout @@ -1821,8 +780,20 @@ export function MainScreen() { workspaceName={currentWorkspace.name} mainBranch={currentWorkspace.branch} /> + ) : selectedTab.type === "diff" ? ( + // Diff tab → display diff view +
+ +
) : ( - // Base level tab (not inside a group) → display full width/height + // Base level tab (terminal, preview, etc.) → display full width/height
)} -
-
-
-
-
-
- - {/* Drag Overlay - follows the cursor */} - - {activeTab ? ( -
- {activeTab.name} + + + )}
- ) : null} -
- +
+ + + {/* Open Task Modal */} + + ); } diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/AddTaskModal.tsx b/apps/desktop/src/renderer/screens/main/components/AddTaskModal.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/AddTaskModal.tsx rename to apps/desktop/src/renderer/screens/main/components/AddTaskModal.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/Avatar.tsx b/apps/desktop/src/renderer/screens/main/components/Avatar.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/Avatar.tsx rename to apps/desktop/src/renderer/screens/main/components/Avatar.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/MainContent/components/DroppableMainContent/DroppableMainContent.tsx b/apps/desktop/src/renderer/screens/main/components/MainContent/components/DroppableMainContent/DroppableMainContent.tsx new file mode 100644 index 00000000000..aa7ceabad24 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/MainContent/components/DroppableMainContent/DroppableMainContent.tsx @@ -0,0 +1,41 @@ +import { useDroppable } from "@dnd-kit/core"; +import type React from "react"; + +interface DroppableMainContentProps { + children: React.ReactNode; + isOver: boolean; +} + +/** + * Droppable wrapper for main content area + */ +export function DroppableMainContent({ + children, + isOver, +}: DroppableMainContentProps) { + const { setNodeRef } = useDroppable({ + id: "main-content-drop-zone", + data: { + type: "main-content", + }, + }); + + return ( +
+ {children} + {isOver && ( +
+
+ Drop to add to split view +
+
+ )} +
+ ); +} + diff --git a/apps/desktop/src/renderer/screens/main/components/MainContent/components/DroppableMainContent/index.ts b/apps/desktop/src/renderer/screens/main/components/MainContent/components/DroppableMainContent/index.ts new file mode 100644 index 00000000000..0cb1b6f06a9 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/MainContent/components/DroppableMainContent/index.ts @@ -0,0 +1,2 @@ +export { DroppableMainContent } from "./DroppableMainContent"; + diff --git a/apps/desktop/src/renderer/screens/main/components/MainContent/utils.ts b/apps/desktop/src/renderer/screens/main/components/MainContent/utils.ts new file mode 100644 index 00000000000..953d43e3454 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/MainContent/utils.ts @@ -0,0 +1,134 @@ +import type { MosaicNode, Tab } from "shared/types"; + +/** + * Renderer-side utility functions for tab operations + */ + +/** + * Find a tab recursively (for finding sub-tabs inside groups) + * Returns the tab and its parent (if it's a sub-tab) + */ +export function findTabRecursive( + tabs: Tab[] | undefined, + tabId: string, +): { tab: Tab; parent?: Tab } | null { + if (!tabs) return null; + + for (const tab of tabs) { + if (tab.id === tabId) { + return { tab }; + } + // Check if this tab is a group tab with children + if (tab.type === "group" && tab.tabs) { + for (const childTab of tab.tabs) { + if (childTab.id === tabId) { + return { tab: childTab, parent: tab }; + } + } + } + } + return null; +} + +/** + * Recursively find a tab by ID + */ +export function findTabById(tabs: Tab[], tabId: string): Tab | null { + for (const tab of tabs) { + if (tab.id === tabId) return tab; + if (tab.type === "group" && tab.tabs) { + const found = findTabById(tab.tabs, tabId); + if (found) return found; + } + } + return null; +} + +/** + * Remove tab ID from mosaic tree + */ +export function removeTabFromMosaicTree( + tree: MosaicNode, + tabId: string, +): MosaicNode | null { + if (typeof tree === "string") { + // If this is the tab to remove, return null + return tree === tabId ? null : tree; + } + + // Recursively remove from branches + const newFirst = removeTabFromMosaicTree(tree.first, tabId); + const newSecond = removeTabFromMosaicTree(tree.second, tabId); + + // If both branches are gone, return null + if (!newFirst && !newSecond) { + return null; + } + + // If one branch is gone, return the other + if (!newFirst) { + return newSecond; + } + if (!newSecond) { + return newFirst; + } + + // Both branches exist, keep the structure + return { + ...tree, + first: newFirst, + second: newSecond, + }; +} + +/** + * Add tab ID to mosaic tree + */ +export function addTabToMosaicTree( + tree: MosaicNode | null | undefined, + tabId: string, +): MosaicNode { + if (!tree) { + return tabId; + } + + if (typeof tree === "string") { + // Prevent duplicate IDs - if the tree already contains this tab ID, just return the tree + if (tree === tabId) { + console.warn( + `[MainScreen] Attempted to add duplicate tab ID "${tabId}" to mosaic tree`, + ); + return tree; + } + + // Single tab - create a split + return { + direction: "row", + first: tree, + second: tabId, + splitPercentage: 50, + }; + } + + // Check if the tab ID already exists in the tree (recursively) + const containsTabId = (node: MosaicNode): boolean => { + if (typeof node === "string") { + return node === tabId; + } + return containsTabId(node.first) || containsTabId(node.second); + }; + + if (containsTabId(tree)) { + console.warn( + `[MainScreen] Tab ID "${tabId}" already exists in mosaic tree, skipping addition`, + ); + return tree; + } + + // Tree node - add to the second branch + return { + ...tree, + second: addTabToMosaicTree(tree.second, tabId), + }; +} + diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx deleted file mode 100644 index e0d521ac74b..00000000000 --- a/apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx +++ /dev/null @@ -1,1116 +0,0 @@ -import { - ResizableHandle, - ResizablePanel, - ResizablePanelGroup, -} from "@superset/ui/resizable"; -import type React from "react"; -import { useEffect, useRef, useState } from "react"; -import type { ImperativePanelHandle } from "react-resizable-panels"; -import type { Tab, Workspace, Worktree } from "shared/types"; -import { WorktreeProvider } from "../../../../contexts/WorktreeContext"; -import { AppFrame } from "../AppFrame"; -import { Background } from "../Background"; -import TabContent from "../MainContent/TabContent"; -import TabGroup from "../MainContent/TabGroup"; -import { PlaceholderState } from "../PlaceholderState"; -import { PlanView } from "../PlanView"; -import { Sidebar } from "../Sidebar"; -import { DiffTab } from "../TabContent/components/DiffTab"; -import { AddTaskModal } from "./AddTaskModal"; -import type { TaskStatus } from "./StatusIndicator"; -import { TaskTabs, type WorktreeWithTask } from "./TaskTabs"; -import { WorktreeTabView } from "./WorktreeTabView"; - -// Type alias for task data used in UI -type UITask = { - id: string; - slug: string; - name: string; - status: TaskStatus; - branch: string; - description: string; - assignee: string; - assigneeAvatarUrl: string; - lastUpdated: string; -}; - -// Type for pending worktrees (optimistic updates) -type PendingWorktree = { - id: string; - isPending: true; - title: string; - branch: string; - description?: string; - taskData?: { - slug: string; - name: string; - status: TaskStatus; - }; -}; - -// Mock tasks data - TODO: Replace with actual task data from backend -const MOCK_TASKS = [ - { - id: "1", - slug: "SSET-1", - name: "Homepage Redesign", - status: "working" as const, - branch: "feature/homepage-redesign", - description: "Redesigning the homepage with new branding and improved UX", - assignee: "Alice", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=1", - lastUpdated: "2 hours ago", - }, - { - id: "2", - slug: "SSET-2", - name: "API Integration", - status: "needs-feedback" as const, - branch: "feature/api-integration", - description: "Integrate new REST API endpoints for user management", - assignee: "Bob", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=12", - lastUpdated: "1 day ago", - }, - { - id: "3", - slug: "SSET-3", - name: "Bug Fixes", - status: "planning" as const, - branch: "fix/various-bugs", - description: "Collection of bug fixes reported by users", - assignee: "Charlie", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=33", - lastUpdated: "3 days ago", - }, - { - id: "4", - slug: "SSET-4", - name: "Performance Optimization", - status: "ready-to-merge" as const, - branch: "perf/optimize-queries", - description: "Optimize database queries for faster page loads", - assignee: "Diana", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=9", - lastUpdated: "5 minutes ago", - }, - { - id: "5", - slug: "SSET-5", - name: "User Authentication System", - status: "working" as const, - branch: "feature/auth-system", - description: - "Implement OAuth2 and JWT-based authentication system with refresh tokens", - assignee: "Eve", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=5", - lastUpdated: "3 hours ago", - }, - { - id: "6", - slug: "SSET-6", - name: "Dark Mode Support", - status: "planning" as const, - branch: "feature/dark-mode", - description: "Add dark mode theme support across the entire application", - assignee: "Frank", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=13", - lastUpdated: "2 days ago", - }, - { - id: "7", - slug: "SSET-7", - name: "Database Migration Scripts", - status: "ready-to-merge" as const, - branch: "db/migration-scripts", - description: - "Create automated migration scripts for production database updates", - assignee: "Grace", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=20", - lastUpdated: "1 hour ago", - }, - { - id: "8", - slug: "SSET-8", - name: "Email Notification Service", - status: "needs-feedback" as const, - branch: "feature/email-notifications", - description: - "Build email notification service using SendGrid for transactional emails", - assignee: "Henry", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=8", - lastUpdated: "4 hours ago", - }, - { - id: "9", - slug: "SSET-9", - name: "Mobile Responsive Design", - status: "working" as const, - branch: "feature/mobile-responsive", - description: - "Make the application fully responsive for mobile and tablet devices", - assignee: "Iris", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=16", - lastUpdated: "6 hours ago", - }, - { - id: "10", - slug: "SSET-10", - name: "Analytics Dashboard", - status: "planning" as const, - branch: "feature/analytics-dashboard", - description: - "Create admin dashboard with charts and metrics for user analytics", - assignee: "Jack", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=11", - lastUpdated: "1 week ago", - }, - { - id: "11", - slug: "SSET-11", - name: "CI/CD Pipeline", - status: "ready-to-merge" as const, - branch: "devops/ci-cd-pipeline", - description: - "Set up automated CI/CD pipeline with GitHub Actions and Docker", - assignee: "Kate", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=25", - lastUpdated: "30 minutes ago", - }, - { - id: "12", - slug: "SSET-12", - name: "Search Functionality", - status: "working" as const, - branch: "feature/search", - description: "Implement full-text search with Elasticsearch integration", - assignee: "Liam", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=14", - lastUpdated: "5 hours ago", - }, - { - id: "13", - slug: "SSET-13", - name: "File Upload System", - status: "needs-feedback" as const, - branch: "feature/file-uploads", - description: - "Build secure file upload system with S3 storage and virus scanning", - assignee: "Mia", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=27", - lastUpdated: "2 hours ago", - }, - { - id: "14", - slug: "SSET-14", - name: "API Rate Limiting", - status: "planning" as const, - branch: "feature/rate-limiting", - description: "Implement rate limiting and throttling for API endpoints", - assignee: "Noah", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=17", - lastUpdated: "4 days ago", - }, - { - id: "15", - slug: "SSET-15", - name: "Internationalization", - status: "working" as const, - branch: "feature/i18n", - description: - "Add multi-language support with i18next for English, Spanish, and French", - assignee: "Olivia", - assigneeAvatarUrl: "https://i.pravatar.cc/150?img=32", - lastUpdated: "8 hours ago", - }, -]; - -// Helper function to enrich worktrees with task metadata -function enrichWorktreesWithTasks( - worktrees: Worktree[], - pendingWorktrees: PendingWorktree[], -): WorktreeWithTask[] { - // First, convert pending worktrees to WorktreeWithTask format - const pendingAsWorktrees: WorktreeWithTask[] = pendingWorktrees.map( - (pending) => ({ - id: pending.id, - branch: pending.branch, - path: "", // Pending worktrees don't have a path yet - tabs: [], - createdAt: new Date().toISOString(), - isPending: true, // Mark as pending for UI - task: pending.taskData - ? { - id: pending.id, - slug: pending.taskData.slug, - title: pending.taskData.name, - status: pending.taskData.status, - description: pending.description || "", - } - : undefined, - }), - ); - - // Then, enrich real worktrees with task metadata - const enrichedWorktrees = worktrees.map((worktree) => { - // Try to find a matching task by branch name - const matchingTask = MOCK_TASKS.find( - (task) => task.branch === worktree.branch, - ); - - if (matchingTask) { - // Worktree has an associated task - add task metadata - return { - ...worktree, - task: { - id: matchingTask.id, - slug: matchingTask.slug, - title: matchingTask.name, - status: matchingTask.status, - description: matchingTask.description, - assignee: { - name: matchingTask.assignee, - avatarUrl: matchingTask.assigneeAvatarUrl, - }, - lastUpdated: matchingTask.lastUpdated, - }, - }; - } - - // Worktree without task - return as-is - return worktree; - }); - - // Merge pending and real worktrees - return [...pendingAsWorktrees, ...enrichedWorktrees]; -} - -export const NewLayoutMain: React.FC = () => { - const sidebarPanelRef = useRef(null); - const [isSidebarOpen, setIsSidebarOpen] = useState(true); - const [showSidebarOverlay, setShowSidebarOverlay] = useState(false); - const [isAddTaskModalOpen, setIsAddTaskModalOpen] = useState(false); - - // Workspace state - const [workspaces, setWorkspaces] = useState(null); - const [currentWorkspace, setCurrentWorkspace] = useState( - null, - ); - const [selectedWorktreeId, setSelectedWorktreeId] = useState( - null, - ); - const [selectedTabId, setSelectedTabId] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [mode, setMode] = useState<"plan" | "edit">("edit"); - const [pendingWorktrees, setPendingWorktrees] = useState( - [], - ); - - // Compute which tasks have worktrees (are "open") - const openTasks = MOCK_TASKS.filter((task) => - currentWorkspace?.worktrees?.some((wt) => wt.branch === task.branch), - ); - - const handleCollapseSidebar = () => { - const panel = sidebarPanelRef.current; - if (panel && !panel.isCollapsed()) { - panel.collapse(); - setIsSidebarOpen(false); - } - }; - - const handleExpandSidebar = () => { - const panel = sidebarPanelRef.current; - if (panel && panel.isCollapsed()) { - panel.expand(); - setIsSidebarOpen(true); - } - }; - - // Get selected worktree - const selectedWorktree = currentWorkspace?.worktrees?.find( - (wt) => wt.id === selectedWorktreeId, - ); - - // Helper function to find a tab recursively (for finding sub-tabs inside groups) - const findTabRecursive = ( - tabs: Tab[] | undefined, - tabId: string, - ): { tab: Tab; parent?: Tab } | null => { - if (!tabs) return null; - - for (const tab of tabs) { - if (tab.id === tabId) { - return { tab }; - } - // Check if this tab is a group tab with children - if (tab.type === "group" && tab.tabs) { - for (const childTab of tab.tabs) { - if (childTab.id === tabId) { - return { tab: childTab, parent: tab }; - } - } - } - } - return null; - }; - - // Get selected tab and its parent (if it's a sub-tab) - const tabResult = selectedWorktree?.tabs - ? findTabRecursive(selectedWorktree.tabs, selectedTabId ?? "") - : null; - - const selectedTab = tabResult?.tab; - const parentGroupTab = tabResult?.parent; - - // Load all workspaces - const loadAllWorkspaces = async () => { - try { - const allWorkspaces = await window.ipcRenderer.invoke("workspace-list"); - setWorkspaces(allWorkspaces); - } catch (error) { - console.error("Failed to load workspaces:", error); - } - }; - - // Optimistically add a tab to the current workspace - const handleTabCreated = (worktreeId: string, tab: Tab) => { - if (!currentWorkspace) return; - - // Find the worktree and add the tab - const updatedWorktrees = currentWorkspace.worktrees.map((wt) => { - if (wt.id === worktreeId) { - return { - ...wt, - tabs: [...wt.tabs, tab], - }; - } - return wt; - }); - - const updatedWorkspace = { - ...currentWorkspace, - worktrees: updatedWorktrees, - activeWorktreeId: worktreeId, - activeTabId: tab.id, - }; - - setCurrentWorkspace(updatedWorkspace); - - // Also update in workspaces array - if (workspaces) { - setWorkspaces( - workspaces.map((ws) => - ws.id === currentWorkspace.id ? updatedWorkspace : ws, - ), - ); - } - }; - - // Handle tab selection - const handleTabSelect = (worktreeId: string, tabId: string) => { - setSelectedWorktreeId(worktreeId); - setSelectedTabId(tabId); - - if (currentWorkspace) { - window.ipcRenderer.invoke("workspace-set-active-selection", { - workspaceId: currentWorkspace.id, - worktreeId, - tabId, - }); - - setCurrentWorkspace({ - ...currentWorkspace, - activeWorktreeId: worktreeId, - activeTabId: tabId, - }); - } - }; - - // Handle tab focus (for terminals) - const handleTabFocus = (tabId: string) => { - if (!currentWorkspace || !selectedWorktreeId) return; - - setSelectedTabId(tabId); - - window.ipcRenderer.invoke("workspace-set-active-selection", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - tabId, - }); - - setCurrentWorkspace({ - ...currentWorkspace, - activeWorktreeId: selectedWorktreeId, - activeTabId: tabId, - }); - }; - - // Handle workspace selection - const handleWorkspaceSelect = async (workspaceId: string) => { - try { - const workspace = await window.ipcRenderer.invoke( - "workspace-get", - workspaceId, - ); - - if (workspace) { - setCurrentWorkspace(workspace); - await window.ipcRenderer.invoke( - "workspace-set-active-workspace-id", - workspaceId, - ); - - const activeSelection = await window.ipcRenderer.invoke( - "workspace-get-active-selection", - workspaceId, - ); - - if (activeSelection?.worktreeId && activeSelection?.tabId) { - setSelectedWorktreeId(activeSelection.worktreeId); - setSelectedTabId(activeSelection.tabId); - } else { - setSelectedWorktreeId(null); - setSelectedTabId(null); - } - } - } catch (error) { - console.error("Failed to load workspace:", error); - } - }; - - // Handle worktree created - const handleWorktreeCreated = async () => { - if (!currentWorkspace) return; - - try { - const refreshedWorkspace = await window.ipcRenderer.invoke( - "workspace-get", - currentWorkspace.id, - ); - - if (refreshedWorkspace) { - setCurrentWorkspace(refreshedWorkspace); - await loadAllWorkspaces(); - } - } catch (error) { - console.error("Failed to refresh workspace:", error); - } - }; - - // Handle worktree update - const handleUpdateWorktree = ( - worktreeId: string, - updatedWorktree: Worktree, - ) => { - if (!currentWorkspace) return; - - const updatedWorktrees = currentWorkspace.worktrees.map((wt) => - wt.id === worktreeId ? updatedWorktree : wt, - ); - - const updatedCurrentWorkspace = { - ...currentWorkspace, - worktrees: updatedWorktrees, - }; - - setCurrentWorkspace(updatedCurrentWorkspace); - - if (workspaces) { - setWorkspaces( - workspaces.map((ws) => - ws.id === currentWorkspace.id ? updatedCurrentWorkspace : ws, - ), - ); - } - }; - - // Handle show diff - creates a diff tab - const handleShowDiff = async (worktreeId: string) => { - if (!currentWorkspace) return; - - // Find the worktree - const worktree = currentWorkspace.worktrees?.find( - (wt) => wt.id === worktreeId, - ); - if (!worktree) return; - - // Check if a diff tab already exists for this worktree - const existingDiffTab = worktree.tabs?.find((tab) => tab.type === "diff"); - - if (existingDiffTab) { - // If a diff tab already exists, just select it - await window.ipcRenderer.invoke("workspace-set-active-selection", { - workspaceId: currentWorkspace.id, - worktreeId: worktreeId, - tabId: existingDiffTab.id, - }); - - // Reload the workspace to get the updated state - const updatedWorkspace = await window.ipcRenderer.invoke( - "workspace-get", - currentWorkspace.id, - ); - if (updatedWorkspace) { - setCurrentWorkspace(updatedWorkspace); - } - - // Update the workspaces array - await loadAllWorkspaces(); - - // Set state to select the tab - setSelectedWorktreeId(worktreeId); - setSelectedTabId(existingDiffTab.id); - return; - } - - // Create a new diff tab - const result = await window.ipcRenderer.invoke("tab-create", { - workspaceId: currentWorkspace.id, - worktreeId: worktreeId, - name: `Changes – ${worktree.branch}`, - type: "diff", - }); - - if (result.success && result.tab) { - // Set active selection in backend first - await window.ipcRenderer.invoke("workspace-set-active-selection", { - workspaceId: currentWorkspace.id, - worktreeId: worktreeId, - tabId: result.tab.id, - }); - - // Reload the workspace to get the updated state with the new tab - const updatedWorkspace = await window.ipcRenderer.invoke( - "workspace-get", - currentWorkspace.id, - ); - if (updatedWorkspace) { - setCurrentWorkspace(updatedWorkspace); - } - - // Update the workspaces array - await loadAllWorkspaces(); - - // Set state to select the new tab - setSelectedWorktreeId(worktreeId); - setSelectedTabId(result.tab.id); - } - }; - - // Task handlers - const handleOpenAddTaskModal = () => { - setIsAddTaskModalOpen(true); - }; - - const handleCloseAddTaskModal = () => { - setIsAddTaskModalOpen(false); - }; - - const handleSelectTask = (task: UITask) => { - if (!currentWorkspace) return; - - // Find existing worktree for this task's branch - const existingWorktree = currentWorkspace.worktrees?.find( - (wt) => wt.branch === task.branch, - ); - - if (existingWorktree) { - // Worktree already exists - switch to it - setSelectedWorktreeId(existingWorktree.id); - if (existingWorktree.tabs && existingWorktree.tabs.length > 0) { - handleTabSelect(existingWorktree.id, existingWorktree.tabs[0].id); - } - handleCloseAddTaskModal(); - } else { - // Worktree doesn't exist - create it with optimistic update - const pendingId = `pending-${Date.now()}`; - const pendingWorktree: PendingWorktree = { - id: pendingId, - isPending: true, - title: task.name, - branch: task.branch, - description: task.description, - taskData: { - slug: task.slug, - name: task.name, - status: task.status, - }, - }; - - // Add pending worktree immediately - setPendingWorktrees((prev) => [...prev, pendingWorktree]); - handleCloseAddTaskModal(); - - void (async () => { - try { - const result = await window.ipcRenderer.invoke("worktree-create", { - workspaceId: currentWorkspace.id, - title: task.name, - branch: task.branch, - createBranch: false, // Branch should already exist - description: task.description, - }); - - if (result.success && result.worktree) { - // Remove pending worktree - setPendingWorktrees((prev) => - prev.filter((wt) => wt.id !== pendingId), - ); - // Refresh workspace to get the real worktree - await handleWorktreeCreated(); - setSelectedWorktreeId(result.worktree.id); - if (result.worktree.tabs && result.worktree.tabs.length > 0) { - handleTabSelect(result.worktree.id, result.worktree.tabs[0].id); - } - } else { - // Remove pending on failure - setPendingWorktrees((prev) => - prev.filter((wt) => wt.id !== pendingId), - ); - } - } catch (error) { - console.error("Failed to create worktree for task:", error); - // Remove pending on error - setPendingWorktrees((prev) => - prev.filter((wt) => wt.id !== pendingId), - ); - } - })(); - } - }; - - const handleCreateTask = (taskData: { - name: string; - description: string; - status: TaskStatus; - assignee: string; - branch: string; - }) => { - if (!currentWorkspace) return; - - // Create pending worktree for optimistic update - const pendingId = `pending-${Date.now()}`; - const pendingWorktree: PendingWorktree = { - id: pendingId, - isPending: true, - title: taskData.name, - branch: taskData.branch, - description: taskData.description, - taskData: { - slug: "...", // Will be generated by backend - name: taskData.name, - status: taskData.status, - }, - }; - - // Add pending worktree immediately - setPendingWorktrees((prev) => [...prev, pendingWorktree]); - handleCloseAddTaskModal(); - - void (async () => { - try { - // Create a worktree for this task - const result = await window.ipcRenderer.invoke("worktree-create", { - workspaceId: currentWorkspace.id, - title: taskData.name, - branch: taskData.branch, - createBranch: true, - description: taskData.description, - }); - - if (result.success && result.worktree) { - // Remove pending worktree - setPendingWorktrees((prev) => - prev.filter((wt) => wt.id !== pendingId), - ); - - // Reload workspace to get the new worktree - await handleWorktreeCreated(); - - // Switch to the new worktree - setSelectedWorktreeId(result.worktree.id); - - // Select first tab if available - if (result.worktree.tabs && result.worktree.tabs.length > 0) { - handleTabSelect(result.worktree.id, result.worktree.tabs[0].id); - } - } else { - // Remove pending on failure - setPendingWorktrees((prev) => - prev.filter((wt) => wt.id !== pendingId), - ); - } - } catch (error) { - console.error("Failed to create task/worktree:", error); - // Remove pending on error - setPendingWorktrees((prev) => prev.filter((wt) => wt.id !== pendingId)); - } - })(); - }; - - const handleCreatePR = async () => { - if (!currentWorkspace || !selectedWorktreeId) return; - - const worktree = currentWorkspace.worktrees?.find( - (wt) => wt.id === selectedWorktreeId, - ); - if (!worktree) return; - - try { - const result = await window.ipcRenderer.invoke("worktree-create-pr", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - }); - - if (result.success) { - // Reload workspace to show updated PR state - const refreshedWorkspace = await window.ipcRenderer.invoke( - "workspace-get", - currentWorkspace.id, - ); - if (refreshedWorkspace) { - setCurrentWorkspace(refreshedWorkspace); - } - - // Open PR URL in default browser only if we have a valid URL - // (--web mode opens browser automatically, so we don't need to open it again) - if (result.prUrl && result.prUrl.startsWith("http")) { - await window.ipcRenderer.invoke("open-external", result.prUrl); - } - } else { - // Show error as alert - alert(`Failed to create PR: ${result.error || "Unknown error"}`); - } - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - alert(`Failed to create PR: ${errorMessage}`); - } - }; - - const handleMergePR = async () => { - if (!currentWorkspace || !selectedWorktreeId) return; - - const worktree = currentWorkspace.worktrees?.find( - (wt) => wt.id === selectedWorktreeId, - ); - if (!worktree) return; - - try { - const result = await window.ipcRenderer.invoke("worktree-merge-pr", { - workspaceId: currentWorkspace.id, - worktreeId: selectedWorktreeId, - }); - - if (result.success) { - // Reload workspace to show updated state - const refreshedWorkspace = await window.ipcRenderer.invoke( - "workspace-get", - currentWorkspace.id, - ); - if (refreshedWorkspace) { - setCurrentWorkspace(refreshedWorkspace); - } - alert("PR merged successfully!"); - } else { - // Show error as alert - alert(`Failed to merge PR: ${result.error || "Unknown error"}`); - } - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - alert(`Failed to merge PR: ${errorMessage}`); - } - }; - - // Load active workspace on mount - useEffect(() => { - const loadActiveWorkspace = async () => { - try { - setLoading(true); - setError(null); - - await loadAllWorkspaces(); - - let workspaceId = await window.ipcRenderer.invoke( - "workspace-get-active-workspace-id", - ); - - if (!workspaceId) { - const lastOpenedWorkspace = await window.ipcRenderer.invoke( - "workspace-get-last-opened", - ); - workspaceId = lastOpenedWorkspace?.id ?? null; - } - - if (workspaceId) { - const workspace = await window.ipcRenderer.invoke( - "workspace-get", - workspaceId, - ); - - if (workspace) { - setCurrentWorkspace(workspace); - - const activeSelection = await window.ipcRenderer.invoke( - "workspace-get-active-selection", - workspaceId, - ); - - if (activeSelection?.worktreeId && activeSelection?.tabId) { - setSelectedWorktreeId(activeSelection.worktreeId); - setSelectedTabId(activeSelection.tabId); - } - } - } - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setLoading(false); - } - }; - - loadActiveWorkspace(); - }, []); - - // Listen for workspace-opened event - useEffect(() => { - const handler = async (workspace: Workspace) => { - console.log( - "[NewLayoutMain] Workspace opened event received:", - workspace, - ); - setLoading(false); - - await window.ipcRenderer.invoke( - "workspace-set-active-workspace-id", - workspace.id, - ); - await loadAllWorkspaces(); - - const refreshedWorkspace = await window.ipcRenderer.invoke( - "workspace-get", - workspace.id, - ); - if (refreshedWorkspace) { - setCurrentWorkspace(refreshedWorkspace); - } - }; - - window.ipcRenderer.on("workspace-opened", handler); - return () => { - window.ipcRenderer.off("workspace-opened", handler); - }; - }, []); - return ( - <> - - - {/* Hover trigger area when sidebar is hidden */} - {!isSidebarOpen && ( -
setShowSidebarOverlay(true)} - /> - )} - - {/* Sidebar overlay when hidden and hovering */} - {!isSidebarOpen && showSidebarOverlay && workspaces && ( -
setShowSidebarOverlay(false)} - > -
- { - setShowSidebarOverlay(false); - }} - onShowDiff={handleShowDiff} - /> -
-
- )} - - -
- {/* Worktree tabs at the top - each tab represents a worktree */} - { - // Don't allow selecting pending worktrees - if (worktreeId.startsWith("pending-")) return; - - setSelectedWorktreeId(worktreeId); - // Select first tab in the worktree - const worktree = currentWorkspace?.worktrees?.find( - (wt) => wt.id === worktreeId, - ); - if (worktree && worktree.tabs && worktree.tabs.length > 0) { - handleTabSelect(worktreeId, worktree.tabs[0].id); - } - }} - mode={mode} - onModeChange={setMode} - /> - - {/* Main content area - conditionally render based on mode */} -
- {mode === "plan" ? ( - // Plan mode - show kanban board - - ) : ( - // Edit mode - show workspace/terminal view - - {/* Sidebar panel with full workspace/worktree management */} - setIsSidebarOpen(false)} - onExpand={() => setIsSidebarOpen(true)} - > - {isSidebarOpen && workspaces && ( - { - const panel = sidebarPanelRef.current; - if (panel && !panel.isCollapsed()) { - panel.collapse(); - } - }} - onShowDiff={handleShowDiff} - /> - )} - - - - - {/* Main content panel */} - - {loading || - error || - !currentWorkspace || - !selectedTab || - !selectedWorktree ? ( - - ) : parentGroupTab ? ( - // Selected tab is a sub-tab of a group → display the parent group's mosaic - - ) : selectedTab.type === "group" ? ( - // Selected tab is a group tab → display its mosaic layout - - ) : selectedTab.type === "diff" ? ( - // Diff tab → display diff view -
- -
- ) : ( - // Base level tab (terminal, preview, etc.) → display full width/height -
- -
- )} -
-
- )} -
-
-
- - {/* Open Task Modal */} - - - ); -}; diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/StatusIndicator.tsx b/apps/desktop/src/renderer/screens/main/components/StatusIndicator.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/StatusIndicator.tsx rename to apps/desktop/src/renderer/screens/main/components/StatusIndicator.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskAssignee.tsx b/apps/desktop/src/renderer/screens/main/components/TaskAssignee.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/TaskAssignee.tsx rename to apps/desktop/src/renderer/screens/main/components/TaskAssignee.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskListItem.tsx b/apps/desktop/src/renderer/screens/main/components/TaskListItem.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/TaskListItem.tsx rename to apps/desktop/src/renderer/screens/main/components/TaskListItem.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskPreview.tsx b/apps/desktop/src/renderer/screens/main/components/TaskPreview.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/TaskPreview.tsx rename to apps/desktop/src/renderer/screens/main/components/TaskPreview.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/AddTaskButton.tsx b/apps/desktop/src/renderer/screens/main/components/TaskTabs/AddTaskButton.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/AddTaskButton.tsx rename to apps/desktop/src/renderer/screens/main/components/TaskTabs/AddTaskButton.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/BasicWorktreeContent.tsx b/apps/desktop/src/renderer/screens/main/components/TaskTabs/BasicWorktreeContent.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/BasicWorktreeContent.tsx rename to apps/desktop/src/renderer/screens/main/components/TaskTabs/BasicWorktreeContent.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/ModeToggle.tsx b/apps/desktop/src/renderer/screens/main/components/TaskTabs/ModeToggle.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/ModeToggle.tsx rename to apps/desktop/src/renderer/screens/main/components/TaskTabs/ModeToggle.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/PRActions.tsx b/apps/desktop/src/renderer/screens/main/components/TaskTabs/PRActions.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/PRActions.tsx rename to apps/desktop/src/renderer/screens/main/components/TaskTabs/PRActions.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/PendingWorktreeContent.tsx b/apps/desktop/src/renderer/screens/main/components/TaskTabs/PendingWorktreeContent.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/PendingWorktreeContent.tsx rename to apps/desktop/src/renderer/screens/main/components/TaskTabs/PendingWorktreeContent.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/SidebarToggle.tsx b/apps/desktop/src/renderer/screens/main/components/TaskTabs/SidebarToggle.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/SidebarToggle.tsx rename to apps/desktop/src/renderer/screens/main/components/TaskTabs/SidebarToggle.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/TaskTabs.tsx b/apps/desktop/src/renderer/screens/main/components/TaskTabs/TaskTabs.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/TaskTabs.tsx rename to apps/desktop/src/renderer/screens/main/components/TaskTabs/TaskTabs.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/TaskWorktreeContent.tsx b/apps/desktop/src/renderer/screens/main/components/TaskTabs/TaskWorktreeContent.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/TaskWorktreeContent.tsx rename to apps/desktop/src/renderer/screens/main/components/TaskTabs/TaskWorktreeContent.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/WorktreeTab.tsx b/apps/desktop/src/renderer/screens/main/components/TaskTabs/WorktreeTab.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/WorktreeTab.tsx rename to apps/desktop/src/renderer/screens/main/components/TaskTabs/WorktreeTab.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/WorktreeTabButton.tsx b/apps/desktop/src/renderer/screens/main/components/TaskTabs/WorktreeTabButton.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/WorktreeTabButton.tsx rename to apps/desktop/src/renderer/screens/main/components/TaskTabs/WorktreeTabButton.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/index.ts b/apps/desktop/src/renderer/screens/main/components/TaskTabs/index.ts similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/index.ts rename to apps/desktop/src/renderer/screens/main/components/TaskTabs/index.ts diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/types.ts b/apps/desktop/src/renderer/screens/main/components/TaskTabs/types.ts similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/types.ts rename to apps/desktop/src/renderer/screens/main/components/TaskTabs/types.ts diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/utils.ts b/apps/desktop/src/renderer/screens/main/components/TaskTabs/utils.ts similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/utils.ts rename to apps/desktop/src/renderer/screens/main/components/TaskTabs/utils.ts diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/WorktreeTabView.tsx b/apps/desktop/src/renderer/screens/main/components/WorktreeTabView.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/WorktreeTabView.tsx rename to apps/desktop/src/renderer/screens/main/components/WorktreeTabView.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/mock-data.ts b/apps/desktop/src/renderer/screens/main/components/mock-data.ts new file mode 100644 index 00000000000..57a3202c08f --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/mock-data.ts @@ -0,0 +1,179 @@ +import type { UITask } from "./types"; + +// Mock tasks data - TODO: Replace with actual task data from backend +export const MOCK_TASKS: UITask[] = [ + { + id: "1", + slug: "SSET-1", + name: "Homepage Redesign", + status: "working", + branch: "feature/homepage-redesign", + description: "Redesigning the homepage with new branding and improved UX", + assignee: "Alice", + assigneeAvatarUrl: "https://i.pravatar.cc/150?img=1", + lastUpdated: "2 hours ago", + }, + { + id: "2", + slug: "SSET-2", + name: "API Integration", + status: "needs-feedback", + branch: "feature/api-integration", + description: "Integrate new REST API endpoints for user management", + assignee: "Bob", + assigneeAvatarUrl: "https://i.pravatar.cc/150?img=12", + lastUpdated: "1 day ago", + }, + { + id: "3", + slug: "SSET-3", + name: "Bug Fixes", + status: "planning", + branch: "fix/various-bugs", + description: "Collection of bug fixes reported by users", + assignee: "Charlie", + assigneeAvatarUrl: "https://i.pravatar.cc/150?img=33", + lastUpdated: "3 days ago", + }, + { + id: "4", + slug: "SSET-4", + name: "Performance Optimization", + status: "ready-to-merge", + branch: "perf/optimize-queries", + description: "Optimize database queries for faster page loads", + assignee: "Diana", + assigneeAvatarUrl: "https://i.pravatar.cc/150?img=9", + lastUpdated: "5 minutes ago", + }, + { + id: "5", + slug: "SSET-5", + name: "User Authentication System", + status: "working", + branch: "feature/auth-system", + description: + "Implement OAuth2 and JWT-based authentication system with refresh tokens", + assignee: "Eve", + assigneeAvatarUrl: "https://i.pravatar.cc/150?img=5", + lastUpdated: "3 hours ago", + }, + { + id: "6", + slug: "SSET-6", + name: "Dark Mode Support", + status: "planning", + branch: "feature/dark-mode", + description: "Add dark mode theme support across the entire application", + assignee: "Frank", + assigneeAvatarUrl: "https://i.pravatar.cc/150?img=13", + lastUpdated: "2 days ago", + }, + { + id: "7", + slug: "SSET-7", + name: "Database Migration Scripts", + status: "ready-to-merge", + branch: "db/migration-scripts", + description: + "Create automated migration scripts for production database updates", + assignee: "Grace", + assigneeAvatarUrl: "https://i.pravatar.cc/150?img=20", + lastUpdated: "1 hour ago", + }, + { + id: "8", + slug: "SSET-8", + name: "Email Notification Service", + status: "needs-feedback", + branch: "feature/email-notifications", + description: + "Build email notification service using SendGrid for transactional emails", + assignee: "Henry", + assigneeAvatarUrl: "https://i.pravatar.cc/150?img=8", + lastUpdated: "4 hours ago", + }, + { + id: "9", + slug: "SSET-9", + name: "Mobile Responsive Design", + status: "working", + branch: "feature/mobile-responsive", + description: + "Make the application fully responsive for mobile and tablet devices", + assignee: "Iris", + assigneeAvatarUrl: "https://i.pravatar.cc/150?img=16", + lastUpdated: "6 hours ago", + }, + { + id: "10", + slug: "SSET-10", + name: "Analytics Dashboard", + status: "planning", + branch: "feature/analytics-dashboard", + description: + "Create admin dashboard with charts and metrics for user analytics", + assignee: "Jack", + assigneeAvatarUrl: "https://i.pravatar.cc/150?img=11", + lastUpdated: "1 week ago", + }, + { + id: "11", + slug: "SSET-11", + name: "CI/CD Pipeline", + status: "ready-to-merge", + branch: "devops/ci-cd-pipeline", + description: + "Set up automated CI/CD pipeline with GitHub Actions and Docker", + assignee: "Kate", + assigneeAvatarUrl: "https://i.pravatar.cc/150?img=25", + lastUpdated: "30 minutes ago", + }, + { + id: "12", + slug: "SSET-12", + name: "Search Functionality", + status: "working", + branch: "feature/search", + description: "Implement full-text search with Elasticsearch integration", + assignee: "Liam", + assigneeAvatarUrl: "https://i.pravatar.cc/150?img=14", + lastUpdated: "5 hours ago", + }, + { + id: "13", + slug: "SSET-13", + name: "File Upload System", + status: "needs-feedback", + branch: "feature/file-uploads", + description: + "Build secure file upload system with S3 storage and virus scanning", + assignee: "Mia", + assigneeAvatarUrl: "https://i.pravatar.cc/150?img=27", + lastUpdated: "2 hours ago", + }, + { + id: "14", + slug: "SSET-14", + name: "API Rate Limiting", + status: "planning", + branch: "feature/rate-limiting", + description: "Implement rate limiting and throttling for API endpoints", + assignee: "Noah", + assigneeAvatarUrl: "https://i.pravatar.cc/150?img=17", + lastUpdated: "4 days ago", + }, + { + id: "15", + slug: "SSET-15", + name: "Internationalization", + status: "working", + branch: "feature/i18n", + description: + "Add multi-language support with i18next for English, Spanish, and French", + assignee: "Olivia", + assigneeAvatarUrl: "https://i.pravatar.cc/150?img=32", + lastUpdated: "8 hours ago", + }, +]; + diff --git a/apps/desktop/src/renderer/screens/main/components/types.ts b/apps/desktop/src/renderer/screens/main/components/types.ts new file mode 100644 index 00000000000..b7f8973e4be --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/types.ts @@ -0,0 +1,29 @@ +import type { TaskStatus } from "./StatusIndicator"; + +// Type alias for task data used in UI +export type UITask = { + id: string; + slug: string; + name: string; + status: TaskStatus; + branch: string; + description: string; + assignee: string; + assigneeAvatarUrl: string; + lastUpdated: string; +}; + +// Type for pending worktrees (optimistic updates) +export type PendingWorktree = { + id: string; + isPending: true; + title: string; + branch: string; + description?: string; + taskData?: { + slug: string; + name: string; + status: TaskStatus; + }; +}; + diff --git a/apps/desktop/src/renderer/screens/main/components/utils.ts b/apps/desktop/src/renderer/screens/main/components/utils.ts new file mode 100644 index 00000000000..85d1b55189c --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/utils.ts @@ -0,0 +1,67 @@ +import type { Worktree } from "shared/types"; +import type { PendingWorktree } from "./types"; +import { MOCK_TASKS } from "./mock-data"; +import type { WorktreeWithTask } from "./TaskTabs"; + +/** + * Helper function to enrich worktrees with task metadata + */ +export function enrichWorktreesWithTasks( + worktrees: Worktree[], + pendingWorktrees: PendingWorktree[], +): WorktreeWithTask[] { + // First, convert pending worktrees to WorktreeWithTask format + const pendingAsWorktrees: WorktreeWithTask[] = pendingWorktrees.map( + (pending) => ({ + id: pending.id, + branch: pending.branch, + path: "", // Pending worktrees don't have a path yet + tabs: [], + createdAt: new Date().toISOString(), + isPending: true, // Mark as pending for UI + task: pending.taskData + ? { + id: pending.id, + slug: pending.taskData.slug, + title: pending.taskData.name, + status: pending.taskData.status, + description: pending.description || "", + } + : undefined, + }), + ); + + // Then, enrich real worktrees with task metadata + const enrichedWorktrees = worktrees.map((worktree) => { + // Try to find a matching task by branch name + const matchingTask = MOCK_TASKS.find( + (task) => task.branch === worktree.branch, + ); + + if (matchingTask) { + // Worktree has an associated task - add task metadata + return { + ...worktree, + task: { + id: matchingTask.id, + slug: matchingTask.slug, + title: matchingTask.name, + status: matchingTask.status, + description: matchingTask.description, + assignee: { + name: matchingTask.assignee, + avatarUrl: matchingTask.assigneeAvatarUrl, + }, + lastUpdated: matchingTask.lastUpdated, + }, + }; + } + + // Worktree without task - return as-is + return worktree; + }); + + // Merge pending and real worktrees + return [...pendingAsWorktrees, ...enrichedWorktrees]; +} + diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/views/BrowserView.tsx b/apps/desktop/src/renderer/screens/main/components/views/BrowserView.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/views/BrowserView.tsx rename to apps/desktop/src/renderer/screens/main/components/views/BrowserView.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/views/GitView.tsx b/apps/desktop/src/renderer/screens/main/components/views/GitView.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/views/GitView.tsx rename to apps/desktop/src/renderer/screens/main/components/views/GitView.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/views/SummaryView.tsx b/apps/desktop/src/renderer/screens/main/components/views/SummaryView.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/views/SummaryView.tsx rename to apps/desktop/src/renderer/screens/main/components/views/SummaryView.tsx diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/views/TerminalsView.tsx b/apps/desktop/src/renderer/screens/main/components/views/TerminalsView.tsx similarity index 100% rename from apps/desktop/src/renderer/screens/main/components/NewLayout/views/TerminalsView.tsx rename to apps/desktop/src/renderer/screens/main/components/views/TerminalsView.tsx