diff --git a/apps/desktop/src/renderer/env.d.ts b/apps/desktop/src/renderer/env.d.ts
index 5a4b0f4410a..11f02fe2a00 100644
--- a/apps/desktop/src/renderer/env.d.ts
+++ b/apps/desktop/src/renderer/env.d.ts
@@ -1,9 +1 @@
///
-
-interface ImportMetaEnv {
- readonly ENABLE_NEW_UI: string;
-}
-
-interface ImportMeta {
- readonly env: ImportMetaEnv;
-}
diff --git a/apps/desktop/src/renderer/screens/main/MainScreen.tsx b/apps/desktop/src/renderer/screens/main/MainScreen.tsx
index 2ec0f7c55d6..cc1c51ffdb5 100644
--- a/apps/desktop/src/renderer/screens/main/MainScreen.tsx
+++ b/apps/desktop/src/renderer/screens/main/MainScreen.tsx
@@ -1,17 +1,3 @@
-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,
@@ -19,75 +5,290 @@ import {
} from "@superset/ui/resizable";
import { useEffect, useRef, useState } from "react";
import type { ImperativePanelHandle } from "react-resizable-panels";
-import type {
- MosaicNode,
- Tab,
- TabType,
- Workspace,
- Worktree,
-} 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 { AddTaskModal } from "./components/Layout/AddTaskModal";
+import type { TaskStatus } from "./components/Layout/StatusIndicator";
+import { TaskTabs, type WorktreeWithTask } from "./components/Layout/TaskTabs";
import TabContent from "./components/MainContent/TabContent";
import TabGroup from "./components/MainContent/TabGroup";
-import { NewLayoutMain } from "./components/NewLayout/NewLayoutMain";
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
-
-
- )}
-
+// 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,
+ }),
);
-}
-export function MainScreen() {
- // Check if new UI is enabled
- const enableNewUI = import.meta.env.ENABLE_NEW_UI === "true";
+ // 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;
+ });
- // If new UI is enabled, render the new layout
- if (enableNewUI) {
- return ;
- }
+ // Merge pending and real worktrees
+ return [...pendingAsWorktrees, ...enrichedWorktrees];
+}
- // Otherwise, render the original layout
+export function MainScreen() {
+ 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,
@@ -95,137 +296,38 @@ 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);
-
- // Diff view state
- const [showDiffView, setShowDiffView] = useState(false);
- const [diffWorktreeId, setDiffWorktreeId] = useState(null);
-
- // Drag and drop state
- const [activeId, setActiveId] = useState(null);
- const [isOverMainContent, setIsOverMainContent] = useState(false);
-
- const selectedWorktree = currentWorkspace?.worktrees?.find(
- (wt) => wt.id === selectedWorktreeId,
+ const [mode, setMode] = useState<"plan" | "edit">("edit");
+ const [pendingWorktrees, setPendingWorktrees] = useState(
+ [],
);
- const diffWorktree = currentWorkspace?.worktrees?.find(
- (wt) => wt.id === diffWorktreeId,
+ // Compute which tasks have worktrees (are "open")
+ const openTasks = MOCK_TASKS.filter((task) =>
+ currentWorkspace?.worktrees?.some((wt) => wt.branch === task.branch),
);
- // 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,
- });
-
- if (!result.success || !result.tab) {
- console.error("[MainScreen] Failed to create tab:", result.error);
- return null;
+ const handleCollapseSidebar = () => {
+ const panel = sidebarPanelRef.current;
+ if (panel && !panel.isCollapsed()) {
+ panel.collapse();
+ setIsSidebarOpen(false);
}
-
- return result.tab;
};
- // Helper: Show diff view for a worktree - now creates a tab
- const handleShowDiffView = 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);
+ const handleExpandSidebar = () => {
+ const panel = sidebarPanelRef.current;
+ if (panel?.isCollapsed()) {
+ panel.expand();
+ setIsSidebarOpen(true);
}
};
- // 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,
- }),
+ // 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)
@@ -259,17 +361,62 @@ export function MainScreen() {
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);
- // 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,
@@ -278,18 +425,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,
@@ -297,6 +444,7 @@ export function MainScreen() {
});
};
+ // Handle workspace selection
const handleWorkspaceSelect = async (workspaceId: string) => {
try {
const workspace = await window.ipcRenderer.invoke(
@@ -306,12 +454,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,
@@ -321,7 +468,6 @@ export function MainScreen() {
setSelectedWorktreeId(activeSelection.worktreeId);
setSelectedTabId(activeSelection.tabId);
} else {
- // No saved selection, reset
setSelectedWorktreeId(null);
setSelectedTabId(null);
}
@@ -331,8 +477,8 @@ export function MainScreen() {
}
};
+ // Handle worktree created
const handleWorktreeCreated = async () => {
- // Refresh workspace data after worktree creation
if (!currentWorkspace) return;
try {
@@ -343,7 +489,6 @@ export function MainScreen() {
if (refreshedWorkspace) {
setCurrentWorkspace(refreshedWorkspace);
- // Also refresh workspaces list
await loadAllWorkspaces();
}
} catch (error) {
@@ -351,8 +496,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) =>
@@ -366,7 +514,6 @@ export function MainScreen() {
setCurrentWorkspace(updatedCurrentWorkspace);
- // Also update the workspaces array so the carousel renders the updated data
if (workspaces) {
setWorkspaces(
workspaces.map((ws) =>
@@ -376,659 +523,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;
+
+ // 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;
}
- };
- // Scan for existing worktrees when workspace is opened
- const scanWorktrees = async (workspaceId: string) => {
- try {
- const result = await window.ipcRenderer.invoke(
- "workspace-scan-worktrees",
- workspaceId,
+ // 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);
+ }
- // Success - worktrees imported
- } 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 (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) => {
- 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);
- }
- };
-
- window.ipcRenderer.on("workspace-opened", handler);
- return () => {
- 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;
- }) => {
- 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,
+ } catch (error) {
+ console.error("Failed to create worktree for task:", error);
+ // Remove pending on error
+ setPendingWorktrees((prev) =>
+ prev.filter((wt) => wt.id !== pendingId),
);
-
- 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);
+ // Add pending worktree immediately
+ setPendingWorktrees((prev) => [...prev, pendingWorktree]);
+ handleCloseAddTaskModal();
- 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;
- }
-
- const draggedWorktreeId = activeData.worktreeId as string;
-
- // Check if the dragged tab is from the same worktree as the currently selected tab
- if (draggedWorktreeId !== selectedWorktreeId) {
- 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") {
- 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) {
- // 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;
- }
+ 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,
+ });
- // Update the mosaic tree to include the new tab
- const updatedMosaicTree = addTabToMosaicTree(
- selectedTab.mosaicTree,
- draggedTabId,
+ if (result.success && result.worktree) {
+ // Remove pending worktree
+ setPendingWorktrees((prev) =>
+ prev.filter((wt) => wt.id !== pendingId),
);
- 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);
- }
- } 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) {
- // 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,
@@ -1036,36 +770,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?.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,
@@ -1073,701 +810,220 @@ 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,
- });
- }
+ const allWorkspaces = await window.ipcRenderer.invoke("workspace-list");
+ setWorkspaces(allWorkspaces);
- // 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();
- }
- },
- });
-
- 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);
- }
+ workspaceId = lastOpenedWorkspace?.id ?? null;
}
- },
- 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
- },
- 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,
+ );
+ const allWorkspaces = await window.ipcRenderer.invoke("workspace-list");
+ setWorkspaces(allWorkspaces);
- 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 && (
+
+
+
+ {/* Open Task Modal */}
+
+ >
);
}
diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/AddTaskModal.tsx b/apps/desktop/src/renderer/screens/main/components/Layout/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/Layout/AddTaskModal.tsx
diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/Avatar.tsx b/apps/desktop/src/renderer/screens/main/components/Layout/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/Layout/Avatar.tsx
diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx b/apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx
similarity index 98%
rename from apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx
rename to apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx
index e0d521ac74b..287746f93d3 100644
--- a/apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx
+++ b/apps/desktop/src/renderer/screens/main/components/Layout/NewLayoutMain.tsx
@@ -241,12 +241,12 @@ function enrichWorktreesWithTasks(
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 || "",
- }
+ id: pending.id,
+ slug: pending.taskData.slug,
+ title: pending.taskData.name,
+ status: pending.taskData.status,
+ description: pending.description || "",
+ }
: undefined,
}),
);
@@ -285,7 +285,7 @@ function enrichWorktreesWithTasks(
return [...pendingAsWorktrees, ...enrichedWorktrees];
}
-export const NewLayoutMain: React.FC = () => {
+export const MainLayout: React.FC = () => {
const sidebarPanelRef = useRef
(null);
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [showSidebarOverlay, setShowSidebarOverlay] = useState(false);
@@ -879,7 +879,7 @@ export const NewLayoutMain: React.FC = () => {
useEffect(() => {
const handler = async (workspace: Workspace) => {
console.log(
- "[NewLayoutMain] Workspace opened event received:",
+ "[MainLayout] Workspace opened event received:",
workspace,
);
setLoading(false);
@@ -1025,10 +1025,10 @@ export const NewLayoutMain: React.FC = () => {
{/* Main content panel */}
{loading ||
- error ||
- !currentWorkspace ||
- !selectedTab ||
- !selectedWorktree ? (
+ error ||
+ !currentWorkspace ||
+ !selectedTab ||
+ !selectedWorktree ? (