mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-09 07:16:04 +00:00
fix(box): restore sandbox config and shared mcp runtime
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
Download,
|
||||
PlusIcon,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Server,
|
||||
Github,
|
||||
BookOpen,
|
||||
@@ -25,26 +26,20 @@ import {
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from '@/components/ui/card';
|
||||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { httpClient, systemInfo } from '@/app/infra/http/HttpClient';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PluginV4 } from '@/app/infra/entities/plugin';
|
||||
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
|
||||
import {
|
||||
usePluginInstallTasks,
|
||||
} from '@/app/home/plugins/components/plugin-install-task';
|
||||
import { usePluginInstallTasks } from '@/app/home/plugins/components/plugin-install-task';
|
||||
import MCPForm from '@/app/home/mcp/components/mcp-form/MCPForm';
|
||||
import type { MCPFormHandle } from '@/app/home/mcp/components/mcp-form/MCPForm';
|
||||
import type {
|
||||
MCPFormDraft,
|
||||
MCPFormHandle,
|
||||
} from '@/app/home/mcp/components/mcp-form/MCPForm';
|
||||
import SkillForm from '@/app/home/skills/components/skill-form/SkillForm';
|
||||
import type { SkillFormDraft } from '@/app/home/skills/components/skill-form/SkillForm';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
@@ -100,7 +95,6 @@ export default function AddExtensionPage() {
|
||||
|
||||
function AddExtensionContent() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const { refreshPlugins, refreshMCPServers, refreshSkills } = useSidebarData();
|
||||
const {
|
||||
addTask,
|
||||
@@ -128,11 +122,15 @@ function AddExtensionContent() {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const mcpFormRef = useRef<MCPFormHandle>(null);
|
||||
const [mcpTesting, setMcpTesting] = useState(false);
|
||||
const [mcpDraft, setMcpDraft] = useState<MCPFormDraft | undefined>();
|
||||
const [skillDraft, setSkillDraft] = useState<SkillFormDraft | undefined>();
|
||||
|
||||
// GitHub install state
|
||||
const [githubURL, setGithubURL] = useState('');
|
||||
const [githubReleases, setGithubReleases] = useState<GithubRelease[]>([]);
|
||||
const [selectedRelease, setSelectedRelease] = useState<GithubRelease | null>(null);
|
||||
const [selectedRelease, setSelectedRelease] = useState<GithubRelease | null>(
|
||||
null,
|
||||
);
|
||||
const [githubAssets, setGithubAssets] = useState<GithubAsset[]>([]);
|
||||
const [selectedAsset, setSelectedAsset] = useState<GithubAsset | null>(null);
|
||||
const [githubOwner, setGithubOwner] = useState('');
|
||||
@@ -141,12 +139,14 @@ function AddExtensionContent() {
|
||||
const [fetchingAssets, setFetchingAssets] = useState(false);
|
||||
const [githubInstallStatus, setGithubInstallStatus] =
|
||||
useState<GithubInstallStatus>(GithubInstallStatus.WAIT_INPUT);
|
||||
const [githubInstallError, setGithubInstallError] = useState<string | null>(null);
|
||||
const [githubInstallError, setGithubInstallError] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Clear any stale completed tasks on mount
|
||||
clearCompletedTasks();
|
||||
}, []);
|
||||
}, [clearCompletedTasks]);
|
||||
|
||||
useEffect(() => {
|
||||
const onComplete = (_taskId: number, success: boolean) => {
|
||||
@@ -161,20 +161,17 @@ function AddExtensionContent() {
|
||||
};
|
||||
}, [registerOnTaskComplete, unregisterOnTaskComplete, refreshPlugins, t]);
|
||||
|
||||
const handleInstallPlugin = useCallback(
|
||||
async (plugin: PluginV4) => {
|
||||
setInstallInfo({
|
||||
plugin_author: plugin.author,
|
||||
plugin_name: plugin.name,
|
||||
plugin_version: plugin.latest_version,
|
||||
});
|
||||
setInstallExtensionType(plugin.type || 'plugin');
|
||||
setPluginInstallStatus(PluginInstallStatus.ASK_CONFIRM);
|
||||
setInstallError(null);
|
||||
setModalOpen(true);
|
||||
},
|
||||
[],
|
||||
);
|
||||
const handleInstallPlugin = useCallback(async (plugin: PluginV4) => {
|
||||
setInstallInfo({
|
||||
plugin_author: plugin.author,
|
||||
plugin_name: plugin.name,
|
||||
plugin_version: plugin.latest_version,
|
||||
});
|
||||
setInstallExtensionType(plugin.type || 'plugin');
|
||||
setPluginInstallStatus(PluginInstallStatus.ASK_CONFIRM);
|
||||
setInstallError(null);
|
||||
setModalOpen(true);
|
||||
}, []);
|
||||
|
||||
function handleModalConfirm() {
|
||||
setPluginInstallStatus(PluginInstallStatus.INSTALLING);
|
||||
@@ -266,7 +263,7 @@ function AddExtensionContent() {
|
||||
});
|
||||
}
|
||||
},
|
||||
[t, addTask, setSelectedTaskId, refreshPlugins],
|
||||
[t, addTask, setSelectedTaskId, refreshPlugins, refreshSkills],
|
||||
);
|
||||
|
||||
const handleFileSelect = useCallback(() => {
|
||||
@@ -308,13 +305,15 @@ function AddExtensionContent() {
|
||||
[uploadFile],
|
||||
);
|
||||
|
||||
function handleMCPCreated(serverName: string) {
|
||||
function handleMCPCreated(_serverName: string) {
|
||||
setMcpDraft(undefined);
|
||||
refreshMCPServers();
|
||||
setPopoverView('menu');
|
||||
setPopoverOpen(false);
|
||||
}
|
||||
|
||||
function handleSkillCreated(skillName: string) {
|
||||
function handleSkillCreated(_skillName: string) {
|
||||
setSkillDraft(undefined);
|
||||
refreshPlugins();
|
||||
refreshSkills();
|
||||
setPopoverView('menu');
|
||||
@@ -467,13 +466,13 @@ function AddExtensionContent() {
|
||||
function getPopoverWidth(): string {
|
||||
switch (popoverView) {
|
||||
case 'mcp':
|
||||
return 'w-[500px]';
|
||||
return 'w-[calc(100vw-2rem)] sm:w-[560px]';
|
||||
case 'skill':
|
||||
return 'w-[460px]';
|
||||
return 'w-[calc(100vw-2rem)] sm:w-[560px]';
|
||||
case 'github':
|
||||
return 'w-[460px]';
|
||||
return 'w-[calc(100vw-2rem)] sm:w-[480px]';
|
||||
default:
|
||||
return 'w-[360px]';
|
||||
return 'w-[calc(100vw-2rem)] sm:w-[380px]';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,9 +490,6 @@ function AddExtensionContent() {
|
||||
open={popoverOpen}
|
||||
onOpenChange={(open) => {
|
||||
setPopoverOpen(open);
|
||||
if (!open) {
|
||||
setPopoverView('menu');
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -508,12 +504,13 @@ function AddExtensionContent() {
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className={`${getPopoverWidth()} p-4 max-h-[80vh] overflow-y-auto`}
|
||||
forceMount
|
||||
className={`${getPopoverWidth()} max-h-[min(720px,80vh)] overflow-hidden p-0`}
|
||||
align="end"
|
||||
>
|
||||
{/* ===== Menu View ===== */}
|
||||
{popoverView === 'menu' && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 p-4">
|
||||
{/* File upload area */}
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-lg p-6 text-center cursor-pointer transition-colors ${
|
||||
@@ -539,68 +536,75 @@ function AddExtensionContent() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 flex items-center">
|
||||
<span className="w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center text-xs">
|
||||
<span className="bg-popover px-2 text-muted-foreground">
|
||||
{t('addExtension.orContinueWith')}
|
||||
<p className="text-center text-xs text-muted-foreground">
|
||||
{t('addExtension.orContinueWith')}
|
||||
</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
className="group flex w-full items-center gap-3 rounded-md bg-muted/30 p-3 text-left transition-colors outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
||||
onClick={() => setPopoverView('mcp')}
|
||||
>
|
||||
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-background text-muted-foreground transition-colors group-hover:text-foreground">
|
||||
<Server className="size-4" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span className="min-w-0 flex-1 space-y-0.5">
|
||||
<span className="block text-sm font-medium leading-none">
|
||||
{t('mcp.addMCPServer')}
|
||||
</span>
|
||||
<span className="block text-xs leading-relaxed text-muted-foreground">
|
||||
{t('addExtension.addMCPServerHint')}
|
||||
</span>
|
||||
</span>
|
||||
<ChevronRight className="size-4 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5" />
|
||||
</button>
|
||||
|
||||
{/* MCP Config button */}
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start gap-2"
|
||||
onClick={() => setPopoverView('mcp')}
|
||||
>
|
||||
<Server className="w-4 h-4" />
|
||||
{t('mcp.addMCPServer')}
|
||||
</Button>
|
||||
|
||||
{/* Two side-by-side buttons */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-col h-auto py-3 gap-1"
|
||||
<button
|
||||
type="button"
|
||||
className="group flex w-full items-center gap-3 rounded-md bg-muted/30 p-3 text-left transition-colors outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
||||
onClick={() => setPopoverView('github')}
|
||||
>
|
||||
<Github className="w-4 h-4" />
|
||||
<span className="text-xs">
|
||||
{t('addExtension.installFromGithub')}
|
||||
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-background text-muted-foreground transition-colors group-hover:text-foreground">
|
||||
<Github className="size-4" />
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-col h-auto py-3 gap-1"
|
||||
<span className="min-w-0 flex-1 space-y-0.5">
|
||||
<span className="block text-sm font-medium leading-none">
|
||||
{t('addExtension.installFromGithub')}
|
||||
</span>
|
||||
<span className="block text-xs leading-relaxed text-muted-foreground">
|
||||
{t('addExtension.installFromGithubHint')}
|
||||
</span>
|
||||
</span>
|
||||
<ChevronRight className="size-4 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="group flex w-full items-center gap-3 rounded-md bg-muted/30 p-3 text-left transition-colors outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
||||
onClick={() => setPopoverView('skill')}
|
||||
>
|
||||
<BookOpen className="w-4 h-4" />
|
||||
<span className="text-xs">
|
||||
{t('addExtension.createSkill')}
|
||||
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-background text-muted-foreground transition-colors group-hover:text-foreground">
|
||||
<BookOpen className="size-4" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Hints for the two buttons */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<p className="text-[11px] text-muted-foreground text-center px-1">
|
||||
{t('addExtension.installFromGithubHint')}
|
||||
</p>
|
||||
<p className="text-[11px] text-muted-foreground text-center px-1">
|
||||
{t('addExtension.createSkillHint')}
|
||||
</p>
|
||||
<span className="min-w-0 flex-1 space-y-0.5">
|
||||
<span className="block text-sm font-medium leading-none">
|
||||
{t('addExtension.createSkill')}
|
||||
</span>
|
||||
<span className="block text-xs leading-relaxed text-muted-foreground">
|
||||
{t('addExtension.createSkillHint')}
|
||||
</span>
|
||||
</span>
|
||||
<ChevronRight className="size-4 shrink-0 text-muted-foreground transition-transform group-hover:translate-x-0.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ===== MCP Form View ===== */}
|
||||
{popoverView === 'mcp' && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex max-h-[min(720px,80vh)] flex-col">
|
||||
<div className="flex items-center gap-2 px-4 pb-1 pt-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -614,17 +618,19 @@ function AddExtensionContent() {
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[60vh] overflow-y-auto pr-1">
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-4">
|
||||
<MCPForm
|
||||
ref={mcpFormRef}
|
||||
initServerName={undefined}
|
||||
initialDraft={mcpDraft}
|
||||
onFormSubmit={() => {}}
|
||||
onNewServerCreated={handleMCPCreated}
|
||||
onDraftChange={setMcpDraft}
|
||||
onTestingChange={setMcpTesting}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 pt-2 border-t">
|
||||
<div className="flex items-center justify-end gap-2 bg-popover px-4 pb-4 pt-1">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@@ -652,8 +658,8 @@ function AddExtensionContent() {
|
||||
|
||||
{/* ===== Skill Form View ===== */}
|
||||
{popoverView === 'skill' && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex max-h-[min(720px,80vh)] flex-col">
|
||||
<div className="flex items-center gap-2 px-4 pb-1 pt-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -667,15 +673,17 @@ function AddExtensionContent() {
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[60vh] overflow-y-auto pr-1">
|
||||
<div className="min-h-0 flex-1 overflow-y-auto p-4">
|
||||
<SkillForm
|
||||
initSkillName={undefined}
|
||||
initialDraft={skillDraft}
|
||||
onNewSkillCreated={handleSkillCreated}
|
||||
onSkillUpdated={() => {}}
|
||||
onDraftChange={setSkillDraft}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2 pt-2 border-t">
|
||||
<div className="flex items-center justify-end gap-2 bg-popover px-4 pb-4 pt-1">
|
||||
<Button type="submit" form="skill-form" size="sm">
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
@@ -685,8 +693,8 @@ function AddExtensionContent() {
|
||||
|
||||
{/* ===== GitHub Install View ===== */}
|
||||
{popoverView === 'github' && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex max-h-[min(720px,80vh)] flex-col">
|
||||
<div className="flex items-center gap-2 px-4 pb-1 pt-3">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
@@ -703,7 +711,7 @@ function AddExtensionContent() {
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
<div className="max-h-[60vh] overflow-y-auto pr-1 space-y-3">
|
||||
<div className="min-h-0 flex-1 space-y-3 overflow-y-auto p-4">
|
||||
{githubInstallStatus === GithubInstallStatus.WAIT_INPUT && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
@@ -743,7 +751,9 @@ function AddExtensionContent() {
|
||||
size="sm"
|
||||
className="h-6 text-xs px-2"
|
||||
onClick={() => {
|
||||
setGithubInstallStatus(GithubInstallStatus.WAIT_INPUT);
|
||||
setGithubInstallStatus(
|
||||
GithubInstallStatus.WAIT_INPUT,
|
||||
);
|
||||
setGithubReleases([]);
|
||||
}}
|
||||
>
|
||||
@@ -755,7 +765,7 @@ function AddExtensionContent() {
|
||||
{githubReleases.map((release) => (
|
||||
<div
|
||||
key={release.id}
|
||||
className="flex items-center justify-between rounded-md border p-2 hover:bg-accent cursor-pointer text-sm"
|
||||
className="flex cursor-pointer items-center justify-between rounded-md px-2 py-2 text-sm hover:bg-accent"
|
||||
onClick={() => handleReleaseSelect(release)}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -764,7 +774,9 @@ function AddExtensionContent() {
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
{release.tag_name} •{' '}
|
||||
{new Date(release.published_at).toLocaleDateString()}
|
||||
{new Date(
|
||||
release.published_at,
|
||||
).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
{release.prerelease && (
|
||||
@@ -795,7 +807,9 @@ function AddExtensionContent() {
|
||||
size="sm"
|
||||
className="h-6 text-xs px-2"
|
||||
onClick={() => {
|
||||
setGithubInstallStatus(GithubInstallStatus.SELECT_RELEASE);
|
||||
setGithubInstallStatus(
|
||||
GithubInstallStatus.SELECT_RELEASE,
|
||||
);
|
||||
setGithubAssets([]);
|
||||
setSelectedAsset(null);
|
||||
}}
|
||||
@@ -805,7 +819,7 @@ function AddExtensionContent() {
|
||||
</Button>
|
||||
</div>
|
||||
{selectedRelease && (
|
||||
<div className="p-1.5 bg-muted rounded text-[11px]">
|
||||
<div className="rounded-md bg-muted/40 px-2 py-1.5 text-[11px]">
|
||||
<span className="font-medium">
|
||||
{selectedRelease.name || selectedRelease.tag_name}
|
||||
</span>
|
||||
@@ -815,7 +829,7 @@ function AddExtensionContent() {
|
||||
{githubAssets.map((asset) => (
|
||||
<div
|
||||
key={asset.id}
|
||||
className="flex items-center justify-between rounded-md border p-2 hover:bg-accent cursor-pointer"
|
||||
className="flex cursor-pointer items-center justify-between rounded-md px-2 py-2 hover:bg-accent"
|
||||
onClick={() => handleAssetSelect(asset)}
|
||||
>
|
||||
<span className="text-xs truncate">{asset.name}</span>
|
||||
@@ -839,7 +853,9 @@ function AddExtensionContent() {
|
||||
size="sm"
|
||||
className="h-6 text-xs px-2"
|
||||
onClick={() => {
|
||||
setGithubInstallStatus(GithubInstallStatus.SELECT_ASSET);
|
||||
setGithubInstallStatus(
|
||||
GithubInstallStatus.SELECT_ASSET,
|
||||
);
|
||||
setSelectedAsset(null);
|
||||
}}
|
||||
>
|
||||
@@ -848,10 +864,12 @@ function AddExtensionContent() {
|
||||
</Button>
|
||||
</div>
|
||||
{selectedRelease && selectedAsset && (
|
||||
<div className="p-2 bg-muted rounded space-y-1 text-xs">
|
||||
<div className="space-y-1 rounded-md bg-muted/40 px-2 py-2 text-xs">
|
||||
<div>
|
||||
<span className="font-medium">Repository: </span>
|
||||
<span>{githubOwner}/{githubRepo}</span>
|
||||
<span>
|
||||
{githubOwner}/{githubRepo}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium">Release: </span>
|
||||
@@ -863,10 +881,7 @@ function AddExtensionContent() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={handleGithubConfirm}
|
||||
>
|
||||
<Button className="w-full" onClick={handleGithubConfirm}>
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -11,13 +11,6 @@ import { Resolver, useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
} from '@/components/ui/card';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -94,18 +87,16 @@ function StatusDisplay({
|
||||
// Tools list component
|
||||
function ToolsList({ tools }: { tools: MCPTool[] }) {
|
||||
return (
|
||||
<div className="space-y-2 max-h-[300px] overflow-y-auto">
|
||||
<div className="max-h-[300px] space-y-1 overflow-y-auto">
|
||||
{tools.map((tool, index) => (
|
||||
<Card key={index} className="py-3 shadow-none">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">{tool.name}</CardTitle>
|
||||
{tool.description && (
|
||||
<CardDescription className="text-xs">
|
||||
{tool.description}
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
</Card>
|
||||
<div key={index} className="rounded-md px-1 py-2">
|
||||
<div className="text-sm font-medium">{tool.name}</div>
|
||||
{tool.description && (
|
||||
<div className="mt-0.5 text-xs text-muted-foreground">
|
||||
{tool.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -164,10 +155,14 @@ type FormValues = z.infer<ReturnType<typeof getFormSchema>> & {
|
||||
ssereadtimeout: number;
|
||||
};
|
||||
|
||||
export type MCPFormDraft = Partial<FormValues>;
|
||||
|
||||
interface MCPFormProps {
|
||||
initServerName?: string;
|
||||
initialDraft?: MCPFormDraft;
|
||||
onFormSubmit: () => void;
|
||||
onNewServerCreated: (serverName: string) => void;
|
||||
onDraftChange?: (draft: MCPFormDraft) => void;
|
||||
onDirtyChange?: (dirty: boolean) => void;
|
||||
onTestingChange?: (testing: boolean) => void;
|
||||
}
|
||||
@@ -181,8 +176,10 @@ export interface MCPFormHandle {
|
||||
const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
||||
{
|
||||
initServerName,
|
||||
initialDraft,
|
||||
onFormSubmit,
|
||||
onNewServerCreated,
|
||||
onDraftChange,
|
||||
onDirtyChange,
|
||||
onTestingChange,
|
||||
},
|
||||
@@ -191,6 +188,7 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
||||
const { t } = useTranslation();
|
||||
const formSchema = getFormSchema(t);
|
||||
const isEditMode = !!initServerName;
|
||||
const initialDraftRef = useRef(initialDraft);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema) as unknown as Resolver<FormValues>,
|
||||
@@ -203,6 +201,7 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
||||
timeout: 30,
|
||||
ssereadtimeout: 300,
|
||||
extra_args: [],
|
||||
...initialDraftRef.current,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -259,9 +258,10 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
||||
timeout: 30,
|
||||
ssereadtimeout: 300,
|
||||
extra_args: [],
|
||||
...initialDraftRef.current,
|
||||
});
|
||||
setExtraArgs([]);
|
||||
setStdioArgs([]);
|
||||
setExtraArgs(initialDraftRef.current?.extra_args ?? []);
|
||||
setStdioArgs(initialDraftRef.current?.args ?? []);
|
||||
setRuntimeInfo(null);
|
||||
isInitializing.current = false;
|
||||
}
|
||||
@@ -274,6 +274,20 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
||||
};
|
||||
}, [initServerName]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!onDraftChange || isEditMode) return;
|
||||
|
||||
const subscription = form.watch((values) => {
|
||||
onDraftChange({
|
||||
...values,
|
||||
extra_args: extraArgs,
|
||||
args: stdioArgs,
|
||||
} as MCPFormDraft);
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, [form, isEditMode, onDraftChange, extraArgs, stdioArgs]);
|
||||
|
||||
// Poll for updates when runtime_info status is CONNECTING
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -595,126 +609,136 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
||||
<form
|
||||
id="mcp-form"
|
||||
onSubmit={form.handleSubmit(handleFormSubmit)}
|
||||
className="space-y-6"
|
||||
className="space-y-5"
|
||||
>
|
||||
{/* Runtime info: status + tools (edit mode only) */}
|
||||
{isEditMode && runtimeInfo && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">{t('mcp.title')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{(mcpTesting ||
|
||||
runtimeInfo.status !== MCPSessionStatus.CONNECTED) && (
|
||||
<div className="p-3 rounded-lg border">
|
||||
<StatusDisplay
|
||||
testing={mcpTesting}
|
||||
runtimeInfo={runtimeInfo}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<section className="space-y-3">
|
||||
<h3 className="text-sm font-medium">{t('mcp.title')}</h3>
|
||||
{(mcpTesting ||
|
||||
runtimeInfo.status !== MCPSessionStatus.CONNECTED) && (
|
||||
<div className="rounded-md bg-muted/40 p-3">
|
||||
<StatusDisplay
|
||||
testing={mcpTesting}
|
||||
runtimeInfo={runtimeInfo}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!mcpTesting &&
|
||||
runtimeInfo.status === MCPSessionStatus.CONNECTED &&
|
||||
runtimeInfo.tools?.length > 0 && (
|
||||
<>
|
||||
<div className="text-sm font-medium">
|
||||
{t('mcp.toolCount', {
|
||||
count: runtimeInfo.tools?.length || 0,
|
||||
})}
|
||||
</div>
|
||||
<ToolsList tools={runtimeInfo.tools} />
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{!mcpTesting &&
|
||||
runtimeInfo.status === MCPSessionStatus.CONNECTED &&
|
||||
runtimeInfo.tools?.length > 0 && (
|
||||
<>
|
||||
<div className="text-sm font-medium">
|
||||
{t('mcp.toolCount', {
|
||||
count: runtimeInfo.tools?.length || 0,
|
||||
})}
|
||||
</div>
|
||||
<ToolsList tools={runtimeInfo.tools} />
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Server configuration */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{isEditMode ? t('mcp.editServer') : t('mcp.createServer')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('mcp.extraParametersDescription')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('mcp.name')}
|
||||
<span className="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<section className="space-y-4">
|
||||
{isEditMode && (
|
||||
<h3 className="text-sm font-medium">{t('mcp.editServer')}</h3>
|
||||
)}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('mcp.name')}
|
||||
<span className="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={isEditMode} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('mcp.serverMode')}</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={isEditMode} />
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('mcp.selectMode')} />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SelectContent>
|
||||
<SelectItem value="http">{t('mcp.http')}</SelectItem>
|
||||
<SelectItem value="stdio">{t('mcp.stdio')}</SelectItem>
|
||||
<SelectItem value="sse">{t('mcp.sse')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mode"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('mcp.serverMode')}</FormLabel>
|
||||
<Select
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
>
|
||||
{(watchMode === 'sse' || watchMode === 'http') && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('mcp.url')}
|
||||
<span className="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t('mcp.selectMode')} />
|
||||
</SelectTrigger>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="http">{t('mcp.http')}</SelectItem>
|
||||
<SelectItem value="stdio">{t('mcp.stdio')}</SelectItem>
|
||||
<SelectItem value="sse">{t('mcp.sse')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{(watchMode === 'sse' || watchMode === 'http') && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="timeout"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('mcp.timeout')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={t('mcp.timeout')}
|
||||
{...field}
|
||||
onChange={(e) => field.onChange(Number(e.target.value))}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{watchMode === 'sse' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="url"
|
||||
name="ssereadtimeout"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('mcp.url')}
|
||||
<span className="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="timeout"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('mcp.timeout')}</FormLabel>
|
||||
<FormLabel>{t('mcp.sseTimeout')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={t('mcp.timeout')}
|
||||
placeholder={t('mcp.sseTimeoutDescription')}
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(Number(e.target.value))
|
||||
@@ -725,133 +749,104 @@ const MCPForm = forwardRef<MCPFormHandle, MCPFormProps>(function MCPForm(
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{watchMode === 'sse' && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ssereadtimeout"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('mcp.sseTimeout')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={t('mcp.sseTimeoutDescription')}
|
||||
{...field}
|
||||
onChange={(e) =>
|
||||
field.onChange(Number(e.target.value))
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
{watchMode === 'stdio' && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="command"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('mcp.command')}
|
||||
<span className="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
{watchMode === 'stdio' && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="command"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('mcp.command')}
|
||||
<span className="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormItem>
|
||||
<FormLabel>{t('mcp.args')}</FormLabel>
|
||||
<div className="space-y-2">
|
||||
{stdioArgs.map((arg, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
placeholder={t('mcp.args')}
|
||||
value={arg.value}
|
||||
onChange={(e) => updateStdioArg(index, e.target.value)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0 text-red-500 hover:text-red-600"
|
||||
onClick={() => removeStdioArg(index)}
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button type="button" variant="outline" onClick={addStdioArg}>
|
||||
{t('mcp.addArgument')}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
<FormItem>
|
||||
<FormLabel>{t('mcp.args')}</FormLabel>
|
||||
<div className="space-y-2">
|
||||
{stdioArgs.map((arg, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
placeholder={t('mcp.args')}
|
||||
value={arg.value}
|
||||
onChange={(e) =>
|
||||
updateStdioArg(index, e.target.value)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0 text-red-500 hover:text-red-600"
|
||||
onClick={() => removeStdioArg(index)}
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={addStdioArg}
|
||||
>
|
||||
{t('mcp.addArgument')}
|
||||
</Button>
|
||||
</div>
|
||||
</FormItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{watchMode === 'sse' || watchMode === 'http'
|
||||
? t('mcp.headers')
|
||||
: t('mcp.env')}
|
||||
</FormLabel>
|
||||
<div className="space-y-2">
|
||||
{extraArgs.map((arg, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
placeholder={t('models.keyName')}
|
||||
value={arg.key}
|
||||
onChange={(e) =>
|
||||
updateExtraArg(index, 'key', e.target.value)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t('models.value')}
|
||||
value={arg.value}
|
||||
onChange={(e) =>
|
||||
updateExtraArg(index, 'value', e.target.value)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0 text-red-500 hover:text-red-600"
|
||||
onClick={() => removeExtraArg(index)}
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button type="button" variant="outline" onClick={addExtraArg}>
|
||||
{watchMode === 'sse' || watchMode === 'http'
|
||||
? t('mcp.headers')
|
||||
: t('mcp.env')}
|
||||
</FormLabel>
|
||||
<div className="space-y-2">
|
||||
{extraArgs.map((arg, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
placeholder={t('models.keyName')}
|
||||
value={arg.key}
|
||||
onChange={(e) =>
|
||||
updateExtraArg(index, 'key', e.target.value)
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder={t('models.value')}
|
||||
value={arg.value}
|
||||
onChange={(e) =>
|
||||
updateExtraArg(index, 'value', e.target.value)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0 text-red-500 hover:text-red-600"
|
||||
onClick={() => removeExtraArg(index)}
|
||||
>
|
||||
<Trash2 className="w-5 h-5" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button type="button" variant="outline" onClick={addExtraArg}>
|
||||
{watchMode === 'sse' || watchMode === 'http'
|
||||
? t('mcp.addHeader')
|
||||
: t('mcp.addEnvVar')}
|
||||
</Button>
|
||||
</div>
|
||||
<FormDescription>
|
||||
{t('mcp.extraParametersDescription')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</CardContent>
|
||||
</Card>
|
||||
? t('mcp.addHeader')
|
||||
: t('mcp.addEnvVar')}
|
||||
</Button>
|
||||
</div>
|
||||
<FormDescription>
|
||||
{t('mcp.extraParametersDescription')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</section>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
||||
@@ -143,7 +143,7 @@ export default function SkillDetailContent({ id }: { id: string }) {
|
||||
</div>
|
||||
|
||||
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||||
<DialogContent>
|
||||
<DialogContent className="max-h-[min(420px,80vh)] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('common.confirmDelete')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -12,52 +12,72 @@ import { toast } from 'sonner';
|
||||
|
||||
interface SkillFormProps {
|
||||
initSkillName?: string;
|
||||
initialDraft?: SkillFormDraft;
|
||||
onNewSkillCreated: (skillName: string) => void;
|
||||
onSkillUpdated: (skillName: string) => void;
|
||||
onDraftChange?: (draft: SkillFormDraft) => void;
|
||||
}
|
||||
|
||||
export default function SkillForm({
|
||||
initSkillName,
|
||||
onNewSkillCreated,
|
||||
onSkillUpdated,
|
||||
}: SkillFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const [skill, setSkill] = useState<Partial<Skill>>({
|
||||
export interface SkillFormDraft {
|
||||
skill: Partial<Skill>;
|
||||
showAdvanced: boolean;
|
||||
}
|
||||
|
||||
const emptySkillDraft: SkillFormDraft = {
|
||||
skill: {
|
||||
name: '',
|
||||
display_name: '',
|
||||
description: '',
|
||||
instructions: '',
|
||||
package_root: '',
|
||||
auto_activate: true,
|
||||
});
|
||||
},
|
||||
showAdvanced: false,
|
||||
};
|
||||
|
||||
export default function SkillForm({
|
||||
initSkillName,
|
||||
initialDraft,
|
||||
onNewSkillCreated,
|
||||
onSkillUpdated,
|
||||
onDraftChange,
|
||||
}: SkillFormProps) {
|
||||
const { t } = useTranslation();
|
||||
const initialDraftRef = useRef(initialDraft ?? emptySkillDraft);
|
||||
const [skill, setSkill] = useState<Partial<Skill>>(
|
||||
initialDraftRef.current.skill,
|
||||
);
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [showAdvanced, setShowAdvanced] = useState(
|
||||
initialDraftRef.current.showAdvanced,
|
||||
);
|
||||
|
||||
const loadSkill = useCallback(
|
||||
async (skillName: string) => {
|
||||
try {
|
||||
const resp = await httpClient.getSkill(skillName);
|
||||
setSkill(resp.skill);
|
||||
} catch (error) {
|
||||
console.error('Failed to load skill:', error);
|
||||
toast.error(t('skills.getSkillListError') + String(error));
|
||||
}
|
||||
},
|
||||
[t],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (initSkillName) {
|
||||
loadSkill(initSkillName);
|
||||
return;
|
||||
}
|
||||
setSkill({
|
||||
name: '',
|
||||
display_name: '',
|
||||
description: '',
|
||||
instructions: '',
|
||||
package_root: '',
|
||||
auto_activate: true,
|
||||
});
|
||||
setShowAdvanced(false);
|
||||
}, [initSkillName]);
|
||||
setSkill(initialDraftRef.current.skill);
|
||||
setShowAdvanced(initialDraftRef.current.showAdvanced);
|
||||
}, [initSkillName, loadSkill]);
|
||||
|
||||
async function loadSkill(skillName: string) {
|
||||
try {
|
||||
const resp = await httpClient.getSkill(skillName);
|
||||
setSkill(resp.skill);
|
||||
} catch (error) {
|
||||
console.error('Failed to load skill:', error);
|
||||
toast.error(t('skills.getSkillListError') + String(error));
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (initSkillName) return;
|
||||
onDraftChange?.({ skill, showAdvanced });
|
||||
}, [initSkillName, onDraftChange, skill, showAdvanced]);
|
||||
|
||||
async function scanDirectory() {
|
||||
const path = skill.package_root?.trim();
|
||||
@@ -183,10 +203,16 @@ export default function SkillForm({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="auto_activate">{t('skills.autoActivate')}</Label>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="auto_activate">{t('skills.autoActivate')}</Label>
|
||||
<p className="text-xs leading-relaxed text-muted-foreground">
|
||||
{t('skills.autoActivateDescription')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="auto_activate"
|
||||
className="mt-0.5"
|
||||
checked={skill.auto_activate ?? true}
|
||||
onCheckedChange={(checked) =>
|
||||
setSkill({ ...skill, auto_activate: checked })
|
||||
@@ -194,10 +220,10 @@ export default function SkillForm({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-md">
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center justify-between w-full p-3 text-sm font-medium text-left"
|
||||
className="flex w-full items-center justify-between rounded-md bg-muted/40 px-3 py-2 text-left text-sm font-medium hover:bg-muted/70"
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
>
|
||||
{t('skills.advancedSettings')}
|
||||
@@ -208,7 +234,7 @@ export default function SkillForm({
|
||||
)}
|
||||
</button>
|
||||
{showAdvanced && (
|
||||
<div className="p-3 pt-0 space-y-4">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('skills.packageRoot')}</Label>
|
||||
<div className="flex gap-2">
|
||||
|
||||
@@ -1347,6 +1347,8 @@ const enUS = {
|
||||
skillDescription: 'Skill Description',
|
||||
skillInstructions: 'Instructions',
|
||||
autoActivate: 'Auto Activate',
|
||||
autoActivateDescription:
|
||||
'When enabled, the Agent may match and activate this skill based on its description during conversations.',
|
||||
saveSuccess: 'Saved successfully',
|
||||
saveError: 'Save failed: ',
|
||||
createSuccess: 'Created successfully',
|
||||
@@ -1475,6 +1477,7 @@ const enUS = {
|
||||
uploadExtension: 'Drag & drop or click to upload',
|
||||
uploadHint: 'Supports .zip (skills) and .lbpkg (plugins) files',
|
||||
orContinueWith: 'or choose an action below',
|
||||
addMCPServerHint: 'Connect an MCP tool server extension',
|
||||
installFromGithub: 'Install Plugin from GitHub',
|
||||
installFromGithubHint: 'Install plugin extension from GitHub Release',
|
||||
createSkill: 'Create New Skill',
|
||||
|
||||
@@ -1392,6 +1392,7 @@ const jaJP = {
|
||||
uploadExtension: 'ドラッグ&ドロップまたはクリックしてアップロード',
|
||||
uploadHint: '.zip(スキル)と.lbpkg(プラグイン)ファイルに対応',
|
||||
orContinueWith: 'または以下の操作を選択',
|
||||
addMCPServerHint: 'MCPツールサーバー拡張を接続',
|
||||
installFromGithub: 'GitHubからプラグインをインストール',
|
||||
installFromGithubHint: 'GitHub Releaseからプラグイン拡張をインストール',
|
||||
createSkill: '新しいスキルを作成',
|
||||
|
||||
@@ -1291,6 +1291,8 @@ const zhHans = {
|
||||
skillDescription: '技能描述',
|
||||
skillInstructions: '指令内容',
|
||||
autoActivate: '自动激活',
|
||||
autoActivateDescription:
|
||||
'开启后,Agent 会在对话中根据技能描述自动匹配并激活此技能。',
|
||||
saveSuccess: '保存成功',
|
||||
saveError: '保存失败:',
|
||||
createSuccess: '创建成功',
|
||||
@@ -1414,6 +1416,7 @@ const zhHans = {
|
||||
uploadExtension: '拖拽或点击上传扩展包',
|
||||
uploadHint: '支持 .zip(技能)和 .lbpkg(插件)文件',
|
||||
orContinueWith: '或选择以下操作',
|
||||
addMCPServerHint: '连接一个 MCP 工具服务器扩展',
|
||||
installFromGithub: '从 GitHub 安装插件',
|
||||
installFromGithubHint: '从 GitHub Release 安装插件扩展',
|
||||
createSkill: '创建新的技能',
|
||||
|
||||
@@ -1327,6 +1327,7 @@ const zhHant = {
|
||||
uploadExtension: '拖拽或點擊上傳擴充套件',
|
||||
uploadHint: '支援 .zip(技能)和 .lbpkg(插件)檔案',
|
||||
orContinueWith: '或選擇以下操作',
|
||||
addMCPServerHint: '連接一個 MCP 工具伺服器擴充',
|
||||
installFromGithub: '從 GitHub 安裝插件',
|
||||
installFromGithubHint: '從 GitHub Release 安裝插件擴充',
|
||||
createSkill: '建立新的技能',
|
||||
@@ -1360,6 +1361,8 @@ const zhHant = {
|
||||
skillDescription: '技能描述',
|
||||
skillInstructions: '指令內容',
|
||||
autoActivate: '自動啟用',
|
||||
autoActivateDescription:
|
||||
'開啟後,Agent 會在對話中根據技能描述自動匹配並啟用此技能。',
|
||||
saveSuccess: '儲存成功',
|
||||
saveError: '儲存失敗:',
|
||||
createSuccess: '創建成功',
|
||||
|
||||
Reference in New Issue
Block a user