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:
Gemini AI
2025-12-24 21:34:16 +04:00
Unverified
parent e8c38b0add
commit 3501c20471
4 changed files with 558 additions and 182 deletions

View 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

View File

@@ -1,9 +1,11 @@
import { Select } from "@kobalte/core/select" 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 { agents, fetchAgents, sessions } from "../stores/sessions"
import { ChevronDown } from "lucide-solid" import { ChevronDown, Plus } from "lucide-solid"
import type { Agent } from "../types/session" import type { Agent } from "../types/session"
import { getLogger } from "../lib/logger" import { getLogger } from "../lib/logger"
import AgentCreatorDialog from "./agent-creator-dialog"
const log = getLogger("session") const log = getLogger("session")
@@ -16,6 +18,7 @@ interface AgentSelectorProps {
export default function AgentSelector(props: AgentSelectorProps) { export default function AgentSelector(props: AgentSelectorProps) {
const instanceAgents = () => agents().get(props.instanceId) || [] const instanceAgents = () => agents().get(props.instanceId) || []
const [showCreator, setShowCreator] = createSignal(false)
const session = createMemo(() => { const session = createMemo(() => {
const instanceSessions = sessions().get(props.instanceId) const instanceSessions = sessions().get(props.instanceId)
@@ -64,7 +67,8 @@ export default function AgentSelector(props: AgentSelectorProps) {
} }
return ( return (
<div class="sidebar-selector"> <>
<div class="sidebar-selector flex items-center gap-1">
<Select <Select
value={availableAgents().find((a) => a.name === props.currentAgent)} value={availableAgents().find((a) => a.name === props.currentAgent)}
onChange={handleChange} onChange={handleChange}
@@ -83,6 +87,9 @@ export default function AgentSelector(props: AgentSelectorProps) {
<Show when={itemProps.item.rawValue.mode === "subagent"}> <Show when={itemProps.item.rawValue.mode === "subagent"}>
<span class="neutral-badge">subagent</span> <span class="neutral-badge">subagent</span>
</Show> </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> </Select.ItemLabel>
<Show when={itemProps.item.rawValue.description}> <Show when={itemProps.item.rawValue.description}>
<Select.ItemDescription class="selector-option-description"> <Select.ItemDescription class="selector-option-description">
@@ -119,6 +126,23 @@ export default function AgentSelector(props: AgentSelectorProps) {
</Select.Content> </Select.Content>
</Select.Portal> </Select.Portal>
</Select> </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> </div>
{/* Agent Creator Dialog */}
<AgentCreatorDialog
instanceId={props.instanceId}
open={showCreator()}
onClose={() => setShowCreator(false)}
/>
</>
) )
} }

View File

@@ -10,6 +10,10 @@ type McpServerConfig = {
command?: string command?: string
args?: string[] args?: string[]
env?: Record<string, string> env?: Record<string, string>
// Remote MCP server support
type?: "remote" | "http" | "sse" | "streamable-http"
url?: string
headers?: Record<string, string>
} }
type McpConfig = { type McpConfig = {
@@ -23,8 +27,10 @@ type McpMarketplaceEntry = {
config: McpServerConfig config: McpServerConfig
tags?: string[] tags?: string[]
source?: string source?: string
requiresApiKey?: boolean
} }
interface McpManagerProps { interface McpManagerProps {
instanceId: string 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_RELEASES = "https://github.com/milisp/mcp-linker/releases"
const MCP_LINKER_MARKET = "https://github.com/milisp/mcp-linker" const MCP_LINKER_MARKET = "https://github.com/milisp/mcp-linker"
const MARKETPLACE_ENTRIES: McpMarketplaceEntry[] = [ 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", id: "sequential-thinking",
name: "Sequential Thinking", name: "Sequential Thinking",
@@ -76,6 +95,7 @@ const MARKETPLACE_ENTRIES: McpMarketplaceEntry[] = [
}, },
] ]
const McpManager: Component<McpManagerProps> = (props) => { const McpManager: Component<McpManagerProps> = (props) => {
const [config, setConfig] = createSignal<McpConfig>({ mcpServers: {} }) const [config, setConfig] = createSignal<McpConfig>({ mcpServers: {} })
const [isLoading, setIsLoading] = createSignal(false) const [isLoading, setIsLoading] = createSignal(false)

View File

@@ -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 extraProviders = await fetchExtraProviders()
const baseProviders = removeDuplicateProviders(filteredBaseProviders, extraProviders) const baseProviders = removeDuplicateProviders(filteredBaseProviders, extraProviders)