Add custom agent creator, Zread MCP, fix model change context continuity
Features added: - Custom Agent Creator dialog with AI generation support (up to 30k chars) - Plus button next to agent selector to create new agents - Zread MCP Server from Z.AI in marketplace (remote HTTP config) - Extended MCP config types to support remote/http/sse servers Bug fixes: - Filter SDK Z.AI/GLM providers to ensure our custom routing with full message history - This fixes the issue where changing models mid-chat lost conversationcontext
This commit is contained in:
327
packages/ui/src/components/agent-creator-dialog.tsx
Normal file
327
packages/ui/src/components/agent-creator-dialog.tsx
Normal file
@@ -0,0 +1,327 @@
|
||||
import { Dialog } from "@kobalte/core/dialog"
|
||||
import { Bot, Loader2, Sparkles, X } from "lucide-solid"
|
||||
import { Component, Show, createSignal } from "solid-js"
|
||||
import { Portal } from "solid-js/web"
|
||||
import { updateInstanceConfig } from "../stores/instance-config"
|
||||
import { fetchAgents } from "../stores/sessions"
|
||||
import { showToastNotification } from "../lib/notifications"
|
||||
import { getLogger } from "../lib/logger"
|
||||
|
||||
const log = getLogger("agent-creator")
|
||||
|
||||
const MAX_PROMPT_LENGTH = 30000
|
||||
|
||||
interface AgentCreatorDialogProps {
|
||||
instanceId: string
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const AgentCreatorDialog: Component<AgentCreatorDialogProps> = (props) => {
|
||||
const [name, setName] = createSignal("")
|
||||
const [description, setDescription] = createSignal("")
|
||||
const [prompt, setPrompt] = createSignal("")
|
||||
const [isGenerating, setIsGenerating] = createSignal(false)
|
||||
const [isSaving, setIsSaving] = createSignal(false)
|
||||
const [useAiGeneration, setUseAiGeneration] = createSignal(true)
|
||||
|
||||
const resetForm = () => {
|
||||
setName("")
|
||||
setDescription("")
|
||||
setPrompt("")
|
||||
setIsGenerating(false)
|
||||
setUseAiGeneration(true)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
resetForm()
|
||||
props.onClose()
|
||||
}
|
||||
|
||||
const generatePromptWithAI = async () => {
|
||||
if (!name().trim() || !description().trim()) {
|
||||
showToastNotification({
|
||||
title: "Missing Information",
|
||||
message: "Please provide both name and description to generate an agent prompt.",
|
||||
variant: "warning",
|
||||
duration: 5000,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setIsGenerating(true)
|
||||
try {
|
||||
// Use Z.AI or another endpoint to generate the prompt
|
||||
const response = await fetch("/api/zai/chat", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
model: "glm-4.5-flash",
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content: `You are an expert AI agent prompt designer. Generate a comprehensive, detailed system prompt for an AI coding assistant agent based on the user's requirements. The prompt should:
|
||||
1. Define the agent's role and expertise
|
||||
2. Specify its capabilities and limitations
|
||||
3. Include guidelines for code style and best practices
|
||||
4. Define how it should interact with users
|
||||
5. Include any domain-specific knowledge relevant to the description
|
||||
|
||||
Output ONLY the agent system prompt, no explanations or markdown formatting.`,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: `Create a system prompt for an AI coding agent with the following details:
|
||||
|
||||
Name: ${name()}
|
||||
Purpose: ${description()}
|
||||
|
||||
Generate a comprehensive system prompt that will make this agent effective at its purpose.`,
|
||||
},
|
||||
],
|
||||
stream: false,
|
||||
max_tokens: 4096,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Generation failed: ${response.status}`)
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const generatedPrompt = data?.choices?.[0]?.message?.content || data?.message?.content || ""
|
||||
|
||||
if (generatedPrompt) {
|
||||
setPrompt(generatedPrompt)
|
||||
showToastNotification({
|
||||
title: "Prompt Generated",
|
||||
message: "AI has generated a system prompt for your agent. Review and edit as needed.",
|
||||
variant: "success",
|
||||
duration: 5000,
|
||||
})
|
||||
} else {
|
||||
throw new Error("No prompt content in response")
|
||||
}
|
||||
} catch (error) {
|
||||
log.error("Failed to generate agent prompt", error)
|
||||
showToastNotification({
|
||||
title: "Generation Failed",
|
||||
message: "Could not generate prompt. Please write one manually or check your Z.AI configuration.",
|
||||
variant: "error",
|
||||
duration: 8000,
|
||||
})
|
||||
} finally {
|
||||
setIsGenerating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!name().trim()) {
|
||||
showToastNotification({
|
||||
title: "Name Required",
|
||||
message: "Please provide a name for the agent.",
|
||||
variant: "warning",
|
||||
duration: 5000,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!prompt().trim()) {
|
||||
showToastNotification({
|
||||
title: "Prompt Required",
|
||||
message: "Please provide a system prompt for the agent.",
|
||||
variant: "warning",
|
||||
duration: 5000,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setIsSaving(true)
|
||||
try {
|
||||
await updateInstanceConfig(props.instanceId, (draft) => {
|
||||
if (!draft.customAgents) {
|
||||
draft.customAgents = []
|
||||
}
|
||||
// Check for duplicate names
|
||||
const existing = draft.customAgents.findIndex((a) => a.name.toLowerCase() === name().toLowerCase())
|
||||
if (existing >= 0) {
|
||||
// Update existing
|
||||
draft.customAgents[existing] = {
|
||||
name: name().trim(),
|
||||
description: description().trim() || undefined,
|
||||
prompt: prompt().trim(),
|
||||
}
|
||||
} else {
|
||||
// Add new
|
||||
draft.customAgents.push({
|
||||
name: name().trim(),
|
||||
description: description().trim() || undefined,
|
||||
prompt: prompt().trim(),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Refresh agents list
|
||||
await fetchAgents(props.instanceId)
|
||||
|
||||
showToastNotification({
|
||||
title: "Agent Created",
|
||||
message: `Custom agent "${name()}" has been saved and is ready to use.`,
|
||||
variant: "success",
|
||||
duration: 5000,
|
||||
})
|
||||
|
||||
handleClose()
|
||||
} catch (error) {
|
||||
log.error("Failed to save custom agent", error)
|
||||
showToastNotification({
|
||||
title: "Save Failed",
|
||||
message: "Could not save the agent. Please try again.",
|
||||
variant: "error",
|
||||
duration: 8000,
|
||||
})
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={props.open} onOpenChange={(open) => !open && handleClose()}>
|
||||
<Portal>
|
||||
<Dialog.Overlay class="fixed inset-0 bg-black/60 backdrop-blur-sm z-[9998]" />
|
||||
<div class="fixed inset-0 flex items-center justify-center z-[9999] p-4">
|
||||
<Dialog.Content class="bg-zinc-900 border border-zinc-700 rounded-2xl shadow-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||
{/* Header */}
|
||||
<div class="flex items-center justify-between p-4 border-b border-zinc-700/50">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="p-2 bg-indigo-500/20 rounded-lg">
|
||||
<Bot size={20} class="text-indigo-400" />
|
||||
</div>
|
||||
<div>
|
||||
<Dialog.Title class="text-lg font-semibold text-white">Create Custom Agent</Dialog.Title>
|
||||
<Dialog.Description class="text-xs text-zinc-400">
|
||||
Define a new AI agent with custom behavior and expertise
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
class="p-1.5 text-zinc-400 hover:text-white hover:bg-zinc-700/50 rounded-lg transition-colors"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{/* Name Input */}
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium text-zinc-300">Agent Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={name()}
|
||||
onInput={(e) => setName(e.currentTarget.value)}
|
||||
placeholder="e.g., React Specialist, Python Expert, Code Reviewer..."
|
||||
class="w-full px-3 py-2 bg-zinc-800 border border-zinc-600 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:border-indigo-500 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description Input */}
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium text-zinc-300">Brief Description</label>
|
||||
<input
|
||||
type="text"
|
||||
value={description()}
|
||||
onInput={(e) => setDescription(e.currentTarget.value)}
|
||||
placeholder="A few words about what this agent specializes in..."
|
||||
class="w-full px-3 py-2 bg-zinc-800 border border-zinc-600 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:border-indigo-500 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Generation Mode Toggle */}
|
||||
<div class="flex items-center gap-4 p-3 bg-zinc-800/50 rounded-lg border border-zinc-700/50">
|
||||
<button
|
||||
onClick={() => setUseAiGeneration(true)}
|
||||
class={`flex-1 py-2 px-3 rounded-lg text-sm font-medium transition-all ${useAiGeneration()
|
||||
? "bg-indigo-500 text-white"
|
||||
: "text-zinc-400 hover:text-white hover:bg-zinc-700/50"
|
||||
}`}
|
||||
>
|
||||
<Sparkles size={14} class="inline-block mr-1.5" />
|
||||
AI Generate
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setUseAiGeneration(false)}
|
||||
class={`flex-1 py-2 px-3 rounded-lg text-sm font-medium transition-all ${!useAiGeneration()
|
||||
? "bg-indigo-500 text-white"
|
||||
: "text-zinc-400 hover:text-white hover:bg-zinc-700/50"
|
||||
}`}
|
||||
>
|
||||
Write Manually
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* AI Generation Button */}
|
||||
<Show when={useAiGeneration()}>
|
||||
<button
|
||||
onClick={generatePromptWithAI}
|
||||
disabled={isGenerating() || !name().trim() || !description().trim()}
|
||||
class="w-full py-2.5 px-4 bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 text-white rounded-lg font-medium text-sm transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||||
>
|
||||
<Show when={isGenerating()} fallback={<><Sparkles size={16} /> Generate Agent Prompt with AI</>}>
|
||||
<Loader2 size={16} class="animate-spin" />
|
||||
Generating...
|
||||
</Show>
|
||||
</button>
|
||||
</Show>
|
||||
|
||||
{/* Prompt Textarea */}
|
||||
<div class="space-y-1.5">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-medium text-zinc-300">System Prompt *</label>
|
||||
<span class="text-xs text-zinc-500">
|
||||
{prompt().length.toLocaleString()} / {MAX_PROMPT_LENGTH.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
<textarea
|
||||
value={prompt()}
|
||||
onInput={(e) => {
|
||||
const value = e.currentTarget.value
|
||||
if (value.length <= MAX_PROMPT_LENGTH) {
|
||||
setPrompt(value)
|
||||
}
|
||||
}}
|
||||
placeholder="Enter the system prompt that defines this agent's behavior, expertise, and guidelines..."
|
||||
rows={12}
|
||||
class="w-full px-3 py-2 bg-zinc-800 border border-zinc-600 rounded-lg text-white placeholder:text-zinc-500 focus:outline-none focus:border-indigo-500 transition-colors resize-none font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div class="flex items-center justify-end gap-3 p-4 border-t border-zinc-700/50 bg-zinc-800/30">
|
||||
<button
|
||||
onClick={handleClose}
|
||||
class="px-4 py-2 text-zinc-400 hover:text-white hover:bg-zinc-700/50 rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving() || !name().trim() || !prompt().trim()}
|
||||
class="px-4 py-2 bg-indigo-500 hover:bg-indigo-400 text-white rounded-lg text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
<Show when={isSaving()} fallback={<>Save Agent</>}>
|
||||
<Loader2 size={14} class="animate-spin" />
|
||||
Saving...
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Portal>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default AgentCreatorDialog
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Select } from "@kobalte/core/select"
|
||||
import { For, Show, createEffect, createMemo } from "solid-js"
|
||||
import { For, Show, createEffect, createMemo, createSignal } from "solid-js"
|
||||
import { agents, fetchAgents, sessions } from "../stores/sessions"
|
||||
import { ChevronDown } from "lucide-solid"
|
||||
import { ChevronDown, Plus } from "lucide-solid"
|
||||
import type { Agent } from "../types/session"
|
||||
import { getLogger } from "../lib/logger"
|
||||
import AgentCreatorDialog from "./agent-creator-dialog"
|
||||
|
||||
const log = getLogger("session")
|
||||
|
||||
|
||||
@@ -16,6 +18,7 @@ interface AgentSelectorProps {
|
||||
|
||||
export default function AgentSelector(props: AgentSelectorProps) {
|
||||
const instanceAgents = () => agents().get(props.instanceId) || []
|
||||
const [showCreator, setShowCreator] = createSignal(false)
|
||||
|
||||
const session = createMemo(() => {
|
||||
const instanceSessions = sessions().get(props.instanceId)
|
||||
@@ -64,61 +67,82 @@ export default function AgentSelector(props: AgentSelectorProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="sidebar-selector">
|
||||
<Select
|
||||
value={availableAgents().find((a) => a.name === props.currentAgent)}
|
||||
onChange={handleChange}
|
||||
options={availableAgents()}
|
||||
optionValue="name"
|
||||
optionTextValue="name"
|
||||
placeholder="Select agent..."
|
||||
itemComponent={(itemProps) => (
|
||||
<Select.Item
|
||||
item={itemProps.item}
|
||||
class="selector-option"
|
||||
>
|
||||
<div class="flex flex-col flex-1 min-w-0">
|
||||
<Select.ItemLabel class="selector-option-label flex items-center gap-2">
|
||||
<span>{itemProps.item.rawValue.name}</span>
|
||||
<Show when={itemProps.item.rawValue.mode === "subagent"}>
|
||||
<span class="neutral-badge">subagent</span>
|
||||
<>
|
||||
<div class="sidebar-selector flex items-center gap-1">
|
||||
<Select
|
||||
value={availableAgents().find((a) => a.name === props.currentAgent)}
|
||||
onChange={handleChange}
|
||||
options={availableAgents()}
|
||||
optionValue="name"
|
||||
optionTextValue="name"
|
||||
placeholder="Select agent..."
|
||||
itemComponent={(itemProps) => (
|
||||
<Select.Item
|
||||
item={itemProps.item}
|
||||
class="selector-option"
|
||||
>
|
||||
<div class="flex flex-col flex-1 min-w-0">
|
||||
<Select.ItemLabel class="selector-option-label flex items-center gap-2">
|
||||
<span>{itemProps.item.rawValue.name}</span>
|
||||
<Show when={itemProps.item.rawValue.mode === "subagent"}>
|
||||
<span class="neutral-badge">subagent</span>
|
||||
</Show>
|
||||
<Show when={itemProps.item.rawValue.mode === "custom"}>
|
||||
<span class="text-[9px] px-1.5 py-0.5 bg-indigo-500/20 text-indigo-400 rounded-full font-medium">custom</span>
|
||||
</Show>
|
||||
</Select.ItemLabel>
|
||||
<Show when={itemProps.item.rawValue.description}>
|
||||
<Select.ItemDescription class="selector-option-description">
|
||||
{itemProps.item.rawValue.description.length > 50
|
||||
? itemProps.item.rawValue.description.slice(0, 50) + "..."
|
||||
: itemProps.item.rawValue.description}
|
||||
</Select.ItemDescription>
|
||||
</Show>
|
||||
</Select.ItemLabel>
|
||||
<Show when={itemProps.item.rawValue.description}>
|
||||
<Select.ItemDescription class="selector-option-description">
|
||||
{itemProps.item.rawValue.description.length > 50
|
||||
? itemProps.item.rawValue.description.slice(0, 50) + "..."
|
||||
: itemProps.item.rawValue.description}
|
||||
</Select.ItemDescription>
|
||||
</Show>
|
||||
</div>
|
||||
</Select.Item>
|
||||
)}
|
||||
>
|
||||
<Select.Trigger
|
||||
data-agent-selector
|
||||
class="selector-trigger"
|
||||
>
|
||||
<Select.Value<Agent>>
|
||||
{(state) => (
|
||||
<div class="selector-trigger-label">
|
||||
<span class="selector-trigger-primary">
|
||||
Agent: {state.selectedOption()?.name ?? "None"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Select.Value>
|
||||
<Select.Icon class="selector-trigger-icon">
|
||||
<ChevronDown class="w-3 h-3" />
|
||||
</Select.Icon>
|
||||
</Select.Trigger>
|
||||
</Select.Item>
|
||||
)}
|
||||
>
|
||||
<Select.Trigger
|
||||
data-agent-selector
|
||||
class="selector-trigger"
|
||||
>
|
||||
<Select.Value<Agent>>
|
||||
{(state) => (
|
||||
<div class="selector-trigger-label">
|
||||
<span class="selector-trigger-primary">
|
||||
Agent: {state.selectedOption()?.name ?? "None"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Select.Value>
|
||||
<Select.Icon class="selector-trigger-icon">
|
||||
<ChevronDown class="w-3 h-3" />
|
||||
</Select.Icon>
|
||||
</Select.Trigger>
|
||||
|
||||
<Select.Portal>
|
||||
<Select.Content class="selector-popover max-h-80 overflow-auto p-1">
|
||||
<Select.Listbox class="selector-listbox" />
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select>
|
||||
</div>
|
||||
<Select.Portal>
|
||||
<Select.Content class="selector-popover max-h-80 overflow-auto p-1">
|
||||
<Select.Listbox class="selector-listbox" />
|
||||
</Select.Content>
|
||||
</Select.Portal>
|
||||
</Select>
|
||||
|
||||
{/* Add Agent Button */}
|
||||
<button
|
||||
onClick={() => setShowCreator(true)}
|
||||
class="p-1.5 text-zinc-500 hover:text-indigo-400 hover:bg-indigo-500/10 rounded-lg transition-all shrink-0"
|
||||
title="Create custom agent"
|
||||
>
|
||||
<Plus size={14} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Agent Creator Dialog */}
|
||||
<AgentCreatorDialog
|
||||
instanceId={props.instanceId}
|
||||
open={showCreator()}
|
||||
onClose={() => setShowCreator(false)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ type McpServerConfig = {
|
||||
command?: string
|
||||
args?: string[]
|
||||
env?: Record<string, string>
|
||||
// Remote MCP server support
|
||||
type?: "remote" | "http" | "sse" | "streamable-http"
|
||||
url?: string
|
||||
headers?: Record<string, string>
|
||||
}
|
||||
|
||||
type McpConfig = {
|
||||
@@ -23,8 +27,10 @@ type McpMarketplaceEntry = {
|
||||
config: McpServerConfig
|
||||
tags?: string[]
|
||||
source?: string
|
||||
requiresApiKey?: boolean
|
||||
}
|
||||
|
||||
|
||||
interface McpManagerProps {
|
||||
instanceId: string
|
||||
}
|
||||
@@ -34,6 +40,19 @@ const log = getLogger("mcp-manager")
|
||||
const MCP_LINKER_RELEASES = "https://github.com/milisp/mcp-linker/releases"
|
||||
const MCP_LINKER_MARKET = "https://github.com/milisp/mcp-linker"
|
||||
const MARKETPLACE_ENTRIES: McpMarketplaceEntry[] = [
|
||||
{
|
||||
id: "zread",
|
||||
name: "Zread (Z.AI)",
|
||||
description: "Search GitHub repos, read code, analyze structure. Powered by Z.AI - requires API key from z.ai/manage-apikey.",
|
||||
config: {
|
||||
type: "remote",
|
||||
url: "https://api.z.ai/api/mcp/zread/mcp",
|
||||
headers: { "Authorization": "Bearer YOUR_ZAI_API_KEY" }
|
||||
},
|
||||
tags: ["github", "code", "search", "z.ai"],
|
||||
source: "z.ai",
|
||||
requiresApiKey: true,
|
||||
},
|
||||
{
|
||||
id: "sequential-thinking",
|
||||
name: "Sequential Thinking",
|
||||
@@ -76,6 +95,7 @@ const MARKETPLACE_ENTRIES: McpMarketplaceEntry[] = [
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
const McpManager: Component<McpManagerProps> = (props) => {
|
||||
const [config, setConfig] = createSignal<McpConfig>({ mcpServers: {} })
|
||||
const [isLoading, setIsLoading] = createSignal(false)
|
||||
@@ -348,61 +368,61 @@ const McpManager: Component<McpManagerProps> = (props) => {
|
||||
<Dialog.Overlay class="modal-overlay" />
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<Dialog.Content class="modal-surface w-full max-w-2xl p-5 flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Dialog.Title class="text-sm font-semibold text-white">Configure MCP Server</Dialog.Title>
|
||||
<Dialog.Description class="text-xs text-zinc-500">
|
||||
Paste the MCP server config JSON. Use marketplace via MCP Linker for curated servers.
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
<button
|
||||
class="text-xs px-2 py-1 rounded border border-white/10 text-zinc-400 hover:text-white"
|
||||
onClick={() => setRawMode((prev) => !prev)}
|
||||
>
|
||||
{rawMode() ? "Server Mode" : "Raw Config (JSON)"}
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Dialog.Title class="text-sm font-semibold text-white">Configure MCP Server</Dialog.Title>
|
||||
<Dialog.Description class="text-xs text-zinc-500">
|
||||
Paste the MCP server config JSON. Use marketplace via MCP Linker for curated servers.
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
<button
|
||||
class="text-xs px-2 py-1 rounded border border-white/10 text-zinc-400 hover:text-white"
|
||||
onClick={() => setRawMode((prev) => !prev)}
|
||||
>
|
||||
{rawMode() ? "Server Mode" : "Raw Config (JSON)"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Show when={!rawMode()}>
|
||||
<label class="flex flex-col gap-1 text-xs text-zinc-400">
|
||||
Server Name
|
||||
<input
|
||||
value={serverName()}
|
||||
onInput={(e) => setServerName(e.currentTarget.value)}
|
||||
class="rounded-md bg-white/5 border border-white/10 px-3 py-2 text-xs text-zinc-200 focus:outline-none focus:border-blue-500/60"
|
||||
placeholder="example-server"
|
||||
/>
|
||||
</label>
|
||||
</Show>
|
||||
<Show when={!rawMode()}>
|
||||
<label class="flex flex-col gap-1 text-xs text-zinc-400">
|
||||
Server Name
|
||||
<input
|
||||
value={serverName()}
|
||||
onInput={(e) => setServerName(e.currentTarget.value)}
|
||||
class="rounded-md bg-white/5 border border-white/10 px-3 py-2 text-xs text-zinc-200 focus:outline-none focus:border-blue-500/60"
|
||||
placeholder="example-server"
|
||||
/>
|
||||
</label>
|
||||
</Show>
|
||||
|
||||
<label class="flex flex-col gap-1 text-xs text-zinc-400">
|
||||
Config JSON
|
||||
<textarea
|
||||
value={serverJson()}
|
||||
onInput={(e) => setServerJson(e.currentTarget.value)}
|
||||
class="min-h-[200px] rounded-md bg-white/5 border border-white/10 px-3 py-2 text-xs text-zinc-200 font-mono focus:outline-none focus:border-blue-500/60"
|
||||
placeholder='{"command":"npx","args":["-y","mcp-server-example"]}'
|
||||
/>
|
||||
</label>
|
||||
<label class="flex flex-col gap-1 text-xs text-zinc-400">
|
||||
Config JSON
|
||||
<textarea
|
||||
value={serverJson()}
|
||||
onInput={(e) => setServerJson(e.currentTarget.value)}
|
||||
class="min-h-[200px] rounded-md bg-white/5 border border-white/10 px-3 py-2 text-xs text-zinc-200 font-mono focus:outline-none focus:border-blue-500/60"
|
||||
placeholder='{"command":"npx","args":["-y","mcp-server-example"]}'
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
resetManualForm()
|
||||
setShowManual(false)
|
||||
}}
|
||||
class="px-3 py-1.5 text-xs rounded-md border border-white/10 text-zinc-300 hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleManualSave}
|
||||
disabled={saving()}
|
||||
class="px-3 py-1.5 text-xs rounded-md bg-blue-500/20 border border-blue-500/40 text-blue-200 hover:text-white disabled:opacity-60"
|
||||
>
|
||||
{saving() ? "Saving..." : "Confirm"}
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
resetManualForm()
|
||||
setShowManual(false)
|
||||
}}
|
||||
class="px-3 py-1.5 text-xs rounded-md border border-white/10 text-zinc-300 hover:text-white"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleManualSave}
|
||||
disabled={saving()}
|
||||
class="px-3 py-1.5 text-xs rounded-md bg-blue-500/20 border border-blue-500/40 text-blue-200 hover:text-white disabled:opacity-60"
|
||||
>
|
||||
{saving() ? "Saving..." : "Confirm"}
|
||||
</button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
@@ -413,83 +433,83 @@ const McpManager: Component<McpManagerProps> = (props) => {
|
||||
<Dialog.Overlay class="modal-overlay" />
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||
<Dialog.Content class="modal-surface w-full max-w-3xl p-5 flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Dialog.Title class="text-sm font-semibold text-white">MCP Marketplace</Dialog.Title>
|
||||
<Dialog.Description class="text-xs text-zinc-500">
|
||||
Curated entries inspired by mcp-linker. Install writes to this workspace's .mcp.json.
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
<button
|
||||
class="mcp-link-button"
|
||||
onClick={() => openExternal(MCP_LINKER_MARKET)}
|
||||
>
|
||||
Open MCP Linker
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Dialog.Title class="text-sm font-semibold text-white">MCP Marketplace</Dialog.Title>
|
||||
<Dialog.Description class="text-xs text-zinc-500">
|
||||
Curated entries inspired by mcp-linker. Install writes to this workspace's .mcp.json.
|
||||
</Dialog.Description>
|
||||
</div>
|
||||
<button
|
||||
class="mcp-link-button"
|
||||
onClick={() => openExternal(MCP_LINKER_MARKET)}
|
||||
>
|
||||
Open MCP Linker
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mcp-market-search">
|
||||
<Search size={14} class="text-zinc-500" />
|
||||
<input
|
||||
value={marketplaceQuery()}
|
||||
onInput={(e) => setMarketplaceQuery(e.currentTarget.value)}
|
||||
placeholder="Search MCP servers..."
|
||||
class="mcp-market-input"
|
||||
/>
|
||||
</div>
|
||||
<div class="mcp-market-search">
|
||||
<Search size={14} class="text-zinc-500" />
|
||||
<input
|
||||
value={marketplaceQuery()}
|
||||
onInput={(e) => setMarketplaceQuery(e.currentTarget.value)}
|
||||
placeholder="Search MCP servers..."
|
||||
class="mcp-market-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mcp-market-list">
|
||||
<Show
|
||||
when={!marketplaceLoading()}
|
||||
fallback={<div class="text-[11px] text-zinc-500 italic">Loading marketplace sources...</div>}
|
||||
>
|
||||
<For each={filteredMarketplace()}>
|
||||
{(entry) => (
|
||||
<div class="mcp-market-card">
|
||||
<div class="mcp-market-card-info">
|
||||
<div class="mcp-market-card-title">
|
||||
{entry.name}
|
||||
<Show when={entry.source}>
|
||||
{(source) => <span class="mcp-market-source">{source()}</span>}
|
||||
</Show>
|
||||
</div>
|
||||
<div class="mcp-market-card-desc">{entry.description}</div>
|
||||
<Show when={entry.tags && entry.tags.length > 0}>
|
||||
<div class="mcp-market-tags">
|
||||
<For each={entry.tags}>
|
||||
{(tag) => <span class="mcp-market-tag">{tag}</span>}
|
||||
</For>
|
||||
<div class="mcp-market-list">
|
||||
<Show
|
||||
when={!marketplaceLoading()}
|
||||
fallback={<div class="text-[11px] text-zinc-500 italic">Loading marketplace sources...</div>}
|
||||
>
|
||||
<For each={filteredMarketplace()}>
|
||||
{(entry) => (
|
||||
<div class="mcp-market-card">
|
||||
<div class="mcp-market-card-info">
|
||||
<div class="mcp-market-card-title">
|
||||
{entry.name}
|
||||
<Show when={entry.source}>
|
||||
{(source) => <span class="mcp-market-source">{source()}</span>}
|
||||
</Show>
|
||||
</div>
|
||||
<div class="mcp-market-card-desc">{entry.description}</div>
|
||||
<Show when={entry.tags && entry.tags.length > 0}>
|
||||
<div class="mcp-market-tags">
|
||||
<For each={entry.tags}>
|
||||
{(tag) => <span class="mcp-market-tag">{tag}</span>}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="mcp-market-card-actions">
|
||||
<button
|
||||
class="mcp-icon-button"
|
||||
title="View config"
|
||||
onClick={() => {
|
||||
setShowManual(true)
|
||||
setRawMode(false)
|
||||
setServerName(entry.id)
|
||||
setServerJson(JSON.stringify(entry.config, null, 2))
|
||||
setShowMarketplace(false)
|
||||
}}
|
||||
>
|
||||
<Settings size={14} />
|
||||
</button>
|
||||
<button
|
||||
class="mcp-market-install"
|
||||
onClick={() => handleMarketplaceInstall(entry)}
|
||||
disabled={saving()}
|
||||
>
|
||||
<Plus size={12} />
|
||||
Install
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="mcp-market-card-actions">
|
||||
<button
|
||||
class="mcp-icon-button"
|
||||
title="View config"
|
||||
onClick={() => {
|
||||
setShowManual(true)
|
||||
setRawMode(false)
|
||||
setServerName(entry.id)
|
||||
setServerJson(JSON.stringify(entry.config, null, 2))
|
||||
setShowMarketplace(false)
|
||||
}}
|
||||
>
|
||||
<Settings size={14} />
|
||||
</button>
|
||||
<button
|
||||
class="mcp-market-install"
|
||||
onClick={() => handleMarketplaceInstall(entry)}
|
||||
disabled={saving()}
|
||||
>
|
||||
<Plus size={12} />
|
||||
Install
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
|
||||
@@ -746,7 +746,12 @@ async function fetchProviders(instanceId: string): Promise<void> {
|
||||
})),
|
||||
}))
|
||||
|
||||
const filteredBaseProviders = providerList.filter((provider) => provider.id !== "zai")
|
||||
// Filter out Z.AI providers from SDK to use our custom routing with full message history
|
||||
const filteredBaseProviders = providerList.filter((provider) =>
|
||||
!provider.id.toLowerCase().includes("zai") &&
|
||||
!provider.id.toLowerCase().includes("z.ai") &&
|
||||
!provider.id.toLowerCase().includes("glm")
|
||||
)
|
||||
|
||||
const extraProviders = await fetchExtraProviders()
|
||||
const baseProviders = removeDuplicateProviders(filteredBaseProviders, extraProviders)
|
||||
|
||||
Reference in New Issue
Block a user