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.
This commit is contained in:
Devvyn committed 2026-10-09 18:41:59 +08:00
1 parent 3a0cee6b90
commit 9c63c41283
5 files changed
+148 -3

No files matched your search

+2
View File
@@ -1635,6 +1635,7 @@
Save; row-reverse keeps Save to the left of it on phones. -->
<div class="set-head-actions">
<button class="modal-close" onclick="app.closeAppSettings()" aria-label="Close app settings">&times;</button>
<button class="set-head-save set-head-apply" onclick="app.applyAppSettings()" title="Save and keep Settings open">Apply</button>
<button class="set-head-save" onclick="app.saveAppSettings()">Save</button>
</div>
</div>
@@ -3037,6 +3038,7 @@
</div>
<div class="form-actions set-foot">
<button class="btn-toolbar" onclick="app.closeAppSettings()">Cancel</button>
<button class="btn-toolbar" onclick="app.applyAppSettings()" title="Save and keep Settings open">Apply</button>
<button class="btn-toolbar btn-primary" onclick="app.saveAppSettings()">Save</button>
</div>
</div>
+42 -3
View File
@@ -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 {
+6
View File
@@ -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);
}