From a9297b86cc4d9b5ca9773ac7c4ebb992db1d3b85 Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Mon, 14 Sep 2026 16:39:23 +0800 Subject: [PATCH] fix(ui): reduce whitespace around agent configuration save controls - Remove empty feedback placeholders and fixed-height rows - Place configuration status beside the save button - Compact save controls in the simplified view - Add responsive layout and notification stability regression tests --- src/pages/AgentControls.tsx | 18 +++---- src/pages/AgentsPage.tsx | 49 ++++++++++-------- src/styles.css | 12 +---- tests/agent-configuration-layout-ui.cjs | 68 +++++++++++++++++++++++++ tests/fixtures/operation-feedback.tsx | 6 ++- 5 files changed, 111 insertions(+), 42 deletions(-) create mode 100644 tests/agent-configuration-layout-ui.cjs diff --git a/src/pages/AgentControls.tsx b/src/pages/AgentControls.tsx index 0ef80ab..a5034a0 100644 --- a/src/pages/AgentControls.tsx +++ b/src/pages/AgentControls.tsx @@ -62,18 +62,16 @@ export function AgentRunControls({ ; } -export function AgentConfigurationFeedback({ pending, notice, error, description, status = '', onNoticeDismiss, onErrorDismiss }: { - pending: boolean; notice: string; error: string; description: string; status?: string; onNoticeDismiss?: () => void; onErrorDismiss?: () => void; +export function AgentConfigurationFeedback({ pending, description, status = '' }: { + pending: boolean; description: string; status?: string; }) { const { t } = useI18n(); - return <> - - -
- {pending ? t('agents.modify.pending') : status || ' '} - {description || ' '} -
- ; + const state = pending ? t('agents.modify.pending') : status; + if (!state && !description) return null; + return
+ {state ? {state} : null} + {description ? {description} : null} +
; } export function AgentConfigManagementPanel({ diff --git a/src/pages/AgentsPage.tsx b/src/pages/AgentsPage.tsx index 7d7ffaa..76570cc 100644 --- a/src/pages/AgentsPage.tsx +++ b/src/pages/AgentsPage.tsx @@ -1687,6 +1687,12 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP ) : null; + const configurationFeedback = ; + const configurationErrorMessage = configurationError || modelSelectionError || modelError; + return (
@@ -1802,19 +1808,21 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP {isDeepSeekHarnessClient ?

{t('agents.harness.defaultHint')}

: null} -
- - +
+ {configurationFeedback} +
+ +
@@ -1890,7 +1898,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP onChange={selectModel} onRefresh={refreshModels} /> - {modelHint || ' '} + {modelHint ? {modelHint} : null} {isDeepSeekHarnessClient ?

{t('agents.harness.defaultHint')}

: null} {harnessCatalogButton} {selected === 'codex' ? ( @@ -2060,6 +2068,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP ) : null}
+ {configurationFeedback} {selected === 'claude-desktop' && !activeStatus?.claudeDesktopModelMappings ?

{t('agents.backup.mappingRequired')}

: null}
) : null} - { setConfigurationNotice(''); setClearNotice(''); }} - status={modelLoading || !activeStatus ? t('agents.modify.checking') - : activeStatus.modificationState === 'invalid' ? t('agents.modify.invalidState') : ''} - onErrorDismiss={() => { setConfigurationError(''); setModelSelectionError(''); setModelError(''); }} - error={configurationError || modelSelectionError || modelError} description={modificationDescription} /> + {activeSubpage !== 'core' ? configurationFeedback : null} + { setConfigurationError(''); setModelSelectionError(''); setModelError(''); }} /> + { setConfigurationNotice(''); setClearNotice(''); }} /> {activeSubpage === 'core' ? .agent-write-state, -.agent-shared-feedback > small { - min-height: 0; - overflow: auto; -} .agent-model-status { display: block; height: 20px; diff --git a/tests/agent-configuration-layout-ui.cjs b/tests/agent-configuration-layout-ui.cjs new file mode 100644 index 0000000..a5b2c81 --- /dev/null +++ b/tests/agent-configuration-layout-ui.cjs @@ -0,0 +1,68 @@ +const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const base = 'http://127.0.0.1:1421'; +(async () => { + const browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--no-proxy-server'] }); + try { + const page = await browser.newPage(); + page.setDefaultTimeout(10000); + await page.route('**/*', route => route.request().url().startsWith(base + '/') ? route.continue() : route.abort()); + const errors = []; + page.on('pageerror', error => errors.push(String(error))); + const open = async query => { + await page.goto(`${base}/tests/fixtures/agent-backups.html?reset-selections&${query}`, { waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => window.fixtureCalls.some(call => call.cmd === 'get_agent_models') + && !document.querySelector('.agent-model-trigger')?.disabled); + }; + const positions = () => page.locator('.agent-save-bar, .agent-save-actions button, .agent-run-controls').evaluateAll(nodes => nodes.map(node => { + const rect = node.getBoundingClientRect(); + return [rect.x + scrollX, rect.y + scrollY, rect.width, rect.height]; + })); + const assertCompact = async () => { + const geometry = await positions(); + assert.ok(geometry[0][3] <= 60, 'The save bar occupies only a button-height row plus padding'); + assert.ok(geometry[2][1] - geometry[0][1] - geometry[0][3] <= 20, 'No blank feedback region separates save and run controls'); + assert.equal(await page.locator('.agent-shared-feedback').count(), 0, 'Empty state feedback does not reserve space'); + assert.ok(await page.locator('.agent-save-actions button').evaluate(node => node.getBoundingClientRect().width <= 160), 'Save button stays compact in both entry points'); + return geometry; + }; + fs.mkdirSync('misc', { recursive: true }); + for (const width of [1280, 640, 360]) for (const embedded of [false, true]) { + await page.setViewportSize({ width, height: 900 }); + await open(`client=codex&fail-apply${embedded ? '&embedded' : ''}`); + const initial = await assertCompact(); + const save = page.getByRole('button', { name: '更新配置', exact: true }); + await page.locator('.agent-model-trigger').click(); + await page.getByRole('option', { name: 'gpt-two' }).click(); + await page.locator('.agent-save-bar').getByText('待应用', { exact: true }).waitFor(); + assert.deepEqual(await positions(), initial, 'Pending state fits beside the existing save button'); + await save.click(); + const failure = page.locator('.app-notice-stack').getByRole('alert'); + await failure.filter({ hasText: /模拟配置写入失败/ }).waitFor(); + assert.deepEqual(await positions(), initial, 'Save failure does not shift controls'); + await failure.getByRole('button').click(); + await failure.waitFor({ state: 'detached' }); + await save.click(); + await page.waitForFunction(() => document.documentElement.dataset.fixtureApplied === '1'); + const success = page.locator('.app-notice-stack').getByRole('status'); + await success.waitFor(); + assert.deepEqual(await positions(), initial, 'Saving and clearing pending state do not shift controls'); + await success.getByRole('button').click(); + await assertCompact(); + if (width === 1280) await page.screenshot({ path: `misc/agent-layout-after-${embedded ? 'embedded' : 'codex'}.png`, fullPage: true }); + await page.getByRole('tab', { name: '配置管理', exact: true }).click(); + assert.equal(await page.locator('.agent-shared-feedback').count(), 0, 'Inactive configuration tabs have no empty state row'); + await open(`client=zcode${embedded ? '&embedded' : ''}`); + const description = page.locator('.agent-save-bar .agent-save-feedback small'); + await description.waitFor(); + assert.match(await description.innerText(), /重新启动 ZCode/); + assert.ok(await description.evaluate(node => node.scrollHeight <= node.clientHeight + 1), 'Persistent instructions wrap naturally without clipping or nested scrollbars'); + const geometry = await positions(); + assert.ok(geometry[2][1] - geometry[0][1] - geometry[0][3] <= 20); + assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false, 'The configuration page fits narrow windows'); + } + assert.deepEqual(errors, []); + console.log('PASS: compact save rows, no empty feedback, stable pending/error/success states, readable instructions, full and embedded views at 1280/640/360px.'); + } finally { await browser.close(); } +})().catch(error => { console.error(error); process.exitCode = 1; }); diff --git a/tests/fixtures/operation-feedback.tsx b/tests/fixtures/operation-feedback.tsx index 1e9ac21..9f0d6f3 100644 --- a/tests/fixtures/operation-feedback.tsx +++ b/tests/fixtures/operation-feedback.tsx @@ -56,7 +56,11 @@ function Fixture() { {mounted && <> }
Existing form
- +
+ +
+
+ {quota && }