feat: enhance agent creation UI/UX, improve agent memory configuration, and refine styling across agent components.

This commit is contained in:
viettranx committed 2026-02-24 11:30:51 +07:00
1 parent 08ced252b2
commit 48878848df
13 files changed
+203 -81

No files matched your search

+11 -1
View File
@@ -489,8 +489,18 @@ func runGateway() {
}
}
// Memory detection
// Memory detection: SQLite (standalone) or PG (managed) — either enables memory.
hasMemory := memMgr != nil
if !hasMemory && managedStores != nil && managedStores.Memory != nil {
hasMemory = true
// PG memory is available but SQLite failed or wasn't created.
// Ensure memory tools are registered so wireManagedExtras can wire PG store to them.
if _, exists := toolsReg.Get("memory_search"); !exists {
toolsReg.Register(tools.NewMemorySearchTool(nil))
toolsReg.Register(tools.NewMemoryGetTool(nil))
slog.Info("memory tools registered for managed mode (PG-backed)")
}
}
// Wire SessionStoreAware + BusAware on tools that need them
for _, name := range []string{"sessions_list", "session_status", "sessions_history", "sessions_send"} {
+60 -1
View File
@@ -66,7 +66,23 @@ func runAutoOnboard(cfgPath string) bool {
fmt.Printf(" Provider: %s (model: %s)\n", provider, cfg.Agents.Defaults.Model)
// 2. Gateway token
// 2. Auto-enable memory: detect embedding-capable API keys from env.
// Embedding providers: openai, openrouter, gemini (same order as resolveEmbeddingProvider).
embProvider := autoDetectEmbeddingProvider(cfg)
if embProvider != "" {
enabled := true
cfg.Agents.Defaults.Memory = &config.MemoryConfig{
Enabled: &enabled,
EmbeddingProvider: embProvider,
}
fmt.Printf(" Memory: enabled (embedding: %s)\n", embProvider)
} else {
fmt.Println(" Memory: enabled (FTS-only, no embedding API key)")
enabled := true
cfg.Agents.Defaults.Memory = &config.MemoryConfig{Enabled: &enabled}
}
// 3. Gateway token
if cfg.Gateway.Token == "" {
cfg.Gateway.Token = onboardGenerateToken(16)
slog.Info("auto-onboard: generated gateway token")
@@ -150,6 +166,36 @@ func runAutoOnboard(cfgPath string) bool {
return true
}
// embeddingCapable lists providers that support text embeddings.
// Only these three have embedding provider implementations in resolveEmbeddingProvider.
var embeddingCapable = map[string]bool{
"openai": true,
"openrouter": true,
"gemini": true,
}
// autoDetectEmbeddingProvider picks an embedding provider from available API keys.
// Priority: primary provider (GOCLAW_PROVIDER) if embedding-capable, then openai → openrouter → gemini.
func autoDetectEmbeddingProvider(cfg *config.Config) string {
// Prioritize the primary provider if it supports embeddings.
primary := cfg.Agents.Defaults.Provider
if embeddingCapable[primary] && resolveProviderAPIKey(cfg, primary) != "" {
return primary
}
// Fallback: first available embedding-capable key.
if cfg.Providers.OpenAI.APIKey != "" {
return "openai"
}
if cfg.Providers.OpenRouter.APIKey != "" {
return "openrouter"
}
if cfg.Providers.Gemini.APIKey != "" {
return "gemini"
}
return ""
}
// detectProvider finds the first provider with an API key in the environment.
func detectProvider(cfg *config.Config) (string, string) {
for _, name := range providerPriority {
@@ -234,6 +280,19 @@ func saveCleanConfig(cfgPath string, cfg *config.Config) error {
agents["defaults"].(map[string]interface{})["subagents"] = cfg.Agents.Defaults.Subagents
}
if mc := cfg.Agents.Defaults.Memory; mc != nil {
mem := map[string]interface{}{
"enabled": mc.Enabled == nil || *mc.Enabled,
}
if mc.EmbeddingProvider != "" {
mem["embedding_provider"] = mc.EmbeddingProvider
}
if mc.EmbeddingModel != "" {
mem["embedding_model"] = mc.EmbeddingModel
}
agents["defaults"].(map[string]interface{})["memory"] = mem
}
// Build gateway section (no token — secret).
gateway := map[string]interface{}{
"host": cfg.Gateway.Host,
+10 -1
View File
@@ -135,6 +135,15 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc {
}
}
// Per-agent memory: enabled if global memory manager exists AND
// per-agent config doesn't explicitly disable it.
hasMemory := deps.HasMemory
if mc := ag.ParseMemoryConfig(); mc != nil && mc.Enabled != nil {
if !*mc.Enabled {
hasMemory = false
}
}
loop := NewLoop(LoopConfig{
ID: ag.AgentKey,
AgentUUID: ag.ID,
@@ -150,7 +159,7 @@ func NewManagedResolver(deps ResolverDeps) ResolverFunc {
ToolPolicy: deps.ToolPolicy,
AgentToolPolicy: ag.ParseToolsConfig(),
SkillsLoader: deps.Skills,
HasMemory: deps.HasMemory,
HasMemory: hasMemory,
ContextFiles: contextFiles,
EnsureUserFiles: deps.EnsureUserFiles,
ContextFileLoader: deps.ContextFileLoader,
+8
View File
@@ -122,6 +122,14 @@ func (h *AgentsHandler) handleCreate(w http.ResponseWriter, r *http.Request) {
}
req.RestrictToWorkspace = true
// Default: enable compaction and memory for new agents
if len(req.CompactionConfig) == 0 {
req.CompactionConfig = json.RawMessage(`{}`)
}
if len(req.MemoryConfig) == 0 {
req.MemoryConfig = json.RawMessage(`{"enabled":true}`)
}
// Check if predefined agent has a description for LLM summoning
description := extractDescription(req.OtherConfig)
if req.AgentType == "predefined" && description != "" && h.summoner != nil {
+3 -1
View File
@@ -51,13 +51,15 @@ function DialogContent({
className,
children,
showCloseButton = true,
overlayTransparent = false,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
overlayTransparent?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogOverlay style={overlayTransparent ? { background: "rgba(0,0,0,0.15)" } : undefined} />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
+5
View File
@@ -1,8 +1,13 @@
/**
* Convert any string to a valid slug: lowercase, [a-z0-9-], no leading/trailing dashes.
* Handles Vietnamese and other diacritical characters by stripping accents first.
*/
export function slugify(input: string): string {
return input
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/đ/g, "d")
.replace(/Đ/g, "d")
.toLowerCase()
.replace(/[^a-z0-9-]/g, "-")
.replace(/-{2,}/g, "-")
+1 -1
View File
@@ -20,7 +20,7 @@ export function AgentCard({ agent, onClick }: AgentCardProps) {
<button
type="button"
onClick={onClick}
className="flex flex-col gap-3 rounded-lg border bg-card p-4 text-left transition-all hover:border-primary/30 hover:shadow-md"
className="flex cursor-pointer flex-col gap-3 rounded-lg border bg-card p-4 text-left transition-all hover:border-primary/30 hover:shadow-md"
>
{/* Top row: icon + name + status */}
<div className="flex items-center gap-3">
+67 -34
View File
@@ -33,6 +33,7 @@ interface AgentCreateDialogProps {
export function AgentCreateDialog({ open, onOpenChange, onCreate }: AgentCreateDialogProps) {
const { providers } = useProviders();
const [agentKey, setAgentKey] = useState("");
const [keyTouched, setKeyTouched] = useState(false);
const [displayName, setDisplayName] = useState("");
const [provider, setProvider] = useState("");
const [model, setModel] = useState("");
@@ -63,6 +64,7 @@ export function AgentCreateDialog({ open, onOpenChange, onCreate }: AgentCreateD
});
onOpenChange(false);
setAgentKey("");
setKeyTouched(false);
setDisplayName("");
setProvider("");
setModel("");
@@ -82,33 +84,44 @@ export function AgentCreateDialog({ open, onOpenChange, onCreate }: AgentCreateD
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg">
<DialogContent className="max-w-2xl max-h-[90vh] flex flex-col">
<DialogHeader>
<DialogTitle>Create Agent</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="agentKey">Agent Key *</Label>
<Input
id="agentKey"
value={agentKey}
onChange={(e) => setAgentKey(slugify(e.target.value))}
placeholder="e.g. my-agent"
/>
<p className="text-xs text-muted-foreground">Lowercase letters, numbers, and hyphens only</p>
</div>
<div className="space-y-2">
<Label htmlFor="displayName">Display Name</Label>
<Input
id="displayName"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="My Agent"
/>
<div className="space-y-4 py-4 overflow-y-auto min-h-0">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="displayName">Display Name *</Label>
<Input
id="displayName"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
onBlur={() => {
if (!keyTouched && displayName.trim()) {
setAgentKey(slugify(displayName.trim()));
}
}}
placeholder="My Agent"
/>
</div>
<div className="space-y-2">
<Label htmlFor="agentKey">Agent Key *</Label>
<Input
id="agentKey"
value={agentKey}
onChange={(e) => {
setKeyTouched(true);
setAgentKey(e.target.value);
}}
onBlur={() => setAgentKey(slugify(agentKey))}
placeholder="e.g. my-agent"
/>
<p className="text-xs text-muted-foreground">Lowercase, numbers, hyphens</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Provider</Label>
<Label>Provider *</Label>
{enabledProviders.length > 0 ? (
<Select value={provider} onValueChange={handleProviderChange}>
<SelectTrigger>
@@ -131,38 +144,58 @@ export function AgentCreateDialog({ open, onOpenChange, onCreate }: AgentCreateD
)}
</div>
<div className="space-y-2">
<Label>Model</Label>
<Label>Model *</Label>
<Combobox
value={model}
onChange={setModel}
options={models.map((m) => ({ value: m.id, label: m.name }))}
placeholder={modelsLoading ? "Loading models..." : "Enter or select model"}
/>
{provider && !modelsLoading && models.length === 0 && (
<p className="text-xs text-muted-foreground">This provider doesn't list models — type the model ID manually.</p>
)}
</div>
</div>
<div className="space-y-2">
<Label>Agent Type</Label>
<Select value={agentType} onValueChange={(v) => setAgentType(v as "open" | "predefined")}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="open">Open (per-user context)</SelectItem>
<SelectItem value="predefined">Predefined (agent-level)</SelectItem>
</SelectContent>
</Select>
<div className="flex gap-2">
<button
type="button"
onClick={() => setAgentType("open")}
className={`flex-1 rounded-md border px-3 py-2 text-sm font-medium transition-colors ${
agentType === "open"
? "border-primary bg-primary text-primary-foreground"
: "border-input bg-background hover:bg-accent"
}`}
>
Open
<span className="block text-xs font-normal opacity-70">Per-user context</span>
</button>
<button
type="button"
onClick={() => setAgentType("predefined")}
className={`flex-1 rounded-md border px-3 py-2 text-sm font-medium transition-colors ${
agentType === "predefined"
? "border-primary bg-primary text-primary-foreground"
: "border-input bg-background hover:bg-accent"
}`}
>
Predefined
<span className="block text-xs font-normal opacity-70">Agent-level config</span>
</button>
</div>
</div>
{agentType === "predefined" && (
<div className="space-y-3">
<Label>Describe Your Agent</Label>
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-1.5">
{AGENT_PRESETS.map((preset) => (
<button
key={preset.label}
type="button"
onClick={() => setDescription(preset.prompt)}
className="rounded-full border px-3 py-1 text-xs transition-colors hover:bg-accent"
className="rounded-full border px-2.5 py-0.5 text-xs transition-colors hover:bg-accent"
>
{preset.label}
</button>
@@ -185,7 +218,7 @@ export function AgentCreateDialog({ open, onOpenChange, onCreate }: AgentCreateD
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={loading}>
Cancel
</Button>
<Button onClick={handleCreate} disabled={!agentKey.trim() || !isValidSlug(agentKey) || loading}>
<Button onClick={handleCreate} disabled={!displayName.trim() || !agentKey.trim() || !isValidSlug(agentKey) || !provider.trim() || !model.trim() || loading}>
{loading ? "Creating..." : "Create"}
</Button>
</DialogFooter>
@@ -149,8 +149,8 @@ export function AgentConfigTab({ agent, onUpdate }: AgentConfigTabProps) {
<Check className="h-3.5 w-3.5" /> Saved
</span>
)}
<Button onClick={handleSave} disabled={saving} className="gap-1.5">
<Save className="h-4 w-4" />
<Button onClick={handleSave} disabled={saving}>
{!saving && <Save className="h-4 w-4" />}
{saving ? "Saving..." : "Save Config"}
</Button>
</div>
@@ -1,7 +1,6 @@
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent } from "@/components/ui/card";
import { ArrowLeft, Bot, Star } from "lucide-react";
import { useAgentDetail } from "../hooks/use-agent-detail";
import { AgentGeneralTab } from "./agent-general-tab";
@@ -88,40 +87,38 @@ export function AgentDetailPage({ agentId, onBack }: AgentDetailPageProps) {
</div>
{/* Tabs */}
<Card>
<CardContent className="pt-6">
<Tabs defaultValue="general">
<TabsList>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="config">Config</TabsTrigger>
<TabsTrigger value="files">Files</TabsTrigger>
<TabsTrigger value="shares">Shares</TabsTrigger>
</TabsList>
<div className="rounded-xl border bg-card p-4 shadow-sm">
<Tabs defaultValue="general">
<TabsList>
<TabsTrigger value="general">General</TabsTrigger>
<TabsTrigger value="config">Config</TabsTrigger>
<TabsTrigger value="files">Files</TabsTrigger>
<TabsTrigger value="shares">Shares</TabsTrigger>
</TabsList>
<TabsContent value="general" className="mt-4">
<AgentGeneralTab agent={agent} onUpdate={updateAgent} />
</TabsContent>
<TabsContent value="general" className="mt-4">
<AgentGeneralTab agent={agent} onUpdate={updateAgent} />
</TabsContent>
<TabsContent value="config" className="mt-4">
<AgentConfigTab agent={agent} onUpdate={updateAgent} />
</TabsContent>
<TabsContent value="config" className="mt-4">
<AgentConfigTab agent={agent} onUpdate={updateAgent} />
</TabsContent>
<TabsContent value="files" className="mt-4">
<AgentFilesTab
agent={agent}
files={files}
onGetFile={getFile}
onSetFile={setFile}
onRegenerate={regenerateAgent}
/>
</TabsContent>
<TabsContent value="files" className="mt-4">
<AgentFilesTab
agent={agent}
files={files}
onGetFile={getFile}
onSetFile={setFile}
onRegenerate={regenerateAgent}
/>
</TabsContent>
<TabsContent value="shares" className="mt-4">
<AgentSharesTab agentId={agentId} />
</TabsContent>
</Tabs>
</CardContent>
</Card>
<TabsContent value="shares" className="mt-4">
<AgentSharesTab agentId={agentId} />
</TabsContent>
</Tabs>
</div>
</div>
);
}
@@ -199,9 +199,8 @@ export function AgentFilesTab({ agent, files, onGetFile, onSetFile, onRegenerate
size="sm"
onClick={handleSave}
disabled={!dirty || saving}
className="gap-1"
>
<Save className="h-3.5 w-3.5" />
{!saving && <Save className="h-3.5 w-3.5" />}
{saving ? "Saving..." : "Save"}
</Button>
)}
@@ -238,8 +238,8 @@ export function AgentGeneralTab({ agent, onUpdate }: AgentGeneralTabProps) {
<Check className="h-3.5 w-3.5" /> Saved
</span>
)}
<Button onClick={handleSave} disabled={saving} className="gap-1.5">
<Save className="h-4 w-4" />
<Button onClick={handleSave} disabled={saving}>
{!saving && <Save className="h-4 w-4" />}
{saving ? "Saving..." : "Save Changes"}
</Button>
</div>
+5 -5
View File
@@ -72,7 +72,7 @@ export function SummoningModal({
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-md" onInteractOutside={(e) => { if (status === "summoning") e.preventDefault(); }}>
<DialogContent className="sm:max-w-md" overlayTransparent onInteractOutside={(e) => { if (status === "summoning") e.preventDefault(); }}>
<DialogHeader>
<DialogTitle className="text-center">
{status === "completed"
@@ -126,7 +126,7 @@ export function SummoningModal({
</div>
{/* Agent name */}
<p className="text-sm text-foreground/80">
<p className="text-sm text-foreground">
{status === "completed" ? (
<span className="font-medium text-emerald-600 dark:text-emerald-400">
{agentName} is ready!
@@ -148,8 +148,8 @@ export function SummoningModal({
return (
<motion.div
key={file.name}
initial={{ opacity: 0.6 }}
animate={{ opacity: done ? 1 : 0.6 }}
initial={{ opacity: 1 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
className="flex items-center gap-3 rounded-md px-3 py-1.5"
>
@@ -165,7 +165,7 @@ export function SummoningModal({
{done ? "\u2713" : i + 1}
</motion.div>
<div className="flex-1">
<span className={`text-sm ${done ? "font-medium" : ""}`}>
<span className={`text-sm text-foreground ${done ? "font-medium" : ""}`}>
{file.name}
</span>
<span className="ml-2 text-xs text-muted-foreground">{file.label}</span>