Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions tests/unit/config-tabs-ui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ test('config template keeps expected config tabs in top and side navigation', ()
assert.match(html, /class="codex-config-grid"/);
assert.match(html, /onSettingsTabClick\('backup'\)/);
assert.match(html, /onSettingsTabClick\('trash'\)/);
assert.match(html, /onSettingsTabClick\('device'\)/);
assert.match(html, /settingsTab === 'backup'/);
assert.match(html, /settingsTab === 'trash'/);
assert.match(html, /settingsTab === 'device'/);
assert.match(html, /sessionTrashCount/);
assert.match(html, /id="side-tab-market"/);
assert.match(html, /id="tab-market"/);
Expand Down Expand Up @@ -107,17 +109,29 @@ test('config template keeps expected config tabs in top and side navigation', ()
assert.doesNotMatch(html, /class="market-ecosystem-grid"/);
assert.match(html, /id="settings-tab-backup"/);
assert.match(html, /id="settings-tab-trash"/);
assert.match(html, /id="settings-tab-device"/);
assert.match(html, /role="tab"/);
assert.match(html, /aria-controls="settings-panel-backup"/);
assert.match(html, /aria-controls="settings-panel-trash"/);
assert.match(html, /aria-controls="settings-panel-device"/);
assert.match(html, /:aria-selected="settingsTab === 'backup'"/);
assert.match(html, /:aria-selected="settingsTab === 'trash'"/);
assert.match(html, /:aria-selected="settingsTab === 'device'"/);
assert.match(html, /id="settings-tab-backup"[\s\S]*:tabindex="settingsTab === 'backup' \? 0 : -1"/);
assert.match(html, /id="settings-tab-trash"[\s\S]*:tabindex="settingsTab === 'trash' \? 0 : -1"/);
assert.match(html, /id="settings-tab-device"[\s\S]*:tabindex="settingsTab === 'device' \? 0 : -1"/);
assert.match(html, /id="settings-panel-backup"/);
assert.match(html, /id="settings-panel-trash"/);
assert.match(html, /id="settings-panel-device"/);
assert.match(html, /<div[\s\S]*v-show="settingsTab === 'backup'"[\s\S]*id="settings-panel-backup"[\s\S]*aria-labelledby="settings-tab-backup">/);
assert.match(html, /<div[\s\S]*v-show="settingsTab === 'trash'"[\s\S]*id="settings-panel-trash"[\s\S]*aria-labelledby="settings-tab-trash">/);
assert.match(html, /<div[\s\S]*v-show="settingsTab === 'device'"[\s\S]*id="settings-panel-device"[\s\S]*aria-labelledby="settings-tab-device">/);
assert.match(html, /id="settings-panel-device"[\s\S]*?<span class="selector-title">配置重置<\/span>/);
assert.match(html, /id="settings-panel-device"[\s\S]*?@click="resetConfig"/);
assert.doesNotMatch(
html.match(/id="panel-config-provider"[\s\S]*?<\/template>/)?.[0] || '',
/<span class="selector-title">配置重置<\/span>/
);
Comment on lines +131 to +134
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Assert the provider panel exists before the negative check.

Line 132 falls back to '', so this passes if panel-config-provider disappears or the regex stops before the moved reset block. That weakens the regression this block is trying to add.

🔧 Suggested change
+    const providerPanel = html.match(/id="panel-config-provider"[\s\S]*?<\/template>/);
+    assert(providerPanel, 'provider config panel should exist');
     assert.doesNotMatch(
-        html.match(/id="panel-config-provider"[\s\S]*?<\/template>/)?.[0] || '',
+        providerPanel[0],
         /<span class="selector-title">配置重置<\/span>/
     );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert.doesNotMatch(
html.match(/id="panel-config-provider"[\s\S]*?<\/template>/)?.[0] || '',
/<span class="selector-title"><\/span>/
);
const providerPanel = html.match(/id="panel-config-provider"[\s\S]*?<\/template>/);
assert(providerPanel, 'provider config panel should exist');
assert.doesNotMatch(
providerPanel[0],
/<span class="selector-title"><\/span>/
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/config-tabs-ui.test.mjs` around lines 131 - 134, Ensure the
provider panel is present before running the negative regex check: first capture
the match for /id="panel-config-provider"[\s\S]*?<\/template>/ and assert it's
truthy (exists) so the test fails if the panel is missing, then run
assert.doesNotMatch against the captured match[0] to confirm the moved reset
block isn't inside that panel; reference the regex/id and the test assertion
lines around assert.doesNotMatch in tests/unit/config-tabs-ui.test.mjs.

assert.match(html, /class="settings-tab-actions trash-header-actions"/);
assert.match(html, /<button class="btn-tool btn-tool-compact" @click="loadSessionTrash\(\{ forceRefresh: true \}\)"/);
assert.match(html, /<button class="btn-tool btn-tool-compact" @click="clearSessionTrash"/);
Expand Down Expand Up @@ -209,7 +223,7 @@ test('config template keeps expected config tabs in top and side navigation', ()
assert.match(templateAgentModals, /<div class="modal modal-wide" role="dialog" aria-modal="true" aria-labelledby="config-template-modal-title">/);
assert.match(templateAgentModals, /<div class="modal-title" id="config-template-modal-title">Config 模板编辑器(手动确认应用)<\/div>/);
assert.match(templateAgentModals, /<div v-if="showAgentsModal" class="modal-overlay" @click\.self="closeAgentsModal">/);
assert.match(templateAgentModals, /<div class="modal modal-wide modal-editor" role="dialog" aria-modal="true" aria-labelledby="agents-modal-title">/);
assert.match(templateAgentModals, /<div class="modal modal-wide modal-editor agents-modal" role="dialog" aria-modal="true" aria-labelledby="agents-modal-title">/);
assert.match(templateAgentModals, /<div class="modal-title" id="agents-modal-title">{{ agentsModalTitle }}<\/div>/);
assert.match(modalsBasic, /<button type="button" class="btn-remove-model" @click="removeModel\(model\)">删除<\/button>/);
assert.doesNotMatch(modalsBasic, /<span class="btn-remove-model" @click="removeModel\(model\)">删除<\/span>/);
Expand Down Expand Up @@ -326,6 +340,7 @@ test('web ui script defines provider mode metadata for codex only', () => {
assert.match(appScript, /sessionTrashHasMoreItems\(\)/);
assert.match(appScript, /sessionTrashHiddenCount\(\)/);
assert.match(appScript, /normalizeSettingsTab\(tab\)/);
assert.match(appScript, /tab === 'trash' \|\| tab === 'device'/);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Scope the device branch check to normalizeSettingsTab().

Line 343 only proves that this snippet exists somewhere in the bundle. Another method could satisfy it while normalizeSettingsTab() still rejects 'device'.

🔧 Suggested change
-    assert.match(appScript, /normalizeSettingsTab\(tab\)/);
-    assert.match(appScript, /tab === 'trash' \|\| tab === 'device'/);
+    assert.match(
+        appScript,
+        /normalizeSettingsTab\(tab\)[\s\S]*tab === 'trash' \|\| tab === 'device'/
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/config-tabs-ui.test.mjs` at line 343, The current test only checks
that a runtime snippet includes "tab === 'trash' || tab === 'device'" somewhere,
but does not verify normalizeSettingsTab actually accepts 'device'; update the
test to scope the assertion to normalizeSettingsTab by either (a)
importing/locating normalizeSettingsTab from the bundle and calling it with
'device' and asserting the result is the expected normalized string, or (b)
asserting the source for the specific function body contains the device branch
(e.g., check appScript or the function string for "function
normalizeSettingsTab" and that it includes "tab === 'device'") — target the
symbol normalizeSettingsTab in your change so the test proves that function, not
some other code, handles the 'device' case.

assert.match(appScript, /switchSettingsTab\(tab,\s*options = \{\}\)/);
assert.match(appScript, /loadSessionTrash\(options = \{\}\)/);
assert.match(appScript, /loadMoreSessionTrashItems\(\)/);
Expand Down Expand Up @@ -413,7 +428,7 @@ test('trash item styles stay aligned with session card layout and keep mobile us
styles,
/@media \(max-width: 540px\)\s*\{[\s\S]*\.session-item-copy\.session-item-pin svg,\s*[\s\S]*width:\s*16px;/
);
assert.match(styles, /@media \(max-width: 540px\)\s*\{[\s\S]*\.session-item-copy\s*\{[\s\S]*width:\s*44px;[\s\S]*height:\s*44px;[\s\S]*min-width:\s*44px;[\s\S]*min-height:\s*44px;/);
assert.match(styles, /@media \(max-width: 540px\)\s*\{[\s\S]*\.session-item-copy\s*\{[\s\S]*width:\s*36px;[\s\S]*height:\s*36px;[\s\S]*min-width:\s*36px;[\s\S]*min-height:\s*36px;/);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The mobile size check no longer proves the pin action was shrunk.

Line 431 now matches a generic .session-item-copy rule, but the surrounding assertions are about .session-item-copy.session-item-pin. A more specific pin override can slip through while this still passes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/config-tabs-ui.test.mjs` at line 431, The mobile-size regex is too
broad—it's matching any .session-item-copy rule and can miss a specific pin
override; update the assertion to target the pin selector by changing the
pattern to look for .session-item-copy.session-item-pin (or the exact selector
order used in the stylesheet) within the `@media` (max-width: 540px) block and
assert the width/height/min-width/min-height values for that selector so the
test verifies the pin override rather than a generic .session-item-copy rule.

assert.match(styles, /\.codex-config-grid\s*\{/);
assert.match(styles, /\.codex-config-grid\s*\{[\s\S]*grid-template-columns:\s*repeat\(auto-fit,\s*minmax\(min\(240px,\s*100%\),\s*1fr\)\);/);
assert.match(styles, /\.codex-config-field\s*\{/);
Expand Down
17 changes: 2 additions & 15 deletions tests/unit/web-ui-behavior-parity.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -319,15 +319,7 @@ test('captured bundled app skeleton only exposes expected data key drift versus
const headDataKeys = Object.keys(headAppOptions.data()).sort();
const extraCurrentKeys = currentDataKeys.filter((key) => !headDataKeys.includes(key)).sort();
const missingCurrentKeys = headDataKeys.filter((key) => !currentDataKeys.includes(key)).sort();
const allowedExtraCurrentKeys = [
'healthCheckDialogLastResult',
'healthCheckDialogLockedProvider',
'healthCheckDialogMessages',
'healthCheckDialogPrompt',
'healthCheckDialogSelectedProvider',
'healthCheckDialogSending',
'showHealthCheckDialog'
];
const allowedExtraCurrentKeys = [];
const allowedMissingCurrentKeys = [];
if (parityAgainstHead) {
const allowedExtraKeySet = new Set(allowedExtraCurrentKeys);
Expand All @@ -347,12 +339,7 @@ test('captured bundled app skeleton only exposes expected data key drift versus
const headMethodKeys = Object.keys(headMethods).sort();
const extraCurrentMethodKeys = currentMethodKeys.filter((key) => !headMethodKeys.includes(key)).sort();
const missingCurrentMethodKeys = headMethodKeys.filter((key) => !currentMethodKeys.includes(key)).sort();
const allowedExtraCurrentMethodKeys = [
'buildDefaultHealthCheckPrompt',
'closeHealthCheckDialog',
'openHealthCheckDialog',
'sendHealthCheckDialogMessage'
];
const allowedExtraCurrentMethodKeys = [];
if (parityAgainstHead) {
const allowedExtraMethodKeySet = new Set(allowedExtraCurrentMethodKeys);
const unexpectedExtraCurrentMethodKeys = extraCurrentMethodKeys.filter((key) => !allowedExtraMethodKeySet.has(key));
Expand Down
1 change: 1 addition & 0 deletions tests/unit/web-ui-source-bundle.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ test('bundled web ui html inlines partials without leaking include directives',

assert.match(html, /id="panel-market"/);
assert.match(html, /id="settings-panel-trash"/);
assert.match(html, /id="settings-panel-device"/);
assert.match(html, /class="modal modal-wide skills-modal"/);
assert.match(html, /<script type="module" src="\/web-ui\/app\.js"><\/script>/);
assert.doesNotMatch(html, /<script type="module" src="web-ui\/app\.js"><\/script>/);
Expand Down
5 changes: 4 additions & 1 deletion web-ui/modules/app.methods.session-trash.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,10 @@ export function createSessionTrashMethods(options = {}) {
},

normalizeSettingsTab(tab) {
return tab === 'trash' ? 'trash' : 'backup';
if (tab === 'trash' || tab === 'device') {
return tab;
}
return 'backup';
},

async onSettingsTabClick(tab) {
Expand Down
2 changes: 1 addition & 1 deletion web-ui/partials/index/layout-header.html
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ <h1 class="main-title">
<span class="sr-only">本地配置中枢,统一管理 Codex / Claude Code / OpenClaw。</span>
</p>
<p class="subtitle" v-else-if="mainTab === 'sessions'">
浏览、导出或独立查看 Codex / Claude 会话记录
统一查看与导出 Codex / Claude 会话
</p>
<p class="subtitle" v-else-if="mainTab === 'market'">
统一管理 Codex / Claude Skills,并聚焦本地导入与分发。
Expand Down
2 changes: 1 addition & 1 deletion web-ui/partials/index/modal-config-template-agents.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
</div>

<div v-if="showAgentsModal" class="modal-overlay" @click.self="closeAgentsModal">
<div class="modal modal-wide modal-editor" role="dialog" aria-modal="true" aria-labelledby="agents-modal-title">
<div class="modal modal-wide modal-editor agents-modal" role="dialog" aria-modal="true" aria-labelledby="agents-modal-title">
<div class="modal-header modal-editor-header">
<div class="modal-title" id="agents-modal-title">{{ agentsModalTitle }}</div>
<div class="modal-header-actions">
Expand Down
10 changes: 0 additions & 10 deletions web-ui/partials/index/panel-config-codex.html
Original file line number Diff line number Diff line change
Expand Up @@ -143,16 +143,6 @@
</button>
</div>

<div class="selector-section">
<div class="selector-header">
<span class="selector-title">配置重置</span>
</div>
<div class="config-template-hint">先备份 config.toml,再写默认配置。</div>
<button class="btn-tool" @click="resetConfig" :disabled="resetConfigLoading || loading || !!initError">
{{ resetConfigLoading ? '重装中...' : '重装配置' }}
</button>
</div>

</template>

<div class="selector-section">
Expand Down
26 changes: 26 additions & 0 deletions web-ui/partials/index/panel-settings.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@
回收站
<span class="settings-tab-badge">{{ sessionTrashCount }}</span>
</button>
<button
id="settings-tab-device"
role="tab"
aria-controls="settings-panel-device"
:aria-selected="settingsTab === 'device'"
:tabindex="settingsTab === 'device' ? 0 : -1"
:class="['config-subtab', { active: settingsTab === 'device' }]"
@click="onSettingsTabClick('device')">
设备
</button>
</div>

<div
Expand Down Expand Up @@ -137,4 +147,20 @@
</div>
</div>
</div>

<div
v-show="settingsTab === 'device'"
id="settings-panel-device"
role="tabpanel"
aria-labelledby="settings-tab-device">
<div class="selector-section">
<div class="selector-header">
<span class="selector-title">配置重置</span>
</div>
<div class="config-template-hint">先备份 config.toml,再写默认配置。</div>
<button class="btn-tool" @click="resetConfig" :disabled="resetConfigLoading || loading || !!initError">
{{ resetConfigLoading ? '重装中...' : '重装配置' }}
</button>
Comment on lines +161 to +163
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add a confirmation step before config reset.

Line 161 triggers resetConfig directly; in web-ui/modules/app.methods.providers.mjs (Line 191 onward), that call immediately invokes api('reset-config'). This creates a high accidental-reset risk from a single misclick.

Suggested guard pattern
-<button class="btn-tool" `@click`="resetConfig" :disabled="resetConfigLoading || loading || !!initError">
+<button class="btn-tool" `@click`="confirmAndResetConfig" :disabled="resetConfigLoading || loading || !!initError">
// In web-ui/modules/app.methods.providers.mjs
async confirmAndResetConfig() {
    const confirmed = await this.requestConfirmDialog({
        title: '重装配置',
        message: '该操作会重写本地配置文件。请确认已完成备份。',
        confirmText: '继续重装',
        cancelText: '取消',
        danger: true
    });
    if (!confirmed) return;
    await this.resetConfig();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/partials/index/panel-settings.html` around lines 161 - 163, The reset
button currently calls resetConfig directly, which risks accidental resets;
change the button click handler to call a new confirmAndResetConfig (leave
:disabled using resetConfigLoading || loading || !!initError) and implement
confirmAndResetConfig in web-ui/modules/app.methods.providers.mjs to open a
confirmation dialog via this.requestConfirmDialog({ title: '重装配置', message:
'该操作会重写本地配置文件。请确认已完成备份。', confirmText: '继续重装', cancelText: '取消', danger: true })
and only call this.resetConfig() when the user confirms; keep existing
loading/error handling and return early if not confirmed.

</div>
</div>
</div>
11 changes: 11 additions & 0 deletions web-ui/styles/modals-core.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
padding: 0;
}

.agents-modal {
width: min(92vw, 900px);
max-height: calc(100vh - 56px);
}

.modal-editor-header {
margin-bottom: 0;
padding: var(--spacing-md) var(--spacing-md) 0;
Expand Down Expand Up @@ -385,6 +390,12 @@
max-height: min(65vh, 620px);
}

.agents-modal .template-editor,
.agents-modal .agents-diff-editor {
min-height: min(52vh, 460px);
max-height: min(58vh, 540px);
}

.agents-diff-line {
display: grid;
grid-template-columns: 16px 1fr;
Expand Down
14 changes: 7 additions & 7 deletions web-ui/styles/responsive.css
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ textarea:focus-visible {

.btn-icon,
.session-item-copy {
min-width: 44px;
min-width: 36px;
}

.session-item {
Expand All @@ -214,10 +214,10 @@ textarea:focus-visible {
}

.session-item-copy {
width: 44px;
height: 44px;
min-width: 44px;
min-height: 44px;
width: 36px;
height: 36px;
min-width: 36px;
min-height: 36px;
border-radius: 6px;
padding: 2px;
display: inline-flex;
Expand All @@ -227,8 +227,8 @@ textarea:focus-visible {
}

.session-item-copy svg {
width: 12px;
height: 12px;
width: 10px;
height: 10px;
}

.session-item-title {
Expand Down
10 changes: 6 additions & 4 deletions web-ui/styles/sessions-list.css
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,8 @@
border: 1px solid rgba(70, 86, 110, 0.35);
background: rgba(70, 86, 110, 0.08);
color: var(--color-text-secondary);
width: 28px;
height: 28px;
width: 22px;
height: 22px;
Comment on lines +317 to +318
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Increase session action hit targets to at least 24px.

Line 317 and Line 318 set the action button to 22px, which is below common accessibility minimum target sizing and makes click/tap precision harder.

Proposed CSS adjustment
 .session-item-copy {
-    width: 22px;
-    height: 22px;
+    width: 24px;
+    height: 24px;
+    min-width: 24px;
+    min-height: 24px;
 }

 .session-item-copy svg {
-    width: 10px;
-    height: 10px;
+    width: 12px;
+    height: 12px;
 }

 .session-item-pin .pin-icon,
 .session-item-pin svg {
-    width: 9px;
-    height: 9px;
+    width: 10px;
+    height: 10px;
 }

Also applies to: 342-344, 350-354

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web-ui/styles/sessions-list.css` around lines 317 - 318, Several CSS rules in
sessions-list.css set action button dimensions to 22px (the width: 22px; height:
22px; declarations) which are below recommended touch target size; update those
declarations to at least 24px (e.g., change width and height to 24px) for the
selector(s) that define session action buttons and similarly update the other
occurrences noted (the blocks around the other width/height declarations
referenced) so all session action hit targets are >=24px and visually align with
existing padding/margin rules.

border-radius: 8px;
display: inline-flex;
align-items: center;
Expand All @@ -339,8 +339,8 @@
}

.session-item-copy svg {
width: 16px;
height: 16px;
width: 10px;
height: 10px;
}

.session-item-pin {
Expand All @@ -349,6 +349,8 @@

.session-item-pin .pin-icon,
.session-item-pin svg {
width: 9px;
height: 9px;
color: rgba(208, 88, 58, 0.78);
}

Expand Down
Loading