@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
@@ -0,0 +1,84 @@
1
+ /**
2
+ * In-memory fake of Ably's `BaseRealtime` surface used by AblyClient tests.
3
+ *
4
+ * Only models the parts the chat layer actually touches:
5
+ * - `connection` with `state` + `.on(event, handler)` events
6
+ * for "connected" | "disconnected" | "suspended" | "failed"
7
+ * - `auth.clientId`, `auth.authorize(token)`
8
+ * - `channels.get(name)` returning a FakeRealtimeChannel with attach/detach,
9
+ * subscribe/unsubscribe/publish, and a `state` machine that can be driven
10
+ * programmatically.
11
+ * - `channels.release(name)`
12
+ * - `connect()` / `close()`
13
+ *
14
+ * The Ably typings are wide and surface lots of unrelated APIs; rather than
15
+ * fight them we expose this fake with its own minimal types and only cast
16
+ * to `BaseRealtime` at the construction site.
17
+ */
18
+ import type { ChannelState, ChannelStateChange, TokenRequest } from "ably";
19
+ import type { BaseRealtime } from "ably/modular";
20
+ type Handler<T> = (event: T) => void;
21
+ export declare class FakeRealtimeChannel {
22
+ readonly name: string;
23
+ state: ChannelState;
24
+ readonly subscriptions: Map<string, ((msg: {
25
+ data: unknown;
26
+ }) => void)[]>;
27
+ readonly published: Array<{
28
+ event: string;
29
+ data: unknown;
30
+ }>;
31
+ attachCalls: number;
32
+ detachCalls: number;
33
+ private _stateListeners;
34
+ private _attachResolvers;
35
+ constructor(name: string);
36
+ attach(): Promise<void>;
37
+ /** Test helper to resolve a pending attach. */
38
+ resolveAttach(): void;
39
+ /** Test helper to reject a pending attach. */
40
+ rejectAttach(err: unknown): void;
41
+ detach(): Promise<void>;
42
+ setState(next: ChannelState): void;
43
+ on(handler: Handler<ChannelStateChange>): void;
44
+ off(handler: Handler<ChannelStateChange>): void;
45
+ subscribe(event: string, callback: (msg: {
46
+ data: unknown;
47
+ }) => void): void;
48
+ unsubscribe(): void;
49
+ publish(event: string, data: unknown): Promise<void>;
50
+ /** Drive an inbound message for tests. */
51
+ emit(event: string, data: unknown): void;
52
+ }
53
+ export declare class FakeAuth {
54
+ clientId: string | null;
55
+ authorizeCalls: number;
56
+ shouldFailAuthorize: boolean;
57
+ authorize(token: TokenRequest): Promise<void>;
58
+ }
59
+ export type ConnectionState = "initialized" | "connecting" | "connected" | "disconnected" | "suspended" | "closing" | "closed" | "failed";
60
+ export declare class FakeConnection {
61
+ state: ConnectionState;
62
+ private _listeners;
63
+ on(event: string, handler: Handler<unknown>): void;
64
+ emit(event: ConnectionState): void;
65
+ }
66
+ export declare class FakeChannels {
67
+ releaseCalls: string[];
68
+ private _registry;
69
+ get(name: string): FakeRealtimeChannel;
70
+ release(name: string): void;
71
+ }
72
+ export declare class FakeBaseRealtime {
73
+ connectCalls: number;
74
+ closeCalls: number;
75
+ readonly connection: FakeConnection;
76
+ readonly auth: FakeAuth;
77
+ readonly channels: FakeChannels;
78
+ constructor();
79
+ connect(): void;
80
+ close(): void;
81
+ /** Cast helper to satisfy AblyClient's constructor signature. */
82
+ asBaseRealtime(): BaseRealtime;
83
+ }
84
+ export {};
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Ably client wrapper for the chat widget.
3
+ *
4
+ * Owns the `BaseRealtime` lifecycle (connect/disconnect), the channel
5
+ * registry (notifications + per-conversation message + per-conversation
6
+ * typing), and the race-safe attach/detach flow.
7
+ *
8
+ * Writes connection state into the store; reads channel routing from the
9
+ * store. Does NOT mutate `messages` / `channels` / `typing` signals
10
+ * directly — those flow through the `applyMessage` / `applyNotification`
11
+ * / `applyTyping` handlers in `ably-handlers.ts`.
12
+ *
13
+ * See: docs/modules/chat/widget-architecture.md (Ably layer)
14
+ */
15
+ import { BaseRealtime } from "ably/modular";
16
+ import type { ApiRequestInstance } from "../chat-api";
17
+ import type { ChatStore } from "../store/chat-store";
18
+ import { type TypingPayload } from "./ably-handlers";
19
+ /**
20
+ * Hooks the controller plugs into the client so domain decisions stay in
21
+ * the controller (e.g. how to react to an inbound message). The client
22
+ * only owns the Ably mechanics.
23
+ */
24
+ export interface AblyClientHooks {
25
+ getDistinctId: () => string;
26
+ onIdentityChange: () => void;
27
+ onNewChannelNotification: () => void;
28
+ onAgentMessageWhileOpen: () => void;
29
+ onMessageTrack: (m: import("../../../utils/globals").ChatMessage) => void;
30
+ onMessageDispatched: (m: import("../../../utils/globals").ChatMessage) => void;
31
+ onTypingDispatched: (isTyping: boolean, senderName: string) => void;
32
+ }
33
+ /**
34
+ * Wire `BaseRealtime` lazily — keeps tests free of the constructor side
35
+ * effect of opening a WebSocket.
36
+ */
37
+ export type BaseRealtimeFactory = (options: ConstructorParameters<typeof BaseRealtime>[0]) => BaseRealtime;
38
+ export declare class AblyClient {
39
+ private readonly instance;
40
+ private readonly store;
41
+ private readonly hooks;
42
+ private _client;
43
+ private _notificationsChannel;
44
+ private _conversationChannel;
45
+ private _typingChannel;
46
+ /**
47
+ * Latest in-flight pre-attach teardown. `_doAttach` must `await` this so a
48
+ * newly-requested attach never calls `auth.authorize()` while a previous
49
+ * channel is still attached — Ably re-validates ALL attached channels
50
+ * against the new token and the previous one would fail with 40160.
51
+ */
52
+ private _teardownInFlight;
53
+ private _factory;
54
+ constructor(instance: ApiRequestInstance, store: ChatStore, hooks: AblyClientHooks, factory?: BaseRealtimeFactory);
55
+ /**
56
+ * Expose the live clientId (used by `authCallback` mismatch guard and by
57
+ * the controller's identity-change detector on the `connected` event).
58
+ */
59
+ get clientId(): string | null;
60
+ /**
61
+ * Whether we already own a live (or live-ish) client. Multiple calls to
62
+ * `ensureConnected()` are idempotent — the first one creates the client,
63
+ * the rest no-op.
64
+ */
65
+ get isLive(): boolean;
66
+ /** Currently-attached per-conversation channel id (null on list view). */
67
+ get currentChannelId(): string | null;
68
+ /**
69
+ * Idempotently bootstrap the Ably client and attach to the project
70
+ * notifications channel. Safe to call from `open()` and on first load.
71
+ */
72
+ ensureConnected(): Promise<void>;
73
+ /**
74
+ * Refresh the Ably token, optionally scoping it to a specific channel.
75
+ * Refuses to authorize on a `failed` connection (terminal — only fix is
76
+ * to recreate the client, which the identity-change hook does).
77
+ */
78
+ refreshToken(channelId?: string): Promise<boolean>;
79
+ /**
80
+ * Subscribe to per-conversation messages + typing for `channelId`.
81
+ *
82
+ * Atomic swap semantics: `realtimeChannelId` and `realtimeAttached`
83
+ * still reflect the **last successful attach** for the entire duration
84
+ * of this method — they only flip to `channelId` after both the main
85
+ * and typing channels have attached and subscribed. This means
86
+ * `realtimeReady` (and therefore the "Connecting…" banner) does not
87
+ * blink during normal channel switches; the worst case is a slightly
88
+ * stale `realtimeChannelId` for a few ms while the new channel
89
+ * negotiates, which is harmless because:
90
+ *
91
+ * - Stale inbound frames are filtered by the `_conversationChannel !==
92
+ * mainChannel` ref guard inside the subscribe callbacks.
93
+ * - `realtimeReady` already requires `realtimeChannelId === channel.value.id`,
94
+ * so once the new channel.value is set the computed signal correctly
95
+ * reads false until the swap completes.
96
+ *
97
+ * Concurrent navigations (rapid switch / goToChannelList) are caught
98
+ * by `wasSuperseded()` and the `pendingRealtimeChannelId` guard before
99
+ * the final write.
100
+ *
101
+ * Before refreshing the token this method awaits any previously-attached
102
+ * conversation channels' detach, so the `auth.authorize()` call in
103
+ * `refreshToken()` cannot trigger an Ably 40160 capability check against
104
+ * the old channel.
105
+ */
106
+ attachConversation(channelId: string): Promise<void>;
107
+ /**
108
+ * Detach + release the per-conversation channels. Safe to call from
109
+ * `close()`, `goToChannelList()`, and rapid switching paths.
110
+ *
111
+ * Tracks the in-flight teardown on `_teardownInFlight` so the next
112
+ * `attachConversation()` can await it before refreshing the token.
113
+ */
114
+ detachConversation(): Promise<void>;
115
+ /** Synchronously clear the channel refs and kick off async detach.
116
+ *
117
+ * Does NOT touch `realtimeChannelId` / `realtimeAttached` — those reflect
118
+ * the last successful attach and are owned by `attachConversation()`
119
+ * (which swaps them atomically when the new channel is fully ready) and
120
+ * by `disconnectAll()` (which clears them on full teardown). Leaving
121
+ * them intact during a channel-switch detach is what kills the
122
+ * "Connecting…" flash; see widget-architecture.md §"Atomic realtime
123
+ * swap". */
124
+ private _teardownActive;
125
+ /** Publish a typing event on the active typing channel. Best-effort. */
126
+ publishTyping(payload: TypingPayload & {
127
+ sender_id: string;
128
+ }): void;
129
+ /**
130
+ * Full teardown — detach all channels, close the Ably connection, reset
131
+ * the store's realtime signals. Used on `destroy()` and identity change.
132
+ */
133
+ disconnectAll(): Promise<void>;
134
+ /**
135
+ * After a `connected` event, if the live clientId no longer matches the
136
+ * distinct id we authorized for (e.g. the page slept through an identity
137
+ * change), trigger a full client recreate.
138
+ */
139
+ private _checkClientIdDrift;
140
+ /**
141
+ * Unsubscribe + detach. We intentionally do NOT call
142
+ * `client.channels.release()` here.
143
+ *
144
+ * Ably's `channels.release(name)` removes the channel object from the
145
+ * client's local registry. Any inbound WebSocket frame that arrives for
146
+ * that name afterwards triggers the library's
147
+ * "received event for non-existent channel" warning, because dispatch
148
+ * looks the channel up in the same registry. In-flight typing and
149
+ * message frames are easy to land after a detach on a rapid switch, so
150
+ * release-on-switch deterministically surfaces the warning.
151
+ *
152
+ * Per Ably's guidance, `release()` is for permanent cleanup. For
153
+ * conversation switching we just `unsubscribe()` + `detach()`; the
154
+ * channel object stays in the registry with no listeners and any
155
+ * residual frames are silently ignored. Full cleanup happens in
156
+ * `disconnectAll()` via `client.close()`, which tears down the
157
+ * connection without per-channel release.
158
+ */
159
+ private _teardownChannel;
160
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Pure functions that translate inbound Ably events into store mutations.
3
+ *
4
+ * No DOM, no fetch, no Ably types — only the wire shapes of the messages
5
+ * Ably delivers to us. Easy to unit-test against a fake `BaseRealtime`.
6
+ *
7
+ * See: docs/modules/chat/widget-architecture.md (Message-arrival contract)
8
+ */
9
+ import type { ChatMessage } from "../../../utils/globals";
10
+ import type { ChatStore } from "../store/chat-store";
11
+ export interface NotificationPayload {
12
+ type: string;
13
+ channel_id: string;
14
+ data?: Record<string, unknown>;
15
+ }
16
+ export interface TypingPayload {
17
+ sender_type: string;
18
+ sender_name: string | null;
19
+ is_typing: boolean;
20
+ }
21
+ export interface ReadCursorPayload {
22
+ reader_type: "user" | "agent";
23
+ reader_id: string | null;
24
+ read_at: string;
25
+ }
26
+ /**
27
+ * Insert or replace a message in the array. Pure id-based reconciliation:
28
+ * because the SDK mints the user message id client-side and the AI bubble
29
+ * adopts the persisted id from the streaming response header, every
30
+ * inbound payload arrives with the same identity the bubble already
31
+ * displays — no content+sender heuristic, no temp-id matching.
32
+ *
33
+ * Two special cases preserved:
34
+ *
35
+ * 1. Streaming `_widgets` metadata: if the local bubble accumulated
36
+ * `_widgets` (from inline tool-call markers) and the Ably echo doesn't
37
+ * carry an authoritative `widgets`, we merge the local `_widgets`
38
+ * into the persisted row so the form / button doesn't disappear.
39
+ *
40
+ * 2. Legacy `temp-ai-*` fallback: very old servers (or a stream that
41
+ * failed to pre-create its row) leave the SDK with a `temp-ai-*` id.
42
+ * When a persisted AI message arrives we swap the first such row so
43
+ * the user still sees a single bubble.
44
+ *
45
+ * Returns the **same array reference** when nothing actually changed
46
+ * (incoming matches an existing row by deep field equality). This lets
47
+ * memoised components (MessageBubble) skip re-renders on duplicate
48
+ * echoes, which is critical for streaming throughput where the same
49
+ * message id can be re-published multiple times.
50
+ */
51
+ export declare function upsertMessage(messages: ChatMessage[], incoming: ChatMessage): ChatMessage[];
52
+ /** Returns true when the message id is already in the array. */
53
+ export declare function hasMessage(messages: ChatMessage[], id: string): boolean;
54
+ /**
55
+ * Apply an inbound chat message to the store. Performs the upsert, updates
56
+ * unread counts (skipping when the user is currently viewing this channel),
57
+ * and clears the typing indicator.
58
+ *
59
+ * `currentUserDistinctId` is consulted only to suppress duplicate side
60
+ * effects on the user's own echoes — the upsert itself is pure id-based.
61
+ *
62
+ * `onAgentMessageWhileOpen` is invoked when an agent/AI message arrives
63
+ * while the widget is open and viewing this channel — used by the
64
+ * controller to schedule `markAsRead`.
65
+ *
66
+ * `onTrack` is invoked for agent/AI messages so the controller can forward
67
+ * to the SDK's `capture()` pipeline and `config.onMessageReceived` callback.
68
+ */
69
+ export declare function applyMessage(store: ChatStore, incoming: ChatMessage, currentUserDistinctId: string | null, callbacks: {
70
+ onAgentMessageWhileOpen: () => void;
71
+ onTrack: (m: ChatMessage) => void;
72
+ onMessageDispatched: (m: ChatMessage) => void;
73
+ }): void;
74
+ /**
75
+ * Handle project-level notifications (new channels, channel updates, closes).
76
+ * Updates the channel list and total badge count in real time without
77
+ * touching `messages` — message-list updates flow through the per-channel
78
+ * Ably channel.
79
+ */
80
+ export declare function applyNotification(store: ChatStore, notification: NotificationPayload, callbacks: {
81
+ onNewChannel: () => void;
82
+ }): void;
83
+ /**
84
+ * Apply an inbound typing event. The user's own typing events are echoed
85
+ * back from Ably and must be ignored here so the indicator only reflects
86
+ * the other party.
87
+ */
88
+ export declare function applyTyping(store: ChatStore, event: TypingPayload, callbacks: {
89
+ onTyping: (isTyping: boolean, senderName: string) => void;
90
+ }): void;
91
+ /**
92
+ * Apply an agent read-cursor advance. User cursors are not needed in the
93
+ * widget because the widget *is* the user.
94
+ */
95
+ export declare function applyReadCursor(store: ChatStore, event: ReadCursorPayload): void;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Ably token provisioning for the chat widget.
3
+ *
4
+ * - `requestToken()` performs the initial token request before the Ably
5
+ * client is created.
6
+ * - `createAuthCallback()` builds the `authCallback` Ably calls when it
7
+ * needs to refresh or scope a token (e.g. attaching to a per-channel
8
+ * subscription). The callback guards against clientId mismatches (40102)
9
+ * that would land the connection in the terminal `failed` state.
10
+ *
11
+ * No DOM, no Ably types — just `fetch` + the wire payload + a thin
12
+ * `IdentityChangeHook` callback for the rare clientId drift case.
13
+ *
14
+ * See: docs/modules/chat/widget-architecture.md (Race-free identity changes)
15
+ */
16
+ import type { TokenRequest } from "ably";
17
+ import { type ApiRequestInstance } from "../chat-api";
18
+ export interface InitialTokenResponse {
19
+ tokenRequest: TokenRequest;
20
+ clientId: string;
21
+ projectId: number;
22
+ }
23
+ /** Wire payload returned by the widget token endpoint. */
24
+ interface RawTokenResponse {
25
+ success: boolean;
26
+ tokenRequest: TokenRequest;
27
+ clientId?: string;
28
+ project_id?: number;
29
+ }
30
+ /**
31
+ * Request an Ably token from the widget endpoint.
32
+ *
33
+ * Returns `null` when the request fails — the caller should bail out and
34
+ * leave the store in `connectionState: "failed"`.
35
+ */
36
+ export declare function requestToken(instance: ApiRequestInstance, distinctId: string, channelId?: string): Promise<RawTokenResponse | null>;
37
+ /**
38
+ * Request the initial token used to bootstrap the Ably client. Asserts that
39
+ * `clientId` and `project_id` are present, since both are required to
40
+ * authorize the connection.
41
+ */
42
+ export declare function requestInitialToken(instance: ApiRequestInstance, distinctId: string): Promise<InitialTokenResponse | null>;
43
+ /**
44
+ * Provide the live Ably client's clientId. Returning `null` short-circuits
45
+ * the mismatch guard (used in tests / before the client exists).
46
+ */
47
+ export type LiveClientIdAccessor = () => string | null;
48
+ /** Notify the controller that a clientId drift was detected. */
49
+ export type IdentityChangeHook = () => void;
50
+ /**
51
+ * Build an Ably `authCallback`. Each refresh:
52
+ *
53
+ * 1. Calls the widget token endpoint with the *current* channel scope
54
+ * (derived from the store's `realtimeChannelId` / `pendingRealtimeChannelId`).
55
+ * 2. Verifies the token's `clientId` matches the live connection's
56
+ * `clientId`. If they differ, refuses the token and fires the identity
57
+ * change hook — refusing here prevents Ably from transitioning to the
58
+ * terminal `failed` state.
59
+ */
60
+ export declare function createAuthCallback(args: {
61
+ instance: ApiRequestInstance;
62
+ getDistinctId: () => string;
63
+ getChannelScope: () => string | null;
64
+ getLiveClientId: LiveClientIdAccessor;
65
+ onIdentityChange: IdentityChangeHook;
66
+ }): (_: unknown, cb: (err: string | null, token: TokenRequest | null) => void) => Promise<void>;
67
+ export {};
@@ -0,0 +1,194 @@
1
+ /**
2
+ * ChatController — orchestrates the chat widget.
3
+ *
4
+ * - Implements `LazyLoadedChatInterface` (the public SDK surface)
5
+ * - Owns the ChatStore (state)
6
+ * - Owns the AblyClient (realtime transport)
7
+ * - Performs HTTP calls (channel CRUD, message send, mark-read)
8
+ * - Mounts the Preact UI inside a Shadow DOM via `mountChatUI()`
9
+ *
10
+ * No DOM logic lives here beyond mount/unmount — components read signals
11
+ * directly and emit user intent back to the controller through props.
12
+ *
13
+ * See: docs/modules/chat/widget-architecture.md
14
+ */
15
+ import { type ChatChannel, type ChatChannelSummary, type ChatConfig, type ChatWidgetView, type LazyLoadedChatInterface, type SendChatMessageContent, type SendChatMessageOptions } from "../../../utils/globals";
16
+ import type { VTilt } from "../../../vtilt";
17
+ import { type ConnectionCallback, type MessageCallback, type TypingCallback, type Unsubscribe } from "../types";
18
+ import { ChatWidgetRegistry, type ChatWidgetDefinition, type WidgetContext } from "../widget-registry";
19
+ import { ChatStore, DEFAULT_THEME } from "../store/chat-store";
20
+ export declare class ChatController implements LazyLoadedChatInterface {
21
+ private _instance;
22
+ private _config;
23
+ private _store;
24
+ private _ably;
25
+ private _ui;
26
+ private _registry;
27
+ private _messageCallbacks;
28
+ private _typingCallbacks;
29
+ private _connectionCallbacks;
30
+ private _lastConnectedNotified;
31
+ private _unsubscribeIdentity;
32
+ private _unsubscribeReset;
33
+ private _identityChangeInFlight;
34
+ private _typingIdleTimer;
35
+ private _isUserTyping;
36
+ private _lastTypingPublishAt;
37
+ private _lastTypingActivityAt;
38
+ private _pendingTypingIntent;
39
+ private _disposeRealtimeReadyWatcher;
40
+ private _isMarkingRead;
41
+ private _streamAbort;
42
+ private _streamingMessageId;
43
+ /** Local typing dot shown before the HTTP response headers arrive. */
44
+ private _optimisticAiTyping;
45
+ private _openedAt;
46
+ private _disposeConnectionWatcher;
47
+ private _outbox;
48
+ /** Distinct id the active outbox storage key was opened for. */
49
+ private _outboxDistinctId;
50
+ private _drainInFlight;
51
+ private _drainTimer;
52
+ private readonly _onOutboxOnline;
53
+ private readonly _onOutboxVisible;
54
+ constructor(instance: VTilt, config?: ChatConfig);
55
+ get isOpen(): boolean;
56
+ get isConnected(): boolean;
57
+ get isLoading(): boolean;
58
+ get unreadCount(): number;
59
+ get channel(): ChatChannel | null;
60
+ get channels(): ChatChannelSummary[];
61
+ get currentView(): ChatWidgetView;
62
+ /** Exposed so components can read the live store inside JSX. */
63
+ get store(): ChatStore;
64
+ /** Exposed so the widget slot can look up custom definitions. */
65
+ get widgets(): ChatWidgetRegistry;
66
+ get theme(): typeof DEFAULT_THEME;
67
+ get config(): ChatConfig;
68
+ open(): void;
69
+ close(): void;
70
+ /** Drop per-channel subs and close Ably — badge while closed uses Tier A REST. */
71
+ private _teardownRealtimeOnClose;
72
+ /**
73
+ * Tier A closed-badge: apply aggregate unread from `GET /api/chat/widget/unread`
74
+ * (called by `chat-wrapper` poll / visibility refresh).
75
+ */
76
+ applyPolledUnreadCount(count: number): void;
77
+ toggle(): void;
78
+ private _bubbleExplicitShow;
79
+ show(): void;
80
+ hide(): void;
81
+ getChannels(): Promise<void>;
82
+ selectChannel(channelId: string): Promise<void>;
83
+ /**
84
+ * Create a new conversation channel.
85
+ *
86
+ * `options.skipGreeting` tells the server not to seed the AI welcome
87
+ * message (used when the user is initiating the conversation via
88
+ * `sendChatMessage` and their own message will arrive immediately after).
89
+ *
90
+ * `options.preserveMessages` keeps any messages already in the store
91
+ * (e.g. an optimistic user temp message inserted before the create call)
92
+ * and merges them with the server response — server messages first, then
93
+ * any local `temp-` rows we still have. This is what powers the
94
+ * "show the user's message instantly while the channel is being
95
+ * created" UX path.
96
+ */
97
+ createChannel(options?: {
98
+ skipGreeting?: boolean;
99
+ preserveMessages?: boolean;
100
+ }): Promise<void>;
101
+ goToChannelList(): void;
102
+ /** Abort the in-flight AI stream and remove its partial bubble. */
103
+ private _cancelActiveStream;
104
+ /**
105
+ * Remove AI bubbles trailing the last user message — used when a new send
106
+ * aborts an in-flight stream before `onStreamStart` registered its id.
107
+ */
108
+ private _stripTrailingAiAfterLastUser;
109
+ /** Show the AI typing bubble while waiting for stream headers / first token. */
110
+ private _showOptimisticAiTyping;
111
+ private _clearOptimisticAiTyping;
112
+ sendMessage(content: SendChatMessageContent, options?: SendChatMessageOptions): Promise<void>;
113
+ /** Retry a failed outbound message (tap-to-retry in the bubble meta). */
114
+ retryFailedMessage(messageId: string): void;
115
+ markAsRead(): void;
116
+ /**
117
+ * Send a silent trigger to the messages API after a widget action so the
118
+ * AI follows up (e.g. acknowledges email collection) without creating a
119
+ * visible user message in the chat.
120
+ */
121
+ triggerAIAfterWidgetAction(channelId: string): Promise<void>;
122
+ onMessage(callback: MessageCallback): Unsubscribe;
123
+ onTyping(callback: TypingCallback): Unsubscribe;
124
+ onConnectionChange(callback: ConnectionCallback): Unsubscribe;
125
+ updateConfig(config: ChatConfig): void;
126
+ private _applyBubbleLayout;
127
+ registerWidget(definition: ChatWidgetDefinition): void;
128
+ /**
129
+ * Called by `MessageInput` on each keystroke.
130
+ *
131
+ * Strategy (industry-standard for Slack/Intercom/Front):
132
+ * - Publish `true` once on the leading edge of a typing burst.
133
+ * - Refresh `true` every 4s while the user keeps typing so the
134
+ * dashboard's local timeout doesn't drop the indicator mid-burst.
135
+ * - Schedule a 5s idle timer that publishes `false` once the user
136
+ * stops. Each new keystroke resets the timer.
137
+ * - When realtime isn't attached (initial load, reconnect window),
138
+ * queue the intent and flush it when `realtimeReady` flips true,
139
+ * provided the activity is still recent (<3s old).
140
+ */
141
+ notifyUserTyping(): void;
142
+ /**
143
+ * Called from `MessageInput` on send/blur, from `close()`, and from
144
+ * `destroy()`. Synchronously emits `false` so the dashboard's
145
+ * indicator clears immediately rather than waiting for the 5s idle
146
+ * timeout.
147
+ */
148
+ stopUserTyping(): void;
149
+ private _scheduleTypingIdle;
150
+ destroy(): void;
151
+ /** Build the WidgetContext passed to custom widget renderers and actions. */
152
+ getWidgetContext(messageId: string, widgetType: string): WidgetContext;
153
+ /**
154
+ * Mark a widget as submitted in local message metadata so the next render
155
+ * shows the confirmation UI instead of the input form, then trigger an AI
156
+ * follow-up unless the widget is `escalate_to_human` (AI mode is off).
157
+ */
158
+ markWidgetSubmittedAndFollowUp(messageId: string, widgetType: string, formData: Record<string, unknown>): void;
159
+ private _outboxStorageKey;
160
+ private _refreshOutboxKey;
161
+ private _markOutboxSending;
162
+ private _failOutboxMessage;
163
+ private _bindOutboxToChannel;
164
+ private _rememberLastCreatedChannel;
165
+ private _migrateOutboxFromDistinctId;
166
+ private _bindOutboxLifecycle;
167
+ private _unbindOutboxLifecycle;
168
+ private _enqueueOutbox;
169
+ private _markMessageDelivered;
170
+ private _scheduleDrainOutbox;
171
+ private _drainOutbox;
172
+ private _deliverOutboxEntry;
173
+ private _createChannelIdForOutbox;
174
+ private _postUserMessageSilent;
175
+ private _postUserMessage;
176
+ private get _distinctId();
177
+ private _stubChannelFromSummary;
178
+ private _handleIdentityChange;
179
+ /**
180
+ * Resolve once `identityChangeInFlight` becomes `false`. Used by
181
+ * `sendMessage` so a `vt.sendChatMessage()` call that arrives immediately
182
+ * after `vt.identify()` doesn't race the in-flight identity reset.
183
+ */
184
+ private _waitForIdentityChange;
185
+ private _trackChatMessage;
186
+ private _autoMarkAsRead;
187
+ private _publishTyping;
188
+ /**
189
+ * Subscribe to `connectionState` changes and forward `connected`/
190
+ * `disconnected` transitions through the legacy `onConnectionChange`
191
+ * callback contract.
192
+ */
193
+ private _watchConnectionState;
194
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Local delivery status on optimistic user messages (not persisted server-side).
3
+ */
4
+ import type { ChatStore } from "../store/chat-store";
5
+ export type MessageDeliveryStatus = "pending" | "sending" | "failed";
6
+ export declare function setMessageDeliveryStatus(store: ChatStore, messageId: string, status: MessageDeliveryStatus | undefined): void;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Chronological ordering for chat messages.
3
+ *
4
+ * The server orders by `(created_at, id)`. The widget store must match
5
+ * that contract whenever messages are inserted out of band (streaming
6
+ * races, Ably echoes) so bubbles don't land in append order.
7
+ */
8
+ import type { ChatMessage } from "../../../utils/globals";
9
+ /** Compare two messages the way Postgres orders inbox rows. */
10
+ export declare function compareMessagesChronologically(a: ChatMessage, b: ChatMessage): number;
11
+ export declare function sortMessagesChronologically(messages: ChatMessage[]): ChatMessage[];
12
+ export declare function insertMessageChronologically(messages: ChatMessage[], incoming: ChatMessage): ChatMessage[];
@@ -0,0 +1,49 @@
1
+ /**
2
+ * AI streaming response reader.
3
+ *
4
+ * Reads chunks from a `Response`'s body, batches store updates to one
5
+ * per animation frame, strips trailing incomplete HTML tags so partial
6
+ * flash, and parses widget blocks at completion.
7
+ *
8
+ * Pure with respect to the DOM — only writes into the store. The bubble
9
+ * keeps the same id from frame one: the server pre-creates the row and
10
+ * returns the persisted id via the `X-Vtilt-Message-Id` header, so the
11
+ * later Ably echo / DB-update merges into the same row with no remount.
12
+ * On older servers that don't send the header we fall back to the legacy
13
+ * `temp-ai-*` id and upsertMessage's id-swap behaviour.
14
+ */
15
+ import type { ChatStore } from "../store/chat-store";
16
+ /** Header the server uses to publish the persisted AI message id up-front. */
17
+ export declare const AI_MESSAGE_ID_HEADER = "X-Vtilt-Message-Id";
18
+ /** Header the server uses to publish the persisted AI message timestamp. */
19
+ export declare const AI_MESSAGE_CREATED_AT_HEADER = "X-Vtilt-Message-Created-At";
20
+ /** Widget block marker regex: `<!--vtilt:widget:{...}-->` */
21
+ export declare const WIDGET_BLOCK_RE: RegExp;
22
+ /** Parse `<!--vtilt:widget:{...}-->` markers out of streamed content. */
23
+ export declare function parseWidgetBlocks(text: string): Array<{
24
+ type: string;
25
+ params: Record<string, unknown>;
26
+ }>;
27
+ interface StreamArgs {
28
+ store: ChatStore;
29
+ response: Response;
30
+ channelId: string;
31
+ tempId?: string;
32
+ /** When aborted, the reader stops and removes its partial bubble. */
33
+ signal?: AbortSignal;
34
+ /** Extra guard for superseded streams (new user send while still reading). */
35
+ isActive?: () => boolean;
36
+ onStreamStart?: (messageId: string) => void;
37
+ onComplete?: (messageId: string) => void;
38
+ }
39
+ /**
40
+ * Read an AI streaming response into the store. The bubble id is decided
41
+ * once at frame one (persisted id from `X-Vtilt-Message-Id` if the server
42
+ * provided it, otherwise a `temp-ai-*` fallback) and never changes, so
43
+ * the same `MessageBubble` instance receives content updates without
44
+ * re-keying.
45
+ */
46
+ export declare function handleStreamingResponse({ store, response, channelId, tempId, signal, isActive, onStreamStart, onComplete, }: StreamArgs): Promise<void>;
47
+ /** Remove a single message by id (used to clean up failed temp messages). */
48
+ export declare function removeMessageById(store: ChatStore, id: string): void;
49
+ export {};