diff --git a/apps/desktop/src/main/lib/terminal-ipcs.ts b/apps/desktop/src/main/lib/terminal-ipcs.ts index 40462033a3a..74ac4142459 100644 --- a/apps/desktop/src/main/lib/terminal-ipcs.ts +++ b/apps/desktop/src/main/lib/terminal-ipcs.ts @@ -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); }, diff --git a/apps/desktop/src/main/lib/tmux-manager.ts b/apps/desktop/src/main/lib/tmux-manager.ts index 4571b51389f..a2ac4d57636 100644 --- a/apps/desktop/src/main/lib/tmux-manager.ts +++ b/apps/desktop/src/main/lib/tmux-manager.ts @@ -199,6 +199,7 @@ class TmuxManager { cwd?: string; cols?: number; rows?: number; + command?: string; }): Promise { try { const sid = options?.id || randomUUID(); @@ -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); diff --git a/apps/desktop/src/renderer/screens/main/components/MainContent/TabContent.tsx b/apps/desktop/src/renderer/screens/main/components/MainContent/TabContent.tsx index 5e682208ff0..59fdf7e9db9 100644 --- a/apps/desktop/src/renderer/screens/main/components/MainContent/TabContent.tsx +++ b/apps/desktop/src/renderer/screens/main/components/MainContent/TabContent.tsx @@ -241,6 +241,7 @@ function TerminalTabContent({ hidden={!isSelected} onFocus={onFocus} cwd={terminalCwd} + command={tab.command} /> ); diff --git a/apps/desktop/src/renderer/screens/main/components/MainContent/Terminal.tsx b/apps/desktop/src/renderer/screens/main/components/MainContent/Terminal.tsx index 110092e2588..9790929e91c 100644 --- a/apps/desktop/src/renderer/screens/main/components/MainContent/Terminal.tsx +++ b/apps/desktop/src/renderer/screens/main/components/MainContent/Terminal.tsx @@ -15,6 +15,7 @@ interface TerminalProps { hidden?: boolean; onFocus?: () => void; cwd?: string; + command?: string | null; } interface TerminalMessage { @@ -75,6 +76,7 @@ export default function TerminalComponent({ hidden = false, onFocus, cwd, + command, }: TerminalProps) { const terminalRef = useRef(null); const [terminal, setTerminal] = useState(null); @@ -299,6 +301,7 @@ export default function TerminalComponent({ cwd, cols, rows, + command: command || undefined, }) .catch((error: Error) => { console.error("Failed to create terminal:", error); 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 b8f90207164..577c3faa0a7 100644 --- a/apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx +++ b/apps/desktop/src/renderer/screens/main/components/NewLayout/NewLayoutMain.tsx @@ -867,7 +867,12 @@ export const NewLayoutMain: React.FC = () => {
{mode === "plan" ? ( // Plan mode - show kanban board - + ) : ( // Edit mode - show workspace/terminal view 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 = ({ @@ -16,6 +30,11 @@ export const KanbanColumn: React.FC = ({ tasks, onTaskClick, statusColor = "bg-neutral-500", + currentWorkspace, + selectedWorktreeId, + onTabSelect, + onReload, + onUpdateTask, }) => { return (
@@ -39,6 +58,11 @@ export const KanbanColumn: React.FC = ({ key={task.id} task={task} onClick={() => onTaskClick(task)} + currentWorkspace={currentWorkspace} + selectedWorktreeId={selectedWorktreeId} + onTabSelect={onTabSelect} + onReload={onReload} + onUpdateTask={onUpdateTask} /> ))}
diff --git a/apps/desktop/src/renderer/screens/main/components/PlanView/PlanView.tsx b/apps/desktop/src/renderer/screens/main/components/PlanView/PlanView.tsx index e2c31342336..a76c3e60e8e 100644 --- a/apps/desktop/src/renderer/screens/main/components/PlanView/PlanView.tsx +++ b/apps/desktop/src/renderer/screens/main/components/PlanView/PlanView.tsx @@ -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"; @@ -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 = ({ + currentWorkspace, + selectedWorktreeId, + onTabSelect, + onReload, +}) => { // Initialize with mock tasks and add some variety to statuses const [tasks, setTasks] = useState(() => { // Modify some tasks to have different statuses for demo purposes @@ -128,6 +141,10 @@ export const PlanView: React.FC = () => { users={mockUsers} onBack={() => setViewingTask(null)} onUpdate={handleUpdateTask} + currentWorkspace={currentWorkspace} + selectedWorktreeId={selectedWorktreeId} + onTabSelect={onTabSelect} + onReload={onReload} /> ); } @@ -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} />
diff --git a/apps/desktop/src/renderer/screens/main/components/PlanView/TaskCard.tsx b/apps/desktop/src/renderer/screens/main/components/PlanView/TaskCard.tsx index 39ef1effcba..c3b107d46b5 100644 --- a/apps/desktop/src/renderer/screens/main/components/PlanView/TaskCard.tsx +++ b/apps/desktop/src/renderer/screens/main/components/PlanView/TaskCard.tsx @@ -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 = { @@ -19,14 +35,90 @@ const statusColors: Record = { canceled: "bg-red-500", }; -export const TaskCard: React.FC = ({ task, onClick }) => { +export const TaskCard: React.FC = ({ + 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 ( + )} ); diff --git a/apps/desktop/src/renderer/screens/main/components/PlanView/TaskPage.tsx b/apps/desktop/src/renderer/screens/main/components/PlanView/TaskPage.tsx index 3d56278785a..f6b54cfdca8 100644 --- a/apps/desktop/src/renderer/screens/main/components/PlanView/TaskPage.tsx +++ b/apps/desktop/src/renderer/screens/main/components/PlanView/TaskPage.tsx @@ -1,7 +1,8 @@ import type { RouterOutputs } from "@superset/api"; -import { ChevronDown, ChevronLeft, User as UserIcon } from "lucide-react"; +import { ChevronDown, ChevronLeft, Play, User as UserIcon } from "lucide-react"; import type React from "react"; import { useEffect, useRef, useState } from "react"; +import type { Workspace } from "shared/types"; type Task = RouterOutputs["task"]["all"][number]; type User = RouterOutputs["user"]["all"][number]; @@ -19,6 +20,10 @@ interface TaskPageProps { assigneeId?: string | null; }, ) => void; + currentWorkspace: Workspace | null; + selectedWorktreeId: string | null; + onTabSelect: (worktreeId: string, tabId: string) => void; + onReload: () => void; } const statusColors: Record = { @@ -48,6 +53,10 @@ export const TaskPage: React.FC = ({ users, onBack, onUpdate, + currentWorkspace, + selectedWorktreeId, + onTabSelect, + onReload, }) => { const statusColor = statusColors[task.status] || "bg-neutral-500"; const [title, setTitle] = useState(task.title); @@ -119,26 +128,99 @@ export const TaskPage: React.FC = ({ ? users.find((u) => u.id === assigneeId) : null; + const handleStartTask = async () => { + 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) + onUpdate(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 (
{/* Header with Breadcrumbs */}
-
+
+
+ + / + + {task.slug} + +
- / - - {task.slug} -
@@ -272,9 +354,8 @@ export const TaskPage: React.FC = ({ )}
diff --git a/packages/models/package.json b/packages/models/package.json new file mode 100644 index 00000000000..a0b7492998a --- /dev/null +++ b/packages/models/package.json @@ -0,0 +1,13 @@ +{ + "name": "@superset/models", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": {}, + "devDependencies": { + "@superset/typescript": "*", + "@types/node": "^24.9.1", + "bun-types": "^1.3.1", + "typescript": "^5.9.3" + } +} \ No newline at end of file