feat(web): unify processor detail workbench

This commit is contained in:
RockChinQ
2026-08-25 11:54:59 +08:00
parent 49d0aac210
commit 46aea2b499
10 changed files with 835 additions and 477 deletions
+30 -58
View File
@@ -1,13 +1,11 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Bug, Settings } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { httpClient } from '@/app/infra/http/HttpClient';
import { useCurrentWorkspace } from '@/app/infra/http';
import { Agent } from '@/app/infra/entities/api';
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
import ProcessorDetailWorkbench from '@/app/home/components/processor-detail/ProcessorDetailWorkbench';
import PipelineDetailContent from '@/app/home/pipelines/PipelineDetailContent';
import AgentCreateContent from './components/AgentCreateContent';
import AgentDebugPanel from './components/AgentDebugPanel';
@@ -18,6 +16,8 @@ export default function AgentDetailContent({ id }: { id: string }) {
const navigate = useNavigate();
const { t } = useTranslation();
const currentWorkspace = useCurrentWorkspace();
const canManage =
currentWorkspace?.permissions.includes('resource.manage') ?? false;
const canOperate =
currentWorkspace?.permissions.includes('runtime.operate') ?? false;
const { refreshPipelines, pipelines, setDetailEntityName } = useSidebarData();
@@ -25,7 +25,6 @@ export default function AgentDetailContent({ id }: { id: string }) {
const [loading, setLoading] = useState(!isCreateMode);
const [formDirty, setFormDirty] = useState(false);
const [formSaving, setFormSaving] = useState(false);
const [activeTab, setActiveTab] = useState('config');
useEffect(() => {
if (isCreateMode) {
@@ -79,42 +78,17 @@ export default function AgentDetailContent({ id }: { id: string }) {
}
return (
<div className="flex h-full min-w-0 flex-col">
<div className="flex items-center justify-between pb-4 shrink-0">
<h1 className="text-xl font-semibold">{t('agents.editAgent')}</h1>
<Button
type="submit"
form="agent-form"
disabled={!formDirty || formSaving}
className={activeTab !== 'config' ? 'invisible' : ''}
>
{t('common.save')}
</Button>
</div>
<Tabs
key={id}
value={activeTab}
onValueChange={setActiveTab}
className="flex min-h-0 min-w-0 flex-1 flex-col"
>
<TabsList className="shrink-0">
<TabsTrigger value="config" className="gap-1.5">
<Settings className="size-3.5" />
{t('pipelines.configuration')}
</TabsTrigger>
{canOperate && (
<TabsTrigger value="debug" className="gap-1.5">
<Bug className="size-3.5" />
{t('agents.debugTab')}
</TabsTrigger>
)}
</TabsList>
<TabsContent
value="config"
className="mt-4 min-h-0 min-w-0 flex-1 overflow-hidden"
>
<ProcessorDetailWorkbench
key={id}
title={t('agents.editAgent')}
saveLabel={t('common.save')}
saveFormId="agent-form"
canSave={canManage}
isDirty={formDirty}
isSaving={formSaving}
configTitle={t('pipelines.configuration')}
configContent={
<fieldset className="contents" disabled={!canManage}>
<AgentFormComponent
agentId={id}
onFinish={() => {
@@ -127,23 +101,21 @@ export default function AgentDetailContent({ id }: { id: string }) {
onDirtyChange={setFormDirty}
onSavingChange={setFormSaving}
/>
</TabsContent>
{canOperate && (
<TabsContent
value="debug"
className="mt-4 min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden"
>
<AgentDebugPanel
agentId={id}
supportedEventPatterns={
agent.supported_event_patterns ??
agent.capability?.supported_event_patterns ?? ['*']
}
/>
</TabsContent>
)}
</Tabs>
</div>
</fieldset>
}
debugTitle={canOperate ? t('agents.debugTab') : undefined}
debugContent={
canOperate ? (
<AgentDebugPanel
agentId={id}
supportedEventPatterns={
agent.supported_event_patterns ??
agent.capability?.supported_event_patterns ?? ['*']
}
/>
) : undefined
}
unsavedLabel={t('pipelines.unsavedChanges')}
/>
);
}
@@ -1,25 +1,11 @@
import { useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import {
AlertTriangle,
Braces,
LoaderCircle,
MessageSquare,
Play,
RotateCcw,
} from 'lucide-react';
import { AlertTriangle, LoaderCircle, Play, RotateCcw } from 'lucide-react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
@@ -205,162 +191,149 @@ export default function AgentDebugPanel({
}
return (
<div className="mx-auto grid w-full min-w-0 max-w-6xl gap-6 pb-8 lg:grid-cols-[minmax(0,1fr)_minmax(22rem,0.8fr)]">
<Card className="min-w-0">
<CardHeader>
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="space-y-1">
<CardTitle className="flex items-center gap-2">
{isMessageEvent ? (
<MessageSquare className="size-5" />
) : (
<Braces className="size-5" />
)}
{t('agents.debugTitle')}
</CardTitle>
<CardDescription>{t('agents.debugDescription')}</CardDescription>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={resetSession}
>
<RotateCcw className="size-4" />
{t('agents.debugResetSession')}
</Button>
<div className="flex h-full min-h-0 min-w-0 flex-col">
<div className="shrink-0 space-y-3 border-b p-3">
<div className="flex items-end gap-2">
<div className="min-w-0 flex-1 space-y-1.5">
<Label>{t('agents.debugEventType')}</Label>
<Select value={preset} onValueChange={selectPreset}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{EVENT_PRESETS.map((item) => (
<SelectItem key={item.value} value={item.value}>
{t(item.labelKey)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardHeader>
<CardContent className="space-y-5">
<Alert>
<AlertTriangle />
<AlertTitle>{t('agents.debugActualRun')}</AlertTitle>
<AlertDescription>
{t('agents.debugActualRunDescription')}
</AlertDescription>
</Alert>
<Button
type="button"
variant="outline"
size="icon"
onClick={resetSession}
title={t('agents.debugResetSession')}
>
<RotateCcw className="size-4" />
</Button>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label>{t('agents.debugEventType')}</Label>
<Select value={preset} onValueChange={selectPreset}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{EVENT_PRESETS.map((item) => (
<SelectItem key={item.value} value={item.value}>
{t(item.labelKey)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{preset === 'custom' && (
<div className="space-y-2">
<Label htmlFor="agent-debug-custom-event">
{t('agents.debugCustomEventType')}
</Label>
<Input
id="agent-debug-custom-event"
value={customEventType}
onChange={(event) => setCustomEventType(event.target.value)}
placeholder="custom.event"
/>
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="agent-debug-input">
{isMessageEvent
? t('agents.debugMessageInput')
: t('agents.debugEventSummary')}
{preset === 'custom' && (
<div className="space-y-1.5">
<Label htmlFor="agent-debug-custom-event">
{t('agents.debugCustomEventType')}
</Label>
<Textarea
id="agent-debug-input"
value={inputText}
onChange={(event) => setInputText(event.target.value)}
className="min-h-24 resize-y"
placeholder={t('agents.debugInputPlaceholder')}
<Input
id="agent-debug-custom-event"
value={customEventType}
onChange={(event) => setCustomEventType(event.target.value)}
placeholder="custom.event"
/>
</div>
)}
<div className="space-y-2">
<div className="flex flex-wrap items-center justify-between gap-2">
<Label htmlFor="agent-debug-payload">
{t('agents.debugEventPayload')}
</Label>
<span className="text-xs text-muted-foreground">
{t('agents.debugSupportedEvents')}: {supportedLabel}
</span>
</div>
<Alert className="py-2">
<AlertTriangle />
<AlertTitle>{t('agents.debugActualRun')}</AlertTitle>
<AlertDescription className="text-xs">
{t('agents.debugActualRunDescription')}
</AlertDescription>
</Alert>
</div>
<div className="min-h-0 flex-1 overflow-y-auto p-3">
<div className="mb-3">
<p className="text-sm font-medium">{t('agents.debugTranscript')}</p>
<p className="text-xs text-muted-foreground">
{t('agents.debugTranscriptDescription')}
</p>
</div>
{entries.length === 0 ? (
<div className="flex min-h-48 items-center justify-center rounded-lg border border-dashed p-6 text-center text-sm text-muted-foreground">
{t('agents.debugEmptyTranscript')}
</div>
) : (
<div className="space-y-3">
{entries.map((entry) => (
<div
key={entry.id}
className={`rounded-lg border p-3 ${
entry.direction === 'output'
? 'border-primary/20 bg-primary/5'
: entry.direction === 'error'
? 'border-destructive/30 bg-destructive/5'
: 'bg-muted/40'
}`}
>
<div className="mb-2 flex items-center justify-between gap-2">
<Badge variant="outline">{entry.eventType}</Badge>
<span className="text-xs text-muted-foreground">
{entry.direction === 'output'
? t('agents.debugAgentOutput')
: entry.direction === 'error'
? t('common.error')
: t('agents.debugTestInput')}
</span>
</div>
<pre className="min-w-0 whitespace-pre-wrap break-words font-sans text-sm leading-relaxed">
{entry.text}
</pre>
</div>
))}
</div>
)}
</div>
<div className="shrink-0 space-y-3 border-t p-3">
<div className="space-y-1.5">
<Label htmlFor="agent-debug-input">
{isMessageEvent
? t('agents.debugMessageInput')
: t('agents.debugEventSummary')}
</Label>
<Textarea
id="agent-debug-input"
value={inputText}
onChange={(event) => setInputText(event.target.value)}
className="min-h-20 resize-y"
placeholder={t('agents.debugInputPlaceholder')}
/>
</div>
<details className="rounded-md border bg-muted/20 px-3 py-2">
<summary className="cursor-pointer text-xs font-medium">
{t('agents.debugEventPayload')}
</summary>
<div className="mt-2 space-y-2">
<p className="text-xs text-muted-foreground">
{t('agents.debugSupportedEvents')}: {supportedLabel}
</p>
<Textarea
id="agent-debug-payload"
value={eventDataText}
onChange={(event) => setEventDataText(event.target.value)}
className="min-h-40 resize-y font-mono text-xs"
className="min-h-28 resize-y font-mono text-xs"
spellCheck={false}
/>
</div>
</details>
<div className="flex justify-end">
<Button type="button" disabled={running} onClick={runDebugEvent}>
{running ? (
<LoaderCircle className="size-4 animate-spin" />
) : (
<Play className="size-4" />
)}
{running ? t('agents.debugRunning') : t('agents.debugRun')}
</Button>
</div>
</CardContent>
</Card>
<Card className="min-h-[28rem] min-w-0">
<CardHeader>
<CardTitle>{t('agents.debugTranscript')}</CardTitle>
<CardDescription>
{t('agents.debugTranscriptDescription')}
</CardDescription>
</CardHeader>
<CardContent>
{entries.length === 0 ? (
<div className="flex min-h-72 items-center justify-center rounded-lg border border-dashed p-8 text-center text-sm text-muted-foreground">
{t('agents.debugEmptyTranscript')}
</div>
<Button
type="button"
className="w-full"
disabled={running}
onClick={runDebugEvent}
>
{running ? (
<LoaderCircle className="size-4 animate-spin" />
) : (
<div className="max-h-[42rem] space-y-4 overflow-y-auto pr-1">
{entries.map((entry) => (
<div
key={entry.id}
className={`rounded-lg border p-3 ${
entry.direction === 'output'
? 'border-primary/20 bg-primary/5'
: entry.direction === 'error'
? 'border-destructive/30 bg-destructive/5'
: 'bg-muted/40'
}`}
>
<div className="mb-2 flex items-center justify-between gap-2">
<Badge variant="outline">{entry.eventType}</Badge>
<span className="text-xs text-muted-foreground">
{entry.direction === 'output'
? t('agents.debugAgentOutput')
: entry.direction === 'error'
? t('common.error')
: t('agents.debugTestInput')}
</span>
</div>
<pre className="min-w-0 whitespace-pre-wrap break-words font-sans text-sm leading-relaxed">
{entry.text}
</pre>
</div>
))}
</div>
<Play className="size-4" />
)}
</CardContent>
</Card>
{running ? t('agents.debugRunning') : t('agents.debugRun')}
</Button>
</div>
</div>
);
}
@@ -6,13 +6,17 @@ import { z } from 'zod';
import { useTranslation } from 'react-i18next';
import { toast } from 'sonner';
import {
Bot,
CircleAlert,
CircleCheck,
Info,
LoaderCircle,
Power,
RefreshCw,
SlidersHorizontal,
Trash2,
Unplug,
Zap,
} from 'lucide-react';
import { httpClient } from '@/app/infra/http/HttpClient';
import { Agent, ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
@@ -60,6 +64,8 @@ interface AgentFormComponentProps {
onSavingChange?: (saving: boolean) => void;
}
type AgentConfigSection = 'events' | 'runner' | 'runner_config' | 'basic';
export default function AgentFormComponent({
agentId,
onFinish,
@@ -76,6 +82,8 @@ export default function AgentFormComponent({
const [pluginStatusError, setPluginStatusError] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [activeSection, setActiveSection] =
useState<AgentConfigSection>('runner');
const isSavingRef = useRef(false);
const formSchema = z.object({
@@ -182,6 +190,35 @@ export default function AgentFormComponent({
const selectedRunnerOption = runnerOptions.find(
(option) => option.name === currentRunner,
);
const runnerSelectorStage = runnerConfigSchema?.stages.find(
(stage) => stage.name === 'runner',
);
const activeRunnerStage = runnerConfigSchema?.stages.find(
(stage) => stage.name === currentRunner,
);
const primarySections: Array<{
name: AgentConfigSection;
label: string;
icon: React.ElementType;
}> = [
{
name: 'events',
label: t('agents.bindableEvents'),
icon: Zap,
},
{
name: 'runner',
label: t('agents.runnerSettings'),
icon: Bot,
},
{
name: 'runner_config',
label: selectedRunnerOption
? extractI18nObject(selectedRunnerOption.label)
: t('pipelines.configuration'),
icon: SlidersHorizontal,
},
];
function renderRunnerStatusActions(showRetry = true) {
return (
@@ -433,63 +470,147 @@ export default function AgentFormComponent({
onSubmit={form.handleSubmit(handleSubmit)}
className="mb-2 flex h-full min-h-0 min-w-0 flex-1 flex-col"
>
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<div className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden">
<div className="mx-auto flex w-full min-w-0 max-w-5xl flex-col gap-6 pb-8">
{
<div className="contents">
<Card className="order-2">
<nav className="mb-4 shrink-0 space-y-2 border-b pb-4">
<div className="overflow-x-auto">
<ol className="grid min-w-[34rem] grid-cols-3 gap-2">
{primarySections.map((section, index) => {
const Icon = section.icon;
return (
<li key={section.name} className="min-w-0">
<button
type="button"
onClick={() => setActiveSection(section.name)}
className={`flex w-full min-w-0 items-center gap-2 rounded-lg border px-3 py-3 text-left text-sm font-medium transition-colors ${
activeSection === section.name
? 'border-primary/50 bg-primary/5 text-foreground shadow-sm'
: 'border-border bg-background text-muted-foreground hover:bg-muted hover:text-foreground'
}`}
>
<span
className={`flex size-7 shrink-0 items-center justify-center rounded-full text-xs ${
activeSection === section.name
? 'bg-primary text-primary-foreground'
: 'bg-muted text-muted-foreground'
}`}
>
{index + 1}
</span>
<Icon className="hidden size-4 shrink-0 xl:block" />
<span className="min-w-0 leading-tight">
{section.label}
</span>
</button>
</li>
);
})}
</ol>
</div>
<button
type="button"
onClick={() => setActiveSection('basic')}
className={`flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs font-medium transition-colors ${
activeSection === 'basic'
? 'bg-accent text-accent-foreground'
: 'text-muted-foreground hover:bg-muted hover:text-foreground'
}`}
>
<Info className="size-3.5" />
{t('agents.basicInfo')}
</button>
</nav>
<div className="min-h-0 min-w-0 flex-1 overflow-y-auto overflow-x-hidden">
<div className="mx-auto w-full min-w-0 max-w-5xl space-y-6 pb-8">
{activeSection === 'runner' && (
<div className="space-y-6">
{renderRunnerStatus()}
{runnerSelectorStage
? renderDynamicStage(runnerSelectorStage)
: !runnerConfigSchema && (
<Card>
<CardHeader>
<CardTitle>
{t('agents.runnerSettings')}
</CardTitle>
<CardDescription>
{t('agents.noRunnerMetadata')}
</CardDescription>
</CardHeader>
</Card>
)}
</div>
)}
{activeSection === 'runner_config' && (
<div className="space-y-6">
{activeRunnerStage ? (
renderDynamicStage(activeRunnerStage)
) : (
<Card>
<CardHeader>
<CardTitle>{t('agents.basicInfo')}</CardTitle>
<CardTitle>{t('agents.runnerSettings')}</CardTitle>
<CardDescription>
{t('agents.basicInfoDescription')}
{t('agents.noRunnerMetadata')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex gap-4 items-start">
<FormField
control={form.control}
name="basic.name"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
{t('common.name')}
<span className="text-destructive">*</span>
</FormLabel>
<FormControl>
<Input
{...field}
value={field.value ?? ''}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="basic.emoji"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.icon')}</FormLabel>
<FormControl>
<EmojiPicker
value={field.value}
onChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</Card>
)}
</div>
)}
{activeSection === 'events' && (
<Card>
<CardHeader>
<CardTitle>{t('agents.bindableEvents')}</CardTitle>
<CardDescription>
{t('agents.bindableEventsDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<FormField
control={form.control}
name="supported_event_patterns_text"
render={({ field }) => (
<FormItem>
<FormLabel>{t('agents.supportedEvents')}</FormLabel>
<FormControl>
<Textarea
{...field}
className="min-h-32 font-mono text-sm"
placeholder={'*\nmessage.received\ngroup.*'}
/>
</FormControl>
<FormDescription>
{t('agents.supportedEventsDescription')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
)}
{activeSection === 'basic' && (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>{t('agents.basicInfo')}</CardTitle>
<CardDescription>
{t('agents.basicInfoDescription')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-start gap-4">
<FormField
control={form.control}
name="basic.description"
name="basic.name"
render={({ field }) => (
<FormItem>
<FormLabel>{t('common.description')}</FormLabel>
<FormItem className="flex-1">
<FormLabel>
{t('common.name')}
<span className="text-destructive">*</span>
</FormLabel>
<FormControl>
<Input {...field} value={field.value ?? ''} />
</FormControl>
@@ -497,122 +618,98 @@ export default function AgentFormComponent({
</FormItem>
)}
/>
<FormField
control={form.control}
name="basic.enabled"
name="basic.emoji"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="flex items-center gap-2">
<Power className="size-4" />
{t('agents.enabled')}
</FormLabel>
<FormDescription>
{t('agents.enabledDescription')}
</FormDescription>
</div>
<FormItem>
<FormLabel>{t('common.icon')}</FormLabel>
<FormControl>
<Switch
checked={field.value ?? true}
onCheckedChange={field.onChange}
<EmojiPicker
value={field.value}
onChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
</div>
<Card className="order-4 border-destructive/50">
<CardHeader>
<CardTitle className="text-destructive">
{t('agents.dangerZone')}
</CardTitle>
<CardDescription>
{t('agents.dangerZoneDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<div className="space-y-1">
<p className="text-sm font-medium">
{t('agents.deleteAgentAction')}
</p>
<p className="text-sm text-muted-foreground">
{t('agents.deleteAgentHint')}
</p>
</div>
<Button
type="button"
variant="destructive"
size="sm"
disabled={isSaving}
onClick={() => setShowDeleteConfirm(true)}
>
<Trash2 className="size-4 mr-1.5" />
{t('common.delete')}
</Button>
</div>
</CardContent>
</Card>
</div>
}
{
<div className="order-1 space-y-6">
{renderRunnerStatus()}
{runnerConfigSchema?.stages.map((stage) =>
renderDynamicStage(stage),
)}
{!runnerConfigSchema && (
<Card>
<CardHeader>
<CardTitle>{t('agents.runnerSettings')}</CardTitle>
<CardDescription>
{t('agents.noRunnerMetadata')}
</CardDescription>
</CardHeader>
</Card>
)}
</div>
}
{
<Card className="order-3">
<CardHeader>
<CardTitle>{t('agents.bindableEvents')}</CardTitle>
<CardDescription>
{t('agents.bindableEventsDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<FormField
control={form.control}
name="supported_event_patterns_text"
name="basic.description"
render={({ field }) => (
<FormItem>
<FormLabel>
{t('agents.supportedEvents')}
</FormLabel>
<FormLabel>{t('common.description')}</FormLabel>
<FormControl>
<Textarea
{...field}
className="min-h-32 font-mono text-sm"
placeholder={'*\nmessage.received\ngroup.*'}
<Input {...field} value={field.value ?? ''} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="basic.enabled"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="flex items-center gap-2">
<Power className="size-4" />
{t('agents.enabled')}
</FormLabel>
<FormDescription>
{t('agents.enabledDescription')}
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value ?? true}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormDescription>
{t('agents.supportedEventsDescription')}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
}
</div>
<Card className="border-destructive/50">
<CardHeader>
<CardTitle className="text-destructive">
{t('agents.dangerZone')}
</CardTitle>
<CardDescription>
{t('agents.dangerZoneDescription')}
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between gap-4">
<div className="space-y-1">
<p className="text-sm font-medium">
{t('agents.deleteAgentAction')}
</p>
<p className="text-sm text-muted-foreground">
{t('agents.deleteAgentHint')}
</p>
</div>
<Button
type="button"
variant="destructive"
size="sm"
disabled={isSaving}
onClick={() => setShowDeleteConfirm(true)}
>
<Trash2 className="mr-1.5 size-4" />
{t('common.delete')}
</Button>
</div>
</CardContent>
</Card>
</div>
)}
</div>
</div>
</form>