@alia.onl/sdk 21.3.1 → 21.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @alia.onl/sdk
2
2
 
3
+ ## 21.3.2
4
+
5
+ ### Fixed
6
+
7
+ - **On web the SDK draws no async chunk boundary of its own**, so an app that lazy-loads its Alia
8
+ sheet keeps the chat in that chunk instead of its first paint. Expo's Metro serializer hoists
9
+ every module two async chunks share into `__common.js`, loaded at first paint; the SDK's own
10
+ `import()` calls (the call's `VoiceSession`, the root entry's `AliaAskProvider` sheet, the
11
+ `expo-audio`, document-picker and image readers) made such chunks inside the app's, and moved the
12
+ whole chat and the Bloom it draws with into `__common.js`. On web those now import statically
13
+ (`.web` forks); native keeps its lazy loads. The root entry's `AliaAskProvider` imports its sheet
14
+ statically on every platform (the root already holds the chat); `@alia.onl/sdk/ask` keeps its
15
+ one lazy boundary. CrowdSource's reviewer: first-paint JS 1.57 MiB gzip → 1.23 MiB, largest
16
+ file 5.05 MiB → 3.79 MiB, its lazy Alia chunk 29 KiB → 404 KiB gzip. `check:entries` holds it.
17
+ - **The source compiles under `erasableSyntaxOnly`.** The SDK ships TypeScript source, and a
18
+ consumer that compiles it with `erasableSyntaxOnly` (OxyHQ/website, `tsc -b`) failed with TS1294
19
+ on two constructor parameter properties in `runtime/conversations.ts`
20
+ (`ConversationNotFoundError.conversationId`, the offline store). They are now declared fields
21
+ assigned in the constructor, and the SDK's own `tsconfig.json` sets `erasableSyntaxOnly`, so
22
+ `typecheck` (CI and `prepublishOnly`) refuses parameter properties, enums and namespaces.
23
+
3
24
  ## 21.3.1
4
25
 
5
26
  - Ask Alia follows the active account language resolved by Oxy Services instead of defaulting to
package/README.md CHANGED
@@ -174,7 +174,14 @@ dismiss(); // e.g. after a host tool changed the page under the sheet
174
174
 
175
175
  Mount it from `@alia.onl/sdk/ask`: that entry is the provider and its hook alone, and the sheet
176
176
  loads with the first `ask` (fetched in the background once the person is signed in), so the chat
177
- stays out of the app's entry bundle. The root entry exports the same two, with the whole chat.
177
+ stays out of the app's entry bundle. The root entry exports the same two, with the whole chat
178
+ (its provider imports the sheet statically).
179
+
180
+ **On web, load the chat through ONE async boundary.** Expo's Metro serializer hoists every module
181
+ two async chunks share into `__common.js`, which loads at first paint. The SDK draws no async chunk
182
+ boundary of its own on web (the call, sounds and pickers import statically there), except the one
183
+ of `@alia.onl/sdk/ask`. So either mount the provider from `@alia.onl/sdk/ask`, or lazy-import your
184
+ own module that imports the root entry — not both, or the chat they share lands in `__common.js`.
178
185
 
179
186
  The sheet offers the person's latest conversations with its lead under the greeting
180
187
  (`recentConversations={false}` turns it off). The code that configures the sheet sits above it, so
@@ -433,7 +440,7 @@ ships no model identifier of its own.
433
440
  ```sh
434
441
  bun run typecheck # tsc --noEmit
435
442
  bun run test # vitest
436
- bun run check:entries # the root entry reaches the call only through its lazy import; no retired modules
443
+ bun run check:entries # entry isolation; on web, no async chunk boundary but the ask entry's; no retired modules
437
444
  ```
438
445
 
439
446
  ## Licence
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alia.onl/sdk",
3
- "version": "21.3.1",
3
+ "version": "21.3.2",
4
4
  "description": "Alia AI chat SDK — the conversation runtime, chat UI, voice calls and dictation for Oxy apps",
5
5
  "main": "src/index.ts",
6
6
  "react-native": "src/index.ts",
package/src/ask.ts CHANGED
@@ -3,5 +3,9 @@
3
3
  // An app mounts the provider at its root from here rather than from the root entry, which
4
4
  // would bring the whole chat along with it.
5
5
 
6
- export { AliaAskProvider, useAskAgent } from './components/AliaAskProvider';
7
- export type { AliaAsk, AliaAskAgents, AliaAskProviderProps } from './components/AliaAskProvider';
6
+ export { AliaAskProvider, useAskAgent } from './components/AliaAskProviderLazy';
7
+ export type {
8
+ AliaAsk,
9
+ AliaAskAgents,
10
+ AliaAskProviderProps,
11
+ } from './components/AliaAskProviderLazy';
@@ -0,0 +1,183 @@
1
+ /**
2
+ * "Ask Alia", "Ask Sindi": one sheet an app mounts once, opened on any agent from anywhere in it.
3
+ *
4
+ * The app names the agents it offers (`agents`, by a key of its own) and configures the sheet
5
+ * once — its host tools, renderers, words, voice, context, upsell and share. A button calls
6
+ * `useAskAgent().ask(key, turn)`. Signed out, the sheet is not mounted (it could not call Alia):
7
+ * `ask` opens Oxy's sign-in instead, and the person asks again once signed in.
8
+ *
9
+ * This module is the provider without its sheet; each entry builds one with `createAskProvider`:
10
+ * - `@alia.onl/sdk/ask` (`AliaAskProviderLazy`) loads the sheet with the first `ask`, through
11
+ * the one `import()` that keeps the chat out of the app's entry bundle;
12
+ * - the root entry (`AliaAskProvider`) already holds the whole chat, so its sheet is a static
13
+ * import. A lazy one there would put an async chunk boundary INSIDE the SDK, and on web Metro
14
+ * hoists every module two async chunks share into `__common.js`, loaded at first paint: an app
15
+ * that lazy-loads the root entry would get the chat in its first paint instead of its own chunk.
16
+ * Both read and write the one context below, so `useAskAgent` works under either.
17
+ */
18
+
19
+ import { useAuth, useOxy } from '@oxy.so/services/ui/client';
20
+ import {
21
+ type ComponentType,
22
+ createContext,
23
+ forwardRef,
24
+ type ReactNode,
25
+ type RefAttributes,
26
+ useCallback,
27
+ useContext,
28
+ useEffect,
29
+ useImperativeHandle,
30
+ useMemo,
31
+ useRef,
32
+ useState,
33
+ } from 'react';
34
+ import type { AliaTurn } from '../hooks/useChatComposer';
35
+ import type { AliaChatSheetProps, AliaChatSheetRef } from './AliaChatSheet';
36
+ import type { AliaAskOpening, AliaAskSheetProps } from './AliaAskSheet';
37
+
38
+ /** The agents an app offers, by its own key. An entry without `id` is Alia. */
39
+ export type AliaAskAgents = Readonly<Record<string, { id?: string }>>;
40
+
41
+ export interface AliaAskProviderProps
42
+ extends Pick<
43
+ AliaChatSheetProps,
44
+ | 'app'
45
+ | 'hostTools'
46
+ | 'toolRenderers'
47
+ | 'labels'
48
+ | 'voice'
49
+ | 'clientContext'
50
+ | 'onFeatureLocked'
51
+ | 'onShare'
52
+ | 'model'
53
+ | 'apiUrl'
54
+ | 'characterRuntimeUrl'
55
+ | 'headerMenu'
56
+ | 'recentConversations'
57
+ > {
58
+ /** The agents `ask` opens, by key. Omitted, the sheet only ever opens on Alia. */
59
+ agents?: AliaAskAgents;
60
+ /** The lines offered over Alia's empty composer; an agent offers its own starters. */
61
+ welcomeSuggestions?: AliaChatSheetProps['welcomeSuggestions'];
62
+ children: ReactNode;
63
+ }
64
+
65
+ export interface AliaAsk {
66
+ /**
67
+ * Open the sheet on the agent `agentKey` (omitted, Alia), with `turn` already sent when given
68
+ * ("ask Sindi about this listing"). Signed out, opens Oxy's sign-in instead.
69
+ */
70
+ ask: (agentKey?: string, turn?: AliaTurn) => void;
71
+ /** Open the sheet on the agent `agentKey` (omitted, Alia), on its stored conversation `conversationId`. */
72
+ resume: (agentKey: string | undefined, conversationId: string) => void;
73
+ /**
74
+ * Close the sheet — a host tool that changed the page under it shows the person the result, once
75
+ * the turn has been written.
76
+ */
77
+ dismiss: () => void;
78
+ }
79
+
80
+ const AskContext = createContext<AliaAsk | null>(null);
81
+
82
+ /** The app's way to open its one Alia sheet. Must be used under `AliaAskProvider`. */
83
+ export function useAskAgent(): AliaAsk {
84
+ const ask = useContext(AskContext);
85
+ if (ask === null) throw new Error('useAskAgent must be used within an AliaAskProvider');
86
+ return ask;
87
+ }
88
+
89
+ /** The opening that waits for the sheet of agent `key` to be the one mounted. */
90
+ interface Opening extends AliaAskOpening {
91
+ key: string | undefined;
92
+ }
93
+
94
+ /** The sheet a provider mounts: `AliaAskSheet`, imported statically or behind `lazy` + `Suspense`. */
95
+ export type AskSheetComponent = ComponentType<AliaAskSheetProps & RefAttributes<AliaChatSheetRef>>;
96
+
97
+ /**
98
+ * Builds the provider around `Sheet`. `preload`, when given, fetches the sheet's module in the
99
+ * background once signed in, so the first `ask` does not wait on the network.
100
+ */
101
+ export function createAskProvider(Sheet: AskSheetComponent, preload?: () => Promise<unknown>) {
102
+ /**
103
+ * Mounts the app's one sheet. Its ref is the same `AliaAsk` as `useAskAgent()`, for the code
104
+ * that configures the sheet and so sits above it — a host-tool executor that closes it.
105
+ */
106
+ return forwardRef<AliaAsk, AliaAskProviderProps>(function AliaAskProvider(
107
+ { agents, welcomeSuggestions, children, labels, ...sheet },
108
+ ref,
109
+ ) {
110
+ const { canUsePrivateApi, signIn } = useAuth();
111
+ const { currentLanguage } = useOxy();
112
+ const localizedLabels = useMemo(
113
+ () => ({ ...labels, locale: labels?.locale ?? currentLanguage }),
114
+ [labels, currentLanguage],
115
+ );
116
+ const sheetRef = useRef<AliaChatSheetRef>(null);
117
+ const [opening, setOpening] = useState<Opening | null>(null);
118
+ const [current, setCurrent] = useState<string | undefined>(undefined);
119
+ /** Nothing is mounted before the first `ask`. */
120
+ const [asked, setAsked] = useState(false);
121
+ const agentsRef = useRef(agents);
122
+ agentsRef.current = agents;
123
+
124
+ const open = useCallback(
125
+ (agentKey: string | undefined, turn?: AliaTurn, conversationId?: string) => {
126
+ if (agentKey !== undefined && agentsRef.current?.[agentKey] === undefined) {
127
+ throw new Error(`AliaAskProvider has no agent "${agentKey}"`);
128
+ }
129
+ if (!canUsePrivateApi) {
130
+ void signIn().catch(() => undefined);
131
+ return;
132
+ }
133
+ setAsked(true);
134
+ setCurrent(agentKey);
135
+ setOpening((previous) => ({
136
+ key: agentKey,
137
+ turn,
138
+ ...(conversationId === undefined ? {} : { conversationId }),
139
+ serial: (previous?.serial ?? 0) + 1,
140
+ }));
141
+ },
142
+ [canUsePrivateApi, signIn],
143
+ );
144
+ const ask = useCallback((agentKey?: string, turn?: AliaTurn) => open(agentKey, turn), [open]);
145
+ const resume = useCallback(
146
+ (agentKey: string | undefined, conversationId: string) =>
147
+ open(agentKey, undefined, conversationId),
148
+ [open],
149
+ );
150
+ const dismiss = useCallback(() => sheetRef.current?.dismiss(), []);
151
+
152
+ const opened = useCallback(() => setOpening(null), []);
153
+
154
+ // Signed in, the sheet's module comes down in the background, ahead of the first `ask`.
155
+ useEffect(() => {
156
+ if (canUsePrivateApi && preload !== undefined) void preload().catch(() => undefined);
157
+ }, [canUsePrivateApi]);
158
+
159
+ const agentId = current === undefined ? undefined : agents?.[current]?.id;
160
+ const agent = useMemo(() => (agentId === undefined ? undefined : { id: agentId }), [agentId]);
161
+ const value = useMemo<AliaAsk>(() => ({ ask, resume, dismiss }), [ask, resume, dismiss]);
162
+ useImperativeHandle(ref, () => value, [value]);
163
+
164
+ return (
165
+ <AskContext.Provider value={value}>
166
+ {children}
167
+ {canUsePrivateApi && asked ? (
168
+ <Sheet
169
+ key={agentId ?? 'alia'}
170
+ ref={sheetRef}
171
+ {...sheet}
172
+ labels={localizedLabels}
173
+ agent={agent}
174
+ welcomeSuggestions={agent === undefined ? welcomeSuggestions : undefined}
175
+ // A different agent is a new sheet: the opening waits for that one to mount.
176
+ opening={opening !== null && opening.key === current ? opening : null}
177
+ onOpened={opened}
178
+ />
179
+ ) : null}
180
+ </AskContext.Provider>
181
+ );
182
+ });
183
+ }
@@ -1,175 +1,18 @@
1
1
  /**
2
- * "Ask Alia", "Ask Sindi": one sheet an app mounts once, opened on any agent from anywhere in it.
2
+ * The ask provider of the root entry (`@alia.onl/sdk`): its sheet is a static import.
3
3
  *
4
- * The app names the agents it offers (`agents`, by a key of its own) and configures the sheet
5
- * once — its host tools, renderers, words, voice, context, upsell and share. A button calls
6
- * `useAskAgent().ask(key, turn)`. Signed out, the sheet is not mounted (it could not call Alia):
7
- * `ask` opens Oxy's sign-in instead, and the person asks again once signed in.
8
- *
9
- * The sheet itself (`AliaAskSheet`) is loaded lazily and mounted on the first `ask`, so an app
10
- * that mounts this provider at its root — imported from `@alia.onl/sdk/ask` — keeps the chat out
11
- * of its entry bundle. Signed in, its module is fetched in the background so the first `ask`
12
- * does not wait on the network.
13
- */
14
-
15
- import { useAuth, useOxy } from '@oxy.so/services/ui/client';
16
- import {
17
- createContext,
18
- forwardRef,
19
- lazy,
20
- type ReactNode,
21
- Suspense,
22
- useCallback,
23
- useContext,
24
- useEffect,
25
- useImperativeHandle,
26
- useMemo,
27
- useRef,
28
- useState,
29
- } from 'react';
30
- import type { AliaTurn } from '../hooks/useChatComposer';
31
- import type { AliaChatSheetProps, AliaChatSheetRef } from './AliaChatSheet';
32
- import type { AliaAskOpening } from './AliaAskSheet';
33
-
34
- const loadSheet = () => import('./AliaAskSheet');
35
- const LazyAskSheet = lazy(() => loadSheet().then((module) => ({ default: module.AliaAskSheet })));
36
-
37
- /** The agents an app offers, by its own key. An entry without `id` is Alia. */
38
- export type AliaAskAgents = Readonly<Record<string, { id?: string }>>;
39
-
40
- export interface AliaAskProviderProps
41
- extends Pick<
42
- AliaChatSheetProps,
43
- | 'app'
44
- | 'hostTools'
45
- | 'toolRenderers'
46
- | 'labels'
47
- | 'voice'
48
- | 'clientContext'
49
- | 'onFeatureLocked'
50
- | 'onShare'
51
- | 'model'
52
- | 'apiUrl'
53
- | 'characterRuntimeUrl'
54
- | 'headerMenu'
55
- | 'recentConversations'
56
- > {
57
- /** The agents `ask` opens, by key. Omitted, the sheet only ever opens on Alia. */
58
- agents?: AliaAskAgents;
59
- /** The lines offered over Alia's empty composer; an agent offers its own starters. */
60
- welcomeSuggestions?: AliaChatSheetProps['welcomeSuggestions'];
61
- children: ReactNode;
62
- }
63
-
64
- export interface AliaAsk {
65
- /**
66
- * Open the sheet on the agent `agentKey` (omitted, Alia), with `turn` already sent when given
67
- * ("ask Sindi about this listing"). Signed out, opens Oxy's sign-in instead.
68
- */
69
- ask: (agentKey?: string, turn?: AliaTurn) => void;
70
- /** Open the sheet on the agent `agentKey` (omitted, Alia), on its stored conversation `conversationId`. */
71
- resume: (agentKey: string | undefined, conversationId: string) => void;
72
- /**
73
- * Close the sheet — a host tool that changed the page under it shows the person the result, once
74
- * the turn has been written.
75
- */
76
- dismiss: () => void;
77
- }
78
-
79
- const AskContext = createContext<AliaAsk | null>(null);
80
-
81
- /** The app's way to open its one Alia sheet. Must be used under `AliaAskProvider`. */
82
- export function useAskAgent(): AliaAsk {
83
- const ask = useContext(AskContext);
84
- if (ask === null) throw new Error('useAskAgent must be used within an AliaAskProvider');
85
- return ask;
86
- }
87
-
88
- /** The opening that waits for the sheet of agent `key` to be the one mounted. */
89
- interface Opening extends AliaAskOpening {
90
- key: string | undefined;
91
- }
92
-
93
- /**
94
- * Mounts the app's one sheet. Its ref is the same `AliaAsk` as `useAskAgent()`, for the code that
95
- * configures the sheet and so sits above it — a host-tool executor that closes it.
4
+ * The root entry already holds the whole chat, so loading `AliaAskSheet` lazily here would save
5
+ * nothing and cost a lot on web: it is an async chunk boundary inside the SDK, and Metro hoists
6
+ * every module two async chunks share into `__common.js`, which loads at first paint. An app that
7
+ * lazy-loads its Alia sheet would then get the chat (and the Bloom it draws with) in its first
8
+ * paint. An app that wants the chat out of its entry bundle mounts the provider from
9
+ * `@alia.onl/sdk/ask` (`AliaAskProviderLazy`) instead. See `AliaAskCore` for the provider itself.
96
10
  */
97
- export const AliaAskProvider = forwardRef<AliaAsk, AliaAskProviderProps>(function AliaAskProvider(
98
- { agents, welcomeSuggestions, children, labels, ...sheet },
99
- ref,
100
- ) {
101
- const { canUsePrivateApi, signIn } = useAuth();
102
- const { currentLanguage } = useOxy();
103
- const localizedLabels = useMemo(
104
- () => ({ ...labels, locale: labels?.locale ?? currentLanguage }),
105
- [labels, currentLanguage],
106
- );
107
- const sheetRef = useRef<AliaChatSheetRef>(null);
108
- const [opening, setOpening] = useState<Opening | null>(null);
109
- const [current, setCurrent] = useState<string | undefined>(undefined);
110
- /** Nothing is mounted before the first `ask`. */
111
- const [asked, setAsked] = useState(false);
112
- const agentsRef = useRef(agents);
113
- agentsRef.current = agents;
114
-
115
- const open = useCallback(
116
- (agentKey: string | undefined, turn?: AliaTurn, conversationId?: string) => {
117
- if (agentKey !== undefined && agentsRef.current?.[agentKey] === undefined) {
118
- throw new Error(`AliaAskProvider has no agent "${agentKey}"`);
119
- }
120
- if (!canUsePrivateApi) {
121
- void signIn().catch(() => undefined);
122
- return;
123
- }
124
- setAsked(true);
125
- setCurrent(agentKey);
126
- setOpening((previous) => ({
127
- key: agentKey,
128
- turn,
129
- ...(conversationId === undefined ? {} : { conversationId }),
130
- serial: (previous?.serial ?? 0) + 1,
131
- }));
132
- },
133
- [canUsePrivateApi, signIn],
134
- );
135
- const ask = useCallback((agentKey?: string, turn?: AliaTurn) => open(agentKey, turn), [open]);
136
- const resume = useCallback(
137
- (agentKey: string | undefined, conversationId: string) =>
138
- open(agentKey, undefined, conversationId),
139
- [open],
140
- );
141
- const dismiss = useCallback(() => sheetRef.current?.dismiss(), []);
142
-
143
- const opened = useCallback(() => setOpening(null), []);
144
11
 
145
- // Signed in, the sheet's module comes down in the background, ahead of the first `ask`.
146
- useEffect(() => {
147
- if (canUsePrivateApi) void loadSheet().catch(() => undefined);
148
- }, [canUsePrivateApi]);
12
+ import { createAskProvider } from './AliaAskCore';
13
+ import { AliaAskSheet } from './AliaAskSheet';
149
14
 
150
- const agentId = current === undefined ? undefined : agents?.[current]?.id;
151
- const agent = useMemo(() => (agentId === undefined ? undefined : { id: agentId }), [agentId]);
152
- const value = useMemo<AliaAsk>(() => ({ ask, resume, dismiss }), [ask, resume, dismiss]);
153
- useImperativeHandle(ref, () => value, [value]);
15
+ export type { AliaAsk, AliaAskAgents, AliaAskProviderProps } from './AliaAskCore';
16
+ export { useAskAgent } from './AliaAskCore';
154
17
 
155
- return (
156
- <AskContext.Provider value={value}>
157
- {children}
158
- {canUsePrivateApi && asked ? (
159
- <Suspense fallback={null}>
160
- <LazyAskSheet
161
- key={agentId ?? 'alia'}
162
- ref={sheetRef}
163
- {...sheet}
164
- labels={localizedLabels}
165
- agent={agent}
166
- welcomeSuggestions={agent === undefined ? welcomeSuggestions : undefined}
167
- // A different agent is a new sheet: the opening waits for that one to mount.
168
- opening={opening !== null && opening.key === current ? opening : null}
169
- onOpened={opened}
170
- />
171
- </Suspense>
172
- ) : null}
173
- </AskContext.Provider>
174
- );
175
- });
18
+ export const AliaAskProvider = createAskProvider(AliaAskSheet);
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The ask provider of `@alia.onl/sdk/ask`: its sheet is loaded lazily and mounted on the first
3
+ * `ask`, so an app that mounts this provider at its root keeps the chat out of its entry bundle.
4
+ * Signed in, the sheet's module is fetched in the background so the first `ask` does not wait on
5
+ * the network.
6
+ *
7
+ * This `import()` is the ONE async boundary the SDK draws, and only for this entry: everything
8
+ * behind it (`AliaAskSheet` and the chat) imports statically on web, so Metro emits one chunk for
9
+ * the sheet and hoists nothing of it into `__common.js` (`scripts/check-entry-isolation.mjs`).
10
+ */
11
+
12
+ import { lazy, Suspense } from 'react';
13
+ import { type AskSheetComponent, createAskProvider } from './AliaAskCore';
14
+
15
+ const loadSheet = () => import('./AliaAskSheet');
16
+ const LazyAskSheet = lazy(() => loadSheet().then((module) => ({ default: module.AliaAskSheet })));
17
+
18
+ const SuspendedAskSheet: AskSheetComponent = (props) => (
19
+ <Suspense fallback={null}>
20
+ <LazyAskSheet {...props} />
21
+ </Suspense>
22
+ );
23
+
24
+ export type { AliaAsk, AliaAskAgents, AliaAskProviderProps } from './AliaAskCore';
25
+ export { useAskAgent } from './AliaAskCore';
26
+
27
+ export const AliaAskProvider = createAskProvider(SuspendedAskSheet, loadSheet);
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The call of a conversation screen, on web: its bar, while a call is open.
3
+ *
4
+ * Native loads the call lazily (`AliaVoiceCall.tsx`) so a screen that never calls never evaluates
5
+ * `react-native-audio-api`. Web never reaches that engine (`lib/voice-audio-engine.ts` is the Web
6
+ * Audio one), and a lazy `import()` here would be an async chunk boundary inside the SDK: Metro
7
+ * hoists every module two async chunks share into `__common.js`, loaded at first paint, so an app
8
+ * that lazy-loads its Alia sheet would get the chat in its first paint. On web the call is a
9
+ * static import and travels in whatever chunk holds the chat.
10
+ */
11
+
12
+ import type { VoiceSessionProps } from '../../types';
13
+ import { VoiceSession } from './VoiceSession';
14
+
15
+ export function AliaVoiceCall(props: VoiceSessionProps) {
16
+ return <VoiceSession {...props} />;
17
+ }
@@ -2,6 +2,7 @@ import { RiFileTextLine } from '@oxy.so/bloom/icons/RiFileTextLine';
2
2
  import { RiImageLine } from '@oxy.so/bloom/icons/RiImageLine';
3
3
  import { useCallback, useMemo } from 'react';
4
4
  import type { AliaAddMenu } from '../lib/add-menu';
5
+ import { loadDocumentPicker } from '../lib/deferred/document-picker';
5
6
  import type { IntakeKind } from '../lib/attachment-intake';
6
7
  import { useLabels } from '../lib/labels';
7
8
  import { ALIA_FEATURES, type FeatureOffer } from '../runtime/agents';
@@ -71,17 +72,15 @@ export function useAttachMenu(
71
72
  size: asset.size,
72
73
  mimeType: asset.mimeType,
73
74
  }))
74
- : // The picker loads with the first document: a chat that never attaches one never does.
75
- ((await (await import('../lib/document-picker')).pickDocuments()) ?? []).map(
76
- (asset) => ({
77
- id: attachmentId('doc'),
78
- type: 'document' as const,
79
- name: asset.name,
80
- uri: asset.uri,
81
- size: asset.size,
82
- mimeType: asset.mimeType,
83
- }),
84
- );
75
+ : // On native the picker loads with the first document: a chat that never attaches one never does.
76
+ ((await (await loadDocumentPicker()).pickDocuments()) ?? []).map((asset) => ({
77
+ id: attachmentId('doc'),
78
+ type: 'document' as const,
79
+ name: asset.name,
80
+ uri: asset.uri,
81
+ size: asset.size,
82
+ mimeType: asset.mimeType,
83
+ }));
85
84
  for (const attachment of picked) store.addAttachment(address, attachment);
86
85
  },
87
86
  [offers, onFeatureLocked, pickImage, address],
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { useEffect, useRef, useCallback } from 'react';
11
11
  import type { AudioPlayer, AudioSource } from 'expo-audio';
12
+ import { loadExpoAudio } from '../lib/deferred/expo-audio';
12
13
  import type { AgentState } from '../types';
13
14
 
14
15
  export type SoundName = 'thinking' | 'toolCall' | 'voiceConnect' | 'voiceDisconnect';
@@ -37,7 +38,7 @@ export function useSoundEffects(enabled: boolean = false, sounds: SoundSources =
37
38
 
38
39
  (async () => {
39
40
  try {
40
- const { createAudioPlayer } = await import('expo-audio');
41
+ const { createAudioPlayer } = await loadExpoAudio();
41
42
  if (!mounted) return;
42
43
 
43
44
  for (const name of SOUND_NAMES) {
@@ -5,6 +5,7 @@ import { useOxy } from '@oxy.so/services/ui/client';
5
5
  import { errorMessage } from '../lib/utils';
6
6
  import { create } from 'zustand';
7
7
  import { createAudioLevelMeter } from '../lib/audio-level';
8
+ import { loadExpoAudio } from '../lib/deferred/expo-audio';
8
9
  import { requestSpeechClip } from '../lib/speech-synthesis';
9
10
  import { ALIA_API_URL } from '../lib/origins';
10
11
 
@@ -142,7 +143,7 @@ export function useTTS(options: UseTTSOptions = {}) {
142
143
 
143
144
  (async () => {
144
145
  try {
145
- const { createAudioPlayer } = await import('expo-audio');
146
+ const { createAudioPlayer } = await loadExpoAudio();
146
147
  if (signal.aborted) return;
147
148
  /**
148
149
  * `crossOrigin` is web-only and ignored elsewhere, and it is what makes
@@ -0,0 +1,2 @@
1
+ /** The document picker, loaded when the person picks a file: native evaluates nothing of it before. */
2
+ export const loadDocumentPicker = () => import('../document-picker');
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The document picker on web, imported with the composer.
3
+ *
4
+ * On web this is a static import: a lazy `import()` would be an async chunk boundary inside the SDK,
5
+ * and Metro hoists every module two async chunks share into `__common.js`, loaded at first paint —
6
+ * an app that lazy-loads its Alia sheet would get the chat in its first paint. Native keeps the
7
+ * lazy load (`./document-picker.ts`); `scripts/check-entry-isolation.mjs` holds that web has no boundary.
8
+ */
9
+
10
+ import * as DocumentPicker from '../document-picker';
11
+
12
+ export const loadDocumentPicker = async () => DocumentPicker;
@@ -0,0 +1,2 @@
1
+ /** `expo-audio`, loaded on first use: native evaluates nothing of it until the chat plays a sound. */
2
+ export const loadExpoAudio = () => import('expo-audio');
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `expo-audio` on web, imported with the chat.
3
+ *
4
+ * On web this is a static import: a lazy `import()` would be an async chunk boundary inside the SDK,
5
+ * and Metro hoists every module two async chunks share into `__common.js`, loaded at first paint —
6
+ * an app that lazy-loads its Alia sheet would get the chat in its first paint. Native keeps the
7
+ * lazy load (`./expo-audio.ts`); `scripts/check-entry-isolation.mjs` holds that web has no boundary.
8
+ */
9
+
10
+ import * as ExpoAudio from 'expo-audio';
11
+
12
+ export const loadExpoAudio = async () => ExpoAudio;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The attachment reader, loaded on the first attachment: on native a turn of plain text (and the
3
+ * runtime entry that sends it) never evaluates `expo-file-system`.
4
+ */
5
+ export const loadImageDataUrl = () => import('../image-data-url');
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The attachment reader on web, imported with the runtime.
3
+ *
4
+ * On web this is a static import: a lazy `import()` would be an async chunk boundary inside the SDK,
5
+ * and Metro hoists every module two async chunks share into `__common.js`, loaded at first paint —
6
+ * an app that lazy-loads its Alia sheet would get the chat in its first paint. Native keeps the
7
+ * lazy load (`./image-data-url.ts`); `scripts/check-entry-isolation.mjs` holds that web has no boundary.
8
+ */
9
+
10
+ import * as ImageDataUrl from '../image-data-url';
11
+
12
+ export const loadImageDataUrl = async () => ImageDataUrl;
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { createAudioLevelMeter } from './audio-level';
12
+ import { loadExpoAudio } from './deferred/expo-audio';
12
13
 
13
14
  export type ProductVoice = 'male' | 'female';
14
15
 
@@ -171,7 +172,7 @@ export interface PlayClipOptions {
171
172
  export async function playSpeechClip(uri: string, options: PlayClipOptions): Promise<void> {
172
173
  const { signal, onLevel } = options;
173
174
  if (signal.aborted) return;
174
- const { createAudioPlayer } = await import('expo-audio');
175
+ const { createAudioPlayer } = await loadExpoAudio();
175
176
  if (signal.aborted) return;
176
177
 
177
178
  const player = createAudioPlayer({ uri }, { crossOrigin: 'anonymous' });
@@ -1,3 +1,4 @@
1
+ import { loadImageDataUrl } from '../lib/deferred/image-data-url';
1
2
  import type { AliaAttachment } from '../types';
2
3
 
3
4
  export type MessageContentPart =
@@ -60,9 +61,9 @@ export async function buildMessageContent(
60
61
 
61
62
  const parts: MessageContentPart[] = [{ type: 'text', text }];
62
63
 
63
- // The readers need the platform's file access, loaded on the first attachment: a turn of plain
64
- // text (and everything that imports this module) never touches it.
65
- const { imageUriToDataUrl } = await import('../lib/image-data-url');
64
+ // The readers need the platform's file access, loaded on the first attachment: on native a turn
65
+ // of plain text (and everything that imports this module) never touches it.
66
+ const { imageUriToDataUrl } = await loadImageDataUrl();
66
67
  for (const att of usable) {
67
68
  if (att.type !== 'image' && upload !== undefined) {
68
69
  try {
@@ -30,8 +30,11 @@ const FIVE_MINUTES = 1000 * 60 * 5;
30
30
 
31
31
  /** A stable signal for a URL whose server-side conversation no longer exists. */
32
32
  export class ConversationNotFoundError extends Error {
33
- constructor(readonly conversationId: string) {
33
+ readonly conversationId: string;
34
+
35
+ constructor(conversationId: string) {
34
36
  super('Conversation not found');
37
+ this.conversationId = conversationId;
35
38
  this.name = 'ConversationNotFoundError';
36
39
  }
37
40
  }
@@ -54,7 +57,11 @@ function messageText(message: Message | undefined): string {
54
57
  * it holds nothing, and the 401 is the caller's to handle.
55
58
  */
56
59
  class OfflineConversations {
57
- constructor(private readonly store: OfflineConversationStore | null) {}
60
+ private readonly store: OfflineConversationStore | null;
61
+
62
+ constructor(store: OfflineConversationStore | null) {
63
+ this.store = store;
64
+ }
58
65
 
59
66
  get available(): boolean {
60
67
  return this.store !== null;
package/tsconfig.json CHANGED
@@ -5,6 +5,7 @@
5
5
  "moduleResolution": "bundler",
6
6
  "jsx": "react-jsx",
7
7
  "strict": true,
8
+ "erasableSyntaxOnly": true,
8
9
  "noUnusedLocals": true,
9
10
  "noUnusedParameters": true,
10
11
  "esModuleInterop": true,