diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx index 019ea6f6862..0a4894a038d 100644 --- a/apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx @@ -7,6 +7,7 @@ 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"; @@ -16,10 +17,9 @@ import { PlanView } from "../PlanView"; import { Sidebar } from "../Sidebar"; import { DiffTab } from "../TabContent/components/DiffTab"; import { AddTaskModal } from "./AddTaskModal"; -import { TaskTabs, type WorktreeWithTask } from "./TaskTabs"; import type { TaskStatus } from "./StatusIndicator"; +import { TaskTabs, type WorktreeWithTask } from "./TaskTabs"; import { WorktreeTabView } from "./WorktreeTabView"; -import { WorktreeProvider } from "../../../../contexts/WorktreeContext"; // Type alias for task data used in UI type UITask = { @@ -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, }), ); @@ -745,9 +745,7 @@ export const NewLayoutMain: React.FC = () => { } catch (error) { console.error("Failed to create task/worktree:", error); // Remove pending on error - setPendingWorktrees((prev) => - prev.filter((wt) => wt.id !== pendingId), - ); + setPendingWorktrees((prev) => prev.filter((wt) => wt.id !== pendingId)); } })(); }; @@ -976,7 +974,7 @@ export const NewLayoutMain: React.FC = () => { /> {/* Main content area - conditionally render based on mode */} -
+
{mode === "plan" ? ( // Plan mode - show kanban board { {/* Main content panel */} {loading || - error || - !currentWorkspace || - !selectedTab || - !selectedWorktree ? ( + error || + !currentWorkspace || + !selectedTab || + !selectedWorktree ? ( void; - onExpandSidebar: () => void; - isSidebarOpen: boolean; - onAddTask: () => void; - onCreatePR?: () => void; - onMergePR?: () => void; - worktrees: WorktreeWithTask[]; - selectedWorktreeId: string | null; - onWorktreeSelect: (worktreeId: string) => void; - mode?: "plan" | "edit"; - onModeChange?: (mode: "plan" | "edit") => void; -} - -export const TaskTabs: React.FC = ({ - onCollapseSidebar, - onExpandSidebar, - isSidebarOpen, - onAddTask, - onCreatePR, - onMergePR, - worktrees, - selectedWorktreeId, - onWorktreeSelect, - mode = "edit", - onModeChange, -}) => { - const selectedWorktree = worktrees.find(wt => wt.id === selectedWorktreeId); - const canCreatePR = selectedWorktree && !selectedWorktree.isPending; - const hasPR = selectedWorktree && selectedWorktree.prUrl; - return ( -
-
- {/* Sidebar collapse/expand toggle */} -
- {isSidebarOpen ? ( - - - - - -

Collapse sidebar

-
-
- ) : ( - - - - - -

Expand sidebar

-
-
- )} -
- - {/* Plan/Edit mode toggle */} - {onModeChange && ( -
-
- - -
-
- )} - - {/* Worktree tabs - each tab represents a worktree */} - {worktrees.map((worktree) => { - const hasTask = !!worktree.task; - const task = worktree.task; - const isPending = worktree.isPending; - const displayTitle = hasTask && task - ? task.slug - : worktree.description || worktree.branch; - - const statusLabel = task - ? task.status === "planning" - ? "Planning" - : task.status === "working" - ? "Working" - : task.status === "needs-feedback" - ? "Needs Feedback" - : "Ready to Merge" - : ""; - - return ( - - - - - - {isPending ? ( -
- {/* Pending state */} -
- -

- Creating worktree... -

-
-

- Setting up git worktree and initializing workspace -

-
- ) : hasTask && task ? ( -
- {/* Task view */} -
-
-

- [{task.slug}] {task.title} -

-

- {task.description} -

-
- - {task.assignee && ( -
- -
- )} -
- -
-
- Status -
- - {statusLabel} -
-
- - {task.lastUpdated && ( -
- Updated - - {task.lastUpdated} - -
- )} - -
- Branch - - {worktree.branch} - -
- -
- Tabs - - {worktree.tabs?.length || 0} open - -
-
-
- ) : ( -
- {/* Worktree-only view */} -
- - Worktree - -

- {displayTitle} -

-
-
-
- Branch: - - {worktree.branch} - -
-
- Tabs: - - {worktree.tabs?.length || 0} open - -
-
-
- )} -
-
- ); - })} - - {/* Add task/worktree button */} - - - - - -

New task

-
-
-
- - {/* Right side actions */} -
- {hasPR && onMergePR ? ( - - - - - -

Merge pull request for {selectedWorktree?.branch}

-
-
- ) : onCreatePR ? ( - - - - - -

- {canCreatePR - ? `Create pull request for ${selectedWorktree?.branch}` - : "Select a worktree to create a PR"} -

-
-
- ) : null} -
-
- ); -}; diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/AddTaskButton.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/AddTaskButton.tsx new file mode 100644 index 00000000000..59adbbd2da7 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/AddTaskButton.tsx @@ -0,0 +1,21 @@ +import { Button } from "@superset/ui/button"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { Plus } from "lucide-react"; +import type React from "react"; + +interface AddTaskButtonProps { + onClick: () => void; +} + +export const AddTaskButton: React.FC = ({ onClick }) => ( + + + + + +

New task

+
+
+); diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/BasicWorktreeContent.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/BasicWorktreeContent.tsx new file mode 100644 index 00000000000..59b8818223f --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/BasicWorktreeContent.tsx @@ -0,0 +1,31 @@ +import type React from "react"; +import type { WorktreeWithTask } from "./types"; + +interface BasicWorktreeContentProps { + worktree: WorktreeWithTask; +} + +export const BasicWorktreeContent: React.FC = ({ + worktree, +}) => { + const displayTitle = worktree.description || worktree.branch; + + return ( +
+
+ Worktree +

{displayTitle}

+
+
+
+ Branch: + {worktree.branch} +
+
+ Tabs: + {worktree.tabs?.length || 0} open +
+
+
+ ); +}; diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/ModeToggle.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/ModeToggle.tsx new file mode 100644 index 00000000000..11d7081f8e8 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/ModeToggle.tsx @@ -0,0 +1,35 @@ +import type React from "react"; + +interface ModeToggleProps { + mode: "plan" | "edit"; + onChange: (mode: "plan" | "edit") => void; +} + +export const ModeToggle: React.FC = ({ mode, onChange }) => ( +
+
+ + +
+
+); diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/PRActions.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/PRActions.tsx new file mode 100644 index 00000000000..24a5524101d --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/PRActions.tsx @@ -0,0 +1,69 @@ +import { Button } from "@superset/ui/button"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { GitMerge, GitPullRequest } from "lucide-react"; +import type React from "react"; + +interface PRActionsProps { + hasPR: boolean; + canCreatePR: boolean; + selectedBranch?: string; + onCreatePR?: () => void; + onMergePR?: () => void; +} + +export const PRActions: React.FC = ({ + hasPR, + canCreatePR, + selectedBranch, + onCreatePR, + onMergePR, +}) => { + if (hasPR && onMergePR) { + return ( + + + + + +

Merge pull request for {selectedBranch}

+
+
+ ); + } + + if (onCreatePR) { + return ( + + + + + +

+ {canCreatePR + ? `Create pull request for ${selectedBranch}` + : "Select a worktree to create a PR"} +

+
+
+ ); + } + + return null; +}; diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/PendingWorktreeContent.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/PendingWorktreeContent.tsx new file mode 100644 index 00000000000..ad67057c68c --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/PendingWorktreeContent.tsx @@ -0,0 +1,14 @@ +import { Loader2 } from "lucide-react"; +import type React from "react"; + +export const PendingWorktreeContent: React.FC = () => ( +
+
+ +

Creating worktree...

+
+

+ Setting up git worktree and initializing workspace +

+
+); diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/SidebarToggle.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/SidebarToggle.tsx new file mode 100644 index 00000000000..ea16de6216e --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/SidebarToggle.tsx @@ -0,0 +1,42 @@ +import { Button } from "@superset/ui/button"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { PanelLeftClose, PanelLeftOpen } from "lucide-react"; +import type React from "react"; + +interface SidebarToggleProps { + isOpen: boolean; + onCollapse: () => void; + onExpand: () => void; +} + +export const SidebarToggle: React.FC = ({ + isOpen, + onCollapse, + onExpand, +}) => ( +
+ {isOpen ? ( + + + + + +

Collapse sidebar

+
+
+ ) : ( + + + + + +

Expand sidebar

+
+
+ )} +
+); diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/TaskTabs.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/TaskTabs.tsx new file mode 100644 index 00000000000..e34c0ef5666 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/TaskTabs.tsx @@ -0,0 +1,71 @@ +import type React from "react"; +import { AddTaskButton } from "./AddTaskButton"; +import { ModeToggle } from "./ModeToggle"; +import { PRActions } from "./PRActions"; +import { SidebarToggle } from "./SidebarToggle"; +import type { TaskTabsProps } from "./types"; +import { WorktreeTab } from "./WorktreeTab"; + +export const TaskTabs: React.FC = ({ + onCollapseSidebar, + onExpandSidebar, + isSidebarOpen, + onAddTask, + onCreatePR, + onMergePR, + worktrees, + selectedWorktreeId, + onWorktreeSelect, + mode = "edit", + onModeChange, +}) => { + const selectedWorktree = worktrees.find((wt) => wt.id === selectedWorktreeId); + const canCreatePR = selectedWorktree && !selectedWorktree.isPending; + const hasPR = selectedWorktree && selectedWorktree.prUrl; + + return ( +
+
+ + + {onModeChange && } + +
+ {worktrees.map((worktree) => ( + onWorktreeSelect(worktree.id)} + /> + ))} +
+ + +
+ +
+ +
+
+ ); +}; diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/TaskWorktreeContent.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/TaskWorktreeContent.tsx new file mode 100644 index 00000000000..1cb4fdcd98a --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/TaskWorktreeContent.tsx @@ -0,0 +1,70 @@ +import type React from "react"; +import { StatusIndicator } from "../StatusIndicator"; +import { TaskAssignee } from "../TaskAssignee"; +import type { WorktreeWithTask } from "./types"; +import { getStatusLabel } from "./utils"; + +interface TaskWorktreeContentProps { + worktree: WorktreeWithTask; + task: NonNullable; +} + +export const TaskWorktreeContent: React.FC = ({ + worktree, + task, +}) => { + const statusLabel = getStatusLabel(task.status); + + return ( +
+
+
+

+ [{task.slug}] {task.title} +

+

+ {task.description} +

+
+ + {task.assignee && ( +
+ +
+ )} +
+ +
+
+ Status +
+ + {statusLabel} +
+
+ + {task.lastUpdated && ( +
+ Updated + {task.lastUpdated} +
+ )} + +
+ Branch + + {worktree.branch} + +
+ +
+ Tabs + {worktree.tabs?.length || 0} open +
+
+
+ ); +}; diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/WorktreeTab.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/WorktreeTab.tsx new file mode 100644 index 00000000000..ff6dedc2143 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/WorktreeTab.tsx @@ -0,0 +1,48 @@ +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@superset/ui/hover-card"; +import type React from "react"; +import { BasicWorktreeContent } from "./BasicWorktreeContent"; +import { PendingWorktreeContent } from "./PendingWorktreeContent"; +import { TaskWorktreeContent } from "./TaskWorktreeContent"; +import type { WorktreeWithTask } from "./types"; +import { WorktreeTabButton } from "./WorktreeTabButton"; + +interface WorktreeTabProps { + worktree: WorktreeWithTask; + isSelected: boolean; + onSelect: () => void; +} + +export const WorktreeTab: React.FC = ({ + worktree, + isSelected, + onSelect, +}) => { + const isPending = worktree.isPending; + const hasTask = !!worktree.task; + const task = worktree.task; + + return ( + + + + + + {isPending ? ( + + ) : hasTask && task ? ( + + ) : ( + + )} + + + ); +}; diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/WorktreeTabButton.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/WorktreeTabButton.tsx new file mode 100644 index 00000000000..d0a8de72e46 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/WorktreeTabButton.tsx @@ -0,0 +1,48 @@ +import { Loader2 } from "lucide-react"; +import type React from "react"; +import { StatusIndicator } from "../StatusIndicator"; +import type { WorktreeWithTask } from "./types"; + +interface WorktreeTabButtonProps { + worktree: WorktreeWithTask; + isSelected: boolean; + onClick: () => void; +} + +export const WorktreeTabButton: React.FC = ({ + worktree, + isSelected, + onClick, +}) => { + const hasTask = !!worktree.task; + const task = worktree.task; + const isPending = worktree.isPending; + const displayTitle = + hasTask && task ? task.slug : worktree.description || worktree.branch; + + return ( + + ); +}; diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/index.ts b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/index.ts new file mode 100644 index 00000000000..584b9fe5a50 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/index.ts @@ -0,0 +1,2 @@ +export { TaskTabs } from "./TaskTabs"; +export type { TaskTabsProps, WorktreeWithTask } from "./types"; diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/types.ts b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/types.ts new file mode 100644 index 00000000000..0564fff9e2d --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/types.ts @@ -0,0 +1,32 @@ +import type { Worktree } from "shared/types"; +import type { TaskStatus } from "../StatusIndicator"; + +export interface WorktreeWithTask extends Worktree { + isPending?: boolean; + task?: { + id: string; + slug: string; + title: string; + status: TaskStatus; + description: string; + assignee?: { + name: string; + avatarUrl: string | null; + }; + lastUpdated?: string; + }; +} + +export interface TaskTabsProps { + onCollapseSidebar: () => void; + onExpandSidebar: () => void; + isSidebarOpen: boolean; + onAddTask: () => void; + onCreatePR?: () => void; + onMergePR?: () => void; + worktrees: WorktreeWithTask[]; + selectedWorktreeId: string | null; + onWorktreeSelect: (worktreeId: string) => void; + mode?: "plan" | "edit"; + onModeChange?: (mode: "plan" | "edit") => void; +} diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/utils.ts b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/utils.ts new file mode 100644 index 00000000000..172ec95a594 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs/utils.ts @@ -0,0 +1,15 @@ +import type { TaskStatus } from "../StatusIndicator"; + +export const getStatusLabel = (status: TaskStatus): string => { + const labels: Record = { + planning: "Planning", + working: "Working", + "needs-feedback": "Needs Feedback", + "ready-to-merge": "Ready to Merge", + backlog: "Backlog", + todo: "Todo", + completed: "Completed", + canceled: "Canceled", + }; + return labels[status] || ""; +};