diff --git a/.superset/setup.json b/.superset/setup.json index da10711d6b8..d99f3d2ffc3 100644 --- a/.superset/setup.json +++ b/.superset/setup.json @@ -1,4 +1,6 @@ { - "copy": [".env"], - "commands": ["bun i", "./update-port.sh"] -} + "copy": [ + "**/.env*" + ], + "commands": [] +} \ No newline at end of file diff --git a/apps/desktop/src/main/lib/workspace/worktree-operations.ts b/apps/desktop/src/main/lib/workspace/worktree-operations.ts index 459d62216e2..0b7428ed8f4 100644 --- a/apps/desktop/src/main/lib/workspace/worktree-operations.ts +++ b/apps/desktop/src/main/lib/workspace/worktree-operations.ts @@ -63,13 +63,15 @@ export async function createWorktree( // Create worktree object with cloned or empty tabs const now = new Date().toISOString(); + // Use description if provided, otherwise use title as description + const description = input.description || input.title; const worktree: Worktree = { id: randomUUID(), branch: branchName, path: worktreeResult.path!, tabs, createdAt: now, - ...(input.description && { description: input.description }), + ...(description && { description }), }; // Add to workspace diff --git a/apps/desktop/src/renderer/globals.css b/apps/desktop/src/renderer/globals.css index 01180ee0262..8692abbe85d 100644 --- a/apps/desktop/src/renderer/globals.css +++ b/apps/desktop/src/renderer/globals.css @@ -146,9 +146,14 @@ width: 100% !important; } - /* Hide scrollbar for workspace carousel */ + /* Hide scrollbar for workspace carousel and tabs */ + .hide-scrollbar { + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE/Edge */ + } + .hide-scrollbar::-webkit-scrollbar { - display: none; + display: none; /* Chrome/Safari/Electron */ } /* Dark mode scrollbar styling */ diff --git a/apps/desktop/src/renderer/screens/main/MainScreen.tsx b/apps/desktop/src/renderer/screens/main/MainScreen.tsx index 02576320478..6bfce618e8e 100644 --- a/apps/desktop/src/renderer/screens/main/MainScreen.tsx +++ b/apps/desktop/src/renderer/screens/main/MainScreen.tsx @@ -214,6 +214,7 @@ export function MainScreen() { setupStatus={setupStatus} setupOutput={setupOutput} onClearStatus={handleClearStatus} + currentWorkspaceId={currentWorkspace?.id || null} /> {/* Workspace Selection Modal */} diff --git a/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal.tsx b/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal.tsx index aaef40cc9df..bf3b1a4b5a1 100644 --- a/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal.tsx @@ -28,16 +28,17 @@ export const AddTaskModal: React.FC = ({ setupStatus, setupOutput, onClearStatus, - apiBaseUrl = "http://localhost:3000", + currentWorkspaceId, }) => { const [mode, setMode] = useState<"list" | "new">(initialMode); const [searchQuery, setSearchQuery] = useState(""); const [selectedTaskId, setSelectedTaskId] = useState(null); - const { tasks, isLoadingTasks, tasksError } = useTaskData( + const { tasks, isLoadingTasks, tasksError, refetch: refetchTasks } = useTaskData( isOpen, mode, - apiBaseUrl, + currentWorkspaceId ?? null, + worktrees, ); const formState = useTaskForm(isOpen, mode, branches, worktrees); @@ -85,16 +86,19 @@ export const AddTaskModal: React.FC = ({ } }, [isOpen, initialMode]); - // Automatically go back to list mode when creation completes + // Automatically go back to list mode when creation completes and refetch tasks useEffect(() => { if (!isCreating && setupStatus && mode === "new") { + // Refetch tasks to get the newly created worktree + void refetchTasks(); + const timer = setTimeout(() => { setMode("list"); onClearStatus?.(); }, 1500); return () => clearTimeout(timer); } - }, [isCreating, setupStatus, mode, onClearStatus]); + }, [isCreating, setupStatus, mode, onClearStatus, refetchTasks]); // Handle opening a task const handleOpenTask = useCallback(() => { diff --git a/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal/CreatingView.tsx b/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal/CreatingView.tsx index 851ba3e62a5..84d9f5a44a4 100644 --- a/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal/CreatingView.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal/CreatingView.tsx @@ -11,22 +11,28 @@ interface CreatingViewProps { onClose: () => void; } +function getStatusType(status?: string): "error" | "success" | "creating" { + if (!status) return "creating"; + + const lowerStatus = status.toLowerCase(); + if (lowerStatus.includes("failed") || lowerStatus.includes("error")) { + return "error"; + } + if (lowerStatus.includes("success") || lowerStatus.includes("completed")) { + return "success"; + } + return "creating"; +} + export const CreatingView: React.FC = ({ setupStatus, setupOutput, isCreating, onClose, }) => { - const isError = - setupStatus && - (setupStatus.toLowerCase().includes("failed") || - setupStatus.toLowerCase().includes("error")); - - const isSuccess = - setupStatus && - !isError && - (setupStatus.toLowerCase().includes("success") || - setupStatus.toLowerCase().includes("completed")); + const statusType = getStatusType(setupStatus); + const isError = statusType === "error"; + const isSuccess = statusType === "success"; return (
diff --git a/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal/types.ts b/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal/types.ts index a2a10b0399d..8bfff1f8838 100644 --- a/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal/types.ts +++ b/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal/types.ts @@ -55,6 +55,6 @@ export interface AddTaskModalProps { setupStatus?: string; setupOutput?: string; onClearStatus?: () => void; - apiBaseUrl?: string; + currentWorkspaceId?: string | null; } diff --git a/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal/useTaskData.ts b/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal/useTaskData.ts index 1b32331e33b..a6e2a0dea08 100644 --- a/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal/useTaskData.ts +++ b/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal/useTaskData.ts @@ -1,81 +1,71 @@ -import { useEffect, useState } from "react"; -import type { APITask, Task } from "./types"; -import { transformAPITaskToUITask } from "./utils"; +import { useCallback, useEffect, useRef, useState } from "react"; +import type { Worktree } from "shared/types"; +import type { Task } from "./types"; +import { transformWorktreeToTask } from "./utils"; export function useTaskData( isOpen: boolean, mode: "list" | "new", - apiBaseUrl: string, + workspaceId: string | null, + worktrees?: Worktree[], ) { const [tasks, setTasks] = useState([]); const [isLoadingTasks, setIsLoadingTasks] = useState(false); const [tasksError, setTasksError] = useState(null); + const fetchRef = useRef<(() => Promise) | null>(null); - // Fetch tasks when modal opens - useEffect(() => { - if (!isOpen || mode !== "list") return; + const fetchTasks = useCallback(async () => { + if (!workspaceId) { + setTasks([]); + return; + } - let cancelled = false; setIsLoadingTasks(true); setTasksError(null); - const fetchTasks = async () => { - try { - const url = `${apiBaseUrl}/api/trpc/task.all?input=${encodeURIComponent("{}")}`; - const response = await fetch(url, { - method: "GET", - headers: { - "Content-Type": "application/json", - }, - }); + try { + // Fetch workspace from config via IPC + const workspace = await window.ipcRenderer.invoke( + "workspace-get", + workspaceId, + ); - if (!response.ok) { - throw new Error(`Failed to fetch tasks: ${response.statusText}`); - } + if (!workspace) { + throw new Error("Workspace not found"); + } - const data = await response.json(); + // Transform worktrees to tasks + const transformedTasks = workspace.worktrees.map(transformWorktreeToTask); + setTasks(transformedTasks); + setTasksError(null); + } catch (error) { + console.error("Failed to fetch tasks:", error); + setTasksError( + error instanceof Error + ? error.message + : "Failed to load tasks from workspace.", + ); + setTasks([]); + } finally { + setIsLoadingTasks(false); + } + }, [workspaceId]); - // Handle different possible response formats - let apiTasks: APITask[] = []; - if (data.result?.data) { - apiTasks = Array.isArray(data.result.data) ? data.result.data : []; - } else if (data.result?.json) { - apiTasks = Array.isArray(data.result.json) ? data.result.json : []; - } else if (Array.isArray(data)) { - apiTasks = data; - } else if (Array.isArray(data.result)) { - apiTasks = data.result; - } + // Store fetch function in ref so it can be called externally + useEffect(() => { + fetchRef.current = fetchTasks; + }, [fetchTasks]); - if (!cancelled) { - const transformedTasks = apiTasks.map(transformAPITaskToUITask); - setTasks(transformedTasks); - setTasksError(null); - } - } catch (error) { - if (!cancelled) { - console.error("Failed to fetch tasks:", error); - setTasksError( - error instanceof Error - ? error.message - : "Failed to load tasks. Please check if the API server is running.", - ); - setTasks([]); - } - } finally { - if (!cancelled) { - setIsLoadingTasks(false); - } - } - }; + // Fetch tasks when modal opens or worktrees change + useEffect(() => { + if (!isOpen || mode !== "list" || !workspaceId) { + setTasks([]); + return; + } void fetchTasks(); + }, [isOpen, mode, workspaceId, fetchTasks, worktrees?.length]); - return () => { - cancelled = true; - }; - }, [isOpen, mode, apiBaseUrl]); - - return { tasks, isLoadingTasks, tasksError }; + return { tasks, isLoadingTasks, tasksError, refetch: fetchTasks }; } diff --git a/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal/utils.ts b/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal/utils.ts index 92af8e38164..23d5a41077f 100644 --- a/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal/utils.ts +++ b/apps/desktop/src/renderer/screens/main/components/Layout/AddTaskModal/utils.ts @@ -1,4 +1,6 @@ +import type { Worktree } from "shared/types"; import type { APITask, Task } from "./types"; +import type { TaskStatus } from "../StatusIndicator"; export function formatRelativeTime(date: Date): string { const now = new Date(); @@ -28,6 +30,43 @@ export function transformAPITaskToUITask(apiTask: APITask): Task { }; } +/** + * Transform a Worktree from workspace config to a Task for display + */ +export function transformWorktreeToTask(worktree: Worktree): Task { + // Generate slug from branch name + const slug = worktree.branch + .toLowerCase() + .replace(/[^a-z0-9-]/g, "-") + .replace(/-+/g, "-") + .replace(/^-+|-+$/g, ""); + + // Determine status based on worktree state + let status: TaskStatus = "planning"; + if (worktree.merged) { + status = "completed"; + } else if (worktree.prUrl) { + status = "ready-to-merge"; + } else if (worktree.tabs && worktree.tabs.length > 0) { + status = "working"; + } + + // Use description as name if available, otherwise use branch name + const name = worktree.description || worktree.branch; + + return { + id: worktree.id, + slug: slug || worktree.id, + name, + status, + branch: worktree.branch, + description: worktree.description || "", + assignee: "Unassigned", + assigneeAvatarUrl: "", + lastUpdated: formatRelativeTime(new Date(worktree.createdAt)), + }; +} + export function generateBranchNameWithCollisionAvoidance(title: string): string { // Convert to lowercase and replace spaces/special chars with hyphens let slug = title diff --git a/apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/TaskTabs.tsx b/apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/TaskTabs.tsx index ca8ae894da4..0b888fe44f7 100644 --- a/apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/TaskTabs.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/TaskTabs.tsx @@ -16,68 +16,80 @@ 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, - onDeleteWorktree, - workspaceId, - mode = "edit", - onModeChange, -}) => { - const [showRemoveDialog, setShowRemoveDialog] = useState(false); - const [removeWarning, setRemoveWarning] = useState(""); - const [worktreeToDelete, setWorktreeToDelete] = useState(null); - const tabsContainerRef = useRef(null); +const TAB_GAP = 4; // gap-1 = 4px +const MIN_TAB_WIDTH = 40; +const MAX_TAB_WIDTH = 240; +const WIDTH_BUFFER = 4; // Buffer to account for rounding and measurement discrepancies +const ADD_BUTTON_WIDTH = 32; // Approximate width of AddTaskButton + +// Custom hook for calculating tab widths based on available space +function useTabWidth(worktrees: Array<{ id: string }>) { + const containerRef = useRef(null); const [tabWidth, setTabWidth] = useState(undefined); - // Calculate tab width based on available space and number of tabs useEffect(() => { + if (!containerRef.current || worktrees.length === 0) { + setTabWidth(undefined); + return; + } + const updateTabWidth = () => { - if (!tabsContainerRef.current || worktrees.length === 0) { + if (!containerRef.current || worktrees.length === 0) { setTabWidth(undefined); return; } - const container = tabsContainerRef.current; - const containerWidth = container.offsetWidth; - const gap = 4; // gap-1 = 4px + const container = containerRef.current; const numTabs = worktrees.length; + const containerWidth = container.offsetWidth; - // Calculate available width per tab (accounting for gaps) - const availableWidth = containerWidth - (gap * (numTabs - 1)); + // Account for AddTaskButton width + gap, and gaps between tabs + // numTabs gaps: (numTabs - 1) between tabs + 1 before button + const addButtonWidth = ADD_BUTTON_WIDTH + TAB_GAP; + const totalGapWidth = TAB_GAP * numTabs; + const availableWidth = containerWidth - totalGapWidth - addButtonWidth - WIDTH_BUFFER; const calculatedWidth = availableWidth / numTabs; - // Apply min/max constraints (Chrome-like: min ~60px, max ~240px) - const MIN_WIDTH = 60; - const MAX_WIDTH = 240; - const constrainedWidth = Math.max( - MIN_WIDTH, - Math.min(MAX_WIDTH, calculatedWidth), - ); + const finalWidth = calculatedWidth < MIN_TAB_WIDTH + ? MIN_TAB_WIDTH + : Math.floor(Math.max(MIN_TAB_WIDTH, Math.min(MAX_TAB_WIDTH, calculatedWidth))); - setTabWidth(constrainedWidth); + setTabWidth(finalWidth); }; updateTabWidth(); - // Update on window resize const resizeObserver = new ResizeObserver(updateTabWidth); - if (tabsContainerRef.current) { - resizeObserver.observe(tabsContainerRef.current); - } + resizeObserver.observe(containerRef.current); return () => { resizeObserver.disconnect(); }; }, [worktrees.length]); + return { containerRef, tabWidth }; +} + +export const TaskTabs: React.FC = ({ + onCollapseSidebar, + onExpandSidebar, + isSidebarOpen, + onAddTask, + onCreatePR, + onMergePR, + worktrees, + selectedWorktreeId, + onWorktreeSelect, + onDeleteWorktree, + workspaceId, + mode = "edit", + onModeChange, +}) => { + const [showRemoveDialog, setShowRemoveDialog] = useState(false); + const [removeWarning, setRemoveWarning] = useState(""); + const [worktreeToDelete, setWorktreeToDelete] = useState(null); + const { containerRef: tabsContainerRef, tabWidth } = useTabWidth(worktrees); + const selectedWorktree = worktrees.find((wt) => wt.id === selectedWorktreeId); const canCreatePR = selectedWorktree && !selectedWorktree.isPending; const hasPR = selectedWorktree?.prUrl; @@ -88,55 +100,54 @@ export const TaskTabs: React.FC = ({ if (!onDeleteWorktree || !workspaceId) return; const worktree = worktrees.find((wt) => wt.id === worktreeId); - // Allow deletion of active/selected worktrees (same as sidebar behavior) - // Only prevent deletion of pending worktrees if (!worktree || worktree.isPending) return; - // Check if the worktree has uncommitted changes try { const canRemoveResult = await window.ipcRenderer.invoke( "worktree-can-remove", - { - workspaceId, - worktreeId, - }, + { workspaceId, worktreeId }, ); - // Build warning message if there are uncommitted changes - let warning = ""; - if (canRemoveResult.hasUncommittedChanges) { - warning = `Warning: This worktree (${worktree.branch}) has uncommitted changes. Removing it will delete these changes permanently.`; - } + const warning = canRemoveResult.hasUncommittedChanges + ? `Warning: This worktree (${worktree.branch}) has uncommitted changes. Removing it will delete these changes permanently.` + : ""; setRemoveWarning(warning); setWorktreeToDelete(worktreeId); setShowRemoveDialog(true); } catch (error) { console.error("Error checking if worktree can be removed:", error); - // Still show dialog even if check fails setRemoveWarning(""); setWorktreeToDelete(worktreeId); setShowRemoveDialog(true); } }; - const confirmRemoveWorktree = async () => { + const handleConfirmRemove = async () => { if (!onDeleteWorktree || !worktreeToDelete) return; + const worktreeId = worktreeToDelete; setShowRemoveDialog(false); setRemoveWarning(""); - const worktreeId = worktreeToDelete; setWorktreeToDelete(null); await onDeleteWorktree(worktreeId); }; + const handleCancelRemove = () => { + setShowRemoveDialog(false); + setRemoveWarning(""); + setWorktreeToDelete(null); + }; + return ( <>
+ {/* Bottom border line */} +
= ({
{worktrees.map((worktree, index) => { const isSelected = selectedWorktreeId === worktree.id; @@ -179,7 +190,9 @@ export const TaskTabs: React.FC = ({
); })} - +
+ +
@@ -206,7 +219,7 @@ export const TaskTabs: React.FC = ({ {worktreeToDelete && ( <> Are you sure you want to remove the worktree " - {worktrees.find((wt) => wt.id === worktreeToDelete)?.branch || + {worktrees.find((wt) => wt.id === worktreeToDelete)?.branch ?? worktreeToDelete} "? This action cannot be undone. @@ -222,17 +235,10 @@ export const TaskTabs: React.FC = ({ )} - - diff --git a/apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/WorktreeTabButton.tsx b/apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/WorktreeTabButton.tsx index c302ec41be9..2fe76318c1b 100644 --- a/apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/WorktreeTabButton.tsx +++ b/apps/desktop/src/renderer/screens/main/components/Layout/TaskTabs/WorktreeTabButton.tsx @@ -23,19 +23,22 @@ export const WorktreeTabButton: React.FC = ({ const isPending = worktree.isPending; return ( -
+ {isSelected && ( +
+ )}