feat(claude-code): align model mapping UI with Claude Code settings
- Add per-role model rows with inline 1M context controls - Add bulk action to apply Sonnet settings to all roles - Persist startup model and context settings in official settings.json fields - Preserve custom role mappings and Subagent configuration - Add multilingual labels and UI regression coverage - Validate behavior with real Claude Code 2.1.292
This commit is contained in:
1 parent
b564cb384c
commit
b90445143c
23 files changed
+717
-191
No files matched your search
@@ -44,7 +44,7 @@ const fs = require('node:fs/promises');
|
||||
className: node.className, width: node.clientWidth, scroll: node.scrollWidth, overflowX: getComputedStyle(node).overflowX,
|
||||
})));
|
||||
assert.ok(dimensions.every(size => size.scroll <= size.width + 1 || (
|
||||
size.className === 'auth-file-table-scroll' && size.width > 700 && size.width < 906 && size.overflowX === 'auto'
|
||||
size.className === 'auth-file-table-scroll' && size.width > 700 && size.width < 980 && size.overflowX === 'auto'
|
||||
)), `${label}: only intermediate widths may scroll inside the table; cards must fit ${JSON.stringify(dimensions)}`);
|
||||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), true, `${label}: page overflow`);
|
||||
};
|
||||
@@ -93,7 +93,7 @@ const fs = require('node:fs/promises');
|
||||
const resetTitle = await resetTimes.first().getAttribute('title');
|
||||
assert.ok(resetTitle.startsWith(await resetTimes.first().innerText()) && resetTitle.includes('小时后'), 'Compact timestamps retain their exact time and relative countdown on hover');
|
||||
await next().click();
|
||||
await card(11).waitFor();
|
||||
await card(7).waitFor();
|
||||
assert.equal(await cards().count(), 2);
|
||||
assert.equal(await next().isDisabled(), true);
|
||||
assert.ok((await pagination().innerText()).includes('2 / 2'));
|
||||
@@ -235,10 +235,10 @@ const fs = require('node:fs/promises');
|
||||
await page.setViewportSize({ width: 1800, height: 1200 });
|
||||
await open('theme=light&locale=zh-CN');
|
||||
const tableBounds = await page.locator('.auth-file-table-scroll').boundingBox();
|
||||
const seventhBounds = await card(7).boundingBox();
|
||||
const lastBounds = await cards().last().boundingBox();
|
||||
await page.screenshot({ path: path.join(screenshotDir, 'reference-dense-preview.png'), clip: {
|
||||
x: tableBounds.x, y: tableBounds.y, width: tableBounds.width,
|
||||
height: seventhBounds.y + seventhBounds.height - tableBounds.y,
|
||||
height: lastBounds.y + lastBounds.height - tableBounds.y,
|
||||
} });
|
||||
await open('theme=light&locale=zh-CN&small=1');
|
||||
await page.locator('.auth-file-table-scroll').screenshot({ path: path.join(screenshotDir, 'reference-small-idle-preview.png') });
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ channel: 'msedge', headless: true });
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
|
||||
await page.goto('http://127.0.0.1:1421/tests/fixtures/agent-backups.html?client=claude-code&reset-selections');
|
||||
const startup = page.getByRole('combobox', { name: '启动模型', exact: true });
|
||||
const subagent = page.getByRole('combobox', { name: 'Subagent 模型', exact: true });
|
||||
await startup.click();
|
||||
await page.getByRole('option', { name: /^sonnet / }).click();
|
||||
assert.equal(await startup.inputValue(), 'sonnet');
|
||||
await page.locator('.claude-role-row .agent-model-trigger').first().click();
|
||||
await page.getByRole('option', { name: /gpt-two/ }).click();
|
||||
await page.getByText('跟随 sonnet → gpt-two', { exact: true }).waitFor();
|
||||
assert.equal(await startup.inputValue(), 'sonnet');
|
||||
await startup.click();
|
||||
await page.getByRole('option', { name: /^gpt-two / }).click();
|
||||
await page.locator('.claude-code-context-settings summary').click();
|
||||
const context = page.getByRole('checkbox', { name: '启动模型 1M', exact: true });
|
||||
await context.check();
|
||||
assert.equal(await startup.inputValue(), 'gpt-two[1m]');
|
||||
assert.equal(await page.getByRole('spinbutton', { name: '全局窗口覆盖', exact: true }).inputValue(), '1000000');
|
||||
await context.uncheck();
|
||||
assert.equal(await page.getByRole('spinbutton', { name: '全局窗口覆盖', exact: true }).inputValue(), '200000');
|
||||
assert.equal(await startup.inputValue(), 'gpt-two');
|
||||
await page.locator('.claude-code-context-settings summary').click();
|
||||
assert.equal(await page.locator('.claude-role-row input[type=checkbox]').count(), 3);
|
||||
await subagent.fill('custom-worker');
|
||||
await subagent.press('Enter');
|
||||
assert.equal(await subagent.inputValue(), 'custom-worker');
|
||||
await page.getByRole('button', { name: '更新配置', exact: true }).click();
|
||||
await page.waitForFunction(() => window.fixtureCalls.some(c => c.args?.claudeCodeModelMappings?.startupModel === 'gpt-two' && c.args?.claudeCodeModelMappings?.subagentModel === 'custom-worker'));
|
||||
await subagent.fill('unsaved-worker');
|
||||
await subagent.press('Escape');
|
||||
await page.evaluate(() => { window.fixtureFailedCommands = ['get_agent_models']; });
|
||||
await page.locator('.claude-role-row .agent-model-trigger').first().click();
|
||||
await page.locator('.agent-model-dropdown:visible button').filter({ has: page.locator('svg.lucide-refresh-cw') }).click();
|
||||
await page.locator('.claude-code-model-error').waitFor();
|
||||
assert.equal(await startup.inputValue(), 'gpt-two');
|
||||
assert.equal(await subagent.inputValue(), 'unsaved-worker');
|
||||
assert.match(await page.locator('.claude-role-row .agent-model-trigger').first().innerText(), /gpt-two/);
|
||||
await subagent.press('Escape');
|
||||
await page.evaluate(() => { window.fixtureFailedCommands = []; });
|
||||
await page.locator('.claude-code-model-error button').click();
|
||||
await page.locator('.claude-code-model-error').waitFor({ state: 'hidden' });
|
||||
assert.equal(await subagent.inputValue(), 'unsaved-worker');
|
||||
await startup.click();
|
||||
await page.getByRole('option', { name: '使用 Claude Code 默认模型', exact: true }).click();
|
||||
await subagent.click();
|
||||
await page.getByRole('option', { name: '使用默认继承行为', exact: true }).click();
|
||||
await page.getByRole('button', { name: '更新配置', exact: true }).click();
|
||||
await page.waitForFunction(() => window.fixtureCalls.some(c => c.args?.claudeCodeModelMappings?.startupModel === '' && c.args?.claudeCodeModelMappings?.subagentModel === ''));
|
||||
await page.getByRole('checkbox', { name: 'sonnet 1M', exact: true }).check();
|
||||
await page.getByRole('button', { name: '将 Sonnet 应用到全部角色', exact: true }).click();
|
||||
for (const role of ['sonnet', 'opus', 'haiku']) {
|
||||
assert.equal(await page.getByRole('checkbox', { name: `${role} 1M`, exact: true }).isChecked(), true);
|
||||
assert.match(await page.locator(`.claude-role-row[data-role="${role}"] .agent-model-trigger`).innerText(), /gpt-two/);
|
||||
}
|
||||
await page.getByRole('button', { name: '更新配置', exact: true }).click();
|
||||
await page.waitForFunction(() => window.fixtureCalls.some(c => {
|
||||
const m = c.args?.claudeCodeModelMappings;
|
||||
return m && m.opus === 'gpt-two' && m.haiku === 'gpt-two' && m.opus1m && m.haiku1m && m.sonnet1m;
|
||||
}));
|
||||
for (const width of [1280, 760]) {
|
||||
await page.setViewportSize({ width, height: 1000 });
|
||||
await page.screenshot({ path: path.join(os.tmpdir(), `claude-code-models-${width}.png`), fullPage: true });
|
||||
for (const field of await page.locator('.claude-code-model-field').all()) {
|
||||
assert(await field.evaluate(el => el.scrollWidth <= el.clientWidth + 1));
|
||||
}
|
||||
}
|
||||
console.log('PASS: select, custom input, defaults, connection failure/retry, draft retention and responsive layout');
|
||||
} finally { await browser.close(); }
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
@@ -0,0 +1,18 @@
|
||||
import { expect, test } from 'bun:test';
|
||||
import { claudeCodeModelPreview, isKnownClaudeCodeModel } from '../src/services/claudeCodeModels';
|
||||
|
||||
const routes = { opus: 'model-a', sonnet: 'model-b', haiku: 'model-c', opus1m: true, sonnet1m: false, haiku1m: false };
|
||||
test('role selections follow changed mappings and role context flags', () => {
|
||||
expect(claudeCodeModelPreview('opus', routes).model).toBe('model-a[1m]');
|
||||
expect(claudeCodeModelPreview('sonnet', { ...routes, sonnet: 'model-d', sonnet1m: true }).model).toBe('model-d[1m]');
|
||||
});
|
||||
test('direct model stays pinned and has its own context flag', () => {
|
||||
expect(claudeCodeModelPreview('model-a', routes)).toEqual({ role: null, model: 'model-a', context1m: false });
|
||||
expect(claudeCodeModelPreview('model-a[1m]', { ...routes, opus: 'other' }).model).toBe('model-a[1m]');
|
||||
});
|
||||
test('role aliases do not need to appear in CPA model list', () => {
|
||||
expect(isKnownClaudeCodeModel('sonnet[1m]', [])).toBeTrue();
|
||||
expect(isKnownClaudeCodeModel('', [])).toBeTrue();
|
||||
expect(isKnownClaudeCodeModel('model-a[1m]', [{ name: 'model-a' }])).toBeTrue();
|
||||
expect(isKnownClaudeCodeModel('unknown', [{ name: 'model-a' }])).toBeFalse();
|
||||
});
|
||||
Reference in new issue
Block a user