feat(FN-4080): complete Step 3 — split thinking persistence settings UI

Fusion-Task-Id: FN-4080
Fusion-Task-Lineage: 086863ba-7100-41ab-9cfc-197301fbcab0
This commit is contained in:
Fusion
2026-05-13 05:46:56 -07:00
committed by gsxdsm
parent 8c4fbd5a61
commit f3f4b20c85
3 changed files with 66 additions and 19 deletions

View File

@@ -6,6 +6,7 @@ import {
isGlobalSettingsKey,
isProjectSettingsKey,
resolvePlanningSettingsModel,
resolvePersistAgentThinkingLog,
resolveProjectDefaultModel,
resolveTitleSummarizerSettingsModel,
} from "@fusion/core";
@@ -1666,6 +1667,9 @@ export function SettingsModal({
if (key === "githubTrackingDefaultRepo" && activeSection !== "global-general") {
continue;
}
if (key === "persistAgentThinkingLog") {
continue;
}
if (isGlobalSettingsKey(key)) {
// Implement null-as-delete semantics for global settings:
// - undefined values are dropped during JSON serialization
@@ -2129,20 +2133,33 @@ export function SettingsModal({
</div>
</div>
<div className="form-group">
<label htmlFor="persistAgentThinkingLog" className="checkbox-label">
<label htmlFor="persistAgentThinkingLogPermanent" className="checkbox-label">
<input
id="persistAgentThinkingLog"
id="persistAgentThinkingLogPermanent"
type="checkbox"
checked={form.persistAgentThinkingLog === true}
checked={resolvePersistAgentThinkingLog(form, { ephemeral: false })}
onChange={(e) =>
setForm((f) => ({ ...f, persistAgentThinkingLog: e.target.checked }))
setForm((f) => ({ ...f, persistAgentThinkingLogPermanent: e.target.checked }))
}
/>
Save AI thinking/reasoning in agent logs
Save AI thinking for permanent agents
</label>
</div>
<div className="form-group">
<label htmlFor="persistAgentThinkingLogEphemeral" className="checkbox-label">
<input
id="persistAgentThinkingLogEphemeral"
type="checkbox"
checked={resolvePersistAgentThinkingLog(form, { ephemeral: true })}
onChange={(e) =>
setForm((f) => ({ ...f, persistAgentThinkingLogEphemeral: e.target.checked }))
}
/>
Save AI thinking for ephemeral / task-worker agents
</label>
<div className="settings-field-help">
When disabled (default), internal thinking deltas are not persisted as log rows.
Assistant text output and tool timeline entries are unchanged.
Leave both thinking toggles off to keep the original default behavior.
This only controls persisted <code>thinking</code> rows and does not affect assistant text or tool rows.
</div>
</div>
<div className="form-group">

View File

@@ -592,14 +592,15 @@ describe("SettingsModal", () => {
expect(screen.getByRole("checkbox", { name: "Save tool output in agent logs" })).not.toBeChecked();
});
it("defaults persistAgentThinkingLog checkbox to unchecked", async () => {
it("defaults thinking-log checkboxes to unchecked", async () => {
renderModal({ initialSection: "global-general" });
await waitForSettingsModalReady();
expect(screen.getByRole("checkbox", { name: "Save AI thinking/reasoning in agent logs" })).not.toBeChecked();
expect(screen.getByRole("checkbox", { name: "Save AI thinking for permanent agents" })).not.toBeChecked();
expect(screen.getByRole("checkbox", { name: "Save AI thinking for ephemeral / task-worker agents" })).not.toBeChecked();
});
it("reflects persisted checked thinking-log value from global settings", async () => {
it("falls back to legacy thinking-log flag when granular fields are unset", async () => {
mockFetchSettings.mockResolvedValue({
...defaultSettings,
persistAgentThinkingLog: true,
@@ -612,7 +613,8 @@ describe("SettingsModal", () => {
renderModal({ initialSection: "global-general" });
await waitForSettingsModalReady();
expect(screen.getByRole("checkbox", { name: "Save AI thinking/reasoning in agent logs" })).toBeChecked();
expect(screen.getByRole("checkbox", { name: "Save AI thinking for permanent agents" })).toBeChecked();
expect(screen.getByRole("checkbox", { name: "Save AI thinking for ephemeral / task-worker agents" })).toBeChecked();
});
it("saves persistAgentToolOutput only via global settings payload", async () => {
@@ -634,11 +636,12 @@ describe("SettingsModal", () => {
}
});
it("saves persistAgentThinkingLog only via global settings payload", async () => {
it("saves granular thinking-log flags only via global settings payload", async () => {
renderModal({ initialSection: "global-general" });
await waitForSettingsModalReady();
await userEvent.click(screen.getByRole("checkbox", { name: "Save AI thinking/reasoning in agent logs" }));
await userEvent.click(screen.getByRole("checkbox", { name: "Save AI thinking for permanent agents" }));
await userEvent.click(screen.getByRole("checkbox", { name: "Save AI thinking for ephemeral / task-worker agents" }));
await userEvent.click(screen.getByRole("button", { name: "Save" }));
await waitFor(() => {
@@ -646,9 +649,13 @@ describe("SettingsModal", () => {
});
const globalPayload = mockUpdateGlobalSettings.mock.calls[0]?.[0] as Record<string, unknown>;
expect(globalPayload.persistAgentThinkingLog).toBe(true);
expect(globalPayload.persistAgentThinkingLogPermanent).toBe(true);
expect(globalPayload.persistAgentThinkingLogEphemeral).toBe(true);
expect(globalPayload.persistAgentThinkingLog).toBeUndefined();
if (mockUpdateSettings.mock.calls.length > 0) {
const projectPayload = mockUpdateSettings.mock.calls[0]?.[0] as Record<string, unknown>;
expect(projectPayload.persistAgentThinkingLogPermanent).toBeUndefined();
expect(projectPayload.persistAgentThinkingLogEphemeral).toBeUndefined();
expect(projectPayload.persistAgentThinkingLog).toBeUndefined();
}
});

View File

@@ -1136,21 +1136,38 @@ describe("PUT /settings/global", () => {
expect(res.body.persistAgentToolOutput).toBe(false);
});
it("accepts persistAgentThinkingLog in global updates", async () => {
const updatedMerged = { persistAgentThinkingLog: true };
it("accepts persistAgentThinkingLogPermanent in global updates", async () => {
const updatedMerged = { persistAgentThinkingLogPermanent: true };
(store.updateGlobalSettings as ReturnType<typeof vi.fn>).mockResolvedValue(updatedMerged);
const res = await REQUEST(
buildApp(),
"PUT",
"/api/settings/global",
JSON.stringify({ persistAgentThinkingLog: true }),
JSON.stringify({ persistAgentThinkingLogPermanent: true }),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(200);
expect(store.updateGlobalSettings).toHaveBeenCalledWith({ persistAgentThinkingLog: true });
expect(res.body.persistAgentThinkingLog).toBe(true);
expect(store.updateGlobalSettings).toHaveBeenCalledWith({ persistAgentThinkingLogPermanent: true });
expect(res.body.persistAgentThinkingLogPermanent).toBe(true);
});
it("accepts persistAgentThinkingLogEphemeral in global updates", async () => {
const updatedMerged = { persistAgentThinkingLogEphemeral: true };
(store.updateGlobalSettings as ReturnType<typeof vi.fn>).mockResolvedValue(updatedMerged);
const res = await REQUEST(
buildApp(),
"PUT",
"/api/settings/global",
JSON.stringify({ persistAgentThinkingLogEphemeral: true }),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(200);
expect(store.updateGlobalSettings).toHaveBeenCalledWith({ persistAgentThinkingLogEphemeral: true });
expect(res.body.persistAgentThinkingLogEphemeral).toBe(true);
});
it("returns 500 on update error", async () => {
@@ -1291,6 +1308,8 @@ describe("GET /settings/scopes", () => {
themeMode: "dark",
defaultProvider: "anthropic",
persistAgentToolOutput: true,
persistAgentThinkingLogPermanent: false,
persistAgentThinkingLogEphemeral: false,
persistAgentThinkingLog: false,
},
project: { maxConcurrent: 4, autoMerge: false },
@@ -1302,10 +1321,14 @@ describe("GET /settings/scopes", () => {
expect(res.body.global.themeMode).toBe("dark");
expect(res.body.global.defaultProvider).toBe("anthropic");
expect(res.body.global.persistAgentToolOutput).toBe(true);
expect(res.body.global.persistAgentThinkingLogPermanent).toBe(false);
expect(res.body.global.persistAgentThinkingLogEphemeral).toBe(false);
expect(res.body.global.persistAgentThinkingLog).toBe(false);
expect(res.body.project.maxConcurrent).toBe(4);
expect(res.body.project.autoMerge).toBe(false);
expect(res.body.project.persistAgentToolOutput).toBeUndefined();
expect(res.body.project.persistAgentThinkingLogPermanent).toBeUndefined();
expect(res.body.project.persistAgentThinkingLogEphemeral).toBeUndefined();
expect(res.body.project.persistAgentThinkingLog).toBeUndefined();
});