fix(agents): combine runner selection and configuration in one tab

This commit is contained in:
Hyu
2026-09-15 14:27:27 +08:00
parent 5c98329170
commit 2e6fe63750
4 changed files with 141 additions and 28 deletions
@@ -261,7 +261,7 @@ export default function AgentDetailContent({ id }: { id: string }) {
hasUnsavedChanges={formDirty}
beforeRun={async () => agentFormRef.current?.save() ?? false}
onOpenRunnerConfig={() =>
agentFormRef.current?.openSection('runner_config')
agentFormRef.current?.openSection('runner')
}
supportedEventPatterns={supportedEventPatterns}
availableEventTypes={availableEventTypes}
@@ -14,7 +14,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import { Bot, Loader2, SlidersHorizontal, Wrench } from 'lucide-react';
import { Bot, Loader2, Wrench } from 'lucide-react';
import { httpClient } from '@/app/infra/http/HttpClient';
import {
Agent,
@@ -72,10 +72,7 @@ interface AgentFormComponentProps {
onPlatformToolsChange?: (tools: AgentPlatformTool[]) => void;
}
export type AgentConfigSection =
| 'runner'
| 'runner_config'
| 'events_and_tools';
export type AgentConfigSection = 'runner' | 'events_and_tools';
export interface AgentFormHandle {
openSection: (section: AgentConfigSection) => void;
@@ -382,13 +379,6 @@ function AgentFormComponent(
label: t('agents.runnerSettings'),
icon: Bot,
},
{
name: 'runner_config',
label: selectedRunnerOption
? extractI18nObject(selectedRunnerOption.label)
: t('pipelines.configuration'),
icon: SlidersHorizontal,
},
{
name: 'events_and_tools',
label: t('agents.eventsAndTools'),
@@ -678,7 +668,7 @@ function AgentFormComponent(
}
>
<div className="min-w-0">
<TabsList className="grid h-auto w-full min-w-0 grid-cols-3">
<TabsList className="grid h-auto w-full min-w-0 grid-cols-2">
{primarySections.map((section) => {
const Icon = section.icon;
return (
@@ -713,11 +703,6 @@ function AgentFormComponent(
</CardHeader>
</Card>
)}
</div>
)}
{activeSection === 'runner_config' && (
<div className="space-y-6">
{runnerInstallRecovering ? (
<Card>
<CardHeader>
@@ -730,7 +715,7 @@ function AgentFormComponent(
</Card>
) : activeRunnerStage ? (
renderDynamicStage(activeRunnerStage)
) : (
) : runnerConfigSchema ? (
<Card>
<CardHeader>
<CardTitle>{t('agents.runnerSettings')}</CardTitle>
@@ -739,7 +724,7 @@ function AgentFormComponent(
</CardDescription>
</CardHeader>
</Card>
)}
) : null}
</div>
)}
-1
View File
@@ -1187,7 +1187,6 @@ test.describe('agent runner resource selectors', () => {
await page.goto('/home/agents?id=agent-scope');
await page.getByRole('tab', { name: /^Runner$/ }).click();
await page.getByRole('tab', { name: 'Local Agent' }).click();
await page.getByRole('button', { name: 'Edit tools' }).click();
const dialog = page.getByRole('dialog');
@@ -78,9 +78,24 @@ test.describe('processor detail workbench', () => {
const flow = configPanel.getByRole('tablist');
await expect(flow.getByRole('tab').nth(0)).toContainText('Runner');
await expect(flow.getByRole('tab').nth(1)).toContainText('Local Agent');
await expect(flow.getByRole('tab').nth(2)).toContainText('Events & tools');
await expect(flow.getByRole('tab')).toHaveCount(3);
await expect(flow.getByRole('tab').nth(1)).toContainText('Events & tools');
await expect(flow.getByRole('tab')).toHaveCount(2);
const selectorCard = configPanel.locator('[data-slot="card"]').filter({
has: page.getByRole('combobox', { name: 'Runner', exact: true }),
});
const configCard = configPanel.locator('[data-slot="card"]').filter({
has: page
.locator('[data-slot="card-title"]')
.getByText('Local Agent', { exact: true }),
});
await expect(selectorCard).toBeVisible();
await expect(configCard).toBeVisible();
const selectorCardBox = await selectorCard.boundingBox();
const configCardBox = await configCard.boundingBox();
expect(configCardBox!.y).toBeGreaterThan(
selectorCardBox!.y + selectorCardBox!.height,
);
await expect(flow.getByText('Management')).toHaveCount(0);
await page.setViewportSize({ width: 1024, height: 900 });
@@ -122,7 +137,7 @@ test.describe('processor detail workbench', () => {
.last(),
).toBeVisible();
await flow.getByRole('tab').nth(2).click();
await flow.getByRole('tab').nth(1).click();
await expect(
configPanel.getByText('Events & tools', { exact: true }).last(),
).toBeVisible();
@@ -171,7 +186,7 @@ test.describe('processor detail workbench', () => {
await page.getByRole('option').filter({ hasText: 'All events' }).click();
await page.keyboard.press('Escape');
await flow.getByRole('tab').nth(1).click();
await flow.getByRole('tab').nth(0).click();
await expect(
configPanel.getByText('Local Agent', { exact: true }).last(),
).toBeVisible();
@@ -277,7 +292,7 @@ test.describe('processor detail workbench', () => {
.getByRole('button', { name: 'Review runner configuration' })
.click();
await expect(
page.getByRole('tab', { name: 'Local Agent', exact: true }),
page.getByRole('tab', { name: 'Runner', exact: true }),
).toHaveAttribute('data-state', 'active');
});
@@ -485,3 +500,117 @@ test.describe('processor detail workbench', () => {
).toBeVisible();
});
});
test('merged runner page preserves both runner configurations when switching and saving', async ({
page,
}) => {
await installLangBotApiMocks(page, { authenticated: true });
const first = 'plugin:qa/first/default';
const second = 'plugin:qa/second/default';
const agent = {
uuid: 'agent-switch',
kind: 'agent',
name: 'Runner switch',
supported_event_patterns: ['*'],
config: {
runner: { id: first },
runner_config: {
[first]: { greeting: 'First saved value' },
[second]: { greeting: 'Second saved value' },
},
},
};
let saved: Record<string, unknown> | undefined;
await page.route('**/api/v1/agents/agent-switch', async (route) => {
if (route.request().method() === 'PUT') {
saved = route.request().postDataJSON();
await route.fulfill({ json: { code: 0, data: {} } });
} else await route.fulfill({ json: { code: 0, data: { agent } } });
});
await page.route('**/api/v1/agents/_/metadata', (route) =>
route.fulfill({
json: {
code: 0,
data: {
platform_tools: [],
host_tools: [],
runner_config: {
name: 'ai',
label: { en_US: 'AI' },
stages: [
{
name: 'runner',
label: { en_US: 'Execution mode' },
config: [
{
name: 'id',
label: { en_US: 'Runner' },
type: 'select',
required: true,
options: [
{ name: first, label: { en_US: 'First runner' } },
{ name: second, label: { en_US: 'Second runner' } },
],
},
],
},
...[first, second].map((name, index) => ({
name,
label: {
en_US: index === 0 ? 'First settings' : 'Second settings',
},
config: [
{
name: 'greeting',
label: { en_US: 'Greeting' },
type: 'string',
required: true,
},
],
})),
],
},
},
},
}),
);
await page.goto('/home/agents?id=agent-switch');
const config = page.getByRole('region', {
name: 'Configuration',
exact: true,
});
const runner = config.getByRole('combobox', { name: 'Runner', exact: true });
const greeting = config.getByRole('textbox');
await expect(greeting).toHaveValue('First saved value');
await greeting.fill('First edited value');
await runner.click();
await page
.getByRole('option', { name: `Second runner ${second}`, exact: true })
.click();
await expect(greeting).toHaveValue('Second saved value');
await greeting.fill('Second edited value');
await runner.click();
await page
.getByRole('option', { name: `First runner ${first}`, exact: true })
.click();
await expect(greeting).toHaveValue('First edited value');
await expect(
config.getByRole('tab', { name: 'Runner', exact: true }),
).toHaveAttribute('data-state', 'active');
await page.getByRole('button', { name: 'Save', exact: true }).click();
await expect
.poll(() => saved)
.toMatchObject({
config: {
runner: { id: first },
runner_config: {
[first]: { greeting: 'First edited value' },
[second]: { greeting: 'Second edited value' },
},
},
});
await expect(
page.getByRole('button', { name: 'Save', exact: true }),
).toBeDisabled();
await page.screenshot({ path: '../../.codex-run/agent-merged-config.png' });
});