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:
gsxdsm
2026-06-02 04:01:31 -07:00
parent f4cbaed494
commit be95b3f2c3
2 changed files with 75 additions and 8 deletions

View File

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

View File

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