@agentprojectcontext/apx 1.60.1 → 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.
- package/package.json +1 -1
- package/src/core/agent/super-agent.js +7 -1
- package/src/core/agent/tools/handlers/_github.js +22 -0
- package/src/core/agent/tools/handlers/call-runtime.js +185 -76
- 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/channels/telegram/dispatch.js +24 -1
- package/src/core/channels/telegram/reply.js +79 -2
- package/src/core/integrations/catalog.js +17 -27
- package/src/core/integrations/plugins/asana.js +22 -0
- package/src/core/integrations/plugins/github.js +160 -0
- package/src/core/stores/runtime-callbacks.js +107 -0
- package/src/host/daemon/callback-reconciler.js +87 -0
- package/src/host/daemon/index.js +7 -0
- package/src/interfaces/web/dist/assets/index-BuII-tAi.css +1 -0
- package/src/interfaces/web/dist/assets/index-CFcs16SV.js +778 -0
- package/src/interfaces/web/dist/assets/index-CFcs16SV.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +3 -3
- package/src/interfaces/web/src/components/integrations/ComingSoonPlugin.tsx +3 -6
- package/src/interfaces/web/src/components/integrations/PluginConnect.tsx +300 -0
- package/src/interfaces/web/src/components/integrations/PluginToolsSection.tsx +7 -8
- package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +28 -26
- package/src/interfaces/web/src/i18n/en.ts +57 -0
- package/src/interfaces/web/src/i18n/es.ts +57 -0
- package/src/interfaces/web/src/lib/api/integrations.ts +28 -5
- package/src/interfaces/web/src/screens/project/IntegrationsTab.tsx +28 -38
- package/src/interfaces/web/src/screens/project/McpsTab.tsx +113 -81
- package/src/interfaces/web/dist/assets/index-DFNV6BWh.js +0 -761
- 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-CFcs16SV.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>
|
|
@@ -2218,9 +2218,9 @@
|
|
|
2218
2218
|
}
|
|
2219
2219
|
},
|
|
2220
2220
|
"node_modules/caniuse-lite": {
|
|
2221
|
-
"version": "1.0.
|
|
2222
|
-
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.
|
|
2223
|
-
"integrity": "sha512-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -0,0 +1,300 @@
|
|
|
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 { t } from "../../i18n";
|
|
7
|
+
import type { TKey } from "../../i18n";
|
|
8
|
+
import { PluginCard } from "./PluginCard";
|
|
9
|
+
import { PluginToolsSection } from "./PluginToolsSection";
|
|
10
|
+
|
|
11
|
+
// One generic component that renders any token-based plugin's config form from
|
|
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);
|
|
17
|
+
|
|
18
|
+
function AsanaLogo({ className }: { className?: string }) {
|
|
19
|
+
return (
|
|
20
|
+
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
|
21
|
+
<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" />
|
|
22
|
+
</svg>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Accent = { text: string; border: string; hover: string; ring: string; wrap: string };
|
|
27
|
+
const ACCENTS: Record<string, Accent> = {
|
|
28
|
+
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" },
|
|
29
|
+
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" },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function iconFor(slug: string, accent: Accent): ReactNode {
|
|
33
|
+
if (slug === "github") return <Github className={cn("h-6 w-6", accent.text)} />;
|
|
34
|
+
if (slug === "asana") return <AsanaLogo className={cn("h-6 w-6", accent.text)} />;
|
|
35
|
+
return <span className={cn("text-lg", accent.text)}>◆</span>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type Step = "idle" | "saving" | "validating" | "done";
|
|
39
|
+
type Opt = { value: string; label: string };
|
|
40
|
+
|
|
41
|
+
export function PluginConnect({ pid, scope, entry }: { pid: string; scope: IntegrationScope; entry: CatalogEntry }) {
|
|
42
|
+
const ui = entry.ui!;
|
|
43
|
+
const accent = ACCENTS[ui.accent || "rose"] || ACCENTS.rose;
|
|
44
|
+
|
|
45
|
+
const [expanded, setExpanded] = useState(false);
|
|
46
|
+
const [values, setValues] = useState<Record<string, string>>({});
|
|
47
|
+
const [reveal, setReveal] = useState<Record<string, boolean>>({});
|
|
48
|
+
const [showHelp, setShowHelp] = useState<string | null>(null);
|
|
49
|
+
const [step, setStep] = useState<Step>("idle");
|
|
50
|
+
const [error, setError] = useState<string | null>(null);
|
|
51
|
+
const [selectOptions, setSelectOptions] = useState<Opt[]>([]);
|
|
52
|
+
const [selectValue, setSelectValue] = useState("");
|
|
53
|
+
|
|
54
|
+
const { data: status, mutate, isLoading } = useSWR<IntegrationStatus>(
|
|
55
|
+
`integration-status-${entry.slug}-${pid}-${scope}`,
|
|
56
|
+
() => Integrations.status(pid, entry.slug, scope),
|
|
57
|
+
{ shouldRetryOnError: false },
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const isActive = status?.status === "active" && status.is_enabled === true;
|
|
61
|
+
const busy = step === "saving" || step === "validating";
|
|
62
|
+
const showForm = !isActive && selectOptions.length === 0;
|
|
63
|
+
|
|
64
|
+
async function handleConnect() {
|
|
65
|
+
if (ui.configFields.some((f) => !values[f.key]?.trim())) return;
|
|
66
|
+
setStep("saving");
|
|
67
|
+
setError(null);
|
|
68
|
+
try {
|
|
69
|
+
await Integrations.configure(pid, entry.slug, scope, { ...values });
|
|
70
|
+
setStep("validating");
|
|
71
|
+
const result = (await Integrations.validate(pid, entry.slug, scope)) as unknown as Record<string, unknown>;
|
|
72
|
+
await mutate();
|
|
73
|
+
if (ui.select && !result[ui.select.key]) {
|
|
74
|
+
const data = (await Integrations.action(pid, entry.slug, ui.select.action, scope)) as Record<string, unknown>;
|
|
75
|
+
const list = (data[ui.select.listKey] as Record<string, unknown>[]) || [];
|
|
76
|
+
if (list.length > 1) {
|
|
77
|
+
setSelectOptions(list.map((o) => ({ value: String(o[ui.select!.valueKey]), label: String(o[ui.select!.labelKey]) })));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
setStep("done");
|
|
81
|
+
setValues({});
|
|
82
|
+
} catch (err) {
|
|
83
|
+
setError(err instanceof Error ? err.message : t("integrations.err_connect"));
|
|
84
|
+
setStep("idle");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function handleSelect() {
|
|
89
|
+
if (!selectValue || !ui.select) return;
|
|
90
|
+
setStep("saving");
|
|
91
|
+
setError(null);
|
|
92
|
+
try {
|
|
93
|
+
await Integrations.configure(pid, entry.slug, scope, { [ui.select.key]: selectValue });
|
|
94
|
+
await Integrations.validate(pid, entry.slug, scope);
|
|
95
|
+
await mutate();
|
|
96
|
+
setSelectOptions([]);
|
|
97
|
+
setStep("done");
|
|
98
|
+
} catch (err) {
|
|
99
|
+
setError(err instanceof Error ? err.message : t("integrations.err_generic"));
|
|
100
|
+
setStep("idle");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function handleDeactivate() {
|
|
105
|
+
setError(null);
|
|
106
|
+
try {
|
|
107
|
+
await Integrations.deactivate(pid, entry.slug, scope);
|
|
108
|
+
await mutate();
|
|
109
|
+
} catch (err) {
|
|
110
|
+
setError(err instanceof Error ? err.message : t("integrations.err_generic"));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
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
|
+
|
|
122
|
+
return (
|
|
123
|
+
<PluginCard
|
|
124
|
+
icon={
|
|
125
|
+
<div className={cn("flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-2xl border bg-gradient-to-br", accent.wrap)}>
|
|
126
|
+
{iconFor(entry.slug, accent)}
|
|
127
|
+
</div>
|
|
128
|
+
}
|
|
129
|
+
title={entry.name}
|
|
130
|
+
description={entry.description}
|
|
131
|
+
hasTools={(entry.tools?.length ?? 0) > 0}
|
|
132
|
+
badges={
|
|
133
|
+
<span
|
|
134
|
+
className={cn(
|
|
135
|
+
"flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px]",
|
|
136
|
+
isActive ? "border-emerald-700 bg-emerald-900/20 text-emerald-400" : "border-border bg-muted text-muted-foreground",
|
|
137
|
+
)}
|
|
138
|
+
>
|
|
139
|
+
<span className={cn("h-1.5 w-1.5 rounded-full", isActive ? "bg-emerald-400" : "bg-muted-foreground")} />
|
|
140
|
+
{badgeLabel}
|
|
141
|
+
</span>
|
|
142
|
+
}
|
|
143
|
+
expanded={expanded}
|
|
144
|
+
onToggle={() => setExpanded((v) => !v)}
|
|
145
|
+
>
|
|
146
|
+
<div className="space-y-4 p-4">
|
|
147
|
+
{error && (
|
|
148
|
+
<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">
|
|
149
|
+
<AlertCircle className="h-3.5 w-3.5 flex-shrink-0" />
|
|
150
|
+
<span className="flex-1">{error}</span>
|
|
151
|
+
<button onClick={() => setError(null)}><X className="h-3.5 w-3.5" /></button>
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
{/* Connected summary */}
|
|
156
|
+
{isActive && selectOptions.length === 0 && (
|
|
157
|
+
<div className="space-y-1 rounded-xl border border-emerald-700/30 bg-emerald-900/10 p-3">
|
|
158
|
+
<div className="flex items-center gap-2">
|
|
159
|
+
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-400" />
|
|
160
|
+
<span className="text-xs font-medium text-emerald-300">{t("integrations.connected")}</span>
|
|
161
|
+
</div>
|
|
162
|
+
{(ui.connectedFields || []).map((key) => {
|
|
163
|
+
const v = status?.[key];
|
|
164
|
+
if (!v) return null;
|
|
165
|
+
return (
|
|
166
|
+
<p key={key} className="pl-5 text-[10px] text-muted-foreground">
|
|
167
|
+
{tk(`integrations.${entry.slug}.connected.${key}`)}: {String(v)}
|
|
168
|
+
</p>
|
|
169
|
+
);
|
|
170
|
+
})}
|
|
171
|
+
</div>
|
|
172
|
+
)}
|
|
173
|
+
|
|
174
|
+
{/* Post-validate selection (e.g. Asana workspace) */}
|
|
175
|
+
{selectOptions.length > 1 && ui.select && (
|
|
176
|
+
<div className="space-y-2">
|
|
177
|
+
<p className="text-[11px] text-muted-foreground">{tk(`integrations.${entry.slug}.select_label`)}:</p>
|
|
178
|
+
<div className="flex gap-2">
|
|
179
|
+
<select
|
|
180
|
+
value={selectValue}
|
|
181
|
+
onChange={(e) => setSelectValue(e.target.value)}
|
|
182
|
+
className={cn("flex-1 rounded-lg border border-border bg-background px-2 py-1.5 text-xs outline-none", accent.ring)}
|
|
183
|
+
>
|
|
184
|
+
<option value="">{t("integrations.select_placeholder")}</option>
|
|
185
|
+
{selectOptions.map((o) => (
|
|
186
|
+
<option key={o.value} value={o.value}>{o.label}</option>
|
|
187
|
+
))}
|
|
188
|
+
</select>
|
|
189
|
+
<button
|
|
190
|
+
onClick={handleSelect}
|
|
191
|
+
disabled={!selectValue || busy}
|
|
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)}
|
|
193
|
+
>
|
|
194
|
+
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : t("integrations.confirm")}
|
|
195
|
+
</button>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
|
|
200
|
+
{/* Config form */}
|
|
201
|
+
{showForm && (
|
|
202
|
+
<div className="space-y-3">
|
|
203
|
+
<p className="text-xs font-semibold text-foreground">{t("integrations.credentials", { name: entry.name })}</p>
|
|
204
|
+
{ui.configFields.map((field) => (
|
|
205
|
+
<div key={field.key} className="space-y-2">
|
|
206
|
+
{field.help_url && (
|
|
207
|
+
<div className="overflow-hidden rounded-lg border border-border">
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
onClick={() => setShowHelp((v) => (v === field.key ? null : field.key))}
|
|
211
|
+
className="flex w-full items-center justify-between px-3 py-2 text-left transition-colors hover:bg-muted/40"
|
|
212
|
+
>
|
|
213
|
+
<span className="text-[11px] text-muted-foreground">
|
|
214
|
+
{tk(`integrations.${entry.slug}.fields.${field.key}.help_label`)} ·{" "}
|
|
215
|
+
<a
|
|
216
|
+
href={field.help_url}
|
|
217
|
+
target="_blank"
|
|
218
|
+
rel="noreferrer"
|
|
219
|
+
onClick={(e) => e.stopPropagation()}
|
|
220
|
+
className={cn("inline-flex items-center gap-0.5 hover:underline", accent.text)}
|
|
221
|
+
>
|
|
222
|
+
{field.help_url_label} <ExternalLink className="h-2.5 w-2.5" />
|
|
223
|
+
</a>
|
|
224
|
+
</span>
|
|
225
|
+
<ChevronDown className={cn("h-3.5 w-3.5 flex-shrink-0 text-muted-foreground transition-transform", showHelp === field.key && "rotate-180")} />
|
|
226
|
+
</button>
|
|
227
|
+
{showHelp === field.key && (
|
|
228
|
+
<div className="space-y-1.5 border-t border-border px-3 pb-3 pt-2.5">
|
|
229
|
+
{tk(`integrations.${entry.slug}.fields.${field.key}.help_steps`).split("\n").map((s, i) => (
|
|
230
|
+
<div key={i} className="flex items-start gap-2">
|
|
231
|
+
<span className={cn("mt-0.5 flex-shrink-0 font-mono text-[10px]", accent.text)}>{i + 1}.</span>
|
|
232
|
+
<p className="text-[11px] text-muted-foreground">{s}</p>
|
|
233
|
+
</div>
|
|
234
|
+
))}
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
<div>
|
|
240
|
+
<label className="mb-1 block text-[10px] text-muted-foreground">{tk(`integrations.${entry.slug}.fields.${field.key}.label`)}</label>
|
|
241
|
+
<div className="relative">
|
|
242
|
+
<input
|
|
243
|
+
type={field.type === "password" && !reveal[field.key] ? "password" : "text"}
|
|
244
|
+
placeholder={field.placeholder}
|
|
245
|
+
value={values[field.key] || ""}
|
|
246
|
+
onChange={(e) => setValues((v) => ({ ...v, [field.key]: e.target.value }))}
|
|
247
|
+
onKeyDown={(e) => e.key === "Enter" && handleConnect()}
|
|
248
|
+
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)}
|
|
249
|
+
/>
|
|
250
|
+
{field.type === "password" && (
|
|
251
|
+
<button
|
|
252
|
+
type="button"
|
|
253
|
+
onClick={() => setReveal((r) => ({ ...r, [field.key]: !r[field.key] }))}
|
|
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"
|
|
255
|
+
>
|
|
256
|
+
{reveal[field.key] ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
|
257
|
+
{reveal[field.key] ? t("integrations.hide") : t("integrations.reveal")}
|
|
258
|
+
</button>
|
|
259
|
+
)}
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
))}
|
|
264
|
+
|
|
265
|
+
{step === "validating" && (
|
|
266
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
267
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" /> {t("integrations.verifying")}
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
|
|
271
|
+
<button
|
|
272
|
+
onClick={handleConnect}
|
|
273
|
+
disabled={ui.configFields.some((f) => !values[f.key]?.trim()) || busy}
|
|
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)}
|
|
275
|
+
>
|
|
276
|
+
{busy ? (
|
|
277
|
+
<><Loader2 className="h-3.5 w-3.5 animate-spin" />{step === "saving" ? t("integrations.saving") : t("integrations.validating")}</>
|
|
278
|
+
) : t("integrations.connect")}
|
|
279
|
+
</button>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{entry.tools && entry.tools.length > 0 && (
|
|
284
|
+
<PluginToolsSection pid={pid} tools={entry.tools} isActive={isActive} />
|
|
285
|
+
)}
|
|
286
|
+
|
|
287
|
+
{isActive && (
|
|
288
|
+
<div className="flex justify-end border-t border-border pt-2">
|
|
289
|
+
<button
|
|
290
|
+
onClick={handleDeactivate}
|
|
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"
|
|
292
|
+
>
|
|
293
|
+
<WifiOff className="h-3.5 w-3.5" /> {t("integrations.deactivate")}
|
|
294
|
+
</button>
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
</PluginCard>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
@@ -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
|
-
|
|
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((
|
|
23
|
+
{tools.map((tool) => (
|
|
23
24
|
<div
|
|
24
|
-
key={
|
|
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">{
|
|
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">{
|
|
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
|
}
|
|
@@ -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
|
}
|
|
@@ -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;
|
|
@@ -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,30 @@ export interface PluginTool {
|
|
|
22
20
|
desc: string;
|
|
23
21
|
}
|
|
24
22
|
|
|
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.
|
|
26
|
+
export interface PluginConfigField {
|
|
27
|
+
key: string;
|
|
28
|
+
type: "password" | "text";
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
help_url?: string;
|
|
31
|
+
help_url_label?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface PluginSelect {
|
|
34
|
+
key: string;
|
|
35
|
+
action: string;
|
|
36
|
+
listKey: string;
|
|
37
|
+
valueKey: string;
|
|
38
|
+
labelKey: string;
|
|
39
|
+
}
|
|
40
|
+
export interface PluginUi {
|
|
41
|
+
accent?: string;
|
|
42
|
+
configFields: PluginConfigField[];
|
|
43
|
+
select?: PluginSelect;
|
|
44
|
+
connectedFields?: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
25
47
|
// One entry of the plugin catalog with its resolved status for this project.
|
|
26
48
|
export interface CatalogEntry {
|
|
27
49
|
slug: string;
|
|
@@ -30,6 +52,7 @@ export interface CatalogEntry {
|
|
|
30
52
|
description: string;
|
|
31
53
|
auth: string;
|
|
32
54
|
tools?: PluginTool[];
|
|
55
|
+
ui?: PluginUi | null;
|
|
33
56
|
coming_soon: boolean;
|
|
34
57
|
status: IntegrationStatus;
|
|
35
58
|
resolved_scope: IntegrationScope | null;
|