Merge pull request #565: feat(settings): add an Apply button that saves without closing

This commit is contained in:
Codeman maintainer committed 2026-10-10 02:53:28 +02:00
commit 5e0cea6ec8
9 files changed
+280 -9

No files matched your search

+2
View File
@@ -721,6 +721,8 @@
// Dynamic common status / toasts
'Settings saved': '设置已保存',
'Settings applied': '设置已应用',
'Save and keep Settings open': '保存并保持设置打开',
'Settings saved locally': '设置已保存到本机',
'Tunnel active': '隧道已启用',
'Tunnel starting — QR code will appear when ready...': '隧道正在启动,准备好后将显示二维码…',
+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>
@@ -3117,6 +3118,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>
+1 -1
View File
@@ -3552,7 +3552,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
}
/* Save + Close are the two ways out of the sheet (save-and-close vs
discard-and-close), hit in the same corner with the same thumb, so here —
discard-and-close; Apply saves but keeps the sheet open), hit in the same corner with the same thumb, so here —
and only here, since Save is header-only below 860px — they share a
recessed tray and matching pill geometry instead of reading as a fat
accent pill parked beside a stray × glyph. Tray colors come from skin
+58 -6
View File
@@ -1227,12 +1227,12 @@ Object.assign(CodemanApp.prototype, {
/** Preview (apply=false) or run (apply=true) the MCP server sync across enabled CLIs. */
async mcpSync(apply) {
const out = this.$('mcpSyncResult');
const show = (html) => {
if (out) { out.style.display = 'block'; out.innerHTML = html; }
const show = (html, hint = '') => {
if (out) { out.style.display = 'block'; out.innerHTML = html; out.dataset.hint = hint; }
};
// 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.', 'save-first');
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;
@@ -2513,7 +2513,31 @@ 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 MCP sync group (its
* Preview and Sync need the saved flag) and the CLI management writes are refreshed in place, so
* turning either on 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.
*
* `_keepSettingsOpenOnce` is the one-shot intent and `_applyInFlight` the double-click guard:
* saveAppSettings() consumes the intent before its first await, so a Save clicked while an Apply
* is still in flight is an ordinary Save and closes the modal.
*/
async applyAppSettings() {
if (this._applyInFlight) return;
this._applyInFlight = true;
this._keepSettingsOpenOnce = true;
try {
await this.saveAppSettings();
} finally {
this._applyInFlight = false;
this._keepSettingsOpenOnce = false;
}
},
async saveAppSettings() {
const keepOpen = this._keepSettingsOpenOnce === true;
this._keepSettingsOpenOnce = false;
// 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();
@@ -2859,6 +2883,7 @@ Object.assign(CodemanApp.prototype, {
...serverSettings
} = settings;
let webhookError = '';
let serverSaved = false;
try {
const res = await this._apiPut('/api/settings', {
...serverSettings,
@@ -2876,10 +2901,14 @@ Object.assign(CodemanApp.prototype, {
this.saveAppSettingsToStorage(settings);
const cb = document.getElementById('appSettingsTunnelEnabled');
if (cb) cb.checked = false;
this.closeAppSettings();
if (!keepOpen) this.closeAppSettings();
return;
}
// `_apiPut` answers null or a non-ok response instead of throwing, so this is the only
// evidence the server kept the flags the Apply refresh below reads.
serverSaved = !!res?.ok;
// Save model configuration separately
await this.saveModelConfigFromSettings();
@@ -2891,7 +2920,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
@@ -2903,9 +2932,13 @@ Object.assign(CodemanApp.prototype, {
this.showToast('Settings saved locally', 'warning');
}
// Only when the settings PUT landed: after a 400 or a dropped connection the server still has the
// old flags, and a webhook-only failure still saved the rest, so this runs ahead of that branch.
if (keepOpen && serverSaved) this._refreshSettingsAfterApply(settings);
if (webhookError) {
document.getElementById('webhookGroup')?.scrollIntoView({ block: 'center' });
} else {
} else if (!keepOpen) {
this.closeAppSettings();
}
@@ -2926,6 +2959,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.dataset?.hint === 'save-first') {
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
@@ -17005,6 +17005,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);
}