@agentprojectcontext/apx 1.59.0 → 1.60.1
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/_asana.js +34 -0
- package/src/core/agent/tools/handlers/asana-create-task.js +38 -0
- package/src/core/agent/tools/handlers/asana-list-projects.js +19 -0
- package/src/core/agent/tools/handlers/asana-list-tasks.js +27 -0
- package/src/core/agent/tools/handlers/asana-update-task.js +32 -0
- package/src/core/agent/tools/names.js +10 -0
- package/src/core/agent/tools/registry.js +11 -0
- package/src/core/integrations/catalog.js +66 -0
- package/src/core/integrations/index.js +10 -0
- package/src/core/integrations/plugins/asana.js +231 -0
- package/src/core/integrations/sources.js +56 -0
- package/src/core/integrations/store.js +118 -0
- package/src/core/stores/project-files.js +21 -2
- package/src/host/daemon/api/integrations.js +191 -0
- package/src/host/daemon/api.js +2 -0
- package/src/interfaces/web/dist/assets/{index-CnQb4N6C.js → index-DFNV6BWh.js} +193 -163
- package/src/interfaces/web/dist/assets/index-DFNV6BWh.js.map +1 -0
- package/src/interfaces/web/dist/assets/index-HU-Wt2l9.css +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/components/integrations/AsanaPlugin.tsx +275 -0
- package/src/interfaces/web/src/components/integrations/ComingSoonPlugin.tsx +44 -0
- package/src/interfaces/web/src/components/integrations/PluginCard.tsx +61 -0
- package/src/interfaces/web/src/components/integrations/PluginToolsSection.tsx +39 -0
- package/src/interfaces/web/src/lib/api/integrations.ts +106 -0
- package/src/interfaces/web/src/lib/api.ts +1 -0
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +6 -2
- package/src/interfaces/web/src/screens/project/IntegrationsTab.tsx +146 -0
- package/src/interfaces/web/dist/assets/index-CnQb4N6C.js.map +0 -1
- package/src/interfaces/web/dist/assets/index-Dv3X-zpx.css +0 -1
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import useSWR from "swr";
|
|
3
|
+
import {
|
|
4
|
+
AlertCircle, CheckCircle2, ChevronDown, ExternalLink, Eye, EyeOff, Loader2, WifiOff, X,
|
|
5
|
+
} from "lucide-react";
|
|
6
|
+
import { cn } from "../../lib/cn";
|
|
7
|
+
import { Integrations, type IntegrationScope, type IntegrationStatus } from "../../lib/api";
|
|
8
|
+
import { PluginCard } from "./PluginCard";
|
|
9
|
+
import { PluginToolsSection, type PluginTool } from "./PluginToolsSection";
|
|
10
|
+
|
|
11
|
+
function AsanaLogo({ className }: { className?: string }) {
|
|
12
|
+
return (
|
|
13
|
+
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
|
14
|
+
<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" />
|
|
15
|
+
</svg>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const ASANA_TOOLS: PluginTool[] = [
|
|
20
|
+
{ slug: "asana_list_projects", desc: "Listar proyectos del workspace" },
|
|
21
|
+
{ slug: "asana_list_tasks", desc: "Listar tareas de un proyecto" },
|
|
22
|
+
{ slug: "asana_create_task", desc: "Crear una tarea" },
|
|
23
|
+
{ slug: "asana_update_task", desc: "Actualizar estado o campos de una tarea" },
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const HELP_STEPS = [
|
|
27
|
+
"Abrí app.asana.com/0/my-apps en el navegador.",
|
|
28
|
+
'Bajá hasta la sección "Personal access tokens" (no tus apps OAuth).',
|
|
29
|
+
'Hacé clic en "+ New access token".',
|
|
30
|
+
"Dale un nombre y confirmá.",
|
|
31
|
+
'Copiá el token completo — empieza con "1/..." y tiene un ":" en el medio.',
|
|
32
|
+
"Pegalo en el campo de abajo.",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
type Step = "idle" | "saving" | "validating" | "done";
|
|
36
|
+
|
|
37
|
+
export function AsanaPlugin({ pid, scope }: { pid: string; scope: IntegrationScope }) {
|
|
38
|
+
const [expanded, setExpanded] = useState(false);
|
|
39
|
+
const [pat, setPat] = useState("");
|
|
40
|
+
const [showPat, setShowPat] = useState(false);
|
|
41
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
42
|
+
const [step, setStep] = useState<Step>("idle");
|
|
43
|
+
const [error, setError] = useState<string | null>(null);
|
|
44
|
+
const [workspaces, setWorkspaces] = useState<{ gid: string; name: string }[]>([]);
|
|
45
|
+
const [selectedWorkspace, setSelectedWorkspace] = useState("");
|
|
46
|
+
|
|
47
|
+
const key = `asana-status-${pid}-${scope}`;
|
|
48
|
+
const { data: status, mutate, isLoading } = useSWR<IntegrationStatus>(
|
|
49
|
+
key,
|
|
50
|
+
() => Integrations.status(pid, "asana", scope),
|
|
51
|
+
{ shouldRetryOnError: false },
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const isActive = status?.status === "active" && status.is_enabled;
|
|
55
|
+
const busy = step === "saving" || step === "validating";
|
|
56
|
+
|
|
57
|
+
async function handleConnect() {
|
|
58
|
+
if (!pat.trim()) return;
|
|
59
|
+
setStep("saving");
|
|
60
|
+
setError(null);
|
|
61
|
+
try {
|
|
62
|
+
await Integrations.asanaConfigure(pid, scope, { personalAccessToken: pat.trim() });
|
|
63
|
+
setStep("validating");
|
|
64
|
+
const result = await Integrations.asanaValidate(pid, scope);
|
|
65
|
+
await mutate();
|
|
66
|
+
if (!result.workspace_gid) {
|
|
67
|
+
const ws = await Integrations.asanaWorkspaces(pid, scope);
|
|
68
|
+
if (ws.workspaces.length > 1) setWorkspaces(ws.workspaces);
|
|
69
|
+
}
|
|
70
|
+
setStep("done");
|
|
71
|
+
setPat("");
|
|
72
|
+
} catch (err) {
|
|
73
|
+
setError(err instanceof Error ? err.message : "Error al conectar con Asana");
|
|
74
|
+
setStep("idle");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function handleSelectWorkspace() {
|
|
79
|
+
if (!selectedWorkspace) return;
|
|
80
|
+
setStep("saving");
|
|
81
|
+
setError(null);
|
|
82
|
+
try {
|
|
83
|
+
await Integrations.asanaConfigure(pid, scope, { workspaceGid: selectedWorkspace });
|
|
84
|
+
await Integrations.asanaValidate(pid, scope);
|
|
85
|
+
await mutate();
|
|
86
|
+
setWorkspaces([]);
|
|
87
|
+
setStep("done");
|
|
88
|
+
} catch (err) {
|
|
89
|
+
setError(err instanceof Error ? err.message : "Error al seleccionar workspace");
|
|
90
|
+
setStep("idle");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function handleDeactivate() {
|
|
95
|
+
setError(null);
|
|
96
|
+
try {
|
|
97
|
+
await Integrations.deactivate(pid, "asana", scope);
|
|
98
|
+
await mutate();
|
|
99
|
+
} catch (err) {
|
|
100
|
+
setError(err instanceof Error ? err.message : "Error al desactivar");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<PluginCard
|
|
106
|
+
icon={
|
|
107
|
+
<div className="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-2xl border border-rose-500/30 bg-gradient-to-br from-rose-500/20 to-pink-500/20">
|
|
108
|
+
<AsanaLogo className="h-6 w-6 text-rose-400" />
|
|
109
|
+
</div>
|
|
110
|
+
}
|
|
111
|
+
title="Asana"
|
|
112
|
+
description="Conectá tu workspace de Asana para que los agentes creen, actualicen y consulten tareas"
|
|
113
|
+
hasTools
|
|
114
|
+
badges={
|
|
115
|
+
<span
|
|
116
|
+
className={cn(
|
|
117
|
+
"flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[10px]",
|
|
118
|
+
isActive
|
|
119
|
+
? "border-emerald-700 bg-emerald-900/20 text-emerald-400"
|
|
120
|
+
: "border-border bg-muted text-muted-foreground",
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
<span className={cn("h-1.5 w-1.5 rounded-full", isActive ? "bg-emerald-400" : "bg-muted-foreground")} />
|
|
124
|
+
{isLoading ? "..." : isActive ? "Activo" : status?.status === "error" ? "Error" : "No configurado"}
|
|
125
|
+
</span>
|
|
126
|
+
}
|
|
127
|
+
rightContent={
|
|
128
|
+
isActive && status?.workspace_name ? (
|
|
129
|
+
<span className="max-w-[120px] truncate font-mono text-[10px] text-muted-foreground">
|
|
130
|
+
{status.workspace_name}
|
|
131
|
+
</span>
|
|
132
|
+
) : null
|
|
133
|
+
}
|
|
134
|
+
expanded={expanded}
|
|
135
|
+
onToggle={() => setExpanded((v) => !v)}
|
|
136
|
+
>
|
|
137
|
+
<div className="space-y-4 p-4">
|
|
138
|
+
{error && (
|
|
139
|
+
<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">
|
|
140
|
+
<AlertCircle className="h-3.5 w-3.5 flex-shrink-0" />
|
|
141
|
+
<span className="flex-1">{error}</span>
|
|
142
|
+
<button onClick={() => setError(null)}><X className="h-3.5 w-3.5" /></button>
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{isActive && (
|
|
147
|
+
<div className="space-y-1 rounded-xl border border-emerald-700/30 bg-emerald-900/10 p-3">
|
|
148
|
+
<div className="flex items-center gap-2">
|
|
149
|
+
<CheckCircle2 className="h-3.5 w-3.5 text-emerald-400" />
|
|
150
|
+
<span className="text-xs font-medium text-emerald-300">Conectado como {status?.user_name}</span>
|
|
151
|
+
</div>
|
|
152
|
+
{status?.user_email && <p className="pl-5 text-[10px] text-muted-foreground">{status.user_email}</p>}
|
|
153
|
+
{status?.workspace_name && (
|
|
154
|
+
<p className="pl-5 text-[10px] text-muted-foreground">Workspace: {status.workspace_name}</p>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
|
|
159
|
+
{workspaces.length > 1 && (
|
|
160
|
+
<div className="space-y-2">
|
|
161
|
+
<p className="text-[11px] text-muted-foreground">Seleccioná el workspace a usar:</p>
|
|
162
|
+
<div className="flex gap-2">
|
|
163
|
+
<select
|
|
164
|
+
value={selectedWorkspace}
|
|
165
|
+
onChange={(e) => setSelectedWorkspace(e.target.value)}
|
|
166
|
+
className="flex-1 rounded-lg border border-border bg-background px-2 py-1.5 text-xs outline-none focus:border-rose-500/50"
|
|
167
|
+
>
|
|
168
|
+
<option value="">Seleccionar workspace...</option>
|
|
169
|
+
{workspaces.map((ws) => (
|
|
170
|
+
<option key={ws.gid} value={ws.gid}>{ws.name}</option>
|
|
171
|
+
))}
|
|
172
|
+
</select>
|
|
173
|
+
<button
|
|
174
|
+
onClick={handleSelectWorkspace}
|
|
175
|
+
disabled={!selectedWorkspace || busy}
|
|
176
|
+
className="rounded-lg border border-rose-700/50 px-3 py-1.5 text-xs text-rose-400 transition-all hover:bg-rose-900/20 disabled:cursor-not-allowed disabled:opacity-50"
|
|
177
|
+
>
|
|
178
|
+
{busy ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : "Confirmar"}
|
|
179
|
+
</button>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
{(!isActive || step === "idle") && workspaces.length === 0 && (
|
|
185
|
+
<div className="space-y-3">
|
|
186
|
+
<p className="mb-1 text-xs font-semibold text-foreground">Credenciales Asana</p>
|
|
187
|
+
|
|
188
|
+
<div className="overflow-hidden rounded-lg border border-border">
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
onClick={() => setShowHelp((v) => !v)}
|
|
192
|
+
className="flex w-full items-center justify-between px-3 py-2 text-left transition-colors hover:bg-muted/40"
|
|
193
|
+
>
|
|
194
|
+
<span className="text-[11px] text-muted-foreground">
|
|
195
|
+
¿Cómo obtener el token? ·{" "}
|
|
196
|
+
<a
|
|
197
|
+
href="https://app.asana.com/0/my-apps"
|
|
198
|
+
target="_blank"
|
|
199
|
+
rel="noreferrer"
|
|
200
|
+
onClick={(e) => e.stopPropagation()}
|
|
201
|
+
className="inline-flex items-center gap-0.5 text-rose-400 hover:underline"
|
|
202
|
+
>
|
|
203
|
+
app.asana.com/0/my-apps <ExternalLink className="h-2.5 w-2.5" />
|
|
204
|
+
</a>
|
|
205
|
+
</span>
|
|
206
|
+
<ChevronDown className={cn("h-3.5 w-3.5 flex-shrink-0 text-muted-foreground transition-transform", showHelp && "rotate-180")} />
|
|
207
|
+
</button>
|
|
208
|
+
{showHelp && (
|
|
209
|
+
<div className="space-y-1.5 border-t border-border px-3 pb-3 pt-2.5">
|
|
210
|
+
{HELP_STEPS.map((s, i) => (
|
|
211
|
+
<div key={i} className="flex items-start gap-2">
|
|
212
|
+
<span className="mt-0.5 flex-shrink-0 font-mono text-[10px] text-rose-400/70">{i + 1}.</span>
|
|
213
|
+
<p className="text-[11px] text-muted-foreground">{s}</p>
|
|
214
|
+
</div>
|
|
215
|
+
))}
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<div>
|
|
221
|
+
<label className="mb-1 block text-[10px] text-muted-foreground">Personal Access Token</label>
|
|
222
|
+
<div className="relative">
|
|
223
|
+
<input
|
|
224
|
+
type={showPat ? "text" : "password"}
|
|
225
|
+
placeholder="1/1234567890abcdef:..."
|
|
226
|
+
value={pat}
|
|
227
|
+
onChange={(e) => setPat(e.target.value)}
|
|
228
|
+
onKeyDown={(e) => e.key === "Enter" && handleConnect()}
|
|
229
|
+
className="w-full rounded-lg border border-border bg-background px-3 py-2 pr-14 font-mono text-xs outline-none placeholder:text-muted-foreground/60 focus:border-rose-500/50"
|
|
230
|
+
/>
|
|
231
|
+
<button
|
|
232
|
+
type="button"
|
|
233
|
+
onClick={() => setShowPat((v) => !v)}
|
|
234
|
+
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"
|
|
235
|
+
>
|
|
236
|
+
{showPat ? <EyeOff className="h-3 w-3" /> : <Eye className="h-3 w-3" />}
|
|
237
|
+
{showPat ? "Ocultar" : "Ver"}
|
|
238
|
+
</button>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
{step === "validating" && (
|
|
243
|
+
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
244
|
+
<Loader2 className="h-3.5 w-3.5 animate-spin" /> Verificando token con Asana...
|
|
245
|
+
</div>
|
|
246
|
+
)}
|
|
247
|
+
|
|
248
|
+
<button
|
|
249
|
+
onClick={handleConnect}
|
|
250
|
+
disabled={!pat.trim() || busy}
|
|
251
|
+
className="flex w-full items-center justify-center gap-1.5 rounded-lg border border-rose-700/50 px-3 py-2 text-xs text-rose-400 transition-all hover:bg-rose-900/20 disabled:cursor-not-allowed disabled:opacity-50"
|
|
252
|
+
>
|
|
253
|
+
{busy ? (
|
|
254
|
+
<><Loader2 className="h-3.5 w-3.5 animate-spin" />{step === "saving" ? "Guardando..." : "Validando..."}</>
|
|
255
|
+
) : isActive ? "Reconectar" : "Conectar"}
|
|
256
|
+
</button>
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
|
|
260
|
+
<PluginToolsSection pid={pid} tools={ASANA_TOOLS} isActive={!!isActive} />
|
|
261
|
+
|
|
262
|
+
{isActive && (
|
|
263
|
+
<div className="flex justify-end border-t border-border pt-2">
|
|
264
|
+
<button
|
|
265
|
+
onClick={handleDeactivate}
|
|
266
|
+
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"
|
|
267
|
+
>
|
|
268
|
+
<WifiOff className="h-3.5 w-3.5" /> Desactivar
|
|
269
|
+
</button>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
</PluginCard>
|
|
274
|
+
);
|
|
275
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Github, MessageCircle, Mic, Puzzle } from "lucide-react";
|
|
3
|
+
import type { CatalogEntry } from "../../lib/api";
|
|
4
|
+
import { PluginCard } from "./PluginCard";
|
|
5
|
+
|
|
6
|
+
const ICONS: Record<string, { icon: typeof Puzzle; className: string; wrap: string }> = {
|
|
7
|
+
github: { icon: Github, className: "text-slate-200", wrap: "border-slate-500/30 from-slate-500/20 to-slate-700/20" },
|
|
8
|
+
whatsapp: { icon: MessageCircle, className: "text-[#25D366]", wrap: "border-[#25D366]/30 from-[#25D366]/20 to-[#128C7E]/20" },
|
|
9
|
+
"local-transcription": { icon: Mic, className: "text-orange-400", wrap: "border-orange-500/30 from-orange-500/20 to-amber-500/20" },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Placeholder card for catalog plugins that are declared but not yet wired to a
|
|
13
|
+
// live service module (github/whatsapp/transcription). Keeps the Integrations
|
|
14
|
+
// page complete and makes it obvious what's on the roster.
|
|
15
|
+
export function ComingSoonPlugin({ entry }: { entry: CatalogEntry }) {
|
|
16
|
+
const [expanded, setExpanded] = useState(false);
|
|
17
|
+
const cfg = ICONS[entry.slug] || { icon: Puzzle, className: "text-muted-foreground", wrap: "border-border from-muted to-muted" };
|
|
18
|
+
const Icon = cfg.icon;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<PluginCard
|
|
22
|
+
icon={
|
|
23
|
+
<div className={`flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-2xl border bg-gradient-to-br ${cfg.wrap}`}>
|
|
24
|
+
<Icon className={`h-6 w-6 ${cfg.className}`} />
|
|
25
|
+
</div>
|
|
26
|
+
}
|
|
27
|
+
title={entry.name}
|
|
28
|
+
description={entry.description}
|
|
29
|
+
badges={
|
|
30
|
+
<span className="rounded-full border border-border bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
|
|
31
|
+
Próximamente
|
|
32
|
+
</span>
|
|
33
|
+
}
|
|
34
|
+
expanded={expanded}
|
|
35
|
+
onToggle={() => setExpanded((v) => !v)}
|
|
36
|
+
>
|
|
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>
|
|
42
|
+
</PluginCard>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { ChevronRight, Wrench } from "lucide-react";
|
|
3
|
+
import { cn } from "../../lib/cn";
|
|
4
|
+
import { Tip } from "../ui";
|
|
5
|
+
|
|
6
|
+
// Collapsible card shell for one integration plugin. Ported from PandaProject's
|
|
7
|
+
// PluginCard but restyled onto APX's design tokens (border/card/muted).
|
|
8
|
+
export function PluginCard({
|
|
9
|
+
icon,
|
|
10
|
+
title,
|
|
11
|
+
description,
|
|
12
|
+
badges,
|
|
13
|
+
rightContent,
|
|
14
|
+
hasTools,
|
|
15
|
+
expanded,
|
|
16
|
+
onToggle,
|
|
17
|
+
children,
|
|
18
|
+
}: {
|
|
19
|
+
icon: ReactNode;
|
|
20
|
+
title: string;
|
|
21
|
+
description: string;
|
|
22
|
+
badges?: ReactNode;
|
|
23
|
+
rightContent?: ReactNode;
|
|
24
|
+
hasTools?: boolean;
|
|
25
|
+
expanded: boolean;
|
|
26
|
+
onToggle: () => void;
|
|
27
|
+
children?: ReactNode;
|
|
28
|
+
}) {
|
|
29
|
+
return (
|
|
30
|
+
<div className="overflow-hidden rounded-xl border border-border bg-card">
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
className="flex w-full items-center gap-4 p-4 text-left transition-colors hover:bg-muted/40"
|
|
34
|
+
onClick={onToggle}
|
|
35
|
+
>
|
|
36
|
+
{icon}
|
|
37
|
+
<div className="min-w-0 flex-1">
|
|
38
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
39
|
+
<p className="text-sm font-semibold text-foreground">{title}</p>
|
|
40
|
+
{badges}
|
|
41
|
+
{hasTools && (
|
|
42
|
+
<Tip content="Esta integración expone tools para los agentes">
|
|
43
|
+
<span>
|
|
44
|
+
<Wrench className="h-3 w-3 text-muted-foreground" />
|
|
45
|
+
</span>
|
|
46
|
+
</Tip>
|
|
47
|
+
)}
|
|
48
|
+
</div>
|
|
49
|
+
<p className="mt-0.5 truncate text-xs text-muted-foreground">{description}</p>
|
|
50
|
+
</div>
|
|
51
|
+
<div className="flex flex-shrink-0 items-center gap-2">
|
|
52
|
+
{rightContent}
|
|
53
|
+
<ChevronRight
|
|
54
|
+
className={cn("h-4 w-4 text-muted-foreground transition-transform", expanded && "rotate-90")}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
</button>
|
|
58
|
+
{expanded && children && <div className="border-t border-border">{children}</div>}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Wrench } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
export interface PluginTool {
|
|
4
|
+
slug: string;
|
|
5
|
+
desc: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Lists the agent tools a connected plugin exposes. Read-only: the tools are
|
|
9
|
+
// registered in APX's tool registry (category "integrations") and become
|
|
10
|
+
// callable for any agent whose role gate allows them, or via discover_tools().
|
|
11
|
+
export function PluginToolsSection({ tools, isActive }: { pid: string; tools: PluginTool[]; isActive: boolean }) {
|
|
12
|
+
if (!isActive) return null;
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="space-y-2.5 rounded-xl border border-border bg-muted/30 p-3">
|
|
16
|
+
<div className="flex items-center justify-between">
|
|
17
|
+
<p className="text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
|
|
18
|
+
Tools para agentes
|
|
19
|
+
</p>
|
|
20
|
+
</div>
|
|
21
|
+
<div className="flex flex-wrap gap-1.5">
|
|
22
|
+
{tools.map((t) => (
|
|
23
|
+
<div
|
|
24
|
+
key={t.slug}
|
|
25
|
+
className="flex items-center gap-1.5 rounded-lg border border-border bg-background px-2 py-1"
|
|
26
|
+
>
|
|
27
|
+
<Wrench className="h-2.5 w-2.5 flex-shrink-0 text-muted-foreground" />
|
|
28
|
+
<span className="font-mono text-[10px] text-foreground">{t.slug}</span>
|
|
29
|
+
<span className="text-[10px] text-muted-foreground/60">·</span>
|
|
30
|
+
<span className="text-[10px] text-muted-foreground">{t.desc}</span>
|
|
31
|
+
</div>
|
|
32
|
+
))}
|
|
33
|
+
</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>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { http } from "../http";
|
|
2
|
+
|
|
3
|
+
// Where an integration record is stored. "global" targets the default project's
|
|
4
|
+
// store (shared across projects); "project" targets the current project. A
|
|
5
|
+
// project uses its own record when present, otherwise the global one.
|
|
6
|
+
export type IntegrationScope = "project" | "global";
|
|
7
|
+
|
|
8
|
+
// Status returned by a plugin's status endpoint. Common fields plus
|
|
9
|
+
// plugin-specific extras (Asana adds user/workspace metadata).
|
|
10
|
+
export interface IntegrationStatus {
|
|
11
|
+
slug: string;
|
|
12
|
+
status: string;
|
|
13
|
+
is_enabled: boolean;
|
|
14
|
+
user_name?: string | null;
|
|
15
|
+
user_email?: string | null;
|
|
16
|
+
workspace_gid?: string | null;
|
|
17
|
+
workspace_name?: string | null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PluginTool {
|
|
21
|
+
slug: string;
|
|
22
|
+
desc: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// One entry of the plugin catalog with its resolved status for this project.
|
|
26
|
+
export interface CatalogEntry {
|
|
27
|
+
slug: string;
|
|
28
|
+
name: string;
|
|
29
|
+
type: string;
|
|
30
|
+
description: string;
|
|
31
|
+
auth: string;
|
|
32
|
+
tools?: PluginTool[];
|
|
33
|
+
coming_soon: boolean;
|
|
34
|
+
status: IntegrationStatus;
|
|
35
|
+
resolved_scope: IntegrationScope | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// A stored integration record (secrets redacted; `<key>_set` booleans instead).
|
|
39
|
+
export interface IntegrationRecord {
|
|
40
|
+
slug: string;
|
|
41
|
+
name: string;
|
|
42
|
+
type: string;
|
|
43
|
+
description: string;
|
|
44
|
+
source: string;
|
|
45
|
+
status: string;
|
|
46
|
+
is_enabled: boolean;
|
|
47
|
+
config: Record<string, unknown>;
|
|
48
|
+
created_at: string;
|
|
49
|
+
updated_at: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AsanaConfigureBody {
|
|
53
|
+
personalAccessToken?: string;
|
|
54
|
+
workspaceGid?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AsanaValidateResult {
|
|
58
|
+
ok: boolean;
|
|
59
|
+
user_name?: string | null;
|
|
60
|
+
user_email?: string | null;
|
|
61
|
+
workspace_gid?: string | null;
|
|
62
|
+
workspace_name?: string | null;
|
|
63
|
+
error?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface AsanaWorkspaces {
|
|
67
|
+
workspaces: { gid: string; name: string }[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const q = (scope: IntegrationScope) => `?scope=${scope}`;
|
|
71
|
+
|
|
72
|
+
export const Integrations = {
|
|
73
|
+
catalog: (pid: string) =>
|
|
74
|
+
http.get<CatalogEntry[]>(`/projects/${pid}/integrations/catalog`),
|
|
75
|
+
|
|
76
|
+
list: (pid: string, scope: IntegrationScope = "project") =>
|
|
77
|
+
http.get<IntegrationRecord[]>(`/projects/${pid}/integrations${q(scope)}`),
|
|
78
|
+
|
|
79
|
+
status: (pid: string, slug: string, scope: IntegrationScope = "project") =>
|
|
80
|
+
http.get<IntegrationStatus>(`/projects/${pid}/integrations/${slug}${q(scope)}`),
|
|
81
|
+
|
|
82
|
+
configure: (pid: string, slug: string, scope: IntegrationScope, body: Record<string, unknown>) =>
|
|
83
|
+
http.post<IntegrationRecord>(`/projects/${pid}/integrations/${slug}/configure${q(scope)}`, body),
|
|
84
|
+
|
|
85
|
+
validate: (pid: string, slug: string, scope: IntegrationScope = "project") =>
|
|
86
|
+
http.post<AsanaValidateResult>(`/projects/${pid}/integrations/${slug}/validate${q(scope)}`, {}),
|
|
87
|
+
|
|
88
|
+
deactivate: (pid: string, slug: string, scope: IntegrationScope = "project") =>
|
|
89
|
+
http.post<IntegrationStatus>(`/projects/${pid}/integrations/${slug}/deactivate${q(scope)}`, {}),
|
|
90
|
+
|
|
91
|
+
action: <T>(pid: string, slug: string, action: string, scope: IntegrationScope = "project") =>
|
|
92
|
+
http.post<T>(`/projects/${pid}/integrations/${slug}/action/${action}${q(scope)}`, {}),
|
|
93
|
+
|
|
94
|
+
remove: (pid: string, slug: string, scope: IntegrationScope = "project") =>
|
|
95
|
+
http.del<void>(`/projects/${pid}/integrations/${slug}${q(scope)}`),
|
|
96
|
+
|
|
97
|
+
// ── Asana convenience wrappers ──────────────────────────────────────────────
|
|
98
|
+
asanaConfigure: (pid: string, scope: IntegrationScope, body: AsanaConfigureBody) =>
|
|
99
|
+
Integrations.configure(pid, "asana", scope, {
|
|
100
|
+
personal_access_token: body.personalAccessToken,
|
|
101
|
+
workspace_gid: body.workspaceGid,
|
|
102
|
+
}),
|
|
103
|
+
asanaValidate: (pid: string, scope: IntegrationScope) => Integrations.validate(pid, "asana", scope),
|
|
104
|
+
asanaWorkspaces: (pid: string, scope: IntegrationScope) =>
|
|
105
|
+
Integrations.action<AsanaWorkspaces>(pid, "asana", "workspaces", scope),
|
|
106
|
+
};
|
|
@@ -9,6 +9,7 @@ export * from "./api/conversations";
|
|
|
9
9
|
export * from "./api/routines";
|
|
10
10
|
export * from "./api/tasks";
|
|
11
11
|
export * from "./api/mcps";
|
|
12
|
+
export * from "./api/integrations";
|
|
12
13
|
export * from "./api/vars";
|
|
13
14
|
export * from "./api/messages";
|
|
14
15
|
export * from "./api/sessions";
|
|
@@ -3,7 +3,7 @@ import { useParams, Routes, Route, Navigate, useLocation, useNavigate } from "re
|
|
|
3
3
|
import {
|
|
4
4
|
Bot, Heart, Zap, Puzzle, FolderKanban, Settings,
|
|
5
5
|
MessagesSquare, Send, KeyRound,
|
|
6
|
-
LayoutDashboard, Boxes, Cpu, ScrollText, History, Brain, FileCode2,
|
|
6
|
+
LayoutDashboard, Boxes, Cpu, ScrollText, History, Brain, FileCode2, Cable,
|
|
7
7
|
Building2, FileText, FolderTree, Sparkles,
|
|
8
8
|
} from "lucide-react";
|
|
9
9
|
import { useNavCollapse, type TabSection } from "../components/common/TabNav";
|
|
@@ -25,6 +25,7 @@ import { AgentsTab } from "./project/AgentsTab";
|
|
|
25
25
|
import { RoutinesTab } from "./project/RoutinesTab";
|
|
26
26
|
import { TasksTab } from "./project/TasksTab";
|
|
27
27
|
import { McpsTab } from "./project/McpsTab";
|
|
28
|
+
import { IntegrationsTab } from "./project/IntegrationsTab";
|
|
28
29
|
import { VarsTab } from "./project/VarsTab";
|
|
29
30
|
import { ChatTab } from "./project/ChatTab";
|
|
30
31
|
import { TelegramTab } from "./project/TelegramTab";
|
|
@@ -38,7 +39,7 @@ import { SkillsTab } from "./project/SkillsTab";
|
|
|
38
39
|
|
|
39
40
|
type NavKey =
|
|
40
41
|
| "" | "chat" | "config" | "telegram"
|
|
41
|
-
| "agents" | "routines" | "tasks" | "mcps" | "vars" | "logs" | "memories" | "artifacts"
|
|
42
|
+
| "agents" | "routines" | "tasks" | "mcps" | "integrations" | "vars" | "logs" | "memories" | "artifacts"
|
|
42
43
|
| "structure" | "docs" | "files" | "skills";
|
|
43
44
|
|
|
44
45
|
export function ProjectScreen() {
|
|
@@ -79,6 +80,7 @@ export function ProjectScreen() {
|
|
|
79
80
|
{ key: "skills", label: t("skills_page.title"), icon: Sparkles },
|
|
80
81
|
{ key: "routines", label: t("project.nav.routines"), icon: Heart },
|
|
81
82
|
{ key: "mcps", label: t("project.nav.mcps"), icon: Puzzle },
|
|
83
|
+
{ key: "integrations", label: "Integrations", icon: Cable },
|
|
82
84
|
{ key: "vars", label: t("project.nav.vars"), icon: KeyRound },
|
|
83
85
|
{ key: "artifacts", label: t("project.nav.artifacts"), icon: FileCode2 },
|
|
84
86
|
{ key: "config", label: t("project.nav.config"), icon: Settings },
|
|
@@ -115,6 +117,7 @@ export function ProjectScreen() {
|
|
|
115
117
|
{ key: "routines", label: t("project.nav.routines"), icon: Heart },
|
|
116
118
|
{ key: "tasks", label: t("project.nav.tasks"), icon: Zap },
|
|
117
119
|
{ key: "mcps", label: t("project.nav.mcps"), icon: Puzzle },
|
|
120
|
+
{ key: "integrations", label: "Integrations", icon: Cable },
|
|
118
121
|
{ key: "vars", label: t("project.nav.vars"), icon: KeyRound },
|
|
119
122
|
{ key: "artifacts", label: t("project.nav.artifacts"), icon: FileCode2 },
|
|
120
123
|
{ key: "logs", label: t("project.nav.logs"), icon: ScrollText },
|
|
@@ -182,6 +185,7 @@ export function ProjectScreen() {
|
|
|
182
185
|
<Route path="routines" element={<RoutinesTab pid={pid} />} />
|
|
183
186
|
<Route path="tasks" element={isBase ? <GlobalTasksTab /> : <TasksTab pid={pid} />} />
|
|
184
187
|
<Route path="mcps" element={<McpsTab pid={pid} />} />
|
|
188
|
+
<Route path="integrations" element={<IntegrationsTab pid={pid} />} />
|
|
185
189
|
<Route path="artifacts" element={<ArtifactsTab pid={pid} />} />
|
|
186
190
|
<Route path="vars" element={<VarsTab pid={pid} />} />
|
|
187
191
|
<Route path="threads" element={<Navigate to={`/p/${pid}/chat`} replace />} />
|