Skip to content

Commit f82a2ff

Browse files
committed
Merge pull request #91 from SakuraByteCore/feat/config-template-confirm-toggle
feat(ui): add toggle for config template diff confirmation
2 parents 60bcdcb + e3bb309 commit f82a2ff

9 files changed

Lines changed: 169 additions & 25 deletions

tests/unit/agents-modal-guards.test.mjs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,55 @@ test('applyConfigTemplate keeps the successful apply result when only the refres
135135
}]);
136136
});
137137

138+
test('applyConfigTemplate applies immediately when diff confirm is disabled', async () => {
139+
let previewCalls = 0;
140+
let applyCalls = 0;
141+
const methods = createCodexConfigMethods({
142+
api: async (action) => {
143+
if (action === 'preview-config-template-diff') {
144+
previewCalls += 1;
145+
return {
146+
diff: {
147+
lines: [{ type: 'add', value: 'model = "qwen-plus"' }],
148+
stats: { added: 1, removed: 0, unchanged: 0 },
149+
hasChanges: true
150+
}
151+
};
152+
}
153+
if (action === 'apply-config-template') {
154+
applyCalls += 1;
155+
return { success: true };
156+
}
157+
return { success: true };
158+
},
159+
getProviderConfigModeMeta() {
160+
return null;
161+
}
162+
});
163+
const context = {
164+
...methods,
165+
showConfigTemplateModal: true,
166+
configTemplateApplying: false,
167+
configTemplateContent: 'draft-template',
168+
configTemplateDiffConfirmEnabled: false,
169+
shownMessages: [],
170+
showMessage(message, type) {
171+
this.shownMessages.push({ message, type });
172+
},
173+
async loadAll() {}
174+
};
175+
176+
await methods.applyConfigTemplate.call(context);
177+
178+
assert.strictEqual(previewCalls, 0);
179+
assert.strictEqual(applyCalls, 1);
180+
assert.strictEqual(context.showConfigTemplateModal, false);
181+
assert.deepStrictEqual(context.shownMessages, [{
182+
message: '模板已应用',
183+
type: 'success'
184+
}]);
185+
});
186+
138187
test('runHealthCheck treats backend error payloads as failures', async () => {
139188
const methods = createCodexConfigMethods({
140189
api: async () => ({ error: 'health failed' }),

tests/unit/config-tabs-ui.test.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ test('config template keeps expected config tabs in top and side navigation', ()
6060
assert.match(html, /settingsTab === 'backup'/);
6161
assert.match(html, /settingsTab === 'trash'/);
6262
assert.match(html, /settingsTab === 'device'/);
63+
assert.match(html, /setConfigTemplateDiffConfirmEnabled/);
64+
assert.match(html, /configTemplateDiffConfirmEnabled/);
6365
assert.match(html, /sessionTrashCount/);
6466
assert.match(html, /v-if="taskOrchestrationTabEnabled" class="top-tab"[\s\S]*id="tab-orchestration"/);
6567
assert.match(html, /v-if="taskOrchestrationTabEnabled" class="side-section" role="navigation" aria-label=""/);

tests/unit/web-ui-behavior-parity.test.mjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,8 @@ test('captured bundled app skeleton only exposes expected data key drift versus
369369
'configTemplateDiffStats',
370370
'configTemplateDiffHasChangesValue',
371371
'configTemplateDiffFingerprint',
372-
'_configTemplateDiffPreviewRequestToken'
372+
'_configTemplateDiffPreviewRequestToken',
373+
'configTemplateDiffConfirmEnabled'
373374
);
374375
if (parityAgainstHead) {
375376
const allowedExtraKeySet = new Set(allowedExtraCurrentKeys);
@@ -447,7 +448,9 @@ test('captured bundled app skeleton only exposes expected data key drift versus
447448
'onConfigTemplateContentInput',
448449
'buildConfigTemplateDiffFingerprint',
449450
'prepareConfigTemplateDiff',
450-
'hasConfigTemplateDiffChanges'
451+
'hasConfigTemplateDiffChanges',
452+
'normalizeConfigTemplateDiffConfirmEnabled',
453+
'setConfigTemplateDiffConfirmEnabled'
451454
);
452455
const allowedMissingCurrentMethodKeys = [
453456
'closeInstallModal',

web-ui/app.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
} from './modules/app.constants.mjs';
77
import { createAppComputed } from './modules/app.computed.index.mjs';
88
import { createAppMethods } from './modules/app.methods.index.mjs';
9+
import { loadConfigTemplateDiffConfirmEnabledFromStorage } from './modules/config-template-confirm-pref.mjs';
910

1011
document.addEventListener('DOMContentLoaded', () => {
1112
if (typeof Vue === 'undefined') {
@@ -86,10 +87,12 @@ document.addEventListener('DOMContentLoaded', () => {
8687
configTemplateDiffHasChangesValue: false,
8788
configTemplateDiffFingerprint: '',
8889
_configTemplateDiffPreviewRequestToken: null,
90+
configTemplateDiffConfirmEnabled: true,
8991
codexApplying: false,
9092
_pendingCodexApplyOptions: null,
9193
agentsContent: '',
9294
agentsPath: '',
95+
agentsPath: '',
9396
agentsExists: false,
9497
agentsLineEnding: '\n',
9598
agentsLoading: false,
@@ -413,6 +416,7 @@ document.addEventListener('DOMContentLoaded', () => {
413416
this.restoreSessionPinnedMap();
414417
this.shareCommandPrefix = this.normalizeShareCommandPrefix(localStorage.getItem('codexmateShareCommandPrefix'));
415418
this.sessionTrashEnabled = this.normalizeSessionTrashEnabled(localStorage.getItem('codexmateSessionTrashEnabled'));
419+
this.configTemplateDiffConfirmEnabled = loadConfigTemplateDiffConfirmEnabledFromStorage(localStorage);
416420
window.addEventListener('resize', this.onWindowResize);
417421
window.addEventListener('keydown', this.handleGlobalKeydown);
418422
window.addEventListener('beforeunload', this.handleBeforeUnload);

web-ui/modules/app.methods.codex-config.mjs

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { runLatestOnlyQueue } from '../logic.mjs';
2+
import { normalizeConfigTemplateDiffConfirmEnabled } from './config-template-confirm-pref.mjs';
23

34
function hasResponseError(response) {
45
if (!response || typeof response !== 'object') {
@@ -736,6 +737,40 @@ export function createCodexConfigMethods(options = {}) {
736737
return;
737738
}
738739

740+
// Default to two-step confirmation when the setting is unset.
741+
// (The normalize helper lives in session-actions; keep a safe fallback here.)
742+
const shouldUseTwoStepConfirm = normalizeConfigTemplateDiffConfirmEnabled(this.configTemplateDiffConfirmEnabled);
743+
744+
const performApply = async () => {
745+
this.configTemplateApplying = true;
746+
try {
747+
const res = await api('apply-config-template', {
748+
template: this.configTemplateContent
749+
});
750+
if (res.error) {
751+
this.showMessage(res.error, 'error');
752+
return;
753+
}
754+
this.showMessage('模板已应用', 'success');
755+
this.closeConfigTemplateModal({ force: true });
756+
try {
757+
await this.loadAll();
758+
} catch (_) {
759+
this.showMessage('模板已应用,但界面刷新失败,请手动刷新', 'error');
760+
}
761+
} catch (e) {
762+
this.showMessage('应用模板失败', 'error');
763+
} finally {
764+
this.configTemplateApplying = false;
765+
}
766+
};
767+
768+
// One-step mode: apply immediately unless user explicitly entered the diff preview state.
769+
if (!shouldUseTwoStepConfirm && !this.configTemplateDiffVisible) {
770+
await performApply();
771+
return;
772+
}
773+
739774
if (!this.configTemplateDiffVisible) {
740775
await this.prepareConfigTemplateDiff();
741776
return;
@@ -757,27 +792,7 @@ export function createCodexConfigMethods(options = {}) {
757792
return;
758793
}
759794

760-
this.configTemplateApplying = true;
761-
try {
762-
const res = await api('apply-config-template', {
763-
template: this.configTemplateContent
764-
});
765-
if (res.error) {
766-
this.showMessage(res.error, 'error');
767-
return;
768-
}
769-
this.showMessage('模板已应用', 'success');
770-
this.closeConfigTemplateModal({ force: true });
771-
try {
772-
await this.loadAll();
773-
} catch (_) {
774-
this.showMessage('模板已应用,但界面刷新失败,请手动刷新', 'error');
775-
}
776-
} catch (e) {
777-
this.showMessage('应用模板失败', 'error');
778-
} finally {
779-
this.configTemplateApplying = false;
780-
}
795+
await performApply();
781796
}
782797
};
783798
}

web-ui/modules/app.methods.session-actions.mjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import {
2+
normalizeConfigTemplateDiffConfirmEnabled,
3+
persistConfigTemplateDiffConfirmEnabledToStorage
4+
} from './config-template-confirm-pref.mjs';
5+
16
export function createSessionActionMethods(options = {}) {
27
const {
38
api,
@@ -170,6 +175,10 @@ export function createSessionActionMethods(options = {}) {
170175
return true;
171176
},
172177

178+
normalizeConfigTemplateDiffConfirmEnabled(value) {
179+
return normalizeConfigTemplateDiffConfirmEnabled(value);
180+
},
181+
173182
setSessionTrashEnabled(value) {
174183
const enabled = this.normalizeSessionTrashEnabled(value);
175184
this.sessionTrashEnabled = enabled;
@@ -178,6 +187,12 @@ export function createSessionActionMethods(options = {}) {
178187
} catch (_) {}
179188
},
180189

190+
setConfigTemplateDiffConfirmEnabled(value) {
191+
const enabled = this.normalizeConfigTemplateDiffConfirmEnabled(value);
192+
this.configTemplateDiffConfirmEnabled = enabled;
193+
persistConfigTemplateDiffConfirmEnabledToStorage(enabled);
194+
},
195+
181196
getShareCommandPrefixInvocation() {
182197
const prefix = this.normalizeShareCommandPrefix(this.shareCommandPrefix);
183198
return prefix === 'codexmate' ? 'codexmate' : 'npm start';
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export const CONFIG_TEMPLATE_DIFF_CONFIRM_STORAGE_KEY = 'codexmateConfigTemplateDiffConfirmEnabled';
2+
3+
export function normalizeConfigTemplateDiffConfirmEnabled(value) {
4+
if (value === false) return false;
5+
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
6+
if (normalized === '0' || normalized === 'false' || normalized === 'off' || normalized === 'no') {
7+
return false;
8+
}
9+
return true;
10+
}
11+
12+
export function loadConfigTemplateDiffConfirmEnabledFromStorage(storage = null) {
13+
const target = storage || (typeof localStorage !== 'undefined' ? localStorage : null);
14+
if (!target || typeof target.getItem !== 'function') {
15+
return true;
16+
}
17+
try {
18+
return normalizeConfigTemplateDiffConfirmEnabled(target.getItem(CONFIG_TEMPLATE_DIFF_CONFIRM_STORAGE_KEY));
19+
} catch (_) {
20+
return true;
21+
}
22+
}
23+
24+
export function persistConfigTemplateDiffConfirmEnabledToStorage(enabled, storage = null) {
25+
const target = storage || (typeof localStorage !== 'undefined' ? localStorage : null);
26+
if (!target || typeof target.setItem !== 'function') {
27+
return;
28+
}
29+
try {
30+
target.setItem(CONFIG_TEMPLATE_DIFF_CONFIRM_STORAGE_KEY, enabled ? 'true' : 'false');
31+
} catch (_) {}
32+
}
33+

web-ui/partials/index/modal-config-template-agents.html

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@
4242
@input="onConfigTemplateContentInput"
4343
placeholder="在这里编辑 config.toml 模板内容"></textarea>
4444
<div class="template-editor-warning">
45-
工具不会自动改动 `config.toml`。保存需两步:先点“确认”预览差异,再点“应用”写入。
45+
<template v-if="configTemplateDiffConfirmEnabled">
46+
两步确认:先预览差异,再应用写入。
47+
</template>
48+
<template v-else>
49+
一步应用:点击“应用”直接写入。
50+
</template>
4651
<div v-if="configTemplateDiffVisible && (configTemplateDiffLoading || configTemplateApplying)" class="agents-diff-hint">正在生成差异或应用中,操作暂不可用。</div>
4752
<div v-else-if="configTemplateDiffVisible && configTemplateDiffError" class="agents-diff-hint">差异预览失败,请返回编辑后重试。</div>
4853
<div v-else-if="configTemplateDiffVisible && !configTemplateDiffHasChanges" class="agents-diff-hint">未检测到改动,可返回编辑继续修改或取消退出。</div>
@@ -60,7 +65,10 @@
6065
返回编辑
6166
</button>
6267
<button class="btn btn-confirm" @click="applyConfigTemplate" :disabled="configTemplateApplying || configTemplateDiffLoading || (configTemplateDiffVisible && !configTemplateDiffHasChanges)">
63-
{{ configTemplateApplying ? (configTemplateDiffVisible ? '应用中...' : '确认中...') : (configTemplateDiffVisible ? '应用' : '确认') }}
68+
{{ configTemplateApplying
69+
? (configTemplateDiffVisible || !configTemplateDiffConfirmEnabled ? '应用中...' : '确认中...')
70+
: (configTemplateDiffVisible || !configTemplateDiffConfirmEnabled ? '应用' : '确认')
71+
}}
6472
</button>
6573
</div>
6674
</div>

web-ui/partials/index/panel-settings.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,21 @@
177177
id="settings-panel-device"
178178
role="tabpanel"
179179
aria-labelledby="settings-tab-device">
180+
<div class="selector-section">
181+
<div class="selector-header">
182+
<span class="selector-title">配置模板二次确认</span>
183+
</div>
184+
<label class="health-remote-toggle">
185+
<input
186+
type="checkbox"
187+
:checked="configTemplateDiffConfirmEnabled"
188+
@change="setConfigTemplateDiffConfirmEnabled($event.target.checked)">
189+
<span>应用模板前先预览差异(两步:确认 → 应用)</span>
190+
</label>
191+
<div class="config-template-hint">
192+
开启后:先展示差异预览,再确认写入。
193+
</div>
194+
</div>
180195
<div class="selector-section">
181196
<div class="selector-header">
182197
<span class="selector-title">配置重置</span>

0 commit comments

Comments
 (0)