@agentforge-io/chat-sdk 2.0.18 → 2.0.20
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/entities.d.ts +24 -0
- package/dist/react.d.ts +28 -0
- package/dist/react.js +49 -17
- package/dist/session.d.ts +11 -1
- package/dist/session.js +28 -10
- package/package.json +1 -1
package/dist/entities.d.ts
CHANGED
|
@@ -19,15 +19,39 @@ export type ChatRole = 'user' | 'assistant' | 'system';
|
|
|
19
19
|
* Future kinds extend this union without breaking back-compat: the
|
|
20
20
|
* View checks `kind` and falls back to plain text when it's unknown.
|
|
21
21
|
*/
|
|
22
|
+
export interface ChatApprovalCopy {
|
|
23
|
+
title: string;
|
|
24
|
+
body: string;
|
|
25
|
+
approveLabel: string;
|
|
26
|
+
approveBusyLabel: string;
|
|
27
|
+
denyLabel: string;
|
|
28
|
+
denyBusyLabel: string;
|
|
29
|
+
approvedPill: string;
|
|
30
|
+
deniedPill: string;
|
|
31
|
+
readOnlyHint: string;
|
|
32
|
+
blockedTitle: string;
|
|
33
|
+
blockedBody: string;
|
|
34
|
+
expiresPrefix: string;
|
|
35
|
+
}
|
|
22
36
|
export type ChatMessageMetadata = {
|
|
23
37
|
kind: 'awaiting_approval';
|
|
24
38
|
approvalId: string;
|
|
25
39
|
toolName: string;
|
|
26
40
|
expiresAt: string;
|
|
41
|
+
connectorName?: string;
|
|
42
|
+
toolDescription?: string;
|
|
43
|
+
/** Server-authored microcopy for the approval bubble. Generated
|
|
44
|
+
* on every pause by an AI copywriter on the backend so language
|
|
45
|
+
* and tone match the live conversation. View layers render it
|
|
46
|
+
* verbatim; when missing, fall back to a minimal meta render. */
|
|
47
|
+
copy?: ChatApprovalCopy;
|
|
27
48
|
} | {
|
|
28
49
|
kind: 'tool_blocked';
|
|
29
50
|
toolName: string;
|
|
30
51
|
reason?: string;
|
|
52
|
+
connectorName?: string;
|
|
53
|
+
toolDescription?: string;
|
|
54
|
+
copy?: ChatApprovalCopy;
|
|
31
55
|
} | {
|
|
32
56
|
kind: string;
|
|
33
57
|
[k: string]: unknown;
|
package/dist/react.d.ts
CHANGED
|
@@ -34,6 +34,34 @@ export type ApprovalActionHandler = (args: {
|
|
|
34
34
|
approvalId: string;
|
|
35
35
|
action: 'approve' | 'deny';
|
|
36
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
|
+
* Server-authored approval microcopy. The bubble renders whatever the
|
|
41
|
+
* server places in `metadata.copy` — title, body, button labels, etc.
|
|
42
|
+
* That copy is generated by the AgentForge backend on every pause via a
|
|
43
|
+
* dedicated Haiku call, so its language and tone always match the
|
|
44
|
+
* conversation the user is having. There is no client-side dictionary
|
|
45
|
+
* to keep in sync.
|
|
46
|
+
*
|
|
47
|
+
* The shape mirrors `ApprovalCopyBundle` in `@agentforge-io/core`.
|
|
48
|
+
*/
|
|
49
|
+
export interface ApprovalCopy {
|
|
50
|
+
title: string;
|
|
51
|
+
body: string;
|
|
52
|
+
approveLabel: string;
|
|
53
|
+
approveBusyLabel: string;
|
|
54
|
+
denyLabel: string;
|
|
55
|
+
denyBusyLabel: string;
|
|
56
|
+
approvedPill: string;
|
|
57
|
+
deniedPill: string;
|
|
58
|
+
readOnlyHint: string;
|
|
59
|
+
blockedTitle: string;
|
|
60
|
+
blockedBody: string;
|
|
61
|
+
/** Prefix shown before the countdown, e.g. "Expires in". The seconds
|
|
62
|
+
* are appended client-side because they tick locally. */
|
|
63
|
+
expiresPrefix: string;
|
|
64
|
+
}
|
|
37
65
|
export interface ChatWidgetProps {
|
|
38
66
|
/** Public chat token (`aft_*`) issued from the admin UI. */
|
|
39
67
|
token: string;
|
package/dist/react.js
CHANGED
|
@@ -153,6 +153,29 @@ function renderMarkdown(src) {
|
|
|
153
153
|
}
|
|
154
154
|
return out.join('');
|
|
155
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* Minimal fallback used when the server didn't supply `metadata.copy`
|
|
158
|
+
* (older backend, copywriter timeout, missing API key, etc.). Intentionally
|
|
159
|
+
* terse and meta-only — the goal is to avoid lying about language. The
|
|
160
|
+
* server is the source of truth for natural copy.
|
|
161
|
+
*/
|
|
162
|
+
function fallbackCopy(ctx) {
|
|
163
|
+
const target = ctx.connectorName ?? ctx.toolName ?? '';
|
|
164
|
+
return {
|
|
165
|
+
title: target || 'Permission required',
|
|
166
|
+
body: target ? `${target} → ✓ / ✗` : 'Permission required',
|
|
167
|
+
approveLabel: '✓',
|
|
168
|
+
approveBusyLabel: '…',
|
|
169
|
+
denyLabel: '✗',
|
|
170
|
+
denyBusyLabel: '…',
|
|
171
|
+
approvedPill: '✓',
|
|
172
|
+
deniedPill: '✗',
|
|
173
|
+
readOnlyHint: '',
|
|
174
|
+
blockedTitle: target || 'Blocked',
|
|
175
|
+
blockedBody: ctx.reason ?? (target ? `${target} blocked` : 'Blocked'),
|
|
176
|
+
expiresPrefix: '⏱',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
156
179
|
/**
|
|
157
180
|
* Drop-in chat widget. Owns its own `ChatSession` and re-renders on every
|
|
158
181
|
* SDK event. Consumers don't need to read `session.getState()` themselves.
|
|
@@ -281,21 +304,18 @@ function ChatWidget(props) {
|
|
|
281
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: () => {
|
|
282
305
|
// After a successful Approve, kick the next turn so the
|
|
283
306
|
// gate's fast-path consumes the approval and the tool
|
|
284
|
-
// actually runs.
|
|
285
|
-
//
|
|
307
|
+
// actually runs. `silent: true` keeps the literal
|
|
308
|
+
// "continue" prompt out of the visible transcript —
|
|
309
|
+
// the visitor just sees the assistant's next reply
|
|
310
|
+
// appearing under the "Approved" pill.
|
|
286
311
|
if (!session)
|
|
287
312
|
return;
|
|
288
313
|
setTimeout(() => {
|
|
289
|
-
void session.send('continue');
|
|
314
|
+
void session.send('continue', { silent: true });
|
|
290
315
|
}, 250);
|
|
291
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" })] })] }));
|
|
292
317
|
}
|
|
293
318
|
function MessageBubble({ message, onApprovalAction, onContinue, }) {
|
|
294
|
-
// Structured metadata takes precedence over plain text. The server
|
|
295
|
-
// tags messages with `kind: 'awaiting_approval' | 'tool_blocked'`
|
|
296
|
-
// and the widget renders a dedicated bubble for each — unknown
|
|
297
|
-
// kinds fall through to the markdown renderer with the plain text
|
|
298
|
-
// body the server also persists.
|
|
299
319
|
const kind = message.metadata?.kind;
|
|
300
320
|
if (kind === 'awaiting_approval') {
|
|
301
321
|
return ((0, jsx_runtime_1.jsx)(ApprovalBubble, { message: message, onAction: onApprovalAction, onContinue: onContinue }));
|
|
@@ -333,7 +353,11 @@ function ApprovalBubble({ message, onAction, onContinue, }) {
|
|
|
333
353
|
const [busy, setBusy] = (0, react_1.useState)(null);
|
|
334
354
|
const [decided, setDecided] = (0, react_1.useState)(null);
|
|
335
355
|
const [err, setErr] = (0, react_1.useState)(null);
|
|
336
|
-
const
|
|
356
|
+
const copy = meta?.copy ?? fallbackCopy({
|
|
357
|
+
connectorName: meta?.connectorName,
|
|
358
|
+
toolName: meta?.toolName,
|
|
359
|
+
});
|
|
360
|
+
const remaining = useExpiresIn(meta?.expiresAt, copy.expiresPrefix);
|
|
337
361
|
if (!meta)
|
|
338
362
|
return null;
|
|
339
363
|
const act = async (action) => {
|
|
@@ -354,20 +378,27 @@ function ApprovalBubble({ message, onAction, onContinue, }) {
|
|
|
354
378
|
setBusy(null);
|
|
355
379
|
}
|
|
356
380
|
};
|
|
357
|
-
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:
|
|
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 })] }));
|
|
358
382
|
}
|
|
359
383
|
/**
|
|
360
384
|
* Terminal "tool blocked" bubble. No action — just informs the visitor
|
|
361
385
|
* that the tool the agent tried isn't available.
|
|
362
386
|
*/
|
|
363
|
-
function BlockedBubble({ message }) {
|
|
387
|
+
function BlockedBubble({ message, }) {
|
|
364
388
|
const meta = message.metadata;
|
|
365
389
|
if (!meta)
|
|
366
390
|
return null;
|
|
367
|
-
|
|
391
|
+
const copy = meta.copy ?? fallbackCopy({
|
|
392
|
+
connectorName: meta.connectorName,
|
|
393
|
+
toolName: meta.toolName,
|
|
394
|
+
reason: meta.reason,
|
|
395
|
+
});
|
|
396
|
+
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 })] }));
|
|
368
397
|
}
|
|
369
|
-
/** Live "expires in" countdown. Returns null after the deadline.
|
|
370
|
-
|
|
398
|
+
/** Live "expires in" countdown. Returns null after the deadline.
|
|
399
|
+
* The prefix label is server-authored so the language matches the
|
|
400
|
+
* rest of the bubble; the hook just appends the localised digits. */
|
|
401
|
+
function useExpiresIn(iso, prefix) {
|
|
371
402
|
const [, force] = (0, react_1.useState)(0);
|
|
372
403
|
(0, react_1.useEffect)(() => {
|
|
373
404
|
if (!iso)
|
|
@@ -383,9 +414,10 @@ function useExpiresIn(iso) {
|
|
|
383
414
|
const totalSeconds = Math.floor(ms / 1000);
|
|
384
415
|
const m = Math.floor(totalSeconds / 60);
|
|
385
416
|
const s = totalSeconds % 60;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
417
|
+
const tail = m >= 60
|
|
418
|
+
? `${Math.floor(m / 60)}h ${m % 60}m`
|
|
419
|
+
: `${m}m ${s.toString().padStart(2, '0')}s`;
|
|
420
|
+
return `${prefix} ${tail}`.trim();
|
|
389
421
|
}
|
|
390
422
|
function ChatIcon() {
|
|
391
423
|
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" }) }));
|
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
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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,
|
|
217
|
+
copy: c.copy,
|
|
205
218
|
};
|
|
206
|
-
|
|
207
|
-
|
|
219
|
+
// Plain-text fallback for legacy clients that don't render
|
|
220
|
+
// metadata.kind. Capable widgets ignore `content` and read
|
|
221
|
+
// `metadata.copy` directly.
|
|
222
|
+
assistant.content = full || c.copy?.body || (c.connectorName ?? '');
|
|
208
223
|
this.updateMessage(assistant);
|
|
209
224
|
continue;
|
|
210
225
|
}
|
|
@@ -214,9 +229,12 @@ 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,
|
|
234
|
+
copy: c.copy,
|
|
217
235
|
};
|
|
218
236
|
assistant.content =
|
|
219
|
-
full || c.reason ||
|
|
237
|
+
full || c.copy?.blockedBody || c.reason || (c.connectorName ?? '');
|
|
220
238
|
this.updateMessage(assistant);
|
|
221
239
|
continue;
|
|
222
240
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentforge-io/chat-sdk",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.20",
|
|
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",
|