FN-5854: preselect default model for new chats
Preselect the resolved default model when creating a new model-based chat. - pass the resolved default model into the new chat dialog - preserve and restore the default model selection when switching into model mode - add ChatView coverage for default-model preselection, create enablement, and unresolved-default fallback Files changed: packages/dashboard/app/components/ChatView.tsx | 18 +++++- packages/dashboard/app/components/__tests__/ChatView.test.tsx | 65 ++++++++++++++++++++-- 2 files changed, 75 insertions(+), 8 deletions(-) Fusion-Task-Id: FN-5854 Fusion-Task-Lineage: d1ace008-0354-44b4-90c8-e0370f38b6bf
This commit is contained in:
@@ -519,16 +519,20 @@ export function resolveSessionProvider(
|
||||
|
||||
interface NewChatDialogProps {
|
||||
projectId?: string;
|
||||
defaultModel: DefaultModelSelection;
|
||||
onClose: () => void;
|
||||
onCreate: (input: { agentId: string; modelProvider?: string; modelId?: string }) => void;
|
||||
}
|
||||
|
||||
function NewChatDialog({ projectId, onClose, onCreate }: NewChatDialogProps) {
|
||||
function NewChatDialog({ projectId, defaultModel, onClose, onCreate }: NewChatDialogProps) {
|
||||
const [chatMode, setChatMode] = useState<"agent" | "model">("agent");
|
||||
const { agents, loading: agentsLoading } = useAgentsMapCache(projectId);
|
||||
const [selectedAgentId, setSelectedAgentId] = useState<string>("");
|
||||
const { models, favoriteProviders: cachedFavoriteProviders, favoriteModels: cachedFavoriteModels, loading: modelsLoading, refresh } = useModelsCache();
|
||||
const [selectedModel, setSelectedModel] = useState<string>("");
|
||||
const defaultModelValue = defaultModel.provider && defaultModel.modelId
|
||||
? `${defaultModel.provider}/${defaultModel.modelId}`
|
||||
: "";
|
||||
const [selectedModel, setSelectedModel] = useState<string>(defaultModelValue);
|
||||
const [favoriteProviders, setFavoriteProviders] = useState<string[]>(cachedFavoriteProviders);
|
||||
const [favoriteModels, setFavoriteModels] = useState<string[]>(cachedFavoriteModels);
|
||||
|
||||
@@ -540,6 +544,13 @@ function NewChatDialog({ projectId, onClose, onCreate }: NewChatDialogProps) {
|
||||
setFavoriteModels(cachedFavoriteModels);
|
||||
}, [cachedFavoriteModels]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!defaultModelValue) {
|
||||
return;
|
||||
}
|
||||
setSelectedModel((current) => current || defaultModelValue);
|
||||
}, [defaultModelValue]);
|
||||
|
||||
const handleToggleFavorite = useCallback(async (provider: string) => {
|
||||
const currentFavorites = favoriteProviders;
|
||||
const isFavorite = currentFavorites.includes(provider);
|
||||
@@ -606,7 +617,6 @@ function NewChatDialog({ projectId, onClose, onCreate }: NewChatDialogProps) {
|
||||
data-testid="chat-new-dialog-mode-agent"
|
||||
onClick={() => {
|
||||
setChatMode("agent");
|
||||
setSelectedModel("");
|
||||
}}
|
||||
>
|
||||
Agent
|
||||
@@ -618,6 +628,7 @@ function NewChatDialog({ projectId, onClose, onCreate }: NewChatDialogProps) {
|
||||
onClick={() => {
|
||||
setChatMode("model");
|
||||
setSelectedAgentId("");
|
||||
setSelectedModel((current) => current || defaultModelValue);
|
||||
}}
|
||||
>
|
||||
Model
|
||||
@@ -3511,6 +3522,7 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
|
||||
{showNewDialog && (
|
||||
<NewChatDialog
|
||||
projectId={projectId}
|
||||
defaultModel={defaultModel}
|
||||
onClose={() => setShowNewDialog(false)}
|
||||
onCreate={handleCreateSession}
|
||||
/>
|
||||
|
||||
@@ -35,6 +35,7 @@ vi.mock("../../hooks/useNavigationHistory", async (importOriginal) => {
|
||||
|
||||
const mockUseChat = vi.mocked(useChatModule.useChat);
|
||||
const mockUseChatRooms = vi.mocked(useChatRoomsModule.useChatRooms);
|
||||
const mockFetchModels = vi.mocked(apiModule.fetchModels);
|
||||
const mockFetchDiscoveredSkills = vi.mocked(apiModule.fetchDiscoveredSkills);
|
||||
const mockCreateObjectURL = vi.fn();
|
||||
const mockRevokeObjectURL = vi.fn();
|
||||
@@ -89,6 +90,17 @@ vi.mock("../CustomModelDropdown", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
const defaultModelsResponse = {
|
||||
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: [],
|
||||
defaultProvider: "anthropic",
|
||||
defaultModelId: "claude-sonnet-4-5",
|
||||
};
|
||||
|
||||
// Mock fetchAgents for new chat dialog
|
||||
vi.mock("../../api", () => ({
|
||||
fetchModels: vi.fn().mockResolvedValue({
|
||||
@@ -98,6 +110,8 @@ vi.mock("../../api", () => ({
|
||||
],
|
||||
favoriteProviders: [],
|
||||
favoriteModels: [],
|
||||
defaultProvider: "anthropic",
|
||||
defaultModelId: "claude-sonnet-4-5",
|
||||
}),
|
||||
fetchAgents: vi.fn().mockResolvedValue([
|
||||
{ id: "agent-001", name: "Alpha", role: "executor", state: "idle", icon: undefined, createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z", metadata: {} },
|
||||
@@ -278,6 +292,7 @@ beforeEach(() => {
|
||||
_resetInitialViewportHeight();
|
||||
setupMockRooms();
|
||||
mockViewportMode("desktop");
|
||||
mockFetchModels.mockResolvedValue({ ...defaultModelsResponse });
|
||||
mockFetchDiscoveredSkills.mockResolvedValue([]);
|
||||
mockCreateObjectURL.mockImplementation((file: File) => `blob:${file.name}`);
|
||||
Object.defineProperty(URL, "createObjectURL", { value: mockCreateObjectURL, writable: true });
|
||||
@@ -420,7 +435,25 @@ describe("ChatView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("creates session with model selection (model mode uses KB agent ID)", async () => {
|
||||
it("preselects the default model and enables Create in model mode", async () => {
|
||||
setupMockChat({ sessions: [], filteredSessions: [] });
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
await userEvent.click(screen.getByTestId("chat-new-btn"));
|
||||
|
||||
const dialog = document.querySelector(".chat-new-dialog") as HTMLElement | null;
|
||||
const createBtn = within(dialog!).getByText("Create") as HTMLButtonElement;
|
||||
|
||||
await userEvent.click(within(dialog!).getByTestId("chat-new-dialog-mode-model"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(within(dialog!).getByTestId("mock-model-dropdown")).toHaveValue("anthropic/claude-sonnet-4-5");
|
||||
});
|
||||
expect(createBtn).toBeEnabled();
|
||||
});
|
||||
|
||||
it("creates session with the preselected default model in model mode", async () => {
|
||||
const createSession = vi.fn().mockResolvedValue({ id: "session-new", agentId: "__fn_agent__" });
|
||||
setupMockChat({ sessions: [], filteredSessions: [], createSession });
|
||||
|
||||
@@ -430,12 +463,11 @@ describe("ChatView", () => {
|
||||
|
||||
const dialog = document.querySelector(".chat-new-dialog") as HTMLElement | null;
|
||||
|
||||
// Switch to model mode
|
||||
await userEvent.click(within(dialog!).getByTestId("chat-new-dialog-mode-model"));
|
||||
|
||||
// Select a model from the dropdown (now visible in model mode)
|
||||
const modelDropdown = within(dialog!).getByTestId("mock-model-dropdown");
|
||||
await userEvent.selectOptions(modelDropdown, "anthropic/claude-sonnet-4-5");
|
||||
await waitFor(() => {
|
||||
expect(within(dialog!).getByTestId("mock-model-dropdown")).toHaveValue("anthropic/claude-sonnet-4-5");
|
||||
});
|
||||
|
||||
await userEvent.click(within(dialog!).getByText("Create"));
|
||||
|
||||
@@ -448,6 +480,29 @@ describe("ChatView", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps Create disabled in model mode when no default model is resolvable", async () => {
|
||||
mockFetchModels.mockResolvedValue({
|
||||
...defaultModelsResponse,
|
||||
defaultProvider: null,
|
||||
defaultModelId: null,
|
||||
});
|
||||
setupMockChat({ sessions: [], filteredSessions: [] });
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
await userEvent.click(screen.getByTestId("chat-new-btn"));
|
||||
|
||||
const dialog = document.querySelector(".chat-new-dialog") as HTMLElement | null;
|
||||
const createBtn = within(dialog!).getByText("Create") as HTMLButtonElement;
|
||||
|
||||
await userEvent.click(within(dialog!).getByTestId("chat-new-dialog-mode-model"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(within(dialog!).getByTestId("mock-model-dropdown")).toHaveValue("");
|
||||
});
|
||||
expect(createBtn).toBeDisabled();
|
||||
});
|
||||
|
||||
it("creates session without model selection omits model fields (agent mode)", async () => {
|
||||
const createSession = vi.fn().mockResolvedValue({ id: "session-new", agentId: "agent-001" });
|
||||
setupMockChat({ sessions: [], filteredSessions: [], createSession });
|
||||
|
||||
Reference in New Issue
Block a user