@agentforge-io/chat-sdk 2.0.20 → 2.0.21

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/dist/react.d.ts CHANGED
@@ -18,22 +18,18 @@
18
18
  import { type CSSProperties } from 'react';
19
19
  import type { ChatTheme } from './entities';
20
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.
21
+ * Optional observer fired after the SDK resolves a tool-approval bubble.
22
+ * The SDK owns the network call (it already has `apiBaseUrl` and the
23
+ * `browserSessionId`); the host only gets notified for analytics or
24
+ * audit logging. The hook is fire-and-forget throwing from it does
25
+ * not roll back the decision the server already committed.
32
26
  */
33
- export type ApprovalActionHandler = (args: {
27
+ export type ApprovalDecisionObserver = (args: {
34
28
  approvalId: string;
35
29
  action: 'approve' | 'deny';
36
- }) => Promise<void>;
30
+ ok: boolean;
31
+ error?: string;
32
+ }) => void;
37
33
  /**
38
34
  * Localizable copy for the approval / blocked bubbles. Pass via the
39
35
  * `approvalCopy` prop on `<ChatWidget>` to drop the defaults (Spanish,
@@ -84,11 +80,19 @@ export interface ChatWidgetProps {
84
80
  /** Inline style on the root container. */
85
81
  style?: CSSProperties;
86
82
  /**
87
- * Handler for tool-approval bubble actions. See `ApprovalActionHandler`
88
- * for the contract. Omit to render approval messages as read-only
89
- * hints.
83
+ * Optional observer called after the SDK resolves an approval bubble.
84
+ * The SDK handles the network call internally pass this only if you
85
+ * want to record the decision for analytics. See
86
+ * `ApprovalDecisionObserver`.
87
+ */
88
+ onApprovalDecision?: ApprovalDecisionObserver;
89
+ /**
90
+ * When `false` (the default), the Approve/Deny buttons render on
91
+ * `awaiting_approval` bubbles. Set to `true` to render bubbles as
92
+ * read-only — useful for transcripts/embeds where the visitor cannot
93
+ * legitimately decide.
90
94
  */
91
- onApprovalAction?: ApprovalActionHandler;
95
+ readOnlyApprovals?: boolean;
92
96
  }
93
97
  /**
94
98
  * Drop-in chat widget. Owns its own `ChatSession` and re-renders on every
package/dist/react.js CHANGED
@@ -181,7 +181,7 @@ function fallbackCopy(ctx) {
181
181
  * SDK event. Consumers don't need to read `session.getState()` themselves.
182
182
  */
183
183
  function ChatWidget(props) {
184
- const { token, apiBaseUrl, inline = false, position, browserSessionId, resumeConversationId, stream, className, style, onApprovalAction, } = props;
184
+ const { token, apiBaseUrl, inline = false, position, browserSessionId, resumeConversationId, stream, className, style, onApprovalDecision, readOnlyApprovals = false, } = props;
185
185
  const [session, setSession] = (0, react_1.useState)(null);
186
186
  const [status, setStatus] = (0, react_1.useState)('idle');
187
187
  const [agent, setAgent] = (0, react_1.useState)();
@@ -301,7 +301,7 @@ function ChatWidget(props) {
301
301
  };
302
302
  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 ? (
303
303
  // eslint-disable-next-line @next/next/no-img-element
304
- (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, onApprovalAction: onApprovalAction, onContinue: () => {
304
+ (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, session: session, readOnly: readOnlyApprovals, onDecision: onApprovalDecision, onContinue: () => {
305
305
  // After a successful Approve, kick the next turn so the
306
306
  // gate's fast-path consumes the approval and the tool
307
307
  // actually runs. `silent: true` keeps the literal
@@ -315,10 +315,10 @@ function ChatWidget(props) {
315
315
  }, 250);
316
316
  } }, 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" })] })] }));
317
317
  }
318
- function MessageBubble({ message, onApprovalAction, onContinue, }) {
318
+ function MessageBubble({ message, session, readOnly, onDecision, onContinue, }) {
319
319
  const kind = message.metadata?.kind;
320
320
  if (kind === 'awaiting_approval') {
321
- return ((0, jsx_runtime_1.jsx)(ApprovalBubble, { message: message, onAction: onApprovalAction, onContinue: onContinue }));
321
+ return ((0, jsx_runtime_1.jsx)(ApprovalBubble, { message: message, session: session, readOnly: readOnly, onDecision: onDecision, onContinue: onContinue }));
322
322
  }
323
323
  if (kind === 'tool_blocked') {
324
324
  return (0, jsx_runtime_1.jsx)(BlockedBubble, { message: message });
@@ -348,7 +348,7 @@ function MessageBubble({ message, onApprovalAction, onContinue, }) {
348
348
  * so the chat auto-sends "continue" and the gate's fast-path consumes
349
349
  * the row on the next turn.
350
350
  */
351
- function ApprovalBubble({ message, onAction, onContinue, }) {
351
+ function ApprovalBubble({ message, session, readOnly, onDecision, onContinue, }) {
352
352
  const meta = message.metadata;
353
353
  const [busy, setBusy] = (0, react_1.useState)(null);
354
354
  const [decided, setDecided] = (0, react_1.useState)(null);
@@ -361,24 +361,27 @@ function ApprovalBubble({ message, onAction, onContinue, }) {
361
361
  if (!meta)
362
362
  return null;
363
363
  const act = async (action) => {
364
- if (!onAction)
364
+ if (!session)
365
365
  return;
366
366
  setBusy(action);
367
367
  setErr(null);
368
368
  try {
369
- await onAction({ approvalId: meta.approvalId, action });
369
+ await session.resolveApproval(meta.approvalId, action);
370
370
  setDecided(action === 'approve' ? 'approved' : 'denied');
371
+ onDecision?.({ approvalId: meta.approvalId, action, ok: true });
371
372
  if (action === 'approve')
372
373
  onContinue();
373
374
  }
374
375
  catch (e) {
375
- setErr(e instanceof Error ? e.message : String(e));
376
+ const message = e instanceof Error ? e.message : String(e);
377
+ setErr(message);
378
+ onDecision?.({ approvalId: meta.approvalId, action, ok: false, error: message });
376
379
  }
377
380
  finally {
378
381
  setBusy(null);
379
382
  }
380
383
  };
381
- 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 }), 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
+ 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 }), 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 && !readOnly && session && ((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 && readOnly && ((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 })] }));
382
385
  }
383
386
  /**
384
387
  * Terminal "tool blocked" bubble. No action — just informs the visitor
package/dist/session.d.ts CHANGED
@@ -36,6 +36,13 @@ export declare class ChatSession {
36
36
  * continue chatting.
37
37
  */
38
38
  end(): Promise<void>;
39
+ /**
40
+ * Resolve a pending tool-approval bubble. The SDK owns the transport
41
+ * call so hosts don't have to know the approval endpoint or assemble
42
+ * the `browserSessionId` payload. Errors propagate so the calling View
43
+ * layer can keep the bubble in pending state and show the message.
44
+ */
45
+ resolveApproval(approvalId: string, action: 'approve' | 'deny'): Promise<void>;
39
46
  /** Tear down. Future sends throw. Useful for SPA unmount. */
40
47
  destroy(): void;
41
48
  /**
package/dist/session.js CHANGED
@@ -129,6 +129,15 @@ class ChatSession {
129
129
  this.handleError(err);
130
130
  }
131
131
  }
132
+ /**
133
+ * Resolve a pending tool-approval bubble. The SDK owns the transport
134
+ * call so hosts don't have to know the approval endpoint or assemble
135
+ * the `browserSessionId` payload. Errors propagate so the calling View
136
+ * layer can keep the bubble in pending state and show the message.
137
+ */
138
+ async resolveApproval(approvalId, action) {
139
+ await this.transport.resolveApproval(approvalId, action, this.browserSessionId);
140
+ }
132
141
  /** Tear down. Future sends throw. Useful for SPA unmount. */
133
142
  destroy() {
134
143
  this.listeners.clear();
@@ -83,6 +83,15 @@ export declare class HttpTransport {
83
83
  metadata?: Record<string, unknown>;
84
84
  }>;
85
85
  } | null>;
86
+ /**
87
+ * Resolve a pending tool-approval bubble. The server verifies the row
88
+ * is owned by `browser:<browserSessionId>` before mutating it, so the
89
+ * SDK forwards the same browser session it uses for messaging. The
90
+ * route lives under `/public/chat/approvals/*` — siblings of this
91
+ * transport's `/public/chat/:token/*` routes — and does not need the
92
+ * widget token (the approvalId is the capability).
93
+ */
94
+ resolveApproval(approvalId: string, action: 'approve' | 'deny', browserSessionId: string): Promise<void>;
86
95
  endConversation(conversationId: string, browserSessionId: string): Promise<void>;
87
96
  /** Parse the server's SSE response body into typed events. */
88
97
  private readSse;
package/dist/transport.js CHANGED
@@ -111,6 +111,26 @@ class HttpTransport {
111
111
  })),
112
112
  };
113
113
  }
114
+ /**
115
+ * Resolve a pending tool-approval bubble. The server verifies the row
116
+ * is owned by `browser:<browserSessionId>` before mutating it, so the
117
+ * SDK forwards the same browser session it uses for messaging. The
118
+ * route lives under `/public/chat/approvals/*` — siblings of this
119
+ * transport's `/public/chat/:token/*` routes — and does not need the
120
+ * widget token (the approvalId is the capability).
121
+ */
122
+ async resolveApproval(approvalId, action, browserSessionId) {
123
+ const base = this.apiBaseUrl.replace(/\/$/, '');
124
+ const res = await fetch(`${base}/public/chat/approvals/${encodeURIComponent(approvalId)}/${action}`, {
125
+ method: 'POST',
126
+ headers: { 'Content-Type': 'application/json' },
127
+ body: JSON.stringify({ browserSessionId }),
128
+ });
129
+ if (!res.ok && res.status !== 204) {
130
+ const data = await safeJson(res);
131
+ throw makeTransportError(data?.message ?? `HTTP ${res.status}`, res.status, data?.error);
132
+ }
133
+ }
114
134
  async endConversation(conversationId, browserSessionId) {
115
135
  const res = await fetch(this.url(`/conversations/${encodeURIComponent(conversationId)}/end`), {
116
136
  method: 'POST',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-io/chat-sdk",
3
- "version": "2.0.20",
3
+ "version": "2.0.21",
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",