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
This commit is contained in:
1 parent
18a9bfbaa9
commit
a9297b86cc
5 files changed
+111
-42
No files matched your search
@@ -62,18 +62,16 @@ export function AgentRunControls({
|
||||
</section>;
|
||||
}
|
||||
|
||||
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 <>
|
||||
<MessageNotice message={error} onDismiss={onErrorDismiss} />
|
||||
<MessageNotice tone="success" message={!error ? notice : null} onDismiss={onNoticeDismiss} />
|
||||
<div className="agent-save-feedback agent-shared-feedback" aria-live="polite">
|
||||
<span className="agent-write-state">{pending ? t('agents.modify.pending') : status || ' '}</span>
|
||||
<small tabIndex={description ? 0 : undefined}>{description || ' '}</small>
|
||||
</div>
|
||||
</>;
|
||||
const state = pending ? t('agents.modify.pending') : status;
|
||||
if (!state && !description) return null;
|
||||
return <div className="agent-save-feedback agent-shared-feedback" aria-live="polite">
|
||||
{state ? <span className="agent-write-state">{state}</span> : null}
|
||||
{description ? <small>{description}</small> : null}
|
||||
</div>;
|
||||
}
|
||||
|
||||
export function AgentConfigManagementPanel({
|
||||
|
||||
+28
-21
@@ -1687,6 +1687,12 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
const configurationFeedback = <AgentConfigurationFeedback pending={hasPendingChanges}
|
||||
status={modelLoading || !activeStatus ? t('agents.modify.checking')
|
||||
: activeStatus.modificationState === 'invalid' ? t('agents.modify.invalidState') : ''}
|
||||
description={modificationDescription} />;
|
||||
const configurationErrorMessage = configurationError || modelSelectionError || modelError;
|
||||
|
||||
return (
|
||||
<section className={`page management-page agents-page${embedded ? ' agents-page-embedded' : ''}`}>
|
||||
<header className="management-header">
|
||||
@@ -1802,19 +1808,21 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
|
||||
{isDeepSeekHarnessClient ? <p className="agent-model-hint">{t('agents.harness.defaultHint')}</p> : null}
|
||||
|
||||
<div className="agent-minimal-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="primary-button"
|
||||
onClick={applySelectedConfiguration}
|
||||
disabled={busy || !canEnable || configurationWriteBlocked}
|
||||
>
|
||||
{['apply', 'install-pi', 'repair-pi'].includes(busyAction ?? '') ? <LoaderCircle size={16} className="spin" /> : null}
|
||||
{isPiClient
|
||||
? activeStatus?.pluginInstalled ? configurationActionLabel : t('agents.pi.install')
|
||||
: configurationActionLabel}
|
||||
</button>
|
||||
|
||||
<div className="agent-save-bar">
|
||||
{configurationFeedback}
|
||||
<div className="agent-save-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="primary-button"
|
||||
onClick={applySelectedConfiguration}
|
||||
disabled={busy || !canEnable || configurationWriteBlocked}
|
||||
>
|
||||
{['apply', 'install-pi', 'repair-pi'].includes(busyAction ?? '') ? <LoaderCircle size={16} className="spin" /> : null}
|
||||
{isPiClient
|
||||
? activeStatus?.pluginInstalled ? configurationActionLabel : t('agents.pi.install')
|
||||
: configurationActionLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1890,7 +1898,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
onChange={selectModel}
|
||||
onRefresh={refreshModels}
|
||||
/>
|
||||
<span className="agent-model-hint agent-model-status" title={modelHint} aria-live="polite">{modelHint || ' '}</span>
|
||||
{modelHint ? <span className="agent-model-hint agent-model-status" title={modelHint} aria-live="polite">{modelHint}</span> : null}
|
||||
{isDeepSeekHarnessClient ? <p className="agent-model-hint">{t('agents.harness.defaultHint')}</p> : null}
|
||||
{harnessCatalogButton}
|
||||
{selected === 'codex' ? (
|
||||
@@ -2060,6 +2068,7 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
) : null}
|
||||
|
||||
<div className="agent-save-bar">
|
||||
{configurationFeedback}
|
||||
{selected === 'claude-desktop' && !activeStatus?.claudeDesktopModelMappings ? <p className="agent-inline-message warning">{t('agents.backup.mappingRequired')}</p> : null}
|
||||
<div className="agent-save-actions">
|
||||
<button type="button" className="primary-button" onClick={applySelectedConfiguration} disabled={busy || !canEnable || configurationWriteBlocked}>
|
||||
@@ -2095,13 +2104,11 @@ export function AgentsPage({ embedded = false, onConfigurationApplied }: AgentsP
|
||||
onUninstallPi={() => void uninstallPiProvider()} />
|
||||
</div>
|
||||
) : null}
|
||||
<AgentConfigurationFeedback pending={hasPendingChanges}
|
||||
notice={configurationNotice || clearNotice}
|
||||
onNoticeDismiss={() => { 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}
|
||||
<MessageNotice message={configurationErrorMessage}
|
||||
onDismiss={() => { setConfigurationError(''); setModelSelectionError(''); setModelError(''); }} />
|
||||
<MessageNotice tone="success" message={!configurationErrorMessage ? configurationNotice || clearNotice : null}
|
||||
onDismiss={() => { setConfigurationNotice(''); setClearNotice(''); }} />
|
||||
{activeSubpage === 'core' ? <AgentRunControls name={activeDefinition.name} dualTargets={hasIndependentCliAndApp}
|
||||
desktop={hasIndependentCliAndApp || selected === 'claude-desktop' || selected === 'zcode'}
|
||||
targets={activeLaunchTargets} enabled={launchEnabled} busyAction={busyAction}
|
||||
|
||||
+2
-10
@@ -13939,7 +13939,8 @@ body.table-col-resizing * {
|
||||
.agent-model-section { grid-column: 1 / -1; }
|
||||
.agent-save-bar { grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; gap: 20px; padding: 16px 0 0; border-top: 1px solid var(--theme-e3e1db); }
|
||||
.agent-save-feedback { display: grid; gap: 6px; min-width: 0; }
|
||||
.agent-save-feedback small { color: var(--theme-7a746c); font-size: var(--font-size-meta); }
|
||||
.agent-save-bar .agent-save-feedback { flex: 1; }
|
||||
.agent-save-feedback small { color: var(--theme-7a746c); font-size: var(--font-size-meta); line-height: var(--line-height-body); overflow-wrap: anywhere; }
|
||||
.agent-save-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 8px; margin-left: auto; min-width: 0; }
|
||||
.agent-save-actions button { min-width: 128px; min-height: 40px; }
|
||||
.agent-shared-feedback { min-width: 0; }
|
||||
@@ -14066,15 +14067,6 @@ body.table-col-resizing * {
|
||||
box-shadow: 0 4px 16px rgba(45, 42, 38, 0.18);
|
||||
}
|
||||
|
||||
/* Persistent agent state uses reserved rows; transient results float above it. */
|
||||
.agent-shared-feedback {
|
||||
grid-template-rows: 20px 36px;
|
||||
}
|
||||
.agent-shared-feedback > .agent-write-state,
|
||||
.agent-shared-feedback > small {
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
.agent-model-status {
|
||||
display: block;
|
||||
height: 20px;
|
||||
|
||||
@@ -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; });
|
||||
+5
-1
@@ -56,7 +56,11 @@ function Fixture() {
|
||||
{mounted && <><FloatingNotice key={first.revision} notice={first.notice} onDismiss={first.clearNotice} />
|
||||
<FloatingNotice key={second.revision} notice={second.notice} onDismiss={second.clearNotice} /></>}
|
||||
<div data-testid="before">Existing form</div>
|
||||
<AgentConfigurationFeedback pending={agent} notice={agent ? 'Agent saved' : ''} error="" description={agent ? 'Persistent description '.repeat(40) : ''} />
|
||||
<div className="agent-save-bar">
|
||||
<AgentConfigurationFeedback pending={agent} description="" />
|
||||
<div className="agent-save-actions"><button>Apply agent</button></div>
|
||||
</div>
|
||||
<MessageNotice tone="success" message={agent ? 'Agent saved' : ''} />
|
||||
{quota && <QuotaActionFeedback name="test.json" quota={{ status: 'success', rows: [], actionResult: { action: 'reset', status: 'refresh-error', error: 'query failed' } }} />}
|
||||
<input data-testid="after" defaultValue="Keep focus" />
|
||||
</section>
|
||||
|
||||
Reference in new issue
Block a user