fix(FN-000): scope dashboard project flows

This commit is contained in:
gsxdsm
2026-04-02 17:51:04 -07:00
parent bf12c22efb
commit 05f2114743
63 changed files with 1332 additions and 904 deletions

View File

@@ -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) {

View File

@@ -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>
);
}
}

View File

@@ -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}
/>

View File

@@ -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}

View File

@@ -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;

View File

@@ -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}

View File

@@ -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}
/>
)}

View File

@@ -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);
}

View File

@@ -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(

View File

@@ -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);

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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(() => {

View File

@@ -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) {

View File

@@ -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 }));
}}
/>

View File

@@ -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();
});

View File

@@ -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();

View File

@@ -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",

View File

@@ -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(() => {

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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}

View File

@@ -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);
});
});

View File

@@ -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);
});
});
});

View File

@@ -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(

View File

@@ -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 />);

View File

@@ -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} />);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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");
});
});

View File

@@ -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 () => {

View File

@@ -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);
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});

View File

@@ -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");
});
});