import { SharedRichEditor } from "@agent-native/toolkit/editor/SharedRichEditor"; import React, { useState, useRef, useEffect, useCallback, useMemo, } from "react"; import { CLAUDE_SONNET_MODEL_ID, CLAUDE_SONNET_MODEL_LABEL, } from "../../agent/model-config.js"; import { type ParsedFrontmatter, getRemoteAgentIdFromPath, getFrontmatterValue, isCustomAgentPath, isRemoteAgentPath, isSkillPath, parseFrontmatter, serializeFrontmatter, } from "../../resources/metadata.js"; import { agentNativePath } from "../api-path.js"; import { cn } from "../utils.js"; import type { Resource } from "./use-resources.js"; export interface ResourceEditorProps { resource: Resource; onSave: (content: string) => void; /** Controlled view mode — if provided, the editor won't manage its own view state */ view?: "visual" | "code"; onViewChange?: (v: "visual" | "code") => void; /** When true, the editor's internal toolbar row is hidden */ hideToolbar?: boolean; /** When true, content can be viewed and selected but not modified */ readOnly?: boolean; } const CONTROL_STYLE = { fontSize: 12, lineHeight: 1 } as const; const VIEW_PREF_KEY = "resource-editor-view"; function getViewPref(): "visual" | "code" { try { const v = localStorage.getItem(VIEW_PREF_KEY); if (v === "code") return "code"; } catch {} return "visual"; } function setViewPref(v: "visual" | "code") { try { localStorage.setItem(VIEW_PREF_KEY, v); } catch {} } const FM_INPUT_STYLE: React.CSSProperties = { background: "transparent", border: "none", outline: "none", color: "inherit", fontSize: "inherit", fontFamily: "inherit", width: "100%", padding: 0, }; function FrontmatterBar({ resourcePath, frontmatter, onChange, readOnly, }: { resourcePath: string; frontmatter: ParsedFrontmatter; onChange: (updated: ParsedFrontmatter) => void; readOnly?: boolean; }) { const getField = (key: string) => getFrontmatterValue(frontmatter, key) ?? ""; const updateField = (key: string, value: string) => { if (readOnly) return; const exists = frontmatter.fields.some((f) => f.key === key); const newFields = exists ? frontmatter.fields.map((f) => (f.key === key ? { ...f, value } : f)) : [...frontmatter.fields, { key, value }]; const updated: ParsedFrontmatter = { ...frontmatter, raw: serializeFrontmatter(newFields), fields: newFields, }; onChange(updated); }; const name = getField("name"); const description = getField("description"); const isUserInvocable = getField("user-invocable") === "true"; const model = getField("model") || "inherit"; const tools = getField("tools") || "inherit"; const isCustomAgent = isCustomAgentPath(resourcePath); const isSkill = isSkillPath(resourcePath); return (
updateField("name", e.target.value)} readOnly={readOnly} placeholder={isCustomAgent ? "Agent name" : "Skill name"} style={{ ...FM_INPUT_STYLE, fontWeight: 600, color: "hsl(var(--foreground))", fontSize: 13, flex: 1, }} /> {isSkill ? ( ) : null} {isCustomAgent ? ( ) : null}
updateField("description", e.target.value)} placeholder={ isCustomAgent ? "Description — what this agent should handle" : "Description — what this skill does" } style={{ ...FM_INPUT_STYLE, marginTop: 2, opacity: 0.8, color: "hsl(var(--muted-foreground))", }} /> {isCustomAgent ? (
) : null}
); } // --- Syntax-highlighted code editor (textarea + overlay) --- function highlightJson(text: string): string { // Escape HTML first const esc = text .replace(/&/g, "&") .replace(//g, ">"); // Tokenize JSON with regex return esc.replace( /("(?:\\.|[^"\\])*")\s*:|("(?:\\.|[^"\\])*")|((?:-?\d+)(?:\.\d+)?(?:[eE][+-]?\d+)?)|(\btrue\b|\bfalse\b|\bnull\b)/g, (match, key, str, num, lit) => { if (key) return `${key}:`; if (str) return `${str}`; if (num) return `${num}`; if (lit) return `${lit}`; return match; }, ); } const shStyles = ` .sh-key { color: #7dd3fc; } .sh-str { color: #86efac; } .sh-num { color: #fca5a5; } .sh-lit { color: #c4b5fd; } `; function SyntaxHighlightEditor({ value, onChange, language: _language, readOnly, }: { value: string; onChange: (v: string) => void; language: "json"; readOnly?: boolean; }) { const textareaRef = useRef(null); const preRef = useRef(null); const highlighted = useMemo(() => highlightJson(value), [value]); const syncScroll = useCallback(() => { if (textareaRef.current && preRef.current) { preRef.current.scrollTop = textareaRef.current.scrollTop; preRef.current.scrollLeft = textareaRef.current.scrollLeft; } }, []); const monoFont = 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace'; const sharedStyle: React.CSSProperties = { fontFamily: monoFont, fontSize: 13, lineHeight: 1.6, padding: 12, margin: 0, border: "none", whiteSpace: "pre", wordWrap: "normal", overflowWrap: "normal", tabSize: 2, }; return ( <>