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: 7 additions & 1 deletion apps/desktop/src/main/lib/terminal-ipcs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ export function registerTerminalIPCs(mainWindow: BrowserWindow) {
"terminal-create",
async (
_event,
options: { id?: string; cols?: number; rows?: number; cwd?: string },
options: {
id?: string;
cols?: number;
rows?: number;
cwd?: string;
command?: string;
},
) => {
return await tmuxManager.create(options);
},
Expand Down
9 changes: 9 additions & 0 deletions apps/desktop/src/main/lib/tmux-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ class TmuxManager {
cwd?: string;
cols?: number;
rows?: number;
command?: string;
}): Promise<string> {
try {
const sid = options?.id || randomUUID();
Expand Down Expand Up @@ -330,6 +331,14 @@ class TmuxManager {
// Persist session registry
this.saveSessionsToDisk();

// Execute initial command if provided
if (options?.command) {
// Wait a bit for terminal to be ready
setTimeout(() => {
this.executeCommand(sid, options.command!);
}, 500);
}

return sid;
} catch (error) {
console.error("[TmuxManager] Failed to create/attach terminal:", error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ function TerminalTabContent({
hidden={!isSelected}
onFocus={onFocus}
cwd={terminalCwd}
command={tab.command}
/>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface TerminalProps {
hidden?: boolean;
onFocus?: () => void;
cwd?: string;
command?: string | null;
}

interface TerminalMessage {
Expand Down Expand Up @@ -75,6 +76,7 @@ export default function TerminalComponent({
hidden = false,
onFocus,
cwd,
command,
}: TerminalProps) {
const terminalRef = useRef<HTMLDivElement>(null);
const [terminal, setTerminal] = useState<XTerm | null>(null);
Expand Down Expand Up @@ -299,6 +301,7 @@ export default function TerminalComponent({
cwd,
cols,
rows,
command: command || undefined,
})
.catch((error: Error) => {
console.error("Failed to create terminal:", error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -867,7 +867,12 @@ export const NewLayoutMain: React.FC = () => {
<div className="flex-1 overflow-hidden border-t border-neutral-700">
{mode === "plan" ? (
// Plan mode - show kanban board
<PlanView />
<PlanView
currentWorkspace={currentWorkspace}
selectedWorktreeId={selectedWorktreeId}
onTabSelect={handleTabSelect}
onReload={loadAllWorkspaces}
/>
) : (
// Edit mode - show workspace/terminal view
<ResizablePanelGroup
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { RouterOutputs } from "@superset/api";
import type React from "react";
import type { Workspace } from "shared/types";
import { TaskCard } from "./TaskCard";

type Task = RouterOutputs["task"]["all"][number];
Expand All @@ -9,13 +10,31 @@ interface KanbanColumnProps {
tasks: Task[];
onTaskClick: (task: Task) => void;
statusColor?: string;
currentWorkspace: Workspace | null;
selectedWorktreeId: string | null;
onTabSelect: (worktreeId: string, tabId: string) => void;
onReload: () => void;
onUpdateTask: (
taskId: string,
updates: {
title: string;
description: string;
status: Task["status"];
assigneeId?: string | null;
},
) => void;
}

export const KanbanColumn: React.FC<KanbanColumnProps> = ({
title,
tasks,
onTaskClick,
statusColor = "bg-neutral-500",
currentWorkspace,
selectedWorktreeId,
onTabSelect,
onReload,
onUpdateTask,
}) => {
return (
<div className="flex flex-col h-full min-w-[300px] w-[300px]">
Expand All @@ -39,6 +58,11 @@ export const KanbanColumn: React.FC<KanbanColumnProps> = ({
key={task.id}
task={task}
onClick={() => onTaskClick(task)}
currentWorkspace={currentWorkspace}
selectedWorktreeId={selectedWorktreeId}
onTabSelect={onTabSelect}
onReload={onReload}
onUpdateTask={onUpdateTask}
/>
))}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { RouterOutputs } from "@superset/api";
import { Plus } from "lucide-react";
import type React from "react";
import { useMemo, useState } from "react";
import type { Workspace } from "shared/types";
import { mockTasks, mockUsers } from "../../../../../lib/mock-data";
import { CreateTaskModal } from "./CreateTaskModal";
import { KanbanColumn } from "./KanbanColumn";
Expand All @@ -10,7 +11,19 @@ import { TaskPage } from "./TaskPage";
type Task = RouterOutputs["task"]["all"][number];
type User = RouterOutputs["user"]["all"][number];

export const PlanView: React.FC = () => {
interface PlanViewProps {
currentWorkspace: Workspace | null;
selectedWorktreeId: string | null;
onTabSelect: (worktreeId: string, tabId: string) => void;
onReload: () => void;
}

export const PlanView: React.FC<PlanViewProps> = ({
currentWorkspace,
selectedWorktreeId,
onTabSelect,
onReload,
}) => {
// Initialize with mock tasks and add some variety to statuses
const [tasks, setTasks] = useState<Task[]>(() => {
// Modify some tasks to have different statuses for demo purposes
Expand Down Expand Up @@ -128,6 +141,10 @@ export const PlanView: React.FC = () => {
users={mockUsers}
onBack={() => setViewingTask(null)}
onUpdate={handleUpdateTask}
currentWorkspace={currentWorkspace}
selectedWorktreeId={selectedWorktreeId}
onTabSelect={onTabSelect}
onReload={onReload}
/>
);
}
Expand Down Expand Up @@ -163,30 +180,55 @@ export const PlanView: React.FC = () => {
tasks={tasksByStatus.backlog}
onTaskClick={setViewingTask}
statusColor="bg-neutral-500"
currentWorkspace={currentWorkspace}
selectedWorktreeId={selectedWorktreeId}
onTabSelect={onTabSelect}
onReload={onReload}
onUpdateTask={handleUpdateTask}
/>
<KanbanColumn
title="Todo"
tasks={tasksByStatus.todo}
onTaskClick={setViewingTask}
statusColor="bg-blue-500"
currentWorkspace={currentWorkspace}
selectedWorktreeId={selectedWorktreeId}
onTabSelect={onTabSelect}
onReload={onReload}
onUpdateTask={handleUpdateTask}
/>
<KanbanColumn
title="Pending"
tasks={tasksByStatus.planning}
onTaskClick={setViewingTask}
statusColor="bg-yellow-500"
currentWorkspace={currentWorkspace}
selectedWorktreeId={selectedWorktreeId}
onTabSelect={onTabSelect}
onReload={onReload}
onUpdateTask={handleUpdateTask}
/>
<KanbanColumn
title="Needs Feedback"
tasks={tasksByStatus["needs-feedback"]}
onTaskClick={setViewingTask}
statusColor="bg-orange-500"
currentWorkspace={currentWorkspace}
selectedWorktreeId={selectedWorktreeId}
onTabSelect={onTabSelect}
onReload={onReload}
onUpdateTask={handleUpdateTask}
/>
<KanbanColumn
title="Completed"
tasks={tasksByStatus.completed}
onTaskClick={setViewingTask}
statusColor="bg-green-600"
currentWorkspace={currentWorkspace}
selectedWorktreeId={selectedWorktreeId}
onTabSelect={onTabSelect}
onReload={onReload}
onUpdateTask={handleUpdateTask}
/>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import type { RouterOutputs } from "@superset/api";
import { Play } from "lucide-react";
import type React from "react";
import { useState } from "react";
import type { Workspace } from "shared/types";

type Task = RouterOutputs["task"]["all"][number];

interface TaskCardProps {
task: Task;
onClick: () => void;
currentWorkspace: Workspace | null;
selectedWorktreeId: string | null;
onTabSelect: (worktreeId: string, tabId: string) => void;
onReload: () => void;
onUpdateTask: (
taskId: string,
updates: {
title: string;
description: string;
status: Task["status"];
assigneeId?: string | null;
},
) => void;
}

const statusColors: Record<string, string> = {
Expand All @@ -19,14 +35,90 @@ const statusColors: Record<string, string> = {
canceled: "bg-red-500",
};

export const TaskCard: React.FC<TaskCardProps> = ({ task, onClick }) => {
export const TaskCard: React.FC<TaskCardProps> = ({
task,
onClick,
currentWorkspace,
selectedWorktreeId,
onTabSelect,
onReload,
onUpdateTask,
}) => {
const statusColor = statusColors[task.status] || "bg-neutral-500";
const [isHovered, setIsHovered] = useState(false);

const handleStartTask = async (e: React.MouseEvent) => {
e.stopPropagation();

if (!currentWorkspace) {
console.error("No workspace selected");
return;
}

// Find worktree to use: either the selected one, task's branch worktree, or first worktree
let targetWorktreeId = selectedWorktreeId;

if (!targetWorktreeId) {
// Try to find a worktree matching the task's branch
const taskWorktree = currentWorkspace.worktrees?.find(
(wt) => wt.branch === task.branch,
);

if (taskWorktree) {
targetWorktreeId = taskWorktree.id;
} else if (currentWorkspace.worktrees && currentWorkspace.worktrees.length > 0) {
// Use the first worktree as fallback
targetWorktreeId = currentWorkspace.worktrees[0].id;
}
}

if (!targetWorktreeId) {
console.error("No worktree available to create terminal");
return;
}

try {
// Create a new terminal with claude command
const result = await window.ipcRenderer.invoke("tab-create", {
workspaceId: currentWorkspace.id,
worktreeId: targetWorktreeId,
name: `Task: ${task.slug}`,
type: "terminal",
command: `claude "hi"`,
});

if (result.success) {
// Update task status to planning (pending)
onUpdateTask(task.id, {
title: task.title,
description: task.description || "",
status: "planning",
});

// Reload workspace to get updated tab data
await onReload();

// Select the new tab after reload
const newTabId = result.tab?.id;
if (newTabId) {
// Small delay to ensure workspace is reloaded
setTimeout(() => {
onTabSelect(targetWorktreeId, newTabId);
}, 100);
}
}
} catch (error) {
console.error("Error starting task:", error);
}
};

return (
<button
type="button"
onClick={onClick}
className="w-full bg-neutral-900/40 hover:bg-neutral-900/70 border border-neutral-800/50 hover:border-neutral-700/70 rounded-xl p-3.5 text-left transition-all group shadow-sm hover:shadow-md"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className="w-full bg-neutral-900/40 hover:bg-neutral-900/70 border border-neutral-800/50 hover:border-neutral-700/70 rounded-xl p-3.5 text-left transition-all group shadow-sm hover:shadow-md relative"
>
{/* Task header */}
<div className="flex items-start gap-2 mb-2.5">
Expand Down Expand Up @@ -56,6 +148,18 @@ export const TaskCard: React.FC<TaskCardProps> = ({ task, onClick }) => {
</div>
)}
<div className="flex-1" />

{/* Start Task button - appears on hover for TODO tasks */}
{isHovered && task.status === "todo" && (
<button
type="button"
onClick={handleStartTask}
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium rounded-md transition-colors flex items-center gap-1.5"
>
<Play size={12} className="fill-white" />
<span>Start Task</span>
</button>
)}
</div>
</button>
);
Expand Down
Loading
Loading