@alia.onl/sdk 12.0.0 → 13.0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,114 @@
1
1
  # @alia.onl/sdk
2
2
 
3
+ ## 13.0.0
4
+
5
+ Fixes found by driving the sheet end to end (the sheet shows plans, links citations, nests lists):
6
+
7
+ - **A plan is drawn.** `plan_preview` was parsed but the sheet passed no handlers, so the card
8
+ never showed. `useAliaChat` now returns `approvePlan` / `rejectPlan` (approve collapses the
9
+ card to "approved"; reject collapses it and stops the turn) and `AliaChatContent` uses them
10
+ unless the host passes its own.
11
+ - **Citations link.** `[n]` markers in a reply link to their sources (`linkifyCitations`,
12
+ `normalizeCitationMarkers`, `citationLabel` are exported from `@alia.onl/sdk/tools`; the app
13
+ re-exports them instead of keeping a copy).
14
+ - **Nested lists** draw under their parent instead of inline beside it.
15
+ - A finished research step reads "Researched", not "Researching".
16
+ - A turn that ends at a question card with no sentence before it counts as answered.
17
+
18
+ - **BREAKING: an agent is drawn with the look the server stores for it.** `agentCharacter(id)` (a
19
+ look hashed from the id) is removed; `readAgentCharacter(value)` parses a stored character
20
+ (Bloom's `parseAvatarCharacter`) and falls back on `DEFAULT_AGENT_CHARACTER`. The `alia.agent`
21
+ event carries `agentCharacter`, and `agentInfo` has `character`. `AliaChatAgent` has `character`;
22
+ `AliaChatSheet` / `AliaChatScreen` take `agents` (the host's directory) so a delegation's
23
+ thinking avatar is the agent's own before its answer arrives. Needs a server that sends
24
+ `agentCharacter`; release this SDK first, since the stream parser rejects unknown keys.
25
+ - **The one who answers thinks with its own avatar, and it is the same avatar that settles under
26
+ the reply** (Bloom's `AgentReplyIdentity`, as in the multi-agent chat). While Alia works, her
27
+ avatar is the indicator, large with "Thinking…", where the `AgentThinking` line was; when her
28
+ text starts it shrinks under it with her name. A call to an agent (`delegateToAgent` /
29
+ `askAgent`, its `agentId` and `agentName` or `agent` arguments) opens the agent's message at
30
+ once, empty and thinking, and its answer fills that same message, so the avatar never remounts;
31
+ an agent that never answers leaves no message. Alia's reply now carries her identity row, as the
32
+ agents' do.
33
+ The reasoning is part of that same component (`AgentReplyIdentity`'s `reasoning`): while the
34
+ turn thinks, "Thinking… ⌃" folds it under the avatar's row, and once the reply starts it is the
35
+ closed "Thought for N seconds" block above it (`ThinkingBlock` draws nothing while it streams, so
36
+ there is one thinking indicator, not two).
37
+ - **No jumps of height, and no flash of the wrong thinker.** A reply, a new row, the reasoning and
38
+ a thinking agent's avatar open and close with their height eased (Bloom's `Collapsible`, now
39
+ with `appear`) instead of popping. Alia is shown thinking only once she has for a moment, so a
40
+ turn she hands to an agent at once shows the agent thinking, not Alia for an instant first.
41
+ - **Bubbles are equal on every side as the eye reads them.** An agent's bubble sets its top and
42
+ bottom padding from the reply's text metrics (`AgentReplyBubble`'s `text`, 16 on 28 here), so
43
+ the gap from the edge to the text is the same above, below and at the sides. Nothing is cut:
44
+ an open `Collapsible` no longer clips.
45
+ - **The greeting's avatar flies up into the header's profile** as the conversation begins
46
+ (Bloom's `avatar-flight`: `useAvatarFlights`, `AvatarAnchor`, `AvatarFlightLayer`), across the
47
+ main and conversation sheets. `AgentProfile`'s new `avatarHidden` keeps the header's avatar
48
+ waiting for it. Needs a Bloom with `avatar-flight` (after 10.3).
49
+ - **`AliaChatSheet` opens the conversation in a second sheet stacked over the main one.** The
50
+ main sheet stays on the greeting, the suggestions and the composer; the first turn (typed or a
51
+ suggestion) opens a fresh conversation sheet on top, whose back button (and the menu's Clear)
52
+ closes just that sheet, back to the main screen. A phone gets sheets flush with the screen
53
+ edges, with the composer clearing the home indicator; from tablet width (768 px) they float.
54
+ `AliaChatContent` gains `onStart` (hand an empty chat's first turn to the host) and
55
+ `initialTurn` (a turn sent once on mount).
56
+ - **The empty chat looks and behaves like the Alia app's, from the same code.** The avatar sits
57
+ beside the greeting (`AliaWelcomeMessage`, now just the greeting: always side by side, no
58
+ `hideMark`, no suggestions) and the header shows Alia's profile only once there is a
59
+ conversation. The suggestions are one-line rows **over the composer** (`ComposerSuggestions`),
60
+ and from two typed characters they become matches with the typed part highlighted, arrows /
61
+ Enter / Escape through the composer's `onKeyPress`. The app now draws its greeting and its
62
+ suggestions with these same components and only supplies its API's data.
63
+ - **Several agents in one turn.** A delegated agent's answer (`alia.agent`) is now its own
64
+ message with its name and avatar, placed before Alia's reply as the app places it, so a turn
65
+ with Lumi, Atlas and Sol shows three speakers. Before, every answer was merged into one message
66
+ under the last agent's name. Alia's reply is dropped when she said nothing after handing over.
67
+ - **A delegated agent shows its 3D avatar.** The speaker line of an agent's message is Bloom's
68
+ `AgentAvatar`, not an image (an agent's account has none): the character comes from the agent's
69
+ id (`agentCharacter(id)`, exported), so the same agent looks the same everywhere and two agents
70
+ in a turn are told apart. The app draws its agents' messages with the same avatar.
71
+ - **Agents are bubbles, and the header holds the team.** As in Bloom's multi-agent chat, a
72
+ delegated agent's reply is a tinted bubble with its avatar and name beneath it. Once agents have
73
+ answered, the header profile stacks Alia with up to two of them and names them together
74
+ ("Alia + Lumi + Atlas"). `headerLeading` / `actions` receive `agents` in their state
75
+ (`AliaChatState`, `AliaChatAgent`), and `AliaChatHeader` takes `agents`.
76
+ - **The row under every reply is Bloom's multi-agent one**: helpful, unhelpful and copy as 22px
77
+ tiles with 12px icons (read aloud is a fourth tile where the host reads), replacing the
78
+ `AiChatFeedbackRow`. New label keys `helpful` and `unhelpful`.
79
+ - **Built on Bloom's `agent-reply`** (peer `@oxy.so/bloom ^10.1.1`): replies, their identity line and
80
+ action tiles, and the header profile are Bloom's `AgentReply`, `AgentReplyBubble`,
81
+ `AgentReplyIdentity`, `AgentReplyActions` and `AgentProfile`, the pieces `MultiAgentChat` is made
82
+ of, not copies. Pressing the profile makes every avatar in it wave.
83
+ - **Questions are asked above the composer, not in the thread.** What Alia asks in her last turn
84
+ (`askUserQuestion`) docks above the composer as Bloom's `Questionnaire`, with a dismiss; typing a
85
+ reply answers just as well. The thread keeps the questions as plain lines (`AskedQuestions`).
86
+ **Breaking:** `QuestionCard` is now the docked questionnaire (`onSubmit` required, new
87
+ `onDismiss`) and no longer draws a read-only variant; the `onAnswerQuestions` prop of the message
88
+ list is gone. `pendingQuestions` joins `@alia.onl/sdk/tools`.
89
+ - **The voice bar is Bloom's voice controls.** `VoiceControls` draws Bloom's `@oxy.so/bloom/voice`
90
+ `VoiceControls` (microphone and end) under the status text, instead of `CallControls`, which is
91
+ made for a dark call stage and lost its captions on the chat's light surface.
92
+ - **The header is back, the profile, share and a menu.** Bloom's `PageHeader` with a back button at
93
+ its start, and at its end share and a three-dots menu (Bloom's `DropdownMenu`, aligned to its
94
+ button) holding Clear and the host's rows. Over the empty chat there is no bar: nothing to do
95
+ there. In the sheet, back returns to the main screen (a fresh chat) and the sheet stays open.
96
+ **Breaking:** `AliaChatHeader` / `AliaChatSheet` / `AliaChatScreen` lose `onClose`; they take
97
+ `onBack` (screen), `onShare` and `headerMenu`; the labels `close` becomes `back`, `share`,
98
+ `more`.
99
+ - **The chat follows Bloom's theme, not the system's scheme.** `AliaMarkdown` and the sheet read
100
+ the app's resolved mode: a person who chose light on a dark OS got white text on a light surface.
101
+ - **Every icon is Bloom's.** The voice button is `RiVolumeUpLine` (it was Entypo); the app's clock,
102
+ robot and connector glyphs are `RiTimeLine`, `RiRobot2Line`, `RiPlugLine` and the SVG generator
103
+ is gone; `@expo/vector-icons` is no longer a peer of the SDK.
104
+ - **Greeting.** The avatar (88px) above the text, both centred; `max-w-xs`, so it always sets in two lines; more room above than below. The Alia app draws the same.
105
+ - Peer `@oxy.so/bloom ^10.2.1` (scrollbar, pill groups, header scrim, voice controls).
106
+ - **Breaking:** `WelcomeSuggestion { id, title, description }` becomes `AliaSuggestion { id, text }`
107
+ (`text` is what is shown and what is sent). New optional props on the sheet, screen and content:
108
+ `suggestionMatches` and `onSuggestionQuery(query)`, so a host can search as the person types.
109
+ New exports: `ComposerSuggestions`, `useComposerSuggestions`, `useSuggestionQuery`,
110
+ `composerCompletions`, `Completion`, `AliaSuggestion`.
111
+
3
112
  ## 12.0.0
4
113
 
5
114
  Breaking: Bloom 10 peer; the `header` render prop replaces the breadcrumb path (`title`, `actions`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alia.onl/sdk",
3
- "version": "12.0.0",
3
+ "version": "13.0.0",
4
4
  "description": "Alia AI chat SDK — reusable chat UI, voice, and streaming components for Oxy apps",
5
5
  "main": "src/index.ts",
6
6
  "react-native": "src/index.ts",
@@ -39,7 +39,7 @@
39
39
  "react": "^19.2.3",
40
40
  "react-native": "^0.86.0",
41
41
  "@expo/vector-icons": "^15.1.1",
42
- "@oxy.so/bloom": ">=10.0.0",
42
+ "@oxy.so/bloom": ">=10.4.0",
43
43
  "@oxy.so/core": ">=3.0.0",
44
44
  "@oxy.so/services": ">=8.0.0",
45
45
  "@react-native-async-storage/async-storage": "^2.2.0",
@@ -9,7 +9,11 @@ export type AliaAvatarState = 'idle' | 'thinking' | 'working' | 'writing';
9
9
  * eyes, nothing on its head. It is `blue_beret` with the hat taken off and the paint changed,
10
10
  * so Bloom retargets that preset's animation onto it; change it here and nowhere else.
11
11
  */
12
- const ALIA_AVATAR: AvatarCharacter = { preset: 'blue_beret', accessory: 'none', color: 'orchid' };
12
+ export const ALIA_AVATAR: AvatarCharacter = {
13
+ preset: 'blue_beret',
14
+ accessory: 'none',
15
+ color: 'orchid',
16
+ };
13
17
 
14
18
  const BUSY: ReadonlySet<AliaAvatarState> = new Set(['thinking', 'working']);
15
19
 
@@ -16,10 +16,20 @@ import { useImagePicker } from '../hooks/useImagePicker';
16
16
  import { imageUriToDataUrl } from '../lib/image-data-url';
17
17
  import { useAmbientWave } from '../hooks/useAmbientWave';
18
18
  import { AmbientField } from './AmbientField';
19
+ import { QuestionCard } from './QuestionCard';
20
+ import { pendingQuestions } from '../lib/questions';
19
21
  import { AliaChatMessageList } from './AliaChatMessageList';
20
- import { AliaWelcomeMessage, type WelcomeSuggestion } from './AliaWelcomeMessage';
22
+ import { AliaFlightsProvider, ALIA_TO_PROFILE } from '../lib/flights';
23
+ import { AliaWelcomeMessage } from './AliaWelcomeMessage';
24
+ import { ComposerSuggestions } from './ComposerSuggestions';
25
+ import { useComposerSuggestions, useSuggestionQuery } from '../hooks/useComposerSuggestions';
21
26
  import { ComposerLoader } from '@oxy.so/bloom/composer-loader';
22
27
  import { AiChatContainer } from '@oxy.so/bloom/ai-chat';
28
+ import {
29
+ AvatarFlightLayer,
30
+ useAvatarFlights,
31
+ type AvatarFlights,
32
+ } from '@oxy.so/bloom/avatar-flight';
23
33
  import { ComposerPill, type ComposerPanelAddMenuGroup } from '@oxy.so/bloom/composer-panel';
24
34
  import { RiImageLine } from '@oxy.so/bloom/icons';
25
35
  import { toast } from '@oxy.so/bloom/toast';
@@ -27,7 +37,46 @@ import { Button } from '@oxy.so/bloom/button';
27
37
  import { type AliaChatLabels, AliaChatLabelsProvider, useLabels } from '../lib/labels';
28
38
  import type { AliaAvatarState } from './AliaAvatar';
29
39
  import { VoiceModeIcon } from './VoiceModeIcon';
30
- import type { ChatMessage, VoiceMessage, VoiceSessionComponent, VoiceSessionState } from '../types';
40
+ import type {
41
+ AliaSuggestion,
42
+ ChatMessage,
43
+ VoiceMessage,
44
+ VoiceSessionComponent,
45
+ VoiceSessionState,
46
+ } from '../types';
47
+
48
+ /** The distinct delegated agents among `messages`, in the order they first answered. */
49
+ function participatingAgents(messages: readonly ChatMessage[]): AliaChatAgent[] {
50
+ const seen = new Map<string, AliaChatAgent>();
51
+ for (const { agentInfo } of messages) {
52
+ if (agentInfo && !seen.has(agentInfo.id)) seen.set(agentInfo.id, agentInfo);
53
+ }
54
+ return [...seen.values()];
55
+ }
56
+
57
+ /** An agent that answered in the conversation, as the header shows it beside Alia. */
58
+ export interface AliaChatAgent {
59
+ id: string;
60
+ name: string;
61
+ /** Its look, as the server stores it (JSON); absent, the default look. */
62
+ character?: unknown;
63
+ }
64
+
65
+ /** What the chat tells its header and actions: whether there is a conversation to clear, and who took part. */
66
+ export interface AliaChatState {
67
+ hasMessages: boolean;
68
+ clear: () => void;
69
+ /** What Alia is doing; the header's avatar shows it. */
70
+ markState: AliaAvatarState;
71
+ /** The delegated agents that have answered, in the order they first did. */
72
+ agents: AliaChatAgent[];
73
+ }
74
+
75
+ /** A turn of the person's: the text, and the photos (`data:` or `https:` URLs) sent with it. */
76
+ export interface AliaTurn {
77
+ text: string;
78
+ images?: ReadonlyArray<string>;
79
+ }
31
80
 
32
81
  export interface AliaChatContentProps {
33
82
  clientContext?: string;
@@ -38,7 +87,12 @@ export interface AliaChatContentProps {
38
87
  // Welcome message
39
88
  welcomeGreeting?: string;
40
89
  welcomeSubtitle?: string;
41
- welcomeSuggestions?: WelcomeSuggestion[];
90
+ /** The lines offered over the composer on an empty chat. */
91
+ welcomeSuggestions?: AliaSuggestion[];
92
+ /** What the host found for the typed text (see `onSuggestionQuery`). */
93
+ suggestionMatches?: AliaSuggestion[];
94
+ /** The typed text, once typing has settled, for the host to search suggestions with. */
95
+ onSuggestionQuery?: (query: string) => void;
42
96
  // Message action callbacks
43
97
  onEditMessage?: (messageId: string, newContent: string) => void;
44
98
  onThumbsUp?: (messageId: string) => void;
@@ -47,12 +101,11 @@ export interface AliaChatContentProps {
47
101
  onRejectPlan?: (planId: string) => void;
48
102
  /** Override markdown renderer (app passes CustomMarkdown) */
49
103
  renderMarkdown?: (content: string) => React.ReactNode;
50
- /** The chat's top bar. Receives what it needs from the chat: whether there is anything to clear, how, and what Alia is doing. */
51
- header?: (state: {
52
- hasMessages: boolean;
53
- clear: () => void;
54
- markState: AliaAvatarState;
55
- }) => React.ReactNode;
104
+ /**
105
+ * The chat's top bar. Receives what it needs from the chat: whether there is anything to clear,
106
+ * how, who took part, and what Alia is doing.
107
+ */
108
+ header?: (state: AliaChatState) => React.ReactNode;
56
109
  /** Dark-mode flag — forwarded to the ambient wave overlay. */
57
110
  isDarkMode?: boolean;
58
111
  /**
@@ -61,6 +114,25 @@ export interface AliaChatContentProps {
61
114
  * from this module. May be a `React.lazy` component.
62
115
  */
63
116
  voiceSession?: VoiceSessionComponent;
117
+ /**
118
+ * Space the composer keeps clear at the bottom edge, in px — the home indicator, when the
119
+ * chat is flush with the screen (a phone sheet). The ambient background still runs behind it.
120
+ */
121
+ bottomInset?: number;
122
+ /**
123
+ * Hands the first turn of an empty chat to the host instead of sending it here — a sheet opens
124
+ * the conversation in a new sheet over this one. Omitted, the chat sends it itself.
125
+ */
126
+ onStart?: (turn: AliaTurn) => void;
127
+ /** The agents the host knows, with their look: a delegation's thinking avatar is drawn from them. */
128
+ agents?: UseAliaChatOptions['agents'];
129
+ /**
130
+ * The avatar flights this chat takes part in, when a sheet shares them across its chats (the
131
+ * greeting's avatar flies into a header in another sheet). Omitted, the chat has its own.
132
+ */
133
+ flights?: AvatarFlights;
134
+ /** A turn sent once as this chat mounts: the conversation a sheet was opened for. */
135
+ initialTurn?: AliaTurn;
64
136
  /** Internal sheet lifecycle bridge: receives the live request canceller. */
65
137
  onStopReady?: (stop: (() => void) | null) => void;
66
138
  /** Overrides for the chat's own copy; anything omitted is English. */
@@ -98,6 +170,8 @@ function AliaChatContentBody({
98
170
  welcomeGreeting,
99
171
  welcomeSubtitle,
100
172
  welcomeSuggestions,
173
+ suggestionMatches,
174
+ onSuggestionQuery,
101
175
  onEditMessage,
102
176
  onThumbsUp,
103
177
  onThumbsDown,
@@ -107,8 +181,15 @@ function AliaChatContentBody({
107
181
  header,
108
182
  isDarkMode,
109
183
  voiceSession: VoiceSession,
184
+ bottomInset = 0,
185
+ onStart,
186
+ agents: knownAgents,
187
+ flights: sharedFlights,
188
+ initialTurn,
110
189
  onStopReady,
111
190
  }: AliaChatContentProps) {
191
+ const ownFlights = useAvatarFlights();
192
+ const flights = sharedFlights ?? ownFlights;
112
193
  const labels = useLabels();
113
194
  const addMenuGroups = useMemo(() => addMenu(labels), [labels]);
114
195
 
@@ -121,8 +202,46 @@ function AliaChatContentBody({
121
202
  [onError],
122
203
  );
123
204
  // ── Chat ──
124
- const chatOptions: UseAliaChatOptions = { apiUrl, model, clientContext };
125
- const { messages, setMessages, send, isStreaming, stop, clear } = useAliaChat(chatOptions);
205
+ const chatOptions: UseAliaChatOptions = { apiUrl, model, clientContext, agents: knownAgents };
206
+ const { messages, setMessages, send, isStreaming, stop, clear, approvePlan, rejectPlan } =
207
+ useAliaChat(chatOptions);
208
+ const agents = useMemo(() => participatingAgents(messages), [messages]);
209
+ // What Alia asked in her last turn and the person has not answered: asked above the composer,
210
+ // not in the thread. Any later message, typed or sent from the card, is the answer.
211
+ const lastMessage = messages.at(-1);
212
+ const questionsAsked = useMemo(
213
+ () => pendingQuestions(messages, isStreaming),
214
+ [messages, isStreaming],
215
+ );
216
+ const [dismissedQuestionsOf, setDismissedQuestionsOf] = useState<string | null>(null);
217
+ const asking = questionsAsked.length > 0 && dismissedQuestionsOf !== lastMessage?.id;
218
+
219
+ // The first turn of an empty chat goes to the host when it asked for it; every other turn is sent here.
220
+ const startsElsewhere = onStart !== undefined && messages.length === 0;
221
+ const firstTurn = messages.length === 0;
222
+ const submitTurn = useCallback(
223
+ async (text: string, images?: ReadonlyArray<string>) => {
224
+ if (startsElsewhere) {
225
+ // The host's conversation sheet opens first so that the flight, drawn when it launches,
226
+ // is above it; this chat stays on the greeting, so the avatar can still be measured.
227
+ onStart({ text, images });
228
+ await flights.launch([ALIA_TO_PROFILE]);
229
+ return;
230
+ }
231
+ // The greeting leaves with the first turn: measure it before it does.
232
+ if (firstTurn) await flights.launch([ALIA_TO_PROFILE]);
233
+ send(text, images ? { images } : undefined);
234
+ },
235
+ [startsElsewhere, onStart, firstTurn, flights, send],
236
+ );
237
+
238
+ // The conversation this chat was opened for, sent once.
239
+ const initialSent = useRef(false);
240
+ useEffect(() => {
241
+ if (!initialTurn || initialSent.current) return;
242
+ initialSent.current = true;
243
+ send(initialTurn.text, initialTurn.images ? { images: initialTurn.images } : undefined);
244
+ }, [initialTurn, send]);
126
245
 
127
246
  useEffect(() => {
128
247
  onStopReady?.(stop);
@@ -156,6 +275,26 @@ function AliaChatContentBody({
156
275
  [pickImage],
157
276
  );
158
277
 
278
+ // Suggestions over the composer on an empty chat, as the Alia app has them.
279
+ const suggestionQuery = useSuggestionQuery(inputValue, messages.length === 0);
280
+ useEffect(() => {
281
+ onSuggestionQuery?.(suggestionQuery);
282
+ }, [suggestionQuery, onSuggestionQuery]);
283
+ const handlePickSuggestion = useCallback(
284
+ (suggestion: AliaSuggestion) => {
285
+ setInputValue('');
286
+ submitTurn(suggestion.text);
287
+ },
288
+ [submitTurn],
289
+ );
290
+ const suggestions = useComposerSuggestions({
291
+ draft: inputValue,
292
+ enabled: messages.length === 0,
293
+ welcome: welcomeSuggestions,
294
+ matches: suggestionMatches,
295
+ onPick: handlePickSuggestion,
296
+ });
297
+
159
298
  const handleSubmit = useCallback(
160
299
  async (value: string) => {
161
300
  const text = value.trim();
@@ -167,7 +306,7 @@ function AliaChatContentBody({
167
306
  const images = await Promise.all(
168
307
  pending.map((photo) => imageUriToDataUrl(photo.uri, photo.mimeType)),
169
308
  );
170
- send(text, { images });
309
+ submitTurn(text, images);
171
310
  } catch (err) {
172
311
  // Keep the draft: the photos could not be read, nothing was sent.
173
312
  setInputValue(value);
@@ -175,7 +314,7 @@ function AliaChatContentBody({
175
314
  reportError(err instanceof Error ? err.message : 'Could not attach the photos');
176
315
  }
177
316
  },
178
- [send, photos, reportError],
317
+ [submitTurn, photos, reportError],
179
318
  );
180
319
 
181
320
  // ── Dictation: the pill's mic toggles it, the transcript lands in the field ──
@@ -263,15 +402,11 @@ function AliaChatContentBody({
263
402
  const welcomeComponent = useMemo(() => {
264
403
  if (!welcomeGreeting || messages.length > 0) return undefined;
265
404
  return (
266
- <AliaWelcomeMessage
267
- greeting={welcomeGreeting}
268
- subtitle={welcomeSubtitle}
269
- suggestions={welcomeSuggestions}
270
- onSuggestionPress={send}
271
- hideMark
272
- />
405
+ <View className="flex-1 items-center justify-center px-4 py-10">
406
+ <AliaWelcomeMessage greeting={welcomeGreeting} subtitle={welcomeSubtitle} />
407
+ </View>
273
408
  );
274
- }, [welcomeGreeting, welcomeSubtitle, welcomeSuggestions, send, messages.length]);
409
+ }, [welcomeGreeting, welcomeSubtitle, messages.length]);
275
410
 
276
411
  // ── Ambient wave — one persistent overlay across idle/voice/TTS/STT ──
277
412
  const wave = useAmbientWave({
@@ -327,8 +462,24 @@ function AliaChatContentBody({
327
462
  </Suspense>
328
463
  ) : (
329
464
  <>
465
+ <ComposerSuggestions
466
+ completions={suggestions.completions}
467
+ selected={suggestions.selected}
468
+ onPick={handlePickSuggestion}
469
+ />
470
+ {/* Beside the loader, not inside the pill: its light traces the box it wraps. */}
471
+ {asking ? (
472
+ <View className="mb-2">
473
+ <QuestionCard
474
+ questions={questionsAsked}
475
+ onSubmit={(message) => send(message)}
476
+ onDismiss={() => setDismissedQuestionsOf(lastMessage?.id ?? null)}
477
+ />
478
+ </View>
479
+ ) : null}
330
480
  <ComposerLoader active={isStreaming}>
331
481
  <ComposerPill
482
+ onKeyPress={suggestions.onKeyPress}
332
483
  surface={false}
333
484
  frosted={isStreaming}
334
485
  value={inputValue}
@@ -353,8 +504,8 @@ function AliaChatContentBody({
353
504
 
354
505
  const hasMessages = messages.length > 0;
355
506
  const headerNode = useMemo(
356
- () => header?.({ hasMessages, clear, markState }),
357
- [header, hasMessages, clear, markState],
507
+ () => header?.({ hasMessages, clear, markState, agents }),
508
+ [header, hasMessages, clear, markState, agents],
358
509
  );
359
510
  const background = useMemo(
360
511
  () => (
@@ -370,34 +521,44 @@ function AliaChatContentBody({
370
521
  );
371
522
 
372
523
  return (
373
- <AiChatContainer
374
- header={headerNode}
375
- composer={composer}
376
- // The thread scrolls behind the header and the composer, which fade in over it — as
377
- // in the app's conversation. The wave is the container's background layer, between
378
- // its surface and the turns.
379
- floatingChrome
380
- background={background}
381
- >
382
- <View className="flex-1">
383
- {/* Welcome or Messages */}
384
- <AliaChatMessageList
385
- messages={messages}
386
- isStreaming={isStreaming}
387
- onReadAloud={handleReadAloud}
388
- ttsActiveMessageId={tts.activeMessageId}
389
- ttsPlaybackState={tts.playbackState}
390
- onEditMessage={onEditMessage}
391
- onThumbsUp={onThumbsUp}
392
- onThumbsDown={onThumbsDown}
393
- onApprovePlan={onApprovePlan}
394
- onRejectPlan={onRejectPlan}
395
- onAnswerQuestions={(message) => send(message)}
396
- renderMarkdown={renderMarkdown}
397
- welcomeComponent={welcomeComponent}
398
- />
399
- </View>
400
- </AiChatContainer>
524
+ <AliaFlightsProvider value={flights}>
525
+ <AiChatContainer
526
+ header={headerNode}
527
+ composer={
528
+ bottomInset > 0 ? (
529
+ <View style={{ paddingBottom: bottomInset }}>{composer}</View>
530
+ ) : (
531
+ composer
532
+ )
533
+ }
534
+ // The thread scrolls behind the header and the composer, which fade in over it — as
535
+ // in the app's conversation. The wave is the container's background layer, between
536
+ // its surface and the turns.
537
+ floatingChrome
538
+ // The sheet or screen around it owns the fill and the shape; the container adds neither.
539
+ surface={false}
540
+ background={background}
541
+ >
542
+ <View className="flex-1">
543
+ {/* Welcome or Messages */}
544
+ <AliaChatMessageList
545
+ messages={messages}
546
+ isStreaming={isStreaming}
547
+ onReadAloud={handleReadAloud}
548
+ ttsActiveMessageId={tts.activeMessageId}
549
+ ttsPlaybackState={tts.playbackState}
550
+ onEditMessage={onEditMessage}
551
+ onThumbsUp={onThumbsUp}
552
+ onThumbsDown={onThumbsDown}
553
+ onApprovePlan={onApprovePlan ?? approvePlan}
554
+ onRejectPlan={onRejectPlan ?? rejectPlan}
555
+ renderMarkdown={renderMarkdown}
556
+ welcomeComponent={welcomeComponent}
557
+ />
558
+ </View>
559
+ </AiChatContainer>
560
+ {sharedFlights ? null : <AvatarFlightLayer flights={ownFlights} />}
561
+ </AliaFlightsProvider>
401
562
  );
402
563
  }
403
564