Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
{
Expand Down Expand Up @@ -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
Comment on lines +234 to +253
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Build breaks: populate required Worktree fields for pending entries.

WorktreeWithTask extends Worktree, so every pending object you create here must satisfy the base interface. Right now the build fails with TS2322 because createdAt is missing, and title isn’t a known property on Worktree. Please supply the required fields and drop the extra property so pending entries compile like the real ones.

 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
+		path: "", // Pending worktrees don't have a path yet
+		createdAt: new Date().toISOString(),
+		tabs: [],
+		description: pending.description,
+		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,
 	}),
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 pendingAsWorktrees: WorktreeWithTask[] = pendingWorktrees.map(
(pending) => ({
id: pending.id,
branch: pending.branch,
path: "", // Pending worktrees don't have a path yet
createdAt: new Date().toISOString(),
tabs: [],
description: pending.description,
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,
}),
);
🧰 Tools
🪛 GitHub Actions: CI

[error] 234-234: TS2322: Type '{ id: string; branch: string; path: string; tabs: never[]; isPending: true; task: { id: string; slug: string; title: string; status: TaskStatus; description: string; } | undefined; }[]' is not assignable to type 'WorktreeWithTask[]'. Property 'createdAt' is missing in type '{ id: string; branch: string; path: string; tabs: never[]; isPending: true; task: { id: string; slug: string; title: string; status: TaskStatus; description: string; } | undefined; }' but required in type 'WorktreeWithTask'.

🤖 Prompt for AI Agents
In apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx
around lines 234 to 253, the temporary pending worktree objects are missing
required Worktree fields and include an extra property that breaks typing; add a
createdAt field on each pending worktree (e.g. use pending.createdAt if present
or set to a current ISO timestamp) so the object satisfies the Worktree base
interface, and remove the non-existent title property from the nested task
object (keep slug/name/status/description as before) so the pending entries
compile like the real ones.

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
Expand All @@ -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 = () => {
Expand All @@ -259,6 +302,9 @@ export const NewLayoutMain: React.FC = () => {
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [mode, setMode] = useState<"plan" | "edit">("edit");
const [pendingWorktrees, setPendingWorktrees] = useState<PendingWorktree[]>(
[],
);

// Compute which tasks have worktrees (are "open")
const openTasks = MOCK_TASKS.filter((task) =>
Expand Down Expand Up @@ -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", {
Expand All @@ -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),
);
}
})();
}
Expand All @@ -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
Expand All @@ -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();

Expand All @@ -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),
);
}
})();
};
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ 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";
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;
Expand Down Expand Up @@ -134,6 +135,7 @@ export const TaskTabs: React.FC<TaskTabsProps> = ({
{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;
Expand All @@ -154,25 +156,43 @@ export const TaskTabs: React.FC<TaskTabsProps> = ({
<button
type="button"
onClick={() => onWorktreeSelect(worktree.id)}
disabled={isPending}
className={`
flex items-center gap-2 px-3 h-8 rounded-t-md transition-all border-t border-x
${
selectedWorktreeId === worktree.id
? "bg-neutral-900 text-white border-neutral-700 -mb-px"
: "bg-neutral-800/50 text-neutral-400 hover:text-neutral-200 hover:bg-neutral-800 border-transparent"
}
${isPending ? "opacity-70 cursor-wait" : ""}
`}
>
{hasTask && task && (
<StatusIndicator status={task.status} showLabel={false} />
{isPending ? (
<Loader2 size={14} className="animate-spin text-blue-400" />
) : (
hasTask &&
task && <StatusIndicator status={task.status} showLabel={false} />
)}
<span className="text-sm whitespace-nowrap">
{hasTask && task ? `[${task.slug}] ${task.title}` : displayTitle}
</span>
</button>
</HoverCardTrigger>
<HoverCardContent side="bottom" align="start" className="w-96">
{hasTask && task ? (
{isPending ? (
<div className="space-y-2">
{/* Pending state */}
<div className="flex items-center gap-2">
<Loader2 size={16} className="animate-spin text-blue-400" />
<h4 className="font-semibold text-sm text-white">
Creating worktree...
</h4>
</div>
<p className="text-xs text-neutral-400">
Setting up git worktree and initializing workspace
</p>
</div>
) : hasTask && task ? (
<div className="space-y-3">
{/* Task view */}
<div className="flex items-start justify-between gap-3">
Expand Down
Loading