feat(FN-3250): add auto-claim setting UI and documentation

Added auto-claim setting UI to the Agent detail view with corresponding documentation in agents.md and test coverage for the new component behavior. The changeset marks this as a minor feature for the published `@runfusion/fusion` package.

Fusion-Task-Id: FN-3250
This commit is contained in:
Fusion
2026-05-05 01:03:28 -07:00
committed by gsxdsm
parent d051c09a1d
commit cd2a464a41
9 changed files with 433 additions and 1 deletions

View File

@@ -2613,6 +2613,10 @@ function deriveHeartbeatEnabled(runtimeConfig: AgentDetail["runtimeConfig"] | un
return runtimeConfig?.enabled !== false;
}
function deriveAutoClaimRelevantTasksEnabled(runtimeConfig: AgentDetail["runtimeConfig"] | undefined): boolean {
return runtimeConfig?.autoClaimRelevantTasks !== false;
}
function deriveBudgetValues(runtimeConfig: AgentDetail["runtimeConfig"] | undefined): Record<string, string> {
const bc = (runtimeConfig ?? {}).budgetConfig as Record<string, unknown> | undefined;
const nextValues: Record<string, string> = {};
@@ -2967,6 +2971,9 @@ function ConfigTab({
const [heartbeatEnabled, setHeartbeatEnabled] = useState<boolean>(
() => deriveHeartbeatEnabled(agent.runtimeConfig),
);
const [autoClaimRelevantTasksEnabled, setAutoClaimRelevantTasksEnabled] = useState<boolean>(
() => deriveAutoClaimRelevantTasksEnabled(agent.runtimeConfig),
);
// Budget config state initialised from agent.runtimeConfig.budgetConfig
const [budgetValues, setBudgetValues] = useState<Record<string, string>>(
@@ -3167,6 +3174,7 @@ function ConfigTab({
// Check heartbeat values
const rc = agent.runtimeConfig ?? {};
if (heartbeatEnabled !== deriveHeartbeatEnabled(agent.runtimeConfig)) return true;
if (autoClaimRelevantTasksEnabled !== deriveAutoClaimRelevantTasksEnabled(agent.runtimeConfig)) return true;
for (const key of ["heartbeatIntervalMs", "heartbeatTimeoutMs", "maxConcurrentRuns", "messageResponseMode"] as const) {
const current = heartbeatValues[key]?.trim() ?? "";
let persisted = rc[key] !== undefined && rc[key] !== null ? String(rc[key]) : "";
@@ -3240,6 +3248,7 @@ function ConfigTab({
previousAgentRuntimeSyncRef.current = nextSnapshot;
setHeartbeatValues(deriveHeartbeatValues(agent.runtimeConfig));
setHeartbeatEnabled(deriveHeartbeatEnabled(agent.runtimeConfig));
setAutoClaimRelevantTasksEnabled(deriveAutoClaimRelevantTasksEnabled(agent.runtimeConfig));
setBudgetValues(deriveBudgetValues(agent.runtimeConfig));
setModelValue(initialModelValue);
setSelectedRuntimeId(initialRuntimeHint);
@@ -3385,6 +3394,7 @@ function ConfigTab({
// Build the runtimeConfig payload — only include non-empty values
const newRuntimeConfig: Record<string, unknown> = { ...agent.runtimeConfig };
newRuntimeConfig.enabled = heartbeatEnabled;
newRuntimeConfig.autoClaimRelevantTasks = autoClaimRelevantTasksEnabled;
for (const key of ["heartbeatIntervalMs", "heartbeatTimeoutMs", "maxConcurrentRuns"] as const) {
const raw = heartbeatValues[key]?.trim();
if (!raw) {
@@ -3480,7 +3490,7 @@ function ConfigTab({
runtimeConfig: newRuntimeConfig,
bundleConfig: newBundleConfig,
};
}, [agent.metadata, agent.runtimeConfig, budgetValues, bundleEntryFile, bundleExternalPath, bundleFiles, bundleMode, formValues, heartbeatEnabled, heartbeatValues, iconValue, modelValue, nameValue, reportsToValue, roleValue, runtimeMode, selectedRuntimeId, selectedSkills, titleValue, validationErrors]);
}, [agent.metadata, agent.runtimeConfig, autoClaimRelevantTasksEnabled, budgetValues, bundleEntryFile, bundleExternalPath, bundleFiles, bundleMode, formValues, heartbeatEnabled, heartbeatValues, iconValue, modelValue, nameValue, reportsToValue, roleValue, runtimeMode, selectedRuntimeId, selectedSkills, titleValue, validationErrors]);
const persistSettings = useCallback(async (showValidationToast: boolean, source: "auto" | "manual") => {
const payload = buildSavePayload();
@@ -3765,6 +3775,19 @@ function ConfigTab({
<span className="config-hint">When enabled, this agent receives scheduled heartbeat runs based on its interval.</span>
</div>
<div className="config-field">
<label className="checkbox-label" htmlFor="hb-autoClaimRelevantTasks">
<input
id="hb-autoClaimRelevantTasks"
type="checkbox"
checked={autoClaimRelevantTasksEnabled}
onChange={(e) => setAutoClaimRelevantTasksEnabled(e.target.checked)}
/>
Auto-Claim Relevant Tasks
</label>
<span className="config-hint">When enabled (default), no-task heartbeats scan open unowned work and auto-claim tasks aligned with this agent&apos;s role and soul.</span>
</div>
<div className="config-field">
<label htmlFor="hb-heartbeatIntervalMs">Heartbeat Interval (s)</label>
<input

View File

@@ -2042,6 +2042,29 @@ describe("AgentDetailView", () => {
});
});
it("defaults auto-claim toggle to enabled when runtimeConfig.autoClaimRelevantTasks is missing", async () => {
mockFetchAgent.mockResolvedValue(createMockAgent({
runtimeConfig: {
heartbeatIntervalMs: 30000,
},
}));
const user = userEvent.setup();
render(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
/>
);
await navigateToSettings(user);
await waitFor(() => {
expect((screen.getByLabelText("Auto-Claim Relevant Tasks") as HTMLInputElement).checked).toBe(true);
});
});
it("shows Save Settings button disabled when no changes", async () => {
mockFetchAgent.mockResolvedValue(createMockAgent({ metadata: {} }));
@@ -2257,6 +2280,42 @@ describe("AgentDetailView", () => {
});
});
it("persists auto-claim toggle changes on save", async () => {
mockFetchAgent.mockResolvedValue(createMockAgent({
runtimeConfig: {
enabled: true,
autoClaimRelevantTasks: true,
heartbeatIntervalMs: 30000,
},
}));
mockUpdateAgent.mockResolvedValue(createMockAgent() as any);
const user = userEvent.setup();
render(
<AgentDetailView
agentId="agent-001"
onClose={vi.fn()}
addToast={vi.fn()}
/>
);
await navigateToSettings(user);
const autoClaimInput = await screen.findByLabelText("Auto-Claim Relevant Tasks");
await user.click(autoClaimInput);
await user.click(screen.getByText("Save Settings"));
await waitFor(() => {
expect(mockUpdateAgent).toHaveBeenCalledWith(
"agent-001",
expect.objectContaining({
runtimeConfig: expect.objectContaining({ autoClaimRelevantTasks: false }),
}),
undefined,
);
});
});
it("forwards projectId to updateAgent", async () => {
const addToast = vi.fn();
mockUpdateAgent.mockResolvedValue(createMockAgent() as any);