diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index 8a0b3dee0d1f..8453b4f64e63 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -1198,7 +1198,10 @@ export function renderApp(state: AppViewState) { gatewayUrl: state.settings.gatewayUrl, assistantName: state.assistantName, configPath: state.configSnapshot?.path ?? null, - rawAvailable: typeof state.configSnapshot?.raw === "string", + rawAvailable: + typeof state.configSnapshot?.raw === "string" || + !!state.configSnapshot?.config || + !!state.configForm, } satisfies Omit< ConfigProps, | "formMode" diff --git a/ui/src/ui/controllers/config.test.ts b/ui/src/ui/controllers/config.test.ts index 0cf4d0d383f9..a204672b2cf3 100644 --- a/ui/src/ui/controllers/config.test.ts +++ b/ui/src/ui/controllers/config.test.ts @@ -183,7 +183,7 @@ describe("applyConfigSnapshot", () => { expect(state.configDraftBaseHash).toBe("hash-remote"); }); - it("forces form mode when the snapshot does not include raw text", () => { + it("keeps raw mode when editable config can be serialized without raw text", () => { const state = createState(); state.configFormMode = "raw"; @@ -195,7 +195,7 @@ describe("applyConfigSnapshot", () => { raw: null, }); - expect(state.configFormMode).toBe("form"); + expect(state.configFormMode).toBe("raw"); expect(state.configRaw).toBe('{\n "gateway": {\n "mode": "local"\n }\n}\n'); }); }); @@ -707,6 +707,29 @@ describe("applyConfig", () => { }); describe("saveConfig", () => { + it("submits generated raw text when the snapshot did not include raw text", async () => { + const request = createRequestWithConfigGet(); + const state = createState(); + state.connected = true; + state.client = { request } as unknown as ConfigState["client"]; + state.configFormMode = "raw"; + applyConfigSnapshot(state, { + hash: "hash-generated-raw", + sourceConfig: { gateway: { mode: "local" } }, + config: { gateway: { mode: "local", runtimeOnly: true } }, + valid: true, + issues: [], + raw: null, + }); + + await saveConfig(state); + + expect(request).toHaveBeenCalledWith("config.set", { + raw: '{\n "gateway": {\n "mode": "local"\n }\n}\n', + baseHash: "hash-generated-raw", + }); + }); + it("submits the original draft base hash after a dirty config refresh", async () => { const request = createRequestWithConfigGet(); const state = createState(); diff --git a/ui/src/ui/controllers/config.ts b/ui/src/ui/controllers/config.ts index ba860d0eaebd..6ea358194a38 100644 --- a/ui/src/ui/controllers/config.ts +++ b/ui/src/ui/controllers/config.ts @@ -113,7 +113,7 @@ export function applyConfigSnapshot( const draftBaseHash = state.configDraftBaseHash ?? state.configSnapshot?.hash ?? null; state.configSnapshot = snapshot; const editableConfig = resolveEditableSnapshotConfig(snapshot); - const rawAvailable = typeof snapshot.raw === "string"; + const rawAvailable = typeof snapshot.raw === "string" || !!editableConfig || !!state.configForm; if (!rawAvailable && state.configFormMode === "raw") { state.configFormMode = "form"; } @@ -161,9 +161,6 @@ function asJsonSchema(value: unknown): JsonSchema | null { * gateway's Zod validation always sees correctly typed values. */ function serializeFormForSubmit(state: ConfigState): string { - if (state.configFormMode === "raw" && typeof state.configSnapshot?.raw !== "string") { - throw new Error("Raw config editing is unavailable for this snapshot. Switch to Form mode."); - } if (state.configFormMode !== "form" || !state.configForm) { return state.configRaw; }