@webless/agent 0.11.0 → 0.12.0

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.
@@ -138,16 +138,16 @@ function parseStep(value) {
138
138
  if (!hasOnlyKeys(value, ["description", "fieldPaths", "id", "label"])) {
139
139
  return null;
140
140
  }
141
- const id = boundedString(value.id, 80);
141
+ const id2 = boundedString(value.id, 80);
142
142
  const label = boundedString(value.label, 160);
143
143
  const description = value.description === void 0 ? void 0 : boundedString(value.description, 500);
144
- if (!id || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/u.test(id) || !label || value.description !== void 0 && !description || !Array.isArray(value.fieldPaths) || value.fieldPaths.length < 1 || value.fieldPaths.length > 32 || value.fieldPaths.some(
144
+ if (!id2 || !/^[A-Za-z0-9][A-Za-z0-9_-]*$/u.test(id2) || !label || value.description !== void 0 && !description || !Array.isArray(value.fieldPaths) || value.fieldPaths.length < 1 || value.fieldPaths.length > 32 || value.fieldPaths.some(
145
145
  (path) => typeof path !== "string" || !/^[A-Za-z0-9_.[\]-]+$/u.test(path) || path.length > 160
146
146
  )) {
147
147
  return null;
148
148
  }
149
149
  return {
150
- id,
150
+ id: id2,
151
151
  label,
152
152
  fieldPaths: value.fieldPaths,
153
153
  ...description ? { description } : {}
@@ -184,14 +184,14 @@ function parseAgentToolUiSurface(value) {
184
184
  ])) {
185
185
  return null;
186
186
  }
187
- const id = boundedString(value.id, 200);
187
+ const id2 = boundedString(value.id, 200);
188
188
  const title = boundedString(value.title, 200);
189
189
  const toolSlug = boundedString(value.toolSlug, 200);
190
190
  const description = value.description === void 0 ? void 0 : boundedString(value.description, 800);
191
191
  const operationId = value.operationId === void 0 ? void 0 : boundedString(value.operationId, 200);
192
192
  const requestId = value.requestId === void 0 ? void 0 : boundedString(value.requestId, 200);
193
193
  const submitLabel = value.submitLabel === void 0 ? void 0 : boundedString(value.submitLabel, 80);
194
- if (!id || !title || !toolSlug || !Array.isArray(value.fields) || value.fields.length < 1 || value.fields.length > 32) {
194
+ if (!id2 || !title || !toolSlug || !Array.isArray(value.fields) || value.fields.length < 1 || value.fields.length > 32) {
195
195
  return null;
196
196
  }
197
197
  const fields = value.fields.map(parseField);
@@ -211,7 +211,7 @@ function parseAgentToolUiSurface(value) {
211
211
  }
212
212
  return {
213
213
  schemaVersion: AGENT_TOOL_UI_SCHEMA_VERSION,
214
- id,
214
+ id: id2,
215
215
  title,
216
216
  toolSlug,
217
217
  fields,
@@ -391,14 +391,14 @@ function parseAgentSearchDiscoveryOutput(value) {
391
391
  var COMPOSER_FORM_SKIP_DISMISSAL = "I'd like to keep chatting without sharing the requested details for now. Please don't ask for them again unless I choose to proceed with something that needs them.";
392
392
  function visitorMessageDisplayText(message) {
393
393
  if (message.role !== "visitor") return message.text;
394
- const text2 = message.text.trim();
395
- if (!text2) return message.text;
396
- if (text2.startsWith(COMPOSER_FORM_SKIP_DISMISSAL)) {
397
- const afterDismissal = text2.slice(COMPOSER_FORM_SKIP_DISMISSAL.length).trim().replace(/^\n+/, "").trim();
394
+ const text3 = message.text.trim();
395
+ if (!text3) return message.text;
396
+ if (text3.startsWith(COMPOSER_FORM_SKIP_DISMISSAL)) {
397
+ const afterDismissal = text3.slice(COMPOSER_FORM_SKIP_DISMISSAL.length).trim().replace(/^\n+/, "").trim();
398
398
  if (afterDismissal) return afterDismissal;
399
399
  }
400
400
  const runtime = message.runtimeText?.trim();
401
- if (runtime && text2 === runtime && runtime.includes(COMPOSER_FORM_SKIP_DISMISSAL)) {
401
+ if (runtime && text3 === runtime && runtime.includes(COMPOSER_FORM_SKIP_DISMISSAL)) {
402
402
  const afterDismissal = runtime.slice(
403
403
  runtime.indexOf(COMPOSER_FORM_SKIP_DISMISSAL) + COMPOSER_FORM_SKIP_DISMISSAL.length
404
404
  ).trim().replace(/^\n+/, "").trim();
@@ -472,13 +472,13 @@ function parseVisitorFormFields(value) {
472
472
  const seen = /* @__PURE__ */ new Set();
473
473
  for (const item of value) {
474
474
  const record2 = asRecord(item);
475
- const id = asString(record2?.id).toLowerCase().replace(/[^a-z0-9_-]/g, "");
475
+ const id2 = asString(record2?.id).toLowerCase().replace(/[^a-z0-9_-]/g, "");
476
476
  const kind = asString(record2?.kind);
477
- if (!record2 || !id || seen.has(id) || !isFieldKind2(kind)) continue;
478
- seen.add(id);
479
- const label = asString(record2.label) || id;
477
+ if (!record2 || !id2 || seen.has(id2) || !isFieldKind2(kind)) continue;
478
+ seen.add(id2);
479
+ const label = asString(record2.label) || id2;
480
480
  fields.push({
481
- id,
481
+ id: id2,
482
482
  kind,
483
483
  label,
484
484
  placeholder: asString(record2.placeholder) || label,
@@ -514,52 +514,52 @@ function formatComposerFormMessage(form, values) {
514
514
  return value ? `${field.label}: ${value}` : "";
515
515
  }).filter(Boolean).join("\n");
516
516
  }
517
- function looksLikeFieldCollection(text2) {
517
+ function looksLikeFieldCollection(text3) {
518
518
  return /\b(please|need|send|share|enter|provide|add|collect|what|which|use)\b/i.test(
519
- text2
520
- ) || /:\s*$/m.test(text2) || /^[-*•]\s+/m.test(text2);
519
+ text3
520
+ ) || /:\s*$/m.test(text3) || /^[-*•]\s+/m.test(text3);
521
521
  }
522
- function looksLikeBookingCopy(text2) {
522
+ function looksLikeBookingCopy(text3) {
523
523
  return /book(ing)? (card|a demo|this time)|pick a (date|time)|available (times|slots)|calendly/i.test(
524
- text2
524
+ text3
525
525
  );
526
526
  }
527
- function echoedLabeledFieldIds(text2) {
527
+ function echoedLabeledFieldIds(text3) {
528
528
  const ids = /* @__PURE__ */ new Set();
529
- if (/\bname\s*:\s+\S+/i.test(text2)) ids.add("name");
530
- if (/\be-?mail\s*:\s+\S+/i.test(text2)) ids.add("email");
531
- if (/\bphone\s*:\s+\S+/i.test(text2)) ids.add("phone");
532
- if (/\bcompany\s*:\s+\S+/i.test(text2)) ids.add("company");
529
+ if (/\bname\s*:\s+\S+/i.test(text3)) ids.add("name");
530
+ if (/\be-?mail\s*:\s+\S+/i.test(text3)) ids.add("email");
531
+ if (/\bphone\s*:\s+\S+/i.test(text3)) ids.add("phone");
532
+ if (/\bcompany\s*:\s+\S+/i.test(text3)) ids.add("company");
533
533
  return ids;
534
534
  }
535
- function matchLibraryFields(text2) {
535
+ function matchLibraryFields(text3) {
536
536
  return FIELD_LIBRARY.flatMap((field) => {
537
- if (field.exclude?.test(text2)) {
538
- const leftover = text2.replace(field.exclude, " ");
537
+ if (field.exclude?.test(text3)) {
538
+ const leftover = text3.replace(field.exclude, " ");
539
539
  if (!field.patterns.some((pattern) => pattern.test(leftover))) return [];
540
- } else if (!field.patterns.some((pattern) => pattern.test(text2))) {
540
+ } else if (!field.patterns.some((pattern) => pattern.test(text3))) {
541
541
  return [];
542
542
  }
543
543
  const { patterns: _patterns, exclude: _exclude, ...next } = field;
544
544
  return [next];
545
545
  }).slice(0, MAX_FORM_FIELDS);
546
546
  }
547
- function looksLikeCompletedActionRecap(text2) {
548
- const confirmingCreate = /\bshould i create this\b/i.test(text2);
549
- const echoingFilledFields = /\b(?:name|email|phone|company)\s*:\s+\S+/i.test(text2) && /[^\s@]+@[^\s@]+\.[^\s@]+/i.test(text2);
547
+ function looksLikeCompletedActionRecap(text3) {
548
+ const confirmingCreate = /\bshould i create this\b/i.test(text3);
549
+ const echoingFilledFields = /\b(?:name|email|phone|company)\s*:\s+\S+/i.test(text3) && /[^\s@]+@[^\s@]+\.[^\s@]+/i.test(text3);
550
550
  if (echoingFilledFields) {
551
- if (looksLikeFieldCollection(text2)) {
552
- const echoed = echoedLabeledFieldIds(text2);
553
- if (matchLibraryFields(text2).some((field) => !echoed.has(field.id))) {
551
+ if (looksLikeFieldCollection(text3)) {
552
+ const echoed = echoedLabeledFieldIds(text3);
553
+ if (matchLibraryFields(text3).some((field) => !echoed.has(field.id))) {
554
554
  return false;
555
555
  }
556
556
  }
557
557
  return true;
558
558
  }
559
- return confirmingCreate && !looksLikeFieldCollection(text2);
559
+ return confirmingCreate && !looksLikeFieldCollection(text3);
560
560
  }
561
- function inferComposerForm(text2) {
562
- const cleaned = text2.trim();
561
+ function inferComposerForm(text3) {
562
+ const cleaned = text3.trim();
563
563
  if (!cleaned || looksLikeBookingCopy(cleaned) || looksLikeCompletedActionRecap(cleaned) || !looksLikeFieldCollection(cleaned)) {
564
564
  return null;
565
565
  }
@@ -574,8 +574,8 @@ function resolveComposerForm(input) {
574
574
  if (input.enabled === false || input.hasBookingOffer || input.hasPendingConfirmation) {
575
575
  return null;
576
576
  }
577
- const text2 = input.agentText.trim();
578
- if (!text2) return null;
577
+ const text3 = input.agentText.trim();
578
+ if (!text3) return null;
579
579
  const card = input.cards?.find((item) => item.type === "visitor_form");
580
580
  if (card?.fields && card.fields.length >= MIN_FORM_FIELDS) {
581
581
  return {
@@ -583,7 +583,7 @@ function resolveComposerForm(input) {
583
583
  fields: card.fields.slice(0, MAX_FORM_FIELDS)
584
584
  };
585
585
  }
586
- return inferComposerForm(text2);
586
+ return inferComposerForm(text3);
587
587
  }
588
588
 
589
589
  // src/react/lib/tool-card.ts
@@ -597,9 +597,9 @@ function preferBookingOffer(current, next) {
597
597
  }
598
598
  return next;
599
599
  }
600
- function looksLikeBookingReady(text2) {
600
+ function looksLikeBookingReady(text3) {
601
601
  return /demo option|options are ready|pick a (date|time)|available (times|slots)|book(ing)? (card|the demo)|\bschedule\b/i.test(
602
- text2
602
+ text3
603
603
  );
604
604
  }
605
605
  function bookingOfferIdentityKey(offer) {
@@ -709,29 +709,29 @@ function bookingCardFromActionOutput(output) {
709
709
  const data = asRecord2(record2?.output) ?? asRecord2(record2?.data) ?? record2;
710
710
  return parseToolCard(data);
711
711
  }
712
- function ensureBookingOfferText(text2, offer) {
713
- if (!offer) return text2;
714
- if (extractToolCards(text2).some((card) => card.type === "booking_offer")) {
715
- return text2;
712
+ function ensureBookingOfferText(text3, offer) {
713
+ if (!offer) return text3;
714
+ if (extractToolCards(text3).some((card) => card.type === "booking_offer")) {
715
+ return text3;
716
716
  }
717
- const visible = stripToolCards(text2).trim() || text2.trim();
717
+ const visible = stripToolCards(text3).trim() || text3.trim();
718
718
  return `${visible}
719
719
 
720
720
  ${formatBookingOfferFence(offer)}`;
721
721
  }
722
- function hideToolCardFences(text2) {
723
- return text2.replace(/```(?:webless-tool-card|json)\s*[\s\S]*?```/gi, "").replace(/```(?:webless-tool-card|json)[\s\S]*$/i, "").replace(/\n{3,}/g, "\n\n").trim();
722
+ function hideToolCardFences(text3) {
723
+ return text3.replace(/```(?:webless-tool-card|json)\s*[\s\S]*?```/gi, "").replace(/```(?:webless-tool-card|json)[\s\S]*$/i, "").replace(/\n{3,}/g, "\n\n").trim();
724
724
  }
725
725
  var BOOKING_CARD_FALLBACK = "Pick a date and time that works for you.";
726
- function looksLikeBookingAvailabilityDump(text2) {
727
- const cleaned = text2.trim();
726
+ function looksLikeBookingAvailabilityDump(text3) {
727
+ const cleaned = text3.trim();
728
728
  if (!cleaned) return false;
729
729
  const isoCount = (cleaned.match(/\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/g) ?? []).length;
730
730
  const utcCount = (cleaned.match(/\bUTC\b/g) ?? []).length;
731
731
  return isoCount >= 2 || utcCount >= 2 || /api\.calendly\.com|calendly\.com\//i.test(cleaned) || /webless-tool-card/i.test(cleaned);
732
732
  }
733
- function sanitizeBookingOfferCopy(text2) {
734
- const cleaned = hideToolCardFences(text2);
733
+ function sanitizeBookingOfferCopy(text3) {
734
+ const cleaned = hideToolCardFences(text3);
735
735
  if (!cleaned || looksLikeBookingAvailabilityDump(cleaned)) {
736
736
  return BOOKING_CARD_FALLBACK;
737
737
  }
@@ -744,9 +744,9 @@ function visitorTimeZone() {
744
744
  return "UTC";
745
745
  }
746
746
  }
747
- function extractToolCards(text2) {
747
+ function extractToolCards(text3) {
748
748
  const cards = [];
749
- for (const match of text2.matchAll(FENCE_PATTERN)) {
749
+ for (const match of text3.matchAll(FENCE_PATTERN)) {
750
750
  try {
751
751
  const card = parseToolCard(JSON.parse(match[1] ?? ""));
752
752
  if (card) cards.push(card);
@@ -755,8 +755,8 @@ function extractToolCards(text2) {
755
755
  }
756
756
  return cards;
757
757
  }
758
- function stripToolCards(text2) {
759
- return text2.replace(FENCE_PATTERN, "").replace(/\n{3,}/g, "\n\n").trim();
758
+ function stripToolCards(text3) {
759
+ return text3.replace(FENCE_PATTERN, "").replace(/\n{3,}/g, "\n\n").trim();
760
760
  }
761
761
  function localDateKey(date) {
762
762
  if (Number.isNaN(date.getTime())) return "";
@@ -876,10 +876,10 @@ function safeText(value) {
876
876
  return value.trim().slice(0, MAX_TEXT_LENGTH);
877
877
  }
878
878
  function safeHref(value) {
879
- const text2 = safeText(value);
880
- if (!text2) return "";
879
+ const text3 = safeText(value);
880
+ if (!text3) return "";
881
881
  try {
882
- const url = new URL(text2);
882
+ const url = new URL(text3);
883
883
  return url.protocol === "https:" ? url.toString() : "";
884
884
  } catch {
885
885
  return "";
@@ -1212,17 +1212,372 @@ function legacyBookingEnvelope(output) {
1212
1212
  } : null;
1213
1213
  }
1214
1214
 
1215
+ // src/react/hooks/useAgentHandoff.ts
1216
+ import { ClientError } from "eve/client";
1217
+ import { useEffect, useRef, useState } from "react";
1218
+ function useAgentHandoff(options) {
1219
+ const [page, setPage] = useState(null);
1220
+ const [busy, setBusy] = useState(false);
1221
+ const [rejected, setRejected] = useState(false);
1222
+ const [error, setError] = useState(null);
1223
+ const callbacks = useRef(options);
1224
+ callbacks.current = options;
1225
+ const pending = useRef(null);
1226
+ const inFlight = useRef(null);
1227
+ const pageRef = useRef(null);
1228
+ const refreshRef = useRef(null);
1229
+ const generation = useRef(0);
1230
+ useEffect(() => {
1231
+ const token = ++generation.current;
1232
+ const controller = new AbortController();
1233
+ let timer;
1234
+ let reading = null;
1235
+ let cursor = 0;
1236
+ pageRef.current = null;
1237
+ pending.current = null;
1238
+ inFlight.current?.abort();
1239
+ inFlight.current = null;
1240
+ setRejected(false);
1241
+ setPage(null);
1242
+ setError(null);
1243
+ setBusy(false);
1244
+ if (!options.enabled || !options.sessionId) return;
1245
+ const active = () => !controller.signal.aborted && generation.current === token;
1246
+ const refresh = () => {
1247
+ reading ??= (async () => {
1248
+ do {
1249
+ const next = await options.client.handoff.read({
1250
+ after: cursor,
1251
+ signal: AbortSignal.any([
1252
+ controller.signal,
1253
+ AbortSignal.timeout(15e3)
1254
+ ])
1255
+ });
1256
+ if (!active()) return;
1257
+ callbacks.current.onEvents(next.events);
1258
+ if (next.status !== "ai") callbacks.current.onOwnership();
1259
+ cursor = next.cursor;
1260
+ pageRef.current = next;
1261
+ setPage(next);
1262
+ setRejected(false);
1263
+ if (!pending.current) setError(null);
1264
+ if (!next.hasMore) break;
1265
+ } while (active());
1266
+ })().finally(() => {
1267
+ reading = null;
1268
+ });
1269
+ return reading;
1270
+ };
1271
+ refreshRef.current = refresh;
1272
+ const poll = async () => {
1273
+ try {
1274
+ await refresh();
1275
+ } catch (cause) {
1276
+ if (active()) {
1277
+ const denied = cause instanceof ClientError && cause.status === 403;
1278
+ setRejected(denied);
1279
+ setError(
1280
+ denied ? "This preview has changed. Restart the preview chat to test the current version." : "Reconnecting to your conversation\u2026"
1281
+ );
1282
+ }
1283
+ } finally {
1284
+ if (active()) timer = setTimeout(() => void poll(), 1500);
1285
+ }
1286
+ };
1287
+ void poll();
1288
+ return () => {
1289
+ controller.abort();
1290
+ inFlight.current?.abort();
1291
+ clearTimeout(timer);
1292
+ refreshRef.current = null;
1293
+ };
1294
+ }, [options.client, options.enabled, options.sessionId]);
1295
+ async function execute(operation) {
1296
+ if (inFlight.current) return;
1297
+ const controller = new AbortController();
1298
+ inFlight.current = controller;
1299
+ const signal = AbortSignal.any([
1300
+ controller.signal,
1301
+ AbortSignal.timeout(15e3)
1302
+ ]);
1303
+ const token = generation.current;
1304
+ pending.current = operation;
1305
+ setBusy(true);
1306
+ setError(null);
1307
+ try {
1308
+ if (operation.type === "start")
1309
+ await options.client.handoff.start(operation.operationId, signal);
1310
+ else if (operation.type === "message") {
1311
+ const { type: _, ...command } = operation;
1312
+ await options.client.handoff.send(command, signal);
1313
+ } else {
1314
+ const { type: _, ...command } = operation;
1315
+ await options.client.handoff.returnToAI(command, signal);
1316
+ }
1317
+ if (token !== generation.current) return;
1318
+ pending.current = null;
1319
+ await refreshRef.current?.();
1320
+ } catch {
1321
+ if (token === generation.current)
1322
+ setError(
1323
+ "Could not confirm delivery. Retry to check the same request."
1324
+ );
1325
+ } finally {
1326
+ if (inFlight.current === controller) inFlight.current = null;
1327
+ if (token === generation.current) setBusy(false);
1328
+ }
1329
+ }
1330
+ const binding = () => {
1331
+ const current = pageRef.current;
1332
+ if (!current?.handoffId) throw new Error("No active human conversation.");
1333
+ return { handoffId: current.handoffId, epoch: current.epoch };
1334
+ };
1335
+ return {
1336
+ page,
1337
+ busy,
1338
+ error,
1339
+ hasPending: Boolean(pending.current),
1340
+ canReset: !busy && !pending.current && (rejected || page?.status === "ai" || page?.status === "failed" || !options.sessionId),
1341
+ blocksAI: Boolean(options.enabled && options.sessionId && !page) || busy || Boolean(pending.current) || Boolean(page && page.status !== "ai"),
1342
+ start: () => execute(
1343
+ pending.current ?? { type: "start", operationId: crypto.randomUUID() }
1344
+ ),
1345
+ send: (message) => {
1346
+ if (pending.current)
1347
+ throw new Error(
1348
+ "Retry the pending request before sending another message."
1349
+ );
1350
+ return execute({
1351
+ type: "message",
1352
+ operationId: crypto.randomUUID(),
1353
+ ...binding(),
1354
+ message
1355
+ });
1356
+ },
1357
+ returnToAI: () => execute(
1358
+ pending.current ?? {
1359
+ type: "return",
1360
+ operationId: crypto.randomUUID(),
1361
+ ...binding()
1362
+ }
1363
+ ),
1364
+ retry: async () => {
1365
+ if (pending.current) return execute(pending.current);
1366
+ try {
1367
+ await refreshRef.current?.();
1368
+ } catch {
1369
+ setError("Reconnecting to your conversation\u2026");
1370
+ }
1371
+ }
1372
+ };
1373
+ }
1374
+
1215
1375
  // src/react/hooks/useAgentChat.ts
1216
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
1376
+ import { useCallback, useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
1217
1377
 
1218
1378
  // src/runtime/client.ts
1219
1379
  import {
1220
1380
  Client,
1221
- ClientError as ClientError2
1381
+ ClientError as ClientError4
1222
1382
  } from "eve/client";
1223
1383
 
1384
+ // src/runtime/handoff.ts
1385
+ import { ClientError as ClientError3 } from "eve/client";
1386
+
1387
+ // src/runtime/generated/handoff-contract.ts
1388
+ import { z } from "zod";
1389
+ var id = z.string().min(1).max(200);
1390
+ var sequence = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
1391
+ var text2 = z.string().trim().min(1).max(16e3);
1392
+ var agentRuntimeHandoffVersion = "webless.ai/agent-runtime-handoff/v1";
1393
+ var agentRuntimeHandoffStatusSchema = z.enum([
1394
+ "ai",
1395
+ "requesting",
1396
+ "queued",
1397
+ "human",
1398
+ "resolved",
1399
+ "failed"
1400
+ ]);
1401
+ var agentRuntimeHandoffActorSchema = z.strictObject({
1402
+ id,
1403
+ name: z.string().trim().min(1).max(200)
1404
+ });
1405
+ var eventBase = {
1406
+ id,
1407
+ sequence: sequence.refine((value) => value > 0),
1408
+ handoffId: id,
1409
+ epoch: sequence.refine((value) => value > 0),
1410
+ createdAt: z.iso.datetime()
1411
+ };
1412
+ var agentRuntimeHandoffEventSchema = z.discriminatedUnion("type", [
1413
+ z.strictObject({
1414
+ ...eventBase,
1415
+ type: z.literal("status"),
1416
+ status: agentRuntimeHandoffStatusSchema,
1417
+ actor: agentRuntimeHandoffActorSchema.nullable()
1418
+ }),
1419
+ z.strictObject({
1420
+ ...eventBase,
1421
+ type: z.literal("visitor.message"),
1422
+ message: text2,
1423
+ operationId: id
1424
+ }),
1425
+ z.strictObject({
1426
+ ...eventBase,
1427
+ type: z.literal("human.message"),
1428
+ message: text2,
1429
+ actor: agentRuntimeHandoffActorSchema
1430
+ })
1431
+ ]);
1432
+ var agentRuntimeHandoffReadRequestSchema = z.strictObject({
1433
+ after: sequence.default(0)
1434
+ });
1435
+ var agentRuntimeHandoffPageSchema = z.strictObject({
1436
+ apiVersion: z.literal(agentRuntimeHandoffVersion),
1437
+ sessionId: id,
1438
+ enabled: z.boolean(),
1439
+ status: agentRuntimeHandoffStatusSchema,
1440
+ handoffId: id.nullable(),
1441
+ epoch: sequence,
1442
+ after: sequence,
1443
+ cursor: sequence,
1444
+ hasMore: z.boolean(),
1445
+ events: z.array(agentRuntimeHandoffEventSchema).max(100)
1446
+ }).superRefine((page, ctx) => {
1447
+ let cursor = page.after;
1448
+ let previousEpoch = 0;
1449
+ const statuses = /* @__PURE__ */ new Map();
1450
+ const ids = /* @__PURE__ */ new Set();
1451
+ const handoffs = /* @__PURE__ */ new Map();
1452
+ const terminalEpochs = /* @__PURE__ */ new Set();
1453
+ let currentStatus;
1454
+ for (const event of page.events) {
1455
+ if (event.epoch < previousEpoch)
1456
+ ctx.addIssue({
1457
+ code: "custom",
1458
+ message: "Handoff epochs cannot decrease."
1459
+ });
1460
+ previousEpoch = event.epoch;
1461
+ if (ids.has(event.id))
1462
+ ctx.addIssue({
1463
+ code: "custom",
1464
+ message: "Duplicate handoff event ID."
1465
+ });
1466
+ ids.add(event.id);
1467
+ const handoffId = handoffs.get(event.epoch);
1468
+ if (handoffId !== void 0 && handoffId !== event.handoffId || event.epoch === page.epoch && event.handoffId !== page.handoffId)
1469
+ ctx.addIssue({
1470
+ code: "custom",
1471
+ message: "Inconsistent handoff epoch identity."
1472
+ });
1473
+ handoffs.set(event.epoch, event.handoffId);
1474
+ if (terminalEpochs.has(event.epoch) && (event.type !== "status" || event.status !== "ai"))
1475
+ ctx.addIssue({
1476
+ code: "custom",
1477
+ message: "Handoff event follows resolution."
1478
+ });
1479
+ if (event.type === "status") {
1480
+ if (event.status === "human" && event.actor === null)
1481
+ ctx.addIssue({
1482
+ code: "custom",
1483
+ message: "Human ownership requires a representative."
1484
+ });
1485
+ const previous = statuses.get(event.epoch);
1486
+ const transitions = {
1487
+ ai: [],
1488
+ requesting: ["queued", "resolved", "failed"],
1489
+ queued: ["human", "resolved", "failed"],
1490
+ human: ["resolved", "failed"],
1491
+ resolved: ["ai"],
1492
+ failed: []
1493
+ };
1494
+ if (previous !== void 0 && !transitions[previous].includes(event.status))
1495
+ ctx.addIssue({
1496
+ code: "custom",
1497
+ message: "Invalid handoff ownership transition."
1498
+ });
1499
+ statuses.set(event.epoch, event.status);
1500
+ if (["resolved", "failed", "ai"].includes(event.status))
1501
+ terminalEpochs.add(event.epoch);
1502
+ if (event.epoch === page.epoch) currentStatus = event.status;
1503
+ }
1504
+ if (event.sequence !== cursor + 1) {
1505
+ ctx.addIssue({
1506
+ code: "custom",
1507
+ message: "Handoff event gap or replay."
1508
+ });
1509
+ }
1510
+ cursor = event.sequence;
1511
+ if (event.epoch > page.epoch) {
1512
+ ctx.addIssue({
1513
+ code: "custom",
1514
+ message: "Event exceeds current epoch."
1515
+ });
1516
+ }
1517
+ }
1518
+ if (!page.hasMore && currentStatus !== void 0 && currentStatus !== page.status)
1519
+ ctx.addIssue({
1520
+ code: "custom",
1521
+ message: "Handoff ownership contradicts its final status event."
1522
+ });
1523
+ if (page.cursor !== cursor || page.hasMore && page.events.length === 0) {
1524
+ ctx.addIssue({ code: "custom", message: "Invalid handoff page cursor." });
1525
+ }
1526
+ if (page.epoch === 0 !== (page.handoffId === null)) {
1527
+ ctx.addIssue({ code: "custom", message: "Invalid handoff identity." });
1528
+ }
1529
+ if (page.handoffId === null && page.status !== "ai") {
1530
+ ctx.addIssue({
1531
+ code: "custom",
1532
+ message: "Handoff identity is required."
1533
+ });
1534
+ }
1535
+ });
1536
+ var agentRuntimeHandoffStartSchema = z.strictObject({
1537
+ operationId: id
1538
+ });
1539
+ var agentRuntimeHandoffCommandSchema = z.strictObject({
1540
+ operationId: id,
1541
+ handoffId: id,
1542
+ epoch: sequence.refine((value) => value > 0)
1543
+ });
1544
+ var agentRuntimeHandoffMessageSchema = agentRuntimeHandoffCommandSchema.extend({ message: text2 });
1545
+ var agentRuntimeHandoffBindingSchema = z.strictObject({
1546
+ tenantId: id,
1547
+ indexId: id,
1548
+ visitorSubject: id,
1549
+ sessionId: id,
1550
+ handoffId: id,
1551
+ epoch: sequence.refine((value) => value > 0)
1552
+ });
1553
+ var providerEventBase = {
1554
+ apiVersion: z.literal(agentRuntimeHandoffVersion),
1555
+ binding: agentRuntimeHandoffBindingSchema,
1556
+ eventId: id,
1557
+ sequence: sequence.refine((value) => value > 0)
1558
+ };
1559
+ var agentRuntimeHandoffProviderEventSchema = z.discriminatedUnion(
1560
+ "type",
1561
+ [
1562
+ z.strictObject({ ...providerEventBase, type: z.literal("queued") }),
1563
+ z.strictObject({
1564
+ ...providerEventBase,
1565
+ type: z.literal("assigned"),
1566
+ actor: agentRuntimeHandoffActorSchema
1567
+ }),
1568
+ z.strictObject({
1569
+ ...providerEventBase,
1570
+ type: z.literal("human.message"),
1571
+ actor: agentRuntimeHandoffActorSchema,
1572
+ message: text2
1573
+ }),
1574
+ z.strictObject({ ...providerEventBase, type: z.literal("resolved") }),
1575
+ z.strictObject({ ...providerEventBase, type: z.literal("failed") })
1576
+ ]
1577
+ );
1578
+
1224
1579
  // src/runtime/capability.ts
1225
- import { ClientError } from "eve/client";
1580
+ import { ClientError as ClientError2 } from "eve/client";
1226
1581
  var MAX_REFRESH_SKEW_MS = 3e4;
1227
1582
  var LOCAL_LOOPBACK_ORIGINS = [
1228
1583
  "http://127.0.0.1:3010",
@@ -1367,7 +1722,7 @@ async function withCapabilityRefresh(capability, request) {
1367
1722
  try {
1368
1723
  return await request();
1369
1724
  } catch (error) {
1370
- if (!(error instanceof ClientError) || error.status !== 401) {
1725
+ if (!(error instanceof ClientError2) || error.status !== 401) {
1371
1726
  throw error;
1372
1727
  }
1373
1728
  capability.invalidate();
@@ -1375,6 +1730,75 @@ async function withCapabilityRefresh(capability, request) {
1375
1730
  }
1376
1731
  }
1377
1732
 
1733
+ // src/runtime/handoff.ts
1734
+ function createHandoffClient(options) {
1735
+ const target = () => {
1736
+ const sessionId = options.getSessionId();
1737
+ if (!sessionId)
1738
+ throw new Error(
1739
+ "Start a conversation before requesting a representative."
1740
+ );
1741
+ return {
1742
+ sessionId,
1743
+ path: `/webless/v1/session/${encodeURIComponent(sessionId)}/handoff`
1744
+ };
1745
+ };
1746
+ const request = (path, init) => withCapabilityRefresh(options.capability, async () => {
1747
+ const response = await options.getClient().fetch(path, {
1748
+ ...init,
1749
+ cache: "no-store",
1750
+ redirect: "error"
1751
+ });
1752
+ if (!response.ok) {
1753
+ throw new ClientError3(
1754
+ response.status,
1755
+ await response.text(),
1756
+ response.headers
1757
+ );
1758
+ }
1759
+ return response;
1760
+ });
1761
+ const command = async (suffix, body, signal) => {
1762
+ const { path } = target();
1763
+ await request(path + suffix, {
1764
+ method: "POST",
1765
+ headers: { "content-type": "application/json" },
1766
+ body: JSON.stringify(body),
1767
+ signal
1768
+ });
1769
+ };
1770
+ return {
1771
+ async read(input = {}) {
1772
+ const { after } = agentRuntimeHandoffReadRequestSchema.parse({
1773
+ after: input.after
1774
+ });
1775
+ const { sessionId, path } = target();
1776
+ const response = await request(`${path}?after=${after}`, {
1777
+ signal: input.signal
1778
+ });
1779
+ const value = await response.json();
1780
+ const page = agentRuntimeHandoffPageSchema.parse(value);
1781
+ if (page.sessionId !== sessionId || page.after !== after) {
1782
+ throw new Error(
1783
+ "The handoff response does not belong to this conversation or cursor."
1784
+ );
1785
+ }
1786
+ return page;
1787
+ },
1788
+ start: (operationId, signal) => command(
1789
+ "",
1790
+ agentRuntimeHandoffStartSchema.parse({ operationId }),
1791
+ signal
1792
+ ),
1793
+ send: (input, signal) => command(
1794
+ "/messages",
1795
+ agentRuntimeHandoffMessageSchema.parse(input),
1796
+ signal
1797
+ ),
1798
+ returnToAI: (input, signal) => command("/return", agentRuntimeHandoffCommandSchema.parse(input), signal)
1799
+ };
1800
+ }
1801
+
1378
1802
  // src/runtime/config.ts
1379
1803
  var DEFAULT_RUNTIME_ORIGIN = "https://runtime.staging.webless.ai";
1380
1804
  var trimTrailingSlash = (value) => value.replace(/\/+$/, "");
@@ -1965,6 +2389,11 @@ var AgentSession = class {
1965
2389
  version,
1966
2390
  visitorSessionId
1967
2391
  });
2392
+ this.handoff = createHandoffClient({
2393
+ getClient: () => this.ensureClient(),
2394
+ getSessionId: () => this.getActiveSessionId(),
2395
+ capability: this.capability
2396
+ });
1968
2397
  }
1969
2398
  indexId;
1970
2399
  version;
@@ -1977,6 +2406,7 @@ var AgentSession = class {
1977
2406
  activeResponse;
1978
2407
  childStreams;
1979
2408
  capability;
2409
+ handoff;
1980
2410
  getActiveSessionId() {
1981
2411
  return this.session?.state.sessionId ?? loadPersistedAgentSession(this.visitorSessionId, this.storeOptions)?.sessionId;
1982
2412
  }
@@ -2087,7 +2517,7 @@ var AgentSession = class {
2087
2517
  () => activeSession.send(message, { signal })
2088
2518
  );
2089
2519
  } catch (error) {
2090
- if (error instanceof ClientError2 && error.status === 409 && error.code === "session_not_active") {
2520
+ if (error instanceof ClientError4 && error.status === 409 && error.code === "session_not_active") {
2091
2521
  clearPersistedAgentSession(this.visitorSessionId, this.storeOptions);
2092
2522
  this.session = void 0;
2093
2523
  session = void 0;
@@ -2261,10 +2691,10 @@ var AgentSession = class {
2261
2691
  }
2262
2692
  this.session = session;
2263
2693
  const inputResponses = responses.map(
2264
- ({ requestId, optionId, text: text2 }) => ({
2694
+ ({ requestId, optionId, text: text3 }) => ({
2265
2695
  requestId,
2266
2696
  ...optionId ? { optionId } : {},
2267
- ...text2 ? { text: text2 } : {}
2697
+ ...text3 ? { text: text3 } : {}
2268
2698
  })
2269
2699
  );
2270
2700
  const response = await withCapabilityRefresh(
@@ -2354,6 +2784,7 @@ function createAgentClient(options) {
2354
2784
  );
2355
2785
  return {
2356
2786
  indexId,
2787
+ handoff: session.handoff,
2357
2788
  version,
2358
2789
  runtimeOrigin,
2359
2790
  visitorSessionId,
@@ -2385,7 +2816,7 @@ function createAgentClient(options) {
2385
2816
  }
2386
2817
 
2387
2818
  // src/runtime/errors.ts
2388
- import { ClientError as ClientError3 } from "eve/client";
2819
+ import { ClientError as ClientError5 } from "eve/client";
2389
2820
  var TRANSIENT_AGENT_ERROR_MESSAGE = "The agent run stopped before the action finished. Please try again.";
2390
2821
  var PREVIEW_AUTHORIZATION_ERROR_MESSAGE = "This preview could not be authorized. Open the latest preview from Webless.";
2391
2822
  function isTransientRuntimeMessage(message) {
@@ -2397,7 +2828,7 @@ function isPreviewAuthorizationMessage(message) {
2397
2828
  return normalized.includes("unpublished preview authorization") || normalized.includes("unpublished preview grant") || normalized.includes("agent studio preview grant") || normalized.includes("preview authorization");
2398
2829
  }
2399
2830
  function formatAgentError(error) {
2400
- if (error instanceof ClientError3) {
2831
+ if (error instanceof ClientError5) {
2401
2832
  if (error.status === 401 && error.code === "index_required") {
2402
2833
  return "Missing indexId \u2014 pass a published index id to createAgentClient().";
2403
2834
  }
@@ -2435,9 +2866,21 @@ function conversationKey(storageKeyPrefix, visitorSessionId) {
2435
2866
  function parseMessage(value) {
2436
2867
  if (typeof value !== "object" || value === null) return null;
2437
2868
  const record2 = value;
2438
- if (typeof record2.id !== "string" || record2.role !== "agent" && record2.role !== "visitor" || typeof record2.text !== "string" || typeof record2.createdAt !== "number" || !Number.isFinite(record2.createdAt)) {
2869
+ if (typeof record2.id !== "string" || record2.role !== "agent" && record2.role !== "visitor" && record2.role !== "human" || typeof record2.text !== "string" || typeof record2.createdAt !== "number" || !Number.isFinite(record2.createdAt)) {
2439
2870
  return null;
2440
2871
  }
2872
+ if (record2.role === "human") {
2873
+ const person = record2.representative;
2874
+ if (typeof person !== "object" || person === null || !("id" in person) || !("name" in person) || typeof person.id !== "string" || typeof person.name !== "string")
2875
+ return null;
2876
+ return {
2877
+ id: record2.id,
2878
+ role: "human",
2879
+ text: record2.text,
2880
+ createdAt: record2.createdAt,
2881
+ representative: { id: person.id, name: person.name }
2882
+ };
2883
+ }
2441
2884
  if (record2.role === "visitor") {
2442
2885
  return {
2443
2886
  id: record2.id,
@@ -2751,15 +3194,15 @@ function appendChatCollectiblePrompts(messages, requests) {
2751
3194
  }
2752
3195
  return next;
2753
3196
  }
2754
- function chatInputResponseForText(requests, text2) {
2755
- const trimmed = text2.trim();
3197
+ function chatInputResponseForText(requests, text3) {
3198
+ const trimmed = text3.trim();
2756
3199
  if (!trimmed) return null;
2757
3200
  const pending = requests.find(isChatCollectibleInputRequest);
2758
3201
  if (!pending) return null;
2759
3202
  return { requestId: pending.requestId, text: trimmed };
2760
3203
  }
2761
- function normalizeAssistantDedupeKey(text2) {
2762
- return text2.trim().replace(/\s+/g, " ").toLowerCase();
3204
+ function normalizeAssistantDedupeKey(text3) {
3205
+ return text3.trim().replace(/\s+/g, " ").toLowerCase();
2763
3206
  }
2764
3207
  function isNearDuplicateAssistantText(left, right) {
2765
3208
  const a = normalizeAssistantDedupeKey(left);
@@ -2872,6 +3315,7 @@ function isJsonRecord(value) {
2872
3315
  return value !== null && typeof value === "object" && !Array.isArray(value);
2873
3316
  }
2874
3317
  function useAgentChat({
3318
+ handoffEnabled = false,
2875
3319
  customerId,
2876
3320
  getUnpublishedPreviewGrant,
2877
3321
  indexId,
@@ -2887,9 +3331,9 @@ function useAgentChat({
2887
3331
  () => createInitialState(greeting?.trim() || DEFAULT_GREETING),
2888
3332
  [greeting]
2889
3333
  );
2890
- const previewGrantProviderRef = useRef(getUnpublishedPreviewGrant);
3334
+ const previewGrantProviderRef = useRef2(getUnpublishedPreviewGrant);
2891
3335
  previewGrantProviderRef.current = getUnpublishedPreviewGrant;
2892
- const toolResultRegistryRef = useRef(toolResultRegistry);
3336
+ const toolResultRegistryRef = useRef2(toolResultRegistry);
2893
3337
  toolResultRegistryRef.current = toolResultRegistry;
2894
3338
  const resolveUnpublishedPreviewGrant = () => {
2895
3339
  const provider = previewGrantProviderRef.current;
@@ -2917,17 +3361,17 @@ function useAgentChat({
2917
3361
  }),
2918
3362
  [resolvedStorageKeyPrefix, visitorSessionId]
2919
3363
  );
2920
- const [state, setState] = useState(
3364
+ const [state, setState] = useState2(
2921
3365
  () => stateFromConversation(
2922
3366
  loadPersistedAgentConversation(resolvedStorageKeyPrefix, visitorId),
2923
3367
  initialState
2924
3368
  )
2925
3369
  );
2926
- const pendingBookingRef = useRef(
3370
+ const pendingBookingRef = useRef2(
2927
3371
  loadPendingWidgetBooking(resolvedStorageKeyPrefix, visitorId)
2928
3372
  );
2929
- const runRef = useRef(null);
2930
- const clientRef = useRef(
3373
+ const runRef = useRef2(null);
3374
+ const clientRef = useRef2(
2931
3375
  createAgentClient({
2932
3376
  locationConsent: true,
2933
3377
  customerId,
@@ -2941,8 +3385,8 @@ function useAgentChat({
2941
3385
  })
2942
3386
  );
2943
3387
  const identityKey = `${customerId ?? ""}|${indexId}|${version ?? "published"}|${runtimeOrigin ?? ""}|${previewBuildId ?? ""}|${resolvedStorageKeyPrefix}|${visitorId}|${greeting ?? ""}`;
2944
- const identityRef = useRef(identityKey);
2945
- useEffect(() => {
3388
+ const identityRef = useRef2(identityKey);
3389
+ useEffect2(() => {
2946
3390
  if (identityRef.current === identityKey) {
2947
3391
  return;
2948
3392
  }
@@ -2981,7 +3425,7 @@ function useAgentChat({
2981
3425
  version,
2982
3426
  visitorId
2983
3427
  ]);
2984
- useEffect(() => {
3428
+ useEffect2(() => {
2985
3429
  if (!hasVisitorMessages(state.messages)) return;
2986
3430
  savePersistedAgentConversation(resolvedStorageKeyPrefix, visitorId, {
2987
3431
  messages: state.messages,
@@ -3001,16 +3445,77 @@ function useAgentChat({
3001
3445
  state.toolResults,
3002
3446
  visitorId
3003
3447
  ]);
3448
+ const handoff = useAgentHandoff({
3449
+ enabled: handoffEnabled,
3450
+ client: clientRef.current,
3451
+ sessionId: clientRef.current.getActiveSessionId(),
3452
+ onOwnership: () => {
3453
+ runRef.current?.abort();
3454
+ runRef.current = null;
3455
+ setState(
3456
+ (prev) => prev.phase === "complete" && !prev.streamingText && !prev.pendingInputs?.length && !prev.toolSteps.length ? prev : {
3457
+ ...prev,
3458
+ phase: "complete",
3459
+ streamingText: "",
3460
+ pendingInputs: [],
3461
+ toolSteps: [],
3462
+ toolResults: [],
3463
+ pendingOffer: null,
3464
+ error: null
3465
+ }
3466
+ );
3467
+ },
3468
+ onEvents: (events) => {
3469
+ setState((prev) => {
3470
+ const seen = new Set(prev.messages.map((message) => message.id));
3471
+ const messages = [];
3472
+ for (const event of events) {
3473
+ if (event.type === "status" || seen.has(`handoff-${event.id}`))
3474
+ continue;
3475
+ messages.push(
3476
+ event.type === "human.message" ? {
3477
+ id: `handoff-${event.id}`,
3478
+ role: "human",
3479
+ text: event.message,
3480
+ createdAt: Date.parse(event.createdAt),
3481
+ representative: event.actor
3482
+ } : {
3483
+ id: `handoff-${event.id}`,
3484
+ role: "visitor",
3485
+ text: event.message,
3486
+ createdAt: Date.parse(event.createdAt)
3487
+ }
3488
+ );
3489
+ }
3490
+ return messages.length ? {
3491
+ ...prev,
3492
+ messages: [...prev.messages, ...messages].sort(
3493
+ (a, b) => a.createdAt - b.createdAt
3494
+ )
3495
+ } : prev;
3496
+ });
3497
+ }
3498
+ });
3499
+ const handoffBlocksAI = useRef2(handoff.blocksAI);
3500
+ handoffBlocksAI.current = handoff.blocksAI;
3004
3501
  const reset = useCallback(() => {
3502
+ if (handoff.blocksAI && !handoff.canReset) return;
3005
3503
  runRef.current?.abort();
3006
3504
  runRef.current = null;
3007
3505
  clientRef.current.reset();
3008
3506
  pendingBookingRef.current = null;
3009
3507
  clearPersistedAgentConversation(resolvedStorageKeyPrefix, visitorId);
3010
3508
  setState(initialState);
3011
- }, [initialState, resolvedStorageKeyPrefix, visitorId]);
3509
+ }, [
3510
+ handoff.blocksAI,
3511
+ handoff.canReset,
3512
+ initialState,
3513
+ resolvedStorageKeyPrefix,
3514
+ visitorId
3515
+ ]);
3012
3516
  const runTurn = useCallback(
3013
3517
  async (input) => {
3518
+ if (handoffBlocksAI.current) return null;
3014
3519
  const {
3015
3520
  controller,
3016
3521
  initialText = "",
@@ -3134,6 +3639,7 @@ function useAgentChat({
3134
3639
  signal
3135
3640
  });
3136
3641
  if (resume && finalText === null) {
3642
+ if (!isActiveRun() || handoffBlocksAI.current) return null;
3137
3643
  finalText = await clientRef.current.sendTurn(visitorText, {
3138
3644
  handlers,
3139
3645
  signal
@@ -3166,8 +3672,12 @@ function useAgentChat({
3166
3672
  messages: appendAgentTurnMessage(
3167
3673
  prev.messages,
3168
3674
  displayText,
3169
- (prev.toolResults ?? []).filter((result) => result.kind === "search"),
3170
- (prev.toolResults ?? []).filter((result) => result.kind !== "search" && result.kind !== "input")
3675
+ (prev.toolResults ?? []).filter(
3676
+ (result) => result.kind === "search"
3677
+ ),
3678
+ (prev.toolResults ?? []).filter(
3679
+ (result) => result.kind !== "search" && result.kind !== "input"
3680
+ )
3171
3681
  ),
3172
3682
  toolResults: (prev.toolResults ?? []).filter(
3173
3683
  (result) => result.kind === "input"
@@ -3235,6 +3745,15 @@ function useAgentChat({
3235
3745
  const outgoing = options?.runtimeText ?? visitorText;
3236
3746
  if (!outgoing.trim()) return null;
3237
3747
  const declinedComposerFormId = options?.declinedComposerFormId?.trim();
3748
+ if (handoff.blocksAI) {
3749
+ if (!["requesting", "queued", "human"].includes(
3750
+ handoff.page?.status ?? ""
3751
+ ) || handoff.busy)
3752
+ return null;
3753
+ if (!trimmed) return null;
3754
+ await handoff.send(trimmed);
3755
+ return null;
3756
+ }
3238
3757
  const chatResponse = chatInputResponseForText(
3239
3758
  state.pendingInputs ?? [],
3240
3759
  outgoing.trim()
@@ -3336,9 +3855,10 @@ ${outgoing}` : outgoing;
3336
3855
  visitorText: runtimeText
3337
3856
  });
3338
3857
  },
3339
- [runTurn, state.pendingInputs]
3858
+ [handoff, runTurn, state.pendingInputs]
3340
3859
  );
3341
3860
  const retry = useCallback(async () => {
3861
+ if (handoffBlocksAI.current) return;
3342
3862
  const visitorMessage = [...state.messages].reverse().find((message) => message.role === "visitor");
3343
3863
  if (!visitorMessage) return;
3344
3864
  if (runRef.current) {
@@ -3372,6 +3892,7 @@ ${outgoing}` : outgoing;
3372
3892
  });
3373
3893
  }, [runTurn, state.messages]);
3374
3894
  const regenerate = useCallback(async () => {
3895
+ if (handoffBlocksAI.current) return null;
3375
3896
  let lastVisitorIndex = -1;
3376
3897
  for (let index = state.messages.length - 1; index >= 0; index -= 1) {
3377
3898
  if (state.messages[index]?.role === "visitor") {
@@ -3429,7 +3950,7 @@ ${outgoing}` : outgoing;
3429
3950
  );
3430
3951
  const respondToInput = useCallback(
3431
3952
  async (response) => {
3432
- if (runRef.current) return;
3953
+ if (handoffBlocksAI.current || runRef.current) return;
3433
3954
  const pending = state.pendingInputs?.find(
3434
3955
  (request) => request.requestId === response.requestId
3435
3956
  );
@@ -3457,7 +3978,8 @@ ${outgoing}` : outgoing;
3457
3978
  },
3458
3979
  [respondToToolInput, runTurn, state.pendingInputs]
3459
3980
  );
3460
- useEffect(() => {
3981
+ useEffect2(() => {
3982
+ if (handoff.blocksAI) return;
3461
3983
  const conversation = loadPersistedAgentConversation(
3462
3984
  resolvedStorageKeyPrefix,
3463
3985
  visitorId
@@ -3479,14 +4001,21 @@ ${outgoing}` : outgoing;
3479
4001
  }
3480
4002
  controller.abort();
3481
4003
  };
3482
- }, [identityKey, resolvedStorageKeyPrefix, runTurn, visitorId]);
3483
- useEffect(() => {
4004
+ }, [
4005
+ handoff.blocksAI,
4006
+ identityKey,
4007
+ resolvedStorageKeyPrefix,
4008
+ runTurn,
4009
+ visitorId
4010
+ ]);
4011
+ useEffect2(() => {
3484
4012
  return () => {
3485
4013
  runRef.current?.abort();
3486
4014
  runRef.current = null;
3487
4015
  };
3488
4016
  }, []);
3489
4017
  return {
4018
+ handoff,
3490
4019
  state,
3491
4020
  reset,
3492
4021
  retry,
@@ -3597,12 +4126,12 @@ var defaultDarkAgentRailTheme = {
3597
4126
  };
3598
4127
 
3599
4128
  // src/react/components/MessageActions/MessageActions.tsx
3600
- import { useEffect as useEffect2, useLayoutEffect, useRef as useRef2, useState as useState2 } from "react";
4129
+ import { useEffect as useEffect3, useLayoutEffect, useRef as useRef3, useState as useState3 } from "react";
3601
4130
  import { createPortal } from "react-dom";
3602
4131
 
3603
4132
  // src/react/lib/speech.ts
3604
- function toSpeechText(text2) {
3605
- let out = hideToolCardFences(text2);
4133
+ function toSpeechText(text3) {
4134
+ let out = hideToolCardFences(text3);
3606
4135
  out = out.replace(/```[\s\S]*?```/g, " ");
3607
4136
  out = out.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
3608
4137
  out = out.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
@@ -3947,12 +4476,12 @@ function StopIcon() {
3947
4476
  }
3948
4477
  ) });
3949
4478
  }
3950
- async function writeToClipboard(text2) {
4479
+ async function writeToClipboard(text3) {
3951
4480
  try {
3952
- await navigator.clipboard.writeText(text2);
4481
+ await navigator.clipboard.writeText(text3);
3953
4482
  } catch {
3954
4483
  const textarea = document.createElement("textarea");
3955
- textarea.value = text2;
4484
+ textarea.value = text3;
3956
4485
  textarea.style.position = "fixed";
3957
4486
  textarea.style.opacity = "0";
3958
4487
  document.body.appendChild(textarea);
@@ -4003,16 +4532,16 @@ function MessageActions({
4003
4532
  speechText
4004
4533
  }) {
4005
4534
  const menuPortalRoot = useAgentRailMenuPortalRoot();
4006
- const [copied, setCopied] = useState2(false);
4007
- const [rating, setRating] = useState2(null);
4008
- const [menuOpen, setMenuOpen] = useState2(false);
4009
- const [menuPosition, setMenuPosition] = useState2(null);
4010
- const [speaking, setSpeaking] = useState2(false);
4011
- const [speechSupported, setSpeechSupported] = useState2(null);
4012
- const copyTimerRef = useRef2(null);
4013
- const menuRef = useRef2(null);
4014
- const portaledMenuRef = useRef2(null);
4015
- const moreButtonRef = useRef2(null);
4535
+ const [copied, setCopied] = useState3(false);
4536
+ const [rating, setRating] = useState3(null);
4537
+ const [menuOpen, setMenuOpen] = useState3(false);
4538
+ const [menuPosition, setMenuPosition] = useState3(null);
4539
+ const [speaking, setSpeaking] = useState3(false);
4540
+ const [speechSupported, setSpeechSupported] = useState3(null);
4541
+ const copyTimerRef = useRef3(null);
4542
+ const menuRef = useRef3(null);
4543
+ const portaledMenuRef = useRef3(null);
4544
+ const moreButtonRef = useRef3(null);
4016
4545
  const answeredLabel = formatAnsweredAt(answeredAt ?? 0);
4017
4546
  const resolvedSpeechText = speechText ?? toSpeechText(copyText);
4018
4547
  const canOpenReceipt = Boolean(receiptSteps) && Boolean(onOpenReceipt);
@@ -4039,7 +4568,7 @@ function MessageActions({
4039
4568
  })
4040
4569
  );
4041
4570
  }, [menuOpen, menuPortalRoot]);
4042
- useEffect2(() => {
4571
+ useEffect3(() => {
4043
4572
  const unsubscribe = subscribeSpeechInterrupts(() => setSpeaking(false));
4044
4573
  return () => {
4045
4574
  unsubscribe();
@@ -4049,7 +4578,7 @@ function MessageActions({
4049
4578
  stopSpeech();
4050
4579
  };
4051
4580
  }, []);
4052
- useEffect2(() => {
4581
+ useEffect3(() => {
4053
4582
  if (!menuOpen) return;
4054
4583
  const handlePointerDown = (event) => {
4055
4584
  const target = event.target;
@@ -4218,7 +4747,7 @@ function MessageActions({
4218
4747
  }
4219
4748
 
4220
4749
  // src/react/components/AgentRail/AgentRail.tsx
4221
- import { useEffect as useEffect7, useRef as useRef7, useState as useState10 } from "react";
4750
+ import { useEffect as useEffect8, useRef as useRef8, useState as useState11 } from "react";
4222
4751
 
4223
4752
  // src/react/lib/agent-theme.ts
4224
4753
  function agentThemeStyle(theme, resolvedColorScheme) {
@@ -4293,7 +4822,7 @@ function resolveAgentColorScheme(colorScheme = "auto", prefersDarkMode) {
4293
4822
  }
4294
4823
 
4295
4824
  // src/react/components/AgentActivityBubble/AgentActivityBubble.tsx
4296
- import { useState as useState3 } from "react";
4825
+ import { useState as useState4 } from "react";
4297
4826
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
4298
4827
  function joinLabels(labels) {
4299
4828
  if (labels.length <= 1) return labels[0] ?? "";
@@ -4412,7 +4941,7 @@ function AgentActivityBubble({
4412
4941
  const statusText = workSummary(steps, failed, brandLabel);
4413
4942
  const softReview = statusText === AGENT_SOFT_REVIEW_STATUS;
4414
4943
  const receiptId = steps.map((step) => step.id).join(":");
4415
- const [expandedReceiptId, setExpandedReceiptId] = useState3(
4944
+ const [expandedReceiptId, setExpandedReceiptId] = useState4(
4416
4945
  null
4417
4946
  );
4418
4947
  const detailsOpen = !active && expandedReceiptId === receiptId;
@@ -4463,7 +4992,7 @@ function AgentActivityBubble({
4463
4992
  }
4464
4993
 
4465
4994
  // src/react/components/AnswerReceiptDialog/AnswerReceiptDialog.tsx
4466
- import { useEffect as useEffect3, useRef as useRef3 } from "react";
4995
+ import { useEffect as useEffect4, useRef as useRef4 } from "react";
4467
4996
  import { createPortal as createPortal2 } from "react-dom";
4468
4997
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
4469
4998
  var FOCUSABLE_SELECTOR = 'button:not(:disabled), a[href], textarea:not(:disabled), input:not(:disabled), [tabindex]:not([tabindex="-1"])';
@@ -4485,10 +5014,10 @@ function AnswerReceiptDialog({
4485
5014
  }) {
4486
5015
  const portalRoots = useAgentRailPortalRoots();
4487
5016
  const overlayRoot = portalRoots?.overlayRef ?? null;
4488
- const cardRef = useRef3(null);
4489
- const closeButtonRef = useRef3(null);
4490
- const previouslyFocusedRef = useRef3(null);
4491
- useEffect3(() => {
5017
+ const cardRef = useRef4(null);
5018
+ const closeButtonRef = useRef4(null);
5019
+ const previouslyFocusedRef = useRef4(null);
5020
+ useEffect4(() => {
4492
5021
  previouslyFocusedRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
4493
5022
  closeButtonRef.current?.focus({ preventScroll: true });
4494
5023
  const handleKeyDown = (event) => {
@@ -4574,10 +5103,10 @@ function AnswerReceiptDialog({
4574
5103
 
4575
5104
  // src/react/components/Composer/Composer.tsx
4576
5105
  import {
4577
- useEffect as useEffect4,
5106
+ useEffect as useEffect5,
4578
5107
  useId,
4579
- useRef as useRef4,
4580
- useState as useState4
5108
+ useRef as useRef5,
5109
+ useState as useState5
4581
5110
  } from "react";
4582
5111
  import { Fragment, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
4583
5112
  var FORM_SUBMITTED_MESSAGE = "Shared my details";
@@ -4640,17 +5169,17 @@ function Composer({
4640
5169
  allowFormResume = true,
4641
5170
  onSubmit
4642
5171
  }) {
4643
- const [value, setValue] = useState4("");
4644
- const [draft, setDraft] = useState4(() => createDraft(form));
4645
- const inputRef = useRef4(null);
4646
- const firstFieldRef = useRef4(null);
4647
- const formRef = useRef4(null);
5172
+ const [value, setValue] = useState5("");
5173
+ const [draft, setDraft] = useState5(() => createDraft(form));
5174
+ const inputRef = useRef5(null);
5175
+ const firstFieldRef = useRef5(null);
5176
+ const formRef = useRef5(null);
4648
5177
  const formId = useId();
4649
5178
  if (form && form.id !== draft.form?.id) setDraft(createDraft(form));
4650
5179
  const savedForm = allowFormResume && !draft.submitted ? draft.form : null;
4651
5180
  const activeForm = !disabled && draft.expanded ? savedForm : null;
4652
5181
  const canSend = !disabled && Boolean(value.trim() || activeForm);
4653
- useEffect4(() => {
5182
+ useEffect5(() => {
4654
5183
  if (activeForm) firstFieldRef.current?.focus();
4655
5184
  else if ((savedForm || draft.submitted) && !disabled)
4656
5185
  inputRef.current?.focus();
@@ -4911,7 +5440,7 @@ function FollowUpChips({
4911
5440
  }
4912
5441
 
4913
5442
  // src/react/components/MessageBubble/MessageBubble.tsx
4914
- import { useState as useState6 } from "react";
5443
+ import { useState as useState7 } from "react";
4915
5444
 
4916
5445
  // src/react/components/SearchReferences/SearchReferences.tsx
4917
5446
  import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
@@ -5003,11 +5532,11 @@ function SearchReferences({
5003
5532
 
5004
5533
  // src/react/components/BookingCard/BookingCard.tsx
5005
5534
  import {
5006
- useEffect as useEffect5,
5535
+ useEffect as useEffect6,
5007
5536
  useId as useId2,
5008
5537
  useMemo as useMemo2,
5009
- useRef as useRef5,
5010
- useState as useState5
5538
+ useRef as useRef6,
5539
+ useState as useState6
5011
5540
  } from "react";
5012
5541
  import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
5013
5542
  var BOOKING_STEPS = [
@@ -5065,16 +5594,16 @@ function InteractiveBookingCard({
5065
5594
  }) {
5066
5595
  const fieldId = useId2();
5067
5596
  const defaultType = offer.eventTypes[0]?.uri ?? offer.slots[0]?.eventTypeUri ?? "";
5068
- const [step, setStep] = useState5("date");
5069
- const [eventTypeUri, setEventTypeUri] = useState5(defaultType);
5070
- const [selectedDate, setSelectedDate] = useState5("");
5071
- const [startTime, setStartTime] = useState5("");
5072
- const [name, setName] = useState5("");
5073
- const [email, setEmail] = useState5("");
5074
- const activeStepRef = useRef5(null);
5075
- const previousStepRef = useRef5(step);
5597
+ const [step, setStep] = useState6("date");
5598
+ const [eventTypeUri, setEventTypeUri] = useState6(defaultType);
5599
+ const [selectedDate, setSelectedDate] = useState6("");
5600
+ const [startTime, setStartTime] = useState6("");
5601
+ const [name, setName] = useState6("");
5602
+ const [email, setEmail] = useState6("");
5603
+ const activeStepRef = useRef6(null);
5604
+ const previousStepRef = useRef6(step);
5076
5605
  const stepIndex = BOOKING_STEPS.findIndex((item) => item.id === step);
5077
- useEffect5(() => {
5606
+ useEffect6(() => {
5078
5607
  if (previousStepRef.current === step) return;
5079
5608
  previousStepRef.current = step;
5080
5609
  activeStepRef.current?.scrollIntoView({ block: "nearest" });
@@ -5091,7 +5620,7 @@ function InteractiveBookingCard({
5091
5620
  }
5092
5621
  return next;
5093
5622
  }, [slots]);
5094
- const [visibleMonth, setVisibleMonth] = useState5(
5623
+ const [visibleMonth, setVisibleMonth] = useState6(
5095
5624
  () => firstAvailableBookingMonth(slots)
5096
5625
  );
5097
5626
  function selectEventType(nextType) {
@@ -5399,8 +5928,8 @@ function resolveMessageUrl(safeUrl, baseUrl) {
5399
5928
  // src/react/components/MessageBubble/MessageBubble.tsx
5400
5929
  import "streamdown/styles.css";
5401
5930
  import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
5402
- function normalizeDedupeText(text2) {
5403
- return text2.trim().replace(/\s+/g, " ").toLowerCase();
5931
+ function normalizeDedupeText(text3) {
5932
+ return text3.trim().replace(/\s+/g, " ").toLowerCase();
5404
5933
  }
5405
5934
  function paragraphsAreNearDuplicates(first, second) {
5406
5935
  const left = normalizeDedupeText(first);
@@ -5416,8 +5945,8 @@ function paragraphsShareOpening(first, second) {
5416
5945
  if (!opening || opening.length < 20) return false;
5417
5946
  return second.trim().startsWith(opening);
5418
5947
  }
5419
- function collapseRepeatedText(text2) {
5420
- const trimmed = text2.trim();
5948
+ function collapseRepeatedText(text3) {
5949
+ const trimmed = text3.trim();
5421
5950
  if (trimmed.length < 40) return trimmed;
5422
5951
  const paragraphs = trimmed.split(/\n{2,}/u).map((part) => part.trim()).filter(Boolean);
5423
5952
  if (paragraphs.length === 2 && (paragraphsAreNearDuplicates(paragraphs[0], paragraphs[1]) || paragraphsShareOpening(paragraphs[0], paragraphs[1]))) {
@@ -5438,13 +5967,14 @@ function MessageBubble({
5438
5967
  message,
5439
5968
  brandLogoUrl,
5440
5969
  bookingDisabled = false,
5970
+ showProvenance = false,
5441
5971
  bookingReadOnly = false,
5442
5972
  linkBaseUrl,
5443
5973
  offer,
5444
5974
  onBook
5445
5975
  }) {
5446
5976
  const resolvedLogoUrl = brandLogoUrl?.trim();
5447
- const [failedLogoUrl, setFailedLogoUrl] = useState6(null);
5977
+ const [failedLogoUrl, setFailedLogoUrl] = useState7(null);
5448
5978
  const showBrandLogo = Boolean(resolvedLogoUrl) && failedLogoUrl !== resolvedLogoUrl;
5449
5979
  const cards = message.role === "agent" ? extractToolCards(message.text) : [];
5450
5980
  const extractedOffers = cards.filter(
@@ -5459,7 +5989,34 @@ function MessageBubble({
5459
5989
  if (message.role === "visitor") {
5460
5990
  const visitorText = visitorMessageDisplayText(message).trim();
5461
5991
  if (!visitorText) return null;
5462
- return /* @__PURE__ */ jsx8("article", { className: "message-bubble message-bubble--visitor", children: /* @__PURE__ */ jsx8("p", { className: "message-bubble__text", children: visitorText }) });
5992
+ return /* @__PURE__ */ jsxs8(
5993
+ "article",
5994
+ {
5995
+ className: "message-bubble message-bubble--visitor",
5996
+ "aria-label": "Message from visitor",
5997
+ children: [
5998
+ showProvenance ? /* @__PURE__ */ jsx8("span", { className: "message-bubble__speaker", children: "You \xB7 Visitor" }) : null,
5999
+ /* @__PURE__ */ jsx8("p", { className: "message-bubble__text", children: visitorText })
6000
+ ]
6001
+ }
6002
+ );
6003
+ }
6004
+ if (message.role === "human") {
6005
+ return /* @__PURE__ */ jsxs8(
6006
+ "article",
6007
+ {
6008
+ className: "message-bubble message-bubble--human",
6009
+ "aria-label": `Message from ${message.representative.name}`,
6010
+ children: [
6011
+ /* @__PURE__ */ jsxs8("strong", { className: "message-bubble__representative", children: [
6012
+ message.representative.name,
6013
+ " ",
6014
+ /* @__PURE__ */ jsx8("span", { children: "Human representative" })
6015
+ ] }),
6016
+ /* @__PURE__ */ jsx8("p", { className: "message-bubble__text", children: message.text })
6017
+ ]
6018
+ }
6019
+ );
5463
6020
  }
5464
6021
  const citations = message.citations ?? [];
5465
6022
  const agentText = /* @__PURE__ */ jsxs8("div", { className: "message-bubble__text", children: [
@@ -5496,35 +6053,43 @@ function MessageBubble({
5496
6053
  ] });
5497
6054
  if (!displayText && !message.searchResults?.length && offers.length === 0)
5498
6055
  return null;
5499
- return /* @__PURE__ */ jsxs8("article", { className: "message-bubble message-bubble--agent", children: [
5500
- displayText || message.searchResults?.length ? showBrandLogo ? /* @__PURE__ */ jsxs8("div", { className: "message-bubble__agent-row", children: [
5501
- /* @__PURE__ */ jsx8("span", { className: "message-bubble__agent-avatar", "aria-hidden": "true", children: /* @__PURE__ */ jsx8(
5502
- "img",
5503
- {
5504
- src: resolvedLogoUrl,
5505
- alt: "",
5506
- onError: () => {
5507
- setFailedLogoUrl(resolvedLogoUrl ?? null);
5508
- }
5509
- }
5510
- ) }),
5511
- agentText
5512
- ] }) : agentText : null,
5513
- offers.map((nextOffer, index) => /* @__PURE__ */ jsx8(
5514
- BookingCard,
5515
- {
5516
- disabled: bookingDisabled,
5517
- readOnly: bookingReadOnly,
5518
- offer: nextOffer,
5519
- onBook
5520
- },
5521
- `${bookingOfferIdentityKey(nextOffer)}-${index}`
5522
- ))
5523
- ] });
6056
+ return /* @__PURE__ */ jsxs8(
6057
+ "article",
6058
+ {
6059
+ className: "message-bubble message-bubble--agent",
6060
+ "aria-label": "Message from AI assistant",
6061
+ children: [
6062
+ showProvenance ? /* @__PURE__ */ jsx8("span", { className: "message-bubble__speaker", children: "AI assistant" }) : null,
6063
+ displayText || message.searchResults?.length ? showBrandLogo ? /* @__PURE__ */ jsxs8("div", { className: "message-bubble__agent-row", children: [
6064
+ /* @__PURE__ */ jsx8("span", { className: "message-bubble__agent-avatar", "aria-hidden": "true", children: /* @__PURE__ */ jsx8(
6065
+ "img",
6066
+ {
6067
+ src: resolvedLogoUrl,
6068
+ alt: "",
6069
+ onError: () => {
6070
+ setFailedLogoUrl(resolvedLogoUrl ?? null);
6071
+ }
6072
+ }
6073
+ ) }),
6074
+ agentText
6075
+ ] }) : agentText : null,
6076
+ offers.map((nextOffer, index) => /* @__PURE__ */ jsx8(
6077
+ BookingCard,
6078
+ {
6079
+ disabled: bookingDisabled,
6080
+ readOnly: bookingReadOnly,
6081
+ offer: nextOffer,
6082
+ onBook
6083
+ },
6084
+ `${bookingOfferIdentityKey(nextOffer)}-${index}`
6085
+ ))
6086
+ ]
6087
+ }
6088
+ );
5524
6089
  }
5525
6090
 
5526
6091
  // src/react/components/HumanInputCard/HumanInputCard.tsx
5527
- import { useState as useState8 } from "react";
6092
+ import { useState as useState9 } from "react";
5528
6093
 
5529
6094
  // src/react/components/ConfirmationCard/ConfirmationCard.tsx
5530
6095
  import { jsx as jsx9, jsxs as jsxs9 } from "react/jsx-runtime";
@@ -5566,7 +6131,7 @@ function ConfirmationCard({
5566
6131
  }
5567
6132
 
5568
6133
  // src/react/components/ToolInputCard/ToolInputCard.tsx
5569
- import { useState as useState7 } from "react";
6134
+ import { useState as useState8 } from "react";
5570
6135
  import { jsx as jsx10, jsxs as jsxs10 } from "react/jsx-runtime";
5571
6136
  function isRecord5(value) {
5572
6137
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -5689,13 +6254,13 @@ function valuesMatch(left, right) {
5689
6254
  }
5690
6255
  function FieldDescription({
5691
6256
  field,
5692
- id
6257
+ id: id2
5693
6258
  }) {
5694
- return field.description ? /* @__PURE__ */ jsx10("span", { id, className: "tool-input-card__description", children: field.description }) : null;
6259
+ return field.description ? /* @__PURE__ */ jsx10("span", { id: id2, className: "tool-input-card__description", children: field.description }) : null;
5695
6260
  }
5696
6261
  function ChoiceField({
5697
6262
  field,
5698
- id,
6263
+ id: id2,
5699
6264
  value,
5700
6265
  disabled,
5701
6266
  describedBy,
@@ -5713,7 +6278,7 @@ function ChoiceField({
5713
6278
  return /* @__PURE__ */ jsxs10(
5714
6279
  "select",
5715
6280
  {
5716
- id,
6281
+ id: id2,
5717
6282
  value: selectedIndex >= 0 ? String(selectedIndex) : "",
5718
6283
  disabled,
5719
6284
  required: field.required,
@@ -5736,7 +6301,7 @@ function ChoiceField({
5736
6301
  {
5737
6302
  className: "tool-input-card__choices",
5738
6303
  role: multiple ? "group" : "radiogroup",
5739
- "aria-labelledby": `${id}-label`,
6304
+ "aria-labelledby": `${id2}-label`,
5740
6305
  "aria-describedby": describedBy,
5741
6306
  "aria-invalid": Boolean(error),
5742
6307
  "aria-required": field.required,
@@ -5747,7 +6312,7 @@ function ChoiceField({
5747
6312
  "input",
5748
6313
  {
5749
6314
  type: multiple ? "checkbox" : "radio",
5750
- name: id,
6315
+ name: id2,
5751
6316
  value: String(index),
5752
6317
  checked,
5753
6318
  disabled,
@@ -5775,22 +6340,22 @@ function ToolField({
5775
6340
  disabled,
5776
6341
  error,
5777
6342
  field,
5778
- id,
6343
+ id: id2,
5779
6344
  value,
5780
6345
  onBlur,
5781
6346
  onChange
5782
6347
  }) {
5783
6348
  const describedBy = [
5784
- field.description ? `${id}-description` : "",
5785
- error ? `${id}-error` : ""
6349
+ field.description ? `${id2}-description` : "",
6350
+ error ? `${id2}-error` : ""
5786
6351
  ].filter(Boolean).join(" ");
5787
6352
  if (field.kind === "checkbox" || field.kind === "confirmation") {
5788
6353
  return /* @__PURE__ */ jsxs10("div", { className: "tool-input-card__field", children: [
5789
- /* @__PURE__ */ jsxs10("label", { className: "tool-input-card__check", htmlFor: id, children: [
6354
+ /* @__PURE__ */ jsxs10("label", { className: "tool-input-card__check", htmlFor: id2, children: [
5790
6355
  /* @__PURE__ */ jsx10(
5791
6356
  "input",
5792
6357
  {
5793
- id,
6358
+ id: id2,
5794
6359
  type: "checkbox",
5795
6360
  checked: value === true,
5796
6361
  disabled,
@@ -5802,18 +6367,18 @@ function ToolField({
5802
6367
  ),
5803
6368
  /* @__PURE__ */ jsxs10("span", { children: [
5804
6369
  /* @__PURE__ */ jsx10("strong", { children: field.label }),
5805
- field.description ? /* @__PURE__ */ jsx10("span", { id: `${id}-description`, children: field.description }) : null
6370
+ field.description ? /* @__PURE__ */ jsx10("span", { id: `${id2}-description`, children: field.description }) : null
5806
6371
  ] })
5807
6372
  ] }),
5808
- error ? /* @__PURE__ */ jsx10("span", { id: `${id}-error`, className: "tool-input-card__error", children: error }) : null
6373
+ error ? /* @__PURE__ */ jsx10("span", { id: `${id2}-error`, className: "tool-input-card__error", children: error }) : null
5809
6374
  ] });
5810
6375
  }
5811
- const label = /* @__PURE__ */ jsxs10("label", { id: `${id}-label`, htmlFor: id, children: [
6376
+ const label = /* @__PURE__ */ jsxs10("label", { id: `${id2}-label`, htmlFor: id2, children: [
5812
6377
  field.label,
5813
6378
  field.required ? /* @__PURE__ */ jsx10("span", { "aria-hidden": "true", children: " *" }) : null
5814
6379
  ] });
5815
6380
  const common = {
5816
- id,
6381
+ id: id2,
5817
6382
  disabled,
5818
6383
  required: field.required,
5819
6384
  "aria-describedby": describedBy || void 0,
@@ -5826,7 +6391,7 @@ function ToolField({
5826
6391
  ChoiceField,
5827
6392
  {
5828
6393
  field,
5829
- id,
6394
+ id: id2,
5830
6395
  value,
5831
6396
  disabled,
5832
6397
  describedBy: describedBy || void 0,
@@ -5863,7 +6428,7 @@ function ToolField({
5863
6428
  onChange: (event) => onChange(Number(event.target.value))
5864
6429
  }
5865
6430
  ),
5866
- /* @__PURE__ */ jsx10("output", { htmlFor: id, children: numericValue })
6431
+ /* @__PURE__ */ jsx10("output", { htmlFor: id2, children: numericValue })
5867
6432
  ] });
5868
6433
  } else {
5869
6434
  const type = field.kind === "date-time" ? "datetime-local" : field.kind === "calendar" ? "date" : field.kind;
@@ -5887,9 +6452,9 @@ function ToolField({
5887
6452
  }
5888
6453
  return /* @__PURE__ */ jsxs10("div", { className: "tool-input-card__field", children: [
5889
6454
  label,
5890
- /* @__PURE__ */ jsx10(FieldDescription, { field, id: `${id}-description` }),
6455
+ /* @__PURE__ */ jsx10(FieldDescription, { field, id: `${id2}-description` }),
5891
6456
  control,
5892
- error ? /* @__PURE__ */ jsx10("span", { id: `${id}-error`, className: "tool-input-card__error", children: error }) : null
6457
+ error ? /* @__PURE__ */ jsx10("span", { id: `${id2}-error`, className: "tool-input-card__error", children: error }) : null
5893
6458
  ] });
5894
6459
  }
5895
6460
  function ToolInputCard({
@@ -5897,11 +6462,11 @@ function ToolInputCard({
5897
6462
  surface,
5898
6463
  onSubmit
5899
6464
  }) {
5900
- const [values, setValues] = useState7(
6465
+ const [values, setValues] = useState8(
5901
6466
  () => initialValues(surface)
5902
6467
  );
5903
- const [touched, setTouched] = useState7(() => /* @__PURE__ */ new Set());
5904
- const [submitted, setSubmitted] = useState7(false);
6468
+ const [touched, setTouched] = useState8(() => /* @__PURE__ */ new Set());
6469
+ const [submitted, setSubmitted] = useState8(false);
5905
6470
  const errors = Object.fromEntries(
5906
6471
  surface.fields.map((field) => [
5907
6472
  field.path,
@@ -5994,7 +6559,7 @@ function HumanInputCard({
5994
6559
  request,
5995
6560
  onRespond
5996
6561
  }) {
5997
- const [text2, setText] = useState8("");
6562
+ const [text3, setText] = useState9("");
5998
6563
  const options = request.options ?? [];
5999
6564
  const showText = request.display === "text" || request.allowFreeform && options.length === 0;
6000
6565
  if (request.ui) {
@@ -6019,7 +6584,7 @@ function HumanInputCard({
6019
6584
  }
6020
6585
  function submitText(event) {
6021
6586
  event.preventDefault();
6022
- const value = text2.trim();
6587
+ const value = text3.trim();
6023
6588
  if (!value || disabled) return;
6024
6589
  onRespond?.({ requestId: request.requestId, text: value });
6025
6590
  }
@@ -6037,12 +6602,12 @@ function HumanInputCard({
6037
6602
  "input",
6038
6603
  {
6039
6604
  id: `human-input-text-${request.requestId}`,
6040
- value: text2,
6605
+ value: text3,
6041
6606
  disabled,
6042
6607
  onChange: (event) => setText(event.target.value)
6043
6608
  }
6044
6609
  ),
6045
- /* @__PURE__ */ jsx11("button", { type: "submit", disabled: disabled || !text2.trim(), children: "Send" })
6610
+ /* @__PURE__ */ jsx11("button", { type: "submit", disabled: disabled || !text3.trim(), children: "Send" })
6046
6611
  ] })
6047
6612
  ] }) : null,
6048
6613
  !showText && options.length === 0 ? /* @__PURE__ */ jsx11("p", { className: "human-input-card__unavailable", role: "status", children: "This request can\u2019t be answered here." }) : null
@@ -6052,7 +6617,7 @@ function HumanInputCard({
6052
6617
  }
6053
6618
 
6054
6619
  // src/react/components/LocationConsent/LocationConsent.tsx
6055
- import { useEffect as useEffect6, useRef as useRef6, useState as useState9 } from "react";
6620
+ import { useEffect as useEffect7, useRef as useRef7, useState as useState10 } from "react";
6056
6621
 
6057
6622
  // src/runtime/location-consent.ts
6058
6623
  function isLocationConsentRequest(request) {
@@ -6100,16 +6665,16 @@ function LocationConsent({
6100
6665
  request,
6101
6666
  onRespond
6102
6667
  }) {
6103
- const [preference, setPreference] = useState9("unset");
6104
- const [locating, setLocating] = useState9(false);
6105
- const [error, setError] = useState9(null);
6106
- const respond = useRef6(onRespond);
6107
- const answered = useRef6(null);
6668
+ const [preference, setPreference] = useState10("unset");
6669
+ const [locating, setLocating] = useState10(false);
6670
+ const [error, setError] = useState10(null);
6671
+ const respond = useRef7(onRespond);
6672
+ const answered = useRef7(null);
6108
6673
  const requestId = request?.requestId;
6109
- useEffect6(() => {
6674
+ useEffect7(() => {
6110
6675
  respond.current = onRespond;
6111
6676
  }, [onRespond]);
6112
- useEffect6(() => {
6677
+ useEffect7(() => {
6113
6678
  if (!requestId || preference === "unset" || answered.current === requestId || !respond.current)
6114
6679
  return;
6115
6680
  if (preference === "off") {
@@ -6451,6 +7016,7 @@ function ChevronDownIcon() {
6451
7016
  var DEFAULT_DISCLAIMER_LABEL = "Agent can make mistakes. Check important info.";
6452
7017
  function AgentRail({
6453
7018
  state,
7019
+ handoff,
6454
7020
  theme,
6455
7021
  colorScheme = "auto",
6456
7022
  brandLabel = "",
@@ -6476,44 +7042,51 @@ function AgentRail({
6476
7042
  onInputResponse,
6477
7043
  onToolInput
6478
7044
  }) {
6479
- const railRef = useRef7(null);
6480
- const overlayRef = useRef7(null);
6481
- const transcriptRef = useRef7(null);
6482
- const responseRef = useRef7(null);
6483
- const threadRef = useRef7(null);
6484
- const lastScrolledVisitorIdRef = useRef7(void 0);
6485
- const pinnedToBottomRef = useRef7(true);
6486
- const smoothScrollToLatestRef = useRef7(false);
6487
- const lockedTranscriptScrollTopRef = useRef7(null);
6488
- const [showJumpToLatest, setShowJumpToLatest] = useState10(false);
6489
- const [receiptOpen, setReceiptOpen] = useState10(false);
7045
+ const railRef = useRef8(null);
7046
+ const overlayRef = useRef8(null);
7047
+ const transcriptRef = useRef8(null);
7048
+ const responseRef = useRef8(null);
7049
+ const threadRef = useRef8(null);
7050
+ const lastScrolledVisitorIdRef = useRef8(void 0);
7051
+ const pinnedToBottomRef = useRef8(true);
7052
+ const smoothScrollToLatestRef = useRef8(false);
7053
+ const lockedTranscriptScrollTopRef = useRef8(null);
7054
+ const [showJumpToLatest, setShowJumpToLatest] = useState11(false);
7055
+ const [receiptOpen, setReceiptOpen] = useState11(false);
6490
7056
  const resolvedBrandLabel = brandLabel.trim();
6491
7057
  const resolvedBrandLogoUrl = brandLogoUrl?.trim();
6492
7058
  const resolvedDisclaimerLabel = disclaimerLabel === void 0 ? DEFAULT_DISCLAIMER_LABEL : disclaimerLabel;
6493
- const [failedLogoUrl, setFailedLogoUrl] = useState10(null);
7059
+ const [failedLogoUrl, setFailedLogoUrl] = useState11(null);
6494
7060
  const showBrandLogo = Boolean(resolvedBrandLogoUrl) && failedLogoUrl !== resolvedBrandLogoUrl;
6495
7061
  const resolvedColorScheme = useAgentColorScheme(colorScheme);
6496
7062
  const railStyle = agentThemeStyle(theme, resolvedColorScheme);
6497
- const locationRequest = state.pendingInputs?.find(isLocationConsentRequest);
7063
+ const handoffActive = Boolean(handoff?.blocksAI);
7064
+ const handoffStatus = handoff?.page?.status;
7065
+ const locationRequest = handoffActive ? void 0 : state.pendingInputs?.find(isLocationConsentRequest);
6498
7066
  const pendingInputRequests = (state.pendingInputs ?? []).filter(
6499
- (request) => !isLocationConsentRequest(request) && shouldRenderVisitorInputCard(request) && (!state.pendingOffer || request.kind === "tool-approval")
7067
+ (request) => !handoffActive && !isLocationConsentRequest(request) && shouldRenderVisitorInputCard(request) && (!state.pendingOffer || request.kind === "tool-approval")
6500
7068
  );
6501
7069
  const isBusy = state.phase === "thinking" || state.phase === "running-tools" || state.phase === "streaming" || state.phase === "waiting-input" && (pendingInputRequests.length > 0 || Boolean(locationRequest));
6502
- const semanticSurfaceDisabled = isBusy && state.phase !== "waiting-input";
7070
+ const semanticSurfaceDisabled = handoffActive || isBusy && state.phase !== "waiting-input";
6503
7071
  const visitorToolResults = (state.toolResults ?? []).filter(
6504
7072
  isRenderableVisitorToolResult
6505
7073
  );
6506
7074
  const activeVisitorToolInput = [...visitorToolResults].reverse().find((result) => result.kind === "input");
6507
7075
  const visibleVisitorToolResults = activeVisitorToolInput ? [activeVisitorToolInput] : visitorToolResults;
6508
- const activityActive = state.toolSteps.some((step) => step.state === "active");
7076
+ const activityActive = state.toolSteps.some(
7077
+ (step) => step.state === "active"
7078
+ );
6509
7079
  const showActivity = state.toolSteps.length > 0 && pendingInputRequests.length === 0 && !state.pendingOffer && (activityActive || isBusy && !state.streamingText);
6510
- const activitySteps = activityActive || !isBusy || state.streamingText ? state.toolSteps : [...state.toolSteps, {
6511
- id: "preparing-answer",
6512
- kind: "tool",
6513
- label: "Preparing your answer",
6514
- detail: "Preparing your answer",
6515
- state: "active"
6516
- }];
7080
+ const activitySteps = activityActive || !isBusy || state.streamingText ? state.toolSteps : [
7081
+ ...state.toolSteps,
7082
+ {
7083
+ id: "preparing-answer",
7084
+ kind: "tool",
7085
+ label: "Preparing your answer",
7086
+ detail: "Preparing your answer",
7087
+ state: "active"
7088
+ }
7089
+ ];
6517
7090
  const hasVisitorMessages2 = state.messages.some(
6518
7091
  (message) => message.role === "visitor"
6519
7092
  );
@@ -6542,7 +7115,7 @@ function AgentRail({
6542
7115
  }
6543
7116
  }
6544
7117
  const lastIsAgent = lastMessage?.role === "agent";
6545
- const showMessageActions = state.phase === "complete" && lastIsAgent && hasVisitorMessages2 && Boolean(hideToolCardFences(lastMessage.text).trim());
7118
+ const showMessageActions = !handoffActive && state.phase === "complete" && lastIsAgent && hasVisitorMessages2 && Boolean(hideToolCardFences(lastMessage.text).trim());
6546
7119
  const streamingMessage = state.streamingText && !lastIsAgent ? {
6547
7120
  createdAt: 0,
6548
7121
  id: "streaming-response",
@@ -6584,7 +7157,7 @@ function AgentRail({
6584
7157
  ...visibleVisitorToolResults
6585
7158
  ].reverse().find((result) => result.kind !== "input")?.id;
6586
7159
  const receiptSteps = answerReceipt && state.toolSteps.length > 0 ? state.toolSteps : void 0;
6587
- useEffect7(() => {
7160
+ useEffect8(() => {
6588
7161
  if (state.phase !== "complete") {
6589
7162
  setReceiptOpen(false);
6590
7163
  }
@@ -6606,7 +7179,7 @@ function AgentRail({
6606
7179
  onFollowUpSelect?.(label);
6607
7180
  }
6608
7181
  const latestVisitorId = visibleMessages[lastVisitorIndex]?.id;
6609
- useEffect7(() => {
7182
+ useEffect8(() => {
6610
7183
  const node = transcriptRef.current;
6611
7184
  if (!node) return;
6612
7185
  if (latestVisitorId !== lastScrolledVisitorIdRef.current) {
@@ -6636,7 +7209,7 @@ function AgentRail({
6636
7209
  state.followUps,
6637
7210
  state.journey
6638
7211
  ]);
6639
- useEffect7(() => {
7212
+ useEffect8(() => {
6640
7213
  const node = transcriptRef.current;
6641
7214
  if (!node) return;
6642
7215
  const handleScroll = () => {
@@ -6650,7 +7223,7 @@ function AgentRail({
6650
7223
  handleScroll();
6651
7224
  return () => node.removeEventListener("scroll", handleScroll);
6652
7225
  }, []);
6653
- useEffect7(() => {
7226
+ useEffect8(() => {
6654
7227
  if (!receiptOpen) {
6655
7228
  lockedTranscriptScrollTopRef.current = null;
6656
7229
  return;
@@ -6702,249 +7275,302 @@ function AgentRail({
6702
7275
  autoFocus: mobileFullscreen || expanded,
6703
7276
  role: mobileFullscreen || expanded ? "dialog" : void 0,
6704
7277
  tabIndex: mobileFullscreen || expanded ? -1 : void 0,
6705
- children: /* @__PURE__ */ jsxs17(
6706
- AgentRailOverlayContext.Provider,
6707
- {
6708
- value: { railRef, overlayRef },
6709
- children: [
6710
- /* @__PURE__ */ jsxs17("div", { className: "agent-rail__surface", inert: receiptOpen || void 0, children: [
6711
- /* @__PURE__ */ jsx18("header", { className: "agent-rail__header", children: /* @__PURE__ */ jsxs17("div", { className: "agent-rail__brand-row", children: [
6712
- onCollapse ? /* @__PURE__ */ jsx18(
6713
- "button",
6714
- {
6715
- type: "button",
6716
- className: "agent-rail__collapse",
6717
- "aria-label": "Collapse assist",
6718
- onClick: onCollapse,
6719
- children: /* @__PURE__ */ jsx18(MinimizeIcon, {})
6720
- }
6721
- ) : onClose ? /* @__PURE__ */ jsx18(
6722
- "button",
6723
- {
6724
- type: "button",
6725
- className: "agent-rail__close",
6726
- "aria-label": "Close agent",
6727
- onClick: onClose,
6728
- children: /* @__PURE__ */ jsx18(CloseIcon2, {})
7278
+ children: /* @__PURE__ */ jsxs17(AgentRailOverlayContext.Provider, { value: { railRef, overlayRef }, children: [
7279
+ /* @__PURE__ */ jsxs17("div", { className: "agent-rail__surface", inert: receiptOpen || void 0, children: [
7280
+ /* @__PURE__ */ jsx18("header", { className: "agent-rail__header", children: /* @__PURE__ */ jsxs17("div", { className: "agent-rail__brand-row", children: [
7281
+ onCollapse ? /* @__PURE__ */ jsx18(
7282
+ "button",
7283
+ {
7284
+ type: "button",
7285
+ className: "agent-rail__collapse",
7286
+ "aria-label": "Collapse assist",
7287
+ onClick: onCollapse,
7288
+ children: /* @__PURE__ */ jsx18(MinimizeIcon, {})
7289
+ }
7290
+ ) : onClose ? /* @__PURE__ */ jsx18(
7291
+ "button",
7292
+ {
7293
+ type: "button",
7294
+ className: "agent-rail__close",
7295
+ "aria-label": "Close agent",
7296
+ onClick: onClose,
7297
+ children: /* @__PURE__ */ jsx18(CloseIcon2, {})
7298
+ }
7299
+ ) : /* @__PURE__ */ jsx18("span", { className: "agent-rail__brand-spacer", "aria-hidden": "true" }),
7300
+ resolvedBrandLabel || showBrandLogo ? /* @__PURE__ */ jsxs17("span", { className: "agent-rail__identity", children: [
7301
+ showBrandLogo ? /* @__PURE__ */ jsx18("span", { className: "agent-rail__brand-mark", "aria-hidden": "true", children: /* @__PURE__ */ jsx18(
7302
+ "img",
7303
+ {
7304
+ className: "agent-rail__brand-logo",
7305
+ src: resolvedBrandLogoUrl,
7306
+ alt: "",
7307
+ onError: () => {
7308
+ setFailedLogoUrl(resolvedBrandLogoUrl ?? null);
6729
7309
  }
6730
- ) : /* @__PURE__ */ jsx18("span", { className: "agent-rail__brand-spacer", "aria-hidden": "true" }),
6731
- resolvedBrandLabel || showBrandLogo ? /* @__PURE__ */ jsxs17("span", { className: "agent-rail__identity", children: [
6732
- showBrandLogo ? /* @__PURE__ */ jsx18("span", { className: "agent-rail__brand-mark", "aria-hidden": "true", children: /* @__PURE__ */ jsx18(
6733
- "img",
6734
- {
6735
- className: "agent-rail__brand-logo",
6736
- src: resolvedBrandLogoUrl,
6737
- alt: "",
6738
- onError: () => {
6739
- setFailedLogoUrl(resolvedBrandLogoUrl ?? null);
6740
- }
6741
- }
6742
- ) }) : null,
6743
- resolvedBrandLabel ? /* @__PURE__ */ jsx18("span", { className: "agent-rail__brand-label", children: resolvedBrandLabel }) : null
6744
- ] }) : null,
6745
- /* @__PURE__ */ jsxs17("span", { className: "agent-rail__actions", children: [
6746
- onReset ? /* @__PURE__ */ jsx18(
6747
- "button",
6748
- {
6749
- type: "button",
6750
- className: "agent-rail__new-chat",
6751
- "aria-label": "Start a new conversation",
6752
- disabled: !hasVisitorMessages2,
6753
- onClick: handleReset,
6754
- children: /* @__PURE__ */ jsx18(NewChatIcon, {})
6755
- }
6756
- ) : null,
6757
- onExpandToggle ? /* @__PURE__ */ jsx18(
6758
- "button",
7310
+ }
7311
+ ) }) : null,
7312
+ resolvedBrandLabel ? /* @__PURE__ */ jsx18("span", { className: "agent-rail__brand-label", children: resolvedBrandLabel }) : null
7313
+ ] }) : null,
7314
+ /* @__PURE__ */ jsxs17("span", { className: "agent-rail__actions", children: [
7315
+ onReset ? /* @__PURE__ */ jsx18(
7316
+ "button",
7317
+ {
7318
+ type: "button",
7319
+ className: "agent-rail__new-chat",
7320
+ "aria-label": "Start a new conversation",
7321
+ disabled: !hasVisitorMessages2 || handoffActive && !handoff?.canReset,
7322
+ onClick: handleReset,
7323
+ children: /* @__PURE__ */ jsx18(NewChatIcon, {})
7324
+ }
7325
+ ) : null,
7326
+ onExpandToggle ? /* @__PURE__ */ jsx18(
7327
+ "button",
7328
+ {
7329
+ type: "button",
7330
+ className: "agent-rail__expand",
7331
+ "aria-label": expanded ? "Exit full screen" : "Open full screen",
7332
+ onClick: onExpandToggle,
7333
+ children: expanded ? /* @__PURE__ */ jsx18(RestoreIcon, {}) : /* @__PURE__ */ jsx18(ExpandIcon, {})
7334
+ }
7335
+ ) : null
7336
+ ] })
7337
+ ] }) }),
7338
+ /* @__PURE__ */ jsx18("div", { ref: transcriptRef, className: "agent-rail__transcript", children: /* @__PURE__ */ jsxs17("div", { ref: threadRef, className: "agent-rail__thread", children: [
7339
+ !hasVisitorMessages2 ? /* @__PURE__ */ jsxs17("section", { className: "agent-rail__welcome", "aria-label": "Welcome", children: [
7340
+ greeting?.role === "agent" ? /* @__PURE__ */ jsx18(
7341
+ MessageBubble,
7342
+ {
7343
+ showProvenance: Boolean(
7344
+ handoff?.page?.enabled || handoffActive
7345
+ ),
7346
+ message: greeting,
7347
+ bookingDisabled: isBusy || handoffActive,
7348
+ brandLogoUrl: showBrandLogo ? resolvedBrandLogoUrl : void 0,
7349
+ onBook
7350
+ }
7351
+ ) : null,
7352
+ showIdleFollowUps ? /* @__PURE__ */ jsx18("div", { className: "agent-rail__followups-slot", children: /* @__PURE__ */ jsx18(
7353
+ FollowUpChips,
7354
+ {
7355
+ suggestions: state.followUps,
7356
+ disabled: isBusy || handoffActive,
7357
+ label: "Start here",
7358
+ onSelect: (suggestion) => handleFollowUpSelect(suggestion.label)
7359
+ }
7360
+ ) }) : null,
7361
+ showActivity ? /* @__PURE__ */ jsx18(
7362
+ AgentActivityBubble,
7363
+ {
7364
+ brandLabel: resolvedBrandLabel,
7365
+ brandLogoUrl: showBrandLogo ? resolvedBrandLogoUrl : void 0,
7366
+ failed: state.phase === "error",
7367
+ steps: activitySteps
7368
+ }
7369
+ ) : null,
7370
+ visibleVisitorToolResults.map((result) => /* @__PURE__ */ jsx18(
7371
+ VisitorToolResultView,
7372
+ {
7373
+ result,
7374
+ disabled: semanticSurfaceDisabled,
7375
+ onToolInput
7376
+ },
7377
+ result.id
7378
+ )),
7379
+ pendingInputRequests.slice(0, 1).map((request) => /* @__PURE__ */ jsx18(
7380
+ HumanInputCard,
7381
+ {
7382
+ request,
7383
+ onRespond: onInputResponse
7384
+ },
7385
+ request.requestId
7386
+ ))
7387
+ ] }) : null,
7388
+ transcriptMessages.map((message, index) => /* @__PURE__ */ jsxs17(
7389
+ "div",
7390
+ {
7391
+ ref: index === lastVisitorIndex + 1 ? responseRef : void 0,
7392
+ className: "agent-rail__turn-block",
7393
+ children: [
7394
+ message.role === "agent" ? message.toolResults?.map((result) => /* @__PURE__ */ jsx18(
7395
+ VisitorToolResultView,
6759
7396
  {
6760
- type: "button",
6761
- className: "agent-rail__expand",
6762
- "aria-label": expanded ? "Exit full screen" : "Open full screen",
6763
- onClick: onExpandToggle,
6764
- children: expanded ? /* @__PURE__ */ jsx18(RestoreIcon, {}) : /* @__PURE__ */ jsx18(ExpandIcon, {})
6765
- }
6766
- ) : null
6767
- ] })
6768
- ] }) }),
6769
- /* @__PURE__ */ jsx18("div", { ref: transcriptRef, className: "agent-rail__transcript", children: /* @__PURE__ */ jsxs17("div", { ref: threadRef, className: "agent-rail__thread", children: [
6770
- !hasVisitorMessages2 ? /* @__PURE__ */ jsxs17("section", { className: "agent-rail__welcome", "aria-label": "Welcome", children: [
6771
- greeting?.role === "agent" ? /* @__PURE__ */ jsx18(
7397
+ result
7398
+ },
7399
+ result.id
7400
+ )) : null,
7401
+ /* @__PURE__ */ jsx18(
6772
7402
  MessageBubble,
6773
7403
  {
6774
- message: greeting,
6775
- bookingDisabled: isBusy,
7404
+ showProvenance: Boolean(
7405
+ handoff?.page?.enabled || handoffActive
7406
+ ),
7407
+ message,
7408
+ bookingDisabled: isBusy || handoffActive,
6776
7409
  brandLogoUrl: showBrandLogo ? resolvedBrandLogoUrl : void 0,
7410
+ offer: index === transcriptMessages.length - 1 ? state.pendingOffer : void 0,
6777
7411
  onBook
6778
7412
  }
6779
- ) : null,
6780
- showIdleFollowUps ? /* @__PURE__ */ jsx18("div", { className: "agent-rail__followups-slot", children: /* @__PURE__ */ jsx18(
6781
- FollowUpChips,
6782
- {
6783
- suggestions: state.followUps,
6784
- disabled: isBusy,
6785
- label: "Start here",
6786
- onSelect: (suggestion) => handleFollowUpSelect(suggestion.label)
6787
- }
6788
- ) }) : null,
6789
- showActivity ? /* @__PURE__ */ jsx18(
6790
- AgentActivityBubble,
7413
+ ),
7414
+ index === lastAgentIndex && showMessageActions && message.role === "agent" ? /* @__PURE__ */ jsx18(
7415
+ MessageActions,
6791
7416
  {
6792
- brandLabel: resolvedBrandLabel,
6793
- brandLogoUrl: showBrandLogo ? resolvedBrandLogoUrl : void 0,
6794
- failed: state.phase === "error",
6795
- steps: activitySteps
7417
+ answeredAt: message.createdAt,
7418
+ copyText: hideToolCardFences(message.text).trim() || message.text,
7419
+ readAloud,
7420
+ receiptSteps,
7421
+ onOpenReceipt: receiptSteps ? openReceipt : void 0,
7422
+ onRegenerate: onRegenerate ? handleRegenerate : void 0,
7423
+ onFeedback: onFeedback ? (rating) => onFeedback(rating, message) : void 0
6796
7424
  }
6797
7425
  ) : null,
6798
- visibleVisitorToolResults.map((result) => /* @__PURE__ */ jsx18(
6799
- VisitorToolResultView,
6800
- {
6801
- result,
6802
- disabled: semanticSurfaceDisabled,
6803
- onToolInput
6804
- },
6805
- result.id
6806
- )),
6807
- pendingInputRequests.slice(0, 1).map((request) => /* @__PURE__ */ jsx18(
6808
- HumanInputCard,
6809
- {
6810
- request,
6811
- onRespond: onInputResponse
6812
- },
6813
- request.requestId
6814
- ))
6815
- ] }) : null,
6816
- transcriptMessages.map((message, index) => /* @__PURE__ */ jsxs17(
6817
- "div",
6818
- {
6819
- ref: index === lastVisitorIndex + 1 ? responseRef : void 0,
6820
- className: "agent-rail__turn-block",
6821
- children: [
6822
- message.role === "agent" ? message.toolResults?.map((result) => /* @__PURE__ */ jsx18(VisitorToolResultView, { result }, result.id)) : null,
6823
- /* @__PURE__ */ jsx18(
6824
- MessageBubble,
6825
- {
6826
- message,
6827
- bookingDisabled: isBusy,
6828
- brandLogoUrl: showBrandLogo ? resolvedBrandLogoUrl : void 0,
6829
- offer: index === transcriptMessages.length - 1 ? state.pendingOffer : void 0,
6830
- onBook
6831
- }
6832
- ),
6833
- index === lastAgentIndex && showMessageActions && message.role === "agent" ? /* @__PURE__ */ jsx18(
6834
- MessageActions,
6835
- {
6836
- answeredAt: message.createdAt,
6837
- copyText: hideToolCardFences(message.text).trim() || message.text,
6838
- readAloud,
6839
- receiptSteps,
6840
- onOpenReceipt: receiptSteps ? openReceipt : void 0,
6841
- onRegenerate: onRegenerate ? handleRegenerate : void 0,
6842
- onFeedback: onFeedback ? (rating) => onFeedback(rating, message) : void 0
6843
- }
6844
- ) : null,
6845
- index === lastVisitorIndex ? /* @__PURE__ */ jsxs17(Fragment4, { children: [
6846
- showActivity ? /* @__PURE__ */ jsx18(
6847
- AgentActivityBubble,
6848
- {
6849
- brandLabel: resolvedBrandLabel,
6850
- brandLogoUrl: showBrandLogo ? resolvedBrandLogoUrl : void 0,
6851
- failed: state.phase === "error",
6852
- steps: activitySteps
6853
- }
6854
- ) : null,
6855
- visibleVisitorToolResults.map((result) => /* @__PURE__ */ jsx18(
6856
- VisitorToolResultView,
6857
- {
6858
- result,
6859
- disabled: semanticSurfaceDisabled,
6860
- onToolInput
6861
- },
6862
- result.id
6863
- )),
6864
- pendingInputRequests.slice(0, 1).map((request) => /* @__PURE__ */ jsx18(
6865
- HumanInputCard,
6866
- {
6867
- request,
6868
- onRespond: onInputResponse
6869
- },
6870
- request.requestId
6871
- ))
6872
- ] }) : null
6873
- ]
6874
- },
6875
- message.role === "agent" && transcriptMessages[index - 1]?.role === "visitor" ? `answer-${transcriptMessages[index - 1]?.id}` : message.id
6876
- )),
6877
- waitingForBooking ? /* @__PURE__ */ jsx18(BookingCardLoader, {}) : null,
6878
- state.error ? /* @__PURE__ */ jsxs17("section", { className: "agent-rail__error", role: "alert", children: [
6879
- /* @__PURE__ */ jsxs17("div", { children: [
6880
- /* @__PURE__ */ jsx18("strong", { children: "Something went wrong" }),
6881
- /* @__PURE__ */ jsx18("p", { children: state.error })
6882
- ] }),
6883
- onRetry ? /* @__PURE__ */ jsx18("button", { type: "button", onClick: onRetry, children: "Try again" }) : null
6884
- ] }) : null
6885
- ] }) }),
6886
- showDisclaimerLabel ? /* @__PURE__ */ jsx18("div", { className: "agent-rail__disclaimer", children: /* @__PURE__ */ jsx18("p", { children: disclaimerLink ? /* @__PURE__ */ jsx18(
6887
- "a",
7426
+ index === lastVisitorIndex ? /* @__PURE__ */ jsxs17(Fragment4, { children: [
7427
+ showActivity ? /* @__PURE__ */ jsx18(
7428
+ AgentActivityBubble,
7429
+ {
7430
+ brandLabel: resolvedBrandLabel,
7431
+ brandLogoUrl: showBrandLogo ? resolvedBrandLogoUrl : void 0,
7432
+ failed: state.phase === "error",
7433
+ steps: activitySteps
7434
+ }
7435
+ ) : null,
7436
+ visibleVisitorToolResults.map((result) => /* @__PURE__ */ jsx18(
7437
+ VisitorToolResultView,
7438
+ {
7439
+ result,
7440
+ disabled: semanticSurfaceDisabled,
7441
+ onToolInput
7442
+ },
7443
+ result.id
7444
+ )),
7445
+ pendingInputRequests.slice(0, 1).map((request) => /* @__PURE__ */ jsx18(
7446
+ HumanInputCard,
7447
+ {
7448
+ request,
7449
+ onRespond: onInputResponse
7450
+ },
7451
+ request.requestId
7452
+ ))
7453
+ ] }) : null
7454
+ ]
7455
+ },
7456
+ message.role === "agent" && transcriptMessages[index - 1]?.role === "visitor" ? `answer-${transcriptMessages[index - 1]?.id}` : message.id
7457
+ )),
7458
+ waitingForBooking ? /* @__PURE__ */ jsx18(BookingCardLoader, {}) : null,
7459
+ state.error ? /* @__PURE__ */ jsxs17("section", { className: "agent-rail__error", role: "alert", children: [
7460
+ /* @__PURE__ */ jsxs17("div", { children: [
7461
+ /* @__PURE__ */ jsx18("strong", { children: "Something went wrong" }),
7462
+ /* @__PURE__ */ jsx18("p", { children: state.error })
7463
+ ] }),
7464
+ onRetry ? /* @__PURE__ */ jsx18("button", { type: "button", onClick: onRetry, children: "Try again" }) : null
7465
+ ] }) : null
7466
+ ] }) }),
7467
+ showDisclaimerLabel ? /* @__PURE__ */ jsx18("div", { className: "agent-rail__disclaimer", children: /* @__PURE__ */ jsx18("p", { children: disclaimerLink ? /* @__PURE__ */ jsx18(
7468
+ "a",
7469
+ {
7470
+ href: disclaimerLink,
7471
+ rel: "noopener noreferrer",
7472
+ target: "_blank",
7473
+ children: resolvedDisclaimerLabel
7474
+ }
7475
+ ) : resolvedDisclaimerLabel }) }) : null,
7476
+ handoff && (handoff.page?.enabled || handoffActive) ? /* @__PURE__ */ jsxs17("div", { className: "agent-rail__handoff", children: [
7477
+ /* @__PURE__ */ jsx18("p", { role: "status", "aria-live": "polite", children: !handoff.page ? "Checking conversation\u2026" : handoffStatus === "human" ? "Connected with support" : handoffStatus === "queued" ? "Waiting for a representative" : handoffStatus === "requesting" ? "Connecting you to support\u2026" : handoffStatus === "resolved" ? "Your conversation with support has ended" : handoffStatus === "failed" ? "We couldn\u2019t connect you to support" : "Need a person?" }),
7478
+ handoffStatus === "ai" ? /* @__PURE__ */ jsx18(
7479
+ "button",
7480
+ {
7481
+ type: "button",
7482
+ disabled: handoff.busy || handoff.hasPending || isBusy,
7483
+ onClick: () => void handoff.start(),
7484
+ children: "Talk to a person"
7485
+ }
7486
+ ) : null,
7487
+ handoffStatus === "resolved" ? /* @__PURE__ */ jsx18(
7488
+ "button",
7489
+ {
7490
+ type: "button",
7491
+ disabled: handoff.busy || handoff.hasPending,
7492
+ onClick: () => void handoff.returnToAI(),
7493
+ children: "Return to AI"
7494
+ }
7495
+ ) : null,
7496
+ handoffStatus === "failed" && onReset ? /* @__PURE__ */ jsx18(
7497
+ "button",
7498
+ {
7499
+ type: "button",
7500
+ disabled: !handoff.canReset,
7501
+ onClick: handleReset,
7502
+ children: "Start a new conversation"
7503
+ }
7504
+ ) : null,
7505
+ handoff.error ? /* @__PURE__ */ jsxs17("div", { role: "alert", children: [
7506
+ /* @__PURE__ */ jsx18("span", { children: handoff.error }),
7507
+ /* @__PURE__ */ jsx18(
7508
+ "button",
6888
7509
  {
6889
- href: disclaimerLink,
6890
- rel: "noopener noreferrer",
6891
- target: "_blank",
6892
- children: resolvedDisclaimerLabel
7510
+ type: "button",
7511
+ disabled: handoff.busy,
7512
+ onClick: () => void handoff.retry(),
7513
+ children: "Retry"
6893
7514
  }
6894
- ) : resolvedDisclaimerLabel }) }) : null,
6895
- /* @__PURE__ */ jsxs17("div", { className: "agent-rail__composer-wrap", children: [
6896
- /* @__PURE__ */ jsx18(
6897
- LocationConsent,
6898
- {
6899
- request: locationRequest,
6900
- onRespond: onInputResponse
6901
- },
6902
- state.messages.find((message) => message.role === "visitor")?.id ?? "empty"
6903
- ),
6904
- showJumpToLatest ? /* @__PURE__ */ jsx18(
6905
- "button",
6906
- {
6907
- type: "button",
6908
- className: "agent-rail__jump-to-latest",
6909
- "aria-label": "Jump to the latest message",
6910
- title: "Jump to the latest message",
6911
- onClick: scrollToLatest,
6912
- children: /* @__PURE__ */ jsx18(ChevronDownIcon, {})
6913
- }
6914
- ) : null,
6915
- /* @__PURE__ */ jsx18(
6916
- Composer,
6917
- {
6918
- variant: expanded || mobileFullscreen ? "dock" : "default",
6919
- disabled: isBusy,
6920
- form: composerForm && lastMessage ? { ...composerForm, id: `${lastMessage.id}:${composerForm.id}` } : null,
6921
- allowFormResume: !state.pendingOffer && !waitingForBooking && !hasPendingConfirmation,
6922
- placeholder: composerPlaceholder,
6923
- onSubmit: handleSubmit
6924
- },
6925
- `${state.messages.find((message) => message.role === "visitor")?.id ?? "empty"}:${latestResultId ?? ""}`
6926
- ),
6927
- poweredByLabel ? /* @__PURE__ */ jsx18("div", { className: "agent-rail__footer", children: /* @__PURE__ */ jsx18("p", { children: /* @__PURE__ */ jsx18("span", { children: poweredByLabel }) }) }) : null
6928
- ] })
6929
- ] }),
6930
- /* @__PURE__ */ jsx18("div", { ref: overlayRef, className: "agent-rail__overlay" }),
6931
- receiptOpen && receiptSteps ? /* @__PURE__ */ jsx18(
6932
- AnswerReceiptDialog,
7515
+ )
7516
+ ] }) : null
7517
+ ] }) : null,
7518
+ /* @__PURE__ */ jsxs17("div", { className: "agent-rail__composer-wrap", children: [
7519
+ /* @__PURE__ */ jsx18(
7520
+ LocationConsent,
7521
+ {
7522
+ request: locationRequest,
7523
+ onRespond: onInputResponse
7524
+ },
7525
+ state.messages.find((message) => message.role === "visitor")?.id ?? "empty"
7526
+ ),
7527
+ showJumpToLatest ? /* @__PURE__ */ jsx18(
7528
+ "button",
6933
7529
  {
6934
- brandLabel: resolvedBrandLabel,
6935
- onClose: () => setReceiptOpen(false),
6936
- steps: receiptSteps
7530
+ type: "button",
7531
+ className: "agent-rail__jump-to-latest",
7532
+ "aria-label": "Jump to the latest message",
7533
+ title: "Jump to the latest message",
7534
+ onClick: scrollToLatest,
7535
+ children: /* @__PURE__ */ jsx18(ChevronDownIcon, {})
6937
7536
  }
6938
- ) : null
6939
- ]
6940
- }
6941
- )
7537
+ ) : null,
7538
+ /* @__PURE__ */ jsx18(
7539
+ Composer,
7540
+ {
7541
+ variant: expanded || mobileFullscreen ? "dock" : "default",
7542
+ disabled: isBusy || Boolean(handoff?.busy || handoff?.hasPending) || handoffActive && !["requesting", "queued", "human"].includes(
7543
+ handoffStatus ?? ""
7544
+ ),
7545
+ form: !handoffActive && composerForm && lastMessage ? {
7546
+ ...composerForm,
7547
+ id: `${lastMessage.id}:${composerForm.id}`
7548
+ } : null,
7549
+ allowFormResume: !handoffActive && !state.pendingOffer && !waitingForBooking && !hasPendingConfirmation,
7550
+ placeholder: handoffActive ? "Message support\u2026" : composerPlaceholder,
7551
+ onSubmit: handleSubmit
7552
+ },
7553
+ `${state.messages.find((message) => message.role === "visitor")?.id ?? "empty"}:${latestResultId ?? ""}`
7554
+ ),
7555
+ poweredByLabel ? /* @__PURE__ */ jsx18("div", { className: "agent-rail__footer", children: /* @__PURE__ */ jsx18("p", { children: /* @__PURE__ */ jsx18("span", { children: poweredByLabel }) }) }) : null
7556
+ ] })
7557
+ ] }),
7558
+ /* @__PURE__ */ jsx18("div", { ref: overlayRef, className: "agent-rail__overlay" }),
7559
+ receiptOpen && receiptSteps ? /* @__PURE__ */ jsx18(
7560
+ AnswerReceiptDialog,
7561
+ {
7562
+ brandLabel: resolvedBrandLabel,
7563
+ onClose: () => setReceiptOpen(false),
7564
+ steps: receiptSteps
7565
+ }
7566
+ ) : null
7567
+ ] })
6942
7568
  }
6943
7569
  );
6944
7570
  }
6945
7571
 
6946
7572
  // src/react/components/AssistEdgeTab/AssistEdgeTab.tsx
6947
- import { useState as useState11 } from "react";
7573
+ import { useState as useState12 } from "react";
6948
7574
  import { Fragment as Fragment5, jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
6949
7575
  function ChatSparkIcon() {
6950
7576
  return /* @__PURE__ */ jsxs18(
@@ -7025,8 +7651,8 @@ function TabMarkIcon({
7025
7651
  }) {
7026
7652
  const custom = customIconUrl?.trim();
7027
7653
  const logo = logoUrl?.trim();
7028
- const [customFailed, setCustomFailed] = useState11(false);
7029
- const [logoFailed, setLogoFailed] = useState11(false);
7654
+ const [customFailed, setCustomFailed] = useState12(false);
7655
+ const [logoFailed, setLogoFailed] = useState12(false);
7030
7656
  if (custom && !customFailed) {
7031
7657
  return /* @__PURE__ */ jsx19(
7032
7658
  "img",
@@ -7214,8 +7840,8 @@ function findPrecedingVisitorText(messages, agentMessageId) {
7214
7840
  }
7215
7841
  return void 0;
7216
7842
  }
7217
- function normalizeAgentFeedbackAnswerText(text2) {
7218
- const normalized = text2.replace(/\s+/g, " ").trim();
7843
+ function normalizeAgentFeedbackAnswerText(text3) {
7844
+ const normalized = text3.replace(/\s+/g, " ").trim();
7219
7845
  if (!normalized) return void 0;
7220
7846
  return normalized.length > 4e3 ? `${normalized.slice(0, 3997)}...` : normalized;
7221
7847
  }
@@ -7256,10 +7882,10 @@ function sendAgentAnswerFeedback(eventUrl, event) {
7256
7882
  }
7257
7883
 
7258
7884
  // src/react/components/AgentWidget/AgentWidget.tsx
7259
- import { useEffect as useEffect10, useRef as useRef8, useState as useState13 } from "react";
7885
+ import { useEffect as useEffect11, useRef as useRef9, useState as useState14 } from "react";
7260
7886
 
7261
7887
  // src/react/page-shift.ts
7262
- import { useEffect as useEffect8 } from "react";
7888
+ import { useEffect as useEffect9 } from "react";
7263
7889
  var PAGE_SHIFT_CLASS = "webless-agent-page-shift";
7264
7890
  var DEFAULT_RAIL_WIDTH_PX = 450;
7265
7891
  function shouldApplyPageShift(input) {
@@ -7311,7 +7937,7 @@ function clearPageMargin() {
7311
7937
  }
7312
7938
  function usePageShift(input) {
7313
7939
  const { active, railSlotRef } = input;
7314
- useEffect8(() => {
7940
+ useEffect9(() => {
7315
7941
  if (typeof document === "undefined") {
7316
7942
  return;
7317
7943
  }
@@ -7339,12 +7965,12 @@ function usePageShift(input) {
7339
7965
  }
7340
7966
 
7341
7967
  // src/react/hooks/useIsMobile.ts
7342
- import { useEffect as useEffect9, useState as useState12 } from "react";
7968
+ import { useEffect as useEffect10, useState as useState13 } from "react";
7343
7969
  function useIsMobile(breakpoint = 767) {
7344
- const [isMobile, setIsMobile] = useState12(
7970
+ const [isMobile, setIsMobile] = useState13(
7345
7971
  () => typeof window !== "undefined" && window.matchMedia(`(max-width: ${breakpoint}px)`).matches
7346
7972
  );
7347
- useEffect9(() => {
7973
+ useEffect10(() => {
7348
7974
  const media = window.matchMedia(`(max-width: ${breakpoint}px)`);
7349
7975
  const onChange = () => setIsMobile(media.matches);
7350
7976
  onChange();
@@ -7374,9 +8000,9 @@ function AgentWidget({
7374
8000
  }) {
7375
8001
  const isMobile = useIsMobile();
7376
8002
  const placement = normalizeAgentPlacement(placementInput);
7377
- const railSlotRef = useRef8(null);
7378
- const [railCollapsed, setRailCollapsed] = useState13(defaultCollapsed);
7379
- const [railExpanded, setRailExpanded] = useState13(false);
8003
+ const railSlotRef = useRef9(null);
8004
+ const [railCollapsed, setRailCollapsed] = useState14(defaultCollapsed);
8005
+ const [railExpanded, setRailExpanded] = useState14(false);
7380
8006
  const pageShiftActive = shouldApplyPageShift({
7381
8007
  pageShift,
7382
8008
  isMobile,
@@ -7429,7 +8055,7 @@ function AgentWidget({
7429
8055
  } : {},
7430
8056
  ...branding?.colors?.border ? { border: branding.colors.border } : {}
7431
8057
  };
7432
- useEffect10(() => {
8058
+ useEffect11(() => {
7433
8059
  if (!registerPanelController) return;
7434
8060
  registerAgentPanelController(customerId, {
7435
8061
  open: () => setRailCollapsed(false),
@@ -7464,7 +8090,7 @@ function AgentWidget({
7464
8090
  })
7465
8091
  );
7466
8092
  }
7467
- useEffect10(() => {
8093
+ useEffect11(() => {
7468
8094
  if (railCollapsed) return;
7469
8095
  const handleKeyDown = (event) => {
7470
8096
  if (event.key === "Tab" && (isMobile || railExpanded)) {
@@ -7608,4 +8234,4 @@ export {
7608
8234
  sendAgentAnswerFeedback,
7609
8235
  AgentWidget
7610
8236
  };
7611
- //# sourceMappingURL=chunk-NW37MHU7.js.map
8237
+ //# sourceMappingURL=chunk-EVWZF34F.js.map