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
8 changes: 5 additions & 3 deletions .superset/setup.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
{
"copy": [".env"],
"commands": ["bun i", "./update-port.sh"]
}
"copy": [
"**/.env*"
],
"commands": []
}
4 changes: 3 additions & 1 deletion apps/desktop/src/main/lib/workspace/worktree-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 7 additions & 2 deletions apps/desktop/src/renderer/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/renderer/screens/main/MainScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ export function MainScreen() {
setupStatus={setupStatus}
setupOutput={setupOutput}
onClearStatus={handleClearStatus}
currentWorkspaceId={currentWorkspace?.id || null}
/>

{/* Workspace Selection Modal */}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,17 @@ export const AddTaskModal: React.FC<AddTaskModalProps> = ({
setupStatus,
setupOutput,
onClearStatus,
apiBaseUrl = "http://localhost:3000",
currentWorkspaceId,
}) => {
const [mode, setMode] = useState<"list" | "new">(initialMode);
const [searchQuery, setSearchQuery] = useState("");
const [selectedTaskId, setSelectedTaskId] = useState<string | null>(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);
Expand Down Expand Up @@ -85,16 +86,19 @@ export const AddTaskModal: React.FC<AddTaskModalProps> = ({
}
}, [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(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CreatingViewProps> = ({
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 (
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,6 @@ export interface AddTaskModalProps {
setupStatus?: string;
setupOutput?: string;
onClearStatus?: () => void;
apiBaseUrl?: string;
currentWorkspaceId?: string | null;
}

Original file line number Diff line number Diff line change
@@ -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<Task[]>([]);
const [isLoadingTasks, setIsLoadingTasks] = useState(false);
const [tasksError, setTasksError] = useState<string | null>(null);
const fetchRef = useRef<(() => Promise<void>) | 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 };
}

Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading