@getcatalystiq/agent-plane-ui 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1091,6 +1091,7 @@ function CancelRunButton({ runId, onCancelled }) {
1091
1091
  ] }) })
1092
1092
  ] });
1093
1093
  }
1094
+ var emptyForm = { name: "", slug: "", description: "", base_url: "", mcp_endpoint_path: "/mcp" };
1094
1095
  function McpServerListPage({ initialData }) {
1095
1096
  const { mutate } = useSWR.useSWRConfig();
1096
1097
  const client = useAgentPlaneClient();
@@ -1099,21 +1100,48 @@ function McpServerListPage({ initialData }) {
1099
1100
  (c) => c.customConnectors.listServers(),
1100
1101
  initialData ? { fallbackData: initialData } : void 0
1101
1102
  );
1102
- const [showAdd, setShowAdd] = React3.useState(false);
1103
- const [adding, setAdding] = React3.useState(false);
1104
- const [newServer, setNewServer] = React3.useState({ name: "", slug: "", description: "", base_url: "", mcp_endpoint_path: "/mcp" });
1103
+ const [showCreate, setShowCreate] = React3.useState(false);
1104
+ const [creating, setCreating] = React3.useState(false);
1105
+ const [createForm, setCreateForm] = React3.useState(emptyForm);
1106
+ const [createError, setCreateError] = React3.useState("");
1107
+ const [editTarget, setEditTarget] = React3.useState(null);
1108
+ const [editing, setEditing] = React3.useState(false);
1109
+ const [editForm, setEditForm] = React3.useState({ name: "", description: "" });
1110
+ const [editError, setEditError] = React3.useState("");
1105
1111
  const [deleteTarget, setDeleteTarget] = React3.useState(null);
1106
1112
  const [deleting, setDeleting] = React3.useState(false);
1107
1113
  const [deleteError, setDeleteError] = React3.useState("");
1108
- async function handleAdd() {
1109
- setAdding(true);
1114
+ async function handleCreate() {
1115
+ setCreating(true);
1116
+ setCreateError("");
1110
1117
  try {
1111
- await client.customConnectors.createServer(newServer);
1112
- setShowAdd(false);
1113
- setNewServer({ name: "", slug: "", description: "", base_url: "", mcp_endpoint_path: "/mcp" });
1118
+ await client.customConnectors.createServer(createForm);
1119
+ setShowCreate(false);
1120
+ setCreateForm(emptyForm);
1114
1121
  mutate("mcp-servers");
1122
+ } catch (err) {
1123
+ setCreateError(err instanceof Error ? err.message : "Failed to create");
1115
1124
  } finally {
1116
- setAdding(false);
1125
+ setCreating(false);
1126
+ }
1127
+ }
1128
+ function openEdit(server) {
1129
+ setEditTarget(server);
1130
+ setEditForm({ name: server.name, description: server.description });
1131
+ setEditError("");
1132
+ }
1133
+ async function handleEdit() {
1134
+ if (!editTarget) return;
1135
+ setEditing(true);
1136
+ setEditError("");
1137
+ try {
1138
+ await client.customConnectors.updateServer(editTarget.id, editForm);
1139
+ setEditTarget(null);
1140
+ mutate("mcp-servers");
1141
+ } catch (err) {
1142
+ setEditError(err instanceof Error ? err.message : "Failed to update");
1143
+ } finally {
1144
+ setEditing(false);
1117
1145
  }
1118
1146
  }
1119
1147
  async function handleDelete() {
@@ -1132,7 +1160,7 @@ function McpServerListPage({ initialData }) {
1132
1160
  }
1133
1161
  if (error) {
1134
1162
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center min-h-[40vh]", children: /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-destructive", children: [
1135
- "Failed to load MCP servers: ",
1163
+ "Failed to load connectors: ",
1136
1164
  error.message
1137
1165
  ] }) });
1138
1166
  }
@@ -1140,17 +1168,7 @@ function McpServerListPage({ initialData }) {
1140
1168
  return /* @__PURE__ */ jsxRuntime.jsx(Skeleton, { className: "h-96 rounded-lg" });
1141
1169
  }
1142
1170
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
1143
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center", children: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "outline", size: "sm", onClick: () => setShowAdd(!showAdd), children: showAdd ? "Cancel" : "Register Connector" }) }),
1144
- showAdd && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-border p-4 space-y-3", children: [
1145
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
1146
- /* @__PURE__ */ jsxRuntime.jsx(Input, { placeholder: "Name", value: newServer.name, onChange: (e) => setNewServer({ ...newServer, name: e.target.value }) }),
1147
- /* @__PURE__ */ jsxRuntime.jsx(Input, { placeholder: "Slug", value: newServer.slug, onChange: (e) => setNewServer({ ...newServer, slug: e.target.value }) }),
1148
- /* @__PURE__ */ jsxRuntime.jsx(Input, { placeholder: "Description", value: newServer.description, onChange: (e) => setNewServer({ ...newServer, description: e.target.value }) }),
1149
- /* @__PURE__ */ jsxRuntime.jsx(Input, { placeholder: "Base URL", value: newServer.base_url, onChange: (e) => setNewServer({ ...newServer, base_url: e.target.value }) }),
1150
- /* @__PURE__ */ jsxRuntime.jsx(Input, { placeholder: "MCP Endpoint Path", value: newServer.mcp_endpoint_path, onChange: (e) => setNewServer({ ...newServer, mcp_endpoint_path: e.target.value }) })
1151
- ] }),
1152
- /* @__PURE__ */ jsxRuntime.jsx(Button, { size: "sm", onClick: handleAdd, disabled: adding || !newServer.name || !newServer.base_url, children: adding ? "Adding..." : "Add Server" })
1153
- ] }),
1171
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center", children: /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "outline", size: "sm", onClick: () => setShowCreate(true), children: "+ New Connector" }) }),
1154
1172
  /* @__PURE__ */ jsxRuntime.jsxs(AdminTable, { children: [
1155
1173
  /* @__PURE__ */ jsxRuntime.jsxs(AdminTableHead, { children: [
1156
1174
  /* @__PURE__ */ jsxRuntime.jsx(Th, { children: "Name" }),
@@ -1164,10 +1182,17 @@ function McpServerListPage({ initialData }) {
1164
1182
  ] }),
1165
1183
  /* @__PURE__ */ jsxRuntime.jsxs("tbody", { children: [
1166
1184
  servers.map((s) => /* @__PURE__ */ jsxRuntime.jsxs(AdminTableRow, { children: [
1167
- /* @__PURE__ */ jsxRuntime.jsx("td", { className: "p-3", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
1168
- s.logo_url && /* @__PURE__ */ jsxRuntime.jsx("img", { src: s.logo_url, alt: "", className: "w-5 h-5 rounded-sm object-contain" }),
1169
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: s.name })
1170
- ] }) }),
1185
+ /* @__PURE__ */ jsxRuntime.jsx("td", { className: "p-3", children: /* @__PURE__ */ jsxRuntime.jsxs(
1186
+ "button",
1187
+ {
1188
+ onClick: () => openEdit(s),
1189
+ className: "flex items-center gap-2 text-left hover:underline cursor-pointer",
1190
+ children: [
1191
+ s.logo_url && /* @__PURE__ */ jsxRuntime.jsx("img", { src: s.logo_url, alt: "", className: "w-5 h-5 rounded-sm object-contain" }),
1192
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-primary", children: s.name })
1193
+ ]
1194
+ }
1195
+ ) }),
1171
1196
  /* @__PURE__ */ jsxRuntime.jsx("td", { className: "p-3 font-mono text-xs text-muted-foreground", children: s.slug }),
1172
1197
  /* @__PURE__ */ jsxRuntime.jsx("td", { className: "p-3 font-mono text-xs text-muted-foreground truncate max-w-xs", title: s.base_url, children: s.base_url }),
1173
1198
  /* @__PURE__ */ jsxRuntime.jsx("td", { className: "p-3", children: /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: s.client_id ? "default" : "secondary", children: s.client_id ? "Registered" : "No DCR" }) }),
@@ -1185,9 +1210,55 @@ function McpServerListPage({ initialData }) {
1185
1210
  }
1186
1211
  ) })
1187
1212
  ] }, s.id)),
1188
- servers.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(EmptyRow, { colSpan: 8, children: 'No custom connectors registered. Click "Register Connector" to add one.' })
1213
+ servers.length === 0 && /* @__PURE__ */ jsxRuntime.jsx(EmptyRow, { colSpan: 8, children: 'No custom connectors registered. Click "+ New Connector" to add one.' })
1189
1214
  ] })
1190
1215
  ] }),
1216
+ /* @__PURE__ */ jsxRuntime.jsx(Dialog, { open: showCreate, onOpenChange: setShowCreate, children: /* @__PURE__ */ jsxRuntime.jsxs(DialogContent, { children: [
1217
+ /* @__PURE__ */ jsxRuntime.jsx(DialogHeader, { children: /* @__PURE__ */ jsxRuntime.jsx(DialogTitle, { children: "Register Connector" }) }),
1218
+ /* @__PURE__ */ jsxRuntime.jsxs(DialogBody, { className: "space-y-3", children: [
1219
+ createError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-destructive", children: createError }),
1220
+ /* @__PURE__ */ jsxRuntime.jsx(FormField, { label: "Name", children: /* @__PURE__ */ jsxRuntime.jsx(Input, { value: createForm.name, onChange: (e) => setCreateForm({ ...createForm, name: e.target.value }), placeholder: "My MCP Server" }) }),
1221
+ /* @__PURE__ */ jsxRuntime.jsx(FormField, { label: "Slug", children: /* @__PURE__ */ jsxRuntime.jsx(Input, { value: createForm.slug, onChange: (e) => setCreateForm({ ...createForm, slug: e.target.value }), placeholder: "my-mcp-server" }) }),
1222
+ /* @__PURE__ */ jsxRuntime.jsx(FormField, { label: "Description", children: /* @__PURE__ */ jsxRuntime.jsx(Input, { value: createForm.description, onChange: (e) => setCreateForm({ ...createForm, description: e.target.value }), placeholder: "What this connector does" }) }),
1223
+ /* @__PURE__ */ jsxRuntime.jsx(FormField, { label: "Base URL", children: /* @__PURE__ */ jsxRuntime.jsx(Input, { value: createForm.base_url, onChange: (e) => setCreateForm({ ...createForm, base_url: e.target.value }), placeholder: "https://my-server.example.com" }) }),
1224
+ /* @__PURE__ */ jsxRuntime.jsx(FormField, { label: "MCP Endpoint Path", children: /* @__PURE__ */ jsxRuntime.jsx(Input, { value: createForm.mcp_endpoint_path, onChange: (e) => setCreateForm({ ...createForm, mcp_endpoint_path: e.target.value }), placeholder: "/mcp" }) })
1225
+ ] }),
1226
+ /* @__PURE__ */ jsxRuntime.jsxs(DialogFooter, { children: [
1227
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "outline", onClick: () => setShowCreate(false), children: "Cancel" }),
1228
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { onClick: handleCreate, disabled: creating || !createForm.name || !createForm.base_url, children: creating ? "Creating..." : "Create" })
1229
+ ] })
1230
+ ] }) }),
1231
+ /* @__PURE__ */ jsxRuntime.jsx(Dialog, { open: !!editTarget, onOpenChange: (open) => {
1232
+ if (!open) setEditTarget(null);
1233
+ }, children: /* @__PURE__ */ jsxRuntime.jsxs(DialogContent, { children: [
1234
+ /* @__PURE__ */ jsxRuntime.jsx(DialogHeader, { children: /* @__PURE__ */ jsxRuntime.jsx(DialogTitle, { children: "Edit Connector" }) }),
1235
+ /* @__PURE__ */ jsxRuntime.jsxs(DialogBody, { className: "space-y-3", children: [
1236
+ editError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-destructive", children: editError }),
1237
+ /* @__PURE__ */ jsxRuntime.jsx(FormField, { label: "Name", children: /* @__PURE__ */ jsxRuntime.jsx(Input, { value: editForm.name, onChange: (e) => setEditForm({ ...editForm, name: e.target.value }) }) }),
1238
+ /* @__PURE__ */ jsxRuntime.jsx(FormField, { label: "Description", children: /* @__PURE__ */ jsxRuntime.jsx(Input, { value: editForm.description, onChange: (e) => setEditForm({ ...editForm, description: e.target.value }) }) }),
1239
+ editTarget && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-xs text-muted-foreground space-y-1", children: [
1240
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1241
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: "Slug:" }),
1242
+ " ",
1243
+ editTarget.slug
1244
+ ] }),
1245
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1246
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: "Base URL:" }),
1247
+ " ",
1248
+ editTarget.base_url
1249
+ ] }),
1250
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
1251
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium", children: "Endpoint:" }),
1252
+ " ",
1253
+ editTarget.mcp_endpoint_path
1254
+ ] })
1255
+ ] })
1256
+ ] }),
1257
+ /* @__PURE__ */ jsxRuntime.jsxs(DialogFooter, { children: [
1258
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "outline", onClick: () => setEditTarget(null), children: "Cancel" }),
1259
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { onClick: handleEdit, disabled: editing || !editForm.name, children: editing ? "Saving..." : "Save" })
1260
+ ] })
1261
+ ] }) }),
1191
1262
  /* @__PURE__ */ jsxRuntime.jsxs(
1192
1263
  ConfirmDialog,
1193
1264
  {
@@ -1198,14 +1269,14 @@ function McpServerListPage({ initialData }) {
1198
1269
  setDeleteError("");
1199
1270
  }
1200
1271
  },
1201
- title: "Delete Server",
1272
+ title: "Delete Connector",
1202
1273
  confirmLabel: "Delete",
1203
1274
  loadingLabel: "Deleting...",
1204
1275
  loading: deleting,
1205
1276
  error: deleteError,
1206
1277
  onConfirm: handleDelete,
1207
1278
  children: [
1208
- "Delete MCP server ",
1279
+ "Delete connector ",
1209
1280
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: deleteTarget?.name }),
1210
1281
  "? This cannot be undone."
1211
1282
  ]
@@ -3100,128 +3171,388 @@ function AgentConnectorsManager({ agentId, toolkits: initialToolkits, composioAl
3100
3171
  ] });
3101
3172
  }
3102
3173
  var CodeEditor = React3.lazy(() => import('./code-editor-ZD5ZILTM.cjs'));
3103
- function AgentSkillManager({ agentId, initialSkills, onSaved }) {
3104
- const client = useAgentPlaneClient();
3105
- const initialFiles = React3.useMemo(
3106
- () => initialSkills.flatMap(
3107
- (s) => s.files.map((f) => ({
3108
- path: s.folder === "(root)" ? f.path : `${s.folder}/${f.path}`,
3109
- content: f.content
3110
- }))
3111
- ),
3112
- [initialSkills]
3113
- );
3174
+ function buildTree(files) {
3175
+ const rootFiles = [];
3176
+ const dirMap = /* @__PURE__ */ new Map();
3177
+ function ensureDir(dirPath) {
3178
+ const existing = dirMap.get(dirPath);
3179
+ if (existing) return existing;
3180
+ const parts = dirPath.split("/");
3181
+ const node = {
3182
+ name: parts[parts.length - 1],
3183
+ fullPath: dirPath,
3184
+ children: [],
3185
+ files: []
3186
+ };
3187
+ dirMap.set(dirPath, node);
3188
+ if (parts.length > 1) {
3189
+ const parentPath = parts.slice(0, -1).join("/");
3190
+ const parent = ensureDir(parentPath);
3191
+ if (!parent.children.some((c) => c.fullPath === dirPath)) {
3192
+ parent.children.push(node);
3193
+ }
3194
+ }
3195
+ return node;
3196
+ }
3197
+ for (const file of files) {
3198
+ const slashIdx = file.path.lastIndexOf("/");
3199
+ if (slashIdx === -1) {
3200
+ rootFiles.push(file);
3201
+ } else {
3202
+ const dirPath = file.path.slice(0, slashIdx);
3203
+ const dir = ensureDir(dirPath);
3204
+ dir.files.push(file);
3205
+ }
3206
+ }
3207
+ const topLevel = [];
3208
+ for (const node of dirMap.values()) {
3209
+ if (!node.fullPath.includes("/")) {
3210
+ topLevel.push(node);
3211
+ }
3212
+ }
3213
+ function sortNode(node) {
3214
+ node.children.sort((a, b) => a.name.localeCompare(b.name));
3215
+ node.files.sort((a, b) => a.path.localeCompare(b.path));
3216
+ node.children.forEach(sortNode);
3217
+ }
3218
+ topLevel.forEach(sortNode);
3219
+ topLevel.sort((a, b) => a.name.localeCompare(b.name));
3220
+ rootFiles.sort((a, b) => a.path.localeCompare(b.path));
3221
+ return { rootFiles, rootDirs: topLevel };
3222
+ }
3223
+ function collectAllDirPaths(nodes) {
3224
+ const paths = /* @__PURE__ */ new Set();
3225
+ function walk(node) {
3226
+ paths.add(node.fullPath);
3227
+ node.children.forEach(walk);
3228
+ }
3229
+ nodes.forEach(walk);
3230
+ return paths;
3231
+ }
3232
+ function FileTreeEditor({
3233
+ initialFiles,
3234
+ onSave,
3235
+ onChange,
3236
+ readOnly = false,
3237
+ hideSave = false,
3238
+ title = "Files",
3239
+ saveLabel = "Save",
3240
+ addFolderLabel = "Folder",
3241
+ newFileTemplate = { filename: "SKILL.md", content: "---\nname: New Skill\ndescription: Describe when this skill should be triggered\n---\n\n# Instructions\n\nDescribe what this skill does...\n" },
3242
+ savedVersion
3243
+ }) {
3114
3244
  const [files, setFiles] = React3.useState(initialFiles);
3115
- const [selectedPath, setSelectedPath] = React3.useState(files[0]?.path ?? null);
3245
+ const [selectedPath, setSelectedPath] = React3.useState(
3246
+ initialFiles.length > 0 ? initialFiles[0].path : null
3247
+ );
3116
3248
  const [saving, setSaving] = React3.useState(false);
3117
- const [addingFile, setAddingFile] = React3.useState(false);
3249
+ const [expanded, setExpanded] = React3.useState(() => {
3250
+ const { rootDirs } = buildTree(initialFiles);
3251
+ return collectAllDirPaths(rootDirs);
3252
+ });
3253
+ const [showAddFolder, setShowAddFolder] = React3.useState(false);
3254
+ const [newFolderName, setNewFolderName] = React3.useState("");
3255
+ const [addingFileInDir, setAddingFileInDir] = React3.useState(null);
3118
3256
  const [newFileName, setNewFileName] = React3.useState("");
3257
+ const [savedSnapshot, setSavedSnapshot] = React3.useState(() => JSON.stringify(initialFiles));
3258
+ React3.useEffect(() => {
3259
+ const snap = JSON.stringify(initialFiles);
3260
+ setSavedSnapshot(snap);
3261
+ setFiles(initialFiles);
3262
+ const { rootDirs } = buildTree(initialFiles);
3263
+ setExpanded(collectAllDirPaths(rootDirs));
3264
+ }, [initialFiles]);
3265
+ React3.useEffect(() => {
3266
+ if (savedVersion !== void 0 && savedVersion > 0) {
3267
+ setSavedSnapshot(JSON.stringify(files));
3268
+ }
3269
+ }, [savedVersion]);
3270
+ const onChangeRef = React3.useRef(onChange);
3271
+ onChangeRef.current = onChange;
3272
+ React3.useEffect(() => {
3273
+ if (onChangeRef.current && JSON.stringify(files) !== savedSnapshot) {
3274
+ onChangeRef.current(files);
3275
+ }
3276
+ }, [files, savedSnapshot]);
3119
3277
  const isDirty = React3.useMemo(
3120
- () => JSON.stringify(files) !== JSON.stringify(initialFiles),
3121
- [files, initialFiles]
3278
+ () => JSON.stringify(files) !== savedSnapshot,
3279
+ [files, savedSnapshot]
3280
+ );
3281
+ const tree = React3.useMemo(() => buildTree(files), [files]);
3282
+ const activeFile = React3.useMemo(
3283
+ () => selectedPath ? files.find((f) => f.path === selectedPath) ?? null : null,
3284
+ [files, selectedPath]
3122
3285
  );
3123
- const selectedFile = files.find((f) => f.path === selectedPath);
3124
- function updateFileContent(path, content) {
3125
- setFiles((prev) => prev.map((f) => f.path === path ? { ...f, content } : f));
3286
+ const handleEditorChange = React3.useCallback((value) => {
3287
+ if (readOnly || !selectedPath) return;
3288
+ setFiles((prev) => prev.map((f) => f.path === selectedPath ? { ...f, content: value } : f));
3289
+ }, [readOnly, selectedPath]);
3290
+ function toggleExpand(dirPath) {
3291
+ setExpanded((prev) => {
3292
+ const next = new Set(prev);
3293
+ if (next.has(dirPath)) next.delete(dirPath);
3294
+ else next.add(dirPath);
3295
+ return next;
3296
+ });
3297
+ }
3298
+ function addFolder() {
3299
+ const name = newFolderName.trim();
3300
+ if (!name) return;
3301
+ const filePath = `${name}/${newFileTemplate.filename}`;
3302
+ if (files.some((f) => f.path === filePath)) return;
3303
+ const content = newFileTemplate.content.replace("# New", `# ${name}`);
3304
+ setFiles((prev) => [...prev, { path: filePath, content }]);
3305
+ setSelectedPath(filePath);
3306
+ setExpanded((prev) => /* @__PURE__ */ new Set([...prev, name]));
3307
+ setNewFolderName("");
3308
+ setShowAddFolder(false);
3126
3309
  }
3127
- function addFile() {
3310
+ function removeDir(dirPath) {
3311
+ const prefix = dirPath + "/";
3312
+ const affectedFiles = files.filter((f) => f.path.startsWith(prefix));
3313
+ if (affectedFiles.length === 0) return;
3314
+ if (!confirm(`Remove "${dirPath}" and all ${affectedFiles.length} file(s)?`)) return;
3315
+ setFiles((prev) => prev.filter((f) => !f.path.startsWith(prefix)));
3316
+ if (selectedPath && selectedPath.startsWith(prefix)) setSelectedPath(null);
3317
+ }
3318
+ function removeFile(filePath) {
3319
+ const fileName = filePath.split("/").pop() ?? filePath;
3320
+ if (!confirm(`Remove file "${fileName}"?`)) return;
3321
+ setFiles((prev) => prev.filter((f) => f.path !== filePath));
3322
+ if (selectedPath === filePath) setSelectedPath(null);
3323
+ }
3324
+ function addFileInDir(dirPath) {
3128
3325
  const name = newFileName.trim();
3129
- if (!name || files.some((f) => f.path === name)) return;
3130
- const content = name.endsWith(".md") ? "---\nname: New Skill\ndescription: Describe when this skill should be triggered\n---\n\n# Instructions\n\nDescribe what this skill does...\n" : "";
3131
- setFiles((prev) => [...prev, { path: name, content }]);
3132
- setSelectedPath(name);
3133
- setAddingFile(false);
3326
+ if (!name) return;
3327
+ const filePath = dirPath ? `${dirPath}/${name}` : name;
3328
+ if (files.some((f) => f.path === filePath)) return;
3329
+ setFiles((prev) => [...prev, { path: filePath, content: "" }]);
3330
+ setSelectedPath(filePath);
3134
3331
  setNewFileName("");
3332
+ setAddingFileInDir(null);
3135
3333
  }
3136
- function removeFile(path) {
3137
- setFiles((prev) => prev.filter((f) => f.path !== path));
3138
- if (selectedPath === path) {
3139
- setSelectedPath(files.find((f) => f.path !== path)?.path ?? null);
3140
- }
3141
- }
3142
- const handleSave = React3.useCallback(async () => {
3334
+ async function handleSave() {
3143
3335
  setSaving(true);
3144
3336
  try {
3145
- const folderMap = /* @__PURE__ */ new Map();
3146
- for (const file of files) {
3147
- const slashIdx = file.path.lastIndexOf("/");
3148
- if (slashIdx === -1) {
3149
- const existing = folderMap.get("(root)") ?? [];
3150
- existing.push({ path: file.path, content: file.content });
3151
- folderMap.set("(root)", existing);
3152
- } else {
3153
- const folder = file.path.slice(0, slashIdx);
3154
- const fileName = file.path.slice(slashIdx + 1);
3155
- const existing = folderMap.get(folder) ?? [];
3156
- existing.push({ path: fileName, content: file.content });
3157
- folderMap.set(folder, existing);
3158
- }
3159
- }
3160
- const skills = Array.from(folderMap.entries()).map(([folder, files2]) => ({ folder, files: files2 }));
3161
- await client.agents.update(agentId, { skills });
3162
- onSaved?.();
3337
+ await onSave(files);
3163
3338
  } finally {
3164
3339
  setSaving(false);
3165
3340
  }
3166
- }, [files, agentId, client, onSaved]);
3167
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border border-muted-foreground/25 p-5", children: [
3168
- /* @__PURE__ */ jsxRuntime.jsx(SectionHeader, { title: "Skills", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
3169
- isDirty && /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: "destructive", className: "text-xs", children: "Unsaved" }),
3170
- /* @__PURE__ */ jsxRuntime.jsx(Button, { size: "sm", variant: "outline", onClick: () => setAddingFile(true), children: "Add File" }),
3171
- /* @__PURE__ */ jsxRuntime.jsx(Button, { size: "sm", onClick: handleSave, disabled: saving || !isDirty, children: saving ? "Saving..." : "Save Skills" })
3172
- ] }) }),
3173
- addingFile && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 mb-3", children: [
3174
- /* @__PURE__ */ jsxRuntime.jsx(
3175
- Input,
3176
- {
3177
- value: newFileName,
3178
- onChange: (e) => setNewFileName(e.target.value),
3179
- placeholder: "folder/SKILL.md",
3180
- className: "max-w-xs text-sm",
3181
- onKeyDown: (e) => e.key === "Enter" && addFile()
3182
- }
3183
- ),
3184
- /* @__PURE__ */ jsxRuntime.jsx(Button, { size: "sm", onClick: addFile, children: "Add" }),
3185
- /* @__PURE__ */ jsxRuntime.jsx(Button, { size: "sm", variant: "ghost", onClick: () => {
3186
- setAddingFile(false);
3187
- setNewFileName("");
3188
- }, children: "Cancel" })
3189
- ] }),
3190
- files.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: 'No skills defined. Click "Add File" to create a skill.' }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-4 min-h-[300px]", children: [
3191
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-48 shrink-0 border-r border-border pr-3 space-y-1", children: files.map((f) => /* @__PURE__ */ jsxRuntime.jsxs(
3341
+ }
3342
+ function renderTreeNode(node, depth) {
3343
+ const isExpanded = expanded.has(node.fullPath);
3344
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3345
+ /* @__PURE__ */ jsxRuntime.jsxs(
3192
3346
  "div",
3193
3347
  {
3194
- className: `flex items-center justify-between group rounded px-2 py-1 text-xs cursor-pointer ${selectedPath === f.path ? "bg-accent text-accent-foreground" : "hover:bg-muted/50"}`,
3195
- onClick: () => setSelectedPath(f.path),
3348
+ className: "flex items-center justify-between cursor-pointer hover:bg-muted/50 py-1 pr-2",
3349
+ style: { paddingLeft: `${depth * 16 + 8}px` },
3350
+ onClick: () => toggleExpand(node.fullPath),
3196
3351
  children: [
3197
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: f.path }),
3198
- /* @__PURE__ */ jsxRuntime.jsx(
3352
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "font-medium text-xs truncate flex items-center gap-1", children: [
3353
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: isExpanded ? "\u25BE" : "\u25B8" }),
3354
+ node.name,
3355
+ "/"
3356
+ ] }),
3357
+ !readOnly && /* @__PURE__ */ jsxRuntime.jsx(
3199
3358
  "button",
3200
3359
  {
3201
- type: "button",
3202
3360
  onClick: (e) => {
3203
3361
  e.stopPropagation();
3204
- removeFile(f.path);
3362
+ removeDir(node.fullPath);
3205
3363
  },
3206
- className: "opacity-0 group-hover:opacity-100 text-muted-foreground hover:text-destructive ml-1",
3364
+ className: "text-muted-foreground hover:text-destructive text-xs ml-1 shrink-0",
3207
3365
  children: "\xD7"
3208
3366
  }
3209
3367
  )
3210
3368
  ]
3211
- },
3212
- f.path
3213
- )) }),
3214
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 min-w-0", children: selectedFile ? /* @__PURE__ */ jsxRuntime.jsx(React3.Suspense, { fallback: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-[300px] animate-pulse bg-muted/50 rounded" }), children: /* @__PURE__ */ jsxRuntime.jsx(
3215
- CodeEditor,
3216
- {
3217
- value: selectedFile.content,
3218
- onChange: (val) => updateFileContent(selectedFile.path, val),
3219
- filename: selectedFile.path
3220
3369
  }
3221
- ) }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: "Select a file to edit" }) })
3370
+ ),
3371
+ isExpanded && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3372
+ node.children.map((child) => renderTreeNode(child, depth + 1)),
3373
+ node.files.map((file) => {
3374
+ const fileName = file.path.split("/").pop() ?? file.path;
3375
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3376
+ "div",
3377
+ {
3378
+ className: `flex items-center justify-between cursor-pointer hover:bg-muted/30 py-1 pr-2 ${selectedPath === file.path ? "bg-primary/10 text-primary" : ""}`,
3379
+ style: { paddingLeft: `${(depth + 1) * 16 + 8}px` },
3380
+ onClick: () => setSelectedPath(file.path),
3381
+ children: [
3382
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs truncate", children: fileName }),
3383
+ !readOnly && /* @__PURE__ */ jsxRuntime.jsx(
3384
+ "button",
3385
+ {
3386
+ onClick: (e) => {
3387
+ e.stopPropagation();
3388
+ removeFile(file.path);
3389
+ },
3390
+ className: "text-muted-foreground hover:text-destructive text-xs ml-1 shrink-0",
3391
+ children: "\xD7"
3392
+ }
3393
+ )
3394
+ ]
3395
+ },
3396
+ file.path
3397
+ );
3398
+ }),
3399
+ !readOnly && /* @__PURE__ */ jsxRuntime.jsx("div", { style: { paddingLeft: `${(depth + 1) * 16 + 8}px` }, className: "py-1 pr-2", children: addingFileInDir === node.fullPath ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1", children: [
3400
+ /* @__PURE__ */ jsxRuntime.jsx(
3401
+ Input,
3402
+ {
3403
+ value: newFileName,
3404
+ onChange: (e) => setNewFileName(e.target.value),
3405
+ placeholder: "file.md",
3406
+ className: "h-6 text-xs",
3407
+ onKeyDown: (e) => e.key === "Enter" && addFileInDir(node.fullPath),
3408
+ autoFocus: true
3409
+ }
3410
+ ),
3411
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { onClick: () => addFileInDir(node.fullPath), size: "sm", className: "h-6 text-xs px-2", children: "+" })
3412
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(
3413
+ "button",
3414
+ {
3415
+ onClick: () => {
3416
+ setAddingFileInDir(node.fullPath);
3417
+ setNewFileName("");
3418
+ },
3419
+ className: "text-xs text-primary hover:underline",
3420
+ children: "+ File"
3421
+ }
3422
+ ) })
3423
+ ] })
3424
+ ] }, node.fullPath);
3425
+ }
3426
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3427
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between mb-3", children: [
3428
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3", children: [
3429
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold", children: title }),
3430
+ isDirty && !readOnly && /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: "destructive", className: "text-xs", children: "Unsaved changes" }),
3431
+ readOnly && /* @__PURE__ */ jsxRuntime.jsx(Badge, { variant: "secondary", className: "text-xs", children: "Read-only" })
3432
+ ] }),
3433
+ !readOnly && !hideSave && /* @__PURE__ */ jsxRuntime.jsx(Button, { onClick: handleSave, disabled: saving || !isDirty, size: "sm", children: saving ? "Saving..." : saveLabel })
3434
+ ] }),
3435
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-4 min-h-[500px]", children: [
3436
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-64 shrink-0 border border-border rounded-md overflow-hidden", children: [
3437
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-2 bg-muted/50 border-b border-border flex items-center justify-between", children: [
3438
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-muted-foreground", children: title }),
3439
+ !readOnly && /* @__PURE__ */ jsxRuntime.jsxs(
3440
+ "button",
3441
+ {
3442
+ onClick: () => setShowAddFolder(!showAddFolder),
3443
+ className: "text-xs text-primary hover:underline",
3444
+ children: [
3445
+ "+ ",
3446
+ addFolderLabel
3447
+ ]
3448
+ }
3449
+ )
3450
+ ] }),
3451
+ showAddFolder && !readOnly && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-2 border-b border-border flex gap-1", children: [
3452
+ /* @__PURE__ */ jsxRuntime.jsx(
3453
+ Input,
3454
+ {
3455
+ value: newFolderName,
3456
+ onChange: (e) => setNewFolderName(e.target.value),
3457
+ placeholder: "folder-name",
3458
+ className: "h-7 text-xs",
3459
+ onKeyDown: (e) => e.key === "Enter" && addFolder(),
3460
+ autoFocus: true
3461
+ }
3462
+ ),
3463
+ /* @__PURE__ */ jsxRuntime.jsx(Button, { onClick: addFolder, size: "sm", className: "h-7 text-xs px-2", children: "Add" })
3464
+ ] }),
3465
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-sm overflow-y-auto", children: [
3466
+ tree.rootFiles.map((file) => /* @__PURE__ */ jsxRuntime.jsxs(
3467
+ "div",
3468
+ {
3469
+ className: `flex items-center justify-between cursor-pointer hover:bg-muted/30 py-1 pr-2 ${selectedPath === file.path ? "bg-primary/10 text-primary" : ""}`,
3470
+ style: { paddingLeft: "8px" },
3471
+ onClick: () => setSelectedPath(file.path),
3472
+ children: [
3473
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs truncate", children: file.path }),
3474
+ !readOnly && /* @__PURE__ */ jsxRuntime.jsx(
3475
+ "button",
3476
+ {
3477
+ onClick: (e) => {
3478
+ e.stopPropagation();
3479
+ removeFile(file.path);
3480
+ },
3481
+ className: "text-muted-foreground hover:text-destructive text-xs ml-1 shrink-0",
3482
+ children: "\xD7"
3483
+ }
3484
+ )
3485
+ ]
3486
+ },
3487
+ file.path
3488
+ )),
3489
+ tree.rootDirs.map((node) => renderTreeNode(node, 0)),
3490
+ files.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "p-3 text-xs text-muted-foreground", children: readOnly ? "No files." : `No ${title.toLowerCase()} yet. Add a ${addFolderLabel.toLowerCase()} to get started.` })
3491
+ ] })
3492
+ ] }),
3493
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 border border-border rounded-md overflow-hidden", children: activeFile ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-full flex flex-col", children: [
3494
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-3 py-1.5 bg-muted/50 border-b border-border text-xs text-muted-foreground", children: activeFile.path }),
3495
+ /* @__PURE__ */ jsxRuntime.jsx(React3.Suspense, { fallback: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1 animate-pulse bg-muted/50" }), children: /* @__PURE__ */ jsxRuntime.jsx(
3496
+ CodeEditor,
3497
+ {
3498
+ value: activeFile.content,
3499
+ onChange: handleEditorChange,
3500
+ filename: activeFile.path
3501
+ }
3502
+ ) })
3503
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "h-full flex items-center justify-center text-muted-foreground text-sm", children: [
3504
+ "Select a file to ",
3505
+ readOnly ? "view" : "edit"
3506
+ ] }) })
3222
3507
  ] })
3223
3508
  ] });
3224
3509
  }
3510
+ function AgentSkillManager({ agentId, initialSkills, onSaved }) {
3511
+ const client = useAgentPlaneClient();
3512
+ const initialFiles = React3.useMemo(
3513
+ () => initialSkills.flatMap(
3514
+ (s) => s.files.map((f) => ({
3515
+ path: s.folder === "(root)" ? f.path : `${s.folder}/${f.path}`,
3516
+ content: f.content
3517
+ }))
3518
+ ),
3519
+ [initialSkills]
3520
+ );
3521
+ const handleSave = React3.useCallback(async (files) => {
3522
+ const folderMap = /* @__PURE__ */ new Map();
3523
+ for (const file of files) {
3524
+ const slashIdx = file.path.lastIndexOf("/");
3525
+ if (slashIdx === -1) {
3526
+ const existing = folderMap.get("(root)") ?? [];
3527
+ existing.push({ path: file.path, content: file.content });
3528
+ folderMap.set("(root)", existing);
3529
+ } else {
3530
+ const folder = file.path.slice(0, slashIdx);
3531
+ const fileName = file.path.slice(slashIdx + 1);
3532
+ const existing = folderMap.get(folder) ?? [];
3533
+ existing.push({ path: fileName, content: file.content });
3534
+ folderMap.set(folder, existing);
3535
+ }
3536
+ }
3537
+ const skills = Array.from(folderMap.entries()).map(([folder, files2]) => ({ folder, files: files2 }));
3538
+ await client.agents.update(agentId, { skills });
3539
+ onSaved?.();
3540
+ }, [agentId, client, onSaved]);
3541
+ return /* @__PURE__ */ jsxRuntime.jsx(
3542
+ FileTreeEditor,
3543
+ {
3544
+ initialFiles,
3545
+ onSave: handleSave,
3546
+ title: "Skills",
3547
+ saveLabel: "Save Skills",
3548
+ addFolderLabel: "Skill",
3549
+ newFileTemplate: {
3550
+ filename: "SKILL.md",
3551
+ content: "---\nname: New Skill\ndescription: Describe when this skill should be triggered\n---\n\n# Instructions\n\nDescribe what this skill does...\n"
3552
+ }
3553
+ }
3554
+ );
3555
+ }
3225
3556
  function AgentPluginManager({ agentId, initialPlugins, onSaved }) {
3226
3557
  const client = useAgentPlaneClient();
3227
3558
  const [plugins, setPlugins] = React3.useState(initialPlugins);