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} hasUnsavedChanges={formDirty}
beforeRun={async () => agentFormRef.current?.save() ?? false} beforeRun={async () => agentFormRef.current?.save() ?? false}
onOpenRunnerConfig={() => onOpenRunnerConfig={() =>
agentFormRef.current?.openSection('runner_config') agentFormRef.current?.openSection('runner')
} }
supportedEventPatterns={supportedEventPatterns} supportedEventPatterns={supportedEventPatterns}
availableEventTypes={availableEventTypes} availableEventTypes={availableEventTypes}
@@ -14,7 +14,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod'; import { z } from 'zod';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { toast } from 'sonner'; 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 { httpClient } from '@/app/infra/http/HttpClient';
import { import {
Agent, Agent,
@@ -72,10 +72,7 @@ interface AgentFormComponentProps {
onPlatformToolsChange?: (tools: AgentPlatformTool[]) => void; onPlatformToolsChange?: (tools: AgentPlatformTool[]) => void;
} }
export type AgentConfigSection = export type AgentConfigSection = 'runner' | 'events_and_tools';
| 'runner'
| 'runner_config'
| 'events_and_tools';
export interface AgentFormHandle { export interface AgentFormHandle {
openSection: (section: AgentConfigSection) => void; openSection: (section: AgentConfigSection) => void;
@@ -382,13 +379,6 @@ function AgentFormComponent(
label: t('agents.runnerSettings'), label: t('agents.runnerSettings'),
icon: Bot, icon: Bot,
}, },
{
name: 'runner_config',
label: selectedRunnerOption
? extractI18nObject(selectedRunnerOption.label)
: t('pipelines.configuration'),
icon: SlidersHorizontal,
},
{ {
name: 'events_and_tools', name: 'events_and_tools',
label: t('agents.eventsAndTools'), label: t('agents.eventsAndTools'),
@@ -678,7 +668,7 @@ function AgentFormComponent(
} }
> >
<div className="min-w-0"> <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) => { {primarySections.map((section) => {
const Icon = section.icon; const Icon = section.icon;
return ( return (
@@ -713,11 +703,6 @@ function AgentFormComponent(
</CardHeader> </CardHeader>
</Card> </Card>
)} )}
</div>
)}
{activeSection === 'runner_config' && (
<div className="space-y-6">
{runnerInstallRecovering ? ( {runnerInstallRecovering ? (
<Card> <Card>
<CardHeader> <CardHeader>
@@ -730,7 +715,7 @@ function AgentFormComponent(
</Card> </Card>
) : activeRunnerStage ? ( ) : activeRunnerStage ? (
renderDynamicStage(activeRunnerStage) renderDynamicStage(activeRunnerStage)
) : ( ) : runnerConfigSchema ? (
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>{t('agents.runnerSettings')}</CardTitle> <CardTitle>{t('agents.runnerSettings')}</CardTitle>
@@ -739,7 +724,7 @@ function AgentFormComponent(
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
</Card> </Card>
)} ) : null}
</div> </div>
)} )}
-1
View File
@@ -1187,7 +1187,6 @@ test.describe('agent runner resource selectors', () => {
await page.goto('/home/agents?id=agent-scope'); await page.goto('/home/agents?id=agent-scope');
await page.getByRole('tab', { name: /^Runner$/ }).click(); await page.getByRole('tab', { name: /^Runner$/ }).click();
await page.getByRole('tab', { name: 'Local Agent' }).click();
await page.getByRole('button', { name: 'Edit tools' }).click(); await page.getByRole('button', { name: 'Edit tools' }).click();
const dialog = page.getByRole('dialog'); const dialog = page.getByRole('dialog');
@@ -78,9 +78,24 @@ test.describe('processor detail workbench', () => {
const flow = configPanel.getByRole('tablist'); const flow = configPanel.getByRole('tablist');
await expect(flow.getByRole('tab').nth(0)).toContainText('Runner'); 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(1)).toContainText('Events & tools');
await expect(flow.getByRole('tab').nth(2)).toContainText('Events & tools'); await expect(flow.getByRole('tab')).toHaveCount(2);
await expect(flow.getByRole('tab')).toHaveCount(3); 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 expect(flow.getByText('Management')).toHaveCount(0);
await page.setViewportSize({ width: 1024, height: 900 }); await page.setViewportSize({ width: 1024, height: 900 });
@@ -122,7 +137,7 @@ test.describe('processor detail workbench', () => {
.last(), .last(),
).toBeVisible(); ).toBeVisible();
await flow.getByRole('tab').nth(2).click(); await flow.getByRole('tab').nth(1).click();
await expect( await expect(
configPanel.getByText('Events & tools', { exact: true }).last(), configPanel.getByText('Events & tools', { exact: true }).last(),
).toBeVisible(); ).toBeVisible();
@@ -171,7 +186,7 @@ test.describe('processor detail workbench', () => {
await page.getByRole('option').filter({ hasText: 'All events' }).click(); await page.getByRole('option').filter({ hasText: 'All events' }).click();
await page.keyboard.press('Escape'); await page.keyboard.press('Escape');
await flow.getByRole('tab').nth(1).click(); await flow.getByRole('tab').nth(0).click();
await expect( await expect(
configPanel.getByText('Local Agent', { exact: true }).last(), configPanel.getByText('Local Agent', { exact: true }).last(),
).toBeVisible(); ).toBeVisible();
@@ -277,7 +292,7 @@ test.describe('processor detail workbench', () => {
.getByRole('button', { name: 'Review runner configuration' }) .getByRole('button', { name: 'Review runner configuration' })
.click(); .click();
await expect( await expect(
page.getByRole('tab', { name: 'Local Agent', exact: true }), page.getByRole('tab', { name: 'Runner', exact: true }),
).toHaveAttribute('data-state', 'active'); ).toHaveAttribute('data-state', 'active');
}); });
@@ -485,3 +500,117 @@ test.describe('processor detail workbench', () => {
).toBeVisible(); ).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' });
});