feat: support DeepSeek Harness Desktop and refine management UI

- Detect and launch DeepSeek Harness Desktop on Windows
- Improve agent client selection and responsive layouts
- Unify dashboard runtime and connection card styling
- Streamline plugin navigation, cards, and installation dialogs
- Update translations and regression tests
This commit is contained in:
lzt404 committed 2026-10-04 17:31:52 +08:00
1 parent 3b11361f1b
commit 2649df000c
24 files changed
+587 -180

No files matched your search

+53 -8
View File
@@ -21,15 +21,13 @@ const screenshots = path.join(os.tmpdir(), 'easycliproxy-agent-clients');
await page.route('**/*', route => route.request().url().startsWith(base + '/') ? route.continue() : route.abort());
fs.mkdirSync(screenshots, { recursive: true });
const clients = () => page.locator('.agent-list-items button strong');
const visibleClients = () => clients().allTextContents();
const activeClient = () => page.locator('.agent-list-items button.active strong').textContent();
const stored = () => page.evaluate(key => localStorage.getItem(key), storageKey);
const manage = () => page.getByRole('button', { name: '管理客户端', exact: true });
const dialog = () => page.getByRole('dialog', { name: '管理客户端', exact: true });
const manage = () => page.getByRole('button', { name: /^(管理客户端|Manage Clients)$/, exact: true });
const dialog = () => page.getByRole('dialog', { name: /^(管理客户端|Manage Clients)$/, exact: true });
const checkbox = name => dialog().getByRole('checkbox', { name, exact: true });
const save = () => dialog().getByRole('button', { name: '保存', exact: true });
const cancel = () => dialog().getByRole('button', { name: '取消', exact: true });
const cancel = () => dialog().getByRole('button', { name: /^(取消|Cancel)$/, exact: true });
const search = () => dialog().getByRole('searchbox', { name: '搜索客户端', exact: true });
const automatic = () => dialog().getByRole('button', { name: '恢复自动显示', exact: true });
const ready = () => page.waitForFunction(() => window.fixtureCalls?.some(call => call.cmd === 'get_agent_config_statuses')
@@ -37,10 +35,15 @@ const screenshots = path.join(os.tmpdir(), 'easycliproxy-agent-clients');
&& !document.querySelector('.agent-client-list-heading button').disabled);
const waitClients = async names => {
await page.waitForFunction(expected => {
const actual = Array.from(document.querySelectorAll('.agent-list-items button strong'), node => node.textContent);
return JSON.stringify(actual) === JSON.stringify(expected);
const list = document.querySelector('.agent-list-items');
const actual = Array.from(list.querySelectorAll('button strong'), node => node.textContent);
const selected = list.querySelector('button.active strong')?.textContent;
return actual.length > 0 && actual.length <= 6 && actual.every(name => expected.includes(name))
&& expected.includes(selected) && list.scrollHeight <= list.clientHeight + 1;
}, names);
assert.deepEqual(await visibleClients(), names);
await manage().click();
assert.deepEqual(await dialog().locator('label:has(input:checked) strong').allTextContents(), names);
await cancel().click();
};
const open = async (extra = 'mixed-clients', saved = null) => {
if (page.url() !== 'about:blank') {
@@ -172,6 +175,48 @@ const screenshots = path.join(os.tmpdir(), 'easycliproxy-agent-clients');
await page.screenshot({ path: path.join(screenshots, 'manager-desktop.png'), fullPage: true });
await closeAndFocus(() => cancel().click());
// Reproduce the reported desktop size with twelve previously saved clients.
await page.setViewportSize({ width: 1368, height: 912 });
const twelve = ['claude-code', 'claude-desktop', 'codex', 'deepseek-harness', 'opencode', 'pi',
'grok-build', 'antigravity-cli', 'workbuddy', 'zcode', 'kimi-code', 'openclaw'];
await page.evaluate(({ key, ids }) => localStorage.setItem(key, JSON.stringify(ids)), { key: storageKey, ids: twelve });
await page.goto(`${base}/tests/fixtures/agent-backups.html?shell&client=claude-code`, { waitUntil: 'domcontentloaded' });
await ready();
await page.waitForFunction(() => document.querySelectorAll('.agent-list-items button').length === 6);
const assertFits = async () => {
const metrics = await page.locator('.agent-list-items').evaluate(list => {
const rect = list.getBoundingClientRect();
return { height: list.clientHeight, scrollHeight: list.scrollHeight, scrollWidth: list.scrollWidth,
width: list.clientWidth, clipped: Array.from(list.children).some(child => {
const r = child.getBoundingClientRect();
return r.top < rect.top - 1 || r.bottom > rect.bottom + 1;
}) };
});
assert.ok(metrics.scrollHeight <= metrics.height + 1 && metrics.scrollWidth <= metrics.width + 1);
assert.equal(metrics.clipped, false, 'all shortcuts are fully visible without hiding overflowing rows');
};
await assertFits();
assert.deepEqual(JSON.parse(await stored()), twelve, 'limiting shortcuts preserves saved preferences');
const right = await page.locator('.agent-config-scroll-region').evaluate(node => [node.clientHeight, node.scrollHeight]);
assert.ok(right[1] <= right[0] + 1, 'Claude Code settings fit at the reported desktop size');
const switchClient = () => page.getByRole('button', { name: '切换客户端', exact: true });
await switchClient().click();
const picker = page.getByRole('dialog', { name: '切换客户端', exact: true });
await picker.getByRole('searchbox').fill('herMES');
await picker.getByRole('button', { name: 'Hermes Agent', exact: true }).click();
assert.equal(await activeClient(), 'Hermes Agent', 'a client outside the first six remains in the shortcuts after selection');
assert.ok(JSON.parse(await stored()).includes('hermes'), 'switching to a hidden client brings it back');
assert.equal(await switchClient().evaluate(node => node === document.activeElement), true);
await assertFits();
await page.setViewportSize({ width: 360, height: 600 });
await page.waitForFunction(() => document.querySelectorAll('.agent-list-items button').length < 6);
await assertFits();
assert.equal(await activeClient(), 'Hermes Agent', 'resizing always retains the active client');
await page.setViewportSize({ width: 1368, height: 912 });
await switchClient().click();
await picker.getByRole('button', { name: 'Claude Code', exact: true }).click();
await page.locator('.agent-workbench').screenshot({ path: path.join(screenshots, 'shortcuts-desktop.png') });
// Both entry points remain usable with narrow windows and English text in dark mode.
for (const embedded of [false, true]) {
for (const english of [false, true]) {
+22 -48
View File
@@ -130,48 +130,23 @@ const desktopViewports = [
return metrics;
};
const assertClientListScroll = async (label, { requireOverflow = false } = {}) => {
const assertClientShortcuts = async label => {
const metrics = await page.locator('.agent-list-items').evaluate(list => {
const style = getComputedStyle(list);
const overflowX = list.scrollWidth > list.clientWidth + 1;
const overflowY = list.scrollHeight > list.clientHeight + 1;
const original = {
left: list.scrollLeft,
top: list.scrollTop,
snap: list.style.scrollSnapType,
behavior: list.style.scrollBehavior,
};
list.style.scrollSnapType = 'none';
list.style.scrollBehavior = 'auto';
const listRect = list.getBoundingClientRect();
const unreachable = Array.from(list.querySelectorAll('button')).filter(button => {
const before = button.getBoundingClientRect();
list.scrollLeft += before.left - listRect.left;
list.scrollTop += before.top - listRect.top;
const after = button.getBoundingClientRect();
return after.left < listRect.left - 1 || after.right > listRect.right + 1
|| after.top < listRect.top - 1 || after.bottom > listRect.bottom + 1;
}).map(button => button.textContent.trim());
const scrollable = (!overflowX || ['auto', 'scroll'].includes(style.overflowX))
&& (!overflowY || ['auto', 'scroll'].includes(style.overflowY));
list.scrollLeft = original.left;
list.scrollTop = original.top;
list.style.scrollSnapType = original.snap;
list.style.scrollBehavior = original.behavior;
return {
overflows: overflowX || overflowY,
scrollable,
unreachable,
};
const rect = list.getBoundingClientRect();
const buttons = Array.from(list.querySelectorAll('button'));
return { count: buttons.length,
overflow: list.scrollHeight > list.clientHeight + 1 || list.scrollWidth > list.clientWidth + 1,
clipped: buttons.filter(button => {
const r = button.getBoundingClientRect();
return r.top < rect.top - 1 || r.bottom > rect.bottom + 1 || r.left < rect.left - 1 || r.right > rect.right + 1;
}).map(button => button.textContent) };
});
assert.equal(metrics.scrollable, true, `${label}: overflowing client options must remain scrollable`);
if (requireOverflow) {
assert.equal(metrics.overflows, true, `${label}: the fixture must exercise scrolling through the client list`);
}
assert.deepEqual(metrics.unreachable, [], `${label}: scrolling must reveal every client option`);
assert.ok(metrics.count >= 1 && metrics.count <= 6, `${label}: show at most six shortcuts`);
assert.equal(metrics.overflow, false, `${label}: shortcuts must fit without scrolling`);
assert.deepEqual(metrics.clipped, [], `${label}: no shortcut may be clipped`);
};
const assertDesktopLayout = async (label, { requireClientOverflow = false } = {}) => {
const assertDesktopLayout = async label => {
const [clientList, configuration] = await page.locator('.agent-client-list, .agent-config-panel')
.evaluateAll(nodes => nodes.map(node => {
const rect = node.getBoundingClientRect();
@@ -184,7 +159,7 @@ const desktopViewports = [
assert.ok(Math.abs(clientList.y - configuration.y) <= 1,
`${label}: the desktop panels must share a top edge`);
await assertNaturalPanel(label);
await assertClientListScroll(label, { requireOverflow: requireClientOverflow });
await assertClientShortcuts(label);
return { clientList, configuration };
};
@@ -200,7 +175,7 @@ const desktopViewports = [
assert.ok(clientList.y + clientList.height <= configuration.y + 1,
`${label}: the client list must stay above the configuration panel`);
await assertNaturalPanel(label, { checkControls: false });
await assertClientListScroll(label);
await assertClientShortcuts(label);
const pageWidth = await page.evaluate(() => ({
clientWidth: document.documentElement.clientWidth,
scrollWidth: document.documentElement.scrollWidth,
@@ -335,7 +310,7 @@ const desktopViewports = [
};
});
const assertFixedShellDesktop = async (label, { requireRightOverflow = false, requireClientOverflow = false } = {}) => {
const assertFixedShellDesktop = async (label, { requireRightOverflow = false } = {}) => {
const metrics = await shellLayoutMetrics();
assert.ok(Math.abs(metrics.client.height - metrics.configuration.height) <= 1,
`${label}: fixed-height desktop panels must remain equal in height`);
@@ -349,7 +324,7 @@ const desktopViewports = [
`${label}: fixed-height desktop layout must not create a far-right page scrollbar`);
assert.ok(metrics.documentScrollWidth <= metrics.documentClientWidth + 1,
`${label}: fixed-height desktop layout must not create horizontal page scrolling`);
await assertClientListScroll(label, { requireOverflow: requireClientOverflow });
await assertClientShortcuts(label);
await assertShellScrollRegion(label, { requireOverflow: requireRightOverflow });
return metrics;
};
@@ -369,10 +344,10 @@ const desktopViewports = [
const label = `${viewport.width}x${viewport.height}/${embedded ? 'embedded' : 'full'}/${client}/core`;
await assertDesktopLayout(label);
if (client === 'zcode' && viewport.height === 941) {
const status = page.locator('.agent-list-items button').filter({ hasText: 'Antigravity CLI' }).locator('small');
const status = page.locator('.agent-list-items button').filter({ hasText: 'Claude Code' }).locator('small');
const longModel = 'gemini-3.8-flash-high-with-a-long-client-status-for-layout-verification';
await page.evaluate(model => {
window.fixtureClientStatusesOverride = { 'antigravity-cli': { appliedModel: model } };
window.fixtureClientStatusesOverride = { 'claude-code': { appliedModel: model } };
}, longModel);
await page.locator('.agent-client-list-heading button').click();
await page.waitForFunction(model => Array.from(document.querySelectorAll('.agent-list-items small'))
@@ -429,7 +404,6 @@ const desktopViewports = [
const label = `${viewport.width}x${viewport.height}/shell/claude-desktop/core`;
const metrics = await assertFixedShellDesktop(label, {
requireRightOverflow: height === 700,
requireClientOverflow: height === 700,
});
fixedShellByHeight.set(height, metrics);
}
@@ -466,7 +440,7 @@ const desktopViewports = [
`${shellNarrowLabel}: the stacked workbench must preserve the fixed window-edge gap`);
assert.ok(shellNarrow.documentScrollHeight <= shellNarrow.documentClientHeight + 1,
`${shellNarrowLabel}: the bounded one-column shell must not leak scrolling to the page`);
await assertClientListScroll(shellNarrowLabel);
await assertClientShortcuts(shellNarrowLabel);
await assertShellScrollRegion(shellNarrowLabel);
// At phone width the shell follows Usage records and returns to natural document scrolling.
@@ -482,7 +456,7 @@ const desktopViewports = [
`${shellPhoneLabel}: phone-width client list must remain above configuration content`);
assert.ok(shellPhone.documentScrollWidth <= shellPhone.documentClientWidth + 1,
`${shellPhoneLabel}: natural document flow must not create horizontal scrolling`);
await assertClientListScroll(shellPhoneLabel);
await assertClientShortcuts(shellPhoneLabel);
await assertShellScrollRegion(shellPhoneLabel, { natural: true });
for (const viewport of [{ width: 640, height: 700 }, { width: 360, height: 941 }]) {
@@ -511,7 +485,7 @@ const desktopViewports = [
await tab('会话管理').click();
await page.locator('.codex-session-row').first().waitFor();
const label = `${viewport.width}x${viewport.height}/full/codex/sessions`;
await assertDesktopLayout(label, { requireClientOverflow: false });
await assertDesktopLayout(label);
const sessions = await page.locator('.codex-session-list').evaluate(list => {
const style = getComputedStyle(list);
const last = list.querySelector('.codex-session-row:last-child');
+8 -1
View File
@@ -12,7 +12,14 @@ const assert = require('node:assert/strict');
page.on('pageerror', error => errors.push(String(error)));
const tab = name => page.getByRole('tab', { name, exact: true });
const button = name => page.getByRole('button', { name, exact: true });
const client = name => page.locator('.agent-list-items button').filter({ has: page.getByText(name, { exact: true }) });
const client = name => ({ click: async () => {
const shortcut = page.locator('.agent-list-items button').filter({ has: page.getByText(name, { exact: true }) });
if (await shortcut.count()) await shortcut.click();
else {
await button('切换客户端').click();
await page.getByRole('dialog', { name: '切换客户端', exact: true }).getByRole('button', { name, exact: true }).click();
}
} });
const active = async name => {
await page.waitForFunction(name => Array.from(document.querySelectorAll('[role=tab]'))
.some(el => el.textContent === name && el.getAttribute('aria-selected') === 'true'), name);
+50
View File
@@ -0,0 +1,50 @@
// Run Vite on port 1421 before this test.
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
const assert = require('node:assert/strict');
(async () => {
const browser = await chromium.launch({ channel: 'msedge', headless: true });
try {
for (const scenario of ['desktop', 'both', 'running', 'cli']) {
const page = await browser.newPage();
await page.addInitScript(scenario => {
window.fixtureClientStatusesOverride = {
'deepseek-harness': {
cliVersion: scenario === 'desktop' ? null : '0.1.9',
appVersion: scenario === 'cli' ? null : '0.2.0-rc.2',
launchTargets: [
...(scenario === 'desktop' ? [] : [{ id: 'cli', label: 'DeepSeek Harness Web', detail: 'dsh web' }]),
...(scenario === 'cli' ? [] : [{ id: 'app', label: 'DeepSeek Harness Desktop', detail: 'DeepSeek Harness.exe' }]),
],
},
};
}, scenario);
await page.goto(`http://localhost:1421/tests/fixtures/agent-backups.html?client=deepseek-harness&reset-selections${scenario === 'running' ? '&running' : ''}`);
const controls = page.locator('.agent-run-controls');
const app = controls.getByRole('button', { name: '启动 App', exact: true });
await app.waitFor();
if (scenario === 'cli') {
assert.ok(await app.isDisabled());
} else {
await page.locator('.agent-status-grid').getByText('0.2.0-rc.2', { exact: true }).waitFor();
await app.click();
await page.waitForFunction(() => window.fixtureCalls.some(call => call.cmd === 'launch_agent'));
const calls = await page.evaluate(() => window.fixtureCalls.filter(call => call.cmd === 'launch_agent'));
assert.equal(calls[0].args.target, 'app');
assert.equal(calls[0].args.deepseekHarnessOptions, null);
}
if (scenario === 'running') {
await controls.getByRole('button', { name: '重启 Web', exact: true }).waitFor();
await controls.getByRole('button', { name: '关闭 DeepSeek Harness', exact: true }).click();
await controls.getByRole('button', { name: '启动 CLI', exact: true }).waitFor();
} else {
assert.equal(await controls.getByRole('button', { name: '启动 CLI', exact: true }).isDisabled(), scenario === 'desktop');
}
assert.equal(await controls.getByRole('button', { name: '重启 App', exact: true }).count(), 0);
await page.close();
}
console.log('DeepSeek Desktop UI: desktop-only, CLI-only, both, and running Web passed');
} finally {
await browser.close();
}
})().catch(error => { console.error(error); process.exitCode = 1; });
+23 -10
View File
@@ -107,14 +107,6 @@ const fs = require('node:fs');
await dialog().getByLabel('Version tag (blank for latest)').fill('../unsafe');
assert.equal(await dialog().getByRole('button', { name: 'Install', exact: true }).isDisabled(), true);
await dialog().getByLabel('Version tag (blank for latest)').fill('v1.2.3');
await dialog().getByRole('button', { name: 'Install', exact: true }).click();
assert.equal((await installs()).length, 0, 'First third-party confirmation cannot install');
await dialog().getByText(/has not been reviewed by the official project/).waitFor();
await dialog().getByRole('button', { name: 'Understood, review source' }).click();
const token = dialog().getByLabel(/Type the following/);
await token.fill('wrong/source');
assert.equal(await dialog().getByRole('button', { name: 'Install', exact: true }).isDisabled(), true);
await token.fill('router-for-me/analytics');
await page.evaluate(() => { window.pluginsFixture.holdInstall = true; });
await dialog().getByRole('button', { name: 'Install', exact: true }).click();
await page.waitForFunction(() => window.pluginsFixture.installs.length === 1);
@@ -166,7 +158,7 @@ const fs = require('node:fs');
await page.getByRole('alert').filter({ hasText: 'Fixture list temporarily unavailable' }).waitFor({ state: 'detached' });
for (const locale of ['en', 'zh-CN']) {
for (const width of [640, 1024]) {
for (const width of [375, 640, 1070, 1368]) {
await page.setViewportSize({ width, height: 800 });
await open(`locale=${locale}&theme=${locale === 'en' ? 'light' : 'dark'}`);
await page.locator('.plugin-card').first().waitFor();
@@ -186,6 +178,27 @@ const fs = require('node:fs');
fs.mkdirSync(process.env.PLUGIN_SCREENSHOT_DIR, { recursive: true });
await page.screenshot({ path: path.join(process.env.PLUGIN_SCREENSHOT_DIR, `plugins-${locale}-${width}.png`), fullPage: true });
}
await card('Community analytics').getByRole('button', { name: locale === 'en' ? 'Install' : '安装', exact: true }).click();
await dialog().waitFor();
// Exercise wrapping and scrolling with metadata longer than a typical registry entry.
await dialog().locator('h2').evaluate(el => { el.textContent += ' · model-fallback-router-compatibility'; });
await dialog().locator('.plugin-install-origin small').evaluate(el => { el.textContent = 'https://raw.githubusercontent.com/example/plugins-store/main/' + 'long-registry-path-'.repeat(10) + 'registry.json'; });
await page.setViewportSize({ width, height: 480 });
const layout = await dialog().evaluate(el => {
const heading = el.querySelector('.config-dialog-heading').getBoundingClientRect();
const body = el.querySelector('.plugin-install-body').getBoundingClientRect();
const footer = el.querySelector('.plugin-dialog-actions').getBoundingClientRect();
const bounds = el.getBoundingClientRect();
return { ordered: heading.bottom <= body.top + 1 && body.bottom <= footer.top + 1,
fits: bounds.top >= 0 && bounds.bottom <= innerHeight && el.scrollWidth <= el.clientWidth + 1 };
});
assert.deepEqual(layout, { ordered: true, fits: true }, 'Dialog sections must not overlap or overflow');
await noOverflow();
if (process.env.PLUGIN_SCREENSHOT_DIR) {
await page.screenshot({ path: path.join(process.env.PLUGIN_SCREENSHOT_DIR, `plugins-install-${locale}-${width}.png`) });
}
await page.keyboard.press('Escape');
await dialog().waitFor({ state: 'detached' });
}
}
await open('unsupported');
@@ -193,7 +206,7 @@ const fs = require('node:fs');
assert.equal(await page.locator('.plugin-tabs').count(), 0);
assert.equal(await page.evaluate(() => window.pluginsFixture.requests.length), 0, 'Unsupported cores do not receive plugin mutations or list requests');
assert.deepEqual(errors, []);
console.log('Plugin UI passed: configured-only and unloaded states, installed counts, store lookup, configuration removal, resources, global/instance toggle, store filters, source trust gate, versions, retry, settings, uninstall, unsupported cores, responsive English/Chinese.');
console.log('Plugin UI passed: configured-only and unloaded states, installed counts, store lookup, configuration removal, resources, global/instance toggle, store filters, single-step installation, versions, retry, settings, uninstall, unsupported cores, responsive English/Chinese.');
} finally {
await browser?.close();
await server.close();