diff --git a/src/renderer/components/ChatInterface.tsx b/src/renderer/components/ChatInterface.tsx index 1a13bccf6..86ec3674e 100644 --- a/src/renderer/components/ChatInterface.tsx +++ b/src/renderer/components/ChatInterface.tsx @@ -28,6 +28,7 @@ import { makePtyId } from '@shared/ptyId'; import { generateTaskName } from '../lib/branchNameGenerator'; import { ensureUniqueTaskName } from '../lib/taskNames'; import type { Project } from '../types/app'; +import { useTaskManagementContext } from '../contexts/TaskManagementContext'; declare const window: Window & { electronAPI: { @@ -64,6 +65,7 @@ const ChatInterface: React.FC = ({ }) => { const { effectiveTheme } = useTheme(); const { toast } = useToast(); + const { tasksByProjectId } = useTaskManagementContext(); const [isAgentInstalled, setIsAgentInstalled] = useState(null); const [agentStatuses, setAgentStatuses] = useState< Record @@ -863,11 +865,12 @@ const ChatInterface: React.FC = ({ const generated = generateTaskName(message); if (!generated) return; - const existingNames = (project.tasks || []).map((t) => t.name); + const projectTasks = project ? (tasksByProjectId[project.id] ?? []) : []; + const existingNames = projectTasks.map((t) => t.name); const uniqueName = ensureUniqueTaskName(generated, existingNames); void onRenameTask(project, task, uniqueName); }, - [project, task, onRenameTask] + [project, task, onRenameTask, tasksByProjectId] ); // Whether to enable first-message capture for this task diff --git a/src/renderer/components/CommandPaletteWrapper.tsx b/src/renderer/components/CommandPaletteWrapper.tsx index 104cee213..cbae9abc7 100644 --- a/src/renderer/components/CommandPaletteWrapper.tsx +++ b/src/renderer/components/CommandPaletteWrapper.tsx @@ -26,20 +26,31 @@ const CommandPaletteWrapper: React.FC = ({ const { toggle: toggleRightSidebar } = useRightSidebar(); const { toggleTheme } = useTheme(); const { projects, handleSelectProject, handleOpenProject } = useProjectManagementContext(); - const { handleSelectTask } = useTaskManagementContext(); + const { handleSelectTask, tasksByProjectId } = useTaskManagementContext(); + + // Populate projects with their tasks from tasksByProjectId + const projectsWithTasks = React.useMemo( + () => + projects.map((project) => ({ + ...project, + tasks: tasksByProjectId[project.id] ?? [], + })), + [projects, tasksByProjectId] + ); return ( { const project = projects.find((p) => p.id === projectId); if (project) handleSelectProject(project); }} onSelectTask={(projectId, taskId) => { + const tasks = tasksByProjectId[projectId] ?? []; + const task = tasks.find((w: Task) => w.id === taskId); const project = projects.find((p) => p.id === projectId); - const task = project?.tasks?.find((w: Task) => w.id === taskId); if (project && task) { handleSelectProject(project); handleSelectTask(task); diff --git a/src/renderer/components/TaskModal.tsx b/src/renderer/components/TaskModal.tsx index dde36df9d..eb761e587 100644 --- a/src/renderer/components/TaskModal.tsx +++ b/src/renderer/components/TaskModal.tsx @@ -45,6 +45,7 @@ export interface CreateTaskResult { useWorktree?: boolean; baseRef?: string; nameGenerated?: boolean; + targetProjectId?: string; } interface TaskModalProps { @@ -63,9 +64,11 @@ interface TaskModalProps { ) => void; } -export type TaskModalOverlayProps = BaseModalProps; +export interface TaskModalOverlayProps extends BaseModalProps { + targetProjectId?: string; +} -export function TaskModalOverlay({ onSuccess, onClose }: TaskModalOverlayProps) { +export function TaskModalOverlay({ onSuccess, onClose, targetProjectId }: TaskModalOverlayProps) { return ( @@ -105,10 +109,11 @@ const TaskModal: React.FC = ({ onClose, onCreateTask }) => { projectBranchOptions: branchOptions, isLoadingBranches, } = useProjectManagementContext(); - const { linkedGithubIssueMap } = useTaskManagementContext(); + const { linkedGithubIssueMap, tasksByProjectId } = useTaskManagementContext(); const projectName = selectedProject?.name || ''; - const existingNames = (selectedProject?.tasks || []).map((w) => w.name); + const projectTasks = selectedProject ? (tasksByProjectId[selectedProject.id] ?? []) : []; + const existingNames = projectTasks.map((w) => w.name); const projectPath = selectedProject?.path; // Form state const [taskName, setTaskName] = useState(''); diff --git a/src/renderer/components/kanban/KanbanBoard.tsx b/src/renderer/components/kanban/KanbanBoard.tsx index 9645cfeaa..e6f5e8844 100644 --- a/src/renderer/components/kanban/KanbanBoard.tsx +++ b/src/renderer/components/kanban/KanbanBoard.tsx @@ -8,6 +8,7 @@ import { getAll, setStatus, type KanbanStatus } from '../../lib/kanbanStore'; import { subscribeDerivedStatus, watchTaskPty, watchTaskActivity } from '../../lib/taskStatus'; import { activityStore } from '../../lib/activityStore'; import { refreshPrStatus } from '../../lib/prStatusStore'; +import { useTaskManagementContext } from '../../contexts/TaskManagementContext'; const order: KanbanStatus[] = ['todo', 'in-progress', 'done']; const titles: Record = { @@ -21,6 +22,8 @@ const KanbanBoard: React.FC<{ onOpenTask?: (ws: Task) => void; onCreateTask?: () => void; }> = ({ project, onOpenTask, onCreateTask }) => { + const { tasksByProjectId } = useTaskManagementContext(); + const tasks = tasksByProjectId[project.id] ?? []; const [statusMap, setStatusMap] = React.useState>({}); React.useEffect(() => { @@ -31,7 +34,7 @@ const KanbanBoard: React.FC<{ React.useEffect(() => { const offs: Array<() => void> = []; const idleTimers = new Map>(); - const wsList = project.tasks || []; + const wsList = tasks || []; for (const ws of wsList) { // Watch PTY output to capture terminal-based providers as activity offs.push(watchTaskPty(ws.id)); @@ -99,12 +102,12 @@ const KanbanBoard: React.FC<{ } return () => offs.forEach((f) => f()); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [project.id, project.tasks?.length]); + }, [project.id, tasks.length]); // Promote any task with local changes directly to "Ready for review" (done) React.useEffect(() => { let cancelled = false; - const wsList = project.tasks || []; + const wsList = tasks || []; const check = async () => { for (const ws of wsList) { const variantPaths: string[] = (() => { @@ -150,12 +153,12 @@ const KanbanBoard: React.FC<{ cancelled = true; window.clearInterval(id); }; - }, [project.id, project.tasks?.length]); + }, [project.id, tasks.length]); // Promote any task with an open PR to "Ready for review" (done) React.useEffect(() => { let cancelled = false; - const wsList = project.tasks || []; + const wsList = tasks || []; const check = async () => { for (const ws of wsList) { const variantPaths: string[] = (() => { @@ -201,11 +204,11 @@ const KanbanBoard: React.FC<{ cancelled = true; window.clearInterval(id); }; - }, [project.id, project.tasks?.length]); + }, [project.id, tasks.length]); React.useEffect(() => { let cancelled = false; - const wsList = project.tasks || []; + const wsList = tasks || []; const check = async () => { for (const ws of wsList) { const variantPaths: string[] = (() => { @@ -253,14 +256,14 @@ const KanbanBoard: React.FC<{ cancelled = true; window.clearInterval(id); }; - }, [project.id, project.tasks?.length]); + }, [project.id, tasks.length]); const byStatus: Record = { todo: [], 'in-progress': [], done: [] }; - for (const ws of project.tasks || []) { + for (const ws of tasks || []) { const s = statusMap[ws.id] || 'todo'; byStatus[s].push(ws); } - const hasAny = (project.tasks?.length ?? 0) > 0; + const hasAny = (tasks.length ?? 0) > 0; const handleDrop = (target: KanbanStatus, taskId: string) => { setStatus(taskId, target); diff --git a/src/renderer/components/titlebar/TitlebarContext.tsx b/src/renderer/components/titlebar/TitlebarContext.tsx index d7f8614b5..df3693913 100644 --- a/src/renderer/components/titlebar/TitlebarContext.tsx +++ b/src/renderer/components/titlebar/TitlebarContext.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; import type { Project, Task } from '../../types/app'; +import { useTaskManagementContext } from '../../contexts/TaskManagementContext'; interface TitlebarContextProps { projects: Project[]; @@ -17,11 +18,13 @@ const TitlebarContext: React.FC = ({ onSelectProject, onSelectTask, }) => { + const { tasksByProjectId } = useTaskManagementContext(); + if (!selectedProject) { return
; } - const tasks = selectedProject?.tasks ?? []; + const tasks = selectedProject ? (tasksByProjectId[selectedProject.id] ?? []) : []; const projectValue = selectedProject.id; const noTaskValue = '__no_task_selected__'; const taskValue = activeTask?.id ?? noTaskValue; diff --git a/src/renderer/hooks/useTaskManagement.ts b/src/renderer/hooks/useTaskManagement.ts index 5b42131f3..a8f57db56 100644 --- a/src/renderer/hooks/useTaskManagement.ts +++ b/src/renderer/hooks/useTaskManagement.ts @@ -220,6 +220,7 @@ export function useTaskManagement() { const archivingTaskIdsRef = useRef>(new Set()); const openTaskModalImplRef = useRef<() => void>(() => {}); const openTaskModal = useCallback(() => openTaskModalImplRef.current(), []); + const startCreateTaskFromSidebarImplRef = useRef<(project: Project) => void>(() => {}); // Reset active task when project management signals a navigation away useEffect(() => { @@ -378,14 +379,9 @@ export function useTaskManagement() { } }, [selectedProject, openTaskModal]); - const handleStartCreateTaskFromSidebar = useCallback( - (project: Project) => { - const targetProject = projects.find((p) => p.id === project.id) || project; - activateProjectView(targetProject); - openTaskModal(); - }, - [activateProjectView, projects, openTaskModal] - ); + const handleStartCreateTaskFromSidebar = useCallback((project: Project) => { + startCreateTaskFromSidebarImplRef.current(project); + }, []); // --------------------------------------------------------------------------- // Delete task mutation @@ -915,12 +911,18 @@ export function useTaskManagement() { autoApprove?: boolean, useWorktree: boolean = true, baseRef?: string, - nameGenerated?: boolean + nameGenerated?: boolean, + targetProjectId?: string ) => { - if (!selectedProject) return; + // Use explicitly passed project ID or fall back to selectedProject + const targetProject = targetProjectId + ? projects.find((p) => p.id === targetProjectId) + : selectedProject; + + if (!targetProject) return; setIsCreatingTask(true); createTaskMutation.mutate({ - project: selectedProject, + project: targetProject, taskName, initialPrompt, agentRuns, @@ -933,7 +935,7 @@ export function useTaskManagement() { baseRef, }); }, - [selectedProject, createTaskMutation] + [selectedProject, projects, createTaskMutation] ); const handleTaskInterfaceReady = useCallback(() => { @@ -965,7 +967,31 @@ export function useTaskManagement() { result.autoApprove, result.useWorktree, result.baseRef, - result.nameGenerated + result.nameGenerated, + result.targetProjectId + ), + }); + }; + + // Wire up handleStartCreateTaskFromSidebar with the latest handleCreateTask + startCreateTaskFromSidebarImplRef.current = (project: Project) => { + const targetProject = projects.find((p) => p.id === project.id) || project; + activateProjectView(targetProject); + showModal('taskModal', { + targetProjectId: targetProject.id, + onSuccess: (result) => + handleCreateTask( + result.name, + result.initialPrompt, + result.agentRuns, + result.linkedLinearIssue ?? null, + result.linkedGithubIssue ?? null, + result.linkedJiraIssue ?? null, + result.autoApprove, + result.useWorktree, + result.baseRef, + result.nameGenerated, + result.targetProjectId ), }); };