diff --git a/web/src/app/home/agents/AgentDetailContent.tsx b/web/src/app/home/agents/AgentDetailContent.tsx index 95813ce8d..abe7cbe2d 100644 --- a/web/src/app/home/agents/AgentDetailContent.tsx +++ b/web/src/app/home/agents/AgentDetailContent.tsx @@ -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} diff --git a/web/src/app/home/agents/components/AgentFormComponent.tsx b/web/src/app/home/agents/components/AgentFormComponent.tsx index 25e6bcb76..8584662ea 100644 --- a/web/src/app/home/agents/components/AgentFormComponent.tsx +++ b/web/src/app/home/agents/components/AgentFormComponent.tsx @@ -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( } >
- + {primarySections.map((section) => { const Icon = section.icon; return ( @@ -713,11 +703,6 @@ function AgentFormComponent( )} -
- )} - - {activeSection === 'runner_config' && ( -
{runnerInstallRecovering ? ( @@ -730,7 +715,7 @@ function AgentFormComponent( ) : activeRunnerStage ? ( renderDynamicStage(activeRunnerStage) - ) : ( + ) : runnerConfigSchema ? ( {t('agents.runnerSettings')} @@ -739,7 +724,7 @@ function AgentFormComponent( - )} + ) : null}
)} diff --git a/web/tests/e2e/crud-smoke.spec.ts b/web/tests/e2e/crud-smoke.spec.ts index 59c7e46e8..fd7a4e0c3 100644 --- a/web/tests/e2e/crud-smoke.spec.ts +++ b/web/tests/e2e/crud-smoke.spec.ts @@ -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'); diff --git a/web/tests/e2e/processor-detail-workbench.spec.ts b/web/tests/e2e/processor-detail-workbench.spec.ts index 1f81c2cf8..6bdb5b45e 100644 --- a/web/tests/e2e/processor-detail-workbench.spec.ts +++ b/web/tests/e2e/processor-detail-workbench.spec.ts @@ -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 | 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' }); +});