@agentprojectcontext/apx 1.61.0 → 1.62.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.
@@ -18,7 +18,7 @@
18
18
  <link rel="apple-touch-icon" href="/favicon/dark/apple-touch-icon.png" media="(prefers-color-scheme: dark)" />
19
19
  <link rel="manifest" href="/favicon/white/site.webmanifest" media="(prefers-color-scheme: light)" />
20
20
  <link rel="manifest" href="/favicon/dark/site.webmanifest" media="(prefers-color-scheme: dark)" />
21
- <script type="module" crossorigin src="/assets/index-DRFIAiiq.js"></script>
21
+ <script type="module" crossorigin src="/assets/index-CFcs16SV.js"></script>
22
22
  <link rel="stylesheet" crossorigin href="/assets/index-BuII-tAi.css">
23
23
  </head>
24
24
  <body class="bg-background text-foreground antialiased">
@@ -2218,9 +2218,9 @@
2218
2218
  }
2219
2219
  },
2220
2220
  "node_modules/caniuse-lite": {
2221
- "version": "1.0.30001800",
2222
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz",
2223
- "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==",
2221
+ "version": "1.0.30001802",
2222
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001802.tgz",
2223
+ "integrity": "sha512-vmv8ub2xwTNmljSKf82mtCk5JH7hC+YgzLj3P5zotvA0tPQ9016tdNNOG8WRca1IxOnhSsivB+J0z5FeE5LOUw==",
2224
2224
  "dev": true,
2225
2225
  "funding": [
2226
2226
  {
@@ -1,6 +1,7 @@
1
1
  import { useState } from "react";
2
2
  import { Github, MessageCircle, Mic, Puzzle } from "lucide-react";
3
3
  import type { CatalogEntry } from "../../lib/api";
4
+ import { t } from "../../i18n";
4
5
  import { PluginCard } from "./PluginCard";
5
6
 
6
7
  const ICONS: Record<string, { icon: typeof Puzzle; className: string; wrap: string }> = {
@@ -28,17 +29,13 @@ export function ComingSoonPlugin({ entry }: { entry: CatalogEntry }) {
28
29
  description={entry.description}
29
30
  badges={
30
31
  <span className="rounded-full border border-border bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
31
- Próximamente
32
+ {t("integrations.coming_soon")}
32
33
  </span>
33
34
  }
34
35
  expanded={expanded}
35
36
  onToggle={() => setExpanded((v) => !v)}
36
37
  >
37
- <div className="p-4 text-xs text-muted-foreground">
38
- Este plugin está declarado en el catálogo pero todavía no está conectable en APX. La
39
- infraestructura de <span className="font-mono">{entry.slug}</span> se va a portar de forma
40
- nativa en una próxima iteración.
41
- </div>
38
+ <div className="p-4 text-xs text-muted-foreground">{t("integrations.coming_soon_body")}</div>
42
39
  </PluginCard>
43
40
  );
44
41
  }
@@ -3,12 +3,17 @@ import useSWR from "swr";
3
3
  import { AlertCircle, CheckCircle2, ChevronDown, ExternalLink, Eye, EyeOff, Github, Loader2, WifiOff, X } from "lucide-react";
4
4
  import { cn } from "../../lib/cn";
5
5
  import { Integrations, type CatalogEntry, type IntegrationScope, type IntegrationStatus } from "../../lib/api";
6
+ import { t } from "../../i18n";
7
+ import type { TKey } from "../../i18n";
6
8
  import { PluginCard } from "./PluginCard";
7
9
  import { PluginToolsSection } from "./PluginToolsSection";
8
10
 
9
11
  // One generic component that renders any token-based plugin's config form from
10
- // its `ui` descriptor (configFields + optional post-validate select). Asana and
11
- // GitHub share it; each keeps its own independent config + credentials.
12
+ // its `ui` descriptor (structure) + i18n (all display text, keyed by slug). Asana
13
+ // and GitHub share it; each keeps its own independent config + credentials.
14
+
15
+ // Dynamic i18n lookup for computed per-slug keys (t is typed to known keys).
16
+ const tk = (key: string, vars?: Record<string, string | number>) => t(key as TKey, vars);
12
17
 
13
18
  function AsanaLogo({ className }: { className?: string }) {
14
19
  return (
@@ -75,7 +80,7 @@ export function PluginConnect({ pid, scope, entry }: { pid: string; scope: Integ
75
80
  setStep("done");
76
81
  setValues({});
77
82
  } catch (err) {
78
- setError(err instanceof Error ? err.message : "Error al conectar");
83
+ setError(err instanceof Error ? err.message : t("integrations.err_connect"));
79
84
  setStep("idle");
80
85
  }
81
86
  }
@@ -91,7 +96,7 @@ export function PluginConnect({ pid, scope, entry }: { pid: string; scope: Integ
91
96
  setSelectOptions([]);
92
97
  setStep("done");
93
98
  } catch (err) {
94
- setError(err instanceof Error ? err.message : "Error al seleccionar");
99
+ setError(err instanceof Error ? err.message : t("integrations.err_generic"));
95
100
  setStep("idle");
96
101
  }
97
102
  }
@@ -102,10 +107,18 @@ export function PluginConnect({ pid, scope, entry }: { pid: string; scope: Integ
102
107
  await Integrations.deactivate(pid, entry.slug, scope);
103
108
  await mutate();
104
109
  } catch (err) {
105
- setError(err instanceof Error ? err.message : "Error al desactivar");
110
+ setError(err instanceof Error ? err.message : t("integrations.err_generic"));
106
111
  }
107
112
  }
108
113
 
114
+ const badgeLabel = isLoading
115
+ ? "…"
116
+ : isActive
117
+ ? t("integrations.status_active")
118
+ : status?.status === "error"
119
+ ? t("integrations.status_error")
120
+ : t("integrations.status_unconfigured");
121
+
109
122
  return (
110
123
  <PluginCard
111
124
  icon={
@@ -124,7 +137,7 @@ export function PluginConnect({ pid, scope, entry }: { pid: string; scope: Integ
124
137
  )}
125
138
  >
126
139
  <span className={cn("h-1.5 w-1.5 rounded-full", isActive ? "bg-emerald-400" : "bg-muted-foreground")} />
127
- {isLoading ? "..." : isActive ? "Activo" : status?.status === "error" ? "Error" : "No configurado"}
140
+ {badgeLabel}
128
141
  </span>
129
142
  }
130
143
  expanded={expanded}
@@ -144,14 +157,14 @@ export function PluginConnect({ pid, scope, entry }: { pid: string; scope: Integ
144
157
  <div className="space-y-1 rounded-xl border border-emerald-700/30 bg-emerald-900/10 p-3">
145
158
  <div className="flex items-center gap-2">
146
159
  <CheckCircle2 className="h-3.5 w-3.5 text-emerald-400" />
147
- <span className="text-xs font-medium text-emerald-300">Conectado</span>
160
+ <span className="text-xs font-medium text-emerald-300">{t("integrations.connected")}</span>
148
161
  </div>
149
- {(ui.connectedFields || []).map((f) => {
150
- const v = status?.[f.key];
162
+ {(ui.connectedFields || []).map((key) => {
163
+ const v = status?.[key];
151
164
  if (!v) return null;
152
165
  return (
153
- <p key={f.key} className="pl-5 text-[10px] text-muted-foreground">
154
- {f.label}: {String(v)}
166
+ <p key={key} className="pl-5 text-[10px] text-muted-foreground">
167
+ {tk(`integrations.${entry.slug}.connected.${key}`)}: {String(v)}
155
168
  </p>
156
169
  );
157
170
  })}
@@ -161,14 +174,14 @@ export function PluginConnect({ pid, scope, entry }: { pid: string; scope: Integ
161
174
  {/* Post-validate selection (e.g. Asana workspace) */}
162
175
  {selectOptions.length > 1 && ui.select && (
163
176
  <div className="space-y-2">
164
- <p className="text-[11px] text-muted-foreground">{ui.select.label}:</p>
177
+ <p className="text-[11px] text-muted-foreground">{tk(`integrations.${entry.slug}.select_label`)}:</p>
165
178
  <div className="flex gap-2">
166
179
  <select
167
180
  value={selectValue}
168
181
  onChange={(e) => setSelectValue(e.target.value)}
169
182
  className={cn("flex-1 rounded-lg border border-border bg-background px-2 py-1.5 text-xs outline-none", accent.ring)}
170
183
  >
171
- <option value="">Seleccionar...</option>
184
+ <option value="">{t("integrations.select_placeholder")}</option>
172
185
  {selectOptions.map((o) => (
173
186
  <option key={o.value} value={o.value}>{o.label}</option>
174
187
  ))}
@@ -178,7 +191,7 @@ export function PluginConnect({ pid, scope, entry }: { pid: string; scope: Integ
178
191
  disabled={!selectValue || busy}
179
192
  className={cn("rounded-lg border px-3 py-1.5 text-xs transition-all disabled:cursor-not-allowed disabled:opacity-50", accent.border, accent.text, accent.hover)}
180
193
  >
181
- {busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : "Confirmar"}
194
+ {busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : t("integrations.confirm")}
182
195
  </button>
183
196
  </div>
184
197
  </div>
@@ -187,10 +200,10 @@ export function PluginConnect({ pid, scope, entry }: { pid: string; scope: Integ
187
200
  {/* Config form */}
188
201
  {showForm && (
189
202
  <div className="space-y-3">
190
- <p className="text-xs font-semibold text-foreground">Credenciales {entry.name}</p>
203
+ <p className="text-xs font-semibold text-foreground">{t("integrations.credentials", { name: entry.name })}</p>
191
204
  {ui.configFields.map((field) => (
192
205
  <div key={field.key} className="space-y-2">
193
- {field.help && (
206
+ {field.help_url && (
194
207
  <div className="overflow-hidden rounded-lg border border-border">
195
208
  <button
196
209
  type="button"
@@ -198,22 +211,22 @@ export function PluginConnect({ pid, scope, entry }: { pid: string; scope: Integ
198
211
  className="flex w-full items-center justify-between px-3 py-2 text-left transition-colors hover:bg-muted/40"
199
212
  >
200
213
  <span className="text-[11px] text-muted-foreground">
201
- {field.help.label} ·{" "}
214
+ {tk(`integrations.${entry.slug}.fields.${field.key}.help_label`)} ·{" "}
202
215
  <a
203
- href={field.help.url}
216
+ href={field.help_url}
204
217
  target="_blank"
205
218
  rel="noreferrer"
206
219
  onClick={(e) => e.stopPropagation()}
207
220
  className={cn("inline-flex items-center gap-0.5 hover:underline", accent.text)}
208
221
  >
209
- {field.help.urlLabel} <ExternalLink className="h-2.5 w-2.5" />
222
+ {field.help_url_label} <ExternalLink className="h-2.5 w-2.5" />
210
223
  </a>
211
224
  </span>
212
225
  <ChevronDown className={cn("h-3.5 w-3.5 flex-shrink-0 text-muted-foreground transition-transform", showHelp === field.key && "rotate-180")} />
213
226
  </button>
214
227
  {showHelp === field.key && (
215
228
  <div className="space-y-1.5 border-t border-border px-3 pb-3 pt-2.5">
216
- {field.help.steps.map((s, i) => (
229
+ {tk(`integrations.${entry.slug}.fields.${field.key}.help_steps`).split("\n").map((s, i) => (
217
230
  <div key={i} className="flex items-start gap-2">
218
231
  <span className={cn("mt-0.5 flex-shrink-0 font-mono text-[10px]", accent.text)}>{i + 1}.</span>
219
232
  <p className="text-[11px] text-muted-foreground">{s}</p>
@@ -224,7 +237,7 @@ export function PluginConnect({ pid, scope, entry }: { pid: string; scope: Integ
224
237
  </div>
225
238
  )}
226
239
  <div>
227
- <label className="mb-1 block text-[10px] text-muted-foreground">{field.label}</label>
240
+ <label className="mb-1 block text-[10px] text-muted-foreground">{tk(`integrations.${entry.slug}.fields.${field.key}.label`)}</label>
228
241
  <div className="relative">
229
242
  <input
230
243
  type={field.type === "password" && !reveal[field.key] ? "password" : "text"}
@@ -241,7 +254,7 @@ export function PluginConnect({ pid, scope, entry }: { pid: string; scope: Integ
241
254
  className="absolute right-2.5 top-1/2 flex -translate-y-1/2 items-center gap-0.5 text-[10px] text-muted-foreground transition-colors hover:text-foreground"
242
255
  >
243
256
  {reveal[field.key] ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
244
- {reveal[field.key] ? "Ocultar" : "Ver"}
257
+ {reveal[field.key] ? t("integrations.hide") : t("integrations.reveal")}
245
258
  </button>
246
259
  )}
247
260
  </div>
@@ -251,7 +264,7 @@ export function PluginConnect({ pid, scope, entry }: { pid: string; scope: Integ
251
264
 
252
265
  {step === "validating" && (
253
266
  <div className="flex items-center gap-2 text-xs text-muted-foreground">
254
- <Loader2 className="h-3.5 w-3.5 animate-spin" /> Verificando...
267
+ <Loader2 className="h-3.5 w-3.5 animate-spin" /> {t("integrations.verifying")}
255
268
  </div>
256
269
  )}
257
270
 
@@ -261,8 +274,8 @@ export function PluginConnect({ pid, scope, entry }: { pid: string; scope: Integ
261
274
  className={cn("flex w-full items-center justify-center gap-1.5 rounded-lg border px-3 py-2 text-xs transition-all disabled:cursor-not-allowed disabled:opacity-50", accent.border, accent.text, accent.hover)}
262
275
  >
263
276
  {busy ? (
264
- <><Loader2 className="h-3.5 w-3.5 animate-spin" />{step === "saving" ? "Guardando..." : "Validando..."}</>
265
- ) : "Conectar"}
277
+ <><Loader2 className="h-3.5 w-3.5 animate-spin" />{step === "saving" ? t("integrations.saving") : t("integrations.validating")}</>
278
+ ) : t("integrations.connect")}
266
279
  </button>
267
280
  </div>
268
281
  )}
@@ -277,7 +290,7 @@ export function PluginConnect({ pid, scope, entry }: { pid: string; scope: Integ
277
290
  onClick={handleDeactivate}
278
291
  className="flex items-center gap-1.5 rounded-lg border border-red-700/50 px-3 py-1.5 text-xs text-red-400 transition-all hover:bg-red-900/20"
279
292
  >
280
- <WifiOff className="h-3.5 w-3.5" /> Desactivar
293
+ <WifiOff className="h-3.5 w-3.5" /> {t("integrations.deactivate")}
281
294
  </button>
282
295
  </div>
283
296
  )}
@@ -1,4 +1,5 @@
1
1
  import { Wrench } from "lucide-react";
2
+ import { t } from "../../i18n";
2
3
 
3
4
  export interface PluginTool {
4
5
  slug: string;
@@ -15,25 +16,23 @@ export function PluginToolsSection({ tools, isActive }: { pid: string; tools: Pl
15
16
  <div className="space-y-2.5 rounded-xl border border-border bg-muted/30 p-3">
16
17
  <div className="flex items-center justify-between">
17
18
  <p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
18
- Tools para agentes
19
+ {t("integrations.tools_for_agents")}
19
20
  </p>
20
21
  </div>
21
22
  <div className="flex flex-wrap gap-1.5">
22
- {tools.map((t) => (
23
+ {tools.map((tool) => (
23
24
  <div
24
- key={t.slug}
25
+ key={tool.slug}
25
26
  className="flex items-center gap-1.5 rounded-lg border border-border bg-background px-2 py-1"
26
27
  >
27
28
  <Wrench className="h-2.5 w-2.5 flex-shrink-0 text-muted-foreground" />
28
- <span className="font-mono text-[10px] text-foreground">{t.slug}</span>
29
+ <span className="font-mono text-[10px] text-foreground">{tool.slug}</span>
29
30
  <span className="text-[10px] text-muted-foreground/60">·</span>
30
- <span className="text-[10px] text-muted-foreground">{t.desc}</span>
31
+ <span className="text-[10px] text-muted-foreground">{tool.desc}</span>
31
32
  </div>
32
33
  ))}
33
34
  </div>
34
- <p className="text-[10px] text-muted-foreground/70">
35
- Disponibles para los agentes que las tengan permitidas, o vía <span className="font-mono">discover_tools</span>.
36
- </p>
35
+ <p className="text-[10px] text-muted-foreground/70">{t("integrations.tools_available_note")}</p>
37
36
  </div>
38
37
  );
39
38
  }
@@ -602,6 +602,7 @@ export const en = {
602
602
  logs_btn: "Logs",
603
603
  testing: "Testing…",
604
604
  test_ok: "OK · {n} tools available",
605
+ tools_count: "{n} tools",
605
606
  logs_title: "Logs · {name}",
606
607
  logs_empty: "No logs yet. Start the MCP by calling a tool or running Test.",
607
608
  logs_events: "Recent events",
@@ -1789,4 +1790,60 @@ export const en = {
1789
1790
  err_load_conversation: "Could not load conversation.",
1790
1791
  err_stream: "Stream error.",
1791
1792
  },
1793
+ integrations: {
1794
+ title: "Integrations",
1795
+ description: "Plugins and tools available for this project",
1796
+ tab_plugins: "Plugins",
1797
+ tab_tools: "Tools",
1798
+ scope_label: "Scope:",
1799
+ scope_project: "This project",
1800
+ scope_global: "Global (default)",
1801
+ plugins_hint: "Channel & service plugins installable per project. Saved in the scope selected above.",
1802
+ more_soon: "More plugins coming soon…",
1803
+ tools_hint: "Tools that connected plugins expose to this project's agents.",
1804
+ tools_empty: "No integration tools yet. Connect a plugin to enable them.",
1805
+ tool_active: "active",
1806
+ tool_inactive: "inactive",
1807
+ status_active: "Active",
1808
+ status_error: "Error",
1809
+ status_unconfigured: "Not configured",
1810
+ connected: "Connected",
1811
+ connect: "Connect",
1812
+ deactivate: "Deactivate",
1813
+ saving: "Saving...",
1814
+ validating: "Validating...",
1815
+ verifying: "Verifying token...",
1816
+ confirm: "Confirm",
1817
+ select_placeholder: "Select...",
1818
+ reveal: "Show",
1819
+ hide: "Hide",
1820
+ credentials: "{name} credentials",
1821
+ coming_soon: "Coming soon",
1822
+ coming_soon_body: "This plugin is declared in the catalog but isn't connectable in APX yet. It will be ported natively in a future iteration.",
1823
+ tools_for_agents: "Agent tools",
1824
+ tools_available_note: "Available to agents that allow them, or via discover_tools.",
1825
+ err_connect: "Failed to connect",
1826
+ err_generic: "Something went wrong",
1827
+ asana: {
1828
+ select_label: "Select the workspace to use",
1829
+ connected: { user_name: "Connected as", user_email: "Email", workspace_name: "Workspace" },
1830
+ fields: {
1831
+ personal_access_token: {
1832
+ label: "Personal Access Token",
1833
+ help_label: "How to get the token?",
1834
+ help_steps: "Open app.asana.com/0/my-apps in your browser.\nScroll to the \"Personal access tokens\" section (not your OAuth apps).\nClick \"+ New access token\".\nGive it a name and confirm.\nCopy the full token — it starts with \"1/...\" and has a \":\" in the middle.\nPaste it in the field below.",
1835
+ },
1836
+ },
1837
+ },
1838
+ github: {
1839
+ connected: { user_login: "Connected as", user_name: "Name" },
1840
+ fields: {
1841
+ token: {
1842
+ label: "Personal Access Token",
1843
+ help_label: "How to get the token?",
1844
+ help_steps: "Open github.com/settings/tokens.\nGenerate a token (classic or fine-grained) with the \"repo\" scope.\nCopy the token — it starts with ghp_ or github_pat_.\nPaste it in the field below.",
1845
+ },
1846
+ },
1847
+ },
1848
+ },
1792
1849
  } as const;
@@ -620,6 +620,7 @@ export const es = {
620
620
  logs_btn: "Logs",
621
621
  testing: "Probando…",
622
622
  test_ok: "OK · {n} tools disponibles",
623
+ tools_count: "{n} tools",
623
624
  logs_title: "Logs · {name}",
624
625
  logs_empty: "Sin logs todavía. Arrancá el MCP llamando un tool o probando.",
625
626
  logs_events: "Eventos recientes",
@@ -1787,6 +1788,62 @@ export const es = {
1787
1788
  err_load_conversation: "No se pudo cargar la conversación.",
1788
1789
  err_stream: "Error de stream.",
1789
1790
  },
1791
+ integrations: {
1792
+ title: "Integrations",
1793
+ description: "Plugins y tools disponibles para este proyecto",
1794
+ tab_plugins: "Plugins",
1795
+ tab_tools: "Tools",
1796
+ scope_label: "Ámbito:",
1797
+ scope_project: "Este proyecto",
1798
+ scope_global: "Global (default)",
1799
+ plugins_hint: "Plugins de canal y servicio instalables por proyecto. Se guardan en el ámbito seleccionado arriba.",
1800
+ more_soon: "Más plugins próximamente…",
1801
+ tools_hint: "Tools que los plugins conectados exponen a los agentes de este proyecto.",
1802
+ tools_empty: "No hay tools de integraciones. Conectá un plugin para habilitarlas.",
1803
+ tool_active: "activo",
1804
+ tool_inactive: "inactivo",
1805
+ status_active: "Activo",
1806
+ status_error: "Error",
1807
+ status_unconfigured: "No configurado",
1808
+ connected: "Conectado",
1809
+ connect: "Conectar",
1810
+ deactivate: "Desactivar",
1811
+ saving: "Guardando...",
1812
+ validating: "Validando...",
1813
+ verifying: "Verificando token...",
1814
+ confirm: "Confirmar",
1815
+ select_placeholder: "Seleccionar...",
1816
+ reveal: "Ver",
1817
+ hide: "Ocultar",
1818
+ credentials: "Credenciales {name}",
1819
+ coming_soon: "Próximamente",
1820
+ coming_soon_body: "Este plugin está declarado en el catálogo pero todavía no está conectable en APX. Se va a portar de forma nativa en una próxima iteración.",
1821
+ tools_for_agents: "Tools para agentes",
1822
+ tools_available_note: "Disponibles para los agentes que las tengan permitidas, o vía discover_tools.",
1823
+ err_connect: "Error al conectar",
1824
+ err_generic: "Ocurrió un error",
1825
+ asana: {
1826
+ select_label: "Seleccioná el workspace a usar",
1827
+ connected: { user_name: "Conectado como", user_email: "Email", workspace_name: "Workspace" },
1828
+ fields: {
1829
+ personal_access_token: {
1830
+ label: "Personal Access Token",
1831
+ help_label: "¿Cómo obtener el token?",
1832
+ help_steps: "Abrí app.asana.com/0/my-apps en el navegador.\nBajá hasta la sección \"Personal access tokens\" (no tus apps OAuth).\nHacé clic en \"+ New access token\".\nDale un nombre y confirmá.\nCopiá el token completo — empieza con \"1/...\" y tiene un \":\" en el medio.\nPegalo en el campo de abajo.",
1833
+ },
1834
+ },
1835
+ },
1836
+ github: {
1837
+ connected: { user_login: "Conectado como", user_name: "Nombre" },
1838
+ fields: {
1839
+ token: {
1840
+ label: "Personal Access Token",
1841
+ help_label: "¿Cómo obtener el token?",
1842
+ help_steps: "Abrí github.com/settings/tokens.\nGenerá un token (classic o fine-grained) con scope \"repo\".\nCopiá el token — empieza con ghp_ o github_pat_.\nPegalo en el campo de abajo.",
1843
+ },
1844
+ },
1845
+ },
1846
+ },
1790
1847
  } as const;
1791
1848
 
1792
1849
  export type EsStrings = typeof es;
@@ -20,32 +20,28 @@ export interface PluginTool {
20
20
  desc: string;
21
21
  }
22
22
 
23
- // Declarative UI descriptor (mirrors the plugin's `ui` in core). Lets the
24
- // generic PluginConnect component render each plugin's config form.
23
+ // Declarative UI descriptor (mirrors the plugin's `ui` in core). STRUCTURE ONLY
24
+ // display text is resolved from i18n keys (integrations.<slug>.*) by the
25
+ // generic PluginConnect component. help_url/help_url_label are non-translatable.
25
26
  export interface PluginConfigField {
26
27
  key: string;
27
- label: string;
28
28
  type: "password" | "text";
29
29
  placeholder?: string;
30
- help?: { label: string; url: string; urlLabel: string; steps: string[] };
30
+ help_url?: string;
31
+ help_url_label?: string;
31
32
  }
32
33
  export interface PluginSelect {
33
34
  key: string;
34
- label: string;
35
35
  action: string;
36
36
  listKey: string;
37
37
  valueKey: string;
38
38
  labelKey: string;
39
39
  }
40
- export interface PluginConnectedField {
41
- key: string;
42
- label: string;
43
- }
44
40
  export interface PluginUi {
45
41
  accent?: string;
46
42
  configFields: PluginConfigField[];
47
43
  select?: PluginSelect;
48
- connectedFields?: PluginConnectedField[];
44
+ connectedFields?: string[];
49
45
  }
50
46
 
51
47
  // One entry of the plugin catalog with its resolved status for this project.
@@ -7,12 +7,13 @@ import { Section } from "../../components/Section";
7
7
  import { Empty, Loading } from "../../components/ui";
8
8
  import { PluginConnect } from "../../components/integrations/PluginConnect";
9
9
  import { ComingSoonPlugin } from "../../components/integrations/ComingSoonPlugin";
10
+ import { t } from "../../i18n";
10
11
 
11
12
  type SubTab = "plugins" | "tools";
12
13
 
13
- const SUBTABS: { value: SubTab; label: string; icon: typeof Puzzle }[] = [
14
- { value: "plugins", label: "Plugins", icon: Puzzle },
15
- { value: "tools", label: "Tools", icon: Wrench },
14
+ const SUBTABS: { value: SubTab; labelKey: "integrations.tab_plugins" | "integrations.tab_tools"; icon: typeof Puzzle }[] = [
15
+ { value: "plugins", labelKey: "integrations.tab_plugins", icon: Puzzle },
16
+ { value: "tools", labelKey: "integrations.tab_tools", icon: Wrench },
16
17
  ];
17
18
 
18
19
  // Renders a connectable plugin (via the generic PluginConnect, driven by its
@@ -28,16 +29,13 @@ function PluginsSection({ pid, scope }: { pid: string; scope: IntegrationScope }
28
29
 
29
30
  return (
30
31
  <div className="space-y-3">
31
- <p className="text-xs text-muted-foreground">
32
- Plugins de canal y servicio instalables por proyecto. Se guardan en el ámbito
33
- seleccionado arriba.
34
- </p>
32
+ <p className="text-xs text-muted-foreground">{t("integrations.plugins_hint")}</p>
35
33
  {isLoading && <Loading />}
36
34
  {(catalog || []).map((entry) => (
37
35
  <PluginRow key={entry.slug} pid={pid} scope={scope} entry={entry} />
38
36
  ))}
39
37
  <div className="rounded-xl border border-dashed border-border p-6 text-center">
40
- <p className="text-sm text-muted-foreground">Más plugins próximamente…</p>
38
+ <p className="text-sm text-muted-foreground">{t("integrations.more_soon")}</p>
41
39
  </div>
42
40
  </div>
43
41
  );
@@ -47,35 +45,33 @@ function ToolsSection({ pid }: { pid: string }) {
47
45
  const { data: catalog } = useSWR(`integrations-catalog-${pid}`, () => Integrations.catalog(pid));
48
46
  const rows = (catalog || [])
49
47
  .filter((c) => !c.coming_soon && (c.tools?.length ?? 0) > 0)
50
- .flatMap((c) => (c.tools || []).map((t) => ({ ...t, plugin: c.name, active: c.status.is_enabled })));
48
+ .flatMap((c) => (c.tools || []).map((tool) => ({ ...tool, plugin: c.name, active: c.status.is_enabled })));
51
49
 
52
50
  return (
53
51
  <div className="space-y-3">
54
- <p className="text-xs text-muted-foreground">
55
- Tools que los plugins conectados exponen a los agentes de este proyecto.
56
- </p>
52
+ <p className="text-xs text-muted-foreground">{t("integrations.tools_hint")}</p>
57
53
  {rows.length === 0 ? (
58
- <Empty>No hay tools de integraciones. Conectá un plugin para habilitarlas.</Empty>
54
+ <Empty>{t("integrations.tools_empty")}</Empty>
59
55
  ) : (
60
56
  <ul className="space-y-2">
61
- {rows.map((t) => (
62
- <li key={t.slug} className={cn("rounded-md border border-border bg-muted/30 px-3 py-2", !t.active && "opacity-55")}>
57
+ {rows.map((tool) => (
58
+ <li key={tool.slug} className={cn("rounded-md border border-border bg-muted/30 px-3 py-2", !tool.active && "opacity-55")}>
63
59
  <div className="flex items-center gap-2">
64
60
  <Wrench className="h-3.5 w-3.5 text-muted-foreground" />
65
- <span className="font-mono text-xs text-foreground">{t.slug}</span>
66
- <span className="ml-auto text-[10px] text-muted-foreground">{t.plugin}</span>
61
+ <span className="font-mono text-xs text-foreground">{tool.slug}</span>
62
+ <span className="ml-auto text-[10px] text-muted-foreground">{tool.plugin}</span>
67
63
  <span
68
64
  className={cn(
69
65
  "rounded border px-1.5 py-0.5 text-[10px]",
70
- t.active
66
+ tool.active
71
67
  ? "border-emerald-700/40 bg-emerald-900/20 text-emerald-400"
72
68
  : "border-border bg-muted text-muted-foreground",
73
69
  )}
74
70
  >
75
- {t.active ? "activo" : "inactivo"}
71
+ {tool.active ? t("integrations.tool_active") : t("integrations.tool_inactive")}
76
72
  </span>
77
73
  </div>
78
- <p className="mt-0.5 pl-5 text-[10px] text-muted-foreground">{t.desc}</p>
74
+ <p className="mt-0.5 pl-5 text-[10px] text-muted-foreground">{tool.desc}</p>
79
75
  </li>
80
76
  ))}
81
77
  </ul>
@@ -91,15 +87,12 @@ export function IntegrationsTab({ pid }: { pid: string }) {
91
87
  const [scope, setScope] = useState<IntegrationScope>(isBase ? "global" : "project");
92
88
 
93
89
  return (
94
- <Section
95
- title="Integrations"
96
- description="Plugins y tools disponibles para este proyecto"
97
- >
90
+ <Section title={t("integrations.title")} description={t("integrations.description")}>
98
91
  {/* Scope selector — a real project can use its own integrations or the
99
92
  global (default-project) ones. */}
100
93
  {!isBase && (
101
94
  <div className="mb-4 flex items-center gap-2">
102
- <span className="text-xs text-muted-foreground">Ámbito:</span>
95
+ <span className="text-xs text-muted-foreground">{t("integrations.scope_label")}</span>
103
96
  {(["project", "global"] as const).map((s) => (
104
97
  <button
105
98
  key={s}
@@ -111,7 +104,7 @@ export function IntegrationsTab({ pid }: { pid: string }) {
111
104
  : "border-border bg-muted/30 text-muted-foreground hover:bg-muted/50",
112
105
  )}
113
106
  >
114
- {s === "project" ? "Este proyecto" : "Global (default)"}
107
+ {s === "project" ? t("integrations.scope_project") : t("integrations.scope_global")}
115
108
  </button>
116
109
  ))}
117
110
  </div>
@@ -130,7 +123,7 @@ export function IntegrationsTab({ pid }: { pid: string }) {
130
123
  tab === s.value ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground",
131
124
  )}
132
125
  >
133
- <Icon className="h-3.5 w-3.5" /> {s.label}
126
+ <Icon className="h-3.5 w-3.5" /> {t(s.labelKey)}
134
127
  </button>
135
128
  );
136
129
  })}
@@ -113,10 +113,6 @@ export function McpsTab({ pid }: { pid: string }) {
113
113
  }
114
114
  };
115
115
 
116
- const testedValues = Object.values(results);
117
- const okCount = testedValues.filter((r) => r.ok).length;
118
- const errCount = testedValues.filter((r) => r.ok === false).length;
119
-
120
116
  return (
121
117
  <div className="grid grid-cols-1 gap-4 lg:grid-cols-4">
122
118
  <div className="lg:col-span-3">
@@ -150,21 +146,6 @@ export function McpsTab({ pid }: { pid: string }) {
150
146
  {list.isLoading && <Loading />}
151
147
  {!list.isLoading && (list.data?.length ?? 0) === 0 && <Empty>{t("project.mcps.empty")}</Empty>}
152
148
 
153
- {(okCount > 0 || errCount > 0) && (
154
- <div className="mb-2 flex items-center gap-4 rounded-md border border-border bg-muted/30 px-3 py-2 text-xs">
155
- {okCount > 0 && (
156
- <span className="flex items-center gap-1.5 text-emerald-400">
157
- <span className="h-2 w-2 rounded-full bg-emerald-400" /> {okCount} conectados
158
- </span>
159
- )}
160
- {errCount > 0 && (
161
- <span className="flex items-center gap-1.5 text-red-400">
162
- <span className="h-2 w-2 rounded-full bg-red-400" /> {errCount} con error
163
- </span>
164
- )}
165
- </div>
166
- )}
167
-
168
149
  <ul className="space-y-2 text-sm">
169
150
  {(list.data || []).map((m) => {
170
151
  const writable = m.source === "apc" || m.source === "runtime" || m.source === "global";
@@ -240,7 +221,7 @@ export function McpsTab({ pid }: { pid: string }) {
240
221
  onClick={() => setExpandedTools((prev) => ({ ...prev, [m.name]: !prev[m.name] }))}
241
222
  className="flex items-center gap-1 text-xs text-muted-fg transition-colors hover:text-fg"
242
223
  >
243
- <Wrench size={12} /> {tools.length} tools
224
+ <Wrench size={12} /> {t("project.mcps.tools_count", { n: tools.length })}
244
225
  <ChevronDown size={12} className={cn("transition-transform", open && "rotate-180")} />
245
226
  </button>
246
227
  {open && (