mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 14:57:15 +00:00
fix(agents): combine runner selection and configuration in one tab
This commit is contained in:
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user