@agentprojectcontext/apx 1.60.1 → 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/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
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
2
|
import useSWR from "swr";
|
|
3
3
|
import {
|
|
4
|
-
|
|
4
|
+
ChevronDown, FlaskConical, Globe, Network, Pencil, Plus, ScrollText,
|
|
5
|
+
Terminal, Trash2, Wrench, X, XCircle,
|
|
5
6
|
} from "lucide-react";
|
|
6
|
-
import { Mcps, Vars, type McpAddBody, type McpScope, type
|
|
7
|
+
import { Mcps, Vars, type McpAddBody, type McpScope, type McpLogsResult, type VarsList } from "../../lib/api";
|
|
7
8
|
import type { McpEntry } from "../../types/daemon";
|
|
9
|
+
import { cn } from "../../lib/cn";
|
|
8
10
|
import { Section } from "../../components/Section";
|
|
9
11
|
import { Badge, Button, Dialog, Empty, Field, Input, Loading, Switch } from "../../components/ui";
|
|
10
12
|
import { Tip } from "../../components/ui/tip";
|
|
@@ -19,6 +21,31 @@ type DialogMode =
|
|
|
19
21
|
| { kind: "new" }
|
|
20
22
|
| { kind: "edit"; entry: McpEntry };
|
|
21
23
|
|
|
24
|
+
// Per-row live test state: mirrors PandaProject's MCP cards — a colored dot +
|
|
25
|
+
// tool count come from the last on-demand test (tools/list) of each server.
|
|
26
|
+
type McpResult = { busy?: boolean; ok?: boolean; error?: string; tools?: { name: string; description: string }[] };
|
|
27
|
+
|
|
28
|
+
// Short one-liner describing how a server connects (http url or stdio command),
|
|
29
|
+
// shown under the name like Panda's config summary. Prefers an explicit
|
|
30
|
+
// description in the raw config when present.
|
|
31
|
+
function mcpSummary(m: McpEntry): string {
|
|
32
|
+
const raw = (m as unknown as { raw?: Record<string, unknown> }).raw || {};
|
|
33
|
+
if (typeof raw.description === "string" && raw.description.trim()) return raw.description.trim();
|
|
34
|
+
if (m.transport === "http" && m.url) return m.url.length > 64 ? m.url.slice(0, 61) + "…" : m.url;
|
|
35
|
+
const cmd = [m.command, ...(m.args || [])].filter(Boolean).join(" ");
|
|
36
|
+
if (cmd) return "$ " + (cmd.length > 64 ? cmd.slice(0, 61) + "…" : cmd);
|
|
37
|
+
return "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Colored status dot: red on a failed test, emerald when tested-ok or enabled,
|
|
41
|
+
// muted when disabled, amber while testing.
|
|
42
|
+
function dotClass(enabled: boolean, res?: McpResult): string {
|
|
43
|
+
if (res?.busy) return "bg-amber-400 animate-pulse";
|
|
44
|
+
if (res?.ok === false) return "bg-red-400";
|
|
45
|
+
if (res?.ok) return "bg-emerald-400";
|
|
46
|
+
return enabled ? "bg-emerald-500/70" : "bg-slate-500";
|
|
47
|
+
}
|
|
48
|
+
|
|
22
49
|
const SOURCE_TONE: Record<string, "info" | "muted" | "success"> = {
|
|
23
50
|
apc: "info",
|
|
24
51
|
runtime: "success",
|
|
@@ -51,7 +78,8 @@ export function McpsTab({ pid }: { pid: string }) {
|
|
|
51
78
|
const vars = useSWR<VarsList>(`/projects/${pid}/vars`, () => Vars.list(pid));
|
|
52
79
|
const [dialog, setDialog] = useState<DialogMode>(null);
|
|
53
80
|
const [activeMcp, setActiveMcp] = useState<string | null>(null);
|
|
54
|
-
const [
|
|
81
|
+
const [results, setResults] = useState<Record<string, McpResult>>({});
|
|
82
|
+
const [expandedTools, setExpandedTools] = useState<Record<string, boolean>>({});
|
|
55
83
|
|
|
56
84
|
const varNames = useMemo(
|
|
57
85
|
() => (vars.data ? Object.keys(vars.data.effective).sort() : []),
|
|
@@ -75,15 +103,20 @@ export function McpsTab({ pid }: { pid: string }) {
|
|
|
75
103
|
|
|
76
104
|
const runTest = async (name: string) => {
|
|
77
105
|
setActiveMcp(name);
|
|
78
|
-
|
|
106
|
+
setResults((prev) => ({ ...prev, [name]: { busy: true } }));
|
|
107
|
+
setExpandedTools((prev) => ({ ...prev, [name]: true }));
|
|
79
108
|
try {
|
|
80
109
|
const result = await Mcps.test(pid, name);
|
|
81
|
-
|
|
110
|
+
setResults((prev) => ({ ...prev, [name]: { ok: result.ok, error: result.error, tools: result.tools } }));
|
|
82
111
|
} catch (e: any) {
|
|
83
|
-
|
|
112
|
+
setResults((prev) => ({ ...prev, [name]: { ok: false, error: e?.message || "error" } }));
|
|
84
113
|
}
|
|
85
114
|
};
|
|
86
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
|
+
|
|
87
120
|
return (
|
|
88
121
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-4">
|
|
89
122
|
<div className="lg:col-span-3">
|
|
@@ -117,11 +150,32 @@ export function McpsTab({ pid }: { pid: string }) {
|
|
|
117
150
|
{list.isLoading && <Loading />}
|
|
118
151
|
{!list.isLoading && (list.data?.length ?? 0) === 0 && <Empty>{t("project.mcps.empty")}</Empty>}
|
|
119
152
|
|
|
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
|
+
|
|
120
168
|
<ul className="space-y-2 text-sm">
|
|
121
169
|
{(list.data || []).map((m) => {
|
|
122
170
|
const writable = m.source === "apc" || m.source === "runtime" || m.source === "global";
|
|
123
171
|
const scopeForRemove: McpScope = sourceToScope(m.source);
|
|
124
172
|
const isActive = activeMcp === m.name;
|
|
173
|
+
const enabled = m.enabled !== false;
|
|
174
|
+
const res = results[m.name];
|
|
175
|
+
const tools = res?.tools || [];
|
|
176
|
+
const open = !!expandedTools[m.name];
|
|
177
|
+
const summary = mcpSummary(m);
|
|
178
|
+
const Icon = m.transport === "http" ? Globe : Network;
|
|
125
179
|
return (
|
|
126
180
|
<li
|
|
127
181
|
key={`${m.source}-${m.name}`}
|
|
@@ -132,45 +186,79 @@ export function McpsTab({ pid }: { pid: string }) {
|
|
|
132
186
|
onClick={() => setActiveMcp(m.name)}
|
|
133
187
|
role="button"
|
|
134
188
|
>
|
|
135
|
-
<div className="flex
|
|
136
|
-
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
{(
|
|
140
|
-
</
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
189
|
+
<div className="flex items-start gap-3">
|
|
190
|
+
{/* Icon + colored status dot */}
|
|
191
|
+
<div className="relative mt-0.5 flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg border border-border bg-background">
|
|
192
|
+
<Icon size={16} className="text-muted-fg" />
|
|
193
|
+
<span className={cn("absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full border-2 border-card", dotClass(enabled, res))} />
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<div className="min-w-0 flex-1">
|
|
197
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
198
|
+
<span className="font-medium">{m.name}</span>
|
|
199
|
+
<Badge tone={SOURCE_TONE[m.source] ?? "muted"}>{sourceLabel(m.source)}</Badge>
|
|
200
|
+
<span className="ml-auto text-xs text-muted-fg">{(m.transport || "stdio").toUpperCase()}</span>
|
|
201
|
+
<div onClick={(e) => e.stopPropagation()}>
|
|
202
|
+
<Switch checked={enabled} onChange={() => toggleEnabled(m)} label="" />
|
|
203
|
+
</div>
|
|
204
|
+
<Tip content={t("project.mcps.test_btn")}>
|
|
205
|
+
<Button size="sm" variant="ghost" onClick={(e) => { e.stopPropagation(); runTest(m.name); }} aria-label={t("project.mcps.test_btn")}>
|
|
206
|
+
{res?.busy ? <FlaskConical size={13} className="animate-pulse" /> : <FlaskConical size={13} />}
|
|
207
|
+
</Button>
|
|
208
|
+
</Tip>
|
|
209
|
+
<Tip content={t("project.mcps.logs_btn")}>
|
|
210
|
+
<Button size="sm" variant="ghost" onClick={(e) => { e.stopPropagation(); setActiveMcp(m.name); }} aria-label={t("project.mcps.logs_btn")}>
|
|
211
|
+
<ScrollText size={13} />
|
|
212
|
+
</Button>
|
|
213
|
+
</Tip>
|
|
214
|
+
{writable && (
|
|
215
|
+
<Tip content={t("project.mcps.edit_btn")}>
|
|
216
|
+
<Button size="sm" variant="ghost" onClick={(e) => { e.stopPropagation(); setDialog({ kind: "edit", entry: m }); }} aria-label={t("project.mcps.edit_btn")}>
|
|
217
|
+
<Pencil size={13} />
|
|
218
|
+
</Button>
|
|
219
|
+
</Tip>
|
|
220
|
+
)}
|
|
221
|
+
{writable && (
|
|
222
|
+
<Button size="sm" variant="destructive" onClick={(e) => { e.stopPropagation(); remove(m.name, scopeForRemove); }}>
|
|
223
|
+
<Trash2 size={13} />
|
|
224
|
+
</Button>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
{summary && <p className="mt-0.5 truncate font-mono text-xs text-muted-fg">{summary}</p>}
|
|
229
|
+
|
|
230
|
+
{res?.ok === false && res.error && (
|
|
231
|
+
<p className="mt-1 flex items-start gap-1 text-xs text-red-400">
|
|
232
|
+
<XCircle size={12} className="mt-0.5 flex-shrink-0" /> <span className="break-words">{res.error}</span>
|
|
233
|
+
</p>
|
|
234
|
+
)}
|
|
235
|
+
|
|
236
|
+
{tools.length > 0 && (
|
|
237
|
+
<div className="mt-1.5" onClick={(e) => e.stopPropagation()}>
|
|
238
|
+
<button
|
|
239
|
+
type="button"
|
|
240
|
+
onClick={() => setExpandedTools((prev) => ({ ...prev, [m.name]: !prev[m.name] }))}
|
|
241
|
+
className="flex items-center gap-1 text-xs text-muted-fg transition-colors hover:text-fg"
|
|
242
|
+
>
|
|
243
|
+
<Wrench size={12} /> {tools.length} tools
|
|
244
|
+
<ChevronDown size={12} className={cn("transition-transform", open && "rotate-180")} />
|
|
245
|
+
</button>
|
|
246
|
+
{open && (
|
|
247
|
+
<div className="mt-1.5 flex flex-wrap gap-1.5">
|
|
248
|
+
{tools.slice(0, 40).map((tool) => (
|
|
249
|
+
<Tip key={tool.name} content={tool.description || "—"}>
|
|
250
|
+
<span className="inline-flex items-center gap-1 rounded border border-border bg-background px-1.5 py-0.5 font-mono text-[10px] text-muted-fg">
|
|
251
|
+
<Terminal size={10} /> {tool.name}
|
|
252
|
+
</span>
|
|
253
|
+
</Tip>
|
|
254
|
+
))}
|
|
255
|
+
{tools.length > 40 && <span className="text-[10px] text-muted-fg">… +{tools.length - 40}</span>}
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
259
|
+
)}
|
|
147
260
|
</div>
|
|
148
|
-
<Tip content={t("project.mcps.test_btn")}>
|
|
149
|
-
<Button size="sm" variant="ghost" onClick={(e) => { e.stopPropagation(); runTest(m.name); }} aria-label={t("project.mcps.test_btn")}>
|
|
150
|
-
<FlaskConical size={13} />
|
|
151
|
-
</Button>
|
|
152
|
-
</Tip>
|
|
153
|
-
<Tip content={t("project.mcps.logs_btn")}>
|
|
154
|
-
<Button size="sm" variant="ghost" onClick={(e) => { e.stopPropagation(); setActiveMcp(m.name); }} aria-label={t("project.mcps.logs_btn")}>
|
|
155
|
-
<ScrollText size={13} />
|
|
156
|
-
</Button>
|
|
157
|
-
</Tip>
|
|
158
|
-
{writable && (
|
|
159
|
-
<Tip content={t("project.mcps.edit_btn")}>
|
|
160
|
-
<Button size="sm" variant="ghost" onClick={(e) => { e.stopPropagation(); setDialog({ kind: "edit", entry: m }); }} aria-label={t("project.mcps.edit_btn")}>
|
|
161
|
-
<Pencil size={13} />
|
|
162
|
-
</Button>
|
|
163
|
-
</Tip>
|
|
164
|
-
)}
|
|
165
|
-
{writable && (
|
|
166
|
-
<Button size="sm" variant="destructive" onClick={(e) => { e.stopPropagation(); remove(m.name, scopeForRemove); }}>
|
|
167
|
-
<Trash2 size={13} />
|
|
168
|
-
</Button>
|
|
169
|
-
)}
|
|
170
261
|
</div>
|
|
171
|
-
{testFor?.name === m.name && (
|
|
172
|
-
<TestResultRow result={testFor.result} busy={!!testFor.busy} onClose={() => setTestFor(null)} />
|
|
173
|
-
)}
|
|
174
262
|
</li>
|
|
175
263
|
);
|
|
176
264
|
})}
|
|
@@ -190,49 +278,12 @@ export function McpsTab({ pid }: { pid: string }) {
|
|
|
190
278
|
</div>
|
|
191
279
|
|
|
192
280
|
<div className="lg:col-span-1">
|
|
193
|
-
<LogsPanel pid={pid} mcpName={activeMcp} runningTest={!!
|
|
281
|
+
<LogsPanel pid={pid} mcpName={activeMcp} runningTest={!!(activeMcp && results[activeMcp]?.busy)} />
|
|
194
282
|
</div>
|
|
195
283
|
</div>
|
|
196
284
|
);
|
|
197
285
|
}
|
|
198
286
|
|
|
199
|
-
function TestResultRow({
|
|
200
|
-
result,
|
|
201
|
-
busy,
|
|
202
|
-
onClose,
|
|
203
|
-
}: { result?: McpTestResult; busy: boolean; onClose: () => void }) {
|
|
204
|
-
return (
|
|
205
|
-
<div className="mt-2 rounded border border-border/60 bg-background/40 p-2 text-xs">
|
|
206
|
-
<div className="mb-1 flex items-center gap-2">
|
|
207
|
-
{busy && <span className="text-muted-fg">{t("project.mcps.testing")}</span>}
|
|
208
|
-
{!busy && result?.ok && (
|
|
209
|
-
<span className="flex items-center gap-1 text-emerald-400">
|
|
210
|
-
<CheckCircle2 size={12} /> {t("project.mcps.test_ok", { n: String(result.tool_count ?? 0) })}
|
|
211
|
-
</span>
|
|
212
|
-
)}
|
|
213
|
-
{!busy && result && !result.ok && (
|
|
214
|
-
<span className="flex items-center gap-1 text-red-400">
|
|
215
|
-
<XCircle size={12} /> {result.error}
|
|
216
|
-
</span>
|
|
217
|
-
)}
|
|
218
|
-
<button className="ml-auto text-muted-fg hover:text-fg" onClick={onClose}>×</button>
|
|
219
|
-
</div>
|
|
220
|
-
{!busy && result?.ok && result.tools && result.tools.length > 0 && (
|
|
221
|
-
<ul className="space-y-0.5 font-mono">
|
|
222
|
-
{result.tools.slice(0, 10).map((tool) => (
|
|
223
|
-
<li key={tool.name} className="truncate">
|
|
224
|
-
<span className="text-primary">{tool.name}</span>
|
|
225
|
-
{tool.description && <span className="ml-2 text-muted-fg">— {tool.description}</span>}
|
|
226
|
-
</li>
|
|
227
|
-
))}
|
|
228
|
-
{result.tools.length > 10 && (
|
|
229
|
-
<li className="text-muted-fg">… +{result.tools.length - 10}</li>
|
|
230
|
-
)}
|
|
231
|
-
</ul>
|
|
232
|
-
)}
|
|
233
|
-
</div>
|
|
234
|
-
);
|
|
235
|
-
}
|
|
236
287
|
|
|
237
288
|
// Right-side terminal-style live logs panel. Polls the daemon every 1.5s
|
|
238
289
|
// while a test is running (or every 4s when idle and an MCP is pinned) so
|