@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
|
@@ -1,28 +1,27 @@
|
|
|
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 {
|
|
10
|
+
import { t } from "../../i18n";
|
|
11
11
|
|
|
12
|
-
type SubTab = "plugins" | "
|
|
12
|
+
type SubTab = "plugins" | "tools";
|
|
13
13
|
|
|
14
|
-
const SUBTABS: { value: SubTab;
|
|
15
|
-
{ value: "plugins",
|
|
16
|
-
{ value: "
|
|
17
|
-
{ 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 },
|
|
18
17
|
];
|
|
19
18
|
|
|
20
|
-
// Renders
|
|
19
|
+
// Renders a connectable plugin (via the generic PluginConnect, driven by its
|
|
20
|
+
// `ui` descriptor) or a coming-soon placeholder. MCP servers are NOT shown here
|
|
21
|
+
// — they have their own top-level "MCPs" nav item.
|
|
21
22
|
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} />;
|
|
23
|
+
if (entry.coming_soon || !entry.ui) return <ComingSoonPlugin entry={entry} />;
|
|
24
|
+
return <PluginConnect pid={pid} scope={scope} entry={entry} />;
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
function PluginsSection({ pid, scope }: { pid: string; scope: IntegrationScope }) {
|
|
@@ -30,16 +29,13 @@ function PluginsSection({ pid, scope }: { pid: string; scope: IntegrationScope }
|
|
|
30
29
|
|
|
31
30
|
return (
|
|
32
31
|
<div className="space-y-3">
|
|
33
|
-
<p className="text-xs text-muted-foreground">
|
|
34
|
-
Plugins de canal y servicio instalables por proyecto. Se guardan en el ámbito
|
|
35
|
-
seleccionado arriba.
|
|
36
|
-
</p>
|
|
32
|
+
<p className="text-xs text-muted-foreground">{t("integrations.plugins_hint")}</p>
|
|
37
33
|
{isLoading && <Loading />}
|
|
38
34
|
{(catalog || []).map((entry) => (
|
|
39
35
|
<PluginRow key={entry.slug} pid={pid} scope={scope} entry={entry} />
|
|
40
36
|
))}
|
|
41
37
|
<div className="rounded-xl border border-dashed border-border p-6 text-center">
|
|
42
|
-
<p className="text-sm text-muted-foreground">
|
|
38
|
+
<p className="text-sm text-muted-foreground">{t("integrations.more_soon")}</p>
|
|
43
39
|
</div>
|
|
44
40
|
</div>
|
|
45
41
|
);
|
|
@@ -49,35 +45,33 @@ function ToolsSection({ pid }: { pid: string }) {
|
|
|
49
45
|
const { data: catalog } = useSWR(`integrations-catalog-${pid}`, () => Integrations.catalog(pid));
|
|
50
46
|
const rows = (catalog || [])
|
|
51
47
|
.filter((c) => !c.coming_soon && (c.tools?.length ?? 0) > 0)
|
|
52
|
-
.flatMap((c) => (c.tools || []).map((
|
|
48
|
+
.flatMap((c) => (c.tools || []).map((tool) => ({ ...tool, plugin: c.name, active: c.status.is_enabled })));
|
|
53
49
|
|
|
54
50
|
return (
|
|
55
51
|
<div className="space-y-3">
|
|
56
|
-
<p className="text-xs text-muted-foreground">
|
|
57
|
-
Tools que los plugins conectados exponen a los agentes de este proyecto.
|
|
58
|
-
</p>
|
|
52
|
+
<p className="text-xs text-muted-foreground">{t("integrations.tools_hint")}</p>
|
|
59
53
|
{rows.length === 0 ? (
|
|
60
|
-
<Empty>
|
|
54
|
+
<Empty>{t("integrations.tools_empty")}</Empty>
|
|
61
55
|
) : (
|
|
62
56
|
<ul className="space-y-2">
|
|
63
|
-
{rows.map((
|
|
64
|
-
<li key={
|
|
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")}>
|
|
65
59
|
<div className="flex items-center gap-2">
|
|
66
60
|
<Wrench className="h-3.5 w-3.5 text-muted-foreground" />
|
|
67
|
-
<span className="font-mono text-xs text-foreground">{
|
|
68
|
-
<span className="ml-auto text-[10px] text-muted-foreground">{
|
|
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>
|
|
69
63
|
<span
|
|
70
64
|
className={cn(
|
|
71
65
|
"rounded border px-1.5 py-0.5 text-[10px]",
|
|
72
|
-
|
|
66
|
+
tool.active
|
|
73
67
|
? "border-emerald-700/40 bg-emerald-900/20 text-emerald-400"
|
|
74
68
|
: "border-border bg-muted text-muted-foreground",
|
|
75
69
|
)}
|
|
76
70
|
>
|
|
77
|
-
{
|
|
71
|
+
{tool.active ? t("integrations.tool_active") : t("integrations.tool_inactive")}
|
|
78
72
|
</span>
|
|
79
73
|
</div>
|
|
80
|
-
<p className="mt-0.5 pl-5 text-[10px] text-muted-foreground">{
|
|
74
|
+
<p className="mt-0.5 pl-5 text-[10px] text-muted-foreground">{tool.desc}</p>
|
|
81
75
|
</li>
|
|
82
76
|
))}
|
|
83
77
|
</ul>
|
|
@@ -93,15 +87,12 @@ export function IntegrationsTab({ pid }: { pid: string }) {
|
|
|
93
87
|
const [scope, setScope] = useState<IntegrationScope>(isBase ? "global" : "project");
|
|
94
88
|
|
|
95
89
|
return (
|
|
96
|
-
<Section
|
|
97
|
-
title="Integrations"
|
|
98
|
-
description="Plugins, MCP servers y tools disponibles para este proyecto"
|
|
99
|
-
>
|
|
90
|
+
<Section title={t("integrations.title")} description={t("integrations.description")}>
|
|
100
91
|
{/* Scope selector — a real project can use its own integrations or the
|
|
101
92
|
global (default-project) ones. */}
|
|
102
93
|
{!isBase && (
|
|
103
94
|
<div className="mb-4 flex items-center gap-2">
|
|
104
|
-
<span className="text-xs text-muted-foreground"
|
|
95
|
+
<span className="text-xs text-muted-foreground">{t("integrations.scope_label")}</span>
|
|
105
96
|
{(["project", "global"] as const).map((s) => (
|
|
106
97
|
<button
|
|
107
98
|
key={s}
|
|
@@ -113,7 +104,7 @@ export function IntegrationsTab({ pid }: { pid: string }) {
|
|
|
113
104
|
: "border-border bg-muted/30 text-muted-foreground hover:bg-muted/50",
|
|
114
105
|
)}
|
|
115
106
|
>
|
|
116
|
-
{s === "project" ? "
|
|
107
|
+
{s === "project" ? t("integrations.scope_project") : t("integrations.scope_global")}
|
|
117
108
|
</button>
|
|
118
109
|
))}
|
|
119
110
|
</div>
|
|
@@ -132,14 +123,13 @@ export function IntegrationsTab({ pid }: { pid: string }) {
|
|
|
132
123
|
tab === s.value ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground",
|
|
133
124
|
)}
|
|
134
125
|
>
|
|
135
|
-
<Icon className="h-3.5 w-3.5" /> {s.
|
|
126
|
+
<Icon className="h-3.5 w-3.5" /> {t(s.labelKey)}
|
|
136
127
|
</button>
|
|
137
128
|
);
|
|
138
129
|
})}
|
|
139
130
|
</div>
|
|
140
131
|
|
|
141
132
|
{tab === "plugins" && <PluginsSection pid={pid} scope={scope} />}
|
|
142
|
-
{tab === "mcp" && <McpsTab pid={pid} />}
|
|
143
133
|
{tab === "tools" && <ToolsSection pid={pid} />}
|
|
144
134
|
</Section>
|
|
145
135
|
);
|
|
@@ -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,12 +103,13 @@ 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
|
|
|
@@ -122,6 +151,12 @@ export function McpsTab({ pid }: { pid: string }) {
|
|
|
122
151
|
const writable = m.source === "apc" || m.source === "runtime" || m.source === "global";
|
|
123
152
|
const scopeForRemove: McpScope = sourceToScope(m.source);
|
|
124
153
|
const isActive = activeMcp === m.name;
|
|
154
|
+
const enabled = m.enabled !== false;
|
|
155
|
+
const res = results[m.name];
|
|
156
|
+
const tools = res?.tools || [];
|
|
157
|
+
const open = !!expandedTools[m.name];
|
|
158
|
+
const summary = mcpSummary(m);
|
|
159
|
+
const Icon = m.transport === "http" ? Globe : Network;
|
|
125
160
|
return (
|
|
126
161
|
<li
|
|
127
162
|
key={`${m.source}-${m.name}`}
|
|
@@ -132,45 +167,79 @@ export function McpsTab({ pid }: { pid: string }) {
|
|
|
132
167
|
onClick={() => setActiveMcp(m.name)}
|
|
133
168
|
role="button"
|
|
134
169
|
>
|
|
135
|
-
<div className="flex
|
|
136
|
-
|
|
137
|
-
<
|
|
138
|
-
|
|
139
|
-
{(
|
|
140
|
-
</
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
170
|
+
<div className="flex items-start gap-3">
|
|
171
|
+
{/* Icon + colored status dot */}
|
|
172
|
+
<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">
|
|
173
|
+
<Icon size={16} className="text-muted-fg" />
|
|
174
|
+
<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))} />
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div className="min-w-0 flex-1">
|
|
178
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
179
|
+
<span className="font-medium">{m.name}</span>
|
|
180
|
+
<Badge tone={SOURCE_TONE[m.source] ?? "muted"}>{sourceLabel(m.source)}</Badge>
|
|
181
|
+
<span className="ml-auto text-xs text-muted-fg">{(m.transport || "stdio").toUpperCase()}</span>
|
|
182
|
+
<div onClick={(e) => e.stopPropagation()}>
|
|
183
|
+
<Switch checked={enabled} onChange={() => toggleEnabled(m)} label="" />
|
|
184
|
+
</div>
|
|
185
|
+
<Tip content={t("project.mcps.test_btn")}>
|
|
186
|
+
<Button size="sm" variant="ghost" onClick={(e) => { e.stopPropagation(); runTest(m.name); }} aria-label={t("project.mcps.test_btn")}>
|
|
187
|
+
{res?.busy ? <FlaskConical size={13} className="animate-pulse" /> : <FlaskConical size={13} />}
|
|
188
|
+
</Button>
|
|
189
|
+
</Tip>
|
|
190
|
+
<Tip content={t("project.mcps.logs_btn")}>
|
|
191
|
+
<Button size="sm" variant="ghost" onClick={(e) => { e.stopPropagation(); setActiveMcp(m.name); }} aria-label={t("project.mcps.logs_btn")}>
|
|
192
|
+
<ScrollText size={13} />
|
|
193
|
+
</Button>
|
|
194
|
+
</Tip>
|
|
195
|
+
{writable && (
|
|
196
|
+
<Tip content={t("project.mcps.edit_btn")}>
|
|
197
|
+
<Button size="sm" variant="ghost" onClick={(e) => { e.stopPropagation(); setDialog({ kind: "edit", entry: m }); }} aria-label={t("project.mcps.edit_btn")}>
|
|
198
|
+
<Pencil size={13} />
|
|
199
|
+
</Button>
|
|
200
|
+
</Tip>
|
|
201
|
+
)}
|
|
202
|
+
{writable && (
|
|
203
|
+
<Button size="sm" variant="destructive" onClick={(e) => { e.stopPropagation(); remove(m.name, scopeForRemove); }}>
|
|
204
|
+
<Trash2 size={13} />
|
|
205
|
+
</Button>
|
|
206
|
+
)}
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
{summary && <p className="mt-0.5 truncate font-mono text-xs text-muted-fg">{summary}</p>}
|
|
210
|
+
|
|
211
|
+
{res?.ok === false && res.error && (
|
|
212
|
+
<p className="mt-1 flex items-start gap-1 text-xs text-red-400">
|
|
213
|
+
<XCircle size={12} className="mt-0.5 flex-shrink-0" /> <span className="break-words">{res.error}</span>
|
|
214
|
+
</p>
|
|
215
|
+
)}
|
|
216
|
+
|
|
217
|
+
{tools.length > 0 && (
|
|
218
|
+
<div className="mt-1.5" onClick={(e) => e.stopPropagation()}>
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
onClick={() => setExpandedTools((prev) => ({ ...prev, [m.name]: !prev[m.name] }))}
|
|
222
|
+
className="flex items-center gap-1 text-xs text-muted-fg transition-colors hover:text-fg"
|
|
223
|
+
>
|
|
224
|
+
<Wrench size={12} /> {t("project.mcps.tools_count", { n: tools.length })}
|
|
225
|
+
<ChevronDown size={12} className={cn("transition-transform", open && "rotate-180")} />
|
|
226
|
+
</button>
|
|
227
|
+
{open && (
|
|
228
|
+
<div className="mt-1.5 flex flex-wrap gap-1.5">
|
|
229
|
+
{tools.slice(0, 40).map((tool) => (
|
|
230
|
+
<Tip key={tool.name} content={tool.description || "—"}>
|
|
231
|
+
<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">
|
|
232
|
+
<Terminal size={10} /> {tool.name}
|
|
233
|
+
</span>
|
|
234
|
+
</Tip>
|
|
235
|
+
))}
|
|
236
|
+
{tools.length > 40 && <span className="text-[10px] text-muted-fg">… +{tools.length - 40}</span>}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
147
241
|
</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
242
|
</div>
|
|
171
|
-
{testFor?.name === m.name && (
|
|
172
|
-
<TestResultRow result={testFor.result} busy={!!testFor.busy} onClose={() => setTestFor(null)} />
|
|
173
|
-
)}
|
|
174
243
|
</li>
|
|
175
244
|
);
|
|
176
245
|
})}
|
|
@@ -190,49 +259,12 @@ export function McpsTab({ pid }: { pid: string }) {
|
|
|
190
259
|
</div>
|
|
191
260
|
|
|
192
261
|
<div className="lg:col-span-1">
|
|
193
|
-
<LogsPanel pid={pid} mcpName={activeMcp} runningTest={!!
|
|
262
|
+
<LogsPanel pid={pid} mcpName={activeMcp} runningTest={!!(activeMcp && results[activeMcp]?.busy)} />
|
|
194
263
|
</div>
|
|
195
264
|
</div>
|
|
196
265
|
);
|
|
197
266
|
}
|
|
198
267
|
|
|
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
268
|
|
|
237
269
|
// Right-side terminal-style live logs panel. Polls the daemon every 1.5s
|
|
238
270
|
// while a test is running (or every 4s when idle and an MCP is pinned) so
|