Skip to content
Open
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
7 changes: 5 additions & 2 deletions src/renderer/components/ChatInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -64,6 +65,7 @@ const ChatInterface: React.FC<Props> = ({
}) => {
const { effectiveTheme } = useTheme();
const { toast } = useToast();
const { tasksByProjectId } = useTaskManagementContext();
const [isAgentInstalled, setIsAgentInstalled] = useState<boolean | null>(null);
const [agentStatuses, setAgentStatuses] = useState<
Record<string, { installed?: boolean; path?: string | null; version?: string | null }>
Expand Down Expand Up @@ -863,11 +865,12 @@ const ChatInterface: React.FC<Props> = ({
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
Expand Down
17 changes: 14 additions & 3 deletions src/renderer/components/CommandPaletteWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,31 @@ const CommandPaletteWrapper: React.FC<CommandPaletteWrapperProps> = ({
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 (
<CommandPalette
isOpen={isOpen}
onClose={onClose}
projects={projects as any}
projects={projectsWithTasks as any}
onSelectProject={(projectId) => {
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);
Expand Down
13 changes: 9 additions & 4 deletions src/renderer/components/TaskModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface CreateTaskResult {
useWorktree?: boolean;
baseRef?: string;
nameGenerated?: boolean;
targetProjectId?: string;
}

interface TaskModalProps {
Expand All @@ -63,9 +64,11 @@ interface TaskModalProps {
) => void;
}

export type TaskModalOverlayProps = BaseModalProps<CreateTaskResult>;
export interface TaskModalOverlayProps extends BaseModalProps<CreateTaskResult> {
targetProjectId?: string;
}

export function TaskModalOverlay({ onSuccess, onClose }: TaskModalOverlayProps) {
export function TaskModalOverlay({ onSuccess, onClose, targetProjectId }: TaskModalOverlayProps) {
return (
<TaskModal
onClose={onClose}
Expand All @@ -92,6 +95,7 @@ export function TaskModalOverlay({ onSuccess, onClose }: TaskModalOverlayProps)
useWorktree,
baseRef,
nameGenerated,
targetProjectId,
})
}
/>
Expand All @@ -105,10 +109,11 @@ const TaskModal: React.FC<TaskModalProps> = ({ 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('');
Expand Down
23 changes: 13 additions & 10 deletions src/renderer/components/kanban/KanbanBoard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<KanbanStatus, string> = {
Expand All @@ -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<Record<string, KanbanStatus>>({});

React.useEffect(() => {
Expand All @@ -31,7 +34,7 @@ const KanbanBoard: React.FC<{
React.useEffect(() => {
const offs: Array<() => void> = [];
const idleTimers = new Map<string, ReturnType<typeof setTimeout>>();
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));
Expand Down Expand Up @@ -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[] = (() => {
Expand Down Expand Up @@ -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[] = (() => {
Expand Down Expand Up @@ -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[] = (() => {
Expand Down Expand Up @@ -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<KanbanStatus, Task[]> = { 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);
Expand Down
5 changes: 4 additions & 1 deletion src/renderer/components/titlebar/TitlebarContext.tsx
Original file line number Diff line number Diff line change
@@ -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[];
Expand All @@ -17,11 +18,13 @@ const TitlebarContext: React.FC<TitlebarContextProps> = ({
onSelectProject,
onSelectTask,
}) => {
const { tasksByProjectId } = useTaskManagementContext();

if (!selectedProject) {
return <div />;
}

const tasks = selectedProject?.tasks ?? [];
const tasks = selectedProject ? (tasksByProjectId[selectedProject.id] ?? []) : [];
const projectValue = selectedProject.id;
const noTaskValue = '__no_task_selected__';
const taskValue = activeTask?.id ?? noTaskValue;
Expand Down
52 changes: 39 additions & 13 deletions src/renderer/hooks/useTaskManagement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ export function useTaskManagement() {
const archivingTaskIdsRef = useRef<Set<string>>(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(() => {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -933,7 +935,7 @@ export function useTaskManagement() {
baseRef,
});
},
[selectedProject, createTaskMutation]
[selectedProject, projects, createTaskMutation]
);

const handleTaskInterfaceReady = useCallback(() => {
Expand Down Expand Up @@ -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
),
});
};
Expand Down