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 ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import type { AgentDetail, AgentState, AgentHeartbeatRun, AgentBudgetStatus, ModelInfo, MemoryFileInfo, AgentCapability, PluginRuntimeInfo } from "../api";
|
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 { Agent } from "../api";
|
||||||
import type { AgentLogEntry, Task } from "@fusion/core";
|
import type { AgentLogEntry, Task } from "@fusion/core";
|
||||||
import { getErrorMessage } from "@fusion/core";
|
import { getErrorMessage } from "@fusion/core";
|
||||||
@@ -2878,6 +2878,8 @@ function ConfigTab({
|
|||||||
// Model/runtime selector state
|
// Model/runtime selector state
|
||||||
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
|
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
|
||||||
const [modelsLoading, setModelsLoading] = useState(false);
|
const [modelsLoading, setModelsLoading] = useState(false);
|
||||||
|
const [favoriteProviders, setFavoriteProviders] = useState<string[]>([]);
|
||||||
|
const [favoriteModels, setFavoriteModels] = useState<string[]>([]);
|
||||||
const [availableRuntimes, setAvailableRuntimes] = useState<PluginRuntimeInfo[]>([]);
|
const [availableRuntimes, setAvailableRuntimes] = useState<PluginRuntimeInfo[]>([]);
|
||||||
const [runtimesLoading, setRuntimesLoading] = useState(false);
|
const [runtimesLoading, setRuntimesLoading] = useState(false);
|
||||||
|
|
||||||
@@ -2936,13 +2938,49 @@ function ConfigTab({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setModelsLoading(true);
|
setModelsLoading(true);
|
||||||
fetchModels()
|
fetchModels()
|
||||||
.then((response) => setAvailableModels(response.models))
|
.then((response) => {
|
||||||
|
setAvailableModels(response.models);
|
||||||
|
setFavoriteProviders(response.favoriteProviders);
|
||||||
|
setFavoriteModels(response.favoriteModels);
|
||||||
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// Gracefully handle unavailable models endpoint
|
// Gracefully handle unavailable models endpoint
|
||||||
})
|
})
|
||||||
.finally(() => setModelsLoading(false));
|
.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(() => {
|
useEffect(() => {
|
||||||
setRuntimesLoading(true);
|
setRuntimesLoading(true);
|
||||||
fetchPluginRuntimes(projectId)
|
fetchPluginRuntimes(projectId)
|
||||||
@@ -3496,6 +3534,10 @@ function ConfigTab({
|
|||||||
placeholder="Use global default"
|
placeholder="Use global default"
|
||||||
label="Agent Model"
|
label="Agent Model"
|
||||||
disabled={modelsLoading}
|
disabled={modelsLoading}
|
||||||
|
favoriteProviders={favoriteProviders}
|
||||||
|
onToggleFavorite={handleToggleFavorite}
|
||||||
|
favoriteModels={favoriteModels}
|
||||||
|
onToggleModelFavorite={handleToggleModelFavorite}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ vi.mock("../../api", () => ({
|
|||||||
fetchModels: vi.fn(),
|
fetchModels: vi.fn(),
|
||||||
fetchPluginRuntimes: vi.fn(),
|
fetchPluginRuntimes: vi.fn(),
|
||||||
upgradeAgentHeartbeatProcedure: vi.fn(),
|
upgradeAgentHeartbeatProcedure: vi.fn(),
|
||||||
|
updateGlobalSettings: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../AgentLogViewer", () => ({
|
vi.mock("../AgentLogViewer", () => ({
|
||||||
@@ -56,7 +57,7 @@ vi.mock("../AgentLogViewer", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../CustomModelDropdown", () => ({
|
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 }> ;
|
models: Array<{ provider: string; id: string }> ;
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (v: string) => void;
|
onChange: (v: string) => void;
|
||||||
@@ -64,10 +65,14 @@ vi.mock("../CustomModelDropdown", () => ({
|
|||||||
label: string;
|
label: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
|
favoriteProviders?: string[];
|
||||||
|
onToggleFavorite?: (provider: string) => void;
|
||||||
|
favoriteModels?: string[];
|
||||||
|
onToggleModelFavorite?: (modelId: string) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const selectId = id ?? "custom-model-dropdown";
|
const selectId = id ?? "custom-model-dropdown";
|
||||||
return (
|
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>
|
<label htmlFor={selectId}>{label}</label>
|
||||||
<select
|
<select
|
||||||
id={selectId}
|
id={selectId}
|
||||||
@@ -112,7 +117,7 @@ vi.mock("../../hooks/useConfirm", () => ({
|
|||||||
useConfirm: () => ({ confirm: mockConfirm }),
|
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";
|
import { subscribeSse } from "../../sse-bus";
|
||||||
|
|
||||||
const mockFetchAgent = vi.mocked(fetchAgent);
|
const mockFetchAgent = vi.mocked(fetchAgent);
|
||||||
@@ -138,6 +143,7 @@ const mockFetchModels = vi.mocked(fetchModels);
|
|||||||
const mockFetchPluginRuntimes = vi.mocked(fetchPluginRuntimes);
|
const mockFetchPluginRuntimes = vi.mocked(fetchPluginRuntimes);
|
||||||
const mockFetchAgentLogsWithMeta = vi.mocked(fetchAgentLogsWithMeta);
|
const mockFetchAgentLogsWithMeta = vi.mocked(fetchAgentLogsWithMeta);
|
||||||
const mockUpgradeAgentHeartbeatProcedure = vi.mocked(upgradeAgentHeartbeatProcedure);
|
const mockUpgradeAgentHeartbeatProcedure = vi.mocked(upgradeAgentHeartbeatProcedure);
|
||||||
|
const mockUpdateGlobalSettings = vi.mocked(updateGlobalSettings);
|
||||||
const mockSubscribeSse = vi.mocked(subscribeSse);
|
const mockSubscribeSse = vi.mocked(subscribeSse);
|
||||||
|
|
||||||
const MOCK_SKILLS = [
|
const MOCK_SKILLS = [
|
||||||
@@ -239,6 +245,7 @@ describe("AgentDetailView", () => {
|
|||||||
heartbeatProcedurePath: ".fusion/agents/agent-001/HEARTBEAT.md",
|
heartbeatProcedurePath: ".fusion/agents/agent-001/HEARTBEAT.md",
|
||||||
procedureFileSeeded: true,
|
procedureFileSeeded: true,
|
||||||
});
|
});
|
||||||
|
mockUpdateGlobalSettings.mockResolvedValue({} as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows loading state initially", () => {
|
it("shows loading state initially", () => {
|
||||||
@@ -1675,6 +1682,31 @@ describe("AgentDetailView", () => {
|
|||||||
expect(modelSelect.value).toBe("openai/gpt-4o");
|
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 () => {
|
it("shows runtime mode selected when agent runtimeConfig has runtimeHint", async () => {
|
||||||
mockFetchAgent.mockResolvedValue(createMockAgent({
|
mockFetchAgent.mockResolvedValue(createMockAgent({
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
|
|||||||
Reference in New Issue
Block a user