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:
Fusion
2026-05-04 08:42:39 -07:00
committed by gsxdsm
parent 1807dbd5e4
commit 3bd7041dae
2 changed files with 79 additions and 5 deletions

View File

@@ -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>
) : (

View File

@@ -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: {