@getcatalystiq/agent-plane-ui 0.1.28 → 0.1.30

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
@@ -3362,7 +3362,8 @@ function FileTreeEditor2({
3362
3362
  addFolderLabel = "Folder",
3363
3363
  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" },
3364
3364
  savedVersion,
3365
- fixedStructure = false
3365
+ fixedStructure = false,
3366
+ headerActions
3366
3367
  }) {
3367
3368
  const [files, setFiles] = React.useState(initialFiles);
3368
3369
  const [selectedPath, setSelectedPath] = React.useState(
@@ -3553,7 +3554,10 @@ function FileTreeEditor2({
3553
3554
  isDirty && !readOnly && /* @__PURE__ */ jsxRuntime.jsx(chunkXXF4U7WL_cjs.Badge, { variant: "destructive", className: "text-xs", children: "Unsaved changes" }),
3554
3555
  readOnly && /* @__PURE__ */ jsxRuntime.jsx(chunkXXF4U7WL_cjs.Badge, { variant: "secondary", className: "text-xs", children: "Read-only" })
3555
3556
  ] }),
3556
- !readOnly && !hideSave && /* @__PURE__ */ jsxRuntime.jsx(chunkXXF4U7WL_cjs.Button, { onClick: handleSave, disabled: saving || !isDirty, size: "sm", children: saving ? "Saving..." : saveLabel })
3557
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
3558
+ headerActions,
3559
+ !readOnly && !hideSave && /* @__PURE__ */ jsxRuntime.jsx(chunkXXF4U7WL_cjs.Button, { onClick: handleSave, disabled: saving || !isDirty, size: "sm", children: saving ? "Saving..." : saveLabel })
3560
+ ] })
3557
3561
  ] }),
3558
3562
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-4 min-h-[500px]", children: [
3559
3563
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-64 shrink-0 border border-border rounded-md overflow-hidden", children: [
@@ -3630,17 +3634,193 @@ function FileTreeEditor2({
3630
3634
  ] })
3631
3635
  ] });
3632
3636
  }
3637
+ function TabButton({ label, active, onClick }) {
3638
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3639
+ "button",
3640
+ {
3641
+ onClick,
3642
+ className: `relative pb-2 text-sm font-medium transition-colors ${active ? "text-foreground" : "text-muted-foreground hover:text-foreground"}`,
3643
+ children: [
3644
+ label,
3645
+ active && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute inset-x-0 bottom-0 h-0.5 bg-foreground rounded-full" })
3646
+ ]
3647
+ }
3648
+ );
3649
+ }
3650
+ function ImportSkillDialog({ open, onOpenChange, onImported, existingFolders }) {
3651
+ const client = chunkXXF4U7WL_cjs.useAgentPlaneClient();
3652
+ const [tab, setTab] = React.useState("all");
3653
+ const [entries, setEntries] = React.useState([]);
3654
+ const [loading, setLoading] = React.useState(false);
3655
+ const [error, setError] = React.useState("");
3656
+ const [search, setSearch] = React.useState("");
3657
+ const [selected, setSelected] = React.useState(null);
3658
+ const [preview, setPreview] = React.useState("");
3659
+ const [previewLoading, setPreviewLoading] = React.useState(false);
3660
+ const [importing, setImporting] = React.useState(false);
3661
+ const [importError, setImportError] = React.useState("");
3662
+ const [url, setUrl] = React.useState("");
3663
+ const [urlImporting, setUrlImporting] = React.useState(false);
3664
+ React.useEffect(() => {
3665
+ if (!open) return;
3666
+ setLoading(true);
3667
+ setError("");
3668
+ setSelected(null);
3669
+ setPreview("");
3670
+ client.skillsDirectory.list(tab).then((data) => setEntries(data)).catch((err) => setError(err instanceof Error ? err.message : "Failed to load skills")).finally(() => setLoading(false));
3671
+ }, [tab, open, client]);
3672
+ const filtered = React.useMemo(() => {
3673
+ if (!search.trim()) return entries;
3674
+ const q = search.toLowerCase();
3675
+ return entries.filter(
3676
+ (e) => e.name.toLowerCase().includes(q) || e.owner.toLowerCase().includes(q) || e.repo.toLowerCase().includes(q)
3677
+ );
3678
+ }, [entries, search]);
3679
+ async function handleSelect(entry) {
3680
+ setSelected(entry);
3681
+ setPreviewLoading(true);
3682
+ setPreview("");
3683
+ setImportError("");
3684
+ try {
3685
+ const content = await client.skillsDirectory.preview(entry.owner, entry.repo, entry.skill);
3686
+ setPreview(content);
3687
+ } catch (err) {
3688
+ setPreview(`Failed to load preview: ${err instanceof Error ? err.message : "Unknown error"}`);
3689
+ } finally {
3690
+ setPreviewLoading(false);
3691
+ }
3692
+ }
3693
+ async function handleImport() {
3694
+ if (!selected) return;
3695
+ setImporting(true);
3696
+ setImportError("");
3697
+ try {
3698
+ const result = await client.skillsDirectory.import({
3699
+ owner: selected.owner,
3700
+ repo: selected.repo,
3701
+ skill_name: selected.skill
3702
+ });
3703
+ if (existingFolders.includes(result.folder)) {
3704
+ setImportError(`Skill folder "${result.folder}" already exists. Remove it first or rename.`);
3705
+ return;
3706
+ }
3707
+ if (result.warnings.length > 0) {
3708
+ setImportError(`Imported with warnings: ${result.warnings.join("; ")}`);
3709
+ }
3710
+ onImported({ folder: result.folder, files: result.files });
3711
+ onOpenChange(false);
3712
+ resetState();
3713
+ } catch (err) {
3714
+ setImportError(err instanceof Error ? err.message : "Failed to import skill");
3715
+ } finally {
3716
+ setImporting(false);
3717
+ }
3718
+ }
3719
+ async function handleUrlImport() {
3720
+ if (!url.trim()) return;
3721
+ setUrlImporting(true);
3722
+ setImportError("");
3723
+ try {
3724
+ const result = await client.skillsDirectory.import({ url: url.trim() });
3725
+ if (existingFolders.includes(result.folder)) {
3726
+ setImportError(`Skill folder "${result.folder}" already exists. Remove it first or rename.`);
3727
+ return;
3728
+ }
3729
+ onImported({ folder: result.folder, files: result.files });
3730
+ onOpenChange(false);
3731
+ resetState();
3732
+ } catch (err) {
3733
+ setImportError(err instanceof Error ? err.message : "Failed to import skill");
3734
+ } finally {
3735
+ setUrlImporting(false);
3736
+ }
3737
+ }
3738
+ function resetState() {
3739
+ setTab("all");
3740
+ setSearch("");
3741
+ setSelected(null);
3742
+ setPreview("");
3743
+ setUrl("");
3744
+ setError("");
3745
+ setImportError("");
3746
+ }
3747
+ return /* @__PURE__ */ jsxRuntime.jsx(chunkXXF4U7WL_cjs.Dialog, { open, onOpenChange: (v) => {
3748
+ onOpenChange(v);
3749
+ if (!v) resetState();
3750
+ }, children: /* @__PURE__ */ jsxRuntime.jsxs(chunkXXF4U7WL_cjs.DialogContent, { className: "max-w-2xl max-h-[80vh] flex flex-col", children: [
3751
+ /* @__PURE__ */ jsxRuntime.jsxs(chunkXXF4U7WL_cjs.DialogHeader, { children: [
3752
+ /* @__PURE__ */ jsxRuntime.jsx(chunkXXF4U7WL_cjs.DialogTitle, { children: "Import from skills.sh" }),
3753
+ /* @__PURE__ */ jsxRuntime.jsx(chunkXXF4U7WL_cjs.DialogDescription, { children: "Browse the open skills directory and import skills into this agent." })
3754
+ ] }),
3755
+ /* @__PURE__ */ jsxRuntime.jsxs(chunkXXF4U7WL_cjs.DialogBody, { className: "flex-1 overflow-hidden flex flex-col gap-4", children: [
3756
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-4", children: [
3757
+ /* @__PURE__ */ jsxRuntime.jsx(TabButton, { label: "All Time", active: tab === "all", onClick: () => setTab("all") }),
3758
+ /* @__PURE__ */ jsxRuntime.jsx(TabButton, { label: "Trending", active: tab === "trending", onClick: () => setTab("trending") }),
3759
+ /* @__PURE__ */ jsxRuntime.jsx(TabButton, { label: "Hot", active: tab === "hot", onClick: () => setTab("hot") })
3760
+ ] }),
3761
+ /* @__PURE__ */ jsxRuntime.jsx(chunkXXF4U7WL_cjs.Input, { placeholder: "Search skills...", value: search, onChange: (e) => setSearch(e.target.value) }),
3762
+ error && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-destructive", children: error }),
3763
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 overflow-hidden flex gap-4 min-h-0", children: [
3764
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1/2 overflow-y-auto border border-muted-foreground/25 rounded-lg", children: loading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 text-sm text-muted-foreground", children: "Loading skills..." }) : filtered.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-4 text-sm text-muted-foreground", children: search ? "No skills match your search" : "No skills found" }) : filtered.map((entry) => /* @__PURE__ */ jsxRuntime.jsxs(
3765
+ "button",
3766
+ {
3767
+ onClick: () => handleSelect(entry),
3768
+ className: `w-full text-left px-3 py-2 border-b border-muted-foreground/10 hover:bg-muted/50 transition-colors ${selected?.skill === entry.skill && selected?.owner === entry.owner ? "bg-muted/50" : ""}`,
3769
+ children: [
3770
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "font-medium text-sm truncate", children: entry.name }),
3771
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-between items-center", children: [
3772
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-xs text-muted-foreground truncate", children: [
3773
+ entry.owner,
3774
+ "/",
3775
+ entry.repo
3776
+ ] }),
3777
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground font-mono ml-2 shrink-0", children: entry.installs })
3778
+ ] })
3779
+ ]
3780
+ },
3781
+ `${entry.owner}/${entry.repo}/${entry.skill}`
3782
+ )) }),
3783
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "w-1/2 overflow-y-auto border border-muted-foreground/25 rounded-lg p-3", children: !selected ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-muted-foreground", children: "Select a skill to preview" }) : previewLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-sm text-muted-foreground", children: "Loading preview..." }) : /* @__PURE__ */ jsxRuntime.jsx("pre", { className: "text-xs whitespace-pre-wrap break-words font-mono", children: preview }) })
3784
+ ] }),
3785
+ importError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-destructive", children: importError }),
3786
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-muted-foreground/25 pt-3", children: /* @__PURE__ */ jsxRuntime.jsx(chunkXXF4U7WL_cjs.FormField, { label: "Or paste a skills.sh URL", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
3787
+ /* @__PURE__ */ jsxRuntime.jsx(
3788
+ chunkXXF4U7WL_cjs.Input,
3789
+ {
3790
+ value: url,
3791
+ onChange: (e) => setUrl(e.target.value),
3792
+ placeholder: "skills.sh/owner/repo/skill",
3793
+ onKeyDown: (e) => e.key === "Enter" && handleUrlImport(),
3794
+ className: "flex-1"
3795
+ }
3796
+ ),
3797
+ /* @__PURE__ */ jsxRuntime.jsx(chunkXXF4U7WL_cjs.Button, { size: "sm", variant: "outline", onClick: handleUrlImport, disabled: urlImporting || !url.trim(), children: urlImporting ? "Importing..." : "Import URL" })
3798
+ ] }) }) })
3799
+ ] }),
3800
+ /* @__PURE__ */ jsxRuntime.jsxs(chunkXXF4U7WL_cjs.DialogFooter, { children: [
3801
+ /* @__PURE__ */ jsxRuntime.jsx(chunkXXF4U7WL_cjs.Button, { variant: "outline", size: "sm", onClick: () => onOpenChange(false), children: "Cancel" }),
3802
+ /* @__PURE__ */ jsxRuntime.jsx(chunkXXF4U7WL_cjs.Button, { size: "sm", onClick: handleImport, disabled: !selected || importing || previewLoading, children: importing ? "Importing..." : "Import Selected" })
3803
+ ] })
3804
+ ] }) });
3805
+ }
3633
3806
  function AgentSkillManager({ agentId, initialSkills, onSaved }) {
3634
3807
  const client = chunkXXF4U7WL_cjs.useAgentPlaneClient();
3808
+ const [importOpen, setImportOpen] = React.useState(false);
3809
+ const [extraSkills, setExtraSkills] = React.useState([]);
3810
+ const allSkills = React.useMemo(() => [...initialSkills, ...extraSkills], [initialSkills, extraSkills]);
3635
3811
  const initialFiles = React.useMemo(
3636
- () => initialSkills.flatMap(
3812
+ () => allSkills.flatMap(
3637
3813
  (s) => s.files.map((f) => ({
3638
3814
  path: s.folder === "(root)" ? f.path : `${s.folder}/${f.path}`,
3639
3815
  content: f.content
3640
3816
  }))
3641
3817
  ),
3642
- [initialSkills]
3818
+ [allSkills]
3643
3819
  );
3820
+ const existingFolders = React.useMemo(() => allSkills.map((s) => s.folder), [allSkills]);
3821
+ const handleImported = React.useCallback((skill) => {
3822
+ setExtraSkills((prev) => [...prev, skill]);
3823
+ }, []);
3644
3824
  const handleSave = React.useCallback(async (files) => {
3645
3825
  const folderMap = /* @__PURE__ */ new Map();
3646
3826
  for (const file of files) {
@@ -3659,22 +3839,35 @@ function AgentSkillManager({ agentId, initialSkills, onSaved }) {
3659
3839
  }
3660
3840
  const skills = Array.from(folderMap.entries()).map(([folder, files2]) => ({ folder, files: files2 }));
3661
3841
  await client.agents.update(agentId, { skills });
3842
+ setExtraSkills([]);
3662
3843
  onSaved?.();
3663
3844
  }, [agentId, client, onSaved]);
3664
- return /* @__PURE__ */ jsxRuntime.jsx(
3665
- FileTreeEditor2,
3666
- {
3667
- initialFiles,
3668
- onSave: handleSave,
3669
- title: "Skills",
3670
- saveLabel: "Save Skills",
3671
- addFolderLabel: "Skill",
3672
- newFileTemplate: {
3673
- filename: "SKILL.md",
3674
- content: "---\nname: New Skill\ndescription: Describe when this skill should be triggered\n---\n\n# Instructions\n\nDescribe what this skill does...\n"
3845
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3846
+ /* @__PURE__ */ jsxRuntime.jsx(
3847
+ FileTreeEditor2,
3848
+ {
3849
+ initialFiles,
3850
+ onSave: handleSave,
3851
+ title: "Skills",
3852
+ saveLabel: "Save Skills",
3853
+ addFolderLabel: "Skill",
3854
+ newFileTemplate: {
3855
+ filename: "SKILL.md",
3856
+ content: "---\nname: New Skill\ndescription: Describe when this skill should be triggered\n---\n\n# Instructions\n\nDescribe what this skill does...\n"
3857
+ },
3858
+ headerActions: /* @__PURE__ */ jsxRuntime.jsx(chunkXXF4U7WL_cjs.Button, { size: "sm", variant: "outline", onClick: () => setImportOpen(true), children: "Import from skills.sh" })
3675
3859
  }
3676
- }
3677
- );
3860
+ ),
3861
+ /* @__PURE__ */ jsxRuntime.jsx(
3862
+ ImportSkillDialog,
3863
+ {
3864
+ open: importOpen,
3865
+ onOpenChange: setImportOpen,
3866
+ onImported: handleImported,
3867
+ existingFolders
3868
+ }
3869
+ )
3870
+ ] });
3678
3871
  }
3679
3872
  function AgentPluginManager({ agentId, initialPlugins, onSaved }) {
3680
3873
  const client = chunkXXF4U7WL_cjs.useAgentPlaneClient();
package/dist/index.d.cts CHANGED
@@ -7,6 +7,22 @@ import * as class_variance_authority_types from 'class-variance-authority/types'
7
7
  import { VariantProps } from 'class-variance-authority';
8
8
  import { DailyAgentStat } from './charts.cjs';
9
9
 
10
+ /** Skills directory types. */
11
+ interface SkillDirectoryEntry {
12
+ name: string;
13
+ owner: string;
14
+ repo: string;
15
+ skill: string;
16
+ installs: string;
17
+ }
18
+ interface ImportedSkillResult {
19
+ folder: string;
20
+ files: Array<{
21
+ path: string;
22
+ content: string;
23
+ }>;
24
+ warnings: string[];
25
+ }
10
26
  /** Minimal stream event types used by the playground UI. */
11
27
  interface PlaygroundTextDeltaEvent {
12
28
  type: "text_delta";
@@ -155,6 +171,17 @@ interface AgentPlaneClient {
155
171
  toolkits(): Promise<unknown[]>;
156
172
  tools(toolkit: string): Promise<unknown[]>;
157
173
  };
174
+ skillsDirectory: {
175
+ list(tab?: "all" | "trending" | "hot"): Promise<SkillDirectoryEntry[]>;
176
+ preview(owner: string, repo: string, skill: string): Promise<string>;
177
+ import(params: {
178
+ owner: string;
179
+ repo: string;
180
+ skill_name: string;
181
+ } | {
182
+ url: string;
183
+ }): Promise<ImportedSkillResult>;
184
+ };
158
185
  pluginMarketplaces: {
159
186
  list(): Promise<unknown[]>;
160
187
  get(marketplaceId: string): Promise<unknown>;
package/dist/index.d.ts CHANGED
@@ -7,6 +7,22 @@ import * as class_variance_authority_types from 'class-variance-authority/types'
7
7
  import { VariantProps } from 'class-variance-authority';
8
8
  import { DailyAgentStat } from './charts.js';
9
9
 
10
+ /** Skills directory types. */
11
+ interface SkillDirectoryEntry {
12
+ name: string;
13
+ owner: string;
14
+ repo: string;
15
+ skill: string;
16
+ installs: string;
17
+ }
18
+ interface ImportedSkillResult {
19
+ folder: string;
20
+ files: Array<{
21
+ path: string;
22
+ content: string;
23
+ }>;
24
+ warnings: string[];
25
+ }
10
26
  /** Minimal stream event types used by the playground UI. */
11
27
  interface PlaygroundTextDeltaEvent {
12
28
  type: "text_delta";
@@ -155,6 +171,17 @@ interface AgentPlaneClient {
155
171
  toolkits(): Promise<unknown[]>;
156
172
  tools(toolkit: string): Promise<unknown[]>;
157
173
  };
174
+ skillsDirectory: {
175
+ list(tab?: "all" | "trending" | "hot"): Promise<SkillDirectoryEntry[]>;
176
+ preview(owner: string, repo: string, skill: string): Promise<string>;
177
+ import(params: {
178
+ owner: string;
179
+ repo: string;
180
+ skill_name: string;
181
+ } | {
182
+ url: string;
183
+ }): Promise<ImportedSkillResult>;
184
+ };
158
185
  pluginMarketplaces: {
159
186
  list(): Promise<unknown[]>;
160
187
  get(marketplaceId: string): Promise<unknown>;
package/dist/index.js CHANGED
@@ -3338,7 +3338,8 @@ function FileTreeEditor2({
3338
3338
  addFolderLabel = "Folder",
3339
3339
  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" },
3340
3340
  savedVersion,
3341
- fixedStructure = false
3341
+ fixedStructure = false,
3342
+ headerActions
3342
3343
  }) {
3343
3344
  const [files, setFiles] = useState(initialFiles);
3344
3345
  const [selectedPath, setSelectedPath] = useState(
@@ -3529,7 +3530,10 @@ function FileTreeEditor2({
3529
3530
  isDirty && !readOnly && /* @__PURE__ */ jsx(Badge, { variant: "destructive", className: "text-xs", children: "Unsaved changes" }),
3530
3531
  readOnly && /* @__PURE__ */ jsx(Badge, { variant: "secondary", className: "text-xs", children: "Read-only" })
3531
3532
  ] }),
3532
- !readOnly && !hideSave && /* @__PURE__ */ jsx(Button, { onClick: handleSave, disabled: saving || !isDirty, size: "sm", children: saving ? "Saving..." : saveLabel })
3533
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
3534
+ headerActions,
3535
+ !readOnly && !hideSave && /* @__PURE__ */ jsx(Button, { onClick: handleSave, disabled: saving || !isDirty, size: "sm", children: saving ? "Saving..." : saveLabel })
3536
+ ] })
3533
3537
  ] }),
3534
3538
  /* @__PURE__ */ jsxs("div", { className: "flex gap-4 min-h-[500px]", children: [
3535
3539
  /* @__PURE__ */ jsxs("div", { className: "w-64 shrink-0 border border-border rounded-md overflow-hidden", children: [
@@ -3606,17 +3610,193 @@ function FileTreeEditor2({
3606
3610
  ] })
3607
3611
  ] });
3608
3612
  }
3613
+ function TabButton({ label, active, onClick }) {
3614
+ return /* @__PURE__ */ jsxs(
3615
+ "button",
3616
+ {
3617
+ onClick,
3618
+ className: `relative pb-2 text-sm font-medium transition-colors ${active ? "text-foreground" : "text-muted-foreground hover:text-foreground"}`,
3619
+ children: [
3620
+ label,
3621
+ active && /* @__PURE__ */ jsx("span", { className: "absolute inset-x-0 bottom-0 h-0.5 bg-foreground rounded-full" })
3622
+ ]
3623
+ }
3624
+ );
3625
+ }
3626
+ function ImportSkillDialog({ open, onOpenChange, onImported, existingFolders }) {
3627
+ const client = useAgentPlaneClient();
3628
+ const [tab, setTab] = useState("all");
3629
+ const [entries, setEntries] = useState([]);
3630
+ const [loading, setLoading] = useState(false);
3631
+ const [error, setError] = useState("");
3632
+ const [search, setSearch] = useState("");
3633
+ const [selected, setSelected] = useState(null);
3634
+ const [preview, setPreview] = useState("");
3635
+ const [previewLoading, setPreviewLoading] = useState(false);
3636
+ const [importing, setImporting] = useState(false);
3637
+ const [importError, setImportError] = useState("");
3638
+ const [url, setUrl] = useState("");
3639
+ const [urlImporting, setUrlImporting] = useState(false);
3640
+ useEffect(() => {
3641
+ if (!open) return;
3642
+ setLoading(true);
3643
+ setError("");
3644
+ setSelected(null);
3645
+ setPreview("");
3646
+ client.skillsDirectory.list(tab).then((data) => setEntries(data)).catch((err) => setError(err instanceof Error ? err.message : "Failed to load skills")).finally(() => setLoading(false));
3647
+ }, [tab, open, client]);
3648
+ const filtered = useMemo(() => {
3649
+ if (!search.trim()) return entries;
3650
+ const q = search.toLowerCase();
3651
+ return entries.filter(
3652
+ (e) => e.name.toLowerCase().includes(q) || e.owner.toLowerCase().includes(q) || e.repo.toLowerCase().includes(q)
3653
+ );
3654
+ }, [entries, search]);
3655
+ async function handleSelect(entry) {
3656
+ setSelected(entry);
3657
+ setPreviewLoading(true);
3658
+ setPreview("");
3659
+ setImportError("");
3660
+ try {
3661
+ const content = await client.skillsDirectory.preview(entry.owner, entry.repo, entry.skill);
3662
+ setPreview(content);
3663
+ } catch (err) {
3664
+ setPreview(`Failed to load preview: ${err instanceof Error ? err.message : "Unknown error"}`);
3665
+ } finally {
3666
+ setPreviewLoading(false);
3667
+ }
3668
+ }
3669
+ async function handleImport() {
3670
+ if (!selected) return;
3671
+ setImporting(true);
3672
+ setImportError("");
3673
+ try {
3674
+ const result = await client.skillsDirectory.import({
3675
+ owner: selected.owner,
3676
+ repo: selected.repo,
3677
+ skill_name: selected.skill
3678
+ });
3679
+ if (existingFolders.includes(result.folder)) {
3680
+ setImportError(`Skill folder "${result.folder}" already exists. Remove it first or rename.`);
3681
+ return;
3682
+ }
3683
+ if (result.warnings.length > 0) {
3684
+ setImportError(`Imported with warnings: ${result.warnings.join("; ")}`);
3685
+ }
3686
+ onImported({ folder: result.folder, files: result.files });
3687
+ onOpenChange(false);
3688
+ resetState();
3689
+ } catch (err) {
3690
+ setImportError(err instanceof Error ? err.message : "Failed to import skill");
3691
+ } finally {
3692
+ setImporting(false);
3693
+ }
3694
+ }
3695
+ async function handleUrlImport() {
3696
+ if (!url.trim()) return;
3697
+ setUrlImporting(true);
3698
+ setImportError("");
3699
+ try {
3700
+ const result = await client.skillsDirectory.import({ url: url.trim() });
3701
+ if (existingFolders.includes(result.folder)) {
3702
+ setImportError(`Skill folder "${result.folder}" already exists. Remove it first or rename.`);
3703
+ return;
3704
+ }
3705
+ onImported({ folder: result.folder, files: result.files });
3706
+ onOpenChange(false);
3707
+ resetState();
3708
+ } catch (err) {
3709
+ setImportError(err instanceof Error ? err.message : "Failed to import skill");
3710
+ } finally {
3711
+ setUrlImporting(false);
3712
+ }
3713
+ }
3714
+ function resetState() {
3715
+ setTab("all");
3716
+ setSearch("");
3717
+ setSelected(null);
3718
+ setPreview("");
3719
+ setUrl("");
3720
+ setError("");
3721
+ setImportError("");
3722
+ }
3723
+ return /* @__PURE__ */ jsx(Dialog, { open, onOpenChange: (v) => {
3724
+ onOpenChange(v);
3725
+ if (!v) resetState();
3726
+ }, children: /* @__PURE__ */ jsxs(DialogContent, { className: "max-w-2xl max-h-[80vh] flex flex-col", children: [
3727
+ /* @__PURE__ */ jsxs(DialogHeader, { children: [
3728
+ /* @__PURE__ */ jsx(DialogTitle, { children: "Import from skills.sh" }),
3729
+ /* @__PURE__ */ jsx(DialogDescription, { children: "Browse the open skills directory and import skills into this agent." })
3730
+ ] }),
3731
+ /* @__PURE__ */ jsxs(DialogBody, { className: "flex-1 overflow-hidden flex flex-col gap-4", children: [
3732
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-4", children: [
3733
+ /* @__PURE__ */ jsx(TabButton, { label: "All Time", active: tab === "all", onClick: () => setTab("all") }),
3734
+ /* @__PURE__ */ jsx(TabButton, { label: "Trending", active: tab === "trending", onClick: () => setTab("trending") }),
3735
+ /* @__PURE__ */ jsx(TabButton, { label: "Hot", active: tab === "hot", onClick: () => setTab("hot") })
3736
+ ] }),
3737
+ /* @__PURE__ */ jsx(Input, { placeholder: "Search skills...", value: search, onChange: (e) => setSearch(e.target.value) }),
3738
+ error && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: error }),
3739
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-hidden flex gap-4 min-h-0", children: [
3740
+ /* @__PURE__ */ jsx("div", { className: "w-1/2 overflow-y-auto border border-muted-foreground/25 rounded-lg", children: loading ? /* @__PURE__ */ jsx("div", { className: "p-4 text-sm text-muted-foreground", children: "Loading skills..." }) : filtered.length === 0 ? /* @__PURE__ */ jsx("div", { className: "p-4 text-sm text-muted-foreground", children: search ? "No skills match your search" : "No skills found" }) : filtered.map((entry) => /* @__PURE__ */ jsxs(
3741
+ "button",
3742
+ {
3743
+ onClick: () => handleSelect(entry),
3744
+ className: `w-full text-left px-3 py-2 border-b border-muted-foreground/10 hover:bg-muted/50 transition-colors ${selected?.skill === entry.skill && selected?.owner === entry.owner ? "bg-muted/50" : ""}`,
3745
+ children: [
3746
+ /* @__PURE__ */ jsx("div", { className: "font-medium text-sm truncate", children: entry.name }),
3747
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-between items-center", children: [
3748
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground truncate", children: [
3749
+ entry.owner,
3750
+ "/",
3751
+ entry.repo
3752
+ ] }),
3753
+ /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground font-mono ml-2 shrink-0", children: entry.installs })
3754
+ ] })
3755
+ ]
3756
+ },
3757
+ `${entry.owner}/${entry.repo}/${entry.skill}`
3758
+ )) }),
3759
+ /* @__PURE__ */ jsx("div", { className: "w-1/2 overflow-y-auto border border-muted-foreground/25 rounded-lg p-3", children: !selected ? /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Select a skill to preview" }) : previewLoading ? /* @__PURE__ */ jsx("div", { className: "text-sm text-muted-foreground", children: "Loading preview..." }) : /* @__PURE__ */ jsx("pre", { className: "text-xs whitespace-pre-wrap break-words font-mono", children: preview }) })
3760
+ ] }),
3761
+ importError && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: importError }),
3762
+ /* @__PURE__ */ jsx("div", { className: "border-t border-muted-foreground/25 pt-3", children: /* @__PURE__ */ jsx(FormField, { label: "Or paste a skills.sh URL", children: /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
3763
+ /* @__PURE__ */ jsx(
3764
+ Input,
3765
+ {
3766
+ value: url,
3767
+ onChange: (e) => setUrl(e.target.value),
3768
+ placeholder: "skills.sh/owner/repo/skill",
3769
+ onKeyDown: (e) => e.key === "Enter" && handleUrlImport(),
3770
+ className: "flex-1"
3771
+ }
3772
+ ),
3773
+ /* @__PURE__ */ jsx(Button, { size: "sm", variant: "outline", onClick: handleUrlImport, disabled: urlImporting || !url.trim(), children: urlImporting ? "Importing..." : "Import URL" })
3774
+ ] }) }) })
3775
+ ] }),
3776
+ /* @__PURE__ */ jsxs(DialogFooter, { children: [
3777
+ /* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: () => onOpenChange(false), children: "Cancel" }),
3778
+ /* @__PURE__ */ jsx(Button, { size: "sm", onClick: handleImport, disabled: !selected || importing || previewLoading, children: importing ? "Importing..." : "Import Selected" })
3779
+ ] })
3780
+ ] }) });
3781
+ }
3609
3782
  function AgentSkillManager({ agentId, initialSkills, onSaved }) {
3610
3783
  const client = useAgentPlaneClient();
3784
+ const [importOpen, setImportOpen] = useState(false);
3785
+ const [extraSkills, setExtraSkills] = useState([]);
3786
+ const allSkills = useMemo(() => [...initialSkills, ...extraSkills], [initialSkills, extraSkills]);
3611
3787
  const initialFiles = useMemo(
3612
- () => initialSkills.flatMap(
3788
+ () => allSkills.flatMap(
3613
3789
  (s) => s.files.map((f) => ({
3614
3790
  path: s.folder === "(root)" ? f.path : `${s.folder}/${f.path}`,
3615
3791
  content: f.content
3616
3792
  }))
3617
3793
  ),
3618
- [initialSkills]
3794
+ [allSkills]
3619
3795
  );
3796
+ const existingFolders = useMemo(() => allSkills.map((s) => s.folder), [allSkills]);
3797
+ const handleImported = useCallback((skill) => {
3798
+ setExtraSkills((prev) => [...prev, skill]);
3799
+ }, []);
3620
3800
  const handleSave = useCallback(async (files) => {
3621
3801
  const folderMap = /* @__PURE__ */ new Map();
3622
3802
  for (const file of files) {
@@ -3635,22 +3815,35 @@ function AgentSkillManager({ agentId, initialSkills, onSaved }) {
3635
3815
  }
3636
3816
  const skills = Array.from(folderMap.entries()).map(([folder, files2]) => ({ folder, files: files2 }));
3637
3817
  await client.agents.update(agentId, { skills });
3818
+ setExtraSkills([]);
3638
3819
  onSaved?.();
3639
3820
  }, [agentId, client, onSaved]);
3640
- return /* @__PURE__ */ jsx(
3641
- FileTreeEditor2,
3642
- {
3643
- initialFiles,
3644
- onSave: handleSave,
3645
- title: "Skills",
3646
- saveLabel: "Save Skills",
3647
- addFolderLabel: "Skill",
3648
- newFileTemplate: {
3649
- filename: "SKILL.md",
3650
- content: "---\nname: New Skill\ndescription: Describe when this skill should be triggered\n---\n\n# Instructions\n\nDescribe what this skill does...\n"
3821
+ return /* @__PURE__ */ jsxs("div", { children: [
3822
+ /* @__PURE__ */ jsx(
3823
+ FileTreeEditor2,
3824
+ {
3825
+ initialFiles,
3826
+ onSave: handleSave,
3827
+ title: "Skills",
3828
+ saveLabel: "Save Skills",
3829
+ addFolderLabel: "Skill",
3830
+ newFileTemplate: {
3831
+ filename: "SKILL.md",
3832
+ content: "---\nname: New Skill\ndescription: Describe when this skill should be triggered\n---\n\n# Instructions\n\nDescribe what this skill does...\n"
3833
+ },
3834
+ headerActions: /* @__PURE__ */ jsx(Button, { size: "sm", variant: "outline", onClick: () => setImportOpen(true), children: "Import from skills.sh" })
3651
3835
  }
3652
- }
3653
- );
3836
+ ),
3837
+ /* @__PURE__ */ jsx(
3838
+ ImportSkillDialog,
3839
+ {
3840
+ open: importOpen,
3841
+ onOpenChange: setImportOpen,
3842
+ onImported: handleImported,
3843
+ existingFolders
3844
+ }
3845
+ )
3846
+ ] });
3654
3847
  }
3655
3848
  function AgentPluginManager({ agentId, initialPlugins, onSaved }) {
3656
3849
  const client = useAgentPlaneClient();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getcatalystiq/agent-plane-ui",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "Embeddable React component library for AgentPlane",
5
5
  "type": "module",
6
6
  "exports": {