@ably/ai-transport 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +54 -47
  2. package/dist/ably-ai-transport.js +1006 -539
  3. package/dist/ably-ai-transport.js.map +1 -1
  4. package/dist/ably-ai-transport.umd.cjs +1 -1
  5. package/dist/ably-ai-transport.umd.cjs.map +1 -1
  6. package/dist/constants.d.ts +4 -0
  7. package/dist/core/codec/types.d.ts +19 -2
  8. package/dist/core/transport/decode-history.d.ts +8 -6
  9. package/dist/core/transport/headers.d.ts +4 -2
  10. package/dist/core/transport/index.d.ts +4 -1
  11. package/dist/core/transport/pipe-stream.d.ts +3 -2
  12. package/dist/core/transport/stream-router.d.ts +11 -1
  13. package/dist/core/transport/tree.d.ts +171 -0
  14. package/dist/core/transport/turn-manager.d.ts +4 -1
  15. package/dist/core/transport/types.d.ts +270 -119
  16. package/dist/core/transport/view.d.ts +166 -0
  17. package/dist/errors.d.ts +19 -2
  18. package/dist/index.d.ts +3 -1
  19. package/dist/react/ably-ai-transport-react.js +1019 -486
  20. package/dist/react/ably-ai-transport-react.js.map +1 -1
  21. package/dist/react/ably-ai-transport-react.umd.cjs +1 -1
  22. package/dist/react/ably-ai-transport-react.umd.cjs.map +1 -1
  23. package/dist/react/contexts/transport-context.d.ts +31 -0
  24. package/dist/react/contexts/transport-provider.d.ts +49 -0
  25. package/dist/react/create-transport-hooks.d.ts +124 -0
  26. package/dist/react/index.d.ts +14 -8
  27. package/dist/react/use-ably-messages.d.ts +14 -8
  28. package/dist/react/use-active-turns.d.ts +7 -3
  29. package/dist/react/use-client-transport.d.ts +78 -5
  30. package/dist/react/use-create-view.d.ts +22 -0
  31. package/dist/react/use-tree.d.ts +20 -0
  32. package/dist/react/use-view.d.ts +79 -0
  33. package/dist/vercel/ably-ai-transport-vercel.js +1478 -842
  34. package/dist/vercel/ably-ai-transport-vercel.js.map +1 -1
  35. package/dist/vercel/ably-ai-transport-vercel.umd.cjs +1 -1
  36. package/dist/vercel/ably-ai-transport-vercel.umd.cjs.map +1 -1
  37. package/dist/vercel/codec/tool-transitions.d.ts +50 -0
  38. package/dist/vercel/index.d.ts +3 -0
  39. package/dist/vercel/react/ably-ai-transport-vercel-react.js +9099 -852
  40. package/dist/vercel/react/ably-ai-transport-vercel-react.js.map +1 -1
  41. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs +45 -1
  42. package/dist/vercel/react/ably-ai-transport-vercel-react.umd.cjs.map +1 -1
  43. package/dist/vercel/react/contexts/chat-transport-context.d.ts +32 -0
  44. package/dist/vercel/react/contexts/chat-transport-provider.d.ts +84 -0
  45. package/dist/vercel/react/index.d.ts +5 -0
  46. package/dist/vercel/react/use-chat-transport.d.ts +61 -20
  47. package/dist/vercel/react/use-message-sync.d.ts +41 -9
  48. package/dist/vercel/react/use-staged-add-tool-approval-response.d.ts +30 -0
  49. package/dist/vercel/tool-approvals.d.ts +124 -0
  50. package/dist/vercel/tool-events.d.ts +26 -0
  51. package/dist/vercel/transport/chat-transport.d.ts +33 -11
  52. package/dist/vercel/transport/index.d.ts +5 -2
  53. package/package.json +23 -17
  54. package/src/constants.ts +6 -0
  55. package/src/core/codec/encoder.ts +10 -1
  56. package/src/core/codec/types.ts +19 -3
  57. package/src/core/transport/client-transport.ts +382 -364
  58. package/src/core/transport/decode-history.ts +229 -81
  59. package/src/core/transport/headers.ts +6 -2
  60. package/src/core/transport/index.ts +13 -5
  61. package/src/core/transport/pipe-stream.ts +8 -5
  62. package/src/core/transport/server-transport.ts +212 -58
  63. package/src/core/transport/stream-router.ts +21 -3
  64. package/src/core/transport/{conversation-tree.ts → tree.ts} +192 -77
  65. package/src/core/transport/turn-manager.ts +28 -10
  66. package/src/core/transport/types.ts +318 -139
  67. package/src/core/transport/view.ts +840 -0
  68. package/src/errors.ts +21 -1
  69. package/src/index.ts +10 -5
  70. package/src/react/contexts/transport-context.ts +37 -0
  71. package/src/react/contexts/transport-provider.tsx +164 -0
  72. package/src/react/create-transport-hooks.ts +144 -0
  73. package/src/react/index.ts +15 -8
  74. package/src/react/use-ably-messages.ts +34 -16
  75. package/src/react/use-active-turns.ts +28 -17
  76. package/src/react/use-client-transport.ts +184 -24
  77. package/src/react/use-create-view.ts +68 -0
  78. package/src/react/use-tree.ts +53 -0
  79. package/src/react/use-view.ts +233 -0
  80. package/src/react/vite.config.ts +4 -1
  81. package/src/vercel/codec/accumulator.ts +64 -79
  82. package/src/vercel/codec/decoder.ts +11 -8
  83. package/src/vercel/codec/encoder.ts +68 -54
  84. package/src/vercel/codec/index.ts +0 -2
  85. package/src/vercel/codec/tool-transitions.ts +122 -0
  86. package/src/vercel/index.ts +17 -0
  87. package/src/vercel/react/contexts/chat-transport-context.ts +40 -0
  88. package/src/vercel/react/contexts/chat-transport-provider.tsx +122 -0
  89. package/src/vercel/react/index.ts +14 -0
  90. package/src/vercel/react/use-chat-transport.ts +164 -42
  91. package/src/vercel/react/use-message-sync.ts +77 -19
  92. package/src/vercel/react/use-staged-add-tool-approval-response.ts +87 -0
  93. package/src/vercel/react/vite.config.ts +4 -2
  94. package/src/vercel/tool-approvals.ts +380 -0
  95. package/src/vercel/tool-events.ts +53 -0
  96. package/src/vercel/transport/chat-transport.ts +225 -79
  97. package/src/vercel/transport/index.ts +14 -3
  98. package/dist/core/transport/conversation-tree.d.ts +0 -9
  99. package/dist/react/use-conversation-tree.d.ts +0 -20
  100. package/dist/react/use-edit.d.ts +0 -7
  101. package/dist/react/use-history.d.ts +0 -19
  102. package/dist/react/use-messages.d.ts +0 -7
  103. package/dist/react/use-regenerate.d.ts +0 -7
  104. package/dist/react/use-send.d.ts +0 -7
  105. package/src/react/use-conversation-tree.ts +0 -71
  106. package/src/react/use-edit.ts +0 -24
  107. package/src/react/use-history.ts +0 -111
  108. package/src/react/use-messages.ts +0 -32
  109. package/src/react/use-regenerate.ts +0 -24
  110. package/src/react/use-send.ts +0 -25
@@ -1,37 +1,197 @@
1
1
  /**
2
- * useClientTransport: creates and memoizes a core ClientTransport instance
3
- * across renders.
2
+ * useClientTransport read a ClientTransport from the nearest TransportProvider.
4
3
  *
5
- * Stores the instance in a ref so the same transport is returned on every render.
6
- * The transport manages its own Ably channel subscription in the constructor
7
- * this hook adds no subscription logic.
4
+ * The transport is created by {@link TransportProvider}, which also wraps the subtree
5
+ * with Ably's `ChannelProvider`. This hook is a thin context reader it does not
6
+ * create or manage transport state.
8
7
  *
9
- * The hook does NOT auto-close the transport on unmount. Channel lifecycle is
10
- * managed by the Ably provider (useChannel), which detaches the channel and
11
- * clears all subscriptions. Auto-closing would break React Strict Mode
12
- * (double-mount calls close() on the first cleanup, leaving a dead transport
13
- * on the second mount). Call transport.close() explicitly if you need to tear
14
- * down the transport independently of the channel lifecycle.
8
+ * **Provider lookup**
9
+ * - Omit `channelName` to use the innermost `TransportProvider` in the tree.
10
+ * - Pass `channelName` to look up a specific provider by name.
11
+ * - Pass `skip: true` to receive a stub transport that throws on any access —
12
+ * safe to hold in state before auth or other conditions are ready.
13
+ *
14
+ * **Error handling**
15
+ * - When no matching provider is found, or when the provider's `createClientTransport`
16
+ * call threw, `transportError` is set on the returned object instead of throwing.
17
+ * The component can render an error state without an error boundary.
18
+ * - Pass `onError` to receive post-construction transport errors (e.g. send failures,
19
+ * channel continuity loss) without wiring `transport.on('error', ...)` manually.
15
20
  */
16
21
 
17
- import { useRef } from 'react';
22
+ import * as Ably from 'ably';
23
+ import { useContext, useEffect, useRef } from 'react';
24
+
25
+ import type { ClientTransport, Tree, View } from '../core/transport/types.js';
26
+ import { ErrorCode } from '../errors.js';
27
+ import { NearestTransportContext, TransportContext } from './contexts/transport-context.js';
18
28
 
19
- import { createClientTransport } from '../core/transport/client-transport.js';
20
- import type { ClientTransport, ClientTransportOptions } from '../core/transport/types.js';
29
+ const SKIPPED_TRANSPORT: ClientTransport<unknown, unknown> = {
30
+ get tree(): Tree<unknown> {
31
+ throw new Ably.ErrorInfo('unable to access tree; hook is skipped', ErrorCode.InvalidArgument, 400);
32
+ },
33
+ get view(): View<unknown, unknown> {
34
+ throw new Ably.ErrorInfo('unable to access view; hook is skipped', ErrorCode.InvalidArgument, 400);
35
+ },
36
+ createView: (): View<unknown, unknown> => {
37
+ throw new Ably.ErrorInfo('unable to create view; hook is skipped', ErrorCode.InvalidArgument, 400);
38
+ },
39
+ cancel: () => {
40
+ throw new Ably.ErrorInfo('unable to cancel; hook is skipped', ErrorCode.InvalidArgument, 400);
41
+ },
42
+ stageEvents: () => {
43
+ throw new Ably.ErrorInfo('unable to stage events; hook is skipped', ErrorCode.InvalidArgument, 400);
44
+ },
45
+ stageMessage: () => {
46
+ throw new Ably.ErrorInfo('unable to stage message; hook is skipped', ErrorCode.InvalidArgument, 400);
47
+ },
48
+ waitForTurn: () => {
49
+ throw new Ably.ErrorInfo('unable to wait for turn; hook is skipped', ErrorCode.InvalidArgument, 400);
50
+ },
51
+ on: () => {
52
+ throw new Ably.ErrorInfo('unable to subscribe; hook is skipped', ErrorCode.InvalidArgument, 400);
53
+ },
54
+ close: () => {
55
+ throw new Ably.ErrorInfo('unable to close; hook is skipped', ErrorCode.InvalidArgument, 400);
56
+ },
57
+ };
21
58
 
22
59
  /**
23
- * Create and memoize a {@link ClientTransport} across renders.
24
- * @param options - Configuration for the client transport.
25
- * @returns The memoized transport instance.
60
+ * Return value of {@link useClientTransport}.
61
+ *
62
+ * `transport` is always a valid object. When `skip` is `true`, when no provider was
63
+ * found, or when the provider's transport construction failed, `transport` is a stub
64
+ * that throws {@link Ably.ErrorInfo} on every access.
65
+ * Check `transportError` before using `transport` to avoid those throws.
26
66
  */
27
- export const useClientTransport = <TEvent, TMessage>(
28
- options: ClientTransportOptions<TEvent, TMessage>,
29
- ): ClientTransport<TEvent, TMessage> => {
30
- const transportRef = useRef<ClientTransport<TEvent, TMessage> | null>(null);
67
+ export interface ClientTransportHandle<TEvent, TMessage> {
68
+ /**
69
+ * The resolved transport.
70
+ *
71
+ * A throwing stub when `skip` is `true`, when no matching {@link TransportProvider}
72
+ * was found in the tree, or when transport construction failed.
73
+ */
74
+ transport: ClientTransport<TEvent, TMessage>;
75
+ /**
76
+ * Set when no matching {@link TransportProvider} was found, when transport
77
+ * construction failed, and `skip` is `false`.
78
+ * `undefined` when the transport resolved successfully or when `skip` is `true`.
79
+ */
80
+ transportError?: Ably.ErrorInfo | undefined;
81
+ }
82
+
83
+ /**
84
+ * Read a {@link ClientTransport} from the nearest {@link TransportProvider}.
85
+ *
86
+ * Returns `{ transport, transportError }`. When no provider is found or transport
87
+ * construction failed, `transportError` is set and `transport` is a stub that throws
88
+ * on access — the hook never throws during render.
89
+ *
90
+ * Pass `onError` to subscribe to post-construction transport errors
91
+ * (e.g. {@link ErrorCode.TransportSendFailed}, {@link ErrorCode.ChannelContinuityLost})
92
+ * without calling `transport.on('error', …)` manually. The subscription is
93
+ * created when the transport resolves and removed on unmount.
94
+ * @param props - Hook options.
95
+ * @param props.channelName - Look up a specific provider by channel name; omit for the nearest.
96
+ * @param props.skip - When `true`, return the stub transport immediately without reading context.
97
+ * @param props.onError - Called whenever the resolved transport emits an error event.
98
+ * @returns `{ transport, transportError }`.
99
+ */
100
+ export const useClientTransport = <TEvent, TMessage>({
101
+ channelName,
102
+ skip,
103
+ onError,
104
+ }: {
105
+ /**
106
+ * Channel name passed to the enclosing {@link TransportProvider}.
107
+ * Omit to use the nearest provider in the tree.
108
+ */
109
+ channelName?: string;
110
+ /**
111
+ * When `true`, skip context lookup and return a stub transport that throws on
112
+ * any access. Use when a condition (auth, feature flag) is not yet resolved.
113
+ */
114
+ skip?: boolean;
115
+ /**
116
+ * Called whenever the resolved transport emits an error event.
117
+ * The subscription is established once the transport resolves and
118
+ * automatically removed on unmount or when the transport changes.
119
+ */
120
+ onError?: (error: Ably.ErrorInfo) => void;
121
+ } = {}): ClientTransportHandle<TEvent, TMessage> => {
122
+ const registry = useContext(TransportContext);
123
+ const nearestSlot = useContext(NearestTransportContext);
124
+ const errorCallbackRef = useRef(onError);
125
+ errorCallbackRef.current = onError;
126
+
127
+ // Compute the transport for the onError subscription *before* any conditional
128
+ // returns to satisfy React's rules of hooks (no hooks in branches).
129
+ // Erased generics — this ref is only used in the useEffect below.
130
+ const resolvedForEffect: ClientTransport<unknown, unknown> | undefined = skip
131
+ ? undefined
132
+ : channelName === undefined
133
+ ? nearestSlot?.transport
134
+ : registry[channelName]?.transport;
135
+
136
+ useEffect(() => {
137
+ if (!resolvedForEffect) return;
138
+ return resolvedForEffect.on('error', (errorInfo) => {
139
+ errorCallbackRef.current?.(errorInfo);
140
+ });
141
+ }, [resolvedForEffect]);
142
+
143
+ if (skip) {
144
+ return {
145
+ transport: SKIPPED_TRANSPORT as unknown as ClientTransport<TEvent, TMessage>,
146
+ };
147
+ }
148
+
149
+ if (channelName !== undefined) {
150
+ const slot = registry[channelName];
151
+ if (slot) {
152
+ if (slot.transport) {
153
+ // CAST: TransportContext stores transports with erased generics.
154
+ // The caller is responsible for using type parameters matching those of the TransportProvider.
155
+ return {
156
+ transport: slot.transport as unknown as ClientTransport<TEvent, TMessage>,
157
+ };
158
+ }
159
+ // Provider exists but construction failed.
160
+ return {
161
+ transport: SKIPPED_TRANSPORT as unknown as ClientTransport<TEvent, TMessage>,
162
+ transportError: slot.error,
163
+ };
164
+ }
165
+ return {
166
+ transport: SKIPPED_TRANSPORT as unknown as ClientTransport<TEvent, TMessage>,
167
+ transportError: new Ably.ErrorInfo(
168
+ `unable to use transport; no TransportProvider found for channelName "${channelName}"`,
169
+ ErrorCode.BadRequest,
170
+ 400,
171
+ ),
172
+ };
173
+ }
31
174
 
32
- if (transportRef.current === null) {
33
- transportRef.current = createClientTransport(options);
175
+ if (nearestSlot) {
176
+ if (nearestSlot.transport) {
177
+ // CAST: NearestTransportContext stores transport with erased generics; types fixed at call site.
178
+ return {
179
+ transport: nearestSlot.transport as unknown as ClientTransport<TEvent, TMessage>,
180
+ };
181
+ }
182
+ // Nearest provider exists but construction failed.
183
+ return {
184
+ transport: SKIPPED_TRANSPORT as unknown as ClientTransport<TEvent, TMessage>,
185
+ transportError: nearestSlot.error,
186
+ };
34
187
  }
35
188
 
36
- return transportRef.current;
189
+ return {
190
+ transport: SKIPPED_TRANSPORT as unknown as ClientTransport<TEvent, TMessage>,
191
+ transportError: new Ably.ErrorInfo(
192
+ 'unable to use transport; no TransportProvider found in the tree',
193
+ ErrorCode.BadRequest,
194
+ 400,
195
+ ),
196
+ };
37
197
  };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * useCreateView — create an independent view with the same API as useView.
3
+ *
4
+ * Calls {@link ClientTransport.createView} to create an independent view over
5
+ * the same conversation tree, then subscribes to it exactly like
6
+ * {@link useView}. The view is closed automatically on unmount or when the
7
+ * transport reference changes.
8
+ *
9
+ * Pass `null` or omit `transport` to defer creation (e.g. when a split pane is
10
+ * collapsed). The returned handle has empty state until a transport is provided.
11
+ * When `transport` is omitted entirely, defaults to the nearest
12
+ * {@link TransportProvider}'s transport via context.
13
+ * Pass `skip: true` to bypass all context reads and view creation entirely.
14
+ */
15
+
16
+ import { useContext, useEffect, useState } from 'react';
17
+
18
+ import type { ClientTransport, View } from '../core/transport/types.js';
19
+ import { NearestTransportContext } from './contexts/transport-context.js';
20
+ import type { ViewHandle } from './use-view.js';
21
+ import { useView } from './use-view.js';
22
+
23
+ /**
24
+ * Create an independent {@link View} and subscribe to it.
25
+ * Returns the same {@link ViewHandle} as {@link useView}, but backed by a
26
+ * newly created view with its own branch selections and pagination state.
27
+ * The view is closed on unmount or when the transport changes.
28
+ * When `transport` is omitted, uses the nearest {@link TransportProvider}'s transport via context.
29
+ * @param props - Options including optional `transport`, `limit` for auto-load, and `skip`.
30
+ * @param props.transport - Transport to create a view from; defaults to the nearest provider.
31
+ * @param props.limit - Max older messages per page; when provided, auto-loads on mount.
32
+ * @param props.skip - When `true`, skip view creation and return an empty handle.
33
+ * @returns A {@link ViewHandle} with nodes, pagination, navigation, and write operations.
34
+ */
35
+ export const useCreateView = <TEvent, TMessage>({
36
+ transport,
37
+ limit,
38
+ skip,
39
+ }: {
40
+ /** The transport to create a view from, or null/undefined to use the nearest provider. */
41
+ transport?: ClientTransport<TEvent, TMessage> | null;
42
+ /** When provided, auto-loads the first page on mount. Omit for manual load. */
43
+ limit?: number;
44
+ /** When `true`, skip view creation and return an empty handle immediately. */
45
+ skip?: boolean;
46
+ } = {}): ViewHandle<TEvent, TMessage> => {
47
+ const nearestSlot = useContext(NearestTransportContext);
48
+ // CAST: NearestTransportContext stores transport with erased generics; types fixed at call site.
49
+ const resolved = skip
50
+ ? undefined
51
+ : ((transport ?? nearestSlot?.transport) as ClientTransport<TEvent, TMessage> | null | undefined);
52
+
53
+ const [view, setView] = useState<View<TEvent, TMessage> | undefined>();
54
+
55
+ useEffect(() => {
56
+ if (!resolved) {
57
+ setView(undefined);
58
+ return;
59
+ }
60
+ const v = resolved.createView();
61
+ setView(v);
62
+ return () => {
63
+ v.close();
64
+ };
65
+ }, [resolved]);
66
+
67
+ return useView({ view, limit, skip });
68
+ };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * useTree — stable structural query callbacks for a ClientTransport's tree.
3
+ *
4
+ * Returns a {@link TreeHandle} with methods to inspect the tree structure.
5
+ * These are thin `useCallback` wrappers around `transport.tree` — no local
6
+ * state or subscriptions. Branch navigation (select, getSelectedIndex) is
7
+ * on {@link ViewHandle} from {@link useView}.
8
+ *
9
+ * When `transport` is omitted, defaults to the nearest
10
+ * {@link TransportProvider}'s transport via context.
11
+ */
12
+
13
+ import { useCallback, useContext } from 'react';
14
+
15
+ import type { ClientTransport, MessageNode } from '../core/transport/types.js';
16
+ import { NearestTransportContext } from './contexts/transport-context.js';
17
+
18
+ /** Handle for querying the conversation tree structure. */
19
+ export interface TreeHandle<TMessage> {
20
+ /** Get all sibling messages at a fork point, ordered chronologically by serial. */
21
+ getSiblings: (msgId: string) => TMessage[];
22
+ /** Whether a message has sibling alternatives (i.e., show navigation arrows). */
23
+ hasSiblings: (msgId: string) => boolean;
24
+ /** Get a node by msgId, or undefined if not found. */
25
+ getNode: (msgId: string) => MessageNode<TMessage> | undefined;
26
+ }
27
+
28
+ /**
29
+ * Provide stable structural query callbacks backed by the transport's tree.
30
+ * When `transport` is omitted, uses the nearest {@link TransportProvider}'s transport via context.
31
+ * @param props - Options including optional `transport`.
32
+ * @param props.transport - Transport to read tree structure from; defaults to the nearest provider.
33
+ * @returns A {@link TreeHandle} with structural query methods.
34
+ */
35
+ export const useTree = <TEvent, TMessage>({
36
+ transport,
37
+ }: { transport?: ClientTransport<TEvent, TMessage> } = {}): TreeHandle<TMessage> => {
38
+ const nearestSlot = useContext(NearestTransportContext);
39
+ // CAST: NearestTransportContext stores transport with erased generics; types fixed at call site.
40
+ const resolved = (transport ?? nearestSlot?.transport) as ClientTransport<TEvent, TMessage> | undefined;
41
+
42
+ const getSiblings = useCallback((msgId: string) => resolved?.tree.getSiblings(msgId) ?? [], [resolved]);
43
+
44
+ const hasSiblings = useCallback((msgId: string) => resolved?.tree.hasSiblings(msgId) ?? false, [resolved]);
45
+
46
+ const getNode = useCallback((msgId: string) => resolved?.tree.getNode(msgId), [resolved]);
47
+
48
+ return {
49
+ getSiblings,
50
+ hasSiblings,
51
+ getNode,
52
+ };
53
+ };
@@ -0,0 +1,233 @@
1
+ /**
2
+ * useView — reactive paginated view of the conversation.
3
+ *
4
+ * Subscribes to view updates and exposes the visible nodes, branch navigation,
5
+ * write operations, pagination state, and a `loadOlder` callback. Pass `transport`
6
+ * to use a transport's default view, or `view` to subscribe to a specific
7
+ * {@link View} directly. When both are omitted, defaults to the nearest
8
+ * {@link TransportProvider}'s transport via context.
9
+ */
10
+
11
+ import * as Ably from 'ably';
12
+ import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
13
+
14
+ import type { ActiveTurn, ClientTransport, MessageNode, SendOptions, View } from '../core/transport/types.js';
15
+ import { ErrorCode } from '../errors.js';
16
+ import { NearestTransportContext } from './contexts/transport-context.js';
17
+
18
+ /** Options for configuring the view's initial load behavior. */
19
+ export interface UseViewOptions {
20
+ /** Maximum number of older messages to load per page. Defaults to 100. */
21
+ limit?: number;
22
+ }
23
+
24
+ /** Handle for the paginated, branch-aware conversation view. */
25
+ export interface ViewHandle<TEvent, TMessage> {
26
+ /** The visible domain messages along the selected branch. */
27
+ messages: TMessage[];
28
+ /** Visible conversation nodes along the selected branch. */
29
+ nodes: MessageNode<TMessage>[];
30
+ /** Whether there are older messages that can be revealed via `loadOlder`. */
31
+ hasOlder: boolean;
32
+ /** Whether a page load is currently in progress. */
33
+ loading: boolean;
34
+ /**
35
+ * Set when the most recent `loadOlder` call failed.
36
+ * Cleared automatically on the next successful load.
37
+ * `undefined` when no error has occurred or when `skip` is `true`.
38
+ */
39
+ loadError: Ably.ErrorInfo | undefined;
40
+ /**
41
+ * Load older messages into the view. No-op if already loading.
42
+ * On failure, `error` is set; on success, `error` is cleared.
43
+ */
44
+ loadOlder: () => Promise<void>;
45
+ /** Select a sibling at a fork point by index. Triggers a view update with the new branch. */
46
+ select: (msgId: string, index: number) => void;
47
+ /** Index of the currently selected sibling at a fork point. */
48
+ getSelectedIndex: (msgId: string) => number;
49
+ /** Get all sibling messages at a fork point, ordered chronologically by serial. */
50
+ getSiblings: (msgId: string) => TMessage[];
51
+ /** Whether a message has sibling alternatives (i.e., show navigation arrows). */
52
+ hasSiblings: (msgId: string) => boolean;
53
+ /** Get a node by msgId, or undefined if not found. */
54
+ getNode: (msgId: string) => MessageNode<TMessage> | undefined;
55
+ /** Send one or more messages in the context of this view's selected branch. */
56
+ send: (messages: TMessage | TMessage[], options?: SendOptions) => Promise<ActiveTurn<TEvent>>;
57
+ /** Regenerate an assistant message, using this view's branch for history. */
58
+ regenerate: (messageId: string, options?: SendOptions) => Promise<ActiveTurn<TEvent>>;
59
+ /** Edit a user message, forking from this view's branch. */
60
+ edit: (messageId: string, newMessages: TMessage | TMessage[], options?: SendOptions) => Promise<ActiveTurn<TEvent>>;
61
+ /** Amend an existing message and start a continuation turn (e.g. tool results). */
62
+ update: (msgId: string, events: TEvent[], options?: SendOptions) => Promise<ActiveTurn<TEvent>>;
63
+ }
64
+
65
+ /**
66
+ * Subscribe to a view and return the visible node list with pagination, navigation, and write operations.
67
+ *
68
+ * `view` takes priority over `transport`. When neither is provided, the nearest
69
+ * {@link TransportProvider}'s transport is used. When `limit` is provided, auto-loads
70
+ * the first page on mount (SWR-style).
71
+ * @param props - Options for selecting the view source and configuring auto-load.
72
+ * @param props.transport - Client transport whose default view to subscribe to; defaults to the nearest provider.
73
+ * @param props.view - A specific {@link View} to subscribe to directly. Takes priority over `transport`.
74
+ * @param props.limit - Max older messages per page; when provided, auto-loads on mount.
75
+ * @param props.skip - When `true`, skip all subscriptions and return an empty handle.
76
+ * @returns A {@link ViewHandle} with nodes, pagination state, navigation, write operations, and loadOlder.
77
+ */
78
+ export const useView = <TEvent, TMessage>({
79
+ transport,
80
+ view,
81
+ limit,
82
+ skip,
83
+ }: {
84
+ /** Client transport whose default view to subscribe to; defaults to the nearest provider when omitted. */
85
+ transport?: ClientTransport<TEvent, TMessage> | null;
86
+ /** A specific {@link View} to subscribe to directly. Takes priority over `transport`. */
87
+ view?: View<TEvent, TMessage> | null;
88
+ /** When provided, auto-loads the first page on mount. Omit for manual loading. */
89
+ limit?: number;
90
+ /** When `true`, skip all subscriptions and return an empty handle immediately. */
91
+ skip?: boolean;
92
+ } = {}): ViewHandle<TEvent, TMessage> => {
93
+ const nearestSlot = useContext(NearestTransportContext);
94
+ // CAST: NearestTransportContext stores transport with erased generics; types fixed at call site.
95
+ const resolvedTransport = skip
96
+ ? undefined
97
+ : (transport ?? (nearestSlot?.transport as unknown as ClientTransport<TEvent, TMessage> | undefined));
98
+ const resolvedView = skip ? undefined : (view ?? resolvedTransport?.view);
99
+
100
+ const [nodes, setNodes] = useState<MessageNode<TMessage>[]>(() => resolvedView?.flattenNodes() ?? []);
101
+ const [hasOlder, setHasOlder] = useState(() => resolvedView?.hasOlder() ?? false);
102
+ const [loading, setLoading] = useState(false);
103
+ const [loadError, setLoadError] = useState<Ably.ErrorInfo | undefined>();
104
+ const loadingRef = useRef(false);
105
+
106
+ // Auto-load first page on mount when limit is provided (SWR-style).
107
+ // Fires once per view instance — subsequent changes to limit
108
+ // only affect manual loadOlder() calls, not the initial auto-load.
109
+ const autoLoad = limit !== undefined;
110
+ const autoLoadedRef = useRef(false);
111
+
112
+ // Subscribe to view updates
113
+ useEffect(() => {
114
+ if (!resolvedView) {
115
+ setNodes([]);
116
+ setHasOlder(false);
117
+ setLoadError(undefined);
118
+ return;
119
+ }
120
+
121
+ // Reset auto-load flag so the new view gets its first page loaded
122
+ autoLoadedRef.current = false;
123
+
124
+ // Sync initial state
125
+ setNodes(resolvedView.flattenNodes());
126
+ setHasOlder(resolvedView.hasOlder());
127
+ setLoadError(undefined);
128
+
129
+ const unsub = resolvedView.on('update', () => {
130
+ setNodes(resolvedView.flattenNodes());
131
+ setHasOlder(resolvedView.hasOlder());
132
+ });
133
+ return unsub;
134
+ }, [resolvedView]);
135
+
136
+ const loadOlder = useCallback(async () => {
137
+ if (!resolvedView || loadingRef.current) return;
138
+ loadingRef.current = true;
139
+ setLoading(true);
140
+ try {
141
+ await resolvedView.loadOlder(limit);
142
+ setLoadError(undefined);
143
+ } catch (error) {
144
+ if (error instanceof Ably.ErrorInfo) {
145
+ setLoadError(error);
146
+ } else {
147
+ setLoadError(new Ably.ErrorInfo('Unknown error loading older messages', ErrorCode.BadRequest, 400));
148
+ }
149
+ } finally {
150
+ loadingRef.current = false;
151
+ setLoading(false);
152
+ }
153
+ }, [resolvedView, limit]);
154
+
155
+ useEffect(() => {
156
+ if (!autoLoad || autoLoadedRef.current || !resolvedView) return;
157
+ autoLoadedRef.current = true;
158
+ void loadOlder();
159
+ }, [autoLoad, resolvedView, loadOlder]);
160
+
161
+ const messages = useMemo(() => nodes.map((n) => n.message), [nodes]);
162
+
163
+ // Branch navigation callbacks
164
+ const select = useCallback(
165
+ (msgId: string, index: number) => {
166
+ resolvedView?.select(msgId, index);
167
+ },
168
+ [resolvedView],
169
+ );
170
+
171
+ const getSelectedIndex = useCallback((msgId: string) => resolvedView?.getSelectedIndex(msgId) ?? 0, [resolvedView]);
172
+
173
+ const getSiblings = useCallback((msgId: string) => resolvedView?.getSiblings(msgId) ?? [], [resolvedView]);
174
+
175
+ const hasSiblings = useCallback((msgId: string) => resolvedView?.hasSiblings(msgId) ?? false, [resolvedView]);
176
+
177
+ const getNode = useCallback((msgId: string) => resolvedView?.getNode(msgId), [resolvedView]);
178
+
179
+ // Write operation callbacks
180
+ const send = useCallback(
181
+ async (msgs: TMessage | TMessage[], opts?: SendOptions) => {
182
+ if (!resolvedView)
183
+ throw new Ably.ErrorInfo('unable to send; view is not available', ErrorCode.InvalidArgument, 400);
184
+ return resolvedView.send(msgs, opts);
185
+ },
186
+ [resolvedView],
187
+ );
188
+
189
+ const regenerate = useCallback(
190
+ async (messageId: string, opts?: SendOptions) => {
191
+ if (!resolvedView)
192
+ throw new Ably.ErrorInfo('unable to regenerate; view is not available', ErrorCode.InvalidArgument, 400);
193
+ return resolvedView.regenerate(messageId, opts);
194
+ },
195
+ [resolvedView],
196
+ );
197
+
198
+ const edit = useCallback(
199
+ async (messageId: string, newMessages: TMessage | TMessage[], opts?: SendOptions) => {
200
+ if (!resolvedView)
201
+ throw new Ably.ErrorInfo('unable to edit; view is not available', ErrorCode.InvalidArgument, 400);
202
+ return resolvedView.edit(messageId, newMessages, opts);
203
+ },
204
+ [resolvedView],
205
+ );
206
+
207
+ const update = useCallback(
208
+ async (msgId: string, events: TEvent[], opts?: SendOptions) => {
209
+ if (!resolvedView)
210
+ throw new Ably.ErrorInfo('unable to update; view is not available', ErrorCode.InvalidArgument, 400);
211
+ return resolvedView.update(msgId, events, opts);
212
+ },
213
+ [resolvedView],
214
+ );
215
+
216
+ return {
217
+ messages,
218
+ nodes,
219
+ hasOlder,
220
+ loading,
221
+ loadError,
222
+ loadOlder,
223
+ select,
224
+ getSelectedIndex,
225
+ getSiblings,
226
+ hasSiblings,
227
+ getNode,
228
+ send,
229
+ regenerate,
230
+ edit,
231
+ update,
232
+ };
233
+ };
@@ -19,11 +19,14 @@ export default defineConfig({
19
19
  formats: ['es', 'umd'],
20
20
  },
21
21
  rollupOptions: {
22
- external: ['ably', 'react'],
22
+ external: ['ably', 'ably/react', 'react', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
23
23
  output: {
24
24
  globals: {
25
25
  ably: 'Ably',
26
+ 'ably/react': 'AblyReact',
26
27
  react: 'React',
28
+ 'react/jsx-runtime': 'ReactJsxRuntime',
29
+ 'react/jsx-dev-runtime': 'ReactJsxDevRuntime',
27
30
  },
28
31
  },
29
32
  },