feat(FN-4062): add global thinking log setting to persist agent reasoning a

Adds a global `thinkingLogEnabled` setting that gates AI thinking log persistence across the engine (executor, reviewer, merger, triage, step-session) and exposes the control in the dashboard Settings modal, with tests verifying settings parity and modal behavior.

Fusion-Task-Id: FN-4062
This commit is contained in:
Fusion
2026-05-11 21:09:28 -07:00
committed by gsxdsm
parent c8d530dec1
commit fab4ed4e61
16 changed files with 152 additions and 4 deletions

View File

@@ -2027,6 +2027,23 @@ export function SettingsModal({
Very large tool payloads may still be clipped even when this stays enabled.
</div>
</div>
<div className="form-group">
<label htmlFor="persistAgentThinkingLog" className="checkbox-label">
<input
id="persistAgentThinkingLog"
type="checkbox"
checked={form.persistAgentThinkingLog === true}
onChange={(e) =>
setForm((f) => ({ ...f, persistAgentThinkingLog: e.target.checked }))
}
/>
Save AI thinking/reasoning in agent logs
</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.
</div>
</div>
<div className="form-group">
<label htmlFor="fnBinaryCheckEnabled" className="checkbox-label">
<input

View File

@@ -513,6 +513,29 @@ describe("SettingsModal", () => {
expect(screen.getByRole("checkbox", { name: "Save tool output in agent logs" })).not.toBeChecked();
});
it("defaults persistAgentThinkingLog checkbox to unchecked", async () => {
renderModal({ initialSection: "global-general" });
await waitForSettingsModalReady();
expect(screen.getByRole("checkbox", { name: "Save AI thinking/reasoning in agent logs" })).not.toBeChecked();
});
it("reflects persisted checked thinking-log value from global settings", async () => {
mockFetchSettings.mockResolvedValue({
...defaultSettings,
persistAgentThinkingLog: true,
});
mockFetchSettingsByScope.mockResolvedValue({
global: { ...defaultSettings, persistAgentThinkingLog: true },
project: {},
});
renderModal({ initialSection: "global-general" });
await waitForSettingsModalReady();
expect(screen.getByRole("checkbox", { name: "Save AI thinking/reasoning in agent logs" })).toBeChecked();
});
it("saves persistAgentToolOutput only via global settings payload", async () => {
renderModal({ initialSection: "global-general" });
await waitForSettingsModalReady();
@@ -532,6 +555,25 @@ describe("SettingsModal", () => {
}
});
it("saves persistAgentThinkingLog 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("button", { name: "Save" }));
await waitFor(() => {
expect(mockUpdateGlobalSettings).toHaveBeenCalled();
});
const globalPayload = mockUpdateGlobalSettings.mock.calls[0]?.[0] as Record<string, unknown>;
expect(globalPayload.persistAgentThinkingLog).toBe(true);
if (mockUpdateSettings.mock.calls.length > 0) {
const projectPayload = mockUpdateSettings.mock.calls[0]?.[0] as Record<string, unknown>;
expect(projectPayload.persistAgentThinkingLog).toBeUndefined();
}
});
it("renders global default tracking repo control", async () => {
renderModal({ initialSection: "global-general" });
await waitForSettingsModalReady();

View File

@@ -1136,6 +1136,23 @@ describe("PUT /settings/global", () => {
expect(res.body.persistAgentToolOutput).toBe(false);
});
it("accepts persistAgentThinkingLog in global updates", async () => {
const updatedMerged = { persistAgentThinkingLog: true };
(store.updateGlobalSettings as ReturnType<typeof vi.fn>).mockResolvedValue(updatedMerged);
const res = await REQUEST(
buildApp(),
"PUT",
"/api/settings/global",
JSON.stringify({ persistAgentThinkingLog: true }),
{ "Content-Type": "application/json" },
);
expect(res.status).toBe(200);
expect(store.updateGlobalSettings).toHaveBeenCalledWith({ persistAgentThinkingLog: true });
expect(res.body.persistAgentThinkingLog).toBe(true);
});
it("returns 500 on update error", async () => {
(store.updateGlobalSettings as ReturnType<typeof vi.fn>).mockRejectedValue(new Error("Write failed"));
@@ -1270,7 +1287,12 @@ describe("GET /settings/scopes", () => {
it("returns settings separated by scope", async () => {
(store.getSettingsByScope as ReturnType<typeof vi.fn>).mockResolvedValue({
global: { themeMode: "dark", defaultProvider: "anthropic", persistAgentToolOutput: true },
global: {
themeMode: "dark",
defaultProvider: "anthropic",
persistAgentToolOutput: true,
persistAgentThinkingLog: false,
},
project: { maxConcurrent: 4, autoMerge: false },
});
@@ -1280,9 +1302,11 @@ 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.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.persistAgentThinkingLog).toBeUndefined();
});
it("returns exact response envelope shape with only global and project keys", async () => {