feat(FN-2843): auto-select quick chat default model

- Extend /models API responses to include optional defaultProvider/defaultModelId from global settings, including empty and error paths
- Update QuickChatFAB to prefer the configured default model on load and fall back to first-model selection only when no agents exist
- Move quick chat model tag/title-wrap inline styles into QuickChatFAB.css using dashboard design tokens
- Add QuickChatFAB tests covering default-model auto-selection with and without agents and legacy behavior when no default is configured
This commit is contained in:
Fusion
2026-04-28 02:32:48 -07:00
committed by gsxdsm
parent 7414184705
commit 1d9d9f4e7f
6 changed files with 150 additions and 46 deletions

View File

@@ -188,6 +188,28 @@
color: var(--text);
}
.quick-chat-panel-title-wrap {
display: flex;
align-items: center;
gap: var(--space-sm);
min-width: 0;
}
.quick-chat-model-tag {
display: inline-flex;
align-items: center;
max-width: 18ch;
padding: var(--space-xs) var(--space-sm);
border-radius: var(--radius-pill);
border: 1px solid color-mix(in srgb, var(--todo) 35%, var(--border));
background: color-mix(in srgb, var(--todo) 14%, transparent);
color: var(--text);
font-size: 0.75rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.quick-chat-panel-header-actions {
display: inline-flex;
align-items: center;

View File

@@ -46,29 +46,6 @@ interface ParsedModelSelection {
modelId: string;
}
const modelTagStyle = {
display: "inline-flex",
alignItems: "center",
maxWidth: "180px",
padding: "var(--space-xs) var(--space-sm)",
borderRadius: "var(--radius-pill)",
border: "1px solid color-mix(in srgb, var(--todo) 35%, var(--border))",
background: "color-mix(in srgb, var(--todo) 14%, transparent)",
color: "var(--text)",
fontSize: "11px",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
} as const;
const headerTitleWrapStyle = {
display: "flex",
alignItems: "center",
gap: "var(--space-sm)",
minWidth: 0,
} as const;
function getAgentLabel(agent: Agent): string {
const base = agent.name?.trim() || agent.id;
return `${base} (${agent.role})`;
@@ -807,8 +784,27 @@ export function QuickChatFAB({
.then((response) => {
const loadedModels = response.models ?? [];
setModels(loadedModels);
// Auto-select first model when no agents exist and no model selected yet
if (agents.length === 0 && loadedModels.length > 0 && !selectedModel) {
if (selectedModel || loadedModels.length === 0) {
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) {
setSelectedModel(defaultSelection);
setChatMode("model");
return;
}
}
// Fallback: auto-select first model only when no agents exist.
if (agents.length === 0) {
const firstModel = loadedModels[0];
if (firstModel) {
setSelectedModel(`${firstModel.provider}/${firstModel.id}`);
@@ -1353,10 +1349,10 @@ export function QuickChatFAB({
)}
<div className="quick-chat-panel-header">
<div style={headerTitleWrapStyle}>
<div className="quick-chat-panel-title-wrap">
<h3>Quick Chat</h3>
{selectedModelTag && (
<span style={modelTagStyle} data-testid="quick-chat-model-tag" title={selectedModelTag}>
<span className="quick-chat-model-tag" data-testid="quick-chat-model-tag" title={selectedModelTag}>
{selectedModelTag}
</span>
)}

View File

@@ -487,6 +487,7 @@ describe("ActivityLogModal", () => {
await waitFor(() => {
expect(screen.getByTestId("activity-log-modal")).toBeTruthy();
expect(screen.getAllByTestId("activity-entry").length).toBeGreaterThan(0);
});
// Verify key structural classes that the mobile CSS targets

View File

@@ -164,6 +164,8 @@ describe("QuickChatFAB", () => {
models: mockModels,
favoriteProviders: [],
favoriteModels: [],
defaultProvider: undefined,
defaultModelId: undefined,
});
createMockStreamResponse();
});
@@ -185,6 +187,63 @@ describe("QuickChatFAB", () => {
expect(screen.queryByTestId("quick-chat-agent-select")).toBeNull();
});
it("auto-selects configured default model when no agents exist", async () => {
mockAgentsHook([]);
mockFetchModels.mockResolvedValue({
models: mockModels,
favoriteProviders: [],
favoriteModels: [],
defaultProvider: "openai",
defaultModelId: "gpt-4o",
});
render(<QuickChatFAB addToast={addToast} />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
await waitFor(() => {
expect(screen.getByTestId("quick-chat-model-tag")).toHaveTextContent("GPT-4o");
});
});
it("auto-selects configured default model and switches to model mode when agents exist", async () => {
mockAgentsHook(mockAgents);
mockFetchModels.mockResolvedValue({
models: mockModels,
favoriteProviders: [],
favoriteModels: [],
defaultProvider: "openai",
defaultModelId: "gpt-4o",
});
render(<QuickChatFAB addToast={addToast} />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
await waitFor(() => {
expect(screen.getByTestId("quick-chat-model-select")).toBeDefined();
expect(screen.getByTestId("quick-chat-model-tag")).toHaveTextContent("GPT-4o");
});
});
it("preserves existing behavior when no default model is configured and agents exist", async () => {
mockAgentsHook(mockAgents);
mockFetchModels.mockResolvedValue({
models: mockModels,
favoriteProviders: [],
favoriteModels: [],
defaultProvider: undefined,
defaultModelId: undefined,
});
render(<QuickChatFAB addToast={addToast} />);
fireEvent.click(screen.getByTestId("quick-chat-fab"));
await waitFor(() => {
expect(screen.getByTestId("quick-chat-agent-select")).toBeDefined();
});
expect(screen.queryByTestId("quick-chat-model-select")).toBeNull();
expect(screen.queryByTestId("quick-chat-model-tag")).toBeNull();
});
it("renders FAB button when agents exist", () => {
render(<QuickChatFAB addToast={addToast} />);