fix(FN-000): scope dashboard project flows
This commit is contained in:
@@ -41,6 +41,7 @@ function relativeTime(iso: string): string {
|
||||
|
||||
interface AgentDetailViewProps {
|
||||
agentId: string;
|
||||
projectId?: string;
|
||||
onClose: () => void;
|
||||
addToast: (message: string, type?: "success" | "error") => void;
|
||||
}
|
||||
@@ -68,7 +69,7 @@ const RUN_STATUS_ICONS: Record<string, { icon: typeof CheckCircle; color: string
|
||||
terminated: { icon: Square, color: "text-gray-500" },
|
||||
};
|
||||
|
||||
export function AgentDetailView({ agentId, onClose, addToast }: AgentDetailViewProps) {
|
||||
export function AgentDetailView({ agentId, projectId, onClose, addToast }: AgentDetailViewProps) {
|
||||
const [agent, setAgent] = useState<AgentDetail | null>(null);
|
||||
const [logs, setLogs] = useState<AgentLogEntry[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -79,7 +80,7 @@ export function AgentDetailView({ agentId, onClose, addToast }: AgentDetailViewP
|
||||
const loadAgent = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const data = await fetchAgent(agentId);
|
||||
const data = await fetchAgent(agentId, projectId);
|
||||
setAgent(data);
|
||||
} catch (err: any) {
|
||||
addToast(`Failed to load agent: ${err.message}`, "error");
|
||||
@@ -87,7 +88,7 @@ export function AgentDetailView({ agentId, onClose, addToast }: AgentDetailViewP
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [agentId, addToast, onClose]);
|
||||
}, [agentId, addToast, onClose, projectId]);
|
||||
|
||||
const loadLogs = useCallback(async () => {
|
||||
// Agent logs are tied to tasks, not agents directly.
|
||||
@@ -96,13 +97,13 @@ export function AgentDetailView({ agentId, onClose, addToast }: AgentDetailViewP
|
||||
// If the agent is working on a task, we could show task logs.
|
||||
if (agent?.taskId) {
|
||||
try {
|
||||
const data = await fetchAgentLogs(agent.taskId);
|
||||
const data = await fetchAgentLogs(agent.taskId, projectId);
|
||||
setLogs(data);
|
||||
} catch (err: any) {
|
||||
console.error("Failed to load task logs:", err);
|
||||
}
|
||||
}
|
||||
}, [agent?.taskId]);
|
||||
}, [agent?.taskId, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadAgent();
|
||||
@@ -121,9 +122,10 @@ export function AgentDetailView({ agentId, onClose, addToast }: AgentDetailViewP
|
||||
return;
|
||||
}
|
||||
|
||||
const es = new EventSource(`/api/tasks/${encodeURIComponent(agent.taskId)}/logs/stream`);
|
||||
const query = projectId ? `?projectId=${encodeURIComponent(projectId)}` : "";
|
||||
const es = new EventSource(`/api/tasks/${encodeURIComponent(agent.taskId)}/logs/stream${query}`);
|
||||
|
||||
es.onmessage = (e) => {
|
||||
const handleAgentLog = (e: MessageEvent) => {
|
||||
try {
|
||||
const entry: AgentLogEntry = JSON.parse(e.data);
|
||||
setLogs(prev => [entry, ...prev]);
|
||||
@@ -138,6 +140,8 @@ export function AgentDetailView({ agentId, onClose, addToast }: AgentDetailViewP
|
||||
}
|
||||
};
|
||||
|
||||
es.addEventListener("agent:log", handleAgentLog as EventListener);
|
||||
|
||||
es.onerror = () => {
|
||||
setIsStreaming(false);
|
||||
};
|
||||
@@ -147,14 +151,15 @@ export function AgentDetailView({ agentId, onClose, addToast }: AgentDetailViewP
|
||||
};
|
||||
|
||||
return () => {
|
||||
es.removeEventListener("agent:log", handleAgentLog as EventListener);
|
||||
es.close();
|
||||
setIsStreaming(false);
|
||||
};
|
||||
}, [agent?.taskId, activeTab]);
|
||||
}, [agent?.taskId, activeTab, projectId]);
|
||||
|
||||
const handleStateChange = async (newState: AgentState) => {
|
||||
try {
|
||||
await updateAgentState(agentId, newState);
|
||||
await updateAgentState(agentId, newState, projectId);
|
||||
addToast(`Agent state updated to ${newState}`, "success");
|
||||
void loadAgent();
|
||||
} catch (err: any) {
|
||||
@@ -165,7 +170,7 @@ export function AgentDetailView({ agentId, onClose, addToast }: AgentDetailViewP
|
||||
const handleDelete = async () => {
|
||||
if (!agent || !confirm(`Delete agent "${agent.name}"? This cannot be undone.`)) return;
|
||||
try {
|
||||
await deleteAgent(agentId);
|
||||
await deleteAgent(agentId, projectId);
|
||||
addToast(`Agent "${agent.name}" deleted`, "success");
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -8,6 +8,7 @@ interface AgentListModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
addToast: (message: string, type?: "success" | "error") => void;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
const AGENT_ROLES: { value: AgentCapability; label: string; icon: string }[] = [
|
||||
@@ -26,7 +27,7 @@ const STATE_COLORS: Record<AgentState, { bg: string; text: string; border: strin
|
||||
terminated: { bg: "var(--state-error-bg)", text: "var(--state-error-text)", border: "var(--state-error-border)" },
|
||||
};
|
||||
|
||||
export function AgentListModal({ isOpen, onClose, addToast }: AgentListModalProps) {
|
||||
export function AgentListModal({ isOpen, onClose, addToast, projectId }: AgentListModalProps) {
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
@@ -51,14 +52,14 @@ export function AgentListModal({ isOpen, onClose, addToast }: AgentListModalProp
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const filter = filterState !== "all" ? { state: filterState } : undefined;
|
||||
const data = await fetchAgents(filter);
|
||||
const data = await fetchAgents(filter, projectId);
|
||||
setAgents(data);
|
||||
} catch (err: any) {
|
||||
addToast(`Failed to load agents: ${err.message}`, "error");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [filterState, addToast]);
|
||||
}, [filterState, addToast, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
@@ -69,7 +70,7 @@ export function AgentListModal({ isOpen, onClose, addToast }: AgentListModalProp
|
||||
const handleCreate = async () => {
|
||||
if (!newAgentName.trim()) return;
|
||||
try {
|
||||
await createAgent({ name: newAgentName.trim(), role: newAgentRole });
|
||||
await createAgent({ name: newAgentName.trim(), role: newAgentRole }, projectId);
|
||||
addToast(`Agent "${newAgentName}" created`, "success");
|
||||
setNewAgentName("");
|
||||
setIsCreating(false);
|
||||
@@ -81,7 +82,7 @@ export function AgentListModal({ isOpen, onClose, addToast }: AgentListModalProp
|
||||
|
||||
const handleStateChange = async (agentId: string, newState: AgentState) => {
|
||||
try {
|
||||
await updateAgentState(agentId, newState);
|
||||
await updateAgentState(agentId, newState, projectId);
|
||||
addToast(`Agent state updated to ${newState}`, "success");
|
||||
void loadAgents();
|
||||
} catch (err: any) {
|
||||
@@ -92,7 +93,7 @@ export function AgentListModal({ isOpen, onClose, addToast }: AgentListModalProp
|
||||
const handleDelete = async (agentId: string, agentName: string) => {
|
||||
if (!confirm(`Delete agent "${agentName}"? This cannot be undone.`)) return;
|
||||
try {
|
||||
await deleteAgent(agentId);
|
||||
await deleteAgent(agentId, projectId);
|
||||
addToast(`Agent "${agentName}" deleted`, "success");
|
||||
void loadAgents();
|
||||
} catch (err: any) {
|
||||
@@ -111,7 +112,7 @@ export function AgentListModal({ isOpen, onClose, addToast }: AgentListModalProp
|
||||
}
|
||||
|
||||
try {
|
||||
await updateAgent(agentId, { role: newRole });
|
||||
await updateAgent(agentId, { role: newRole }, projectId);
|
||||
addToast(`Agent role updated to ${AGENT_ROLES.find(r => r.value === newRole)?.label ?? newRole}`, "success");
|
||||
setEditingRoleForAgent(null);
|
||||
void loadAgents();
|
||||
@@ -734,4 +735,4 @@ export function AgentListModal({ isOpen, onClose, addToast }: AgentListModalProp
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { AgentDetailView } from "./AgentDetailView";
|
||||
|
||||
export interface AgentsViewProps {
|
||||
addToast: (message: string, type?: "success" | "error") => void;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
const AGENT_ROLES: { value: AgentCapability; label: string; icon: string }[] = [
|
||||
@@ -25,7 +26,7 @@ const STATE_COLORS: Record<AgentState, { bg: string; text: string; border: strin
|
||||
terminated: { bg: "var(--state-error-bg)", text: "var(--state-error-text)", border: "var(--state-error-border)" },
|
||||
};
|
||||
|
||||
export function AgentsView({ addToast }: AgentsViewProps) {
|
||||
export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
const [agents, setAgents] = useState<Agent[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
@@ -51,14 +52,14 @@ export function AgentsView({ addToast }: AgentsViewProps) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const filter = filterState !== "all" ? { state: filterState } : undefined;
|
||||
const data = await fetchAgents(filter);
|
||||
const data = await fetchAgents(filter, projectId);
|
||||
setAgents(data);
|
||||
} catch (err: any) {
|
||||
addToast(`Failed to load agents: ${err.message}`, "error");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [filterState, addToast]);
|
||||
}, [filterState, addToast, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadAgents();
|
||||
@@ -67,7 +68,7 @@ export function AgentsView({ addToast }: AgentsViewProps) {
|
||||
const handleCreate = async () => {
|
||||
if (!newAgentName.trim()) return;
|
||||
try {
|
||||
await createAgent({ name: newAgentName.trim(), role: newAgentRole });
|
||||
await createAgent({ name: newAgentName.trim(), role: newAgentRole }, projectId);
|
||||
addToast(`Agent "${newAgentName}" created`, "success");
|
||||
setNewAgentName("");
|
||||
setIsCreating(false);
|
||||
@@ -79,7 +80,7 @@ export function AgentsView({ addToast }: AgentsViewProps) {
|
||||
|
||||
const handleStateChange = async (agentId: string, newState: AgentState) => {
|
||||
try {
|
||||
await updateAgentState(agentId, newState);
|
||||
await updateAgentState(agentId, newState, projectId);
|
||||
addToast(`Agent state updated to ${newState}`, "success");
|
||||
void loadAgents();
|
||||
} catch (err: any) {
|
||||
@@ -90,7 +91,7 @@ export function AgentsView({ addToast }: AgentsViewProps) {
|
||||
const handleDelete = async (agentId: string, agentName: string) => {
|
||||
if (!confirm(`Delete agent "${agentName}"? This cannot be undone.`)) return;
|
||||
try {
|
||||
await deleteAgent(agentId);
|
||||
await deleteAgent(agentId, projectId);
|
||||
addToast(`Agent "${agentName}" deleted`, "success");
|
||||
void loadAgents();
|
||||
} catch (err: any) {
|
||||
@@ -109,7 +110,7 @@ export function AgentsView({ addToast }: AgentsViewProps) {
|
||||
}
|
||||
|
||||
try {
|
||||
await updateAgent(agentId, { role: newRole });
|
||||
await updateAgent(agentId, { role: newRole }, projectId);
|
||||
addToast(`Agent role updated to ${AGENT_ROLES.find(r => r.value === newRole)?.label ?? newRole}`, "success");
|
||||
setEditingRoleForAgent(null);
|
||||
void loadAgents();
|
||||
@@ -499,6 +500,7 @@ export function AgentsView({ addToast }: AgentsViewProps) {
|
||||
{selectedAgentId && (
|
||||
<AgentDetailView
|
||||
agentId={selectedAgentId}
|
||||
projectId={projectId}
|
||||
onClose={() => setSelectedAgentId(null)}
|
||||
addToast={addToast}
|
||||
/>
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { ModelInfo } from "../api";
|
||||
|
||||
interface BoardProps {
|
||||
tasks: Task[];
|
||||
projectId?: string;
|
||||
maxConcurrent: number;
|
||||
onMoveTask: (id: string, column: ColumnType) => Promise<Task>;
|
||||
onOpenDetail: (task: TaskDetail) => void;
|
||||
@@ -53,9 +54,9 @@ function areTaskArraysEqual(previous: Task[], next: Task[]): boolean {
|
||||
return previous.every((task, index) => task === next[index]);
|
||||
}
|
||||
|
||||
export function Board({ tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, onArchiveAllDone, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenFilesForTask }: BoardProps) {
|
||||
export function Board({ tasks, projectId, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, onArchiveAllDone, searchQuery = "", availableModels, onPlanningMode, onSubtaskBreakdown, onOpenFilesForTask }: BoardProps) {
|
||||
const [archivedCollapsed, setArchivedCollapsed] = useState(true);
|
||||
const { fetchBatch } = useBatchBadgeFetch();
|
||||
const { fetchBatch } = useBatchBadgeFetch(projectId);
|
||||
const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const tasksByColumnCacheRef = useRef<Record<ColumnType, Task[]>>({
|
||||
triage: [],
|
||||
@@ -151,6 +152,7 @@ export function Board({ tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast
|
||||
key={col}
|
||||
column={col}
|
||||
tasks={tasksByColumn[col]}
|
||||
projectId={projectId}
|
||||
maxConcurrent={maxConcurrent}
|
||||
onMoveTask={onMoveTask}
|
||||
onOpenDetail={onOpenDetail}
|
||||
|
||||
@@ -8,6 +8,7 @@ interface ChangedFilesModalProps {
|
||||
taskId: string;
|
||||
worktree: string | undefined;
|
||||
column: string;
|
||||
projectId?: string;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
@@ -44,8 +45,8 @@ function getDiffStat(diff: string): string {
|
||||
return statLines.join("\n").trim();
|
||||
}
|
||||
|
||||
export function ChangedFilesModal({ taskId, worktree, column, isOpen, onClose }: ChangedFilesModalProps) {
|
||||
const { files, loading, error, selectedFile, setSelectedFile } = useChangedFiles(taskId, worktree, column);
|
||||
export function ChangedFilesModal({ taskId, worktree, column, projectId, isOpen, onClose }: ChangedFilesModalProps) {
|
||||
const { files, loading, error, selectedFile, setSelectedFile } = useChangedFiles(taskId, worktree, column, projectId);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
@@ -17,6 +17,7 @@ const VISIBLE_TASKS_INCREMENT = 25;
|
||||
interface ColumnProps {
|
||||
column: ColumnType;
|
||||
tasks: Task[];
|
||||
projectId?: string;
|
||||
maxConcurrent: number;
|
||||
onMoveTask: (id: string, column: ColumnType) => Promise<Task>;
|
||||
onOpenDetail: (task: TaskDetail) => void;
|
||||
@@ -48,7 +49,7 @@ interface ColumnProps {
|
||||
onOpenFilesForTask?: (taskId: string, worktree: string | undefined, column: string) => void;
|
||||
}
|
||||
|
||||
function ColumnComponent({ column, tasks, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, onArchiveAllDone, collapsed, onToggleCollapse, allTasks, availableModels, onPlanningMode, onSubtaskBreakdown, onOpenFilesForTask }: ColumnProps) {
|
||||
function ColumnComponent({ column, tasks, projectId, maxConcurrent, onMoveTask, onOpenDetail, addToast, onQuickCreate, onNewTask, autoMerge, onToggleAutoMerge, globalPaused, onUpdateTask, onArchiveTask, onUnarchiveTask, onArchiveAllDone, collapsed, onToggleCollapse, allTasks, availableModels, onPlanningMode, onSubtaskBreakdown, onOpenFilesForTask }: ColumnProps) {
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
const [visibleTaskCount, setVisibleTaskCount] = useState(VISIBLE_TASKS_INITIAL);
|
||||
const countFlashing = useFlashOnIncrease(tasks.length);
|
||||
@@ -207,6 +208,7 @@ function ColumnComponent({ column, tasks, maxConcurrent, onMoveTask, onOpenDetai
|
||||
label={group.label}
|
||||
activeTasks={group.activeTasks}
|
||||
queuedTasks={group.queuedTasks}
|
||||
projectId={projectId}
|
||||
onOpenDetail={onOpenDetail}
|
||||
addToast={addToast}
|
||||
globalPaused={globalPaused}
|
||||
@@ -223,6 +225,7 @@ function ColumnComponent({ column, tasks, maxConcurrent, onMoveTask, onOpenDetai
|
||||
<TaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
onOpenDetail={onOpenDetail}
|
||||
addToast={addToast}
|
||||
globalPaused={globalPaused}
|
||||
|
||||
@@ -49,6 +49,7 @@ export interface HeaderProps {
|
||||
currentProject?: ProjectInfo | null;
|
||||
onSelectProject?: (project: ProjectInfo) => void;
|
||||
onViewAllProjects?: () => void;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
function useIsMobile() {
|
||||
@@ -96,6 +97,7 @@ export function Header({
|
||||
currentProject,
|
||||
onSelectProject,
|
||||
onViewAllProjects,
|
||||
projectId,
|
||||
}: HeaderProps) {
|
||||
const isMobile = useIsMobile();
|
||||
const [isMobileSearchOpen, setIsMobileSearchOpen] = useState(false);
|
||||
@@ -416,6 +418,7 @@ export function Header({
|
||||
<QuickScriptsDropdown
|
||||
onOpenScripts={onOpenScripts}
|
||||
onRunScript={onRunScript}
|
||||
projectId={projectId}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ interface InlineCreateCardProps {
|
||||
onSubmit: (input: TaskCreateInput) => Promise<Task>;
|
||||
onCancel: () => void;
|
||||
addToast: (msg: string, type?: ToastType) => void;
|
||||
projectId?: string;
|
||||
/**
|
||||
* Optional model list from a parent surface. When omitted, InlineCreateCard
|
||||
* fetches models itself so it can stay reusable in both list and board flows
|
||||
@@ -61,6 +62,7 @@ export function InlineCreateCard({
|
||||
onSubmit,
|
||||
onCancel,
|
||||
addToast,
|
||||
projectId,
|
||||
availableModels,
|
||||
onPlanningMode,
|
||||
onSubtaskBreakdown,
|
||||
@@ -160,7 +162,7 @@ export function InlineCreateCard({
|
||||
}
|
||||
});
|
||||
|
||||
fetchSettings()
|
||||
fetchSettings(projectId)
|
||||
.then((nextSettings) => {
|
||||
if (!cancelled) {
|
||||
setSettings(nextSettings);
|
||||
@@ -175,7 +177,7 @@ export function InlineCreateCard({
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [availableModels]);
|
||||
}, [availableModels, projectId]);
|
||||
|
||||
const executorSelectionValue = getModelSelectionValue(executorProvider, executorModelId);
|
||||
const validatorSelectionValue = getModelSelectionValue(validatorProvider, validatorModelId);
|
||||
@@ -293,7 +295,7 @@ export function InlineCreateCard({
|
||||
const failures: string[] = [];
|
||||
for (const img of pendingImages) {
|
||||
try {
|
||||
await uploadAttachment(task.id, img.file);
|
||||
await uploadAttachment(task.id, img.file, projectId);
|
||||
} catch {
|
||||
failures.push(img.file.name);
|
||||
}
|
||||
|
||||
@@ -474,6 +474,7 @@ export function ListView({
|
||||
payload.modelId,
|
||||
payload.validatorModelProvider,
|
||||
payload.validatorModelId,
|
||||
projectId,
|
||||
);
|
||||
|
||||
// Optimistically update parent with returned tasks
|
||||
@@ -492,18 +493,18 @@ export function ListView({
|
||||
} finally {
|
||||
setIsApplying(false);
|
||||
}
|
||||
}, [selectedTaskIds, tasks, executorModel, validatorModel, addToast, clearSelection, onTasksUpdated]);
|
||||
}, [selectedTaskIds, tasks, executorModel, validatorModel, projectId, addToast, clearSelection, onTasksUpdated]);
|
||||
|
||||
const handleRowClick = useCallback(
|
||||
async (task: Task) => {
|
||||
try {
|
||||
const detail = await fetchTaskDetail(task.id);
|
||||
const detail = await fetchTaskDetail(task.id, projectId);
|
||||
onOpenDetail(detail);
|
||||
} catch (err: any) {
|
||||
addToast("Failed to load task details", "error");
|
||||
}
|
||||
},
|
||||
[onOpenDetail, addToast]
|
||||
[onOpenDetail, addToast, projectId]
|
||||
);
|
||||
|
||||
const handleDragStart = useCallback(
|
||||
|
||||
@@ -55,6 +55,7 @@ interface MissionManagerProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
projectId?: string;
|
||||
onSelectTask?: (taskId: string) => void;
|
||||
availableTasks?: Array<{ id: string; title?: string }>;
|
||||
}
|
||||
@@ -120,7 +121,7 @@ const EMPTY_MISSION_FORM: MissionFormData = {
|
||||
title: "",
|
||||
description: "",
|
||||
status: "planning",
|
||||
autoAdvance: true,
|
||||
autoAdvance: false,
|
||||
};
|
||||
|
||||
const EMPTY_MILESTONE_FORM: MilestoneFormData = {
|
||||
@@ -143,7 +144,7 @@ const EMPTY_FEATURE_FORM: FeatureFormData = {
|
||||
status: "defined",
|
||||
};
|
||||
|
||||
export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availableTasks = [] }: MissionManagerProps) {
|
||||
export function MissionManager({ isOpen, onClose, addToast, projectId, onSelectTask, availableTasks = [] }: MissionManagerProps) {
|
||||
const [missions, setMissions] = useState<Mission[]>([]);
|
||||
const [selectedMission, setSelectedMission] = useState<MissionWithHierarchy | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -183,19 +184,19 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
const loadMissions = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await fetchMissions();
|
||||
const data = await fetchMissions(projectId);
|
||||
setMissions(data);
|
||||
} catch (err: any) {
|
||||
addToast(err.message || "Failed to load missions", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [addToast]);
|
||||
}, [addToast, projectId]);
|
||||
|
||||
const loadMissionDetail = useCallback(async (missionId: string) => {
|
||||
try {
|
||||
setDetailLoading(true);
|
||||
const data = await fetchMission(missionId);
|
||||
const data = await fetchMission(missionId, projectId);
|
||||
setSelectedMission(data);
|
||||
// Auto-expand first milestone and slice
|
||||
if (data.milestones.length > 0) {
|
||||
@@ -209,7 +210,7 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
}, [addToast]);
|
||||
}, [addToast, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
@@ -232,7 +233,7 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
title: mission.title,
|
||||
description: mission.description || "",
|
||||
status: mission.status,
|
||||
autoAdvance: mission.autoAdvance ?? true,
|
||||
autoAdvance: mission.autoAdvance ?? false,
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -254,7 +255,7 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
await createMission({
|
||||
title: missionForm.title.trim(),
|
||||
description: missionForm.description.trim() || undefined,
|
||||
});
|
||||
}, projectId);
|
||||
addToast("Mission created", "success");
|
||||
} else if (editingMissionId) {
|
||||
await updateMission(editingMissionId, {
|
||||
@@ -262,7 +263,7 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
description: missionForm.description.trim() || undefined,
|
||||
status: missionForm.status,
|
||||
autoAdvance: missionForm.autoAdvance,
|
||||
});
|
||||
}, projectId);
|
||||
addToast("Mission updated", "success");
|
||||
// Refresh detail view if viewing this mission
|
||||
if (selectedMission?.id === editingMissionId) {
|
||||
@@ -276,11 +277,11 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [missionForm, isCreatingMission, editingMissionId, addToast, loadMissions, loadMissionDetail, selectedMission, handleCancelMission]);
|
||||
}, [missionForm, isCreatingMission, editingMissionId, addToast, loadMissions, loadMissionDetail, selectedMission, handleCancelMission, projectId]);
|
||||
|
||||
const handleDeleteMission = useCallback(async (missionId: string) => {
|
||||
try {
|
||||
await deleteMission(missionId);
|
||||
await deleteMission(missionId, projectId);
|
||||
addToast("Mission deleted", "success");
|
||||
if (selectedMission?.id === missionId) {
|
||||
setSelectedMission(null);
|
||||
@@ -290,7 +291,7 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
} catch (err: any) {
|
||||
addToast(err.message || "Failed to delete mission", "error");
|
||||
}
|
||||
}, [addToast, loadMissions, selectedMission]);
|
||||
}, [addToast, loadMissions, selectedMission, projectId]);
|
||||
|
||||
// Milestone handlers
|
||||
const handleCreateMilestone = useCallback(() => {
|
||||
@@ -329,7 +330,7 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
title: milestoneForm.title.trim(),
|
||||
description: milestoneForm.description.trim() || undefined,
|
||||
dependencies: milestoneForm.dependencies,
|
||||
});
|
||||
}, projectId);
|
||||
addToast("Milestone created", "success");
|
||||
} else if (editingMilestoneId) {
|
||||
await updateMilestone(editingMilestoneId, {
|
||||
@@ -337,7 +338,7 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
description: milestoneForm.description.trim() || undefined,
|
||||
status: milestoneForm.status,
|
||||
dependencies: milestoneForm.dependencies,
|
||||
});
|
||||
}, projectId);
|
||||
addToast("Milestone updated", "success");
|
||||
}
|
||||
await loadMissionDetail(selectedMission!.id);
|
||||
@@ -347,18 +348,18 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [milestoneForm, isCreatingMilestone, editingMilestoneId, selectedMission, addToast, loadMissionDetail, handleCancelMilestone, missionForm.title]);
|
||||
}, [milestoneForm, isCreatingMilestone, editingMilestoneId, selectedMission, addToast, loadMissionDetail, handleCancelMilestone, missionForm.title, projectId]);
|
||||
|
||||
const handleDeleteMilestone = useCallback(async (milestoneId: string) => {
|
||||
try {
|
||||
await deleteMilestone(milestoneId);
|
||||
await deleteMilestone(milestoneId, projectId);
|
||||
addToast("Milestone deleted", "success");
|
||||
await loadMissionDetail(selectedMission!.id);
|
||||
setDeleteConfirmId(null);
|
||||
} catch (err: any) {
|
||||
addToast(err.message || "Failed to delete milestone", "error");
|
||||
}
|
||||
}, [addToast, loadMissionDetail, selectedMission]);
|
||||
}, [addToast, loadMissionDetail, selectedMission, projectId]);
|
||||
|
||||
const toggleMilestoneExpanded = useCallback((milestoneId: string) => {
|
||||
setExpandedMilestones((prev) => {
|
||||
@@ -409,14 +410,14 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
await createSlice(selectedMilestoneIdForNewSlice, {
|
||||
title: sliceForm.title.trim(),
|
||||
description: sliceForm.description.trim() || undefined,
|
||||
});
|
||||
}, projectId);
|
||||
addToast("Slice created", "success");
|
||||
} else if (editingSliceId) {
|
||||
await updateSlice(editingSliceId, {
|
||||
title: sliceForm.title.trim(),
|
||||
description: sliceForm.description.trim() || undefined,
|
||||
status: sliceForm.status,
|
||||
});
|
||||
}, projectId);
|
||||
addToast("Slice updated", "success");
|
||||
}
|
||||
await loadMissionDetail(selectedMission!.id);
|
||||
@@ -426,28 +427,28 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [sliceForm, isCreatingSlice, editingSliceId, selectedMilestoneIdForNewSlice, selectedMission, addToast, loadMissionDetail, handleCancelSlice]);
|
||||
}, [sliceForm, isCreatingSlice, editingSliceId, selectedMilestoneIdForNewSlice, selectedMission, addToast, loadMissionDetail, handleCancelSlice, projectId]);
|
||||
|
||||
const handleDeleteSlice = useCallback(async (sliceId: string) => {
|
||||
try {
|
||||
await deleteSlice(sliceId);
|
||||
await deleteSlice(sliceId, projectId);
|
||||
addToast("Slice deleted", "success");
|
||||
await loadMissionDetail(selectedMission!.id);
|
||||
setDeleteConfirmId(null);
|
||||
} catch (err: any) {
|
||||
addToast(err.message || "Failed to delete slice", "error");
|
||||
}
|
||||
}, [addToast, loadMissionDetail, selectedMission]);
|
||||
}, [addToast, loadMissionDetail, selectedMission, projectId]);
|
||||
|
||||
const handleActivateSlice = useCallback(async (sliceId: string) => {
|
||||
try {
|
||||
await activateSlice(sliceId);
|
||||
await activateSlice(sliceId, projectId);
|
||||
addToast("Slice activated", "success");
|
||||
await loadMissionDetail(selectedMission!.id);
|
||||
} catch (err: any) {
|
||||
addToast(err.message || "Failed to activate slice", "error");
|
||||
}
|
||||
}, [addToast, loadMissionDetail, selectedMission]);
|
||||
}, [addToast, loadMissionDetail, selectedMission, projectId]);
|
||||
|
||||
const toggleSliceExpanded = useCallback((sliceId: string) => {
|
||||
setExpandedSlices((prev) => {
|
||||
@@ -500,7 +501,7 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
title: featureForm.title.trim(),
|
||||
description: featureForm.description.trim() || undefined,
|
||||
acceptanceCriteria: featureForm.acceptanceCriteria.trim() || undefined,
|
||||
});
|
||||
}, projectId);
|
||||
addToast("Feature created", "success");
|
||||
} else if (editingFeatureId) {
|
||||
await updateFeature(editingFeatureId, {
|
||||
@@ -508,7 +509,7 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
description: featureForm.description.trim() || undefined,
|
||||
acceptanceCriteria: featureForm.acceptanceCriteria.trim() || undefined,
|
||||
status: featureForm.status,
|
||||
});
|
||||
}, projectId);
|
||||
addToast("Feature updated", "success");
|
||||
}
|
||||
await loadMissionDetail(selectedMission!.id);
|
||||
@@ -518,18 +519,18 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [featureForm, isCreatingFeature, editingFeatureId, selectedSliceIdForNewFeature, selectedMission, addToast, loadMissionDetail, handleCancelFeature]);
|
||||
}, [featureForm, isCreatingFeature, editingFeatureId, selectedSliceIdForNewFeature, selectedMission, addToast, loadMissionDetail, handleCancelFeature, projectId]);
|
||||
|
||||
const handleDeleteFeature = useCallback(async (featureId: string) => {
|
||||
try {
|
||||
await deleteFeature(featureId);
|
||||
await deleteFeature(featureId, projectId);
|
||||
addToast("Feature deleted", "success");
|
||||
await loadMissionDetail(selectedMission!.id);
|
||||
setDeleteConfirmId(null);
|
||||
} catch (err: any) {
|
||||
addToast(err.message || "Failed to delete feature", "error");
|
||||
}
|
||||
}, [addToast, loadMissionDetail, selectedMission]);
|
||||
}, [addToast, loadMissionDetail, selectedMission, projectId]);
|
||||
|
||||
const handleLinkTask = useCallback(async () => {
|
||||
if (!linkTaskFeatureId || !selectedTaskId.trim()) {
|
||||
@@ -538,7 +539,7 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
}
|
||||
|
||||
try {
|
||||
await linkFeatureToTask(linkTaskFeatureId, selectedTaskId.trim());
|
||||
await linkFeatureToTask(linkTaskFeatureId, selectedTaskId.trim(), projectId);
|
||||
addToast("Feature linked to task", "success");
|
||||
await loadMissionDetail(selectedMission!.id);
|
||||
setLinkTaskFeatureId(null);
|
||||
@@ -546,17 +547,17 @@ export function MissionManager({ isOpen, onClose, addToast, onSelectTask, availa
|
||||
} catch (err: any) {
|
||||
addToast(err.message || "Failed to link feature to task", "error");
|
||||
}
|
||||
}, [linkTaskFeatureId, selectedTaskId, addToast, loadMissionDetail, selectedMission]);
|
||||
}, [linkTaskFeatureId, selectedTaskId, addToast, loadMissionDetail, selectedMission, projectId]);
|
||||
|
||||
const handleUnlinkTask = useCallback(async (featureId: string) => {
|
||||
try {
|
||||
await unlinkFeatureFromTask(featureId);
|
||||
await unlinkFeatureFromTask(featureId, projectId);
|
||||
addToast("Feature unlinked from task", "success");
|
||||
await loadMissionDetail(selectedMission!.id);
|
||||
} catch (err: any) {
|
||||
addToast(err.message || "Failed to unlink feature", "error");
|
||||
}
|
||||
}, [addToast, loadMissionDetail, selectedMission]);
|
||||
}, [addToast, loadMissionDetail, selectedMission, projectId]);
|
||||
|
||||
const handleSelectMission = useCallback((mission: Mission) => {
|
||||
loadMissionDetail(mission.id);
|
||||
|
||||
@@ -275,7 +275,6 @@ export function ModelSelectorTab({ task, addToast }: ModelSelectorTabProps) {
|
||||
.catch((err) => setModelsError(err.message))
|
||||
.finally(() => setModelsLoading(false));
|
||||
}}
|
||||
className="btn btn-sm"
|
||||
style={{ marginLeft: "8px" }}
|
||||
>
|
||||
Retry
|
||||
|
||||
@@ -17,6 +17,7 @@ interface PendingImage {
|
||||
interface NewTaskModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
projectId?: string;
|
||||
tasks: Task[]; // for dependency selection
|
||||
onCreateTask: (input: TaskCreateInput) => Promise<Task>;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
@@ -24,7 +25,7 @@ interface NewTaskModalProps {
|
||||
onSubtaskBreakdown?: (description: string) => void;
|
||||
}
|
||||
|
||||
export function NewTaskModal({ isOpen, onClose, tasks, onCreateTask, addToast, onPlanningMode, onSubtaskBreakdown }: NewTaskModalProps) {
|
||||
export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask, addToast, onPlanningMode, onSubtaskBreakdown }: NewTaskModalProps) {
|
||||
const [description, setDescription] = useState("");
|
||||
const [dependencies, setDependencies] = useState<string[]>([]);
|
||||
const [showDepDropdown, setShowDepDropdown] = useState(false);
|
||||
@@ -63,14 +64,14 @@ export function NewTaskModal({ isOpen, onClose, tasks, onCreateTask, addToast, o
|
||||
})
|
||||
.catch(() => {/* silently fail - models just won't be available */})
|
||||
.finally(() => setModelsLoading(false));
|
||||
fetchSettings()
|
||||
fetchSettings(projectId)
|
||||
.then((nextSettings) => setSettings(nextSettings))
|
||||
.catch(() => setSettings(null));
|
||||
fetchWorkflowSteps()
|
||||
fetchWorkflowSteps(projectId)
|
||||
.then((steps) => setWorkflowSteps(steps.filter((s) => s.enabled)))
|
||||
.catch(() => setWorkflowSteps([]));
|
||||
}
|
||||
}, [isOpen]);
|
||||
}, [isOpen, projectId]);
|
||||
|
||||
// Track dirty state
|
||||
useEffect(() => {
|
||||
@@ -237,7 +238,7 @@ export function NewTaskModal({ isOpen, onClose, tasks, onCreateTask, addToast, o
|
||||
const failures: string[] = [];
|
||||
for (const img of pendingImages) {
|
||||
try {
|
||||
await uploadAttachment(task.id, img.file);
|
||||
await uploadAttachment(task.id, img.file, projectId);
|
||||
} catch {
|
||||
failures.push(img.file.name);
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ describe("PlanningModeModal", () => {
|
||||
mockStartPlanningStreaming.mockResolvedValue({ sessionId: "session-123" });
|
||||
|
||||
// Default: simulate receiving a question after a brief delay
|
||||
mockConnectPlanningStream.mockImplementation((sessionId: string, handlers: any) => {
|
||||
mockConnectPlanningStream.mockImplementation((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
setTimeout(() => {
|
||||
handlers.onQuestion?.(mockQuestion);
|
||||
}, 10);
|
||||
@@ -191,7 +191,7 @@ describe("PlanningModeModal", () => {
|
||||
|
||||
// Wait for startPlanningStreaming to be called (allow time for setTimeout in useEffect)
|
||||
await waitFor(() => {
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build a login system from new task dialog");
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build a login system from new task dialog", undefined);
|
||||
}, { timeout: 2000 });
|
||||
|
||||
// Should transition to question view
|
||||
@@ -213,7 +213,7 @@ describe("PlanningModeModal", () => {
|
||||
|
||||
// The auto-start should happen with the initial plan (allow time for setTimeout in useEffect)
|
||||
await waitFor(() => {
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Pre-filled plan from new task");
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Pre-filled plan from new task", undefined);
|
||||
}, { timeout: 2000 });
|
||||
});
|
||||
});
|
||||
@@ -236,7 +236,7 @@ describe("PlanningModeModal", () => {
|
||||
|
||||
// Wait for streaming to be called
|
||||
await waitFor(() => {
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build auth system");
|
||||
expect(mockStartPlanningStreaming).toHaveBeenCalledWith("Build auth system", undefined);
|
||||
});
|
||||
|
||||
// Should transition to question view via streaming
|
||||
@@ -247,7 +247,7 @@ describe("PlanningModeModal", () => {
|
||||
|
||||
it("shows error message when planning fails", async () => {
|
||||
// Override the default mock to simulate an error
|
||||
mockConnectPlanningStream.mockImplementationOnce((sessionId: string, handlers: any) => {
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
setTimeout(() => {
|
||||
handlers.onError?.("Rate limit exceeded");
|
||||
}, 10);
|
||||
@@ -315,7 +315,7 @@ describe("PlanningModeModal", () => {
|
||||
let streamConnectionCount = 0;
|
||||
let streamHandlers: any = null;
|
||||
|
||||
mockConnectPlanningStream.mockImplementation((sessionId: string, handlers: any) => {
|
||||
mockConnectPlanningStream.mockImplementation((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
streamConnectionCount++;
|
||||
streamHandlers = handlers;
|
||||
|
||||
@@ -379,7 +379,7 @@ describe("PlanningModeModal", () => {
|
||||
describe("Summary view", () => {
|
||||
it("shows summary when planning is complete", async () => {
|
||||
// Override mock to return summary instead of question
|
||||
mockConnectPlanningStream.mockImplementationOnce((sessionId: string, handlers: any) => {
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
setTimeout(() => {
|
||||
handlers.onSummary?.(mockSummary);
|
||||
}, 10);
|
||||
@@ -427,7 +427,7 @@ describe("PlanningModeModal", () => {
|
||||
};
|
||||
|
||||
// Override mock to return summary
|
||||
mockConnectPlanningStream.mockImplementationOnce((sessionId: string, handlers: any) => {
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
setTimeout(() => {
|
||||
handlers.onSummary?.(mockSummary);
|
||||
}, 10);
|
||||
@@ -460,7 +460,7 @@ describe("PlanningModeModal", () => {
|
||||
fireEvent.click(screen.getByText("Create Task"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateTaskFromPlanning).toHaveBeenCalledWith("session-123");
|
||||
expect(mockCreateTaskFromPlanning).toHaveBeenCalledWith("session-123", undefined);
|
||||
expect(mockOnTaskCreated).toHaveBeenCalledWith(createdTask);
|
||||
});
|
||||
});
|
||||
@@ -495,7 +495,7 @@ describe("PlanningModeModal", () => {
|
||||
describe("Loading state", () => {
|
||||
it("shows 'Generating next question...' text when loading without streaming content", async () => {
|
||||
// Mock to delay the question response so we stay in loading state
|
||||
mockConnectPlanningStream.mockImplementationOnce((sessionId: string, handlers: any) => {
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
// Don't call any handlers - stay in loading state
|
||||
return {
|
||||
close: vi.fn(),
|
||||
@@ -528,7 +528,7 @@ describe("PlanningModeModal", () => {
|
||||
|
||||
it("shows thinking container even when streaming output is initially empty", async () => {
|
||||
// Mock to delay the question response so we stay in loading state
|
||||
mockConnectPlanningStream.mockImplementationOnce((sessionId: string, handlers: any) => {
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
// Don't call any handlers - stay in loading state
|
||||
return {
|
||||
close: vi.fn(),
|
||||
@@ -563,7 +563,7 @@ describe("PlanningModeModal", () => {
|
||||
it("shows 'AI is thinking...' text and renders streaming content when it arrives", async () => {
|
||||
let streamHandlers: any = null;
|
||||
|
||||
mockConnectPlanningStream.mockImplementationOnce((sessionId: string, handlers: any) => {
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
streamHandlers = handlers;
|
||||
return {
|
||||
close: vi.fn(),
|
||||
@@ -624,7 +624,7 @@ describe("PlanningModeModal", () => {
|
||||
|
||||
let streamHandlers: any = null;
|
||||
|
||||
mockConnectPlanningStream.mockImplementation((sessionId: string, handlers: any) => {
|
||||
mockConnectPlanningStream.mockImplementation((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
streamHandlers = handlers;
|
||||
|
||||
setTimeout(() => {
|
||||
@@ -815,7 +815,7 @@ describe("PlanningModeModal", () => {
|
||||
|
||||
it("shows confirmation in summary view", async () => {
|
||||
// Override mock to return summary
|
||||
mockConnectPlanningStream.mockImplementationOnce((sessionId: string, handlers: any) => {
|
||||
mockConnectPlanningStream.mockImplementationOnce((_sessionId: string, _projectId: string | undefined, handlers: any) => {
|
||||
setTimeout(() => {
|
||||
handlers.onSummary?.(mockSummary);
|
||||
}, 10);
|
||||
|
||||
@@ -17,6 +17,7 @@ interface PlanningModeModalProps {
|
||||
onTaskCreated: (task: Task) => void;
|
||||
tasks: Task[];
|
||||
initialPlan?: string;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
interface QuestionResponse {
|
||||
@@ -36,7 +37,7 @@ const EXAMPLE_PLANS = [
|
||||
"Refactor the task card component for better performance",
|
||||
];
|
||||
|
||||
export function PlanningModeModal({ isOpen, onClose, onTaskCreated, tasks, initialPlan: initialPlanProp }: PlanningModeModalProps) {
|
||||
export function PlanningModeModal({ isOpen, onClose, onTaskCreated, tasks, initialPlan: initialPlanProp, projectId }: PlanningModeModalProps) {
|
||||
const [initialPlan, setInitialPlan] = useState("");
|
||||
const [view, setView] = useState<ViewState>({ type: "initial" });
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -64,11 +65,11 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, tasks, initi
|
||||
|
||||
try {
|
||||
// Use streaming mode for real-time AI thinking display
|
||||
const { sessionId } = await startPlanningStreaming(plan.trim());
|
||||
const { sessionId } = await startPlanningStreaming(plan.trim(), projectId);
|
||||
currentSessionIdRef.current = sessionId;
|
||||
|
||||
// Connect to SSE stream
|
||||
const connection = connectPlanningStream(sessionId, {
|
||||
const connection = connectPlanningStream(sessionId, projectId, {
|
||||
onThinking: (data) => {
|
||||
setStreamingOutput((prev) => prev + data);
|
||||
},
|
||||
@@ -108,7 +109,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, tasks, initi
|
||||
setView({ type: "initial" });
|
||||
currentSessionIdRef.current = null;
|
||||
}
|
||||
}, [initialPlan]);
|
||||
}, [initialPlan, projectId]);
|
||||
|
||||
// Focus textarea when opening
|
||||
useEffect(() => {
|
||||
@@ -177,7 +178,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, tasks, initi
|
||||
|
||||
if (view.type === "question" || view.type === "summary") {
|
||||
try {
|
||||
await cancelPlanning(view.session.sessionId);
|
||||
await cancelPlanning(view.session.sessionId, projectId);
|
||||
} catch {
|
||||
// Ignore errors on cancel
|
||||
}
|
||||
@@ -232,7 +233,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, tasks, initi
|
||||
|
||||
try {
|
||||
// Submit response - AI will broadcast events via the already-connected stream
|
||||
await respondToPlanning(sessionId, responses);
|
||||
await respondToPlanning(sessionId, responses, projectId);
|
||||
setResponseHistory((prev) => [...prev, responses]);
|
||||
setHasProgress(true);
|
||||
// Events (question/summary) will arrive via the existing SSE stream
|
||||
@@ -251,7 +252,7 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, tasks, initi
|
||||
setView({ type: "loading" });
|
||||
|
||||
try {
|
||||
const task = await createTaskFromPlanning(view.session.sessionId);
|
||||
const task = await createTaskFromPlanning(view.session.sessionId, projectId);
|
||||
onTaskCreated(task);
|
||||
handleCancel();
|
||||
} catch (err: any) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ToastType } from "../hooks/useToast";
|
||||
|
||||
interface PrSectionProps {
|
||||
taskId: string;
|
||||
projectId?: string;
|
||||
prInfo?: PrInfo;
|
||||
automationStatus?: string | null;
|
||||
hasGitHubToken: boolean;
|
||||
@@ -22,6 +23,7 @@ const STATUS_COLORS = {
|
||||
|
||||
export function PrSection({
|
||||
taskId,
|
||||
projectId,
|
||||
prInfo,
|
||||
automationStatus,
|
||||
hasGitHubToken,
|
||||
@@ -44,7 +46,7 @@ export function PrSection({
|
||||
const newPr = await createPr(taskId, {
|
||||
title: prTitle.trim(),
|
||||
body: prBody.trim() || undefined,
|
||||
});
|
||||
}, projectId);
|
||||
onPrCreated(newPr);
|
||||
setShowCreateForm(false);
|
||||
setPrTitle("");
|
||||
@@ -55,14 +57,14 @@ export function PrSection({
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}, [taskId, prTitle, prBody, onPrCreated, addToast]);
|
||||
}, [taskId, prTitle, prBody, projectId, onPrCreated, addToast]);
|
||||
|
||||
const handleRefresh = useCallback(async () => {
|
||||
if (!prInfo) return;
|
||||
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
const updated = await refreshPrStatus(taskId);
|
||||
const updated = await refreshPrStatus(taskId, projectId);
|
||||
setRefreshState(updated);
|
||||
onPrUpdated(updated.prInfo);
|
||||
addToast("PR status refreshed", "success");
|
||||
@@ -71,7 +73,7 @@ export function PrSection({
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
}, [taskId, prInfo, onPrUpdated, addToast]);
|
||||
}, [taskId, prInfo, projectId, onPrUpdated, addToast]);
|
||||
|
||||
// No PR yet - show create button or automation state
|
||||
if (!prInfo) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { fetchScripts } from "../api";
|
||||
export interface QuickScriptsDropdownProps {
|
||||
onOpenScripts: () => void;
|
||||
onRunScript: (name: string, command: string) => void;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,6 +24,7 @@ export interface QuickScriptsDropdownProps {
|
||||
export function QuickScriptsDropdown({
|
||||
onOpenScripts,
|
||||
onRunScript,
|
||||
projectId,
|
||||
}: QuickScriptsDropdownProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [scripts, setScripts] = useState<Record<string, string>>({});
|
||||
@@ -49,7 +51,7 @@ export function QuickScriptsDropdown({
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
|
||||
fetchScripts()
|
||||
fetchScripts(projectId)
|
||||
.then((data) => {
|
||||
if (!cancelled) {
|
||||
setScripts(data);
|
||||
@@ -69,7 +71,7 @@ export function QuickScriptsDropdown({
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isOpen]);
|
||||
}, [isOpen, projectId]);
|
||||
|
||||
// Close dropdown on outside click
|
||||
useEffect(() => {
|
||||
|
||||
@@ -14,6 +14,7 @@ interface ScriptsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
projectId?: string;
|
||||
/** Callback when user wants to run a script - opens terminal modal */
|
||||
onRunScript?: (name: string, command: string) => void;
|
||||
}
|
||||
@@ -39,7 +40,7 @@ function truncateCommand(command: string, maxLength: number = 60): string {
|
||||
return command.slice(0, maxLength - 3) + "...";
|
||||
}
|
||||
|
||||
export function ScriptsModal({ isOpen, onClose, addToast, onRunScript }: ScriptsModalProps) {
|
||||
export function ScriptsModal({ isOpen, onClose, addToast, projectId, onRunScript }: ScriptsModalProps) {
|
||||
const [scripts, setScripts] = useState<Record<string, string>>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
@@ -52,14 +53,14 @@ export function ScriptsModal({ isOpen, onClose, addToast, onRunScript }: Scripts
|
||||
const loadScripts = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await fetchScripts();
|
||||
const data = await fetchScripts(projectId);
|
||||
setScripts(data);
|
||||
} catch (err: any) {
|
||||
addToast(err.message || "Failed to load scripts", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [addToast]);
|
||||
}, [addToast, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
@@ -118,7 +119,7 @@ export function ScriptsModal({ isOpen, onClose, addToast, onRunScript }: Scripts
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
await addScript(trimmedName, trimmedCommand);
|
||||
await addScript(trimmedName, trimmedCommand, projectId);
|
||||
addToast(isEditing ? "Script updated" : "Script created", "success");
|
||||
setIsEditing(null);
|
||||
setIsCreating(false);
|
||||
@@ -134,11 +135,11 @@ export function ScriptsModal({ isOpen, onClose, addToast, onRunScript }: Scripts
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}, [form, isEditing, addToast, loadScripts]);
|
||||
}, [form, isEditing, addToast, loadScripts, projectId]);
|
||||
|
||||
const handleDelete = useCallback(async (name: string) => {
|
||||
try {
|
||||
await removeScript(name);
|
||||
await removeScript(name, projectId);
|
||||
addToast("Script deleted", "success");
|
||||
setDeleteConfirmName(null);
|
||||
if (isEditing === name) {
|
||||
@@ -149,7 +150,7 @@ export function ScriptsModal({ isOpen, onClose, addToast, onRunScript }: Scripts
|
||||
} catch (err: any) {
|
||||
addToast(err.message || "Failed to delete script", "error");
|
||||
}
|
||||
}, [isEditing, addToast, loadScripts]);
|
||||
}, [isEditing, addToast, loadScripts, projectId]);
|
||||
|
||||
const handleRun = useCallback((name: string, command: string) => {
|
||||
if (onRunScript) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { THINKING_LEVELS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS } from "@fusion/core";
|
||||
import type { Settings, GlobalSettings, ThemeMode, ColorTheme, ModelPreset } from "@fusion/core";
|
||||
import type { Settings, GlobalSettings, ThemeMode, ColorTheme, ModelPreset, NtfyNotificationEvent } from "@fusion/core";
|
||||
import { fetchSettings, updateSettings, updateGlobalSettings, fetchAuthStatus, loginProvider, logoutProvider, fetchModels, testNtfyNotification, fetchBackups, createBackup, exportSettings, importSettings } from "../api";
|
||||
import type { AuthProvider, ModelInfo, BackupListResponse, SettingsExportData } from "../api";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
@@ -56,6 +56,7 @@ export type SectionId = (typeof SETTINGS_SECTIONS)[number]["id"];
|
||||
interface SettingsModalProps {
|
||||
onClose: () => void;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
projectId?: string;
|
||||
/** Optional section to show when the modal first opens. Defaults to "general". */
|
||||
initialSection?: SectionId;
|
||||
/** Current theme mode */
|
||||
@@ -71,6 +72,7 @@ interface SettingsModalProps {
|
||||
export function SettingsModal({
|
||||
onClose,
|
||||
addToast,
|
||||
projectId,
|
||||
initialSection,
|
||||
themeMode = "dark",
|
||||
colorTheme = "default",
|
||||
@@ -115,7 +117,7 @@ export function SettingsModal({
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchSettings()
|
||||
fetchSettings(projectId)
|
||||
.then((s) => {
|
||||
setForm(s);
|
||||
setLoading(false);
|
||||
@@ -124,7 +126,7 @@ export function SettingsModal({
|
||||
addToast(err.message, "error");
|
||||
setLoading(false);
|
||||
});
|
||||
}, [addToast]);
|
||||
}, [addToast, projectId]);
|
||||
|
||||
// Load auth status when the authentication section is active
|
||||
const loadAuthStatus = useCallback(async () => {
|
||||
@@ -149,12 +151,12 @@ export function SettingsModal({
|
||||
useEffect(() => {
|
||||
if (activeSection === "backups") {
|
||||
setBackupLoading(true);
|
||||
fetchBackups()
|
||||
fetchBackups(projectId)
|
||||
.then((info) => setBackupInfo(info))
|
||||
.catch(() => setBackupInfo(null))
|
||||
.finally(() => setBackupLoading(false));
|
||||
}
|
||||
}, [activeSection]);
|
||||
}, [activeSection, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeSection === "authentication") {
|
||||
@@ -224,7 +226,7 @@ export function SettingsModal({
|
||||
const result = await testNtfyNotification({
|
||||
ntfyEnabled: form.ntfyEnabled,
|
||||
ntfyTopic: form.ntfyTopic,
|
||||
});
|
||||
}, projectId);
|
||||
if (result.success) {
|
||||
addToast("Test notification sent — check your ntfy app!", "success");
|
||||
} else {
|
||||
@@ -235,16 +237,16 @@ export function SettingsModal({
|
||||
} finally {
|
||||
setTestNotificationLoading(false);
|
||||
}
|
||||
}, [addToast, form.ntfyEnabled, form.ntfyTopic]);
|
||||
}, [addToast, form.ntfyEnabled, form.ntfyTopic, projectId]);
|
||||
|
||||
const handleBackupNow = useCallback(async () => {
|
||||
setBackupLoading(true);
|
||||
try {
|
||||
const result = await createBackup();
|
||||
const result = await createBackup(projectId);
|
||||
if (result.success) {
|
||||
addToast("Backup created successfully", "success");
|
||||
// Refresh backup list
|
||||
const info = await fetchBackups();
|
||||
const info = await fetchBackups(projectId);
|
||||
setBackupInfo(info);
|
||||
} else {
|
||||
addToast(result.error || "Failed to create backup", "error");
|
||||
@@ -254,7 +256,7 @@ export function SettingsModal({
|
||||
} finally {
|
||||
setBackupLoading(false);
|
||||
}
|
||||
}, [addToast]);
|
||||
}, [addToast, projectId]);
|
||||
|
||||
// Export/Import handlers
|
||||
const handleExport = useCallback(async () => {
|
||||
@@ -262,7 +264,7 @@ export function SettingsModal({
|
||||
// Default scope based on active section
|
||||
const scope = activeSectionScope === "global" ? "global" :
|
||||
activeSectionScope === "project" ? "project" : "both";
|
||||
const data = await exportSettings(scope);
|
||||
const data = await exportSettings(scope, projectId);
|
||||
|
||||
// Create and download the JSON file
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
||||
@@ -281,7 +283,7 @@ export function SettingsModal({
|
||||
} catch (err: any) {
|
||||
addToast(err.message || "Failed to export settings", "error");
|
||||
}
|
||||
}, [addToast, activeSectionScope]);
|
||||
}, [addToast, activeSectionScope, projectId]);
|
||||
|
||||
const handleFileSelect = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
@@ -308,9 +310,9 @@ export function SettingsModal({
|
||||
|
||||
setImportLoading(true);
|
||||
try {
|
||||
const result = await importSettings(importPreview, { scope: importScope, merge: importMerge });
|
||||
const result = await importSettings(importPreview, { scope: importScope, merge: importMerge }, projectId);
|
||||
if (result.success) {
|
||||
const parts = [];
|
||||
const parts: string[] = [];
|
||||
if (result.globalCount > 0) parts.push(`${result.globalCount} global`);
|
||||
if (result.projectCount > 0) parts.push(`${result.projectCount} project`);
|
||||
addToast(`Imported ${parts.join(", ")} setting(s)`, "success");
|
||||
@@ -318,7 +320,7 @@ export function SettingsModal({
|
||||
setImportPreview(null);
|
||||
setImportFile(null);
|
||||
// Refresh settings to show imported values
|
||||
const refreshed = await fetchSettings();
|
||||
const refreshed = await fetchSettings(projectId);
|
||||
setForm(refreshed);
|
||||
} else {
|
||||
addToast(result.error || "Import failed", "error");
|
||||
@@ -328,7 +330,7 @@ export function SettingsModal({
|
||||
} finally {
|
||||
setImportLoading(false);
|
||||
}
|
||||
}, [addToast, importPreview, importScope, importMerge]);
|
||||
}, [addToast, importPreview, importScope, importMerge, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
@@ -379,7 +381,7 @@ export function SettingsModal({
|
||||
// Save both scopes in parallel if they have changes
|
||||
await Promise.all([
|
||||
Object.keys(globalPatch).length > 0 ? updateGlobalSettings(globalPatch) : Promise.resolve(),
|
||||
Object.keys(projectPatch).length > 0 ? updateSettings(projectPatch) : Promise.resolve(),
|
||||
Object.keys(projectPatch).length > 0 ? updateSettings(projectPatch, projectId) : Promise.resolve(),
|
||||
]);
|
||||
|
||||
addToast("Settings saved", "success");
|
||||
@@ -387,7 +389,7 @@ export function SettingsModal({
|
||||
} catch (err: any) {
|
||||
addToast(err.message, "error");
|
||||
}
|
||||
}, [form, prefixError, presetDraft, onClose, addToast]);
|
||||
}, [form, prefixError, presetDraft, onClose, addToast, projectId]);
|
||||
|
||||
const savePresetDraft = () => {
|
||||
if (!presetDraft) return;
|
||||
@@ -1408,10 +1410,10 @@ export function SettingsModal({
|
||||
type="checkbox"
|
||||
checked={form.ntfyEvents?.includes("in-review") ?? true}
|
||||
onChange={(e) => {
|
||||
const current = form.ntfyEvents ?? ["in-review", "merged", "failed"];
|
||||
const current = form.ntfyEvents ?? (["in-review", "merged", "failed"] as NtfyNotificationEvent[]);
|
||||
const newEvents = e.target.checked
|
||||
? [...new Set([...current, "in-review"])]
|
||||
: current.filter((ev) => ev !== "in-review");
|
||||
? (current.includes("in-review") ? current : [...current, "in-review" as NtfyNotificationEvent])
|
||||
: current.filter((ev): ev is NtfyNotificationEvent => ev !== "in-review");
|
||||
setForm((f) => ({ ...f, ntfyEvents: newEvents.length > 0 ? newEvents : undefined }));
|
||||
}}
|
||||
/>
|
||||
@@ -1424,10 +1426,10 @@ export function SettingsModal({
|
||||
type="checkbox"
|
||||
checked={form.ntfyEvents?.includes("merged") ?? true}
|
||||
onChange={(e) => {
|
||||
const current = form.ntfyEvents ?? ["in-review", "merged", "failed"];
|
||||
const current = form.ntfyEvents ?? (["in-review", "merged", "failed"] as NtfyNotificationEvent[]);
|
||||
const newEvents = e.target.checked
|
||||
? [...new Set([...current, "merged"])]
|
||||
: current.filter((ev) => ev !== "merged");
|
||||
? (current.includes("merged") ? current : [...current, "merged" as NtfyNotificationEvent])
|
||||
: current.filter((ev): ev is NtfyNotificationEvent => ev !== "merged");
|
||||
setForm((f) => ({ ...f, ntfyEvents: newEvents.length > 0 ? newEvents : undefined }));
|
||||
}}
|
||||
/>
|
||||
@@ -1440,10 +1442,10 @@ export function SettingsModal({
|
||||
type="checkbox"
|
||||
checked={form.ntfyEvents?.includes("failed") ?? true}
|
||||
onChange={(e) => {
|
||||
const current = form.ntfyEvents ?? ["in-review", "merged", "failed"];
|
||||
const current = form.ntfyEvents ?? (["in-review", "merged", "failed"] as NtfyNotificationEvent[]);
|
||||
const newEvents = e.target.checked
|
||||
? [...new Set([...current, "failed"])]
|
||||
: current.filter((ev) => ev !== "failed");
|
||||
? (current.includes("failed") ? current : [...current, "failed" as NtfyNotificationEvent])
|
||||
: current.filter((ev): ev is NtfyNotificationEvent => ev !== "failed");
|
||||
setForm((f) => ({ ...f, ntfyEvents: newEvents.length > 0 ? newEvents : undefined }));
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -34,7 +34,7 @@ describe("SubtaskBreakdownModal", () => {
|
||||
vi.clearAllMocks();
|
||||
streamHandlers = undefined;
|
||||
mockStartSubtaskBreakdown.mockResolvedValue({ sessionId: "session-123" });
|
||||
mockConnectSubtaskStream.mockImplementation((_sessionId, handlers) => {
|
||||
mockConnectSubtaskStream.mockImplementation((_sessionId, _projectId, handlers) => {
|
||||
streamHandlers = handlers;
|
||||
return { close: vi.fn(), isConnected: () => true };
|
||||
});
|
||||
@@ -60,7 +60,7 @@ describe("SubtaskBreakdownModal", () => {
|
||||
|
||||
it("shows generating state after auto-start", async () => {
|
||||
renderModal();
|
||||
await waitFor(() => expect(mockStartSubtaskBreakdown).toHaveBeenCalledWith("Build a complex feature"));
|
||||
await waitFor(() => expect(mockStartSubtaskBreakdown).toHaveBeenCalledWith("Build a complex feature", undefined));
|
||||
expect(await screen.findByText("AI is generating subtasks...")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ interface SubtaskBreakdownModalProps {
|
||||
initialDescription: string;
|
||||
onTasksCreated: (tasks: Task[]) => void;
|
||||
parentTaskId?: string;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
type ViewState =
|
||||
@@ -53,7 +54,7 @@ function hasDependencyCycle(subtasks: SubtaskItem[]): boolean {
|
||||
return subtasks.some((item) => visit(item.id));
|
||||
}
|
||||
|
||||
export function SubtaskBreakdownModal({ isOpen, onClose, initialDescription, onTasksCreated, parentTaskId }: SubtaskBreakdownModalProps) {
|
||||
export function SubtaskBreakdownModal({ isOpen, onClose, initialDescription, onTasksCreated, parentTaskId, projectId }: SubtaskBreakdownModalProps) {
|
||||
const [view, setView] = useState<ViewState>({ type: "initial" });
|
||||
const [subtasks, setSubtasks] = useState<SubtaskItem[]>([]);
|
||||
const [thinkingOutput, setThinkingOutput] = useState("");
|
||||
@@ -98,14 +99,14 @@ export function SubtaskBreakdownModal({ isOpen, onClose, initialDescription, onT
|
||||
}
|
||||
if (sessionId) {
|
||||
try {
|
||||
await cancelSubtaskBreakdown(sessionId);
|
||||
await cancelSubtaskBreakdown(sessionId, projectId);
|
||||
} catch {
|
||||
// ignore cancel errors
|
||||
}
|
||||
}
|
||||
resetState();
|
||||
onClose();
|
||||
}, [dirty, onClose, resetState, sessionId, view.type]);
|
||||
}, [dirty, onClose, resetState, sessionId, view.type, projectId]);
|
||||
|
||||
const beginBreakdown = useCallback(async () => {
|
||||
if (!initialDescription.trim()) return;
|
||||
@@ -113,10 +114,10 @@ export function SubtaskBreakdownModal({ isOpen, onClose, initialDescription, onT
|
||||
setThinkingOutput("");
|
||||
|
||||
try {
|
||||
const { sessionId } = await startSubtaskBreakdown(initialDescription.trim());
|
||||
const { sessionId } = await startSubtaskBreakdown(initialDescription.trim(), projectId);
|
||||
setView({ type: "generating", sessionId });
|
||||
streamRef.current?.close();
|
||||
streamRef.current = connectSubtaskStream(sessionId, {
|
||||
streamRef.current = connectSubtaskStream(sessionId, projectId, {
|
||||
onThinking: (data) => setThinkingOutput((prev) => prev + data),
|
||||
onSubtasks: (items) => {
|
||||
setSubtasks(items);
|
||||
@@ -274,7 +275,7 @@ export function SubtaskBreakdownModal({ isOpen, onClose, initialDescription, onT
|
||||
setError(null);
|
||||
setView({ type: "creating", sessionId });
|
||||
try {
|
||||
const result = await createTasksFromBreakdown(sessionId, subtasks, parentTaskId);
|
||||
const result = await createTasksFromBreakdown(sessionId, subtasks, parentTaskId, projectId);
|
||||
onTasksCreated(result.tasks);
|
||||
resetState();
|
||||
onClose();
|
||||
|
||||
@@ -133,7 +133,7 @@ describe("TaskCard", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpload).toHaveBeenCalledWith("FN-001", file);
|
||||
expect(mockUpload).toHaveBeenCalledWith("FN-001", file, undefined);
|
||||
expect(addToast).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Attached test.png"),
|
||||
"success",
|
||||
|
||||
@@ -33,6 +33,7 @@ const ACTIVE_STATUSES = new Set(["planning", "researching", "executing", "finali
|
||||
|
||||
interface TaskCardProps {
|
||||
task: Task;
|
||||
projectId?: string;
|
||||
queued?: boolean;
|
||||
onOpenDetail: (task: TaskDetail) => void;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
@@ -79,6 +80,7 @@ function areTaskCardPropsEqual(previous: TaskCardProps, next: TaskCardProps): bo
|
||||
|
||||
return (
|
||||
previous.queued === next.queued &&
|
||||
previous.projectId === next.projectId &&
|
||||
previous.globalPaused === next.globalPaused &&
|
||||
previous.onOpenDetail === next.onOpenDetail &&
|
||||
previous.addToast === next.addToast &&
|
||||
@@ -119,6 +121,7 @@ function areTaskCardPropsEqual(previous: TaskCardProps, next: TaskCardProps): bo
|
||||
|
||||
function TaskCardComponent({
|
||||
task,
|
||||
projectId,
|
||||
queued,
|
||||
onOpenDetail,
|
||||
addToast,
|
||||
@@ -227,7 +230,7 @@ function TaskCardComponent({
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
for (const file of files) {
|
||||
try {
|
||||
await uploadAttachment(task.id, file);
|
||||
await uploadAttachment(task.id, file, projectId);
|
||||
addToast(`Attached ${file.name} to ${task.id}`, "success");
|
||||
} catch (err: any) {
|
||||
addToast(`Failed to attach ${file.name}: ${err.message}`, "error");
|
||||
@@ -238,7 +241,7 @@ function TaskCardComponent({
|
||||
const handleClick = useCallback(async () => {
|
||||
if (isEditing) return; // Don't open detail when editing
|
||||
try {
|
||||
const detail = await fetchTaskDetail(task.id);
|
||||
const detail = await fetchTaskDetail(task.id, projectId);
|
||||
onOpenDetail(detail);
|
||||
} catch {
|
||||
addToast("Failed to load task details", "error");
|
||||
@@ -301,7 +304,7 @@ function TaskCardComponent({
|
||||
const handleDepClick = useCallback(async (e: React.MouseEvent, depId: string) => {
|
||||
e.stopPropagation(); // Prevent card click
|
||||
try {
|
||||
const detail = await fetchTaskDetail(depId);
|
||||
const detail = await fetchTaskDetail(depId, projectId);
|
||||
onOpenDetail(detail);
|
||||
} catch (err: any) {
|
||||
addToast(`Failed to load dependency ${depId}`, "error");
|
||||
@@ -331,10 +334,10 @@ function TaskCardComponent({
|
||||
}, [hasGitHubBadge, isInViewport, subscribeToBadge, task.id, unsubscribeFromBadge]);
|
||||
|
||||
const liveBadgeData = badgeUpdates.get(task.id);
|
||||
const { files: sessionFiles, loading: sessionFilesLoading } = useSessionFiles(task.id, task.worktree, task.column);
|
||||
const { files: sessionFiles, loading: sessionFilesLoading } = useSessionFiles(task.id, task.worktree, task.column, projectId);
|
||||
|
||||
// Get fresh batch data if available
|
||||
const batchData = useMemo(() => getFreshBatchData(task.id), [task.id]);
|
||||
const batchData = useMemo(() => getFreshBatchData(task.id, projectId), [task.id, projectId]);
|
||||
|
||||
// Pick the freshest data among WebSocket, batch, and task data
|
||||
const livePrInfo = useMemo(() => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { highlightDiff } from "../utils/highlightDiff";
|
||||
interface TaskChangesTabProps {
|
||||
taskId: string;
|
||||
worktree?: string;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
function getFileStatus(file: string, patch: string): "added" | "modified" | "deleted" | "unknown" {
|
||||
@@ -36,7 +37,7 @@ function formatFileSize(bytes: number): string {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
export function TaskChangesTab({ taskId, worktree }: TaskChangesTabProps) {
|
||||
export function TaskChangesTab({ taskId, worktree, projectId }: TaskChangesTabProps) {
|
||||
const [diffData, setDiffData] = useState<TaskDiff | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -51,7 +52,7 @@ export function TaskChangesTab({ taskId, worktree }: TaskChangesTabProps) {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await fetchTaskDiff(taskId);
|
||||
const data = await fetchTaskDiff(taskId, undefined, projectId);
|
||||
setDiffData(data);
|
||||
// Auto-expand first file if there are files
|
||||
if (data.files.length > 0) {
|
||||
@@ -62,7 +63,7 @@ export function TaskChangesTab({ taskId, worktree }: TaskChangesTabProps) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [taskId, worktree]);
|
||||
}, [taskId, worktree, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
loadDiff();
|
||||
|
||||
@@ -10,6 +10,7 @@ interface TaskCommentsProps {
|
||||
onTaskUpdated?: (task: Task) => void;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
currentAuthor?: string;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
type CommentType = "comment" | "guidance";
|
||||
@@ -24,7 +25,7 @@ function isAIGuidanceComment(author: string): boolean {
|
||||
return author === "agent" || author === "system";
|
||||
}
|
||||
|
||||
export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "user" }: TaskCommentsProps) {
|
||||
export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "user", projectId }: TaskCommentsProps) {
|
||||
const [draft, setDraft] = useState("");
|
||||
const [editingId, setEditingId] = useState<string | null>(null);
|
||||
const [editingText, setEditingText] = useState("");
|
||||
@@ -47,12 +48,12 @@ export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "u
|
||||
setSubmitting(true);
|
||||
try {
|
||||
if (commentType === "guidance") {
|
||||
const updated = await addSteeringComment(task.id, text);
|
||||
const updated = await addSteeringComment(task.id, text, projectId);
|
||||
setDraft("");
|
||||
onTaskUpdated?.(updated);
|
||||
addToast("AI Guidance added", "success");
|
||||
} else {
|
||||
const updated = await addTaskComment(task.id, text, currentAuthor);
|
||||
const updated = await addTaskComment(task.id, text, currentAuthor, projectId);
|
||||
setDraft("");
|
||||
onTaskUpdated?.(updated);
|
||||
addToast("Comment added", "success");
|
||||
@@ -69,7 +70,7 @@ export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "u
|
||||
if (!text) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const updated = await updateTaskComment(task.id, commentId, text);
|
||||
const updated = await updateTaskComment(task.id, commentId, text, projectId);
|
||||
setEditingId(null);
|
||||
setEditingText("");
|
||||
onTaskUpdated?.(updated);
|
||||
@@ -84,7 +85,7 @@ export function TaskComments({ task, onTaskUpdated, addToast, currentAuthor = "u
|
||||
async function handleDelete(commentId: string) {
|
||||
setDeletingId(commentId);
|
||||
try {
|
||||
const updated = await deleteTaskComment(task.id, commentId);
|
||||
const updated = await deleteTaskComment(task.id, commentId, projectId);
|
||||
onTaskUpdated?.(updated);
|
||||
addToast("Comment deleted", "success");
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -74,6 +74,7 @@ function formatBytes(bytes: number): string {
|
||||
|
||||
interface TaskDetailModalProps {
|
||||
task: TaskDetail;
|
||||
projectId?: string;
|
||||
tasks?: Task[];
|
||||
onClose: () => void;
|
||||
onOpenDetail: (task: TaskDetail) => void; // For clicking dependencies
|
||||
@@ -94,6 +95,7 @@ const EDITABLE_COLUMNS: Set<Column> = new Set(["triage", "todo"]);
|
||||
|
||||
export function TaskDetailModal({
|
||||
task,
|
||||
projectId,
|
||||
tasks = [],
|
||||
onClose,
|
||||
onOpenDetail,
|
||||
@@ -184,7 +186,7 @@ export function TaskDetailModal({
|
||||
await updateTask(task.id, {
|
||||
title: editTitle.trim() || undefined,
|
||||
description: editDescription.trim() || undefined,
|
||||
});
|
||||
}, projectId);
|
||||
addToast(`Updated ${task.id}`, "success");
|
||||
setIsEditing(false);
|
||||
} catch (err: any) {
|
||||
@@ -229,6 +231,7 @@ export function TaskDetailModal({
|
||||
const { entries: agentLogEntries, loading: agentLogLoading } = useAgentLogs(
|
||||
task.id,
|
||||
activeTab === "agent-log",
|
||||
projectId,
|
||||
);
|
||||
useEffect(() => {
|
||||
const handleKey = (e: KeyboardEvent) => {
|
||||
@@ -311,10 +314,10 @@ export function TaskDetailModal({
|
||||
const handleTogglePause = useCallback(async () => {
|
||||
try {
|
||||
if (task.paused) {
|
||||
await unpauseTask(task.id);
|
||||
await unpauseTask(task.id, projectId);
|
||||
addToast(`Unpaused ${task.id}`, "success");
|
||||
} else {
|
||||
await pauseTask(task.id);
|
||||
await pauseTask(task.id, projectId);
|
||||
addToast(`Paused ${task.id}`, "success");
|
||||
}
|
||||
onClose();
|
||||
@@ -325,7 +328,7 @@ export function TaskDetailModal({
|
||||
|
||||
const handleApprovePlan = useCallback(async () => {
|
||||
try {
|
||||
await approvePlan(task.id);
|
||||
await approvePlan(task.id, projectId);
|
||||
addToast(`Plan approved — ${task.id} moved to Todo`, "success");
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
@@ -336,7 +339,7 @@ export function TaskDetailModal({
|
||||
const handleRejectPlan = useCallback(async () => {
|
||||
if (!confirm("Reject this plan? The specification will be discarded and regenerated.")) return;
|
||||
try {
|
||||
await rejectPlan(task.id);
|
||||
await rejectPlan(task.id, projectId);
|
||||
addToast(`Plan rejected — ${task.id} returned to Triage for re-specification`, "info");
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
@@ -366,7 +369,7 @@ export function TaskDetailModal({
|
||||
}
|
||||
setIsRefining(true);
|
||||
try {
|
||||
const newTask = await refineTask(task.id, refineFeedback.trim());
|
||||
const newTask = await refineTask(task.id, refineFeedback.trim(), projectId);
|
||||
addToast(`Refinement task created: ${newTask.id}`, "success");
|
||||
onClose();
|
||||
} catch (err: any) {
|
||||
@@ -379,7 +382,7 @@ export function TaskDetailModal({
|
||||
const uploadFile = useCallback(async (file: File) => {
|
||||
setUploading(true);
|
||||
try {
|
||||
const attachment = await uploadAttachment(task.id, file);
|
||||
const attachment = await uploadAttachment(task.id, file, projectId);
|
||||
setAttachments((prev) => [...prev, attachment]);
|
||||
addToast("Screenshot attached", "success");
|
||||
} catch (err: any) {
|
||||
@@ -434,7 +437,7 @@ export function TaskDetailModal({
|
||||
|
||||
const handleDeleteAttachment = useCallback(async (filename: string) => {
|
||||
try {
|
||||
await deleteAttachment(task.id, filename);
|
||||
await deleteAttachment(task.id, filename, projectId);
|
||||
setAttachments((prev) => prev.filter((a) => a.filename !== filename));
|
||||
addToast("Attachment deleted", "info");
|
||||
} catch (err: any) {
|
||||
@@ -446,7 +449,7 @@ export function TaskDetailModal({
|
||||
const newDeps = [...dependencies, depId];
|
||||
setDependencies(newDeps);
|
||||
try {
|
||||
await updateTask(task.id, { dependencies: newDeps });
|
||||
await updateTask(task.id, { dependencies: newDeps }, projectId);
|
||||
} catch (err: any) {
|
||||
setDependencies(dependencies);
|
||||
addToast(err.message, "error");
|
||||
@@ -458,7 +461,7 @@ export function TaskDetailModal({
|
||||
const newDeps = dependencies.filter((d) => d !== depId);
|
||||
setDependencies(newDeps);
|
||||
try {
|
||||
await updateTask(task.id, { dependencies: newDeps });
|
||||
await updateTask(task.id, { dependencies: newDeps }, projectId);
|
||||
} catch (err: any) {
|
||||
setDependencies(dependencies);
|
||||
addToast(err.message, "error");
|
||||
@@ -467,7 +470,7 @@ export function TaskDetailModal({
|
||||
|
||||
const handleDepClick = useCallback(async (depId: string) => {
|
||||
try {
|
||||
const detail = await fetchTaskDetail(depId);
|
||||
const detail = await fetchTaskDetail(depId, projectId);
|
||||
onOpenDetail(detail);
|
||||
} catch (err: any) {
|
||||
addToast(`Failed to load dependency ${depId}`, "error");
|
||||
@@ -478,7 +481,7 @@ export function TaskDetailModal({
|
||||
const handleSaveSpec = useCallback(async (newContent: string) => {
|
||||
setIsSavingSpec(true);
|
||||
try {
|
||||
await updateTask(task.id, { prompt: newContent });
|
||||
await updateTask(task.id, { prompt: newContent }, projectId);
|
||||
addToast("Spec updated", "success");
|
||||
// Update local task data
|
||||
task.prompt = newContent;
|
||||
@@ -493,7 +496,7 @@ export function TaskDetailModal({
|
||||
const handleRequestSpecRevision = useCallback(async (feedback: string) => {
|
||||
setIsRequestingRevision(true);
|
||||
try {
|
||||
await requestSpecRevision(task.id, feedback);
|
||||
await requestSpecRevision(task.id, feedback, projectId);
|
||||
addToast("AI revision requested. Task moved to triage.", "success");
|
||||
// Task has been moved to triage, close modal
|
||||
onClose();
|
||||
@@ -706,9 +709,9 @@ export function TaskDetailModal({
|
||||
/>
|
||||
</div>
|
||||
) : activeTab === "changes" ? (
|
||||
<TaskChangesTab taskId={task.id} worktree={task.worktree} />
|
||||
<TaskChangesTab taskId={task.id} worktree={task.worktree} projectId={projectId} />
|
||||
) : activeTab === "comments" ? (
|
||||
<TaskComments task={task} addToast={addToast} />
|
||||
<TaskComments task={task} addToast={addToast} projectId={projectId} />
|
||||
) : activeTab === "activity" ? (
|
||||
<div className="detail-section detail-activity">
|
||||
<h4>Activity</h4>
|
||||
@@ -984,6 +987,7 @@ export function TaskDetailModal({
|
||||
{task.column === "in-review" && (
|
||||
<PrSection
|
||||
taskId={task.id}
|
||||
projectId={projectId}
|
||||
prInfo={task.prInfo}
|
||||
automationStatus={task.status ?? null}
|
||||
hasGitHubToken={githubTokenConfigured ?? false}
|
||||
|
||||
@@ -37,7 +37,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand }: TerminalModal
|
||||
const xtermRef = useRef<XTerm | null>(null);
|
||||
const fitAddonRef = useRef<ITerminalAddon | null>(null);
|
||||
const hasInitialCommandRun = useRef(false);
|
||||
const xtermInitializedRef = useRef(false);
|
||||
const xtermInitializedRef = useRef<string | false>(false);
|
||||
|
||||
// Use the session management hook
|
||||
const {
|
||||
|
||||
@@ -32,6 +32,7 @@ interface WorkflowStepManagerProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
interface StepFormData {
|
||||
@@ -80,7 +81,7 @@ function getCategoryColors(category: string): { bg: string; text: string } {
|
||||
}
|
||||
}
|
||||
|
||||
export function WorkflowStepManager({ isOpen, onClose, addToast }: WorkflowStepManagerProps) {
|
||||
export function WorkflowStepManager({ isOpen, onClose, addToast, projectId }: WorkflowStepManagerProps) {
|
||||
const [steps, setSteps] = useState<WorkflowStep[]>([]);
|
||||
const [templates, setTemplates] = useState<WorkflowStepTemplate[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -97,14 +98,14 @@ export function WorkflowStepManager({ isOpen, onClose, addToast }: WorkflowStepM
|
||||
const loadSteps = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await fetchWorkflowSteps();
|
||||
const data = await fetchWorkflowSteps(projectId);
|
||||
setSteps(data);
|
||||
} catch (err: any) {
|
||||
addToast(err.message || "Failed to load workflow steps", "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [addToast]);
|
||||
}, [addToast, projectId]);
|
||||
|
||||
const loadTemplates = useCallback(async () => {
|
||||
try {
|
||||
@@ -163,7 +164,7 @@ export function WorkflowStepManager({ isOpen, onClose, addToast }: WorkflowStepM
|
||||
prompt: form.prompt.trim() || undefined,
|
||||
enabled: form.enabled,
|
||||
};
|
||||
await createWorkflowStep(input);
|
||||
await createWorkflowStep(input, projectId);
|
||||
addToast("Workflow step created", "success");
|
||||
} else if (editingId) {
|
||||
await updateWorkflowStep(editingId, {
|
||||
@@ -171,7 +172,7 @@ export function WorkflowStepManager({ isOpen, onClose, addToast }: WorkflowStepM
|
||||
description: form.description.trim(),
|
||||
prompt: form.prompt,
|
||||
enabled: form.enabled,
|
||||
});
|
||||
}, projectId);
|
||||
addToast("Workflow step updated", "success");
|
||||
}
|
||||
|
||||
@@ -188,7 +189,7 @@ export function WorkflowStepManager({ isOpen, onClose, addToast }: WorkflowStepM
|
||||
|
||||
const handleDelete = useCallback(async (id: string) => {
|
||||
try {
|
||||
await deleteWorkflowStep(id);
|
||||
await deleteWorkflowStep(id, projectId);
|
||||
addToast("Workflow step deleted", "success");
|
||||
setDeleteConfirmId(null);
|
||||
if (editingId === id) {
|
||||
@@ -219,13 +220,13 @@ export function WorkflowStepManager({ isOpen, onClose, addToast }: WorkflowStepM
|
||||
prompt: form.prompt.trim() || undefined,
|
||||
enabled: form.enabled,
|
||||
};
|
||||
const created = await createWorkflowStep(input);
|
||||
const created = await createWorkflowStep(input, projectId);
|
||||
setIsCreating(false);
|
||||
setEditingId(created.id);
|
||||
|
||||
// Now refine
|
||||
setRefining(true);
|
||||
const result = await refineWorkflowStepPrompt(created.id);
|
||||
const result = await refineWorkflowStepPrompt(created.id, projectId);
|
||||
setForm((prev) => ({ ...prev, prompt: result.prompt }));
|
||||
addToast("Prompt refined with AI", "success");
|
||||
await loadSteps();
|
||||
@@ -242,7 +243,7 @@ export function WorkflowStepManager({ isOpen, onClose, addToast }: WorkflowStepM
|
||||
|
||||
setRefining(true);
|
||||
try {
|
||||
const result = await refineWorkflowStepPrompt(editingId);
|
||||
const result = await refineWorkflowStepPrompt(editingId, projectId);
|
||||
setForm((prev) => ({ ...prev, prompt: result.prompt }));
|
||||
addToast("Prompt refined with AI", "success");
|
||||
await loadSteps();
|
||||
@@ -256,7 +257,7 @@ export function WorkflowStepManager({ isOpen, onClose, addToast }: WorkflowStepM
|
||||
const handleAddTemplate = useCallback(async (template: WorkflowStepTemplate) => {
|
||||
setAddingTemplateId(template.id);
|
||||
try {
|
||||
await createWorkflowStepFromTemplate(template.id);
|
||||
await createWorkflowStepFromTemplate(template.id, projectId);
|
||||
addToast(`Added ${template.name} workflow step`, "success");
|
||||
await loadSteps();
|
||||
// Switch to "My Workflow Steps" tab to show the newly added step
|
||||
|
||||
@@ -8,6 +8,7 @@ interface WorktreeGroupProps {
|
||||
label: string;
|
||||
activeTasks: Task[];
|
||||
queuedTasks: Task[];
|
||||
projectId?: string;
|
||||
onOpenDetail: (task: TaskDetail) => void;
|
||||
addToast: (message: string, type?: ToastType) => void;
|
||||
globalPaused?: boolean;
|
||||
@@ -22,6 +23,7 @@ function WorktreeGroupComponent({
|
||||
label,
|
||||
activeTasks,
|
||||
queuedTasks,
|
||||
projectId,
|
||||
onOpenDetail,
|
||||
addToast,
|
||||
globalPaused,
|
||||
@@ -37,12 +39,13 @@ function WorktreeGroupComponent({
|
||||
<span className="worktree-label">{label}</span>
|
||||
</div>
|
||||
{activeTasks.map((task) => (
|
||||
<TaskCard key={task.id} task={task} onOpenDetail={onOpenDetail} addToast={addToast} globalPaused={globalPaused} onUpdateTask={onUpdateTask} onOpenFilesForTask={onOpenFilesForTask} />
|
||||
<TaskCard key={task.id} task={task} projectId={projectId} onOpenDetail={onOpenDetail} addToast={addToast} globalPaused={globalPaused} onUpdateTask={onUpdateTask} onOpenFilesForTask={onOpenFilesForTask} />
|
||||
))}
|
||||
{queuedTasks.map((task) => (
|
||||
<TaskCard
|
||||
key={task.id}
|
||||
task={task}
|
||||
projectId={projectId}
|
||||
queued
|
||||
onOpenDetail={onOpenDetail}
|
||||
addToast={addToast}
|
||||
|
||||
@@ -89,6 +89,21 @@ describe("AgentDetailView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("fetches the agent using the active project context", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
projectId="proj_123"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchAgent).toHaveBeenCalledWith("agent-001", "proj_123");
|
||||
});
|
||||
});
|
||||
|
||||
it("displays role badge", async () => {
|
||||
render(
|
||||
<AgentDetailView
|
||||
@@ -219,7 +234,7 @@ describe("AgentDetailView", () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchAgent).toHaveBeenCalledWith("agent-001");
|
||||
expect(mockFetchAgent).toHaveBeenCalledWith("agent-001", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -296,10 +296,13 @@ describe("AgentListModal", () => {
|
||||
fireEvent.click(createButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockCreateAgent).toHaveBeenCalledWith({
|
||||
name: "My New Agent",
|
||||
role: "executor",
|
||||
});
|
||||
expect(mockCreateAgent).toHaveBeenCalledWith(
|
||||
{
|
||||
name: "My New Agent",
|
||||
role: "executor",
|
||||
},
|
||||
undefined
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockAddToast).toHaveBeenCalledWith(
|
||||
@@ -393,7 +396,7 @@ describe("AgentListModal", () => {
|
||||
fireEvent.click(startButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-001", "active");
|
||||
expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-001", "active", undefined);
|
||||
});
|
||||
|
||||
expect(mockAddToast).toHaveBeenCalledWith(
|
||||
@@ -463,7 +466,7 @@ describe("AgentListModal", () => {
|
||||
fireEvent.click(pauseButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-002", "paused");
|
||||
expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-002", "paused", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -496,7 +499,7 @@ describe("AgentListModal", () => {
|
||||
fireEvent.click(stopButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-002", "terminated");
|
||||
expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-002", "terminated", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -530,7 +533,7 @@ describe("AgentListModal", () => {
|
||||
fireEvent.click(screen.getByTitle("Resume"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-003", "active");
|
||||
expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-003", "active", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -618,7 +621,7 @@ describe("AgentListModal", () => {
|
||||
fireEvent.click(screen.getByTitle("Delete"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDeleteAgent).toHaveBeenCalledWith("agent-004");
|
||||
expect(mockDeleteAgent).toHaveBeenCalledWith("agent-004", undefined);
|
||||
});
|
||||
|
||||
expect(mockAddToast).toHaveBeenCalledWith(
|
||||
@@ -672,7 +675,7 @@ describe("AgentListModal", () => {
|
||||
fireEvent.change(filterSelect, { target: { value: "active" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchAgents).toHaveBeenCalledWith({ state: "active" });
|
||||
expect(mockFetchAgents).toHaveBeenCalledWith({ state: "active" }, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -693,13 +696,13 @@ describe("AgentListModal", () => {
|
||||
fireEvent.change(filterSelect, { target: { value: "idle" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchAgents).toHaveBeenLastCalledWith({ state: "idle" });
|
||||
expect(mockFetchAgents).toHaveBeenLastCalledWith({ state: "idle" }, undefined);
|
||||
});
|
||||
|
||||
fireEvent.change(filterSelect, { target: { value: "all" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchAgents).toHaveBeenLastCalledWith(undefined);
|
||||
expect(mockFetchAgents).toHaveBeenLastCalledWith(undefined, undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -94,6 +94,13 @@ describe("AgentsView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("passes projectId to agent fetches", async () => {
|
||||
render(<AgentsView addToast={mockAddToast} projectId="proj_123" />);
|
||||
await waitFor(() => {
|
||||
expect(mockFetchAgents).toHaveBeenCalledWith(undefined, "proj_123");
|
||||
});
|
||||
});
|
||||
|
||||
it("renders empty state when no agents", async () => {
|
||||
mockFetchAgents.mockResolvedValue([]);
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
@@ -220,7 +227,7 @@ describe("AgentsView", () => {
|
||||
fireEvent.change(filterSelect, { target: { value: "active" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchAgents).toHaveBeenCalledWith({ state: "active" });
|
||||
expect(mockFetchAgents).toHaveBeenCalledWith({ state: "active" }, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -235,13 +242,13 @@ describe("AgentsView", () => {
|
||||
fireEvent.change(filterSelect, { target: { value: "idle" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchAgents).toHaveBeenLastCalledWith({ state: "idle" });
|
||||
expect(mockFetchAgents).toHaveBeenLastCalledWith({ state: "idle" }, undefined);
|
||||
});
|
||||
|
||||
fireEvent.change(filterSelect, { target: { value: "all" } });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchAgents).toHaveBeenLastCalledWith(undefined);
|
||||
expect(mockFetchAgents).toHaveBeenLastCalledWith(undefined, undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -268,7 +275,7 @@ describe("AgentsView", () => {
|
||||
expect(mockCreateAgent).toHaveBeenCalledWith({
|
||||
name: "My Agent",
|
||||
role: "custom",
|
||||
});
|
||||
}, undefined);
|
||||
});
|
||||
|
||||
expect(mockAddToast).toHaveBeenCalledWith(
|
||||
@@ -338,7 +345,7 @@ describe("AgentsView", () => {
|
||||
fireEvent.click(screen.getByTitle("Activate"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-001", "active");
|
||||
expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-001", "active", undefined);
|
||||
});
|
||||
|
||||
expect(mockAddToast).toHaveBeenCalledWith(
|
||||
@@ -370,7 +377,7 @@ describe("AgentsView", () => {
|
||||
fireEvent.click(pauseButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-002", "paused");
|
||||
expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-002", "paused", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -384,7 +391,7 @@ describe("AgentsView", () => {
|
||||
fireEvent.click(screen.getByTitle("Resume"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-003", "active");
|
||||
expect(mockUpdateAgentState).toHaveBeenCalledWith("agent-003", "active", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -448,7 +455,7 @@ describe("AgentsView", () => {
|
||||
fireEvent.click(screen.getByTitle("Delete"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockDeleteAgent).toHaveBeenCalledWith("agent-004");
|
||||
expect(mockDeleteAgent).toHaveBeenCalledWith("agent-004", undefined);
|
||||
});
|
||||
|
||||
expect(mockAddToast).toHaveBeenCalledWith(
|
||||
|
||||
@@ -140,7 +140,7 @@ describe("App deep link handling", () => {
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchTaskDetail).toHaveBeenCalledWith("FN-123");
|
||||
expect(fetchTaskDetail).toHaveBeenCalledWith("FN-123", "proj_123");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -160,7 +160,7 @@ describe("App deep link handling", () => {
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchTaskDetail).toHaveBeenCalledWith("FN-404");
|
||||
expect(fetchTaskDetail).toHaveBeenCalledWith("FN-404", "proj_123");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -197,7 +197,7 @@ describe("App deep link handling", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchTaskDetail).toHaveBeenCalledWith("FN-789");
|
||||
expect(fetchTaskDetail).toHaveBeenCalledWith("FN-789", "proj_456");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -242,7 +242,7 @@ describe("App deep link handling", () => {
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchTaskDetail).toHaveBeenCalledWith("FN-123");
|
||||
expect(fetchTaskDetail).toHaveBeenCalledWith("FN-123", "proj_123");
|
||||
});
|
||||
|
||||
// setCurrentProject should NOT be called since we're already on this project
|
||||
@@ -262,7 +262,7 @@ describe("App deep link handling", () => {
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchTaskDetail).toHaveBeenCalledWith("FN-123");
|
||||
expect(fetchTaskDetail).toHaveBeenCalledWith("FN-123", "proj_123");
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -274,6 +274,44 @@ describe("App deep link handling", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("App mission wiring", () => {
|
||||
afterEach(() => {
|
||||
localStorage.removeItem("kb-dashboard-view-mode");
|
||||
});
|
||||
|
||||
it("hides mission controls when no project is selected", async () => {
|
||||
mockCurrentProjectState.currentProject = null;
|
||||
mockProjectsState.projects = [];
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchSettings).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId("missions-btn")).toBeNull();
|
||||
});
|
||||
|
||||
it("shows mission controls in project view when a project is selected", async () => {
|
||||
localStorage.setItem("kb-dashboard-view-mode", "project");
|
||||
mockCurrentProjectState.currentProject = {
|
||||
id: "proj_123",
|
||||
name: "Test Project",
|
||||
path: "/test",
|
||||
status: "active",
|
||||
isolationMode: "in-process",
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
};
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("missions-btn")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("App auto-open Settings on unauthenticated", () => {
|
||||
it("auto-opens Settings to Authentication tab when all providers are unauthenticated", async () => {
|
||||
render(<App />);
|
||||
@@ -410,7 +448,7 @@ describe("App global pause (hard stop)", () => {
|
||||
});
|
||||
|
||||
// Should call updateSettings with globalPause: true
|
||||
expect(updateSettings).toHaveBeenCalledWith({ globalPause: true });
|
||||
expect(updateSettings).toHaveBeenCalledWith({ globalPause: true }, "proj_123");
|
||||
});
|
||||
|
||||
it("reverts global pause state on updateSettings failure", async () => {
|
||||
@@ -485,7 +523,7 @@ describe("App engine pause (soft pause)", () => {
|
||||
});
|
||||
|
||||
// Should call updateSettings with enginePaused: true
|
||||
expect(updateSettings).toHaveBeenCalledWith({ enginePaused: true });
|
||||
expect(updateSettings).toHaveBeenCalledWith({ enginePaused: true }, "proj_123");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -638,6 +676,19 @@ describe("App view switching", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("hides agent view controls when no project is active", async () => {
|
||||
mockCurrentProjectState.currentProject = null;
|
||||
localStorage.setItem("kb-dashboard-view-mode", "overview");
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByTitle("Agents view")).toBeNull();
|
||||
});
|
||||
|
||||
localStorage.removeItem("kb-dashboard-view-mode");
|
||||
});
|
||||
|
||||
it("renders AgentsView when agents view is selected", async () => {
|
||||
render(<App />);
|
||||
|
||||
|
||||
@@ -55,6 +55,17 @@ describe("Header", () => {
|
||||
expect(btn).toBeDefined();
|
||||
});
|
||||
|
||||
it("renders the missions button when mission management is available", () => {
|
||||
const onOpen = vi.fn();
|
||||
render(<Header onOpenMissions={onOpen} />);
|
||||
expect(screen.getByTestId("missions-btn")).toBeDefined();
|
||||
});
|
||||
|
||||
it("does not render the missions button when mission management is unavailable", () => {
|
||||
render(<Header />);
|
||||
expect(screen.queryByTestId("missions-btn")).toBeNull();
|
||||
});
|
||||
|
||||
it("calls onOpenGitHubImport when import button is clicked", () => {
|
||||
const onOpen = vi.fn();
|
||||
render(<Header onOpenGitHubImport={onOpen} />);
|
||||
|
||||
@@ -167,7 +167,7 @@ describe("ListView", () => {
|
||||
fireEvent.click(row!);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchTaskDetail).toHaveBeenCalledWith("FN-001");
|
||||
expect(fetchTaskDetail).toHaveBeenCalledWith("FN-001", undefined);
|
||||
});
|
||||
|
||||
expect(mockOnOpenDetail).toHaveBeenCalledWith(mockDetail);
|
||||
|
||||
@@ -119,7 +119,7 @@ describe("PrSection", () => {
|
||||
expect(createPr).toHaveBeenCalledWith("FN-001", {
|
||||
title: "My PR Title",
|
||||
body: undefined,
|
||||
});
|
||||
}, undefined);
|
||||
});
|
||||
|
||||
expect(mockOnPrCreated).toHaveBeenCalledWith(mockPrInfo);
|
||||
@@ -243,7 +243,7 @@ describe("PrSection", () => {
|
||||
fireEvent.click(refreshButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(refreshPrStatus).toHaveBeenCalledWith("FN-001");
|
||||
expect(refreshPrStatus).toHaveBeenCalledWith("FN-001", undefined);
|
||||
});
|
||||
|
||||
expect(mockOnPrUpdated).toHaveBeenCalledWith(updatedPr);
|
||||
|
||||
@@ -106,7 +106,7 @@ describe("ScriptsModal", () => {
|
||||
fireEvent.click(screen.getByTestId("script-save-btn"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addScript).toHaveBeenCalledWith("new-script", "echo hello");
|
||||
expect(addScript).toHaveBeenCalledWith("new-script", "echo hello", undefined);
|
||||
expect(addToast).toHaveBeenCalledWith("Script created", "success");
|
||||
});
|
||||
});
|
||||
@@ -158,7 +158,7 @@ describe("ScriptsModal", () => {
|
||||
fireEvent.click(screen.getByTestId("script-save-btn"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addScript).toHaveBeenCalledWith("my-script_v2", "echo test");
|
||||
expect(addScript).toHaveBeenCalledWith("my-script_v2", "echo test", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -222,7 +222,7 @@ describe("ScriptsModal", () => {
|
||||
fireEvent.click(screen.getByTestId("confirm-delete-script-build"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(removeScript).toHaveBeenCalledWith("build");
|
||||
expect(removeScript).toHaveBeenCalledWith("build", undefined);
|
||||
expect(addToast).toHaveBeenCalledWith("Script deleted", "success");
|
||||
});
|
||||
});
|
||||
@@ -317,7 +317,7 @@ describe("ScriptsModal", () => {
|
||||
fireEvent.click(screen.getByTestId("script-save-btn"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addScript).toHaveBeenCalledWith("build", "npm run build:prod");
|
||||
expect(addScript).toHaveBeenCalledWith("build", "npm run build:prod", undefined);
|
||||
expect(addToast).toHaveBeenCalledWith("Script updated", "success");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,10 +33,14 @@ vi.mock("../../api", () => ({
|
||||
fetchAuthStatus: vi.fn(() => Promise.resolve({ providers: [{ id: "anthropic", name: "Anthropic", authenticated: false }] })),
|
||||
loginProvider: vi.fn(() => Promise.resolve({ url: "https://auth.example.com/login" })),
|
||||
logoutProvider: vi.fn(() => Promise.resolve({ success: true })),
|
||||
fetchModels: vi.fn(() => Promise.resolve([
|
||||
{ provider: "anthropic", id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", reasoning: true, contextWindow: 200000 },
|
||||
{ provider: "openai", id: "gpt-4o", name: "GPT-4o", reasoning: false, contextWindow: 128000 },
|
||||
])),
|
||||
fetchModels: vi.fn(() => Promise.resolve({
|
||||
models: [
|
||||
{ provider: "anthropic", id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5", reasoning: true, contextWindow: 200000 },
|
||||
{ provider: "openai", id: "gpt-4o", name: "GPT-4o", reasoning: false, contextWindow: 128000 },
|
||||
],
|
||||
favoriteProviders: [],
|
||||
favoriteModels: [],
|
||||
})),
|
||||
testNtfyNotification: vi.fn(() => Promise.resolve({ success: true })),
|
||||
}));
|
||||
|
||||
@@ -579,7 +583,7 @@ describe("SettingsModal", () => {
|
||||
});
|
||||
|
||||
it("shows empty state when no models available", async () => {
|
||||
(fetchModels as ReturnType<typeof vi.fn>).mockResolvedValueOnce([]);
|
||||
(fetchModels as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ models: [], favoriteProviders: [], favoriteModels: [] });
|
||||
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
@@ -649,7 +653,7 @@ describe("SettingsModal", () => {
|
||||
});
|
||||
|
||||
it("shows empty state in Execution Model section when no models available", async () => {
|
||||
(fetchModels as ReturnType<typeof vi.fn>).mockResolvedValueOnce([]);
|
||||
(fetchModels as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ models: [], favoriteProviders: [], favoriteModels: [] });
|
||||
|
||||
render(<SettingsModal onClose={onClose} addToast={addToast} />);
|
||||
await waitFor(() => expect(fetchSettings).toHaveBeenCalled());
|
||||
@@ -1493,7 +1497,7 @@ describe("SettingsModal", () => {
|
||||
fireEvent.click(testButton);
|
||||
|
||||
await waitFor(() => expect(testNtfyNotification).toHaveBeenCalledTimes(1));
|
||||
expect(testNtfyNotification).toHaveBeenCalledWith({ ntfyEnabled: true, ntfyTopic: "my-valid-topic" });
|
||||
expect(testNtfyNotification).toHaveBeenCalledWith({ ntfyEnabled: true, ntfyTopic: "my-valid-topic" }, undefined);
|
||||
});
|
||||
|
||||
it("Success toast is shown when test notification succeeds", async () => {
|
||||
|
||||
@@ -621,7 +621,7 @@ describe("TaskCard clickable dependencies", () => {
|
||||
fireEvent.click(depBadge);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalledWith("FN-001");
|
||||
expect(mockFetch).toHaveBeenCalledWith("FN-001", undefined);
|
||||
expect(onOpenDetail).toHaveBeenCalledWith(mockDetail);
|
||||
});
|
||||
});
|
||||
@@ -2384,7 +2384,7 @@ describe("TaskCard detail opening", () => {
|
||||
fireEvent.click(cardTitle);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalledWith("FN-099");
|
||||
expect(mockFetch).toHaveBeenCalledWith("FN-099", undefined);
|
||||
expect(onOpenDetail).toHaveBeenCalledWith(mockDetail);
|
||||
});
|
||||
});
|
||||
@@ -2419,7 +2419,7 @@ describe("TaskCard detail opening", () => {
|
||||
fireEvent.click(screen.getByText("Test task"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalledWith("FN-099");
|
||||
expect(mockFetch).toHaveBeenCalledWith("FN-099", undefined);
|
||||
expect(onOpenDetail).toHaveBeenCalledWith(mockDetail);
|
||||
expect(onOpenDetail).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ describe("TaskComments", () => {
|
||||
fireEvent.change(screen.getByPlaceholderText(/Add a comment/), { target: { value: "Hello" } });
|
||||
fireEvent.click(screen.getByText("Add Comment"));
|
||||
|
||||
await waitFor(() => expect(addTaskComment).toHaveBeenCalledWith("FN-001", "Hello", "user"));
|
||||
await waitFor(() => expect(addTaskComment).toHaveBeenCalledWith("FN-001", "Hello", "user", undefined));
|
||||
expect(onTaskUpdated).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -55,7 +55,7 @@ describe("TaskComments", () => {
|
||||
fireEvent.change(screen.getByDisplayValue("Original"), { target: { value: "Updated" } });
|
||||
fireEvent.click(screen.getByText("Save"));
|
||||
|
||||
await waitFor(() => expect(updateTaskComment).toHaveBeenCalledWith("FN-001", "c1", "Updated"));
|
||||
await waitFor(() => expect(updateTaskComment).toHaveBeenCalledWith("FN-001", "c1", "Updated", undefined));
|
||||
expect(onTaskUpdated).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -66,7 +66,7 @@ describe("TaskComments", () => {
|
||||
render(<TaskComments task={makeTask({ comments: [{ id: "c1", text: "Original", author: "user", createdAt: "2026-01-01T00:00:00.000Z" }] })} addToast={vi.fn()} onTaskUpdated={onTaskUpdated} />);
|
||||
fireEvent.click(screen.getByText("Delete"));
|
||||
|
||||
await waitFor(() => expect(deleteTaskComment).toHaveBeenCalledWith("FN-001", "c1"));
|
||||
await waitFor(() => expect(deleteTaskComment).toHaveBeenCalledWith("FN-001", "c1", undefined));
|
||||
expect(onTaskUpdated).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -170,7 +170,7 @@ describe("TaskComments", () => {
|
||||
fireEvent.click(screen.getByText("Add Guidance"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addSteeringComment).toHaveBeenCalledWith("FN-001", "Guidance text");
|
||||
expect(addSteeringComment).toHaveBeenCalledWith("FN-001", "Guidance text", undefined);
|
||||
expect(addTaskComment).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -187,7 +187,7 @@ describe("TaskComments", () => {
|
||||
fireEvent.click(screen.getByText("Add Comment"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addTaskComment).toHaveBeenCalledWith("FN-001", "User text", "user");
|
||||
expect(addTaskComment).toHaveBeenCalledWith("FN-001", "User text", "user", undefined);
|
||||
expect(addSteeringComment).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -252,7 +252,7 @@ describe("TaskComments", () => {
|
||||
fireEvent.keyDown(textarea, { key: "Enter", ctrlKey: true });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addTaskComment).toHaveBeenCalledWith("FN-001", "Keyboard", "user");
|
||||
expect(addTaskComment).toHaveBeenCalledWith("FN-001", "Keyboard", "user", undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -268,7 +268,7 @@ describe("TaskComments", () => {
|
||||
fireEvent.keyDown(textarea, { key: "Enter", metaKey: true });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(addTaskComment).toHaveBeenCalledWith("FN-001", "Mac", "user");
|
||||
expect(addTaskComment).toHaveBeenCalledWith("FN-001", "Mac", "user", undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -302,7 +302,7 @@ describe("TaskDetailModal", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpload).toHaveBeenCalledWith("FN-099", imageFile);
|
||||
expect(mockUpload).toHaveBeenCalledWith("FN-099", imageFile, undefined);
|
||||
expect(addToast).toHaveBeenCalledWith("Screenshot attached", "success");
|
||||
});
|
||||
});
|
||||
@@ -432,7 +432,7 @@ describe("TaskDetailModal", () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpload).toHaveBeenCalledWith("FN-099", imageFile);
|
||||
expect(mockUpload).toHaveBeenCalledWith("FN-099", imageFile, undefined);
|
||||
expect(addToast).toHaveBeenCalledWith("Screenshot attached", "success");
|
||||
});
|
||||
});
|
||||
@@ -502,7 +502,7 @@ describe("TaskDetailModal", () => {
|
||||
fireEvent.click(screen.getByText("FN-001"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateTask).toHaveBeenCalledWith("FN-099", { dependencies: ["FN-001"] });
|
||||
expect(updateTask).toHaveBeenCalledWith("FN-099", { dependencies: ["FN-001"] }, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -525,7 +525,7 @@ describe("TaskDetailModal", () => {
|
||||
fireEvent.click(removeButtons[0]); // Remove KB-001
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateTask).toHaveBeenCalledWith("FN-099", { dependencies: ["FN-002"] });
|
||||
expect(updateTask).toHaveBeenCalledWith("FN-099", { dependencies: ["FN-002"] }, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1438,7 +1438,7 @@ describe("TaskDetailModal", () => {
|
||||
fireEvent.click(depLink);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetch).toHaveBeenCalledWith("FN-001");
|
||||
expect(mockFetch).toHaveBeenCalledWith("FN-001", undefined);
|
||||
expect(onOpenDetail).toHaveBeenCalledWith(mockDetail);
|
||||
});
|
||||
});
|
||||
@@ -1497,7 +1497,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(onOpenDetail).not.toHaveBeenCalled();
|
||||
// updateTask should be called to remove the dependency
|
||||
await waitFor(() => {
|
||||
expect(updateTask).toHaveBeenCalledWith("FN-099", { dependencies: [] });
|
||||
expect(updateTask).toHaveBeenCalledWith("FN-099", { dependencies: [] }, undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1593,7 +1593,7 @@ describe("TaskDetailModal", () => {
|
||||
fireEvent.click(screen.getByText("Save"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdate).toHaveBeenCalledWith("FN-099", { prompt: "# Updated" });
|
||||
expect(mockUpdate).toHaveBeenCalledWith("FN-099", { prompt: "# Updated" }, undefined);
|
||||
});
|
||||
|
||||
// Should return to view mode
|
||||
@@ -1646,7 +1646,7 @@ describe("TaskDetailModal", () => {
|
||||
fireEvent.click(screen.getByText("Request AI Revision"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(requestSpecRevision).toHaveBeenCalledWith("FN-099", "Please add more error handling details");
|
||||
expect(requestSpecRevision).toHaveBeenCalledWith("FN-099", "Please add more error handling details", undefined);
|
||||
expect(addToast).toHaveBeenCalledWith("AI revision requested. Task moved to triage.", "success");
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
@@ -1804,7 +1804,7 @@ describe("TaskDetailModal", () => {
|
||||
fireEvent.click(screen.getByText("Approve Plan"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockApprovePlan).toHaveBeenCalledWith("FN-001");
|
||||
expect(mockApprovePlan).toHaveBeenCalledWith("FN-001", undefined);
|
||||
});
|
||||
expect(addToast).toHaveBeenCalledWith("Plan approved — FN-001 moved to Todo", "success");
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
@@ -1844,7 +1844,7 @@ describe("TaskDetailModal", () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockRejectPlan).toHaveBeenCalledWith("FN-001");
|
||||
expect(mockRejectPlan).toHaveBeenCalledWith("FN-001", undefined);
|
||||
});
|
||||
expect(addToast).toHaveBeenCalledWith(
|
||||
"Plan rejected — FN-001 returned to Triage for re-specification",
|
||||
@@ -2393,7 +2393,7 @@ describe("TaskDetailModal", () => {
|
||||
fireEvent.click(screen.getByText("Create Refinement Task"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(refineTask).toHaveBeenCalledWith("FN-001", "Need to add more tests");
|
||||
expect(refineTask).toHaveBeenCalledWith("FN-001", "Need to add more tests", undefined);
|
||||
expect(addToast).toHaveBeenCalledWith("Refinement task created: FN-002", "success");
|
||||
expect(onClose).toHaveBeenCalled();
|
||||
});
|
||||
@@ -2597,7 +2597,7 @@ describe("TaskDetailModal", () => {
|
||||
expect(mockUpdate).toHaveBeenCalledWith("FN-001", {
|
||||
title: "New title",
|
||||
description: "New description",
|
||||
});
|
||||
}, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ describe("WorkflowStepManager", () => {
|
||||
description: "New description",
|
||||
prompt: undefined,
|
||||
enabled: true,
|
||||
});
|
||||
}, undefined);
|
||||
expect(addToast).toHaveBeenCalledWith("Workflow step created", "success");
|
||||
});
|
||||
});
|
||||
@@ -167,7 +167,7 @@ describe("WorkflowStepManager", () => {
|
||||
await waitFor(() => {
|
||||
expect(updateWorkflowStep).toHaveBeenCalledWith("WS-001", expect.objectContaining({
|
||||
name: "Updated Name",
|
||||
}));
|
||||
}), undefined);
|
||||
expect(addToast).toHaveBeenCalledWith("Workflow step updated", "success");
|
||||
});
|
||||
});
|
||||
@@ -192,7 +192,7 @@ describe("WorkflowStepManager", () => {
|
||||
fireEvent.click(confirmBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteWorkflowStep).toHaveBeenCalledWith("WS-001");
|
||||
expect(deleteWorkflowStep).toHaveBeenCalledWith("WS-001", undefined);
|
||||
expect(addToast).toHaveBeenCalledWith("Workflow step deleted", "success");
|
||||
});
|
||||
});
|
||||
@@ -216,7 +216,7 @@ describe("WorkflowStepManager", () => {
|
||||
fireEvent.click(refineBtn);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(refineWorkflowStepPrompt).toHaveBeenCalledWith("WS-001");
|
||||
expect(refineWorkflowStepPrompt).toHaveBeenCalledWith("WS-001", undefined);
|
||||
expect(addToast).toHaveBeenCalledWith("Prompt refined with AI", "success");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user