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:
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user