@v-tilt/browser 1.13.0 → 1.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/dist/all-external-dependencies.js.map +1 -1
  2. package/dist/array.chat.js +2 -0
  3. package/dist/array.chat.js.map +1 -0
  4. package/dist/array.chat.no-external.js +2 -0
  5. package/dist/array.chat.no-external.js.map +1 -0
  6. package/dist/array.full.chat.js +2 -0
  7. package/dist/array.full.chat.js.map +1 -0
  8. package/dist/array.full.chat.no-external.js +2 -0
  9. package/dist/array.full.chat.no-external.js.map +1 -0
  10. package/dist/array.full.js +1 -1
  11. package/dist/array.full.js.map +1 -1
  12. package/dist/array.full.no-external.js +2 -0
  13. package/dist/array.full.no-external.js.map +1 -0
  14. package/dist/array.js +1 -1
  15. package/dist/array.js.map +1 -1
  16. package/dist/array.no-external.js +1 -1
  17. package/dist/array.no-external.js.map +1 -1
  18. package/dist/chat.js +1 -1
  19. package/dist/chat.js.map +1 -1
  20. package/dist/entrypoints/all-external-dependencies.d.ts +10 -3
  21. package/dist/entrypoints/array.chat.d.ts +10 -0
  22. package/dist/entrypoints/array.chat.no-external.d.ts +6 -0
  23. package/dist/entrypoints/array.full.chat.d.ts +13 -0
  24. package/dist/entrypoints/array.full.chat.no-external.d.ts +7 -0
  25. package/dist/entrypoints/array.full.d.ts +5 -9
  26. package/dist/entrypoints/array.full.no-external.d.ts +12 -0
  27. package/dist/entrypoints/module.chat.es.d.ts +7 -0
  28. package/dist/entrypoints/module.full.chat.es.d.ts +12 -0
  29. package/dist/entrypoints/module.full.es.d.ts +12 -0
  30. package/dist/entrypoints/module.no-external.es.d.ts +1 -0
  31. package/dist/extensions/chat/bubble-drag.d.ts +20 -5
  32. package/dist/extensions/chat/chat-wrapper.d.ts +8 -2
  33. package/dist/extensions/chat/chat.d.ts +21 -351
  34. package/dist/extensions/chat/controller/__tests__/fakes/ably-realtime-fake.d.ts +84 -0
  35. package/dist/extensions/chat/controller/ably-client.d.ts +160 -0
  36. package/dist/extensions/chat/controller/ably-handlers.d.ts +95 -0
  37. package/dist/extensions/chat/controller/ably-token.d.ts +67 -0
  38. package/dist/extensions/chat/controller/chat-controller.d.ts +194 -0
  39. package/dist/extensions/chat/controller/message-delivery-status.d.ts +6 -0
  40. package/dist/extensions/chat/controller/message-order.d.ts +12 -0
  41. package/dist/extensions/chat/controller/message-stream.d.ts +49 -0
  42. package/dist/extensions/chat/lib/bubble-offset.d.ts +18 -0
  43. package/dist/extensions/chat/lib/merge-chat-config.d.ts +3 -0
  44. package/dist/extensions/chat/normalize-send-content.d.ts +2 -0
  45. package/dist/extensions/chat/outbox/message-delivery.d.ts +17 -0
  46. package/dist/extensions/chat/outbox/message-outbox.d.ts +57 -0
  47. package/dist/extensions/chat/store/chat-store.d.ts +122 -0
  48. package/dist/extensions/chat/types.d.ts +1 -19
  49. package/dist/extensions/chat/ui/ChannelItem.d.ts +12 -0
  50. package/dist/extensions/chat/ui/ChannelListView.d.ts +14 -0
  51. package/dist/extensions/chat/ui/ChatBubble.d.ts +14 -0
  52. package/dist/extensions/chat/ui/ChatHeader.d.ts +13 -0
  53. package/dist/extensions/chat/ui/ChatPanel.d.ts +14 -0
  54. package/dist/extensions/chat/ui/ChatRoot.d.ts +31 -0
  55. package/dist/extensions/chat/ui/ClosedBanner.d.ts +7 -0
  56. package/dist/extensions/chat/ui/ConnectionBanner.d.ts +32 -0
  57. package/dist/extensions/chat/ui/ConversationView.d.ts +14 -0
  58. package/dist/extensions/chat/ui/MessageBubble.d.ts +25 -0
  59. package/dist/extensions/chat/ui/MessageInput.d.ts +14 -0
  60. package/dist/extensions/chat/ui/MessageList.d.ts +19 -0
  61. package/dist/extensions/chat/ui/NewMessagesPill.d.ts +13 -0
  62. package/dist/extensions/chat/ui/Skeletons.d.ts +14 -0
  63. package/dist/extensions/chat/ui/TypingBubble.d.ts +23 -0
  64. package/dist/extensions/chat/ui/TypingIndicator.d.ts +12 -0
  65. package/dist/extensions/chat/ui/WidgetSlot.d.ts +20 -0
  66. package/dist/extensions/chat/ui/hooks/use-auto-mark-read.d.ts +18 -0
  67. package/dist/extensions/chat/ui/hooks/use-scroll-preservation.d.ts +33 -0
  68. package/dist/extensions/chat/ui/icons.d.ts +18 -0
  69. package/dist/extensions/chat/ui/message-render.d.ts +18 -0
  70. package/dist/extensions/chat/ui/shadow-styles.d.ts +13 -0
  71. package/dist/extensions/chat/ui/utils/mobile-body-scroll-lock.d.ts +14 -0
  72. package/dist/extensions/chat/widget-registry.d.ts +21 -5
  73. package/dist/extensions/chat/widgets/collect-email.d.ts +4 -2
  74. package/dist/extensions/chat/widgets/escalate-to-human.d.ts +2 -1
  75. package/dist/external-scripts-loader.js +1 -1
  76. package/dist/external-scripts-loader.js.map +1 -1
  77. package/dist/feature.d.ts +1 -0
  78. package/dist/lib/merge-vtilt-config.d.ts +9 -0
  79. package/dist/main.js +1 -1
  80. package/dist/main.js.map +1 -1
  81. package/dist/module.chat.d.ts +1788 -0
  82. package/dist/module.chat.js +2 -0
  83. package/dist/module.chat.js.map +1 -0
  84. package/dist/module.d.ts +48 -3
  85. package/dist/module.full.chat.d.ts +1792 -0
  86. package/dist/module.full.chat.js +2 -0
  87. package/dist/module.full.chat.js.map +1 -0
  88. package/dist/module.full.d.ts +1793 -0
  89. package/dist/module.full.js +2 -0
  90. package/dist/module.full.js.map +1 -0
  91. package/dist/module.js +1 -1
  92. package/dist/module.js.map +1 -1
  93. package/dist/module.no-external.d.ts +48 -3
  94. package/dist/module.no-external.js +1 -1
  95. package/dist/module.no-external.js.map +1 -1
  96. package/dist/recorder.js.map +1 -1
  97. package/dist/snippet-stub-methods.d.ts +14 -0
  98. package/dist/utils/globals.d.ts +53 -27
  99. package/dist/utils/index.d.ts +8 -0
  100. package/dist/vtilt.d.ts +6 -1
  101. package/dist/web-vitals.js.map +1 -1
  102. package/package.json +74 -71
  103. package/dist/extensions/chat/chat-styles.d.ts +0 -27
  104. package/dist/extensions/chat/message-content-styles.d.ts +0 -1
  105. package/dist/extensions/chat/message-html.d.ts +0 -6
  106. package/dist/extensions/chat/message-markdown.d.ts +0 -8
  107. package/dist/extensions/ga4-proxy.d.ts +0 -59
  108. package/dist/utils/type-utils.d.ts +0 -4
  109. package/dist/web-vitals.d.ts +0 -81
@@ -1,8 +1,15 @@
1
1
  /**
2
- * All External Dependencies
2
+ * Lazy extension entrypoints bundled into `array.full*` / `module.full*` builds.
3
3
  *
4
- * This file imports all external dependency entrypoints.
5
- * Used by array.full.ts for the full bundle that includes everything.
4
+ * Registers factories on `window.__VTiltExtensions__` so `loadExternalDependency`
5
+ * is skipped when these bundles are used with `disable_external_dependency_loading`.
6
+ *
7
+ * **Chat is intentionally excluded** — same as PostHog's `all-external-dependencies.ts`
8
+ * (recorder, surveys, web-vitals, … but not `conversations`). Chat is ~60 KB gzip
9
+ * and not every site uses it. Opt in via:
10
+ * - `array.chat.js` / `module.chat.es.js` (core + chat only)
11
+ * - `array.full.chat.js` / `module.full.chat.es.js` (recorder + vitals + chat)
12
+ * - side-effect `import "./chat"` before `vt.init()` (bundler)
6
13
  */
7
14
  import "./recorder";
8
15
  import "./web-vitals";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Core snippet plus chat — equivalent to loading `array.js` and `chat.js`
3
+ * together. Chat registers on `__VTiltExtensions__` at parse time so there is
4
+ * no second network request for `chat.js`.
5
+ *
6
+ * Usage:
7
+ * <script src="https://cdn.example.com/dist/array.chat.js"></script>
8
+ */
9
+ import "./chat";
10
+ import "./array";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * `array.chat.js` with external script injection disabled at runtime.
3
+ * Pair with `disable_external_dependency_loading: true` in `vt.init()`.
4
+ */
5
+ import "./chat";
6
+ import "./array.no-external";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Full IIFE bundle with chat pre-registered on `__VTiltExtensions__`.
3
+ *
4
+ * Use when the chat widget must be ready immediately (hero CTAs, programmatic
5
+ * `vt.sendChatMessage()` on first paint) without a second script request.
6
+ *
7
+ * PostHog equivalent: load `array.js` and side-effect-import `conversations.js`,
8
+ * or use their bundler pattern:
9
+ * import "posthog-js/dist/conversations"
10
+ */
11
+ import "./all-external-dependencies";
12
+ import "./chat";
13
+ import "./array";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Full IIFE bundle with recorder, web-vitals, and chat pre-registered.
3
+ * Pair with `disable_external_dependency_loading: true`.
4
+ */
5
+ import "./all-external-dependencies";
6
+ import "./chat";
7
+ import "./array.no-external";
@@ -1,17 +1,13 @@
1
1
  /**
2
- * Full Bundle Entrypoint
3
- *
4
- * Same as array.ts but includes all external dependencies bundled in.
5
- * This is useful for users who want a single script tag without lazy loading.
2
+ * Full IIFE bundle — core SDK plus session recording and web vitals
3
+ * pre-registered on `__VTiltExtensions__`.
6
4
  *
7
5
  * Usage:
8
6
  * <script src="https://cdn.example.com/dist/array.full.js"></script>
9
7
  *
10
- * This bundle includes:
11
- * - Core SDK (array.no-external.ts)
12
- * - External scripts loader
13
- * - Session recording (rrweb)
14
- * - Web Vitals tracking
8
+ * Does **not** include chat (PostHog keeps conversations out of `array.full` too).
9
+ * For chat without a second request, use `array.chat.js` (core + chat) or
10
+ * `array.full.chat.js` (also includes recorder and web vitals).
15
11
  */
16
12
  import "./all-external-dependencies";
17
13
  import "./array";
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Full IIFE bundle with external script loading disabled at runtime.
3
+ *
4
+ * Same as `array.full.ts` but intended for use with
5
+ * `disable_external_dependency_loading: true` — extensions are already on
6
+ * `__VTiltExtensions__`. PostHog equivalent: `array.full.no-external.ts`.
7
+ *
8
+ * Does not include chat; use `array.full.chat.no-external.ts` when chat must
9
+ * be pre-registered without a separate network request.
10
+ */
11
+ import "./all-external-dependencies";
12
+ import "./array.no-external";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * ESM core bundle with chat pre-registered — bundler equivalent of `array.chat.js`.
3
+ */
4
+ import "./chat";
5
+ import vt from "./module.es";
6
+ export * from "./module.es";
7
+ export default vt;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Full ESM bundle with chat pre-registered — no extra `chat.js` fetch.
3
+ *
4
+ * PostHog equivalent:
5
+ * import "posthog-js/dist/conversations"
6
+ * import posthog from "posthog-js/dist/module.no-external"
7
+ */
8
+ import "./all-external-dependencies";
9
+ import "./chat";
10
+ import vt from "./module.es";
11
+ export * from "./module.es";
12
+ export default vt;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Full ESM bundle — core SDK plus recorder and web-vitals pre-registered on
3
+ * `__VTiltExtensions__`. Same role as PostHog's `module.full.es.ts`.
4
+ *
5
+ * Chat is not included (see `module.full.chat.es.ts` or side-effect-import
6
+ * `./chat` before init). Matches PostHog keeping `conversations` out of the
7
+ * default full bundle because of bundle size.
8
+ */
9
+ import "./all-external-dependencies";
10
+ import vt from "./module.es";
11
+ export * from "./module.es";
12
+ export default vt;
@@ -1,4 +1,5 @@
1
1
  export { VTilt } from "../vtilt";
2
2
  export * from "../types";
3
+ export { VTILT_SNIPPET_STUB_METHODS, VTILT_SNIPPET_STUB_METHOD_NAMES, type VtiltSnippetStubMethodName, } from "../snippet-stub-methods";
3
4
  export declare const vt: import("../vtilt").VTilt;
4
5
  export default vt;
@@ -8,6 +8,14 @@
8
8
  interface DraggableOptions {
9
9
  /** Element that initiates the drag (defaults to the target element itself) */
10
10
  handle?: HTMLElement;
11
+ /**
12
+ * Fired after the element settles at a new position: initial restore
13
+ * from localStorage, drag end, and viewport resize re-clamp. The chat
14
+ * widget uses this to recompute the panel's opening direction so the
15
+ * panel always extends into whichever side of the viewport has more
16
+ * room (see `usePanelAnchor` in `ui/ChatRoot.tsx`).
17
+ */
18
+ onPositionChange?: () => void;
11
19
  }
12
20
  interface DragHandle {
13
21
  destroy: () => void;
@@ -15,13 +23,20 @@ interface DragHandle {
15
23
  /**
16
24
  * Make a fixed-position element draggable.
17
25
  *
18
- * @param el The element to reposition via translate3d.
19
- * @param opts.handle Optional drag handle element (e.g. bubble inside a container).
20
- * Pointer events listen on the handle; transform applies to `el`.
21
- * Defaults to `el` itself.
26
+ * @param el The element to reposition via translate3d.
27
+ * @param opts.handle Optional drag handle element (e.g. bubble
28
+ * inside a container). Pointer events listen
29
+ * on the handle; transform applies to `el`.
30
+ * Defaults to `el` itself.
31
+ * @param opts.onPositionChange Called after the element settles —
32
+ * initial restore, drag end, and viewport
33
+ * resize re-clamp. Used by the chat widget
34
+ * to recompute the panel's opening
35
+ * direction.
22
36
  *
23
37
  * - Restores persisted position from localStorage on attach.
24
- * - Persists position on drag end.
38
+ * - Persists position on drag end (and on resize when the clamp had to
39
+ * correct an out-of-bounds value, so the next page load is consistent).
25
40
  * - Distinguishes click from drag via a 5px movement threshold.
26
41
  * When dragged, the subsequent `click` event on the handle is suppressed once.
27
42
  * - Re-clamps on viewport resize.
@@ -50,6 +50,7 @@ export declare class ChatWrapper extends LazyFeature<ChatConfig, LazyLoadedChatI
50
50
  private _connectionCallbacks;
51
51
  /** True after vt.showChat() so preloaded widget stays visible after replacing the lightweight bubble. */
52
52
  private _bubbleExplicitShow;
53
+ private _unreadPollIntervalId;
53
54
  constructor(instance: VTilt, config?: ChatConfig);
54
55
  static extractConfig(config: VTiltConfig): ChatConfig;
55
56
  get isEnabled(): boolean;
@@ -79,19 +80,24 @@ export declare class ChatWrapper extends LazyFeature<ChatConfig, LazyLoadedChatI
79
80
  selectChannel(channelId: string): void;
80
81
  createChannel(): void;
81
82
  goToChannelList(): void;
82
- sendMessage(content: SendChatMessageContent, options?: SendChatMessageOptions): void;
83
+ sendMessage(content: SendChatMessageContent, options?: SendChatMessageOptions): Promise<void>;
83
84
  markAsRead(): void;
84
85
  registerWidget(definition: Parameters<NonNullable<LazyLoadedChatInterface["registerWidget"]>>[0]): void;
85
86
  onMessage(callback: MessageCallback): Unsubscribe;
86
87
  onTyping(callback: TypingCallback): Unsubscribe;
87
88
  onConnectionChange(callback: ConnectionCallback): Unsubscribe;
88
- updateConfig(config: Partial<ChatConfig>): void;
89
+ private _isBubbleDraggable;
89
90
  getMergedConfig(): ChatConfig;
91
+ /** Apply corner + inset to the lightweight bubble and/or loaded shadow host. */
92
+ private _syncBubbleLayout;
90
93
  destroy(): void;
91
94
  private _fetchServerSettings;
92
95
  private _fetchUnreadForBubble;
93
96
  private _showBubble;
94
97
  private _onVisibilityChange;
98
+ private _shouldPollUnread;
99
+ private _startUnreadPoll;
100
+ private _stopUnreadPoll;
95
101
  private _schedulePreload;
96
102
  private _lazyLoadAndThen;
97
103
  }
@@ -1,353 +1,23 @@
1
1
  /**
2
- * Chat Widget - Lazy loaded chat implementation using Ably for real-time messaging.
2
+ * Chat Widget thin re-export shim.
3
+ *
4
+ * The entire chat implementation moved to a layered architecture as part of
5
+ * the May 2026 Preact rewrite. The previous monolithic ~3800-line class
6
+ * (`LazyLoadedChat`) is now decomposed into:
7
+ *
8
+ * - controller/ (chat-controller, ably-client, ably-handlers, ably-token,
9
+ * message-stream) — orchestration and Ably transport
10
+ * - store/ (chat-store) — signal-based reactive state
11
+ * - ui/ (ChatRoot.tsx + components + hooks)
12
+ * — Preact components rendered in a Shadow DOM
13
+ * - widgets/ (collect-email.tsx, escalate-to-human.tsx)
14
+ * — built-in tool widgets (Preact)
15
+ *
16
+ * See `docs/modules/chat/widget-architecture.md` for the full architecture.
17
+ *
18
+ * This file remains so external callers (`entrypoints/chat.ts`,
19
+ * `ChatWrapper`, tests) can keep importing `LazyLoadedChat` from
20
+ * `extensions/chat/chat` unchanged. New code should import the controller
21
+ * and store directly from their own modules.
3
22
  */
4
- import type { VTilt } from "../../vtilt";
5
- import { type ChatConfig, type ChatChannel, type ChatChannelSummary, type ChatWidgetView, type LazyLoadedChatInterface, type SendChatMessageContent, type SendChatMessageOptions } from "../../utils/globals";
6
- import { type MessageCallback, type TypingCallback, type ConnectionCallback, type Unsubscribe } from "./types";
7
- import { type WidgetContext } from "./widget-registry";
8
- export declare class LazyLoadedChat implements LazyLoadedChatInterface {
9
- private _instance;
10
- private _config;
11
- private _state;
12
- private _container;
13
- private _widget;
14
- private _bubble;
15
- private _bubbleDragHandle;
16
- private _chatTheme;
17
- private _widgetResizeCleanup;
18
- private _ably;
19
- private _notificationsChannel;
20
- private _ablyChannel;
21
- private _typingChannel;
22
- private _connectionState;
23
- private _ablyProjectId;
24
- /** Channel id included in the last per-channel Ably token authorize. */
25
- private _realtimeChannelId;
26
- /** Channel id we are connecting to (select/create in flight). */
27
- private _pendingRealtimeChannelId;
28
- private _messageCallbacks;
29
- private _typingCallbacks;
30
- private _connectionCallbacks;
31
- private _typingDebounce;
32
- private _isUserTyping;
33
- private _initialUserReadAt;
34
- private _isMarkingRead;
35
- private _widgetRegistry;
36
- /** Tracks whether show() was explicitly called (vs open() temporarily revealing the container). */
37
- private _bubbleExplicitShow;
38
- private _lastConnectedDistinctId;
39
- private _unsubscribeIdentity;
40
- private _unsubscribeReset;
41
- private _identityChangeInFlight;
42
- private _userScrolledUp;
43
- private _unreadNewMessagesCount;
44
- private _messagesScrollListenerBound;
45
- private _conversationListenersBound;
46
- private _channelListListenersBound;
47
- constructor(instance: VTilt, config?: ChatConfig);
48
- get isOpen(): boolean;
49
- get isConnected(): boolean;
50
- get isLoading(): boolean;
51
- get unreadCount(): number;
52
- get channel(): ChatChannel | null;
53
- get channels(): ChatChannelSummary[];
54
- get currentView(): ChatWidgetView;
55
- private get _theme();
56
- private get _distinctId();
57
- /**
58
- * Track a chat message event via the SDK capture pipeline.
59
- * Gated by remote config chatTracking settings.
60
- *
61
- * - User messages: only if chatTracking.trackUserMessages is enabled
62
- * - AI/Agent messages: only if chatTracking.trackAgentMessages is enabled
63
- */
64
- private _trackChatMessage;
65
- open(): void;
66
- close(): void;
67
- toggle(): void;
68
- show(): void;
69
- hide(): void;
70
- /**
71
- * Build a stub ChatChannel from a ChatChannelSummary so that
72
- * `selectChannel` can populate state synchronously while the real
73
- * `GET /channels/:id` response is in flight. The header reads `ai_mode`
74
- * and `status` immediately; remaining fields are filled by the response.
75
- */
76
- private _stubChannelFromSummary;
77
- /**
78
- * Handle a vTilt identity change (anon → identified, or user → user).
79
- *
80
- * Ably enforces that the connection's `clientId` is immutable for the
81
- * lifetime of a Realtime connection. Authorizing a token with a different
82
- * `clientId` puts the connection into the terminal `failed` state and
83
- * produces a 40102 error. The correct response is to fully close the
84
- * existing client and create a new one with a token bound to the new id.
85
- *
86
- * This also clears per-identity state (channels, messages, unread count)
87
- * since those belong to the previous user.
88
- */
89
- private _handleIdentityChange;
90
- getChannels(): Promise<void>;
91
- selectChannel(channelId: string): Promise<void>;
92
- createChannel(): Promise<void>;
93
- goToChannelList(): Promise<void>;
94
- sendMessage(content: SendChatMessageContent, options?: SendChatMessageOptions): Promise<void>;
95
- markAsRead(): void;
96
- /**
97
- * Send a silent trigger to the messages API after a widget action completes.
98
- * This makes the AI respond to the action (e.g., acknowledge email collection)
99
- * without creating a visible user message in the chat.
100
- */
101
- private _triggerAIAfterWidgetAction;
102
- private _autoMarkAsRead;
103
- private _isMessageReadByUser;
104
- /**
105
- * Calculate unread count based on messages and user_last_read_at
106
- * (unread agent/AI messages for the user)
107
- */
108
- private _calculateUnreadCount;
109
- onMessage(callback: MessageCallback): Unsubscribe;
110
- onTyping(callback: TypingCallback): Unsubscribe;
111
- onConnectionChange(callback: ConnectionCallback): Unsubscribe;
112
- destroy(): void;
113
- /**
114
- * Ensure Ably client is connected and subscribed to the project notifications channel.
115
- * Called when the widget opens. Safe to call multiple times.
116
- */
117
- private _ensureAblyConnected;
118
- /**
119
- * Request an Ably token for the current connection scope.
120
- * Pass channelId when subscribing to a conversation; omit on channel list.
121
- */
122
- private _refreshAblyToken;
123
- /**
124
- * Subscribe to per-channel messages and typing for a specific conversation.
125
- *
126
- * Attach is async; the user may close the widget or switch to a different
127
- * channel while we are still awaiting `attach()`. In that case
128
- * `_disconnectChannel()` runs concurrently — it nulls `this._ablyChannel`
129
- * and calls `detach()`, which makes Ably reject the pending `attach()` with
130
- * "Attach request superseded by a subsequent detach request". We treat
131
- * that race as expected: bail out silently instead of logging an error.
132
- */
133
- private _connectChannel;
134
- /**
135
- * Unsubscribe, detach, and release per-channel Ably channels.
136
- * Ably only allows release() when the channel is initialized, detached, or
137
- * failed — not when attached. So we detach first, then release.
138
- * Releasing removes the channel from the Ably internal registry so a
139
- * subsequent auth.authorize() with narrower capabilities won't try to
140
- * reattach it and fail with "Channel denied access."
141
- */
142
- private _disconnectChannel;
143
- /**
144
- * Fully disconnect from Ably — unsubscribe everything and close the connection.
145
- */
146
- private _disconnectAll;
147
- /**
148
- * Handle project-level notifications (new channels, updates, closes).
149
- * Updates channel list and badge count in real time.
150
- */
151
- private _handleNotification;
152
- private _handleNewMessage;
153
- private _handleTypingEvent;
154
- private _handleReadCursorEvent;
155
- private _notifyConnectionChange;
156
- private _createUI;
157
- private _attachEventListeners;
158
- /**
159
- * Mark a widget as submitted in local message metadata.
160
- * Called after a successful widget action — _renderWidgets() uses this
161
- * to show the confirmation UI instead of the input form.
162
- */
163
- private _markWidgetSubmitted;
164
- private _handleUserTyping;
165
- private _sendTypingIndicator;
166
- private _handleSend;
167
- private _updateUI;
168
- private _isMobile;
169
- private _containerBaseStyle;
170
- private _savedTransform;
171
- private _isMobileFullscreen;
172
- /**
173
- * Mobile fullscreen: make the *container* fullscreen instead of the widget.
174
- * Using position:fixed on the widget is unreliable because the container
175
- * may have a CSS transform (from drag) which creates a new containing block.
176
- * The container is a direct child of <body> so position:fixed is always safe.
177
- *
178
- * Both elements get their full style replaced atomically via setAttribute.
179
- * Display is set by _updateUI after this call.
180
- */
181
- private _applyMobileFullscreen;
182
- private _removeMobileFullscreen;
183
- private _scrollY;
184
- private _lockBodyScroll;
185
- /**
186
- * Ensure the opened widget:
187
- * 1. Covers the bubble (anchored at container bottom-right) for UI consistency.
188
- * 2. Stays fully inside the viewport by shifting its position when near edges (no size change).
189
- *
190
- * On mobile the CSS media query handles fullscreen positioning, so this is a no-op.
191
- */
192
- private _constrainWidgetToViewport;
193
- private _updateHeader;
194
- /**
195
- * Render the channel-list view with mode-aware diffing.
196
- *
197
- * Three modes share a single content container so the click delegation
198
- * stays bound across them:
199
- * - empty: no channels and not loading → welcome / CTA screen
200
- * - skeleton: no channels and loading → shimmer placeholders
201
- * - populated: 1+ channels → scaffold + diff items by id
202
- *
203
- * Only the populated mode preserves item DOM identity; the other two are
204
- * rare transient states where a full innerHTML write is cheap.
205
- */
206
- private _renderChannelList;
207
- /**
208
- * Wire a single delegated click handler on the list container so we don't
209
- * re-attach per-item listeners on every render.
210
- */
211
- private _setupChannelListDelegation;
212
- private _createChannelItemNode;
213
- private _updateChannelItemNode;
214
- /**
215
- * Compact fingerprint used to decide whether a channel-item DOM node needs
216
- * an update. Only includes fields that the rendered HTML depends on.
217
- */
218
- private _channelItemFingerprint;
219
- private _getEmptyListHTML;
220
- private _getListScaffoldHTML;
221
- private _getListSkeletonHTML;
222
- /**
223
- * Build a skeleton bubble that mirrors the shape of a real chat message
224
- * from `_getMessageHTML`: same avatar size (32×32), same bubble padding
225
- * (12px 16px), same asymmetric border-radius (20/20/20/4 incoming,
226
- * 20/20/4/20 outgoing), and a meta line underneath sized like the
227
- * "Sender · 12:34" footer. The shimmer fills the bubble interior — not
228
- * floating lines — so the placeholder reads as a "loading message"
229
- * rather than a generic content blob. Width varies per call so the
230
- * stack of bubbles has visual rhythm instead of looking like a parade
231
- * of identical rectangles.
232
- */
233
- private _getSkeletonBubbleHTML;
234
- /**
235
- * Skeleton placeholder shown while `selectChannel` is loading the
236
- * messages of an existing conversation. Three alternating bubbles
237
- * (incoming → outgoing → incoming) imply history is on its way.
238
- * Bubble widths vary to avoid the "fake repeated rectangles" look.
239
- */
240
- private _getMessageSkeletonHTML;
241
- /**
242
- * New-conversation loader shown while `createChannel` (or
243
- * `sendMessage({channel: 'new'})`) is in flight. A single incoming
244
- * bubble using *exactly* the same shape, padding, border-radius, and
245
- * avatar treatment as a real incoming message from `_getMessageHTML`
246
- * — so when the real greeting arrives it morphs in place rather than
247
- * snapping to a new layout. The avatar uses the live AI/Support color
248
- * + icon (identity is known from `_config.aiMode`), and the bubble
249
- * carries three animated typing dots instead of text, signalling
250
- * "the assistant is preparing your greeting."
251
- */
252
- private _getNewChannelLoaderHTML;
253
- private _getChannelItemHTML;
254
- private _getConversationHTML;
255
- private _attachConversationListeners;
256
- private _formatRelativeTime;
257
- /**
258
- * Render messages with a key-based diff.
259
- *
260
- * - Each message node carries `data-msg-key="msg:<id>"` and a fingerprint;
261
- * if both match what we already have, the node is left untouched.
262
- * - The "New" divider lives as a sibling node with its own key so it
263
- * participates in the same diff (no flicker when it moves).
264
- * - Scroll position is preserved: we only snap to the bottom when the user
265
- * was already near the bottom or just sent their own message. Otherwise
266
- * we surface a "↓ N new messages" pill instead of yanking them down.
267
- * - Newly-inserted message nodes get the `vtilt-msg-enter` class for a
268
- * subtle fade-in animation. The initial bulk render skips animation.
269
- */
270
- private _renderMessages;
271
- /**
272
- * Build the "New" divider node (rendered above the first unread agent/AI
273
- * message when scrolled into view).
274
- */
275
- private _createMessageDividerNode;
276
- /**
277
- * Build a single message DOM node from the message model.
278
- */
279
- private _createMessageNode;
280
- /**
281
- * Compact fingerprint used to decide whether a message node needs an update.
282
- * Streaming AI responses update the content field many times — re-rendering
283
- * the same DOM lets the cursor and layout stay stable for the user.
284
- */
285
- private _messageFingerprint;
286
- /**
287
- * Wire (once per container) a scroll listener that tracks whether the user
288
- * is reading history above the latest message. Used to suppress auto-scroll
289
- * and to dismiss the "new messages" pill when the user scrolls back down.
290
- */
291
- private _setupMessagesScrollListener;
292
- private _isNearBottom;
293
- private _showNewMessagesPill;
294
- private _hideNewMessagesPill;
295
- private _getMessageHTML;
296
- private _isMessageReadByAgent;
297
- /**
298
- * Handle streaming AI response
299
- * Industry standard: Real-time streaming with progressive UI updates
300
- */
301
- private _handleStreamingResponse;
302
- /**
303
- * Remove temporary message by ID
304
- */
305
- private _removeTempMessage;
306
- private _getTimeOpen;
307
- /**
308
- * Bubble body for any message — plain escaped text or sanitized rich HTML,
309
- * using the same rules for user (html/markdown) and agent/AI messages.
310
- */
311
- private _renderMessageContent;
312
- private _renderMessageHtml;
313
- /**
314
- * Render widgets for a message. Widget data sources (checked in order):
315
- * 1. metadata._widgets -- set during streaming (temp messages)
316
- * 2. metadata.widgets -- persisted in DB (loaded on refresh, received via Ably)
317
- * 3. Content markers -- fallback for legacy messages with <!--vtilt:widget:...--> in content
318
- *
319
- * If a widget has submitted=true in metadata, render the confirmation instead of the input.
320
- */
321
- private _renderWidgets;
322
- /**
323
- * Build WidgetContext for widget renderers.
324
- */
325
- private _getWidgetContext;
326
- /**
327
- * Extract widget blocks from streamed text, strip them, and return parsed widgets.
328
- */
329
- private _parseWidgetBlocks;
330
- /**
331
- * Public method for SDK consumers to register custom widgets.
332
- */
333
- registerWidget(definition: {
334
- type: string;
335
- toolDescription: string;
336
- parameters: Record<string, {
337
- type: string;
338
- description: string;
339
- }>;
340
- render: (params: Record<string, unknown>, context: WidgetContext) => string;
341
- onAction: (action: string, data: Record<string, unknown>, context: WidgetContext) => Promise<{
342
- replaceHTML?: string;
343
- sendMessage?: string;
344
- }>;
345
- }): void;
346
- private _escapeHTML;
347
- /**
348
- * Get a human-readable preview for a message (for channel list).
349
- * Falls back to widget labels if content is empty (tool-only messages).
350
- */
351
- private _getMessagePreview;
352
- private _formatTime;
353
- }
23
+ export { ChatController as LazyLoadedChat } from "./controller/chat-controller";