@agentforge-io/chat-sdk 2.1.0 → 2.1.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.
Files changed (2) hide show
  1. package/dist/react.js +45 -6
  2. package/package.json +1 -1
package/dist/react.js CHANGED
@@ -250,17 +250,36 @@ function ChatWidget(props) {
250
250
  const messagesRef = (0, react_1.useRef)(null);
251
251
  const inputRef = (0, react_1.useRef)(null);
252
252
  const styleId = (0, react_1.useId)();
253
- // Auto-focus the composer on desktop (pointer:fine) once the session
254
- // is ready. Skipped on touch devices focusing a textarea on iOS /
255
- // Android opens the on-screen keyboard, which is hostile on a public
256
- // profile page where the visitor probably wants to read first.
253
+ // Track whether the user has already engaged with the widget. We
254
+ // start at false (just landed on the page) and flip to true the
255
+ // moment `handleSend` fires. The auto-focus effect below uses this
256
+ // to decide whether the impending `status = 'ready'` is the first
257
+ // landing (skip focus on touch — opening the keyboard unprompted
258
+ // is hostile) or a return from a send-streaming cycle (re-focus
259
+ // ALWAYS — the user is mid-conversation, losing the cursor
260
+ // breaks the typing-rhythm).
261
+ const hasInteractedRef = (0, react_1.useRef)(false);
262
+ // Auto-focus the composer once the session is ready.
263
+ //
264
+ // Landing (hasInteractedRef.current === false):
265
+ // • pointer:fine → focus (desktop user expects ready-to-type)
266
+ // • pointer:coarse → skip (iOS/Android opening the keyboard
267
+ // before the visitor reads anything
268
+ // is jarring)
269
+ //
270
+ // Returning from a send (hasInteractedRef.current === true):
271
+ // • always focus, including touch — the user just hit Send,
272
+ // they're in conversation mode, their next thought is the
273
+ // next message, the keyboard should stay alive.
257
274
  (0, react_1.useEffect)(() => {
258
275
  if (status !== 'ready')
259
276
  return;
260
277
  if (typeof window === 'undefined')
261
278
  return;
262
- if (!window.matchMedia('(pointer: fine)').matches)
279
+ if (!hasInteractedRef.current &&
280
+ !window.matchMedia('(pointer: fine)').matches) {
263
281
  return;
282
+ }
264
283
  inputRef.current?.focus({ preventScroll: true });
265
284
  }, [status]);
266
285
  // ── Session lifecycle. Recreate when token / apiBaseUrl changes. ──────
@@ -306,11 +325,26 @@ function ChatWidget(props) {
306
325
  };
307
326
  }, [token, apiBaseUrl, browserSessionId, resumeConversationId, stream]);
308
327
  // Auto-scroll on new tokens.
328
+ //
329
+ // We defer the scroll into a requestAnimationFrame so the DOM has
330
+ // actually grown by the time we read `scrollHeight`. Without that
331
+ // tick, a stream of small text_deltas can leave the scroll lagging
332
+ // 1–2 chunks behind because the effect runs synchronously after
333
+ // the React commit but BEFORE the browser paints the new rows.
334
+ // Result: the user sees the latest sentence half-cut at the bottom.
335
+ //
336
+ // `behavior: 'smooth'` doesn't help much during a fast stream (each
337
+ // rAF queues a new smooth-scroll that cancels the previous one) but
338
+ // it does make the FINAL settle look fluid — and that's the part
339
+ // the user notices when the stream stops.
309
340
  (0, react_1.useEffect)(() => {
310
341
  const el = messagesRef.current;
311
342
  if (!el)
312
343
  return;
313
- el.scrollTop = el.scrollHeight;
344
+ const raf = requestAnimationFrame(() => {
345
+ el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
346
+ });
347
+ return () => cancelAnimationFrame(raf);
314
348
  }, [messages]);
315
349
  // Inject the widget stylesheet exactly once per page. We key the <style>
316
350
  // tag by a fixed id so multiple widget mounts share it.
@@ -331,6 +365,11 @@ function ChatWidget(props) {
331
365
  const text = draft.trim();
332
366
  if (!text)
333
367
  return;
368
+ // Mark engagement BEFORE the status flip so the auto-focus
369
+ // effect (which watches `status`) sees the flag when it
370
+ // re-runs after `ready` returns. Subsequent renders will
371
+ // refocus the textarea on every send completion.
372
+ hasInteractedRef.current = true;
334
373
  setDraft('');
335
374
  void session.send(text);
336
375
  }, [session, draft]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-io/chat-sdk",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
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",