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 834137b8da9..b8f90207164 100644 --- a/apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx @@ -34,6 +34,20 @@ type UITask = { 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 = [ { @@ -212,10 +226,36 @@ const MOCK_TASKS = [ ]; // Helper function to enrich worktrees with task metadata -function enrichWorktreesWithTasks(worktrees: Worktree[]): WorktreeWithTask[] { - return worktrees.map((worktree) => { +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: [], + 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); + const matchingTask = MOCK_TASKS.find( + (task) => task.branch === worktree.branch, + ); if (matchingTask) { // Worktree has an associated task - add task metadata @@ -239,6 +279,9 @@ function enrichWorktreesWithTasks(worktrees: Worktree[]): WorktreeWithTask[] { // Worktree without task - return as-is return worktree; }); + + // Merge pending and real worktrees + return [...pendingAsWorktrees, ...enrichedWorktrees]; } export const NewLayoutMain: React.FC = () => { @@ -259,6 +302,9 @@ export const NewLayoutMain: React.FC = () => { 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) => @@ -543,7 +589,25 @@ export const NewLayoutMain: React.FC = () => { } handleCloseAddTaskModal(); } else { - // Worktree doesn't exist - create it + // 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", { @@ -555,15 +619,28 @@ export const NewLayoutMain: React.FC = () => { }); 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); } - handleCloseAddTaskModal(); + } 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), + ); } })(); } @@ -578,6 +655,25 @@ export const NewLayoutMain: React.FC = () => { }) => { 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 @@ -590,6 +686,11 @@ export const NewLayoutMain: React.FC = () => { }); 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(); @@ -600,12 +701,18 @@ export const NewLayoutMain: React.FC = () => { if (result.worktree.tabs && result.worktree.tabs.length > 0) { handleTabSelect(result.worktree.id, result.worktree.tabs[0].id); } - - // Close the modal - handleCloseAddTaskModal(); + } 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), + ); } })(); }; @@ -734,9 +841,15 @@ export const NewLayoutMain: React.FC = () => { onExpandSidebar={handleExpandSidebar} isSidebarOpen={isSidebarOpen} onAddTask={handleOpenAddTaskModal} - worktrees={enrichWorktreesWithTasks(currentWorkspace?.worktrees || [])} + worktrees={enrichWorktreesWithTasks( + currentWorkspace?.worktrees || [], + pendingWorktrees, + )} selectedWorktreeId={selectedWorktreeId} onWorktreeSelect={(worktreeId) => { + // Don't allow selecting pending worktrees + if (worktreeId.startsWith("pending-")) return; + setSelectedWorktreeId(worktreeId); // Select first tab in the worktree const worktree = currentWorkspace?.worktrees?.find( diff --git a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs.tsx b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs.tsx index cab07775fba..9141ec6b826 100644 --- a/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs.tsx +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/TaskTabs.tsx @@ -5,7 +5,7 @@ import { HoverCardTrigger, } from "@superset/ui/hover-card"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; -import { PanelLeftClose, PanelLeftOpen, Plus } from "lucide-react"; +import { Loader2, PanelLeftClose, PanelLeftOpen, Plus } from "lucide-react"; import type React from "react"; import type { Worktree } from "shared/types"; import { StatusIndicator, type TaskStatus } from "./StatusIndicator"; @@ -13,6 +13,7 @@ import { TaskAssignee } from "./TaskAssignee"; // Extended Worktree type with optional task metadata export interface WorktreeWithTask extends Worktree { + isPending?: boolean; // Flag for optimistic updates task?: { id: string; slug: string; @@ -134,6 +135,7 @@ export const TaskTabs: React.FC = ({ {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; @@ -154,6 +156,7 @@ export const TaskTabs: React.FC = ({ - {hasTask && task ? ( + {isPending ? ( +
+ {/* Pending state */} +
+ +

+ Creating worktree... +

+
+

+ Setting up git worktree and initializing workspace +

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