feat(FN-5307): merge fusion/fn-5307

This commit is contained in:
gsxdsm
2026-05-20 09:02:19 -07:00
parent c9ff2078b5
commit 5696823eaa
6 changed files with 288 additions and 102 deletions

View File

@@ -3,8 +3,7 @@ import { useState, useCallback, useEffect, useRef } from "react";
import { DEFAULT_TASK_PRIORITY, type Task, type TaskCreateInput, type TaskPriority } from "@fusion/core";
import { getErrorMessage } from "@fusion/core";
import type { ToastType } from "../hooks/useToast";
import { uploadAttachment, fetchAgents } from "../api";
import type { Agent } from "../api";
import { uploadAttachment } from "../api";
import { Bot } from "lucide-react";
import { useSetupReadiness } from "../hooks/useSetupReadiness";
import { SetupWarningBanner } from "./SetupWarningBanner";
@@ -15,6 +14,7 @@ import { useMobileKeyboard } from "../hooks/useMobileKeyboard";
import { useMobileScrollLock } from "../hooks/useMobileScrollLock";
import { useNodes } from "../hooks/useNodes";
import { useViewportMode } from "../hooks/useViewportMode";
import { useAgentsMapCache } from "../hooks/useAgentsMapCache";
interface NewTaskModalProps {
isOpen: boolean;
@@ -62,9 +62,8 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
// Agent assignment state
const [selectedAgentId, setSelectedAgentId] = useState<string | null>(null);
const [agents, setAgents] = useState<Agent[]>([]);
const { agents, loading: agentsLoading } = useAgentsMapCache(projectId);
const [showAgentPicker, setShowAgentPicker] = useState(false);
const [agentsLoading, setAgentsLoading] = useState(false);
const agentPickerRef = useRef<HTMLDivElement>(null);
// Quick-fields dependency picker state
@@ -88,25 +87,9 @@ export function NewTaskModal({ isOpen, onClose, projectId, tasks, onCreateTask,
}, []);
// Load agents for agent picker
const loadAgents = useCallback(async () => {
if (agents.length > 0) {
setShowAgentPicker(true);
return;
}
setAgentsLoading(true);
try {
const result = await fetchAgents(undefined, projectId);
setAgents(result);
setShowAgentPicker(true);
} catch (err) {
const msg = getErrorMessage(err);
addToast(msg ? `Failed to load agents: ${msg}` : "Failed to load agents", "error");
setShowAgentPicker(false);
} finally {
setAgentsLoading(false);
}
}, [agents.length, projectId, addToast]);
const loadAgents = useCallback(() => {
setShowAgentPicker(true);
}, []);
// Close agent picker when clicking outside
useEffect(() => {

View File

@@ -14,7 +14,7 @@ import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import type { Components } from "react-markdown";
import { ChevronDown, Eye, EyeOff, Hash, MessageSquare, Paperclip, Plus, Send, Square, Wrench, X } from "lucide-react";
import { attachmentBaseUrlForRoom, fetchDiscoveredSkills, fetchModels, type Agent, type ModelInfo } from "../api";
import { attachmentBaseUrlForRoom, type Agent, type ModelInfo } from "../api";
import type { DiscoveredSkill } from "@fusion/dashboard";
import { CustomModelDropdown } from "./CustomModelDropdown";
import { ProviderIcon } from "./ProviderIcon";
@@ -22,6 +22,8 @@ import { AgentMentionPopup } from "./AgentMentionPopup";
import { matchesAgentMentionFilter } from "./mentionMatching";
import { FN_AGENT_ID, useQuickChat, type ChatMessageInfo, type ToolCallInfo } from "../hooks/useQuickChat";
import { useAgents } from "../hooks/useAgents";
import { useModelsCache } from "../hooks/useModelsCache";
import { useDiscoveredSkillsCache } from "../hooks/useDiscoveredSkillsCache";
import { FileMentionPopup } from "./FileMentionPopup";
import { useFileMention } from "../hooks/useFileMention";
import { useMobileKeyboard } from "../hooks/useMobileKeyboard";
@@ -903,6 +905,13 @@ export function QuickChatFAB({
roomContext = null,
}: QuickChatFABProps) {
const { agents } = useAgents(projectId);
const {
models,
defaultProvider,
defaultModelId,
loading: modelsLoading,
} = useModelsCache();
const { skills: discoveredSkills, loading: skillsLoading } = useDiscoveredSkillsCache(projectId);
// Internal state for uncontrolled mode, controlled state when open prop is provided
const [internalOpen, setInternalOpen] = useState(false);
const isControlled = open !== undefined;
@@ -933,13 +942,9 @@ export function QuickChatFAB({
const [newSessionMode, setNewSessionMode] = useState<"agent" | "model">("model");
const [newSessionAgentId, setNewSessionAgentId] = useState<string>("");
const [newSessionModel, setNewSessionModel] = useState<string>("");
const [models, setModels] = useState<ModelInfo[]>([]);
const [modelsLoading, setModelsLoading] = useState(false);
const [selectedModel, setSelectedModel] = useState<string>("");
const [configuredDefaultModelSelection, setConfiguredDefaultModelSelection] = useState<string>("");
const [messageInput, setMessageInput] = useState("");
const [discoveredSkills, setDiscoveredSkills] = useState<DiscoveredSkill[]>([]);
const [skillsLoading, setSkillsLoading] = useState(false);
const [showSkillMenu, setShowSkillMenu] = useState(false);
const [skillFilter, setSkillFilter] = useState("");
const [highlightedSkillIndex, setHighlightedSkillIndex] = useState(0);
@@ -1211,86 +1216,46 @@ export function QuickChatFAB({
}
}, [agents, hasPersistedAgentSessionSelection, selectedAgentId]);
// Lazy-load models on first panel open.
useEffect(() => {
if (!isOpen || modelsRequestedRef.current) {
if (!isOpen) {
return;
}
modelsRequestedRef.current = true;
modelsInitSettledRef.current = false;
setModelsLoading(true);
if (!modelsRequestedRef.current) {
modelsRequestedRef.current = true;
modelsInitSettledRef.current = false;
}
fetchModels()
.then((response) => {
const loadedModels = response.models ?? [];
setModels(loadedModels);
if (modelsLoading || !modelsRequestedRef.current || modelsInitSettledRef.current) {
return;
}
if (selectedModelRef.current || loadedModels.length === 0) {
if (!selectedModelRef.current && models.length > 0) {
if (defaultProvider && defaultModelId) {
const defaultSelection = `${defaultProvider}/${defaultModelId}`;
const hasDefaultModel = models.some((model) => `${model.provider}/${model.id}` === defaultSelection);
if (hasDefaultModel) {
setConfiguredDefaultModelSelection(defaultSelection);
if (!selectedModelRef.current) {
setSelectedModel(defaultSelection);
}
if (!hasAppliedInitialSessionRef.current) {
setChatMode("model");
}
modelsInitSettledRef.current = true;
return;
}
}
const defaultProvider = response.defaultProvider;
const defaultModelId = response.defaultModelId;
if (defaultProvider && defaultModelId) {
const defaultSelection = `${defaultProvider}/${defaultModelId}`;
const hasDefaultModel = loadedModels.some(
(model) => `${model.provider}/${model.id}` === defaultSelection,
);
if (hasDefaultModel) {
setConfiguredDefaultModelSelection(defaultSelection);
if (!selectedModelRef.current) {
setSelectedModel(defaultSelection);
}
// Switch to model mode regardless of whether agents are present —
// a configured default model is an explicit user preference and
// should drive the panel to its corresponding mode immediately,
// otherwise the tag/dropdown auto-selection would be invisible
// until the user manually toggles modes.
if (!hasAppliedInitialSessionRef.current) {
setChatMode("model");
}
return;
}
}
setConfiguredDefaultModelSelection("");
// Always pre-select the first model so users can start chatting in model mode
// without having to manually pick from the dropdown.
const firstModel = loadedModels[0];
if (firstModel && !selectedModelRef.current) {
setSelectedModel(`${firstModel.provider}/${firstModel.id}`);
}
})
.catch((error: unknown) => {
console.error("[QuickChatFAB] Failed to load models:", error);
setModels([]);
setConfiguredDefaultModelSelection("");
})
.finally(() => {
modelsInitSettledRef.current = true;
setModelsLoading(false);
});
}, [isOpen, agents.length, selectedModel]);
useEffect(() => {
if (!isOpen || !projectId) {
return;
setConfiguredDefaultModelSelection("");
const firstModel = models[0];
if (firstModel && !selectedModelRef.current) {
setSelectedModel(`${firstModel.provider}/${firstModel.id}`);
}
}
setSkillsLoading(true);
fetchDiscoveredSkills(projectId)
.then((skills) => {
setDiscoveredSkills(skills);
})
.catch(() => {
setDiscoveredSkills([]);
})
.finally(() => {
setSkillsLoading(false);
});
}, [isOpen, projectId]);
modelsInitSettledRef.current = true;
}, [defaultModelId, defaultProvider, isOpen, models, modelsLoading]);
useEffect(() => {
if (!isOpen) return;

View File

@@ -0,0 +1,105 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { NewTaskModal } from "../NewTaskModal";
import { useAgentsMapCache } from "../../hooks/useAgentsMapCache";
import { writeCache, SWR_CACHE_KEYS } from "../../utils/swrCache";
const mockFetchAgents = vi.fn();
vi.mock("../../api", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../api")>();
return {
...actual,
fetchAgents: (...args: unknown[]) => mockFetchAgents(...args),
uploadAttachment: vi.fn().mockResolvedValue({ attachment: null }),
};
});
vi.mock("../../hooks/useSetupReadiness", () => ({ useSetupReadiness: vi.fn(() => ({ hasAiProvider: true, hasGithub: true, loading: false })) }));
vi.mock("../../hooks/useConfirm", () => ({ useConfirm: vi.fn(() => ({ confirm: vi.fn().mockResolvedValue(true) })) }));
vi.mock("../../hooks/useMobileKeyboard", () => ({ useMobileKeyboard: vi.fn(() => ({ keyboardOverlap: 0, viewportHeight: null, viewportOffsetTop: 0, keyboardOpen: false })) }));
vi.mock("../../hooks/useMobileScrollLock", () => ({ useMobileScrollLock: vi.fn() }));
vi.mock("../../hooks/useNodes", () => ({ useNodes: vi.fn(() => ({ nodes: [] })) }));
vi.mock("../../hooks/useViewportMode", () => ({ useViewportMode: vi.fn(() => "desktop") }));
function deferred<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((res) => {
resolve = res;
});
return { promise, resolve };
}
describe("NewTaskModal shared cache", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
mockFetchAgents.mockResolvedValue([]);
});
const baseProps = {
isOpen: true,
projectId: "p1",
tasks: [],
onCreateTask: vi.fn(),
addToast: vi.fn(),
onClose: vi.fn(),
};
it("shows cached agents without cold fetch", () => {
writeCache(`${SWR_CACHE_KEYS.CHAT_AGENTS_MAP_PREFIX}p1`, [
{ id: "agent-1", name: "Agent One", role: "executor", state: "active" },
{ id: "agent-2", name: "Agent Two", role: "reviewer", state: "active" },
], { maxBytes: 500_000 });
render(<NewTaskModal {...baseProps} />);
fireEvent.click(screen.getByTestId("new-task-agent-button"));
expect(screen.getByText("Agent One")).toBeInTheDocument();
expect(screen.getByText("Agent Two")).toBeInTheDocument();
});
it("reuses warm cache across remounts", () => {
writeCache(`${SWR_CACHE_KEYS.CHAT_AGENTS_MAP_PREFIX}p1`, [
{ id: "agent-1", name: "Agent One", role: "executor", state: "active" },
], { maxBytes: 500_000 });
const first = render(<NewTaskModal {...baseProps} />);
first.unmount();
render(<NewTaskModal {...baseProps} />);
expect(mockFetchAgents.mock.calls.length).toBeLessThanOrEqual(1);
});
it("dedups agent fetch with another useAgentsMapCache consumer", () => {
const request = deferred<Array<{ id: string; name: string; role: string; state: string }>>();
mockFetchAgents.mockReturnValue(request.promise);
function AgentsConsumer() {
useAgentsMapCache("p1");
return null;
}
render(
<>
<NewTaskModal {...baseProps} />
<AgentsConsumer />
</>,
);
expect(mockFetchAgents).toHaveBeenCalledTimes(1);
request.resolve([]);
});
it("opens picker synchronously on cache hit", () => {
writeCache(`${SWR_CACHE_KEYS.CHAT_AGENTS_MAP_PREFIX}p1`, [
{ id: "agent-1", name: "Agent One", role: "executor", state: "active" },
], { maxBytes: 500_000 });
render(<NewTaskModal {...baseProps} />);
fireEvent.click(screen.getByTestId("new-task-agent-button"));
expect(screen.getByText("Select agent")).toBeInTheDocument();
expect(screen.queryByText("Loading agents...")).toBeNull();
});
});

View File

@@ -0,0 +1,133 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { fireEvent, render, screen } from "@testing-library/react";
import { QuickChatFAB } from "../QuickChatFAB";
import { useModelsCache } from "../../hooks/useModelsCache";
import { writeCache, SWR_CACHE_KEYS } from "../../utils/swrCache";
const mockFetchModels = vi.fn();
const mockFetchDiscoveredSkills = vi.fn();
const mockUseAgents = vi.fn();
vi.mock("../../api", async (importOriginal) => {
const actual = await importOriginal<typeof import("../../api")>();
return {
...actual,
fetchModels: (...args: unknown[]) => mockFetchModels(...args),
fetchDiscoveredSkills: (...args: unknown[]) => mockFetchDiscoveredSkills(...args),
fetchTasks: vi.fn().mockResolvedValue([]),
searchFiles: vi.fn().mockResolvedValue({ files: [] }),
};
});
vi.mock("../../hooks/useAgents", () => ({ useAgents: (...args: unknown[]) => mockUseAgents(...args) }));
vi.mock("../../hooks/useQuickChat", () => ({
FN_AGENT_ID: "__fn_agent__",
useQuickChat: vi.fn(() => ({
activeSession: null,
messages: [],
isStreaming: false,
streamingText: "",
streamingThinking: null,
streamingToolCalls: [],
sessions: [],
sessionsLoading: false,
messagesLoading: false,
sendMessage: vi.fn(),
stopStreaming: vi.fn(),
pendingMessage: "",
clearPendingMessage: vi.fn(),
switchSession: vi.fn(),
selectSession: vi.fn(),
startModelChat: vi.fn(),
startFreshSession: vi.fn(),
refreshSessions: vi.fn(),
skipNextSessionInitRef: { current: false },
})),
}));
vi.mock("../../hooks/useFileMention", () => ({ useFileMention: vi.fn(() => ({ mentionActive: false, detectMention: vi.fn(), dismissMention: vi.fn(), handleKeyDown: vi.fn(), selectTask: vi.fn(), selectFile: vi.fn(), tasks: [], files: [], combinedItems: [], loading: false, mentionQuery: "", selectedIndex: 0, setSelectedIndex: vi.fn() })) }));
vi.mock("../../hooks/useMobileKeyboard", () => ({ useMobileKeyboard: vi.fn(() => ({ keyboardOpen: false, keyboardOverlap: 0, viewportHeight: null, viewportOffsetTop: 0 })) }));
vi.mock("../../hooks/useViewportMode", () => ({ useViewportMode: vi.fn(() => "desktop") }));
vi.mock("react-markdown", () => ({ default: ({ children }: { children: string }) => children }));
function deferred<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((res) => {
resolve = res;
});
return { promise, resolve };
}
describe("QuickChatFAB shared cache", () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
mockUseAgents.mockReturnValue({ agents: [{ id: "agent-1", name: "Agent One", role: "executor", state: "active" }], activeAgents: [], stats: null, isLoading: false, loadAgents: vi.fn(), loadStats: vi.fn() });
mockFetchModels.mockResolvedValue({ models: [], favoriteProviders: [], favoriteModels: [], defaultProvider: null, defaultModelId: null });
mockFetchDiscoveredSkills.mockResolvedValue([]);
});
it("uses cached models and selects configured default model", () => {
writeCache(SWR_CACHE_KEYS.MODELS, {
models: [
{ provider: "openai", id: "gpt-4o", name: "GPT-4o" },
{ provider: "anthropic", id: "claude-3-7-sonnet", name: "Claude" },
],
favoriteProviders: [],
favoriteModels: [],
defaultProvider: "openai",
defaultModelId: "gpt-4o",
}, { maxBytes: 500_000 });
render(<QuickChatFAB addToast={vi.fn()} projectId="p1" open />);
expect(screen.getByTestId("quick-chat-model-tag")).toHaveTextContent("GPT-4o");
});
it("shows cached discovered skills immediately after slash trigger", () => {
writeCache(`${SWR_CACHE_KEYS.DISCOVERED_SKILLS_PREFIX}p1`, [
{ id: "s1", name: "fusion-basics", relativePath: "skills/fusion-basics", source: "acme/skills" },
{ id: "s2", name: "deploy-helper", relativePath: "skills/deploy-helper", source: "acme/skills" },
], { maxBytes: 500_000 });
render(<QuickChatFAB addToast={vi.fn()} projectId="p1" open />);
fireEvent.change(screen.getByTestId("quick-chat-input"), { target: { value: "/" } });
expect(screen.getByTestId("quick-chat-skill-menu")).toHaveTextContent("fusion-basics");
expect(screen.getByTestId("quick-chat-skill-menu")).toHaveTextContent("deploy-helper");
});
it("dedups model fetch with another useModelsCache consumer", () => {
const request = deferred<{ models: unknown[]; favoriteProviders: string[]; favoriteModels: string[]; defaultProvider: string | null; defaultModelId: string | null }>();
mockFetchModels.mockReturnValue(request.promise);
function ModelsConsumer() {
useModelsCache();
return null;
}
render(
<>
<QuickChatFAB addToast={vi.fn()} projectId="p1" open />
<ModelsConsumer />
</>,
);
expect(mockFetchModels).toHaveBeenCalledTimes(1);
request.resolve({ models: [], favoriteProviders: [], favoriteModels: [], defaultProvider: null, defaultModelId: null });
});
it("keeps agent mode when no configured default model exists", () => {
writeCache(SWR_CACHE_KEYS.MODELS, {
models: [{ provider: "openai", id: "gpt-4o", name: "GPT-4o" }],
favoriteProviders: [],
favoriteModels: [],
defaultProvider: null,
defaultModelId: null,
}, { maxBytes: 500_000 });
render(<QuickChatFAB addToast={vi.fn()} projectId="p1" open />);
expect(screen.queryByTestId("quick-chat-model-tag")).toBeNull();
expect(screen.getByTestId("quick-chat-session-dropdown-trigger")).toHaveTextContent("Select a session");
});
});

View File

@@ -1235,7 +1235,7 @@ describe("QuickChatFAB session-first UX", () => {
});
it("renders non-member mention chips when roomContext is provided", async () => {
mockFetchChatMessages.mockResolvedValueOnce({
mockFetchChatMessages.mockResolvedValue({
messages: [
{
id: "msg-room-mention",
@@ -1410,7 +1410,7 @@ describe("QuickChatFAB session-first UX", () => {
it("linkifies file paths in markdown assistant messages", async () => {
const openFile = vi.fn();
mockFetchChatMessages.mockResolvedValueOnce({
mockFetchChatMessages.mockResolvedValue({
messages: [{ id: "msg-path", sessionId: "session-model", role: "assistant", content: "See packages/dashboard/app/App.tsx:9", createdAt: new Date().toISOString() }],
});
@@ -1429,7 +1429,7 @@ describe("QuickChatFAB session-first UX", () => {
it("linkifies file paths in plain-text render mode", async () => {
const openFile = vi.fn();
mockFetchChatMessages.mockResolvedValueOnce({
mockFetchChatMessages.mockResolvedValue({
messages: [{ id: "msg-plain", sessionId: "session-model", role: "assistant", content: "Check packages/dashboard/app/components/QuickChatFAB.tsx", createdAt: new Date().toISOString() }],
});
@@ -1562,7 +1562,7 @@ describe("QuickChatFAB session-first UX", () => {
messagesLoading: false,
selectRoom, createRoom: vi.fn(), deleteRoom: vi.fn(), sendRoomMessage: vi.fn(), clearRoom: vi.fn(), refreshRooms: vi.fn(),
});
mockFetchChatMessages.mockResolvedValueOnce({
mockFetchChatMessages.mockResolvedValue({
messages: [{ id: "session-msg", sessionId: "session-model", role: "assistant", content: "hello from session", createdAt: "2026-05-16T00:00:00.000Z" }],
});

View File

@@ -12,7 +12,7 @@ const qualityAppTests = [
"app/api/**/*.test.ts",
// Representative workflow/component coverage. Exhaustive modal/view suites
// stay available in the full `dashboard-app` project.
"app/components/__tests__/{ActiveAgentsPanel,ActivityLogModal,AgentMentionPopup,AgentMetricsBar,AgentOnboardingModal,AgentReflectionsTab,AgentTokenStatsPanel,App,AuthTokenRecoveryDialog,Board,board-mobile,board-mobile-view-switch,ChatView,ChatView.autosize,ChatView.chat-input-autosize,ChatView.default-model-icon,ChatView.draft,ChatView.hash-mention,ChatView.rooms,ChatView.swipe-back,Column,ConfirmDialog,ConversationHistory,DashboardLoader,DevServerView.mobile,DirectoryPicker,DuplicateWarningModal,ErrorBoundary,ExecutorStatusBar,FileBrowser,FileEditor,GitHubBadge,InlineCreateCard,LoginInstructions,MemoryView,MessageComposer,MessageComposer.autosize,MobileNavBar,NewTaskModal,NodeCard,NodeHealthDot,NodeStatusIndicator,PlanningModeModal.autosize,PrChecksList,PrCreateModal,PrCreateModal.layout,ProjectCard,ProjectSelector,ProviderIcon,PrPanel,PrPanel.merge,PrPanel.reviews,QuickChatFAB,ReliabilityView,ResearchView,SecretsView,SecretsView.mobile,SettingsModal,SettingsModal.worktrunk,StashRecoveryView,TaskCard,TaskCard.badge-height,TaskCard.badge-wrap,TaskCard.footer-wrap,TaskChangesTab,TaskComments,TaskDetailModal,TaskDetailModal.create-pr-e2e,TaskDetailModal.create-pr-integration,TaskDetailModal.github-tracking-header,TaskDetailModal.github-tracking-stale,TaskDetailModal.rebind-banner,TaskDocumentsTab,TaskForm,TaskIdIntegrityBanner,TrackingRepoSelect,WorkflowResultsTab,WorktrunkInstallApprovalDetails}.test.tsx",
"app/components/__tests__/{ActiveAgentsPanel,ActivityLogModal,AgentMentionPopup,AgentMetricsBar,AgentOnboardingModal,AgentReflectionsTab,AgentTokenStatsPanel,App,AuthTokenRecoveryDialog,Board,board-mobile,board-mobile-view-switch,ChatView,ChatView.autosize,ChatView.chat-input-autosize,ChatView.default-model-icon,ChatView.draft,ChatView.hash-mention,ChatView.rooms,ChatView.swipe-back,Column,ConfirmDialog,ConversationHistory,DashboardLoader,DevServerView.mobile,DirectoryPicker,DuplicateWarningModal,ErrorBoundary,ExecutorStatusBar,FileBrowser,FileEditor,GitHubBadge,InlineCreateCard,LoginInstructions,MemoryView,MessageComposer,MessageComposer.autosize,MobileNavBar,NewTaskModal,NewTaskModal.shared-cache,NodeCard,NodeHealthDot,NodeStatusIndicator,PlanningModeModal.autosize,PrChecksList,PrCreateModal,PrCreateModal.layout,ProjectCard,ProjectSelector,ProviderIcon,PrPanel,PrPanel.merge,PrPanel.reviews,QuickChatFAB,QuickChatFAB.shared-cache,ReliabilityView,ResearchView,SecretsView,SecretsView.mobile,SettingsModal,SettingsModal.worktrunk,StashRecoveryView,TaskCard,TaskCard.badge-height,TaskCard.badge-wrap,TaskCard.footer-wrap,TaskChangesTab,TaskComments,TaskDetailModal,TaskDetailModal.create-pr-e2e,TaskDetailModal.create-pr-integration,TaskDetailModal.github-tracking-header,TaskDetailModal.github-tracking-stale,TaskDetailModal.rebind-banner,TaskDocumentsTab,TaskForm,TaskIdIntegrityBanner,TrackingRepoSelect,WorkflowResultsTab,WorktrunkInstallApprovalDetails}.test.tsx",
// Hooks and utilities are fast, user-visible state/formatting behavior.
"app/context/**/*.test.tsx",
"app/hooks/__tests__/{useAgents,useAgentLogs,useAppSettings,useAuthOnboarding,useConfirm,useCurrentProject,useNodes,useNodeSettingsSync,useProjects,useQuickChat,useTasks,useTerminalSessions,useTheme,useToast,useUsageData,useViewState}.test.{ts,tsx}",