diff --git a/CHANGELOG.md b/CHANGELOG.md index 6661a5a3d..281ea1e06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Docs: https://docs.clawd.bot - BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing. - Web UI: hide internal `message_id` hints in chat bubbles. - Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent. +- Web UI: keep raw config edits from toggling channel save state; enable save/apply on raw changes only. (#1673) Thanks @Glucksberg. - Heartbeat: normalize target identifiers for consistent routing. - TUI: reload history after gateway reconnect to restore session state. (#1663) - Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639) diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 830434595..996f09dd0 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -12,12 +12,7 @@ import { SYNTHETIC_BASE_URL, SYNTHETIC_MODEL_CATALOG, } from "./synthetic-models.js"; -import { - buildVeniceModelDefinition, - discoverVeniceModels, - VENICE_BASE_URL, - VENICE_MODEL_CATALOG, -} from "./venice-models.js"; +import { discoverVeniceModels, VENICE_BASE_URL } from "./venice-models.js"; type ModelsConfig = NonNullable; export type ProviderConfig = NonNullable[string]; diff --git a/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts b/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts index 29ebe3265..221410316 100644 --- a/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts +++ b/src/agents/models-config.skips-writing-models-json-no-env-token.test.ts @@ -48,15 +48,29 @@ describe("models-config", () => { const previous = process.env.COPILOT_GITHUB_TOKEN; const previousGh = process.env.GH_TOKEN; const previousGithub = process.env.GITHUB_TOKEN; + const previousVenice = process.env.VENICE_API_KEY; + const previousKimiCode = process.env.KIMICODE_API_KEY; + const previousKimiCodeAlt = process.env.KIMI_CODE_API_KEY; const previousMinimax = process.env.MINIMAX_API_KEY; const previousMoonshot = process.env.MOONSHOT_API_KEY; const previousSynthetic = process.env.SYNTHETIC_API_KEY; + const previousAwsAccessKey = process.env.AWS_ACCESS_KEY_ID; + const previousAwsSecretKey = process.env.AWS_SECRET_ACCESS_KEY; + const previousAwsProfile = process.env.AWS_PROFILE; + const previousAwsBearer = process.env.AWS_BEARER_TOKEN; delete process.env.COPILOT_GITHUB_TOKEN; delete process.env.GH_TOKEN; delete process.env.GITHUB_TOKEN; + delete process.env.VENICE_API_KEY; + delete process.env.KIMICODE_API_KEY; + delete process.env.KIMI_CODE_API_KEY; delete process.env.MINIMAX_API_KEY; delete process.env.MOONSHOT_API_KEY; delete process.env.SYNTHETIC_API_KEY; + delete process.env.AWS_ACCESS_KEY_ID; + delete process.env.AWS_SECRET_ACCESS_KEY; + delete process.env.AWS_PROFILE; + delete process.env.AWS_BEARER_TOKEN; try { vi.resetModules(); @@ -79,12 +93,26 @@ describe("models-config", () => { else process.env.GH_TOKEN = previousGh; if (previousGithub === undefined) delete process.env.GITHUB_TOKEN; else process.env.GITHUB_TOKEN = previousGithub; + if (previousVenice === undefined) delete process.env.VENICE_API_KEY; + else process.env.VENICE_API_KEY = previousVenice; + if (previousKimiCode === undefined) delete process.env.KIMICODE_API_KEY; + else process.env.KIMICODE_API_KEY = previousKimiCode; + if (previousKimiCodeAlt === undefined) delete process.env.KIMI_CODE_API_KEY; + else process.env.KIMI_CODE_API_KEY = previousKimiCodeAlt; if (previousMinimax === undefined) delete process.env.MINIMAX_API_KEY; else process.env.MINIMAX_API_KEY = previousMinimax; if (previousMoonshot === undefined) delete process.env.MOONSHOT_API_KEY; else process.env.MOONSHOT_API_KEY = previousMoonshot; if (previousSynthetic === undefined) delete process.env.SYNTHETIC_API_KEY; else process.env.SYNTHETIC_API_KEY = previousSynthetic; + if (previousAwsAccessKey === undefined) delete process.env.AWS_ACCESS_KEY_ID; + else process.env.AWS_ACCESS_KEY_ID = previousAwsAccessKey; + if (previousAwsSecretKey === undefined) delete process.env.AWS_SECRET_ACCESS_KEY; + else process.env.AWS_SECRET_ACCESS_KEY = previousAwsSecretKey; + if (previousAwsProfile === undefined) delete process.env.AWS_PROFILE; + else process.env.AWS_PROFILE = previousAwsProfile; + if (previousAwsBearer === undefined) delete process.env.AWS_BEARER_TOKEN; + else process.env.AWS_BEARER_TOKEN = previousAwsBearer; } }); }); diff --git a/src/agents/venice-models.ts b/src/agents/venice-models.ts index 25716cc51..32bd2f93b 100644 --- a/src/agents/venice-models.ts +++ b/src/agents/venice-models.ts @@ -340,7 +340,9 @@ export async function discoverVeniceModels(): Promise { }); if (!response.ok) { - console.warn(`[venice-models] Failed to discover models: HTTP ${response.status}, using static catalog`); + console.warn( + `[venice-models] Failed to discover models: HTTP ${response.status}, using static catalog`, + ); return VENICE_MODEL_CATALOG.map(buildVeniceModelDefinition); } @@ -351,7 +353,9 @@ export async function discoverVeniceModels(): Promise { } // Merge discovered models with catalog metadata - const catalogById = new Map(VENICE_MODEL_CATALOG.map((m) => [m.id, m])); + const catalogById = new Map( + VENICE_MODEL_CATALOG.map((m) => [m.id, m]), + ); const models: ModelDefinitionConfig[] = []; for (const apiModel of data.data) { diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index d9834d0ec..0543950a3 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -512,7 +512,6 @@ export function renderApp(state: AppViewState) { activeSubsection: state.configActiveSubsection, onRawChange: (next) => { state.configRaw = next; - state.configFormDirty = true; }, onFormModeChange: (mode) => (state.configFormMode = mode), onFormPatch: (path, value) => updateConfigFormValue(state, path, value), diff --git a/ui/src/ui/views/config.browser.test.ts b/ui/src/ui/views/config.browser.test.ts index c64a4c788..bcd240fbb 100644 --- a/ui/src/ui/views/config.browser.test.ts +++ b/ui/src/ui/views/config.browser.test.ts @@ -96,6 +96,34 @@ describe("config view", () => { expect(applyButton?.disabled).toBe(true); }); + it("enables save and apply when raw changes", () => { + const container = document.createElement("div"); + render( + renderConfig({ + ...baseProps(), + formMode: "raw", + raw: "{\n gateway: { mode: \"local\" }\n}\n", + originalRaw: "{\n}\n", + }), + container, + ); + + const saveButton = Array.from( + container.querySelectorAll("button"), + ).find((btn) => btn.textContent?.trim() === "Save") as + | HTMLButtonElement + | undefined; + const applyButton = Array.from( + container.querySelectorAll("button"), + ).find((btn) => btn.textContent?.trim() === "Apply") as + | HTMLButtonElement + | undefined; + expect(saveButton).not.toBeUndefined(); + expect(applyButton).not.toBeUndefined(); + expect(saveButton?.disabled).toBe(false); + expect(applyButton?.disabled).toBe(false); + }); + it("switches mode via the sidebar toggle", () => { const container = document.createElement("div"); const onFormModeChange = vi.fn();