feat(FN-3306): add research recovery, CI shards, agent detail model favorit
Merges FN-3306 agent detail favorites wiring and test coverage, FN-3244 mobile FAB drag path hardening, FN-3303 mobile split layout updates, FN-3366 CI shard contract documentation with a new `scripts/ci-test-shard.mjs`, and FN-3014 research recovery semantics — plus expanded research extension tool Fusion-Task-Id: FN-3306
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import type { AgentDetail, AgentState, AgentHeartbeatRun, AgentBudgetStatus, ModelInfo, MemoryFileInfo, AgentCapability, PluginRuntimeInfo } from "../api";
|
||||
import { fetchAgent, updateAgent, updateAgentState, deleteAgent, fetchAgentLogsWithMeta, fetchAgentRunLogs, fetchAgentChildren, fetchAgentRuns, fetchAgentRunDetail, startAgentRun, stopAgentRun, updateAgentInstructions, updateAgentSoul, updateAgentMemory, fetchAgentMemoryFiles, fetchAgentMemoryFile, saveAgentMemoryFile, fetchAgentTasks, fetchChainOfCommand, fetchAgentBudgetStatus, resetAgentBudget, fetchWorkspaceFileContent, saveWorkspaceFileContent, fetchModels, fetchPluginRuntimes, fetchAgents, upgradeAgentHeartbeatProcedure } from "../api";
|
||||
import { fetchAgent, updateAgent, updateAgentState, deleteAgent, fetchAgentLogsWithMeta, fetchAgentRunLogs, fetchAgentChildren, fetchAgentRuns, fetchAgentRunDetail, startAgentRun, stopAgentRun, updateAgentInstructions, updateAgentSoul, updateAgentMemory, fetchAgentMemoryFiles, fetchAgentMemoryFile, saveAgentMemoryFile, fetchAgentTasks, fetchChainOfCommand, fetchAgentBudgetStatus, resetAgentBudget, fetchWorkspaceFileContent, saveWorkspaceFileContent, fetchModels, fetchPluginRuntimes, fetchAgents, upgradeAgentHeartbeatProcedure, updateGlobalSettings } from "../api";
|
||||
import type { Agent } from "../api";
|
||||
import type { AgentLogEntry, Task } from "@fusion/core";
|
||||
import { getErrorMessage } from "@fusion/core";
|
||||
@@ -2878,6 +2878,8 @@ function ConfigTab({
|
||||
// Model/runtime selector state
|
||||
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
|
||||
const [modelsLoading, setModelsLoading] = useState(false);
|
||||
const [favoriteProviders, setFavoriteProviders] = useState<string[]>([]);
|
||||
const [favoriteModels, setFavoriteModels] = useState<string[]>([]);
|
||||
const [availableRuntimes, setAvailableRuntimes] = useState<PluginRuntimeInfo[]>([]);
|
||||
const [runtimesLoading, setRuntimesLoading] = useState(false);
|
||||
|
||||
@@ -2936,13 +2938,49 @@ function ConfigTab({
|
||||
useEffect(() => {
|
||||
setModelsLoading(true);
|
||||
fetchModels()
|
||||
.then((response) => setAvailableModels(response.models))
|
||||
.then((response) => {
|
||||
setAvailableModels(response.models);
|
||||
setFavoriteProviders(response.favoriteProviders);
|
||||
setFavoriteModels(response.favoriteModels);
|
||||
})
|
||||
.catch(() => {
|
||||
// Gracefully handle unavailable models endpoint
|
||||
})
|
||||
.finally(() => setModelsLoading(false));
|
||||
}, []);
|
||||
|
||||
const handleToggleFavorite = useCallback(async (provider: string) => {
|
||||
const currentFavorites = favoriteProviders;
|
||||
const isFavorite = currentFavorites.includes(provider);
|
||||
const newFavorites = isFavorite
|
||||
? currentFavorites.filter((p) => p !== provider)
|
||||
: [provider, ...currentFavorites];
|
||||
|
||||
setFavoriteProviders(newFavorites);
|
||||
|
||||
try {
|
||||
await updateGlobalSettings({ favoriteProviders: newFavorites, favoriteModels });
|
||||
} catch {
|
||||
setFavoriteProviders(currentFavorites);
|
||||
}
|
||||
}, [favoriteProviders, favoriteModels]);
|
||||
|
||||
const handleToggleModelFavorite = useCallback(async (modelId: string) => {
|
||||
const currentFavorites = favoriteModels;
|
||||
const isFavorite = currentFavorites.includes(modelId);
|
||||
const newFavorites = isFavorite
|
||||
? currentFavorites.filter((m) => m !== modelId)
|
||||
: [modelId, ...currentFavorites];
|
||||
|
||||
setFavoriteModels(newFavorites);
|
||||
|
||||
try {
|
||||
await updateGlobalSettings({ favoriteProviders, favoriteModels: newFavorites });
|
||||
} catch {
|
||||
setFavoriteModels(currentFavorites);
|
||||
}
|
||||
}, [favoriteProviders, favoriteModels]);
|
||||
|
||||
useEffect(() => {
|
||||
setRuntimesLoading(true);
|
||||
fetchPluginRuntimes(projectId)
|
||||
@@ -3496,6 +3534,10 @@ function ConfigTab({
|
||||
placeholder="Use global default"
|
||||
label="Agent Model"
|
||||
disabled={modelsLoading}
|
||||
favoriteProviders={favoriteProviders}
|
||||
onToggleFavorite={handleToggleFavorite}
|
||||
favoriteModels={favoriteModels}
|
||||
onToggleModelFavorite={handleToggleModelFavorite}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -36,6 +36,7 @@ vi.mock("../../api", () => ({
|
||||
fetchModels: vi.fn(),
|
||||
fetchPluginRuntimes: vi.fn(),
|
||||
upgradeAgentHeartbeatProcedure: vi.fn(),
|
||||
updateGlobalSettings: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../AgentLogViewer", () => ({
|
||||
@@ -56,7 +57,7 @@ vi.mock("../AgentLogViewer", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../CustomModelDropdown", () => ({
|
||||
CustomModelDropdown: ({ models, value, onChange, disabled, label, placeholder, id }: {
|
||||
CustomModelDropdown: ({ models, value, onChange, disabled, label, placeholder, id, favoriteProviders = [], favoriteModels = [] }: {
|
||||
models: Array<{ provider: string; id: string }> ;
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
@@ -64,10 +65,14 @@ vi.mock("../CustomModelDropdown", () => ({
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
id?: string;
|
||||
favoriteProviders?: string[];
|
||||
onToggleFavorite?: (provider: string) => void;
|
||||
favoriteModels?: string[];
|
||||
onToggleModelFavorite?: (modelId: string) => void;
|
||||
}) => {
|
||||
const selectId = id ?? "custom-model-dropdown";
|
||||
return (
|
||||
<div data-testid="custom-model-dropdown">
|
||||
<div data-testid="custom-model-dropdown" data-favorite-providers={favoriteProviders.join(",")} data-favorite-models={favoriteModels.join(",")}>
|
||||
<label htmlFor={selectId}>{label}</label>
|
||||
<select
|
||||
id={selectId}
|
||||
@@ -112,7 +117,7 @@ vi.mock("../../hooks/useConfirm", () => ({
|
||||
useConfirm: () => ({ confirm: mockConfirm }),
|
||||
}));
|
||||
|
||||
import { fetchAgent, fetchAgents, updateAgent, updateAgentState, deleteAgent, fetchAgentChildren, fetchAgentRunLogs, fetchAgentRuns, fetchAgentRunDetail, fetchAgentTasks, fetchChainOfCommand, fetchAgentBudgetStatus, resetAgentBudget, updateAgentInstructions, updateAgentSoul, updateAgentMemory, fetchWorkspaceFileContent, saveWorkspaceFileContent, fetchDiscoveredSkills, fetchModels, fetchPluginRuntimes, fetchAgentLogsWithMeta, upgradeAgentHeartbeatProcedure } from "../../api";
|
||||
import { fetchAgent, fetchAgents, updateAgent, updateAgentState, deleteAgent, fetchAgentChildren, fetchAgentRunLogs, fetchAgentRuns, fetchAgentRunDetail, fetchAgentTasks, fetchChainOfCommand, fetchAgentBudgetStatus, resetAgentBudget, updateAgentInstructions, updateAgentSoul, updateAgentMemory, fetchWorkspaceFileContent, saveWorkspaceFileContent, fetchDiscoveredSkills, fetchModels, fetchPluginRuntimes, fetchAgentLogsWithMeta, upgradeAgentHeartbeatProcedure, updateGlobalSettings } from "../../api";
|
||||
import { subscribeSse } from "../../sse-bus";
|
||||
|
||||
const mockFetchAgent = vi.mocked(fetchAgent);
|
||||
@@ -138,6 +143,7 @@ const mockFetchModels = vi.mocked(fetchModels);
|
||||
const mockFetchPluginRuntimes = vi.mocked(fetchPluginRuntimes);
|
||||
const mockFetchAgentLogsWithMeta = vi.mocked(fetchAgentLogsWithMeta);
|
||||
const mockUpgradeAgentHeartbeatProcedure = vi.mocked(upgradeAgentHeartbeatProcedure);
|
||||
const mockUpdateGlobalSettings = vi.mocked(updateGlobalSettings);
|
||||
const mockSubscribeSse = vi.mocked(subscribeSse);
|
||||
|
||||
const MOCK_SKILLS = [
|
||||
@@ -239,6 +245,7 @@ describe("AgentDetailView", () => {
|
||||
heartbeatProcedurePath: ".fusion/agents/agent-001/HEARTBEAT.md",
|
||||
procedureFileSeeded: true,
|
||||
});
|
||||
mockUpdateGlobalSettings.mockResolvedValue({} as any);
|
||||
});
|
||||
|
||||
it("shows loading state initially", () => {
|
||||
@@ -1675,6 +1682,31 @@ describe("AgentDetailView", () => {
|
||||
expect(modelSelect.value).toBe("openai/gpt-4o");
|
||||
});
|
||||
|
||||
it("passes favorited providers and models to model dropdown", async () => {
|
||||
mockFetchModels.mockResolvedValueOnce({
|
||||
models: [
|
||||
{ provider: "openai", id: "gpt-4o", name: "gpt-4o", reasoning: false, contextWindow: 128000 },
|
||||
],
|
||||
favoriteProviders: ["openai"],
|
||||
favoriteModels: ["openai/gpt-4o"],
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<AgentDetailView
|
||||
agentId="agent-001"
|
||||
onClose={vi.fn()}
|
||||
addToast={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await navigateToSettings(user);
|
||||
|
||||
const dropdown = await screen.findByTestId("custom-model-dropdown");
|
||||
expect(dropdown).toHaveAttribute("data-favorite-providers", "openai");
|
||||
expect(dropdown).toHaveAttribute("data-favorite-models", "openai/gpt-4o");
|
||||
});
|
||||
|
||||
it("shows runtime mode selected when agent runtimeConfig has runtimeHint", async () => {
|
||||
mockFetchAgent.mockResolvedValue(createMockAgent({
|
||||
runtimeConfig: {
|
||||
|
||||
Reference in New Issue
Block a user