@greatapps/greatagents-ui 0.3.13 → 0.3.15

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@greatapps/greatagents-ui",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "description": "Shared agents UI components for Great platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -63,29 +63,59 @@ function buildPreview(
63
63
  let preview = promptText;
64
64
 
65
65
  const activeObjectives = objectives.filter((o) => o.active);
66
- if (activeObjectives.length > 0) {
67
- preview += "\n\n[SKILLS DISPONÍVEIS]\n";
68
- for (const obj of activeObjectives) {
69
- preview += `- ${obj.title}`;
70
- if (obj.prompt) preview += `: ${obj.prompt}`;
71
- preview += "\n";
66
+ const enabledAgentTools = agentTools.filter((at) => at.enabled);
67
+
68
+ const toolMap = new Map(allTools.map((t) => [t.id, t]));
69
+
70
+ // Separate capabilities vs integrations
71
+ const capabilityTools: { at: AgentTool; tool: Tool }[] = [];
72
+ const integrationTools: { at: AgentTool; tool: Tool }[] = [];
73
+ for (const at of enabledAgentTools) {
74
+ const tool = toolMap.get(at.id_tool);
75
+ if (!tool) continue;
76
+ if (tool.type === "integration") {
77
+ integrationTools.push({ at, tool });
78
+ } else {
79
+ capabilityTools.push({ at, tool });
72
80
  }
73
81
  }
74
82
 
75
- const enabledAgentTools = agentTools.filter((at) => at.enabled);
76
- if (enabledAgentTools.length > 0) {
77
- const toolMap = new Map(allTools.map((t) => [t.id, t]));
78
- preview += "\n[TOOLS DISPONÍVEIS]\n";
79
- for (const at of enabledAgentTools) {
80
- const tool = toolMap.get(at.id_tool);
81
- const name = tool?.name || `Tool #${at.id_tool}`;
82
- const desc = tool?.description ? `: ${tool.description}` : "";
83
- preview += `- ${name}${desc}`;
84
- if (at.custom_instructions) {
85
- preview += `\n Instruções: ${at.custom_instructions}`;
83
+ const hasContent = activeObjectives.length > 0 || capabilityTools.length > 0 || integrationTools.length > 0;
84
+
85
+ if (hasContent) {
86
+ preview += "\n\n[CAPACIDADES E INTEGRAÇÕES]";
87
+
88
+ // Internal capabilities
89
+ if (activeObjectives.length > 0 || capabilityTools.length > 0) {
90
+ preview += "\n\n## Capacidades Internas (GClinic)";
91
+
92
+ for (const obj of activeObjectives) {
93
+ preview += `\n\n### ${obj.title} (${obj.slug})`;
94
+ if (obj.prompt) preview += `\n${obj.prompt}`;
95
+ }
96
+
97
+ for (const { at, tool } of capabilityTools) {
98
+ preview += `\n\n### ${tool.name} (${tool.slug})`;
99
+ if (tool.description) preview += `\n${tool.description}`;
100
+ if (at.custom_instructions) preview += `\n${at.custom_instructions}`;
86
101
  }
87
- preview += "\n";
88
102
  }
103
+
104
+ // External integrations
105
+ if (integrationTools.length > 0) {
106
+ preview += "\n\n## Integrações Externas";
107
+
108
+ for (const { at, tool } of integrationTools) {
109
+ preview += `\n\n### ${tool.name} (${tool.slug})`;
110
+ if (at.custom_instructions) preview += `\n${at.custom_instructions}`;
111
+ }
112
+ }
113
+
114
+ // Rules
115
+ preview += "\n\n## Regras";
116
+ preview += "\n- Sempre confirme com o usuário antes de criar ou alterar registros.";
117
+ preview += "\n- Nunca invente dados — sempre consulte primeiro.";
118
+ preview += "\n- Use EXATAMENTE os nomes de função listados acima.";
89
119
  }
90
120
 
91
121
  return preview;
@@ -277,13 +307,20 @@ export function AgentPromptEditor({ config, agent }: AgentPromptEditorProps) {
277
307
  <div className="border-t px-4 py-3">
278
308
  <pre className="max-h-96 overflow-auto whitespace-pre-wrap font-mono text-sm leading-relaxed">
279
309
  {previewText.split("\n").map((line, i) => {
280
- const isSection =
281
- line.startsWith("[SKILLS DISPONÍVEIS]") ||
282
- line.startsWith("[TOOLS DISPONÍVEIS]");
310
+ const isTopSection = line.startsWith("[CAPACIDADES E INTEGRAÇÕES]");
311
+ const isH2 = line.startsWith("## ");
312
+ const isH3 = line.startsWith("### ");
313
+ const cls = isTopSection
314
+ ? "font-bold text-foreground"
315
+ : isH2
316
+ ? "font-semibold text-muted-foreground"
317
+ : isH3
318
+ ? "font-medium text-muted-foreground"
319
+ : "";
283
320
  return (
284
321
  <span
285
322
  key={i}
286
- className={isSection ? "font-semibold text-muted-foreground" : ""}
323
+ className={cls}
287
324
  >
288
325
  {line}
289
326
  {"\n"}
@@ -25,7 +25,7 @@ import {
25
25
  Skeleton,
26
26
  } from "@greatapps/greatauth-ui/ui";
27
27
  import {
28
- Calendar,
28
+ CalendarCheck,
29
29
  Users,
30
30
  Settings,
31
31
  HeartHandshake,
@@ -55,7 +55,7 @@ function getOperationLabel(slug: string): string {
55
55
  // ---------------------------------------------------------------------------
56
56
 
57
57
  const CATEGORY_ICONS: Record<string, React.ElementType> = {
58
- agenda: Calendar,
58
+ agenda: CalendarCheck,
59
59
  cadastros: Users,
60
60
  infraestrutura: Settings,
61
61
  relacionamentos: HeartHandshake,
@@ -1,15 +1,33 @@
1
1
  'use client';
2
2
 
3
+ import { useState } from "react";
3
4
  import type { IntegrationCardData, IntegrationCardState } from "../../hooks/use-integrations";
4
- import { Badge, Button, Tooltip, TooltipContent, TooltipTrigger } from "@greatapps/greatauth-ui/ui";
5
+ import {
6
+ Badge,
7
+ Button,
8
+ DropdownMenu,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuTrigger,
12
+ AlertDialog,
13
+ AlertDialogAction,
14
+ AlertDialogCancel,
15
+ AlertDialogContent,
16
+ AlertDialogDescription,
17
+ AlertDialogFooter,
18
+ AlertDialogHeader,
19
+ AlertDialogTitle,
20
+ } from "@greatapps/greatauth-ui/ui";
5
21
  import {
6
22
  CalendarSync,
7
23
  Plug,
8
24
  Settings,
9
25
  RefreshCw,
10
- Users,
11
26
  Clock,
12
27
  Plus,
28
+ MoreVertical,
29
+ Unplug,
30
+ Trash2,
13
31
  } from "lucide-react";
14
32
  import type { LucideIcon } from "lucide-react";
15
33
  import { cn } from "../../lib";
@@ -23,7 +41,6 @@ const ICON_MAP: Record<string, LucideIcon> = {
23
41
  Plug,
24
42
  Settings,
25
43
  RefreshCw,
26
- Users,
27
44
  Clock,
28
45
  Plus,
29
46
  };
@@ -60,20 +77,6 @@ const STATE_BADGES: Record<IntegrationCardState, BadgeVariant> = {
60
77
  },
61
78
  };
62
79
 
63
- function getActionLabel(card: IntegrationCardData): string {
64
- if (card.isAddNew) return "Conectar";
65
- switch (card.state) {
66
- case "available":
67
- return "Conectar";
68
- case "connected":
69
- return "Configurar";
70
- case "expired":
71
- return "Reconectar";
72
- default:
73
- return "";
74
- }
75
- }
76
-
77
80
  // ---------------------------------------------------------------------------
78
81
  // Component
79
82
  // ---------------------------------------------------------------------------
@@ -81,25 +84,36 @@ function getActionLabel(card: IntegrationCardData): string {
81
84
  export interface IntegrationCardProps {
82
85
  card: IntegrationCardData;
83
86
  onConnect: (card: IntegrationCardData) => void;
87
+ onReconnect?: (card: IntegrationCardData) => void;
88
+ onDisconnect?: (card: IntegrationCardData) => void;
89
+ onDelete?: (card: IntegrationCardData) => void;
84
90
  }
85
91
 
86
- export function IntegrationCard({ card, onConnect }: IntegrationCardProps) {
87
- const { definition, state, sharedByAgentsCount, isAddNew, accountLabel } = card;
92
+ export function IntegrationCard({
93
+ card,
94
+ onConnect,
95
+ onReconnect,
96
+ onDisconnect,
97
+ onDelete,
98
+ }: IntegrationCardProps) {
99
+ const { definition, state, isAddNew, accountLabel } = card;
88
100
  const Icon = resolveIcon(definition.icon);
89
101
  const isComingSoon = state === "coming_soon";
90
- const actionLabel = getActionLabel(card);
102
+ const isConnected = state === "connected" || state === "expired";
91
103
 
92
- // "Add new" card uses a muted/outlined style
104
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
105
+
106
+ // "Add new" card — clean outline style with single "Conectar" button
93
107
  if (isAddNew) {
94
108
  return (
95
109
  <div
96
110
  className={cn(
97
- "group relative flex flex-col gap-3 rounded-xl border border-dashed bg-card/50 p-5 transition-shadow",
98
- "hover:shadow-md hover:border-solid hover:bg-card cursor-pointer",
111
+ "group relative flex flex-col gap-3 rounded-xl border bg-card/50 p-5 transition-all",
112
+ "hover:shadow-md hover:bg-card cursor-pointer",
99
113
  )}
100
114
  role="button"
101
115
  tabIndex={0}
102
- aria-label={`Adicionar conta ${definition.name}`}
116
+ aria-label={`Conectar ${definition.name}`}
103
117
  onClick={() => onConnect(card)}
104
118
  onKeyDown={(e) => {
105
119
  if (e.key === "Enter" || e.key === " ") {
@@ -109,24 +123,18 @@ export function IntegrationCard({ card, onConnect }: IntegrationCardProps) {
109
123
  }}
110
124
  >
111
125
  {/* Header row */}
112
- <div className="flex items-start justify-between gap-2">
126
+ <div className="flex items-start gap-3">
113
127
  <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/5 text-primary/60">
114
128
  <Icon className="h-5 w-5" />
115
129
  </div>
116
- <Badge variant="outline" className="text-xs bg-muted text-muted-foreground">
117
- Adicionar
118
- </Badge>
119
- </div>
120
-
121
- {/* Name + description */}
122
- <div className="space-y-1">
123
- <h3 className="text-sm font-semibold leading-tight text-muted-foreground">
124
- {definition.name}
125
- </h3>
126
- <p className="text-xs text-muted-foreground/70 leading-relaxed flex items-center gap-1">
127
- <Plus className="h-3 w-3" />
128
- Adicionar conta
129
- </p>
130
+ <div className="flex-1 min-w-0 space-y-0.5">
131
+ <h3 className="text-sm font-semibold leading-tight text-foreground">
132
+ {definition.name}
133
+ </h3>
134
+ <p className="text-xs text-muted-foreground leading-relaxed">
135
+ Conectar nova conta
136
+ </p>
137
+ </div>
130
138
  </div>
131
139
 
132
140
  {/* Footer */}
@@ -140,92 +148,132 @@ export function IntegrationCard({ card, onConnect }: IntegrationCardProps) {
140
148
  onConnect(card);
141
149
  }}
142
150
  >
143
- {actionLabel}
151
+ Conectar
144
152
  </Button>
145
153
  </div>
146
154
  </div>
147
155
  );
148
156
  }
149
157
 
150
- // Connected / expired / available card
151
- const badge = STATE_BADGES[state];
152
-
153
- return (
154
- <div
155
- className={cn(
156
- "group relative flex flex-col gap-3 rounded-xl border bg-card p-5 transition-shadow",
157
- isComingSoon
158
- ? "opacity-60 cursor-default"
159
- : "hover:shadow-md cursor-pointer",
160
- )}
161
- role="button"
162
- tabIndex={isComingSoon ? -1 : 0}
163
- aria-label={`${definition.name}${accountLabel ? ` — ${accountLabel}` : ""} — ${badge.label}`}
164
- aria-disabled={isComingSoon}
165
- onClick={() => !isComingSoon && onConnect(card)}
166
- onKeyDown={(e) => {
167
- if (!isComingSoon && (e.key === "Enter" || e.key === " ")) {
168
- e.preventDefault();
169
- onConnect(card);
170
- }
171
- }}
172
- >
173
- {/* Header row */}
174
- <div className="flex items-start justify-between gap-2">
175
- <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
176
- <Icon className="h-5 w-5" />
158
+ // Coming soon card
159
+ if (isComingSoon) {
160
+ const badge = STATE_BADGES[state];
161
+ return (
162
+ <div
163
+ className="group relative flex flex-col gap-3 rounded-xl border bg-card p-5 opacity-60 cursor-default"
164
+ aria-disabled
165
+ >
166
+ <div className="flex items-start justify-between gap-2">
167
+ <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
168
+ <Icon className="h-5 w-5" />
169
+ </div>
170
+ <Badge variant="outline" className={cn("text-xs", badge.className)}>
171
+ {badge.label}
172
+ </Badge>
177
173
  </div>
178
- <Badge variant="outline" className={cn("text-xs", badge.className)}>
179
- {badge.label}
180
- </Badge>
181
- </div>
182
-
183
- {/* Name + account label */}
184
- <div className="space-y-1">
185
- <h3 className="text-sm font-semibold leading-tight">{definition.name}</h3>
186
- {accountLabel ? (
187
- <p className="text-xs text-muted-foreground leading-relaxed truncate" title={accountLabel}>
188
- {accountLabel}
189
- </p>
190
- ) : (
174
+ <div className="space-y-1">
175
+ <h3 className="text-sm font-semibold leading-tight">{definition.name}</h3>
191
176
  <p className="text-xs text-muted-foreground leading-relaxed">
192
177
  {definition.description}
193
178
  </p>
194
- )}
179
+ </div>
195
180
  </div>
181
+ );
182
+ }
196
183
 
197
- {/* Footer */}
198
- <div className="mt-auto flex items-center justify-between gap-2 pt-1">
199
- {sharedByAgentsCount > 0 ? (
200
- <Tooltip>
201
- <TooltipTrigger asChild>
202
- <span className="inline-flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400">
203
- <Users className="h-3.5 w-3.5" />
204
- Compartilhada
205
- </span>
206
- </TooltipTrigger>
207
- <TooltipContent>
208
- Esta credencial está disponível para todos os agentes da conta
209
- </TooltipContent>
210
- </Tooltip>
211
- ) : (
212
- <span />
213
- )}
184
+ // Connected / expired card
185
+ const badge = STATE_BADGES[state];
214
186
 
215
- {!isComingSoon && (
216
- <Button
217
- variant={state === "expired" ? "destructive" : "outline"}
218
- size="sm"
219
- className="text-xs"
220
- onClick={(e) => {
221
- e.stopPropagation();
222
- onConnect(card);
223
- }}
224
- >
225
- {actionLabel}
226
- </Button>
227
- )}
187
+ return (
188
+ <>
189
+ <div
190
+ className="group relative flex flex-col gap-3 rounded-xl border bg-card p-5 transition-shadow hover:shadow-md"
191
+ >
192
+ {/* Header row */}
193
+ <div className="flex items-start justify-between gap-2">
194
+ <div className="flex items-start gap-3 min-w-0 flex-1">
195
+ <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
196
+ <Icon className="h-5 w-5" />
197
+ </div>
198
+ <div className="flex-1 min-w-0 space-y-0.5">
199
+ <h3 className="text-sm font-semibold leading-tight">{definition.name}</h3>
200
+ {accountLabel && (
201
+ <p className="text-xs text-muted-foreground leading-relaxed truncate" title={accountLabel}>
202
+ {accountLabel}
203
+ </p>
204
+ )}
205
+ </div>
206
+ </div>
207
+ <Badge variant="outline" className={cn("text-xs shrink-0", badge.className)}>
208
+ {badge.label}
209
+ </Badge>
210
+ </div>
211
+
212
+ {/* Footer with "Configurar" dropdown */}
213
+ <div className="mt-auto flex items-center justify-end gap-2 pt-1">
214
+ <DropdownMenu>
215
+ <DropdownMenuTrigger asChild>
216
+ <Button
217
+ variant="outline"
218
+ size="sm"
219
+ className="text-xs gap-1.5"
220
+ >
221
+ <Settings className="h-3.5 w-3.5" />
222
+ Configurar
223
+ </Button>
224
+ </DropdownMenuTrigger>
225
+ <DropdownMenuContent align="end">
226
+ <DropdownMenuItem
227
+ onClick={() => onReconnect?.(card)}
228
+ className="gap-2"
229
+ >
230
+ <RefreshCw className="h-4 w-4" />
231
+ Reconectar
232
+ </DropdownMenuItem>
233
+ <DropdownMenuItem
234
+ onClick={() => onDisconnect?.(card)}
235
+ className="gap-2"
236
+ >
237
+ <Unplug className="h-4 w-4" />
238
+ Desconectar
239
+ </DropdownMenuItem>
240
+ <DropdownMenuItem
241
+ onClick={() => setDeleteDialogOpen(true)}
242
+ className="gap-2 text-destructive focus:text-destructive"
243
+ >
244
+ <Trash2 className="h-4 w-4" />
245
+ Remover
246
+ </DropdownMenuItem>
247
+ </DropdownMenuContent>
248
+ </DropdownMenu>
249
+ </div>
228
250
  </div>
229
- </div>
251
+
252
+ {/* Delete confirmation dialog */}
253
+ <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
254
+ <AlertDialogContent>
255
+ <AlertDialogHeader>
256
+ <AlertDialogTitle>Remover integração?</AlertDialogTitle>
257
+ <AlertDialogDescription>
258
+ Esta ação vai remover a credencial
259
+ {accountLabel ? ` (${accountLabel})` : ""} de {definition.name}.
260
+ Esta ação não pode ser desfeita.
261
+ </AlertDialogDescription>
262
+ </AlertDialogHeader>
263
+ <AlertDialogFooter>
264
+ <AlertDialogCancel>Cancelar</AlertDialogCancel>
265
+ <AlertDialogAction
266
+ onClick={() => {
267
+ onDelete?.(card);
268
+ setDeleteDialogOpen(false);
269
+ }}
270
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
271
+ >
272
+ Remover
273
+ </AlertDialogAction>
274
+ </AlertDialogFooter>
275
+ </AlertDialogContent>
276
+ </AlertDialog>
277
+ </>
230
278
  );
231
279
  }
@@ -347,10 +347,13 @@ export function IntegrationWizard({
347
347
  //
348
348
  // For OAuth, the credential was already created by the backend callback.
349
349
  // The parent component handles any additional linking (agent_tool creation)
350
- // via the onComplete callback.
350
+ // and query invalidation via the onComplete callback.
351
+ //
352
+ // Call onComplete BEFORE closing the dialog so query invalidation
353
+ // triggers while the dialog is still mounted (avoids stale UI).
351
354
 
352
355
  onComplete();
353
- onOpenChange(false);
356
+
354
357
  toast.success(
355
358
  `${integration.name} ${isReconnect ? "reconectado" : "configurado"} com sucesso!`,
356
359
  );
@@ -4,12 +4,14 @@ import { useCallback } from "react";
4
4
  import type { GagentsHookConfig } from "../../hooks/types";
5
5
  import { useIntegrationState } from "../../hooks/use-integrations";
6
6
  import { useAgentTools, useAddAgentTool, useRemoveAgentTool } from "../../hooks/use-agent-tools";
7
- import { useTools } from "../../hooks/use-tools";
7
+ import { useCreateTool, useTools } from "../../hooks/use-tools";
8
8
  import { Switch, Tooltip, TooltipContent, TooltipTrigger } from "@greatapps/greatauth-ui/ui";
9
9
  import { Plug, Loader2 } from "lucide-react";
10
10
  import type { LucideIcon } from "lucide-react";
11
11
  import { CalendarSync } from "lucide-react";
12
12
  import { cn } from "../../lib";
13
+ import type { IntegrationCardData } from "../../hooks/use-integrations";
14
+ import type { Tool } from "../../types";
13
15
 
14
16
  // ---------------------------------------------------------------------------
15
17
  // Icon mapping
@@ -24,6 +26,14 @@ function resolveIcon(name: string): LucideIcon {
24
26
  return ICON_MAP[name] ?? Plug;
25
27
  }
26
28
 
29
+ // ---------------------------------------------------------------------------
30
+ // Integration custom_instructions per slug
31
+ // ---------------------------------------------------------------------------
32
+
33
+ const INTEGRATION_INSTRUCTIONS: Record<string, string> = {
34
+ "google-calendar": `Você tem acesso ao Google Calendar através da integração google-calendar.\nFunções disponíveis:\n- google_calendar_setup_oauth: Configurar conexão OAuth\n- google_calendar_check_status: Verificar status da conexão\n- google_calendar_list_events: Listar eventos do calendário\n- google_calendar_create_event: Criar novo evento\n- google_calendar_update_event: Atualizar evento existente\n- google_calendar_delete_event: Cancelar/remover evento\n\nUse EXATAMENTE os nomes de função listados acima.`,
35
+ };
36
+
27
37
  // ---------------------------------------------------------------------------
28
38
  // Props
29
39
  // ---------------------------------------------------------------------------
@@ -41,14 +51,15 @@ export function IntegrationsTab({
41
51
  config,
42
52
  agentId,
43
53
  }: IntegrationsTabProps) {
44
- const { cards, isLoading } = useIntegrationState(config, null);
45
- const { data: toolsData } = useTools(config);
54
+ const { cards, isLoading } = useIntegrationState(config, agentId);
46
55
  const { data: agentToolsData, isLoading: agentToolsLoading } = useAgentTools(config, agentId);
56
+ const { data: toolsData } = useTools(config);
47
57
  const addAgentTool = useAddAgentTool(config);
48
58
  const removeAgentTool = useRemoveAgentTool(config);
59
+ const createTool = useCreateTool(config);
49
60
 
50
- const tools = toolsData?.data ?? [];
51
61
  const agentTools = agentToolsData?.data ?? [];
62
+ const allTools = toolsData?.data ?? [];
52
63
 
53
64
  // Only show connected credentials (account-level)
54
65
  const connectedCards = cards.filter(
@@ -56,26 +67,58 @@ export function IntegrationsTab({
56
67
  );
57
68
 
58
69
  const handleToggle = useCallback(
59
- (credentialId: number, toolSlug: string, checked: boolean) => {
60
- // Find the tool record matching this integration slug
61
- const tool = tools.find((t) => t.slug === toolSlug);
62
- if (!tool) return;
63
-
70
+ async (card: IntegrationCardData, checked: boolean) => {
64
71
  if (checked) {
65
- // Add agent_tool linking this agent to this tool
72
+ let toolId = card.tool?.id;
73
+
74
+ // If no tool record exists for this integration, create one on-the-fly
75
+ if (!toolId) {
76
+ const existingTool = allTools.find((t) => t.slug === card.definition.slug);
77
+ if (existingTool) {
78
+ toolId = existingTool.id;
79
+ } else {
80
+ try {
81
+ const result = await createTool.mutateAsync({
82
+ name: card.definition.name,
83
+ slug: card.definition.slug,
84
+ type: "integration",
85
+ description: card.definition.description,
86
+ });
87
+ const d = result?.data;
88
+ toolId = (Array.isArray(d) ? d[0]?.id : (d as Tool | undefined)?.id) ?? undefined;
89
+ if (!toolId) {
90
+ console.error("[IntegrationsTab] Failed to create tool — no ID returned");
91
+ return;
92
+ }
93
+ } catch (err) {
94
+ console.error("[IntegrationsTab] Error creating tool:", err);
95
+ return;
96
+ }
97
+ }
98
+ }
99
+
100
+ // Create agent_tool with custom_instructions for the integration
101
+ const customInstructions = INTEGRATION_INSTRUCTIONS[card.definition.slug];
66
102
  addAgentTool.mutate({
67
103
  idAgent: agentId,
68
- body: { id_tool: tool.id, enabled: true },
104
+ body: {
105
+ id_tool: toolId,
106
+ enabled: true,
107
+ ...(customInstructions ? { custom_instructions: customInstructions } : {}),
108
+ },
69
109
  });
70
110
  } else {
71
111
  // Find the agent_tool to remove
72
- const agentTool = agentTools.find((at) => at.id_tool === tool.id);
73
- if (agentTool) {
74
- removeAgentTool.mutate({ idAgent: agentId, id: agentTool.id });
112
+ const toolId = card.tool?.id;
113
+ if (toolId) {
114
+ const agentTool = agentTools.find((at) => at.id_tool === toolId);
115
+ if (agentTool) {
116
+ removeAgentTool.mutate({ idAgent: agentId, id: agentTool.id });
117
+ }
75
118
  }
76
119
  }
77
120
  },
78
- [tools, agentTools, agentId, addAgentTool, removeAgentTool],
121
+ [agentTools, allTools, agentId, addAgentTool, removeAgentTool, createTool],
79
122
  );
80
123
 
81
124
  // Loading state
@@ -110,11 +153,8 @@ export function IntegrationsTab({
110
153
  <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
111
154
  {connectedCards.map((card) => {
112
155
  const Icon = resolveIcon(card.definition.icon);
113
- const tool = tools.find((t) => t.slug === card.definition.slug);
114
- const isLinked = tool
115
- ? agentTools.some((at) => at.id_tool === tool.id)
116
- : false;
117
- const isMutating = addAgentTool.isPending || removeAgentTool.isPending;
156
+ const isLinked = card.linkedToAgent;
157
+ const isMutating = addAgentTool.isPending || removeAgentTool.isPending || createTool.isPending;
118
158
 
119
159
  return (
120
160
  <div
@@ -149,14 +189,11 @@ export function IntegrationsTab({
149
189
  )}
150
190
  </div>
151
191
 
152
- {/* Toggle */}
192
+ {/* Toggle — no longer blocked by missing tool record */}
153
193
  <Switch
154
194
  checked={isLinked}
155
195
  disabled={isMutating}
156
- onCheckedChange={(checked) =>
157
- card.credentialId &&
158
- handleToggle(card.credentialId, card.definition.slug, checked)
159
- }
196
+ onCheckedChange={(checked) => handleToggle(card, checked)}
160
197
  aria-label={`${isLinked ? "Desativar" : "Ativar"} ${card.definition.name} para este agente`}
161
198
  />
162
199
  </div>