@usereq/widget 0.2.20 → 0.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usereq/widget",
3
- "version": "0.2.20",
3
+ "version": "0.2.22",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -1,6 +1,5 @@
1
1
  import React from "react";
2
2
  import { createRoot, type Root } from "react-dom/client";
3
- import { DEFAULT_START_CONVERSATION_LABEL } from "../chat-widget/chat-widget-defaults";
4
3
  import {
5
4
  DEFAULT_WIDGET_APPEARANCE,
6
5
  DEFAULT_WIDGET_PRESET,
@@ -15,7 +14,10 @@ import {
15
14
  type WidgetSessionState,
16
15
  type WidgetVariant,
17
16
  } from "../types";
18
- import { widgetTriggerRuleKey } from "../shared/widget-config";
17
+ import {
18
+ normalizeWidgetMobileHeight,
19
+ widgetTriggerRuleKey,
20
+ } from "../shared/widget-config";
19
21
  import { WidgetRuntime, type WidgetRuntimeStatus } from "../renderer/index";
20
22
  import { buildAssistantMessageRevealSteps } from "../chat-widget/message-reveal";
21
23
  import { DEFAULT_STOP_CONVERSATION_PROMPT } from "../shared/stop-confirmation";
@@ -23,7 +25,6 @@ import {
23
25
  bootstrapWidget,
24
26
  resetWidgetSession,
25
27
  submitWidgetStopConfirmation,
26
- startWidgetConversation,
27
28
  submitWidgetMessage,
28
29
  } from "../runtime/bootstrap";
29
30
  import { getWidgetShadowCss } from "../shared/shadow-css";
@@ -67,7 +68,12 @@ export class AgentWidgetElement extends HTMLElement {
67
68
  private sendVersion = 0;
68
69
  private confirmationVersion = 0;
69
70
  private bootstrapPhase: "idle" | "booting" | "ready" | "blocked" = "idle";
70
- private autoStartTriggered = false;
71
+ // Back-to-back send support. The composer stays enabled while the agent
72
+ // is replying; new submissions land here and are drained sequentially by
73
+ // `sendMessage`. Each entry carries the optimistic bubble id so failures
74
+ // can yank exactly the right row.
75
+ private pendingQueue: Array<{ content: string; optimisticId: string }> = [];
76
+ private draining = false;
71
77
 
72
78
  constructor() {
73
79
  super();
@@ -207,6 +213,10 @@ export class AgentWidgetElement extends HTMLElement {
207
213
  this.welcomeMessage = null;
208
214
  this.errorMessage = null;
209
215
  this.status = "idle";
216
+ // Bumping sendVersion above invalidates the in-flight drain; reset
217
+ // the queue + flag so the next session starts fresh.
218
+ this.pendingQueue = [];
219
+ this.draining = false;
210
220
  }
211
221
 
212
222
  private renderWidget() {
@@ -224,32 +234,7 @@ export class AgentWidgetElement extends HTMLElement {
224
234
  const variant = this.getResolvedVariant();
225
235
  const preset = this.getResolvedPreset();
226
236
  const behavior = this.sessionConfig?.widgetConfig.widgetBehavior;
227
- const autoStartEnabled = behavior?.autoStart === true;
228
237
  const alwaysOpen = behavior?.alwaysOpen === true;
229
- const awaitingConversation =
230
- this.status === "booting" ||
231
- (this.sessionState != null && !this.sessionState.conversationId);
232
- // When auto-start is on we never render the manual CTA — except as a
233
- // fallback if the auto-start attempt errored, so the visitor isn't
234
- // stranded on a blank panel.
235
- const showFallbackCta = autoStartEnabled && this.status === "error";
236
- const emptyAction =
237
- !autoStartEnabled && awaitingConversation
238
- ? {
239
- label: DEFAULT_START_CONVERSATION_LABEL,
240
- onClick: () => void this.handleStartConversation(),
241
- disabled: this.status !== "ready",
242
- }
243
- : showFallbackCta
244
- ? {
245
- label: DEFAULT_START_CONVERSATION_LABEL,
246
- onClick: () => {
247
- this.autoStartTriggered = false;
248
- void this.handleStartConversation();
249
- },
250
- disabled: false,
251
- }
252
- : undefined;
253
238
 
254
239
  this.root.render(
255
240
  React.createElement(WidgetRuntime, {
@@ -267,19 +252,23 @@ export class AgentWidgetElement extends HTMLElement {
267
252
  welcomeMessage: this.getWelcomeMessage(),
268
253
  spotlights: this.getSpotlights(),
269
254
  ctaLink: this.getCtaLink(),
255
+ mobileHeight: normalizeWidgetMobileHeight(
256
+ this.sessionConfig?.widgetConfig.mobileHeight,
257
+ ),
270
258
  open: this.open,
271
259
  alwaysOpen,
272
260
  status: this.status,
273
261
  messages: this.messages,
274
262
  errorMessage: this.errorMessage,
275
263
  triggerRule: this.sessionConfig?.widgetConfig.triggerRule ?? null,
276
- emptyAction,
264
+ // Lazy session: no "Start a chat" gate. The composer is live from
265
+ // the moment the panel opens; the conversation is created on the
266
+ // server the first time the user actually sends something.
267
+ emptyAction: undefined,
277
268
  onStopConversation:
278
269
  this.sessionState?.conversationId != null
279
270
  ? () => void this.handleStopConversation()
280
271
  : undefined,
281
- sendDisabled:
282
- this.status === "sending" || !this.sessionState?.conversationId,
283
272
  onOpenChange: (nextOpen: boolean) => void this.handleOpenChange(nextOpen),
284
273
  onSendMessage: (content: string) => void this.sendMessage(content),
285
274
  onConfirmationDecision: (confirmationId: string, accepted: boolean) =>
@@ -335,6 +324,11 @@ export class AgentWidgetElement extends HTMLElement {
335
324
  this.welcomeMessage = state.session.widgetConfig.welcomeMessage;
336
325
  this.status = "ready";
337
326
  this.bootstrapPhase = "ready";
327
+ // Lazy session: when the visitor doesn't already have a running
328
+ // conversation, seed a client-side welcome bubble from the boot
329
+ // config so the panel doesn't open on a blank empty state. The
330
+ // real conversation is only created server-side once they type.
331
+ this.seedClientWelcomeIfNeeded();
338
332
  // Apply the agent's configured open-state on mount. `alwaysOpen`
339
333
  // wins (the panel is pinned regardless), and `defaultOpen` opens it
340
334
  // once if the visitor hasn't already interacted with the launcher.
@@ -343,17 +337,6 @@ export class AgentWidgetElement extends HTMLElement {
343
337
  this.open = true;
344
338
  }
345
339
  this.renderWidget();
346
- // Bootstrap has finished and status is now "ready". If `autoStart`
347
- // is on AND the panel is open (either because `defaultOpen` /
348
- // `alwaysOpen` just opened it, or because the visitor opened it
349
- // before bootstrap finished and `handleOpenChange` is awaiting
350
- // this very `ensureBootstrap` call), trigger the auto-start now.
351
- // Without this, the `defaultOpen + autoStart` combination renders
352
- // an empty panel with no CTA (autoStart suppresses the Start CTA)
353
- // and the visitor sees no welcome message ever.
354
- // `maybeAutoStartConversation` is idempotent — it guards on
355
- // `autoStartTriggered` so calling it from multiple paths is safe.
356
- this.maybeAutoStartConversation();
357
340
  } catch (error) {
358
341
  if (runVersion !== this.bootstrapVersion) {
359
342
  return;
@@ -374,35 +357,125 @@ export class AgentWidgetElement extends HTMLElement {
374
357
  return this.bootstrapPromise;
375
358
  }
376
359
 
360
+ /**
361
+ * Entry point for user-typed sends. Drops the optimistic bubble in
362
+ * immediately so the user sees their own backlog, then either kicks off
363
+ * the drain loop (if idle) or appends to `pendingQueue` (if a prior send
364
+ * is still mid-stream). The drain loop runs at most one POST in flight
365
+ * at a time — the server-side session memory expects sequential turns,
366
+ * and the AI engineer confirmed parallel requests are *possible* but
367
+ * the answers would interleave out-of-order on the wire.
368
+ */
377
369
  private async sendMessage(content: string) {
378
370
  const trimmedContent = content.trim();
379
- if (!trimmedContent || !this.sessionState?.conversationId) return;
380
-
381
- const runVersion = ++this.sendVersion;
382
- const optimisticMessageId = `optimistic:${Date.now()}`;
371
+ // Lazy session: no `conversationId` check here. If the visitor is
372
+ // sending their very first message, `submitWidgetMessage` will call
373
+ // POST /conversations/public/start under the hood before posting.
374
+ if (!trimmedContent || !this.sessionState) return;
375
+
376
+ const optimisticMessageId = `optimistic:${Date.now()}:${Math.random()
377
+ .toString(36)
378
+ .slice(2, 8)}`;
383
379
  const optimisticMessage: WidgetMessage = {
384
380
  id: optimisticMessageId,
385
- conversationId: this.sessionState.conversationId,
381
+ // Empty when no conversation exists yet — the real id lands on the
382
+ // server row we swap in from `submitWidgetMessage`'s response.
383
+ conversationId: this.sessionState.conversationId ?? "",
386
384
  role: "user",
387
385
  content: trimmedContent,
388
386
  createdAt: new Date().toISOString(),
389
387
  };
390
388
 
391
- this.status = "sending";
392
389
  this.errorMessage = null;
393
390
  this.messages = [...this.messages, optimisticMessage];
394
391
  this.renderWidget();
395
392
 
393
+ // Drain already running → just join the queue. The drainer will
394
+ // consume this entry as soon as the in-flight POST + reveal settle.
395
+ if (this.draining) {
396
+ this.pendingQueue.push({
397
+ content: trimmedContent,
398
+ optimisticId: optimisticMessageId,
399
+ });
400
+ return;
401
+ }
402
+
403
+ this.draining = true;
404
+ this.status = "sending";
405
+ this.renderWidget();
406
+
407
+ const runVersion = this.sendVersion;
408
+ let succeeded = true;
409
+
410
+ try {
411
+ await this.processSend(trimmedContent, optimisticMessageId, runVersion);
412
+ while (this.pendingQueue.length > 0 && runVersion === this.sendVersion) {
413
+ const next = this.pendingQueue.shift()!;
414
+ await this.processSend(next.content, next.optimisticId, runVersion);
415
+ }
416
+ } catch {
417
+ succeeded = false;
418
+ if (runVersion === this.sendVersion) {
419
+ // Drop the rest of the backlog — each queued message expects the
420
+ // server to have already consumed the prior turn, which didn't
421
+ // happen. Leaving them queued would silently fire mid-failure.
422
+ const failedIds = new Set(
423
+ this.pendingQueue.map((entry) => entry.optimisticId),
424
+ );
425
+ this.pendingQueue = [];
426
+ this.messages = this.messages.filter((m) => !failedIds.has(m.id));
427
+ // `processSend` already set status="error" + errorMessage; render
428
+ // once more so the cleared queue is visible.
429
+ this.renderWidget();
430
+ }
431
+ } finally {
432
+ this.draining = false;
433
+ if (succeeded && runVersion === this.sendVersion) {
434
+ this.status = "ready";
435
+ this.renderWidget();
436
+ }
437
+ }
438
+ }
439
+
440
+ /**
441
+ * One round-trip: POST → swap optimistic → stagger assistant reveal.
442
+ * Throws on failure so the outer drainer can clear the backlog. The
443
+ * `runVersion` guard short-circuits late state writes if a hard reset
444
+ * (agent-id change, conversation reset) bumped `sendVersion` mid-drain.
445
+ */
446
+ private async processSend(
447
+ content: string,
448
+ optimisticId: string,
449
+ runVersion: number,
450
+ ) {
396
451
  try {
397
452
  const result = await submitWidgetMessage({
398
453
  agentId: this.agentId,
399
- content: trimmedContent,
454
+ content,
400
455
  });
401
- if (runVersion !== this.sendVersion) {
402
- return;
403
- }
456
+ if (runVersion !== this.sendVersion) return;
404
457
  this.sessionState = result.session;
405
458
 
459
+ // Lazy-start swap: when this was the first send, `submitWidgetMessage`
460
+ // created the conversation server-side and the server inserted its
461
+ // own welcome message row. Replace the placeholder client-welcome
462
+ // bubble with the real server row so ids/timestamps match the DB.
463
+ if (result.welcomeMessage) {
464
+ const clientWelcomeId = this.getClientWelcomeId();
465
+ this.messages = this.messages.map((m) =>
466
+ m.id === clientWelcomeId
467
+ ? {
468
+ id: result.welcomeMessage!.id,
469
+ conversationId: result.conversationId,
470
+ role: "assistant",
471
+ content: result.welcomeMessage!.content,
472
+ createdAt: result.welcomeMessage!.createdAt,
473
+ }
474
+ : m,
475
+ );
476
+ this.renderWidget();
477
+ }
478
+
406
479
  const revealSteps = buildAssistantMessageRevealSteps(
407
480
  result.assistantMessages.map((message) => ({
408
481
  id: message.id,
@@ -418,10 +491,10 @@ export class AgentWidgetElement extends HTMLElement {
418
491
  );
419
492
 
420
493
  for (const step of revealSteps) {
421
- await new Promise((resolve) => window.setTimeout(resolve, step.delayMs));
422
- if (runVersion !== this.sendVersion) {
423
- return;
424
- }
494
+ await new Promise((resolve) =>
495
+ window.setTimeout(resolve, step.delayMs),
496
+ );
497
+ if (runVersion !== this.sendVersion) return;
425
498
  const dto = step.message;
426
499
  this.messages = [
427
500
  ...this.messages,
@@ -435,22 +508,17 @@ export class AgentWidgetElement extends HTMLElement {
435
508
  ];
436
509
  this.renderWidget();
437
510
  }
438
-
439
- if (runVersion === this.sendVersion) {
440
- this.status = "ready";
441
- this.renderWidget();
442
- }
443
511
  } catch (error) {
444
512
  if (runVersion !== this.sendVersion) {
513
+ // The hard reset path already cleared state — swallow.
445
514
  return;
446
515
  }
447
- this.messages = this.messages.filter(
448
- (message) => message.id !== optimisticMessageId,
449
- );
516
+ this.messages = this.messages.filter((m) => m.id !== optimisticId);
450
517
  this.status = "error";
451
518
  this.errorMessage =
452
519
  error instanceof Error ? error.message : "Failed to send message";
453
520
  this.renderWidget();
521
+ throw error;
454
522
  }
455
523
  }
456
524
 
@@ -461,55 +529,34 @@ export class AgentWidgetElement extends HTMLElement {
461
529
  if (this.open && (this.status === "idle" || !this.sessionState)) {
462
530
  await this.ensureBootstrap();
463
531
  }
532
+ }
464
533
 
465
- this.maybeAutoStartConversation();
534
+ private getClientWelcomeId(): string {
535
+ return `client-welcome:${this.agentId}`;
466
536
  }
467
537
 
468
538
  /**
469
- * Auto-start opt-in: when the agent has `widgetBehavior.autoStart`
470
- * enabled, the first time the visitor opens the launcher we transparently
471
- * call /conversations/public/start so they skip the empty state. Fires
472
- * once per element lifetime; subsequent opens reuse the existing
473
- * conversation via the standard sessionStorage rehydrate.
539
+ * Lazy session seeding. When bootstrap finishes with no live conversation
540
+ * and no rehydrated transcript, drop a synthetic assistant bubble carrying
541
+ * the boot-config welcome copy into `this.messages`. Skipped when there's
542
+ * already a transcript (rehydrated session) or no welcome copy configured.
543
+ * The bubble's id is stable (`client-welcome:{agentId}`) so `processSend`
544
+ * can swap it for the real server row after the first successful send.
474
545
  */
475
- private maybeAutoStartConversation() {
476
- if (this.autoStartTriggered) return;
477
- if (!this.open) return;
478
- if (this.status !== "ready") return;
479
- if (!this.sessionConfig) return;
480
- if (!this.sessionConfig.widgetConfig.widgetBehavior?.autoStart) return;
546
+ private seedClientWelcomeIfNeeded() {
547
+ if (this.messages.length > 0) return;
481
548
  if (this.sessionState?.conversationId) return;
482
-
483
- this.autoStartTriggered = true;
484
- void this.handleStartConversation();
485
- }
486
-
487
- private async handleStartConversation() {
488
- if (!this.sessionState) {
489
- await this.ensureBootstrap();
490
- }
491
- if (!this.sessionState) {
492
- return;
493
- }
494
-
495
- this.status = "booting";
496
- this.errorMessage = null;
497
- this.renderWidget();
498
-
499
- try {
500
- const result = await startWidgetConversation({
501
- agentId: this.agentId,
502
- });
503
- this.sessionState = result.session;
504
- this.messages = result.messages;
505
- this.status = "ready";
506
- this.renderWidget();
507
- } catch (error) {
508
- this.status = "error";
509
- this.errorMessage =
510
- error instanceof Error ? error.message : "Failed to start conversation";
511
- this.renderWidget();
512
- }
549
+ const welcome = this.getWelcomeMessage();
550
+ if (!welcome) return;
551
+ this.messages = [
552
+ {
553
+ id: this.getClientWelcomeId(),
554
+ conversationId: "",
555
+ role: "assistant",
556
+ content: welcome,
557
+ createdAt: new Date().toISOString(),
558
+ },
559
+ ];
513
560
  }
514
561
 
515
562
  private async handleStopConversation() {
@@ -14,6 +14,7 @@ import type { WidgetMessage } from "../types";
14
14
  import {
15
15
  splitWidgetPlacement,
16
16
  type WidgetAppearance,
17
+ type WidgetMobileHeight,
17
18
  type WidgetPlacement,
18
19
  type WidgetPreset,
19
20
  type WidgetVariant,
@@ -49,6 +50,11 @@ export type WidgetRuntimeProps = {
49
50
  welcomeMessage: string | null;
50
51
  spotlights: string[];
51
52
  ctaLink?: string | null;
53
+ /**
54
+ * How much of the screen the open panel covers on mobile. Ignored on
55
+ * desktop, where the panel floats above the launcher regardless.
56
+ */
57
+ mobileHeight?: WidgetMobileHeight;
52
58
  open: boolean;
53
59
  /**
54
60
  * When true, the embedded panel is pinned open and the launcher is hidden.
@@ -93,6 +99,7 @@ export function WidgetRuntime({
93
99
  welcomeMessage,
94
100
  spotlights,
95
101
  ctaLink,
102
+ mobileHeight = "full",
96
103
  open,
97
104
  alwaysOpen = false,
98
105
  status,
@@ -142,11 +149,13 @@ export function WidgetRuntime({
142
149
  ctaLink,
143
150
  actionLabel: appearance.actionText || undefined,
144
151
  showStopConversation: Boolean(onStopConversation),
152
+ mobileHeight,
145
153
  }),
146
154
  [
147
155
  agentAvatarUrl,
148
156
  appearance,
149
157
  ctaLink,
158
+ mobileHeight,
150
159
  onStopConversation,
151
160
  placement,
152
161
  preset,
@@ -227,6 +227,15 @@ export async function bootstrapWidget(input: {
227
227
  return bootstrapWidgetWithDeps(input, defaultBootstrapDeps);
228
228
  }
229
229
 
230
+ /**
231
+ * Send a user message. Lazy-starts the conversation on the first send —
232
+ * `startPublicChat` is called transparently when no `conversationId` is
233
+ * stored yet, so visitors never create empty conversations by opening
234
+ * the panel and walking away. When a start happens as part of this call
235
+ * the server-inserted welcome message rides back on `welcomeMessage`
236
+ * so the caller can reconcile the client-side placeholder bubble with
237
+ * the real DB row.
238
+ */
230
239
  export async function submitWidgetMessage(input: {
231
240
  agentId: string;
232
241
  content: string;
@@ -236,37 +245,52 @@ export async function submitWidgetMessage(input: {
236
245
  messages: WidgetMessage[];
237
246
  userMessage: WidgetMessage;
238
247
  assistantMessages: WidgetMessage[];
248
+ welcomeMessage?: WidgetMessage;
239
249
  }> {
240
- const stored = await ensureWidgetSession({
250
+ let stored = await ensureWidgetSession({
241
251
  agentId: input.agentId,
242
252
  pageUrl: window.location.href,
243
253
  referrer: document.referrer || undefined,
244
254
  });
245
- if (!stored.conversationId) {
246
- throw new Error("Widget conversation not started");
255
+
256
+ let welcomeMessage: WidgetMessage | undefined;
257
+ let conversationId: string;
258
+ if (stored.conversationId) {
259
+ conversationId = stored.conversationId;
260
+ } else {
261
+ const started = await startPublicChat({
262
+ agentId: input.agentId,
263
+ sessionToken: stored.sessionToken,
264
+ });
265
+ conversationId = started.conversation.id;
266
+ stored = { ...stored, conversationId };
267
+ saveSession(input.agentId, stored);
268
+ welcomeMessage = started.welcomeMessage;
247
269
  }
248
270
 
249
271
  const response = await sendPublicChatMessage({
250
- conversationId: stored.conversationId,
272
+ conversationId,
251
273
  sessionToken: stored.sessionToken,
252
274
  content: input.content,
253
275
  });
254
276
 
255
277
  const nextSession: WidgetSessionState = {
256
278
  ...stored,
257
- conversationId: stored.conversationId,
279
+ conversationId,
258
280
  };
259
281
  saveSession(input.agentId, nextSession);
260
282
 
261
283
  return {
262
284
  session: nextSession,
263
- conversationId: stored.conversationId,
285
+ conversationId,
264
286
  messages: sortByCreatedAt([
287
+ ...(welcomeMessage ? [welcomeMessage] : []),
265
288
  response.userMessage,
266
289
  ...response.assistantMessages,
267
290
  ]),
268
291
  userMessage: response.userMessage,
269
292
  assistantMessages: response.assistantMessages,
293
+ welcomeMessage,
270
294
  };
271
295
  }
272
296
 
@@ -337,39 +361,6 @@ export async function submitWidgetStopConfirmation(input: {
337
361
  };
338
362
  }
339
363
 
340
- export async function startWidgetConversation(input: {
341
- agentId: string;
342
- }): Promise<{
343
- session: WidgetSessionState;
344
- conversationId: string;
345
- messages: WidgetMessage[];
346
- welcomeMessage: WidgetMessage;
347
- }> {
348
- const stored = await ensureWidgetSession({
349
- agentId: input.agentId,
350
- pageUrl: window.location.href,
351
- referrer: document.referrer || undefined,
352
- });
353
-
354
- const response = await startPublicChat({
355
- agentId: input.agentId,
356
- sessionToken: stored.sessionToken,
357
- });
358
-
359
- const nextSession: WidgetSessionState = {
360
- ...stored,
361
- conversationId: response.conversation.id,
362
- };
363
- saveSession(input.agentId, nextSession);
364
-
365
- return {
366
- session: nextSession,
367
- conversationId: response.conversation.id,
368
- messages: sortByCreatedAt([response.welcomeMessage]),
369
- welcomeMessage: response.welcomeMessage,
370
- };
371
- }
372
-
373
364
  export async function resetWidgetSession(agentId: string): Promise<void> {
374
365
  clearSession(agentId);
375
366
  }
@@ -18,6 +18,27 @@ export type WidgetPreset = "default" | "facebook_messenger";
18
18
 
19
19
  export const DEFAULT_WIDGET_PRESET: WidgetPreset = "default";
20
20
 
21
+ /**
22
+ * How much of the screen the open panel covers on mobile. `full` is the
23
+ * edge-to-edge sheet; the others render a bottom drawer over the host page.
24
+ */
25
+ export type WidgetMobileHeight = "half" | "three-quarter" | "full";
26
+
27
+ export const DEFAULT_WIDGET_MOBILE_HEIGHT: WidgetMobileHeight = "full";
28
+
29
+ export function normalizeWidgetMobileHeight(
30
+ value: string | null | undefined,
31
+ ): WidgetMobileHeight {
32
+ switch (value) {
33
+ case "half":
34
+ case "three-quarter":
35
+ case "full":
36
+ return value;
37
+ default:
38
+ return DEFAULT_WIDGET_MOBILE_HEIGHT;
39
+ }
40
+ }
41
+
21
42
  export type WidgetAppearance = ResolvedChatWidgetAppearance;
22
43
 
23
44
  export const DEFAULT_WIDGET_APPEARANCE: WidgetAppearance =
package/src/types.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  WidgetAppearance,
3
3
  WidgetBehavior,
4
+ WidgetMobileHeight,
4
5
  WidgetPlacement,
5
6
  WidgetPreset,
6
7
  WidgetVariant,
@@ -10,14 +11,17 @@ import type {
10
11
  export {
11
12
  DEFAULT_WIDGET_APPEARANCE,
12
13
  DEFAULT_WIDGET_BEHAVIOR,
14
+ DEFAULT_WIDGET_MOBILE_HEIGHT,
13
15
  DEFAULT_WIDGET_PRESET,
14
16
  type WidgetAppearance,
15
17
  type WidgetBehavior,
18
+ type WidgetMobileHeight,
16
19
  type WidgetPlacement,
17
20
  type WidgetPreset,
18
21
  type WidgetVariant,
19
22
  normalizeWidgetAppearance,
20
23
  normalizeWidgetBehavior,
24
+ normalizeWidgetMobileHeight,
21
25
  normalizeWidgetPlacement,
22
26
  normalizeWidgetPreset,
23
27
  normalizeWidgetVariant,
@@ -41,6 +45,12 @@ export type WidgetSessionConfig = {
41
45
  widgetVariant: WidgetVariant;
42
46
  widgetPreset: WidgetPreset;
43
47
  widgetPlacement: WidgetPlacement;
48
+ /**
49
+ * Mobile presentation of the open panel. Optional on the wire so an
50
+ * embed running against an older API (which doesn't send the field)
51
+ * still boots — it falls back to the edge-to-edge default.
52
+ */
53
+ mobileHeight?: WidgetMobileHeight | null;
44
54
  allowedDomains: string[];
45
55
  triggerRule: WidgetTriggerRule | null;
46
56
  };
@@ -1,6 +0,0 @@
1
- /**
2
- * Default values for the chat widget when database / agent config is not set.
3
- */
4
-
5
- /** Default label for the open empty-state "start conversation" CTA. */
6
- export const DEFAULT_START_CONVERSATION_LABEL = "Start a new conversation";