@agentprojectcontext/apx 1.58.0 → 1.60.0

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.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/src/core/agent/skills/index.js +9 -0
  3. package/src/core/agent/skills/inspector.js +7 -2
  4. package/src/core/agent/skills/policy.js +128 -0
  5. package/src/core/agent/super-agent.js +8 -1
  6. package/src/core/agent/tools/handlers/_asana.js +34 -0
  7. package/src/core/agent/tools/handlers/asana-create-task.js +38 -0
  8. package/src/core/agent/tools/handlers/asana-list-projects.js +19 -0
  9. package/src/core/agent/tools/handlers/asana-list-tasks.js +27 -0
  10. package/src/core/agent/tools/handlers/asana-update-task.js +32 -0
  11. package/src/core/agent/tools/handlers/list-skills.js +6 -3
  12. package/src/core/agent/tools/handlers/load-skill.js +9 -2
  13. package/src/core/agent/tools/names.js +10 -0
  14. package/src/core/agent/tools/registry.js +11 -0
  15. package/src/core/integrations/catalog.js +66 -0
  16. package/src/core/integrations/index.js +10 -0
  17. package/src/core/integrations/plugins/asana.js +231 -0
  18. package/src/core/integrations/sources.js +56 -0
  19. package/src/core/integrations/store.js +118 -0
  20. package/src/host/daemon/api/integrations.js +191 -0
  21. package/src/host/daemon/api/skills.js +301 -17
  22. package/src/host/daemon/api.js +2 -0
  23. package/src/interfaces/cli/commands/skills.js +3 -2
  24. package/src/interfaces/web/dist/assets/index-DFNV6BWh.js +761 -0
  25. package/src/interfaces/web/dist/assets/{index-DPAuXATr.js.map → index-DFNV6BWh.js.map} +1 -1
  26. package/src/interfaces/web/dist/assets/index-HU-Wt2l9.css +1 -0
  27. package/src/interfaces/web/dist/index.html +2 -2
  28. package/src/interfaces/web/src/components/integrations/AsanaPlugin.tsx +275 -0
  29. package/src/interfaces/web/src/components/integrations/ComingSoonPlugin.tsx +44 -0
  30. package/src/interfaces/web/src/components/integrations/PluginCard.tsx +61 -0
  31. package/src/interfaces/web/src/components/integrations/PluginToolsSection.tsx +39 -0
  32. package/src/interfaces/web/src/components/settings/SkillsManager.tsx +465 -0
  33. package/src/interfaces/web/src/components/settings/SkillsSettings.tsx +49 -0
  34. package/src/interfaces/web/src/i18n/en.ts +67 -0
  35. package/src/interfaces/web/src/i18n/es.ts +67 -0
  36. package/src/interfaces/web/src/lib/api/integrations.ts +106 -0
  37. package/src/interfaces/web/src/lib/api/skills.ts +79 -8
  38. package/src/interfaces/web/src/lib/api.ts +1 -0
  39. package/src/interfaces/web/src/screens/ProjectScreen.tsx +12 -4
  40. package/src/interfaces/web/src/screens/SettingsScreen.tsx +3 -3
  41. package/src/interfaces/web/src/screens/project/IntegrationsTab.tsx +146 -0
  42. package/src/interfaces/web/src/screens/project/SkillsTab.tsx +13 -0
  43. package/src/interfaces/web/dist/assets/index-Cl0WXtxF.css +0 -1
  44. package/src/interfaces/web/dist/assets/index-DPAuXATr.js +0 -705
@@ -0,0 +1,106 @@
1
+ import { http } from "../http";
2
+
3
+ // Where an integration record is stored. "global" targets the default project's
4
+ // store (shared across projects); "project" targets the current project. A
5
+ // project uses its own record when present, otherwise the global one.
6
+ export type IntegrationScope = "project" | "global";
7
+
8
+ // Status returned by a plugin's status endpoint. Common fields plus
9
+ // plugin-specific extras (Asana adds user/workspace metadata).
10
+ export interface IntegrationStatus {
11
+ slug: string;
12
+ status: string;
13
+ is_enabled: boolean;
14
+ user_name?: string | null;
15
+ user_email?: string | null;
16
+ workspace_gid?: string | null;
17
+ workspace_name?: string | null;
18
+ }
19
+
20
+ export interface PluginTool {
21
+ slug: string;
22
+ desc: string;
23
+ }
24
+
25
+ // One entry of the plugin catalog with its resolved status for this project.
26
+ export interface CatalogEntry {
27
+ slug: string;
28
+ name: string;
29
+ type: string;
30
+ description: string;
31
+ auth: string;
32
+ tools?: PluginTool[];
33
+ coming_soon: boolean;
34
+ status: IntegrationStatus;
35
+ resolved_scope: IntegrationScope | null;
36
+ }
37
+
38
+ // A stored integration record (secrets redacted; `<key>_set` booleans instead).
39
+ export interface IntegrationRecord {
40
+ slug: string;
41
+ name: string;
42
+ type: string;
43
+ description: string;
44
+ source: string;
45
+ status: string;
46
+ is_enabled: boolean;
47
+ config: Record<string, unknown>;
48
+ created_at: string;
49
+ updated_at: string;
50
+ }
51
+
52
+ export interface AsanaConfigureBody {
53
+ personalAccessToken?: string;
54
+ workspaceGid?: string;
55
+ }
56
+
57
+ export interface AsanaValidateResult {
58
+ ok: boolean;
59
+ user_name?: string | null;
60
+ user_email?: string | null;
61
+ workspace_gid?: string | null;
62
+ workspace_name?: string | null;
63
+ error?: string;
64
+ }
65
+
66
+ export interface AsanaWorkspaces {
67
+ workspaces: { gid: string; name: string }[];
68
+ }
69
+
70
+ const q = (scope: IntegrationScope) => `?scope=${scope}`;
71
+
72
+ export const Integrations = {
73
+ catalog: (pid: string) =>
74
+ http.get<CatalogEntry[]>(`/projects/${pid}/integrations/catalog`),
75
+
76
+ list: (pid: string, scope: IntegrationScope = "project") =>
77
+ http.get<IntegrationRecord[]>(`/projects/${pid}/integrations${q(scope)}`),
78
+
79
+ status: (pid: string, slug: string, scope: IntegrationScope = "project") =>
80
+ http.get<IntegrationStatus>(`/projects/${pid}/integrations/${slug}${q(scope)}`),
81
+
82
+ configure: (pid: string, slug: string, scope: IntegrationScope, body: Record<string, unknown>) =>
83
+ http.post<IntegrationRecord>(`/projects/${pid}/integrations/${slug}/configure${q(scope)}`, body),
84
+
85
+ validate: (pid: string, slug: string, scope: IntegrationScope = "project") =>
86
+ http.post<AsanaValidateResult>(`/projects/${pid}/integrations/${slug}/validate${q(scope)}`, {}),
87
+
88
+ deactivate: (pid: string, slug: string, scope: IntegrationScope = "project") =>
89
+ http.post<IntegrationStatus>(`/projects/${pid}/integrations/${slug}/deactivate${q(scope)}`, {}),
90
+
91
+ action: <T>(pid: string, slug: string, action: string, scope: IntegrationScope = "project") =>
92
+ http.post<T>(`/projects/${pid}/integrations/${slug}/action/${action}${q(scope)}`, {}),
93
+
94
+ remove: (pid: string, slug: string, scope: IntegrationScope = "project") =>
95
+ http.del<void>(`/projects/${pid}/integrations/${slug}${q(scope)}`),
96
+
97
+ // ── Asana convenience wrappers ──────────────────────────────────────────────
98
+ asanaConfigure: (pid: string, scope: IntegrationScope, body: AsanaConfigureBody) =>
99
+ Integrations.configure(pid, "asana", scope, {
100
+ personal_access_token: body.personalAccessToken,
101
+ workspace_gid: body.workspaceGid,
102
+ }),
103
+ asanaValidate: (pid: string, scope: IntegrationScope) => Integrations.validate(pid, "asana", scope),
104
+ asanaWorkspaces: (pid: string, scope: IntegrationScope) =>
105
+ Integrations.action<AsanaWorkspaces>(pid, "asana", "workspaces", scope),
106
+ };
@@ -1,16 +1,44 @@
1
1
  import { http } from "../http";
2
2
 
3
+ export type SkillSource = "builtin" | "global" | "project" | string;
4
+
3
5
  export type SkillEntry = {
4
6
  slug: string;
5
- source: "bundled" | "user" | "project" | string;
7
+ source: SkillSource;
6
8
  description: string;
9
+ /** Effective enabled state for the requested scope. */
10
+ enabled?: boolean;
11
+ /** Built-in APX skill — always active, never disableable. */
12
+ private?: boolean;
13
+ /** Whether THIS scope holds an explicit override (vs. inherited). */
14
+ overridden?: boolean;
7
15
  };
8
16
 
9
17
  export type SkillsList = {
10
18
  count: number;
19
+ /** Echoed scope key: "default" (super-agent) or a project path. */
20
+ scope?: string;
11
21
  skills: SkillEntry[];
12
22
  };
13
23
 
24
+ export interface SkillDetail {
25
+ slug: string;
26
+ source: SkillSource;
27
+ description: string;
28
+ frontmatter: Record<string, string>;
29
+ body: string;
30
+ file: string;
31
+ enabled: boolean;
32
+ private: boolean;
33
+ overridden: boolean;
34
+ }
35
+
36
+ export interface CreateResult {
37
+ ok: boolean;
38
+ slug: string;
39
+ source: string;
40
+ }
41
+
14
42
  export interface InspectorConfig {
15
43
  enabled: boolean;
16
44
  load_threshold: number;
@@ -62,16 +90,59 @@ export interface InspectResult {
62
90
 
63
91
  export const Skills = {
64
92
  /**
65
- * List installed skills (bundled + user + optional project-scoped). The
66
- * description is already condensed for one-line rendering.
93
+ * List installed skills (built-in + user + optional project-scoped), each
94
+ * annotated with `enabled`/`private` for the requested scope. Pass a project
95
+ * path to both scan that project's skills AND resolve enabled-state against
96
+ * it; omit it for the super-agent ("default") scope.
67
97
  */
68
- list: (projectPath?: string) =>
69
- http.get<SkillsList>(
70
- projectPath
71
- ? `/skills?project_path=${encodeURIComponent(projectPath)}`
72
- : "/skills",
98
+ list: (projectPath?: string) => {
99
+ const qs = new URLSearchParams();
100
+ if (projectPath) {
101
+ qs.set("project_path", projectPath);
102
+ qs.set("scope", projectPath);
103
+ }
104
+ const q = qs.toString();
105
+ return http.get<SkillsList>(q ? `/skills?${q}` : "/skills");
106
+ },
107
+
108
+ /** Full body + frontmatter of a skill, for the viewer. */
109
+ detail: (slug: string, projectPath?: string) => {
110
+ const qs = projectPath ? `?project_path=${encodeURIComponent(projectPath)}` : "";
111
+ return http.get<SkillDetail>(`/skills/${encodeURIComponent(slug)}/detail${qs}`);
112
+ },
113
+
114
+ /**
115
+ * Enable/disable a skill for a scope. `enabled: null` clears the override so
116
+ * the skill inherits the super-agent default again. `scope` is "default" or a
117
+ * project path.
118
+ */
119
+ setEnabled: (body: { slug: string; enabled: boolean | null; scope?: string }) =>
120
+ http.put<{ ok: boolean; slug: string; scope: string; enabled: boolean | null }>(
121
+ "/skills/enabled",
122
+ body,
73
123
  ),
74
124
 
125
+ /**
126
+ * Create a user skill from the online editor. With `project_path` it lands in
127
+ * that project's .apc/skills/; otherwise in ~/.apx/skills/.
128
+ */
129
+ create: (body: { slug: string; description?: string; body?: string; project_path?: string }) =>
130
+ http.post<CreateResult>("/skills", body),
131
+
132
+ /** Import a skill from an uploaded .zip (base64-encoded). */
133
+ importZip: (body: { data: string; project_path?: string }) =>
134
+ http.post<CreateResult>("/skills/import/zip", body),
135
+
136
+ /** Import a skill by cloning a git repo. */
137
+ importRepo: (body: { url: string; project_path?: string }) =>
138
+ http.post<CreateResult>("/skills/import/repo", body),
139
+
140
+ /** Delete a user skill (global or project-scoped). */
141
+ remove: (slug: string, projectPath?: string) => {
142
+ const qs = projectPath ? `?project_path=${encodeURIComponent(projectPath)}` : "";
143
+ return http.del<{ ok: boolean; slug: string }>(`/skills/${encodeURIComponent(slug)}${qs}`);
144
+ },
145
+
75
146
  /** Skill Inspector config + index status. */
76
147
  inspector: () => http.get<InspectorState>("/skills/inspector"),
77
148
 
@@ -9,6 +9,7 @@ export * from "./api/conversations";
9
9
  export * from "./api/routines";
10
10
  export * from "./api/tasks";
11
11
  export * from "./api/mcps";
12
+ export * from "./api/integrations";
12
13
  export * from "./api/vars";
13
14
  export * from "./api/messages";
14
15
  export * from "./api/sessions";
@@ -3,8 +3,8 @@ import { useParams, Routes, Route, Navigate, useLocation, useNavigate } from "re
3
3
  import {
4
4
  Bot, Heart, Zap, Puzzle, FolderKanban, Settings,
5
5
  MessagesSquare, Send, KeyRound,
6
- LayoutDashboard, Boxes, Cpu, ScrollText, History, Brain, FileCode2,
7
- Building2, FileText, FolderTree,
6
+ LayoutDashboard, Boxes, Cpu, ScrollText, History, Brain, FileCode2, Cable,
7
+ Building2, FileText, FolderTree, Sparkles,
8
8
  } from "lucide-react";
9
9
  import { useNavCollapse, type TabSection } from "../components/common/TabNav";
10
10
  import { TabLayout } from "../components/common/TabLayout";
@@ -25,6 +25,7 @@ import { AgentsTab } from "./project/AgentsTab";
25
25
  import { RoutinesTab } from "./project/RoutinesTab";
26
26
  import { TasksTab } from "./project/TasksTab";
27
27
  import { McpsTab } from "./project/McpsTab";
28
+ import { IntegrationsTab } from "./project/IntegrationsTab";
28
29
  import { VarsTab } from "./project/VarsTab";
29
30
  import { ChatTab } from "./project/ChatTab";
30
31
  import { TelegramTab } from "./project/TelegramTab";
@@ -34,11 +35,12 @@ import { AgentDetailScreen } from "./project/AgentDetailScreen";
34
35
  import { StructureTab } from "./project/StructureTab";
35
36
  import { DocsTab } from "./project/DocsTab";
36
37
  import { FilesTab } from "./project/FilesTab";
38
+ import { SkillsTab } from "./project/SkillsTab";
37
39
 
38
40
  type NavKey =
39
41
  | "" | "chat" | "config" | "telegram"
40
- | "agents" | "routines" | "tasks" | "mcps" | "vars" | "logs" | "memories" | "artifacts"
41
- | "structure" | "docs" | "files";
42
+ | "agents" | "routines" | "tasks" | "mcps" | "integrations" | "vars" | "logs" | "memories" | "artifacts"
43
+ | "structure" | "docs" | "files" | "skills";
42
44
 
43
45
  export function ProjectScreen() {
44
46
  const navigate = useNavigate();
@@ -75,8 +77,10 @@ export function ProjectScreen() {
75
77
  items: [
76
78
  { key: "agents", label: t("project.nav.agents"), icon: Bot },
77
79
  { key: "memories", label: t("project.nav.memories"), icon: Brain },
80
+ { key: "skills", label: t("skills_page.title"), icon: Sparkles },
78
81
  { key: "routines", label: t("project.nav.routines"), icon: Heart },
79
82
  { key: "mcps", label: t("project.nav.mcps"), icon: Puzzle },
83
+ { key: "integrations", label: "Integrations", icon: Cable },
80
84
  { key: "vars", label: t("project.nav.vars"), icon: KeyRound },
81
85
  { key: "artifacts", label: t("project.nav.artifacts"), icon: FileCode2 },
82
86
  { key: "config", label: t("project.nav.config"), icon: Settings },
@@ -97,6 +101,7 @@ export function ProjectScreen() {
97
101
  { key: "agents", label: t("project.nav.agents"), icon: Bot },
98
102
  ...(isCompany ? [{ key: "structure", label: t("project.nav.structure"), icon: Building2 }] : []),
99
103
  { key: "memories", label: t("project.nav.memories"), icon: Brain },
104
+ { key: "skills", label: t("skills_page.title"), icon: Sparkles },
100
105
  ],
101
106
  },
102
107
  {
@@ -112,6 +117,7 @@ export function ProjectScreen() {
112
117
  { key: "routines", label: t("project.nav.routines"), icon: Heart },
113
118
  { key: "tasks", label: t("project.nav.tasks"), icon: Zap },
114
119
  { key: "mcps", label: t("project.nav.mcps"), icon: Puzzle },
120
+ { key: "integrations", label: "Integrations", icon: Cable },
115
121
  { key: "vars", label: t("project.nav.vars"), icon: KeyRound },
116
122
  { key: "artifacts", label: t("project.nav.artifacts"), icon: FileCode2 },
117
123
  { key: "logs", label: t("project.nav.logs"), icon: ScrollText },
@@ -175,9 +181,11 @@ export function ProjectScreen() {
175
181
  <Route path="docs" element={<DocsTab pid={pid} />} />
176
182
  <Route path="files" element={<FilesTab pid={pid} />} />
177
183
  <Route path="memories" element={<MemoriesTab pid={pid} />} />
184
+ <Route path="skills" element={<SkillsTab pid={pid} />} />
178
185
  <Route path="routines" element={<RoutinesTab pid={pid} />} />
179
186
  <Route path="tasks" element={isBase ? <GlobalTasksTab /> : <TasksTab pid={pid} />} />
180
187
  <Route path="mcps" element={<McpsTab pid={pid} />} />
188
+ <Route path="integrations" element={<IntegrationsTab pid={pid} />} />
181
189
  <Route path="artifacts" element={<ArtifactsTab pid={pid} />} />
182
190
  <Route path="vars" element={<VarsTab pid={pid} />} />
183
191
  <Route path="threads" element={<Navigate to={`/p/${pid}/chat`} replace />} />
@@ -8,7 +8,7 @@ import { TabLayout } from "../components/common/TabLayout";
8
8
  import { IdentityPanel } from "../components/settings/IdentityPanel";
9
9
  import { SuperAgentPanel } from "../components/settings/SuperAgentPanel";
10
10
  import { MemoryPanel } from "../components/settings/MemoryPanel";
11
- import { SkillsInspectorPanel } from "../components/settings/SkillsInspectorPanel";
11
+ import { SkillsSettings } from "../components/settings/SkillsSettings";
12
12
  import { ModelsTab } from "./base/ModelsTab";
13
13
  import { TelegramSettingsTabs } from "../components/settings/TelegramSettingsTabs";
14
14
  import { DevicesPanel } from "../components/settings/DevicesPanel";
@@ -37,7 +37,7 @@ const SECTIONS: TabSection[] = [
37
37
  { key: "super_agent", label: t("settings.tabs.super_agent"), icon: Bot },
38
38
  { key: "engines", label: t("settings.tabs.engines"), icon: Cpu },
39
39
  { key: "memory", label: "Memory (RAG)", icon: Database },
40
- { key: "skills", label: "Skills (RAG)", icon: Sparkles },
40
+ { key: "skills", label: t("skills_page.title"), icon: Sparkles },
41
41
  ],
42
42
  },
43
43
  {
@@ -75,7 +75,7 @@ const PANELS: Record<TabKey, () => ReactElement> = {
75
75
  super_agent: () => <SuperAgentPanel />,
76
76
  engines: () => <ModelsTab />,
77
77
  memory: () => <MemoryPanel />,
78
- skills: () => <SkillsInspectorPanel />,
78
+ skills: () => <SkillsSettings />,
79
79
  telegram: () => <TelegramSettingsTabs />,
80
80
  devices: () => <DevicesPanel />,
81
81
  voice: () => <VoiceScreen />,
@@ -0,0 +1,146 @@
1
+ import { useState } from "react";
2
+ import useSWR from "swr";
3
+ import { Network, Puzzle, Wrench } from "lucide-react";
4
+ import { Integrations, type CatalogEntry, type IntegrationScope } from "../../lib/api";
5
+ import { cn } from "../../lib/cn";
6
+ import { Section } from "../../components/Section";
7
+ import { Empty, Loading } from "../../components/ui";
8
+ import { AsanaPlugin } from "../../components/integrations/AsanaPlugin";
9
+ import { ComingSoonPlugin } from "../../components/integrations/ComingSoonPlugin";
10
+ import { McpsTab } from "./McpsTab";
11
+
12
+ type SubTab = "plugins" | "mcp" | "tools";
13
+
14
+ const SUBTABS: { value: SubTab; label: string; icon: typeof Puzzle }[] = [
15
+ { value: "plugins", label: "Plugins", icon: Puzzle },
16
+ { value: "mcp", label: "MCP Servers", icon: Network },
17
+ { value: "tools", label: "Tools", icon: Wrench },
18
+ ];
19
+
20
+ // Renders the live plugin (Asana) or a coming-soon placeholder per catalog entry.
21
+ function PluginRow({ pid, scope, entry }: { pid: string; scope: IntegrationScope; entry: CatalogEntry }) {
22
+ if (entry.coming_soon) return <ComingSoonPlugin entry={entry} />;
23
+ if (entry.slug === "asana") return <AsanaPlugin pid={pid} scope={scope} />;
24
+ // Implemented plugin without a bespoke UI yet — fall back to the placeholder.
25
+ return <ComingSoonPlugin entry={entry} />;
26
+ }
27
+
28
+ function PluginsSection({ pid, scope }: { pid: string; scope: IntegrationScope }) {
29
+ const { data: catalog, isLoading } = useSWR(`integrations-catalog-${pid}`, () => Integrations.catalog(pid));
30
+
31
+ return (
32
+ <div className="space-y-3">
33
+ <p className="text-xs text-muted-foreground">
34
+ Plugins de canal y servicio instalables por proyecto. Se guardan en el ámbito
35
+ seleccionado arriba.
36
+ </p>
37
+ {isLoading && <Loading />}
38
+ {(catalog || []).map((entry) => (
39
+ <PluginRow key={entry.slug} pid={pid} scope={scope} entry={entry} />
40
+ ))}
41
+ <div className="rounded-xl border border-dashed border-border p-6 text-center">
42
+ <p className="text-sm text-muted-foreground">Más plugins próximamente…</p>
43
+ </div>
44
+ </div>
45
+ );
46
+ }
47
+
48
+ function ToolsSection({ pid }: { pid: string }) {
49
+ const { data: catalog } = useSWR(`integrations-catalog-${pid}`, () => Integrations.catalog(pid));
50
+ const rows = (catalog || [])
51
+ .filter((c) => !c.coming_soon && (c.tools?.length ?? 0) > 0)
52
+ .flatMap((c) => (c.tools || []).map((t) => ({ ...t, plugin: c.name, active: c.status.is_enabled })));
53
+
54
+ return (
55
+ <div className="space-y-3">
56
+ <p className="text-xs text-muted-foreground">
57
+ Tools que los plugins conectados exponen a los agentes de este proyecto.
58
+ </p>
59
+ {rows.length === 0 ? (
60
+ <Empty>No hay tools de integraciones. Conectá un plugin para habilitarlas.</Empty>
61
+ ) : (
62
+ <ul className="space-y-2">
63
+ {rows.map((t) => (
64
+ <li key={t.slug} className={cn("rounded-md border border-border bg-muted/30 px-3 py-2", !t.active && "opacity-55")}>
65
+ <div className="flex items-center gap-2">
66
+ <Wrench className="h-3.5 w-3.5 text-muted-foreground" />
67
+ <span className="font-mono text-xs text-foreground">{t.slug}</span>
68
+ <span className="ml-auto text-[10px] text-muted-foreground">{t.plugin}</span>
69
+ <span
70
+ className={cn(
71
+ "rounded border px-1.5 py-0.5 text-[10px]",
72
+ t.active
73
+ ? "border-emerald-700/40 bg-emerald-900/20 text-emerald-400"
74
+ : "border-border bg-muted text-muted-foreground",
75
+ )}
76
+ >
77
+ {t.active ? "activo" : "inactivo"}
78
+ </span>
79
+ </div>
80
+ <p className="mt-0.5 pl-5 text-[10px] text-muted-foreground">{t.desc}</p>
81
+ </li>
82
+ ))}
83
+ </ul>
84
+ )}
85
+ </div>
86
+ );
87
+ }
88
+
89
+ export function IntegrationsTab({ pid }: { pid: string }) {
90
+ const isBase = String(pid) === "0";
91
+ const [tab, setTab] = useState<SubTab>("plugins");
92
+ // On the base project, project scope IS the global (default) store.
93
+ const [scope, setScope] = useState<IntegrationScope>(isBase ? "global" : "project");
94
+
95
+ return (
96
+ <Section
97
+ title="Integrations"
98
+ description="Plugins, MCP servers y tools disponibles para este proyecto"
99
+ >
100
+ {/* Scope selector — a real project can use its own integrations or the
101
+ global (default-project) ones. */}
102
+ {!isBase && (
103
+ <div className="mb-4 flex items-center gap-2">
104
+ <span className="text-xs text-muted-foreground">Ámbito:</span>
105
+ {(["project", "global"] as const).map((s) => (
106
+ <button
107
+ key={s}
108
+ onClick={() => setScope(s)}
109
+ className={cn(
110
+ "rounded-md border px-2.5 py-1 text-xs transition-colors",
111
+ scope === s
112
+ ? "border-primary/50 bg-primary/10 text-foreground"
113
+ : "border-border bg-muted/30 text-muted-foreground hover:bg-muted/50",
114
+ )}
115
+ >
116
+ {s === "project" ? "Este proyecto" : "Global (default)"}
117
+ </button>
118
+ ))}
119
+ </div>
120
+ )}
121
+
122
+ {/* Sub-tabs */}
123
+ <div className="mb-4 inline-flex rounded-lg border border-border bg-muted/30 p-0.5">
124
+ {SUBTABS.map((s) => {
125
+ const Icon = s.icon;
126
+ return (
127
+ <button
128
+ key={s.value}
129
+ onClick={() => setTab(s.value)}
130
+ className={cn(
131
+ "flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs transition-colors",
132
+ tab === s.value ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground",
133
+ )}
134
+ >
135
+ <Icon className="h-3.5 w-3.5" /> {s.label}
136
+ </button>
137
+ );
138
+ })}
139
+ </div>
140
+
141
+ {tab === "plugins" && <PluginsSection pid={pid} scope={scope} />}
142
+ {tab === "mcp" && <McpsTab pid={pid} />}
143
+ {tab === "tools" && <ToolsSection pid={pid} />}
144
+ </Section>
145
+ );
146
+ }
@@ -0,0 +1,13 @@
1
+ import { useProject } from "../../hooks/useProjects";
2
+ import { Loading } from "../../components/ui";
3
+ import { SkillsManager } from "../../components/settings/SkillsManager";
4
+
5
+ // Per-project skills view: the Claude-Desktop-style manager locked to THIS
6
+ // project's scope so you can enable/disable and add skills while working in it.
7
+ // The base project (pid "0" = super-agent admin) manages the "default" scope.
8
+ export function SkillsTab({ pid }: { pid: string }) {
9
+ const { project } = useProject(pid);
10
+ const scope = pid === "0" ? "default" : project?.path;
11
+ if (!scope) return <Loading />;
12
+ return <SkillsManager scope={scope} />;
13
+ }