feat(FN-5307): merge fusion/fn-5307
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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" }],
|
||||
});
|
||||
|
||||
|
||||
@@ -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}",
|
||||
|
||||
Reference in New Issue
Block a user