@agentprojectcontext/apx 1.60.0 → 1.61.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.
- package/package.json +1 -1
- package/src/core/agent/tools/handlers/_github.js +22 -0
- package/src/core/agent/tools/handlers/github-create-issue.js +30 -0
- package/src/core/agent/tools/handlers/github-list-repos.js +20 -0
- package/src/core/agent/tools/names.js +6 -0
- package/src/core/agent/tools/registry.js +4 -0
- package/src/core/integrations/catalog.js +17 -27
- package/src/core/integrations/plugins/asana.js +42 -0
- package/src/core/integrations/plugins/github.js +172 -0
- package/src/core/stores/project-files.js +21 -2
- package/src/interfaces/web/dist/assets/index-BuII-tAi.css +1 -0
- package/src/interfaces/web/dist/assets/{index-DFNV6BWh.js → index-DRFIAiiq.js} +171 -171
- package/src/interfaces/web/dist/assets/index-DRFIAiiq.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/components/integrations/PluginConnect.tsx +287 -0
- package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +28 -26
- package/src/interfaces/web/src/lib/api/integrations.ts +32 -5
- package/src/interfaces/web/src/screens/project/IntegrationsTab.tsx +9 -12
- package/src/interfaces/web/src/screens/project/McpsTab.tsx +132 -81
- package/src/interfaces/web/dist/assets/index-DFNV6BWh.js.map +0 -1
- package/src/interfaces/web/dist/assets/index-HU-Wt2l9.css +0 -1
- package/src/interfaces/web/src/components/integrations/AsanaPlugin.tsx +0 -275
|
@@ -18,8 +18,8 @@
|
|
|
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-
|
|
22
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
21
|
+
<script type="module" crossorigin src="/assets/index-DRFIAiiq.js"></script>
|
|
22
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BuII-tAi.css">
|
|
23
23
|
</head>
|
|
24
24
|
<body class="bg-background text-foreground antialiased">
|
|
25
25
|
<div id="root"></div>
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { useState, type ReactNode } from "react";
|
|
2
|
+
import useSWR from "swr";
|
|
3
|
+
import { AlertCircle, CheckCircle2, ChevronDown, ExternalLink, Eye, EyeOff, Github, Loader2, WifiOff, X } from "lucide-react";
|
|
4
|
+
import { cn } from "../../lib/cn";
|
|
5
|
+
import { Integrations, type CatalogEntry, type IntegrationScope, type IntegrationStatus } from "../../lib/api";
|
|
6
|
+
import { PluginCard } from "./PluginCard";
|
|
7
|
+
import { PluginToolsSection } from "./PluginToolsSection";
|
|
8
|
+
|
|
9
|
+
// 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
|
+
|
|
13
|
+
function AsanaLogo({ className }: { className?: string }) {
|
|
14
|
+
return (
|
|
15
|
+
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
|
16
|
+
<path d="M18.833 9.637a4.167 4.167 0 1 1 0 8.333 4.167 4.167 0 0 1 0-8.333zm-13.666 0a4.167 4.167 0 1 1 0 8.333 4.167 4.167 0 0 1 0-8.333zM12 2a4.167 4.167 0 1 1 0 8.333A4.167 4.167 0 0 1 12 2z" />
|
|
17
|
+
</svg>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Accent = { text: string; border: string; hover: string; ring: string; wrap: string };
|
|
22
|
+
const ACCENTS: Record<string, Accent> = {
|
|
23
|
+
rose: { text: "text-rose-400", border: "border-rose-700/50", hover: "hover:bg-rose-900/20", ring: "focus:border-rose-500/50", wrap: "border-rose-500/30 from-rose-500/20 to-pink-500/20" },
|
|
24
|
+
slate: { text: "text-slate-200", border: "border-slate-600/60", hover: "hover:bg-slate-700/30", ring: "focus:border-slate-400/60", wrap: "border-slate-500/30 from-slate-500/20 to-slate-700/20" },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function iconFor(slug: string, accent: Accent): ReactNode {
|
|
28
|
+
if (slug === "github") return <Github className={cn("h-6 w-6", accent.text)} />;
|
|
29
|
+
if (slug === "asana") return <AsanaLogo className={cn("h-6 w-6", accent.text)} />;
|
|
30
|
+
return <span className={cn("text-lg", accent.text)}>◆</span>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Step = "idle" | "saving" | "validating" | "done";
|
|
34
|
+
type Opt = { value: string; label: string };
|
|
35
|
+
|
|
36
|
+
export function PluginConnect({ pid, scope, entry }: { pid: string; scope: IntegrationScope; entry: CatalogEntry }) {
|
|
37
|
+
const ui = entry.ui!;
|
|
38
|
+
const accent = ACCENTS[ui.accent || "rose"] || ACCENTS.rose;
|
|
39
|
+
|
|
40
|
+
const [expanded, setExpanded] = useState(false);
|
|
41
|
+
const [values, setValues] = useState<Record<string, string>>({});
|
|
42
|
+
const [reveal, setReveal] = useState<Record<string, boolean>>({});
|
|
43
|
+
const [showHelp, setShowHelp] = useState<string | null>(null);
|
|
44
|
+
const [step, setStep] = useState<Step>("idle");
|
|
45
|
+
const [error, setError] = useState<string | null>(null);
|
|
46
|
+
const [selectOptions, setSelectOptions] = useState<Opt[]>([]);
|
|
47
|
+
const [selectValue, setSelectValue] = useState("");
|
|
48
|
+
|
|
49
|
+
const { data: status, mutate, isLoading } = useSWR<IntegrationStatus>(
|
|
50
|
+
`integration-status-${entry.slug}-${pid}-${scope}`,
|
|
51
|
+
() => Integrations.status(pid, entry.slug, scope),
|
|
52
|
+
{ shouldRetryOnError: false },
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const isActive = status?.status === "active" && status.is_enabled === true;
|
|
56
|
+
const busy = step === "saving" || step === "validating";
|
|
57
|
+
const showForm = !isActive && selectOptions.length === 0;
|
|
58
|
+
|
|
59
|
+
async function handleConnect() {
|
|
60
|
+
if (ui.configFields.some((f) => !values[f.key]?.trim())) return;
|
|
61
|
+
setStep("saving");
|
|
62
|
+
setError(null);
|
|
63
|
+
try {
|
|
64
|
+
await Integrations.configure(pid, entry.slug, scope, { ...values });
|
|
65
|
+
setStep("validating");
|
|
66
|
+
const result = (await Integrations.validate(pid, entry.slug, scope)) as unknown as Record<string, unknown>;
|
|
67
|
+
await mutate();
|
|
68
|
+
if (ui.select && !result[ui.select.key]) {
|
|
69
|
+
const data = (await Integrations.action(pid, entry.slug, ui.select.action, scope)) as Record<string, unknown>;
|
|
70
|
+
const list = (data[ui.select.listKey] as Record<string, unknown>[]) || [];
|
|
71
|
+
if (list.length > 1) {
|
|
72
|
+
setSelectOptions(list.map((o) => ({ value: String(o[ui.select!.valueKey]), label: String(o[ui.select!.labelKey]) })));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
setStep("done");
|
|
76
|
+
setValues({});
|
|
77
|
+
} catch (err) {
|
|
78
|
+
setError(err instanceof Error ? err.message : "Error al conectar");
|
|
79
|
+
setStep("idle");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function handleSelect() {
|
|
84
|
+
if (!selectValue || !ui.select) return;
|
|
85
|
+
setStep("saving");
|
|
86
|
+
setError(null);
|
|
87
|
+
try {
|
|
88
|
+
await Integrations.configure(pid, entry.slug, scope, { [ui.select.key]: selectValue });
|
|
89
|
+
await Integrations.validate(pid, entry.slug, scope);
|
|
90
|
+
await mutate();
|
|
91
|
+
setSelectOptions([]);
|
|
92
|
+
setStep("done");
|
|
93
|
+
} catch (err) {
|
|
94
|
+
setError(err instanceof Error ? err.message : "Error al seleccionar");
|
|
95
|
+
setStep("idle");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function handleDeactivate() {
|
|
100
|
+
setError(null);
|
|
101
|
+
try {
|
|
102
|
+
await Integrations.deactivate(pid, entry.slug, scope);
|
|
103
|
+
await mutate();
|
|
104
|
+
} catch (err) {
|
|
105
|
+
setError(err instanceof Error ? err.message : "Error al desactivar");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<PluginCard
|
|
111
|
+
icon={
|
|
112
|
+
<div className={cn("flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-2xl border bg-gradient-to-br", accent.wrap)}>
|
|
113
|
+
{iconFor(entry.slug, accent)}
|
|
114
|
+
</div>
|
|
115
|
+
}
|
|
116
|
+
title={entry.name}
|
|
117
|
+
description={entry.description}
|
|
118
|
+
hasTools={(entry.tools?.length ?? 0) > 0}
|
|
119
|
+
badges={
|
|
120
|
+
<span
|
|
121
|
+
className={cn(
|
|
122
|
+
"flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px]",
|
|
123
|
+
isActive ? "border-emerald-700 bg-emerald-900/20 text-emerald-400" : "border-border bg-muted text-muted-foreground",
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
<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"}
|
|
128
|
+
</span>
|
|
129
|
+
}
|
|
130
|
+
expanded={expanded}
|
|
131
|
+
onToggle={() => setExpanded((v) => !v)}
|
|
132
|
+
>
|
|
133
|
+
<div className="space-y-4 p-4">
|
|
134
|
+
{error && (
|
|
135
|
+
<div className="flex items-center gap-2 rounded-lg border border-red-700/30 bg-red-900/20 px-3 py-2.5 text-xs text-red-300">
|
|
136
|
+
<AlertCircle className="h-3.5 w-3.5 flex-shrink-0" />
|
|
137
|
+
<span className="flex-1">{error}</span>
|
|
138
|
+
<button onClick={() => setError(null)}><X className="h-3.5 w-3.5" /></button>
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
|
|
142
|
+
{/* Connected summary */}
|
|
143
|
+
{isActive && selectOptions.length === 0 && (
|
|
144
|
+
<div className="space-y-1 rounded-xl border border-emerald-700/30 bg-emerald-900/10 p-3">
|
|
145
|
+
<div className="flex items-center gap-2">
|
|
146
|
+
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-400" />
|
|
147
|
+
<span className="text-xs font-medium text-emerald-300">Conectado</span>
|
|
148
|
+
</div>
|
|
149
|
+
{(ui.connectedFields || []).map((f) => {
|
|
150
|
+
const v = status?.[f.key];
|
|
151
|
+
if (!v) return null;
|
|
152
|
+
return (
|
|
153
|
+
<p key={f.key} className="pl-5 text-[10px] text-muted-foreground">
|
|
154
|
+
{f.label}: {String(v)}
|
|
155
|
+
</p>
|
|
156
|
+
);
|
|
157
|
+
})}
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{/* Post-validate selection (e.g. Asana workspace) */}
|
|
162
|
+
{selectOptions.length > 1 && ui.select && (
|
|
163
|
+
<div className="space-y-2">
|
|
164
|
+
<p className="text-[11px] text-muted-foreground">{ui.select.label}:</p>
|
|
165
|
+
<div className="flex gap-2">
|
|
166
|
+
<select
|
|
167
|
+
value={selectValue}
|
|
168
|
+
onChange={(e) => setSelectValue(e.target.value)}
|
|
169
|
+
className={cn("flex-1 rounded-lg border border-border bg-background px-2 py-1.5 text-xs outline-none", accent.ring)}
|
|
170
|
+
>
|
|
171
|
+
<option value="">Seleccionar...</option>
|
|
172
|
+
{selectOptions.map((o) => (
|
|
173
|
+
<option key={o.value} value={o.value}>{o.label}</option>
|
|
174
|
+
))}
|
|
175
|
+
</select>
|
|
176
|
+
<button
|
|
177
|
+
onClick={handleSelect}
|
|
178
|
+
disabled={!selectValue || busy}
|
|
179
|
+
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
|
+
>
|
|
181
|
+
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : "Confirmar"}
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
)}
|
|
186
|
+
|
|
187
|
+
{/* Config form */}
|
|
188
|
+
{showForm && (
|
|
189
|
+
<div className="space-y-3">
|
|
190
|
+
<p className="text-xs font-semibold text-foreground">Credenciales {entry.name}</p>
|
|
191
|
+
{ui.configFields.map((field) => (
|
|
192
|
+
<div key={field.key} className="space-y-2">
|
|
193
|
+
{field.help && (
|
|
194
|
+
<div className="overflow-hidden rounded-lg border border-border">
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
onClick={() => setShowHelp((v) => (v === field.key ? null : field.key))}
|
|
198
|
+
className="flex w-full items-center justify-between px-3 py-2 text-left transition-colors hover:bg-muted/40"
|
|
199
|
+
>
|
|
200
|
+
<span className="text-[11px] text-muted-foreground">
|
|
201
|
+
{field.help.label} ·{" "}
|
|
202
|
+
<a
|
|
203
|
+
href={field.help.url}
|
|
204
|
+
target="_blank"
|
|
205
|
+
rel="noreferrer"
|
|
206
|
+
onClick={(e) => e.stopPropagation()}
|
|
207
|
+
className={cn("inline-flex items-center gap-0.5 hover:underline", accent.text)}
|
|
208
|
+
>
|
|
209
|
+
{field.help.urlLabel} <ExternalLink className="h-2.5 w-2.5" />
|
|
210
|
+
</a>
|
|
211
|
+
</span>
|
|
212
|
+
<ChevronDown className={cn("h-3.5 w-3.5 flex-shrink-0 text-muted-foreground transition-transform", showHelp === field.key && "rotate-180")} />
|
|
213
|
+
</button>
|
|
214
|
+
{showHelp === field.key && (
|
|
215
|
+
<div className="space-y-1.5 border-t border-border px-3 pb-3 pt-2.5">
|
|
216
|
+
{field.help.steps.map((s, i) => (
|
|
217
|
+
<div key={i} className="flex items-start gap-2">
|
|
218
|
+
<span className={cn("mt-0.5 flex-shrink-0 font-mono text-[10px]", accent.text)}>{i + 1}.</span>
|
|
219
|
+
<p className="text-[11px] text-muted-foreground">{s}</p>
|
|
220
|
+
</div>
|
|
221
|
+
))}
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
<div>
|
|
227
|
+
<label className="mb-1 block text-[10px] text-muted-foreground">{field.label}</label>
|
|
228
|
+
<div className="relative">
|
|
229
|
+
<input
|
|
230
|
+
type={field.type === "password" && !reveal[field.key] ? "password" : "text"}
|
|
231
|
+
placeholder={field.placeholder}
|
|
232
|
+
value={values[field.key] || ""}
|
|
233
|
+
onChange={(e) => setValues((v) => ({ ...v, [field.key]: e.target.value }))}
|
|
234
|
+
onKeyDown={(e) => e.key === "Enter" && handleConnect()}
|
|
235
|
+
className={cn("w-full rounded-lg border border-border bg-background px-3 py-2 font-mono text-xs outline-none placeholder:text-muted-foreground/60", field.type === "password" && "pr-14", accent.ring)}
|
|
236
|
+
/>
|
|
237
|
+
{field.type === "password" && (
|
|
238
|
+
<button
|
|
239
|
+
type="button"
|
|
240
|
+
onClick={() => setReveal((r) => ({ ...r, [field.key]: !r[field.key] }))}
|
|
241
|
+
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
|
+
>
|
|
243
|
+
{reveal[field.key] ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
|
244
|
+
{reveal[field.key] ? "Ocultar" : "Ver"}
|
|
245
|
+
</button>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
))}
|
|
251
|
+
|
|
252
|
+
{step === "validating" && (
|
|
253
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
254
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" /> Verificando...
|
|
255
|
+
</div>
|
|
256
|
+
)}
|
|
257
|
+
|
|
258
|
+
<button
|
|
259
|
+
onClick={handleConnect}
|
|
260
|
+
disabled={ui.configFields.some((f) => !values[f.key]?.trim()) || busy}
|
|
261
|
+
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
|
+
>
|
|
263
|
+
{busy ? (
|
|
264
|
+
<><Loader2 className="h-3.5 w-3.5 animate-spin" />{step === "saving" ? "Guardando..." : "Validando..."}</>
|
|
265
|
+
) : "Conectar"}
|
|
266
|
+
</button>
|
|
267
|
+
</div>
|
|
268
|
+
)}
|
|
269
|
+
|
|
270
|
+
{entry.tools && entry.tools.length > 0 && (
|
|
271
|
+
<PluginToolsSection pid={pid} tools={entry.tools} isActive={isActive} />
|
|
272
|
+
)}
|
|
273
|
+
|
|
274
|
+
{isActive && (
|
|
275
|
+
<div className="flex justify-end border-t border-border pt-2">
|
|
276
|
+
<button
|
|
277
|
+
onClick={handleDeactivate}
|
|
278
|
+
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
|
+
>
|
|
280
|
+
<WifiOff className="h-3.5 w-3.5" /> Desactivar
|
|
281
|
+
</button>
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
</PluginCard>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
@@ -97,7 +97,8 @@ export function SkillsInspectorPanel() {
|
|
|
97
97
|
};
|
|
98
98
|
|
|
99
99
|
return (
|
|
100
|
-
<div className="
|
|
100
|
+
<div className="space-y-6">
|
|
101
|
+
<div className="grid gap-6 lg:grid-cols-2 lg:items-start">
|
|
101
102
|
<Section
|
|
102
103
|
title={t("settings_ui.inspector_title")}
|
|
103
104
|
description={t("settings_ui.inspector_desc")}
|
|
@@ -142,31 +143,6 @@ export function SkillsInspectorPanel() {
|
|
|
142
143
|
</div>
|
|
143
144
|
</Section>
|
|
144
145
|
|
|
145
|
-
<Section
|
|
146
|
-
title={t("settings_ui.thresholds_title")}
|
|
147
|
-
description={t("settings_ui.thresholds_desc")}
|
|
148
|
-
>
|
|
149
|
-
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
150
|
-
{knobs().map((k) => (
|
|
151
|
-
<Field key={k.key} label={k.label} hint={k.hint}>
|
|
152
|
-
<Input
|
|
153
|
-
type="number"
|
|
154
|
-
step={k.step}
|
|
155
|
-
min={k.min}
|
|
156
|
-
max={k.max}
|
|
157
|
-
defaultValue={String(cfg[k.key])}
|
|
158
|
-
disabled={busy}
|
|
159
|
-
onBlur={(ev) => {
|
|
160
|
-
const n = Number(ev.target.value);
|
|
161
|
-
if (Number.isFinite(n) && n !== cfg[k.key]) apply({ [k.key]: n });
|
|
162
|
-
}}
|
|
163
|
-
className="max-w-[12rem]"
|
|
164
|
-
/>
|
|
165
|
-
</Field>
|
|
166
|
-
))}
|
|
167
|
-
</div>
|
|
168
|
-
</Section>
|
|
169
|
-
|
|
170
146
|
<Section
|
|
171
147
|
title={t("settings_ui.test_title")}
|
|
172
148
|
description={t("settings_ui.test_desc")}
|
|
@@ -226,6 +202,32 @@ export function SkillsInspectorPanel() {
|
|
|
226
202
|
)}
|
|
227
203
|
</div>
|
|
228
204
|
</Section>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<Section
|
|
208
|
+
title={t("settings_ui.thresholds_title")}
|
|
209
|
+
description={t("settings_ui.thresholds_desc")}
|
|
210
|
+
>
|
|
211
|
+
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 xl:grid-cols-4">
|
|
212
|
+
{knobs().map((k) => (
|
|
213
|
+
<Field key={k.key} label={k.label} hint={k.hint}>
|
|
214
|
+
<Input
|
|
215
|
+
type="number"
|
|
216
|
+
step={k.step}
|
|
217
|
+
min={k.min}
|
|
218
|
+
max={k.max}
|
|
219
|
+
defaultValue={String(cfg[k.key])}
|
|
220
|
+
disabled={busy}
|
|
221
|
+
onBlur={(ev) => {
|
|
222
|
+
const n = Number(ev.target.value);
|
|
223
|
+
if (Number.isFinite(n) && n !== cfg[k.key]) apply({ [k.key]: n });
|
|
224
|
+
}}
|
|
225
|
+
className="max-w-[12rem]"
|
|
226
|
+
/>
|
|
227
|
+
</Field>
|
|
228
|
+
))}
|
|
229
|
+
</div>
|
|
230
|
+
</Section>
|
|
229
231
|
</div>
|
|
230
232
|
);
|
|
231
233
|
}
|
|
@@ -6,15 +6,13 @@ import { http } from "../http";
|
|
|
6
6
|
export type IntegrationScope = "project" | "global";
|
|
7
7
|
|
|
8
8
|
// Status returned by a plugin's status endpoint. Common fields plus
|
|
9
|
-
// plugin-specific extras (Asana adds user/workspace
|
|
9
|
+
// plugin-specific extras (Asana adds user/workspace, GitHub adds user_login…),
|
|
10
|
+
// so it carries an index signature for the generic component to read.
|
|
10
11
|
export interface IntegrationStatus {
|
|
11
12
|
slug: string;
|
|
12
13
|
status: string;
|
|
13
14
|
is_enabled: boolean;
|
|
14
|
-
|
|
15
|
-
user_email?: string | null;
|
|
16
|
-
workspace_gid?: string | null;
|
|
17
|
-
workspace_name?: string | null;
|
|
15
|
+
[key: string]: unknown;
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
export interface PluginTool {
|
|
@@ -22,6 +20,34 @@ export interface PluginTool {
|
|
|
22
20
|
desc: string;
|
|
23
21
|
}
|
|
24
22
|
|
|
23
|
+
// Declarative UI descriptor (mirrors the plugin's `ui` in core). Lets the
|
|
24
|
+
// generic PluginConnect component render each plugin's config form.
|
|
25
|
+
export interface PluginConfigField {
|
|
26
|
+
key: string;
|
|
27
|
+
label: string;
|
|
28
|
+
type: "password" | "text";
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
help?: { label: string; url: string; urlLabel: string; steps: string[] };
|
|
31
|
+
}
|
|
32
|
+
export interface PluginSelect {
|
|
33
|
+
key: string;
|
|
34
|
+
label: string;
|
|
35
|
+
action: string;
|
|
36
|
+
listKey: string;
|
|
37
|
+
valueKey: string;
|
|
38
|
+
labelKey: string;
|
|
39
|
+
}
|
|
40
|
+
export interface PluginConnectedField {
|
|
41
|
+
key: string;
|
|
42
|
+
label: string;
|
|
43
|
+
}
|
|
44
|
+
export interface PluginUi {
|
|
45
|
+
accent?: string;
|
|
46
|
+
configFields: PluginConfigField[];
|
|
47
|
+
select?: PluginSelect;
|
|
48
|
+
connectedFields?: PluginConnectedField[];
|
|
49
|
+
}
|
|
50
|
+
|
|
25
51
|
// One entry of the plugin catalog with its resolved status for this project.
|
|
26
52
|
export interface CatalogEntry {
|
|
27
53
|
slug: string;
|
|
@@ -30,6 +56,7 @@ export interface CatalogEntry {
|
|
|
30
56
|
description: string;
|
|
31
57
|
auth: string;
|
|
32
58
|
tools?: PluginTool[];
|
|
59
|
+
ui?: PluginUi | null;
|
|
33
60
|
coming_soon: boolean;
|
|
34
61
|
status: IntegrationStatus;
|
|
35
62
|
resolved_scope: IntegrationScope | null;
|
|
@@ -1,28 +1,26 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import useSWR from "swr";
|
|
3
|
-
import {
|
|
3
|
+
import { Puzzle, Wrench } from "lucide-react";
|
|
4
4
|
import { Integrations, type CatalogEntry, type IntegrationScope } from "../../lib/api";
|
|
5
5
|
import { cn } from "../../lib/cn";
|
|
6
6
|
import { Section } from "../../components/Section";
|
|
7
7
|
import { Empty, Loading } from "../../components/ui";
|
|
8
|
-
import {
|
|
8
|
+
import { PluginConnect } from "../../components/integrations/PluginConnect";
|
|
9
9
|
import { ComingSoonPlugin } from "../../components/integrations/ComingSoonPlugin";
|
|
10
|
-
import { McpsTab } from "./McpsTab";
|
|
11
10
|
|
|
12
|
-
type SubTab = "plugins" | "
|
|
11
|
+
type SubTab = "plugins" | "tools";
|
|
13
12
|
|
|
14
13
|
const SUBTABS: { value: SubTab; label: string; icon: typeof Puzzle }[] = [
|
|
15
14
|
{ value: "plugins", label: "Plugins", icon: Puzzle },
|
|
16
|
-
{ value: "mcp", label: "MCP Servers", icon: Network },
|
|
17
15
|
{ value: "tools", label: "Tools", icon: Wrench },
|
|
18
16
|
];
|
|
19
17
|
|
|
20
|
-
// Renders
|
|
18
|
+
// Renders a connectable plugin (via the generic PluginConnect, driven by its
|
|
19
|
+
// `ui` descriptor) or a coming-soon placeholder. MCP servers are NOT shown here
|
|
20
|
+
// — they have their own top-level "MCPs" nav item.
|
|
21
21
|
function PluginRow({ pid, scope, entry }: { pid: string; scope: IntegrationScope; entry: CatalogEntry }) {
|
|
22
|
-
if (entry.coming_soon) return <ComingSoonPlugin entry={entry} />;
|
|
23
|
-
|
|
24
|
-
// Implemented plugin without a bespoke UI yet — fall back to the placeholder.
|
|
25
|
-
return <ComingSoonPlugin entry={entry} />;
|
|
22
|
+
if (entry.coming_soon || !entry.ui) return <ComingSoonPlugin entry={entry} />;
|
|
23
|
+
return <PluginConnect pid={pid} scope={scope} entry={entry} />;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
function PluginsSection({ pid, scope }: { pid: string; scope: IntegrationScope }) {
|
|
@@ -95,7 +93,7 @@ export function IntegrationsTab({ pid }: { pid: string }) {
|
|
|
95
93
|
return (
|
|
96
94
|
<Section
|
|
97
95
|
title="Integrations"
|
|
98
|
-
description="Plugins
|
|
96
|
+
description="Plugins y tools disponibles para este proyecto"
|
|
99
97
|
>
|
|
100
98
|
{/* Scope selector — a real project can use its own integrations or the
|
|
101
99
|
global (default-project) ones. */}
|
|
@@ -139,7 +137,6 @@ export function IntegrationsTab({ pid }: { pid: string }) {
|
|
|
139
137
|
</div>
|
|
140
138
|
|
|
141
139
|
{tab === "plugins" && <PluginsSection pid={pid} scope={scope} />}
|
|
142
|
-
{tab === "mcp" && <McpsTab pid={pid} />}
|
|
143
140
|
{tab === "tools" && <ToolsSection pid={pid} />}
|
|
144
141
|
</Section>
|
|
145
142
|
);
|