From 9c63c41283b3b30cba7f7e60e49b8dfaf96b3524 Mon Sep 17 00:00:00 2001 From: Devvyn <22340871+opticon454@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:53:06 +0800 Subject: [PATCH] feat(settings): add an Apply button that saves without closing Switches such as MCP server sync only unlock their controls once saved, and Save closed the modal, so the user had to reopen Settings to continue. Apply runs the same save, keeps the modal open and refreshes the dependent groups (MCP sync, custom model endpoints, CLI management) in place. --- .changeset/settings-apply-button.md | 5 ++ src/web/public/index.html | 2 + src/web/public/settings-ui.js | 45 +++++++++++++- src/web/public/styles.css | 6 ++ test/settings-apply-button.test.ts | 93 +++++++++++++++++++++++++++++ 5 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 .changeset/settings-apply-button.md create mode 100644 test/settings-apply-button.test.ts diff --git a/.changeset/settings-apply-button.md b/.changeset/settings-apply-button.md new file mode 100644 index 00000000..950394df --- /dev/null +++ b/.changeset/settings-apply-button.md @@ -0,0 +1,5 @@ +--- +'aicodeman': minor +--- + +Settings has an Apply button next to Save. It saves the same way but keeps Settings open and refreshes the groups that depend on a saved value, so switching on MCP server sync (or custom model endpoints, or CLI management) brings up its controls straight away instead of needing a close-and-reopen. The MCP sync hint now says to Apply or Save first. diff --git a/src/web/public/index.html b/src/web/public/index.html index 335d3785..d28db067 100644 --- a/src/web/public/index.html +++ b/src/web/public/index.html @@ -1635,6 +1635,7 @@ Save; row-reverse keeps Save to the left of it on phones. -->
+
@@ -3037,6 +3038,7 @@
+
diff --git a/src/web/public/settings-ui.js b/src/web/public/settings-ui.js index a419121b..a7c2f77a 100644 --- a/src/web/public/settings-ui.js +++ b/src/web/public/settings-ui.js @@ -1225,7 +1225,7 @@ Object.assign(CodemanApp.prototype, { }; // Switched on in this modal but not saved yet: the routes would only answer "disabled". if (!this._mcpSyncSavedOn) { - show('Save settings to turn MCP sync on first, then reopen Settings to preview or sync.'); + show('Apply or Save settings to turn MCP sync on first, then preview or sync.'); return; } if (apply && !confirm('Add missing MCP servers to every installed, enabled CLI\'s config file? Env values and headers on those servers are copied too.')) return; @@ -2506,7 +2506,25 @@ Object.assign(CodemanApp.prototype, { claudeEl.className = 'voice-provider-status' + (status?.available ? ' active' : ''); }, + /** + * Apply button: the same save as Save, but the modal stays open and the groups that depend on + * a saved value (MCP sync, custom model endpoints, CLI management) are refreshed in place, so + * a switch that unlocks more settings needs no close-and-reopen. It is a wrapper rather than + * an option on saveAppSettings() so that function's signature (which tests locate by text) + * stays as it was. + */ + async applyAppSettings() { + if (this._applyingSettings) return; + this._applyingSettings = true; + try { + await this.saveAppSettings(); + } finally { + this._applyingSettings = false; + } + }, + async saveAppSettings() { + const keepOpen = this._applyingSettings === true; // Gesture overlay is injected at page render (server-side), so a change to it // only takes effect on reload — remember the prior value to decide below. const _prev = this.loadAppSettingsFromStorage(); @@ -2866,7 +2884,7 @@ Object.assign(CodemanApp.prototype, { this.saveAppSettingsToStorage(settings); const cb = document.getElementById('appSettingsTunnelEnabled'); if (cb) cb.checked = false; - this.closeAppSettings(); + if (!keepOpen) this.closeAppSettings(); return; } @@ -2881,7 +2899,7 @@ Object.assign(CodemanApp.prototype, { if (webhookError) { this.showToast(`Settings saved, but not the webhook: ${webhookError}`, 'warning'); } else { - this.showToast('Settings saved', 'success'); + this.showToast(keepOpen ? 'Settings applied' : 'Settings saved', 'success'); } // Show tunnel-specific feedback if toggled on @@ -2895,6 +2913,8 @@ Object.assign(CodemanApp.prototype, { if (webhookError) { document.getElementById('webhookGroup')?.scrollIntoView({ block: 'center' }); + } else if (keepOpen) { + this._refreshSettingsAfterApply(settings); } else { this.closeAppSettings(); } @@ -2916,6 +2936,25 @@ Object.assign(CodemanApp.prototype, { } }, + /** + * After Apply: bring the groups whose contents depend on a SAVED value up to date without + * reopening the modal. openAppSettings does the same on open; this is the part of it that + * a save can change, without touching what the user is editing or the scroll position. + */ + _refreshSettingsAfterApply(settings) { + // The MCP routes read the saved flag, so switching it on is only usable from now. + this._mcpSyncSavedOn = settings.mcpSyncEnabled === true; + const out = this.$('mcpSyncResult'); + if (this._mcpSyncSavedOn && out && out.textContent.startsWith('Apply or Save settings')) { + out.style.display = 'none'; + out.innerHTML = ''; + } + this.applyMcpSyncVisibility(); + this.applyCustomModelEndpointsVisibility(); + this.applyCliManagementVisibility(); + this._applyDoctorAdminGate(); + }, + // Load model configuration from server for the settings modal async loadModelConfigForSettings() { try { diff --git a/src/web/public/styles.css b/src/web/public/styles.css index df4d47bf..b5e7ead1 100644 --- a/src/web/public/styles.css +++ b/src/web/public/styles.css @@ -16974,6 +16974,12 @@ html[data-tab-orientation='vertical'] .home-sessions { transform 0.1s ease; } +:is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-head-save.set-head-apply { + background: transparent; + color: var(--text); + box-shadow: inset 0 0 0 1px var(--border); +} + :is(#appSettingsModal, #sessionOptionsModal, #createCaseModal) .set-head-save:hover { filter: brightness(1.08); } diff --git a/test/settings-apply-button.test.ts b/test/settings-apply-button.test.ts new file mode 100644 index 00000000..c8e989e1 --- /dev/null +++ b/test/settings-apply-button.test.ts @@ -0,0 +1,93 @@ +/** + * @fileoverview Settings "Apply": saves like Save but keeps the modal open and refreshes the + * groups that depend on a saved value (MCP sync, custom model endpoints, CLI management). + */ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import vm from 'node:vm'; +import { describe, expect, it, vi } from 'vitest'; + +const publicDir = resolve(import.meta.dirname, '../src/web/public'); +const html = readFileSync(resolve(publicDir, 'index.html'), 'utf8'); +const settingsUi = readFileSync(resolve(publicDir, 'settings-ui.js'), 'utf8'); + +function loadApp() { + const CodemanApp = function CodemanApp() {} as unknown as { prototype: Record }; + const context = vm.createContext({ + CodemanApp, + localStorage: { getItem: () => null, setItem: () => {} }, + document: { getElementById: () => null, querySelectorAll: () => [], addEventListener: () => {} }, + window: {}, + console, + }); + vm.runInContext(settingsUi, context, { filename: 'settings-ui.js' }); + return new (CodemanApp as unknown as new () => Record)(); +} + +describe('Settings Apply button', () => { + it('sits next to Save in the footer and the phone header, wired to applyAppSettings', () => { + const start = html.indexOf('