@agentforge-io/chat-sdk 2.0.17 → 2.0.19

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.
@@ -24,10 +24,20 @@ export type ChatMessageMetadata = {
24
24
  approvalId: string;
25
25
  toolName: string;
26
26
  expiresAt: string;
27
+ /** Friendly connector display name surfaced by the server when
28
+ * available (e.g. `'Granola'`). Lets the View render a
29
+ * natural-language card without the host having to map slugs
30
+ * to product names. */
31
+ connectorName?: string;
32
+ /** First line of the tool's description from the connector
33
+ * definition. Used as a one-line "what will happen" hint. */
34
+ toolDescription?: string;
27
35
  } | {
28
36
  kind: 'tool_blocked';
29
37
  toolName: string;
30
38
  reason?: string;
39
+ connectorName?: string;
40
+ toolDescription?: string;
31
41
  } | {
32
42
  kind: string;
33
43
  [k: string]: unknown;
package/dist/react.d.ts CHANGED
@@ -17,6 +17,59 @@
17
17
  */
18
18
  import { type CSSProperties } from 'react';
19
19
  import type { ChatTheme } from './entities';
20
+ /**
21
+ * Optional callback invoked when the visitor clicks the Approve/Deny
22
+ * button on a tool-approval bubble. When omitted, the bubble renders
23
+ * as a read-only "waiting" hint — the visitor can't decide from inside
24
+ * the widget. When wired, the widget calls this with `{approvalId,
25
+ * action}` and the host implementation typically POSTs to
26
+ * `/public/chat/approvals/:id/{approve|deny}` with the visitor's
27
+ * `browserSessionId` for ownership verification.
28
+ *
29
+ * Throwing or rejecting from the handler keeps the bubble in its
30
+ * pending state and surfaces the error inline so the visitor can
31
+ * retry.
32
+ */
33
+ export type ApprovalActionHandler = (args: {
34
+ approvalId: string;
35
+ action: 'approve' | 'deny';
36
+ }) => Promise<void>;
37
+ /**
38
+ * Localizable copy for the approval / blocked bubbles. Pass via the
39
+ * `approvalCopy` prop on `<ChatWidget>` to drop the defaults (Spanish,
40
+ * conversational) and tailor every line to your audience.
41
+ *
42
+ * Functions receive whatever context the server enriched the chunk
43
+ * with — typically a friendly connector name like `'Granola'`. When
44
+ * the server didn't supply it, the function is called with `undefined`
45
+ * and the implementation should pick a sensible generic line. The
46
+ * widget itself never references the raw tool slug in copy.
47
+ */
48
+ export interface ApprovalCopy {
49
+ title: string;
50
+ body: (ctx: {
51
+ connectorName?: string;
52
+ toolDescription?: string;
53
+ }) => string;
54
+ approveLabel: string;
55
+ approveBusyLabel: string;
56
+ denyLabel: string;
57
+ denyBusyLabel: string;
58
+ approvedPill: string;
59
+ deniedPill: string;
60
+ /** Shown when the host didn't wire `onApprovalAction` — the visitor
61
+ * can't decide from inside the widget so we tell them where to go. */
62
+ readOnlyHint: string;
63
+ /** Used by the `expires in …` countdown. Receives minutes/seconds
64
+ * and returns the formatted string. */
65
+ expiresIn: (mins: number, secs: number) => string;
66
+ /** Block bubble title + body. */
67
+ blockedTitle: string;
68
+ blockedBody: (ctx: {
69
+ connectorName?: string;
70
+ reason?: string;
71
+ }) => string;
72
+ }
20
73
  export interface ChatWidgetProps {
21
74
  /** Public chat token (`aft_*`) issued from the admin UI. */
22
75
  token: string;
@@ -38,6 +91,18 @@ export interface ChatWidgetProps {
38
91
  className?: string;
39
92
  /** Inline style on the root container. */
40
93
  style?: CSSProperties;
94
+ /**
95
+ * Handler for tool-approval bubble actions. See `ApprovalActionHandler`
96
+ * for the contract. Omit to render approval messages as read-only
97
+ * hints.
98
+ */
99
+ onApprovalAction?: ApprovalActionHandler;
100
+ /**
101
+ * Override any subset of the approval-card copy. Keys you don't
102
+ * provide fall back to the Spanish defaults in `DEFAULT_APPROVAL_COPY`.
103
+ * Use this for translations or to match your product's voice.
104
+ */
105
+ approvalCopy?: Partial<ApprovalCopy>;
41
106
  }
42
107
  /**
43
108
  * Drop-in chat widget. Owns its own `ChatSession` and re-renders on every
package/dist/react.js CHANGED
@@ -153,12 +153,38 @@ function renderMarkdown(src) {
153
153
  }
154
154
  return out.join('');
155
155
  }
156
+ /**
157
+ * Defaults are Spanish + conversational. The host overrides individual
158
+ * keys via the `approvalCopy` prop; unset keys keep these values.
159
+ */
160
+ const DEFAULT_APPROVAL_COPY = {
161
+ title: 'Necesito tu permiso',
162
+ body: ({ connectorName }) => connectorName
163
+ ? `¿Me das permiso para usar ${connectorName}?`
164
+ : '¿Me das permiso para continuar?',
165
+ approveLabel: 'Permitir',
166
+ approveBusyLabel: 'Permitiendo…',
167
+ denyLabel: 'No, gracias',
168
+ denyBusyLabel: 'Cancelando…',
169
+ approvedPill: 'Listo, continuando…',
170
+ deniedPill: 'Cancelado',
171
+ readOnlyHint: 'Pídele al administrador del workspace que lo apruebe.',
172
+ expiresIn: (m, s) => m >= 60
173
+ ? `Expira en ${Math.floor(m / 60)}h ${m % 60}m`
174
+ : `Expira en ${m}m ${s.toString().padStart(2, '0')}s`,
175
+ blockedTitle: 'No disponible',
176
+ blockedBody: ({ connectorName, reason }) => reason ??
177
+ (connectorName
178
+ ? `No puedo usar ${connectorName} en esta cuenta.`
179
+ : 'No puedo usar esa herramienta en esta cuenta.'),
180
+ };
156
181
  /**
157
182
  * Drop-in chat widget. Owns its own `ChatSession` and re-renders on every
158
183
  * SDK event. Consumers don't need to read `session.getState()` themselves.
159
184
  */
160
185
  function ChatWidget(props) {
161
- const { token, apiBaseUrl, inline = false, position, browserSessionId, resumeConversationId, stream, className, style, } = props;
186
+ const { token, apiBaseUrl, inline = false, position, browserSessionId, resumeConversationId, stream, className, style, onApprovalAction, approvalCopy, } = props;
187
+ const copy = { ...DEFAULT_APPROVAL_COPY, ...(approvalCopy ?? {}) };
162
188
  const [session, setSession] = (0, react_1.useState)(null);
163
189
  const [status, setStatus] = (0, react_1.useState)('idle');
164
190
  const [agent, setAgent] = (0, react_1.useState)();
@@ -278,9 +304,28 @@ function ChatWidget(props) {
278
304
  };
279
305
  return ((0, jsx_runtime_1.jsxs)("div", { className: rootClass, style: rootStyle, "data-style-id": styleId, children: [!inline && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-toggle", "aria-label": open ? 'Close chat' : 'Open chat', "aria-expanded": open, onClick: () => setOpen((v) => !v), children: open ? (0, jsx_runtime_1.jsx)(CloseIcon, {}) : (0, jsx_runtime_1.jsx)(ChatIcon, {}) })), (0, jsx_runtime_1.jsxs)("div", { className: `af-panel ${open ? 'af-open' : ''}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "af-header", children: [theme?.avatarUrl ? (
280
306
  // eslint-disable-next-line @next/next/no-img-element
281
- (0, jsx_runtime_1.jsx)("img", { className: "af-header-avatar", src: theme.avatarUrl, alt: "" })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "af-header-info", children: [(0, jsx_runtime_1.jsx)("div", { className: "af-header-title", children: theme?.title ?? agent?.name ?? 'Chat' }), (0, jsx_runtime_1.jsx)("div", { className: "af-header-subtitle", children: status === 'loading' ? 'Loading…' : agent?.description ?? '' })] }), !inline && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-close", onClick: () => setOpen(false), "aria-label": "Close chat", children: (0, jsx_runtime_1.jsx)(CloseIcon, {}) }))] }), (0, jsx_runtime_1.jsx)("div", { className: "af-messages", ref: messagesRef, children: messages.map((m) => ((0, jsx_runtime_1.jsx)(MessageBubble, { message: m }, m.id))) }), lastError && status === 'error' && ((0, jsx_runtime_1.jsx)("div", { className: "af-error", children: lastError })), (0, jsx_runtime_1.jsxs)("div", { className: "af-input-row", children: [(0, jsx_runtime_1.jsx)("textarea", { className: "af-input", value: draft, onChange: (e) => setDraft(e.target.value), onKeyDown: onKeyDown, placeholder: "Type a message\u2026", rows: 1, disabled: status === 'ended' || status === 'loading' || status === 'idle' }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-send", onClick: handleSend, disabled: sendDisabled, "aria-label": "Send message", children: (0, jsx_runtime_1.jsx)(SendIcon, {}) })] }), (0, jsx_runtime_1.jsx)("div", { className: "af-footer", children: "Powered by AgentForge" })] })] }));
307
+ (0, jsx_runtime_1.jsx)("img", { className: "af-header-avatar", src: theme.avatarUrl, alt: "" })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "af-header-info", children: [(0, jsx_runtime_1.jsx)("div", { className: "af-header-title", children: theme?.title ?? agent?.name ?? 'Chat' }), (0, jsx_runtime_1.jsx)("div", { className: "af-header-subtitle", children: status === 'loading' ? 'Loading…' : agent?.description ?? '' })] }), !inline && ((0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-close", onClick: () => setOpen(false), "aria-label": "Close chat", children: (0, jsx_runtime_1.jsx)(CloseIcon, {}) }))] }), (0, jsx_runtime_1.jsx)("div", { className: "af-messages", ref: messagesRef, children: messages.map((m) => ((0, jsx_runtime_1.jsx)(MessageBubble, { message: m, copy: copy, onApprovalAction: onApprovalAction, onContinue: () => {
308
+ // After a successful Approve, kick the next turn so the
309
+ // gate's fast-path consumes the approval and the tool
310
+ // actually runs. `silent: true` keeps the literal
311
+ // "continue" prompt out of the visible transcript —
312
+ // the visitor just sees the assistant's next reply
313
+ // appearing under the "Approved" pill.
314
+ if (!session)
315
+ return;
316
+ setTimeout(() => {
317
+ void session.send('continue', { silent: true });
318
+ }, 250);
319
+ } }, m.id))) }), lastError && status === 'error' && ((0, jsx_runtime_1.jsx)("div", { className: "af-error", children: lastError })), (0, jsx_runtime_1.jsxs)("div", { className: "af-input-row", children: [(0, jsx_runtime_1.jsx)("textarea", { className: "af-input", value: draft, onChange: (e) => setDraft(e.target.value), onKeyDown: onKeyDown, placeholder: "Type a message\u2026", rows: 1, disabled: status === 'ended' || status === 'loading' || status === 'idle' }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-send", onClick: handleSend, disabled: sendDisabled, "aria-label": "Send message", children: (0, jsx_runtime_1.jsx)(SendIcon, {}) })] }), (0, jsx_runtime_1.jsx)("div", { className: "af-footer", children: "Powered by AgentForge" })] })] }));
282
320
  }
283
- function MessageBubble({ message }) {
321
+ function MessageBubble({ message, copy, onApprovalAction, onContinue, }) {
322
+ const kind = message.metadata?.kind;
323
+ if (kind === 'awaiting_approval') {
324
+ return ((0, jsx_runtime_1.jsx)(ApprovalBubble, { message: message, copy: copy, onAction: onApprovalAction, onContinue: onContinue }));
325
+ }
326
+ if (kind === 'tool_blocked') {
327
+ return (0, jsx_runtime_1.jsx)(BlockedBubble, { message: message, copy: copy });
328
+ }
284
329
  const cls = `af-msg af-msg-${message.role}${message.role === 'assistant' && message.isStreaming
285
330
  ? message.content
286
331
  ? ' af-msg-streaming'
@@ -299,6 +344,78 @@ function MessageBubble({ message }) {
299
344
  // User & system messages stay as plain text — they're typed verbatim.
300
345
  return (0, jsx_runtime_1.jsx)("div", { className: cls, children: message.content });
301
346
  }
347
+ /**
348
+ * Awaiting-approval bubble. Renders the tool name + a countdown, and
349
+ * either the Approve/Deny buttons (when the host wired the handler)
350
+ * or a read-only hint. After a successful approve, fires `onContinue`
351
+ * so the chat auto-sends "continue" and the gate's fast-path consumes
352
+ * the row on the next turn.
353
+ */
354
+ function ApprovalBubble({ message, copy, onAction, onContinue, }) {
355
+ const meta = message.metadata;
356
+ const [busy, setBusy] = (0, react_1.useState)(null);
357
+ const [decided, setDecided] = (0, react_1.useState)(null);
358
+ const [err, setErr] = (0, react_1.useState)(null);
359
+ const remaining = useExpiresIn(meta?.expiresAt, copy.expiresIn);
360
+ if (!meta)
361
+ return null;
362
+ const act = async (action) => {
363
+ if (!onAction)
364
+ return;
365
+ setBusy(action);
366
+ setErr(null);
367
+ try {
368
+ await onAction({ approvalId: meta.approvalId, action });
369
+ setDecided(action === 'approve' ? 'approved' : 'denied');
370
+ if (action === 'approve')
371
+ onContinue();
372
+ }
373
+ catch (e) {
374
+ setErr(e instanceof Error ? e.message : String(e));
375
+ }
376
+ finally {
377
+ setBusy(null);
378
+ }
379
+ };
380
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "af-msg af-msg-approval", children: [(0, jsx_runtime_1.jsx)("div", { className: "af-approval-title", children: copy.title }), (0, jsx_runtime_1.jsx)("div", { className: "af-approval-body", children: copy.body({
381
+ connectorName: meta.connectorName,
382
+ toolDescription: meta.toolDescription,
383
+ }) }), remaining && !decided && ((0, jsx_runtime_1.jsx)("div", { className: "af-approval-meta", children: remaining })), decided && ((0, jsx_runtime_1.jsx)("div", { className: `af-approval-pill af-approval-pill-${decided}`, children: decided === 'approved' ? copy.approvedPill : copy.deniedPill })), !decided && onAction && ((0, jsx_runtime_1.jsxs)("div", { className: "af-approval-actions", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-approval-btn af-approval-approve", disabled: busy !== null, onClick: () => act('approve'), children: busy === 'approve' ? copy.approveBusyLabel : copy.approveLabel }), (0, jsx_runtime_1.jsx)("button", { type: "button", className: "af-approval-btn af-approval-deny", disabled: busy !== null, onClick: () => act('deny'), children: busy === 'deny' ? copy.denyBusyLabel : copy.denyLabel })] })), !decided && !onAction && ((0, jsx_runtime_1.jsx)("div", { className: "af-approval-hint", children: copy.readOnlyHint })), err && (0, jsx_runtime_1.jsx)("div", { className: "af-approval-error", children: err })] }));
384
+ }
385
+ /**
386
+ * Terminal "tool blocked" bubble. No action — just informs the visitor
387
+ * that the tool the agent tried isn't available.
388
+ */
389
+ function BlockedBubble({ message, copy, }) {
390
+ const meta = message.metadata;
391
+ if (!meta)
392
+ return null;
393
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "af-msg af-msg-blocked", children: [(0, jsx_runtime_1.jsx)("div", { className: "af-blocked-title", children: copy.blockedTitle }), (0, jsx_runtime_1.jsx)("div", { className: "af-blocked-body", children: copy.blockedBody({
394
+ connectorName: meta.connectorName,
395
+ reason: meta.reason,
396
+ }) })] }));
397
+ }
398
+ /** Live "expires in" countdown. Returns null after the deadline.
399
+ * The format function is host-supplied so the label stays localisable —
400
+ * the hook itself doesn't hard-code any user-facing string. */
401
+ function useExpiresIn(iso, format) {
402
+ const [, force] = (0, react_1.useState)(0);
403
+ (0, react_1.useEffect)(() => {
404
+ if (!iso)
405
+ return;
406
+ const t = setInterval(() => force((n) => n + 1), 1000);
407
+ return () => clearInterval(t);
408
+ }, [iso]);
409
+ if (!iso)
410
+ return null;
411
+ const ms = new Date(iso).getTime() - Date.now();
412
+ if (!Number.isFinite(ms) || ms <= 0)
413
+ return null;
414
+ const totalSeconds = Math.floor(ms / 1000);
415
+ const m = Math.floor(totalSeconds / 60);
416
+ const s = totalSeconds % 60;
417
+ return format(m, s);
418
+ }
302
419
  function ChatIcon() {
303
420
  return ((0, jsx_runtime_1.jsx)("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": true, children: (0, jsx_runtime_1.jsx)("path", { d: "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" }) }));
304
421
  }
@@ -402,4 +519,25 @@ const WIDGET_CSS = `
402
519
  .af-send svg { width: 16px; height: 16px; }
403
520
  .af-error { padding: 10px 16px; font-size: 12px; color: #b91c1c; background: #fef2f2; border-top: 1px solid #fee2e2; }
404
521
  .af-footer { padding: 6px 12px; font-size: 10px; color: var(--af-muted); text-align: center; background: var(--af-bubble-bg); border-top: 1px solid var(--af-border); }
522
+ /* Approval and blocked bubbles. Amber for needs-decision, red for
523
+ * won't-happen. Same animation-in as the regular .af-msg so the
524
+ * transition feels uniform. */
525
+ .af-msg-approval { align-self: flex-start; max-width: 90%; padding: 12px 14px; border-radius: 14px; border-bottom-left-radius: 4px; font-size: 13px; line-height: 1.5; background: #fffbeb; border: 1px solid #fde68a; color: #7c2d12; animation: af-msg-in 220ms cubic-bezier(0.2, 0.8, 0.2, 1); }
526
+ .af-approval-title { font-weight: 600; font-size: 13px; margin-bottom: 4px; }
527
+ .af-approval-body { font-size: 13px; line-height: 1.5; }
528
+ .af-approval-tool { background: rgba(0,0,0,0.06); padding: 1px 6px; border-radius: 4px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
529
+ .af-approval-meta { font-size: 11px; margin-top: 6px; color: #92400e; }
530
+ .af-approval-actions { display: flex; gap: 8px; margin-top: 10px; }
531
+ .af-approval-btn { padding: 6px 12px; font-size: 12px; font-weight: 600; border-radius: 8px; cursor: pointer; transition: opacity 0.15s ease; }
532
+ .af-approval-btn:disabled { opacity: 0.6; cursor: not-allowed; }
533
+ .af-approval-approve { background: var(--af-primary); color: white; border: none; }
534
+ .af-approval-deny { background: white; color: #7c2d12; border: 1px solid #fde68a; }
535
+ .af-approval-pill { display: inline-block; margin-top: 8px; padding: 2px 8px; border-radius: 999px; font-size: 11px; font-weight: 600; }
536
+ .af-approval-pill-approved { background: #dcfce7; color: #166534; }
537
+ .af-approval-pill-denied { background: #fee2e2; color: #991b1b; }
538
+ .af-approval-hint { font-size: 12px; margin-top: 8px; color: #7c2d12; }
539
+ .af-approval-error { font-size: 11px; margin-top: 6px; color: #b91c1c; }
540
+ .af-msg-blocked { align-self: flex-start; max-width: 90%; padding: 10px 14px; border-radius: 14px; border-bottom-left-radius: 4px; font-size: 13px; line-height: 1.5; background: #fef2f2; border: 1px solid #fecaca; color: #7f1d1d; animation: af-msg-in 220ms cubic-bezier(0.2, 0.8, 0.2, 1); }
541
+ .af-blocked-title { font-weight: 600; font-size: 13px; margin-bottom: 4px; }
542
+ .af-blocked-body { font-size: 12px; line-height: 1.5; }
405
543
  `;
package/dist/session.d.ts CHANGED
@@ -42,8 +42,18 @@ export declare class ChatSession {
42
42
  * Send a user message. Opens the conversation on first call. Returns the
43
43
  * final assistant content for callers that want to await it; the same
44
44
  * content is also emitted via `message_added` / `message_updated`.
45
+ *
46
+ * `silent: true` suppresses the user-message bubble that would normally
47
+ * appear in the view layer. Used by the tool-approval auto-continue
48
+ * flow: after the visitor clicks Approve, the widget fires a hidden
49
+ * `send('continue', { silent: true })` so the gate's fast-path
50
+ * triggers without leaking a literal "continue" message into the
51
+ * conversation. The server still sees a regular user message and the
52
+ * agent's reply comes back through the normal stream.
45
53
  */
46
- send(text: string): Promise<string>;
54
+ send(text: string, opts?: {
55
+ silent?: boolean;
56
+ }): Promise<string>;
47
57
  private runStream;
48
58
  private runNonStream;
49
59
  private appendMessage;
package/dist/session.js CHANGED
@@ -142,8 +142,16 @@ class ChatSession {
142
142
  * Send a user message. Opens the conversation on first call. Returns the
143
143
  * final assistant content for callers that want to await it; the same
144
144
  * content is also emitted via `message_added` / `message_updated`.
145
+ *
146
+ * `silent: true` suppresses the user-message bubble that would normally
147
+ * appear in the view layer. Used by the tool-approval auto-continue
148
+ * flow: after the visitor clicks Approve, the widget fires a hidden
149
+ * `send('continue', { silent: true })` so the gate's fast-path
150
+ * triggers without leaking a literal "continue" message into the
151
+ * conversation. The server still sees a regular user message and the
152
+ * agent's reply comes back through the normal stream.
145
153
  */
146
- async send(text) {
154
+ async send(text, opts) {
147
155
  const trimmed = text.trim();
148
156
  if (!trimmed)
149
157
  return '';
@@ -152,12 +160,14 @@ class ChatSession {
152
160
  }
153
161
  if (!this.started)
154
162
  await this.start();
155
- this.appendMessage({
156
- id: makeMessageId('u'),
157
- role: 'user',
158
- content: trimmed,
159
- createdAt: new Date(),
160
- });
163
+ if (!opts?.silent) {
164
+ this.appendMessage({
165
+ id: makeMessageId('u'),
166
+ role: 'user',
167
+ content: trimmed,
168
+ createdAt: new Date(),
169
+ });
170
+ }
161
171
  const assistant = {
162
172
  id: makeMessageId('a'),
163
173
  role: 'assistant',
@@ -202,9 +212,14 @@ class ChatSession {
202
212
  approvalId: c.approvalId,
203
213
  toolName: c.toolName,
204
214
  expiresAt: c.expiresAt,
215
+ connectorName: c.connectorName,
216
+ toolDescription: c.toolDescription,
205
217
  };
206
218
  assistant.content =
207
- full || `(waiting for approval to run \`${c.toolName}\`)`;
219
+ full ||
220
+ (c.connectorName
221
+ ? `Necesito tu permiso para usar ${c.connectorName}.`
222
+ : `Necesito tu permiso para continuar.`);
208
223
  this.updateMessage(assistant);
209
224
  continue;
210
225
  }
@@ -214,9 +229,15 @@ class ChatSession {
214
229
  kind: 'tool_blocked',
215
230
  toolName: c.toolName,
216
231
  reason: c.reason,
232
+ connectorName: c.connectorName,
233
+ toolDescription: c.toolDescription,
217
234
  };
218
235
  assistant.content =
219
- full || c.reason || `Tool "${c.toolName}" is blocked.`;
236
+ full ||
237
+ c.reason ||
238
+ (c.connectorName
239
+ ? `No puedo usar ${c.connectorName} en esta cuenta.`
240
+ : `No puedo usar esa herramienta en esta cuenta.`);
220
241
  this.updateMessage(assistant);
221
242
  continue;
222
243
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-io/chat-sdk",
3
- "version": "2.0.17",
3
+ "version": "2.0.19",
4
4
  "description": "Framework-free chat session SDK for AgentForge public chat tokens. Headless — no DOM. Drop into any frontend (React, Vue, Svelte, vanilla) and listen for events.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",