@alia.onl/sdk 15.8.0 → 19.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 (142) hide show
  1. package/CHANGELOG.md +262 -13
  2. package/README.md +223 -151
  3. package/package.json +16 -6
  4. package/src/animations.ts +0 -1
  5. package/src/components/AliaAskProvider.tsx +127 -0
  6. package/src/components/AliaChatContent.tsx +604 -392
  7. package/src/components/AliaChatHeader.tsx +14 -38
  8. package/src/components/AliaChatScreen.tsx +56 -31
  9. package/src/components/AliaChatSheet.tsx +110 -36
  10. package/src/components/AliaComposer.tsx +1 -1
  11. package/src/components/AliaParticipants.tsx +202 -0
  12. package/src/components/AliaParticipantsHeader.tsx +60 -0
  13. package/src/components/AliaSharedConversation.tsx +86 -0
  14. package/src/components/AliaWelcomeMessage.tsx +18 -5
  15. package/src/components/Image.tsx +12 -0
  16. package/src/components/Markdown.tsx +498 -14
  17. package/src/components/PlanPreviewCard.tsx +7 -49
  18. package/src/components/ReplyMarkdown.tsx +359 -0
  19. package/src/components/RichBlocks.tsx +195 -0
  20. package/src/components/canvas/CanvasComponent.tsx +59 -0
  21. package/src/components/canvas/ChartRenderer.tsx +73 -0
  22. package/src/components/canvas/CodeRenderer.tsx +24 -0
  23. package/src/components/canvas/FormRenderer.tsx +101 -0
  24. package/src/components/canvas/MarkdownRenderer.tsx +14 -0
  25. package/src/components/canvas/TableRenderer.tsx +47 -0
  26. package/src/components/thought/FilesAndSources.tsx +120 -0
  27. package/src/components/thought/ThoughtPanel.tsx +417 -0
  28. package/src/components/thought/ToolStep.tsx +141 -0
  29. package/src/components/thread/AgentResultCard.tsx +125 -0
  30. package/src/components/thread/AgentTaskCard.tsx +127 -0
  31. package/src/components/thread/AliaThread.tsx +401 -0
  32. package/src/components/thread/MessageBlockBoundary.tsx +78 -0
  33. package/src/components/thread/NewConversationOffer.tsx +54 -0
  34. package/src/components/thread/ThreadRow.tsx +458 -0
  35. package/src/components/thread/ThreadSearch.tsx +129 -0
  36. package/src/components/thread/ToolResultCard.tsx +254 -0
  37. package/src/components/thread/TurnMenu.tsx +54 -0
  38. package/src/components/thread/TurnStatusLine.tsx +34 -0
  39. package/src/components/voice/AliaVoiceCall.tsx +21 -0
  40. package/src/components/voice/VoiceControls.tsx +2 -28
  41. package/src/components/voice/VoiceSession.tsx +39 -11
  42. package/src/hooks/useAliaActivity.ts +2 -2
  43. package/src/hooks/useAtBottom.ts +51 -0
  44. package/src/hooks/useAttachMenu.ts +94 -0
  45. package/src/hooks/useChatComposer.ts +299 -0
  46. package/src/hooks/useChatVoice.ts +129 -0
  47. package/src/hooks/useDocumentPicker.ts +3 -29
  48. package/src/hooks/useFailedTurnToast.ts +43 -0
  49. package/src/hooks/useMessageVote.ts +50 -0
  50. package/src/hooks/useOpenThought.ts +79 -0
  51. package/src/hooks/usePendingQuestions.ts +2 -2
  52. package/src/hooks/useReadAloud.ts +92 -0
  53. package/src/hooks/useShareHandler.ts +24 -0
  54. package/src/hooks/useSoundEffects.ts +1 -1
  55. package/src/hooks/useThoughtScope.ts +59 -0
  56. package/src/hooks/useThreadRows.ts +145 -0
  57. package/src/hooks/useThreadView.ts +68 -0
  58. package/src/hooks/useTimelineWindow.ts +256 -0
  59. package/src/hooks/useVoiceLauncher.ts +180 -0
  60. package/src/hooks/useVoiceRoom.ts +4 -4
  61. package/src/index.ts +43 -29
  62. package/src/lib/activity-store.ts +1 -1
  63. package/src/lib/add-menu.ts +7 -0
  64. package/src/lib/call-thread.ts +33 -0
  65. package/src/lib/citations.ts +79 -2
  66. package/src/lib/clipboard.ts +23 -0
  67. package/src/lib/content-utils.ts +10 -0
  68. package/src/lib/copy-en.ts +284 -0
  69. package/src/lib/copy.ts +32 -0
  70. package/src/lib/document-picker.ts +31 -0
  71. package/src/lib/flights.ts +22 -13
  72. package/src/lib/focus-return.ts +66 -0
  73. package/src/lib/keyboard.native.tsx +19 -0
  74. package/src/lib/keyboard.tsx +9 -0
  75. package/src/lib/labels.tsx +69 -17
  76. package/src/lib/participants.ts +86 -0
  77. package/src/lib/power-level-options.ts +66 -0
  78. package/src/lib/questions.ts +2 -2
  79. package/src/lib/task-labels.ts +4 -4
  80. package/src/lib/tool-call-status.ts +1 -1
  81. package/src/lib/tool-icons.ts +36 -0
  82. package/src/lib/tool-renderers.tsx +48 -0
  83. package/src/lib/turn-state.ts +2 -2
  84. package/src/lib/voice-call-request.ts +49 -0
  85. package/src/lib/voice-call.ts +98 -35
  86. package/src/lib/voice-controls-labels.ts +31 -0
  87. package/src/lib/voice-protocol.ts +34 -15
  88. package/src/lib/work-log.ts +6 -6
  89. package/src/runtime/agents.ts +151 -0
  90. package/src/runtime/api.ts +141 -0
  91. package/src/runtime/app-assertion.ts +53 -0
  92. package/src/runtime/attachment-utils.ts +96 -0
  93. package/src/runtime/composer-draft-store.ts +270 -0
  94. package/src/runtime/config.tsx +186 -0
  95. package/src/runtime/conversation-store.ts +49 -0
  96. package/src/runtime/conversations.ts +557 -0
  97. package/src/runtime/error-utils.ts +104 -0
  98. package/src/runtime/host-tools.ts +140 -0
  99. package/src/runtime/index.ts +113 -0
  100. package/src/runtime/message-history.ts +124 -0
  101. package/src/runtime/model.ts +39 -0
  102. package/src/runtime/oxy-files.ts +37 -0
  103. package/src/runtime/power-levels.ts +66 -0
  104. package/src/runtime/query-keys.ts +34 -0
  105. package/src/runtime/random-uuid.ts +25 -0
  106. package/src/runtime/routes.ts +51 -0
  107. package/src/runtime/sharing.ts +165 -0
  108. package/src/runtime/sse-frame-reader.ts +88 -0
  109. package/src/runtime/stream-events.ts +146 -0
  110. package/src/runtime/stream-outcome.ts +21 -0
  111. package/src/runtime/thought-store.ts +103 -0
  112. package/src/runtime/thread/agent-activity.ts +261 -0
  113. package/src/runtime/thread/automation-labels.ts +79 -0
  114. package/src/runtime/thread/conversation-export.ts +135 -0
  115. package/src/runtime/thread/message-days.ts +120 -0
  116. package/src/runtime/thread/message-processor.ts +93 -0
  117. package/src/runtime/thread/task-utils.ts +11 -0
  118. package/src/runtime/thread/thought-utils.ts +752 -0
  119. package/src/runtime/thread/thread-history.ts +139 -0
  120. package/src/runtime/thread/timeline.ts +202 -0
  121. package/src/runtime/thread/title-tags.ts +8 -0
  122. package/src/runtime/thread/tool-cards.ts +383 -0
  123. package/src/runtime/turn-failure.ts +108 -0
  124. package/src/runtime/types.ts +148 -0
  125. package/src/runtime/usage-limit-error.ts +44 -0
  126. package/src/runtime/use-agent-activity.ts +80 -0
  127. package/src/runtime/use-conversation-stream.ts +1433 -0
  128. package/src/runtime/use-conversation-thread.ts +556 -0
  129. package/src/runtime/use-start-conversation.ts +107 -0
  130. package/src/runtime/use-thread-history.ts +64 -0
  131. package/src/runtime/use-thread-search.ts +95 -0
  132. package/src/runtime/use-turn-edit.ts +99 -0
  133. package/src/tools.ts +2 -2
  134. package/src/types.ts +18 -47
  135. package/src/components/AliaChatMessageList.tsx +0 -532
  136. package/src/components/ThinkingBlock.tsx +0 -43
  137. package/src/hooks/useAliaChat.ts +0 -573
  138. package/src/lib/catalogue.ts +0 -236
  139. package/src/lib/chat-stream.ts +0 -655
  140. package/src/lib/chat-transport.ts +0 -98
  141. package/src/lib/delegation.ts +0 -34
  142. package/src/lib/work-log-copy.ts +0 -61
@@ -0,0 +1,180 @@
1
+ /**
2
+ * The host side of a voice call, shared by every surface that offers one.
3
+ *
4
+ * It owns whether a call is open, the state it reports back (for the ambient
5
+ * wave, the sound effects and the transcript), the gate before one opens (plan,
6
+ * credits, sign-in: the host answers, the SDK tells the person), and the end of
7
+ * a call that is no longer the screen's — another screen on show, another
8
+ * conversation, another account. `sessionProps` is everything `VoiceSession`
9
+ * needs; the sheet, `AliaChatScreen` and the Alia app all spread it.
10
+ *
11
+ * Nothing here imports the call or its audio engine.
12
+ */
13
+
14
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
15
+ import { toast } from '@oxy.so/bloom/toast';
16
+ import { useLabels, type VoiceBlockReason } from '../lib/labels';
17
+ import { isRequestFor, useVoiceCallRequests } from '../lib/voice-call-request';
18
+ import type { VoiceNotice, VoiceSessionProps, VoiceSessionState } from '../types';
19
+
20
+ export interface UseVoiceLauncherOptions {
21
+ /** Alia API base URL. */
22
+ apiUrl?: string;
23
+ /** The conversation open on this screen, which is where the call lives. */
24
+ conversationId?: string;
25
+ /**
26
+ * Called after the gate, when no conversation is open: creates (and shows) the one the call
27
+ * will live in and returns its id, or `undefined` when it could not be made (the call then
28
+ * does not open). Every call has a conversation, so its transcript has a home. The call opens
29
+ * on the screen that shows that conversation: this one, or the one the host navigates to.
30
+ */
31
+ resolveConversation?: () => Promise<string | undefined>;
32
+ /** Whose call it would be. A live call ends when the account, or the open conversation, changes. */
33
+ accountId?: string | null;
34
+ /** The agent the call talks to; omitted, Alia. */
35
+ agentId?: string;
36
+ /** Language spoken (BCP-47); omitted, the platform's. */
37
+ locale?: string;
38
+ /** Which of the product's two voices answers. */
39
+ voice?: 'male' | 'female';
40
+ /** Alia stored messages of the call in the conversation: refresh the thread. */
41
+ onMessagesSaved?: VoiceSessionProps['onMessagesSaved'];
42
+ /**
43
+ * The gate before a call opens: why it may not (no account, no plan, no credits), or `null`.
44
+ * The person is told (`labels.voiceBlocked`); `onVoiceBlocked` adds what only the host can do.
45
+ */
46
+ canStartVoice?: () => VoiceBlockReason | null;
47
+ /** The call was not opened for `reason`: sign the person in, open the plans… */
48
+ onVoiceBlocked?: (reason: VoiceBlockReason) => void;
49
+ /** Where the call's notices go; omitted, Bloom's toast. */
50
+ onNotice?: (notice: VoiceNotice) => void;
51
+ /** A call is over, however it ended: refresh credits, the thread… */
52
+ onCallEnded?: () => void;
53
+ /** Whether the screen holding the call is the one on show. A live call ends when it is not. */
54
+ isFocused?: boolean;
55
+ }
56
+
57
+ export interface VoiceLauncher {
58
+ /** A call is open (or being opened). */
59
+ isActive: boolean;
60
+ /** What the call reports back; `null` until it is under way. */
61
+ state: VoiceSessionState | null;
62
+ /** Open a call, after the gate. Does nothing while one is open or the screen is not on show. */
63
+ start: () => Promise<void>;
64
+ /** End the call. */
65
+ end: () => void;
66
+ /** Spread onto `VoiceSession` (or `AliaVoiceCall`). */
67
+ sessionProps: VoiceSessionProps;
68
+ }
69
+
70
+ /** Bloom's toast, by kind: the default home of a call's notices. */
71
+ export function toastNotice({ kind, text }: VoiceNotice): void {
72
+ if (kind === 'error') toast.error(text);
73
+ else if (kind === 'warning') toast.warning(text);
74
+ else toast.info(text);
75
+ }
76
+
77
+ export function useVoiceLauncher(options: UseVoiceLauncherOptions = {}): VoiceLauncher {
78
+ const labels = useLabels();
79
+ const {
80
+ apiUrl,
81
+ conversationId,
82
+ resolveConversation,
83
+ accountId = null,
84
+ agentId,
85
+ locale,
86
+ voice,
87
+ onMessagesSaved,
88
+ canStartVoice,
89
+ onVoiceBlocked,
90
+ onNotice,
91
+ onCallEnded,
92
+ isFocused = true,
93
+ } = options;
94
+ const [isActive, setIsActive] = useState(false);
95
+ const [state, setState] = useState<VoiceSessionState | null>(null);
96
+ const latest = useRef({
97
+ canStartVoice,
98
+ onVoiceBlocked,
99
+ onNotice,
100
+ onCallEnded,
101
+ resolveConversation,
102
+ labels,
103
+ });
104
+ latest.current = {
105
+ canStartVoice,
106
+ onVoiceBlocked,
107
+ onNotice,
108
+ onCallEnded,
109
+ resolveConversation,
110
+ labels,
111
+ };
112
+
113
+ const notify = useCallback((notice: VoiceNotice) => {
114
+ (latest.current.onNotice ?? toastNotice)(notice);
115
+ }, []);
116
+
117
+ const end = useCallback(() => {
118
+ setIsActive(false);
119
+ setState(null);
120
+ latest.current.onCallEnded?.();
121
+ }, []);
122
+
123
+ const start = useCallback(async () => {
124
+ if (isActive || !isFocused) return;
125
+ const reason = latest.current.canStartVoice?.() ?? null;
126
+ if (reason !== null) {
127
+ notify({
128
+ kind: reason === 'credits-exhausted' ? 'error' : 'info',
129
+ text: latest.current.labels.voiceBlocked(reason),
130
+ });
131
+ latest.current.onVoiceBlocked?.(reason);
132
+ return;
133
+ }
134
+ // The gate first: a call that may not open must not leave an empty conversation behind.
135
+ const home = conversationId ?? (await latest.current.resolveConversation?.());
136
+ if (home === undefined) return;
137
+ // The call opens where its conversation is shown (see `useVoiceCallRequests`): here, at once,
138
+ // when it already is; wherever the host takes the new conversation, when it was just made.
139
+ useVoiceCallRequests.getState().ask(home, accountId);
140
+ }, [isActive, isFocused, conversationId, accountId, notify]);
141
+
142
+ const request = useVoiceCallRequests((store) => store.request);
143
+ useEffect(() => {
144
+ if (isActive || !isFocused || !isRequestFor(request, conversationId, accountId)) return;
145
+ useVoiceCallRequests.getState().clear();
146
+ setIsActive(true);
147
+ }, [request, isActive, isFocused, conversationId, accountId]);
148
+
149
+ // A call belongs to one account and one conversation, on the screen that shows it: it ends when
150
+ // the screen goes out of focus, the account changes, or the screen moves to another conversation.
151
+ const openedIn = useRef<{ conversationId: string | undefined; accountId: string | null }>(
152
+ undefined,
153
+ );
154
+ useEffect(() => {
155
+ if (!isActive) {
156
+ openedIn.current = undefined;
157
+ return;
158
+ }
159
+ openedIn.current ??= { conversationId, accountId };
160
+ const call = openedIn.current;
161
+ if (!isFocused || call.accountId !== accountId || call.conversationId !== conversationId) end();
162
+ }, [isActive, isFocused, accountId, conversationId, end]);
163
+
164
+ const sessionProps = useMemo<VoiceSessionProps>(
165
+ () => ({
166
+ ...(apiUrl === undefined ? {} : { apiUrl }),
167
+ ...(conversationId === undefined ? {} : { conversationId }),
168
+ ...(agentId === undefined ? {} : { agentId }),
169
+ ...(locale === undefined ? {} : { locale }),
170
+ ...(voice === undefined ? {} : { voice }),
171
+ ...(onMessagesSaved === undefined ? {} : { onMessagesSaved }),
172
+ onNotice: notify,
173
+ onStateChange: setState,
174
+ onEnd: end,
175
+ }),
176
+ [apiUrl, conversationId, agentId, locale, voice, onMessagesSaved, notify, end],
177
+ );
178
+
179
+ return { isActive, state, start, end, sessionProps };
180
+ }
@@ -25,6 +25,7 @@ import { VoiceCall, type VoiceCallSnapshot, type VoiceSocketFactory } from '../l
25
25
  import { VoiceLevelChannel, type VoiceLevelSource } from '../lib/voice-levels';
26
26
  import type { ProductVoice, VoiceCloseReason } from '../lib/voice-protocol';
27
27
  import { ALIA_API_URL } from '../lib/origins';
28
+ import { useAppAssertionToken } from '../runtime/app-assertion';
28
29
 
29
30
  export interface UseVoiceRoomOptions {
30
31
  apiUrl?: string;
@@ -43,8 +44,6 @@ export interface UseVoiceRoomOptions {
43
44
  lang?: string;
44
45
  /** Alia stored messages of the call: refresh the thread. */
45
46
  onMessagesSaved?: (conversationId: string, messageIds: readonly string[]) => void;
46
- /** A tool filed an action for the person's OK; its card is in the conversation. */
47
- onApprovalRequested?: (toolName: string) => void;
48
47
  /** Test seams: the device's audio and the socket. */
49
48
  createAudioEngine?: () => VoiceAudioEngine;
50
49
  openSocket?: VoiceSocketFactory;
@@ -101,6 +100,7 @@ export function agentStateOf(state: VoiceCallState): AgentState {
101
100
 
102
101
  export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
103
102
  const { oxyServices } = useOxy();
103
+ const getAppAssertion = useAppAssertionToken();
104
104
  const levels = useMemo(() => new VoiceLevelChannel(), []);
105
105
  const [snapshot, setSnapshot] = useState<VoiceCallSnapshot>(IDLE);
106
106
  const callRef = useRef<VoiceCall | null>(null);
@@ -121,6 +121,7 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
121
121
  const call = new VoiceCall({
122
122
  apiUrl: current.apiUrl || ALIA_API_URL,
123
123
  getToken,
124
+ getAppAssertion,
124
125
  conversationId: current.conversationId ?? newConversationId(),
125
126
  ...(current.agentId === undefined ? {} : { agentId: current.agentId }),
126
127
  voice: current.voicePreference ?? 'female',
@@ -133,11 +134,10 @@ export function useVoiceRoom(options: UseVoiceRoomOptions = {}) {
133
134
  },
134
135
  onMessagesSaved: (conversationId, ids) =>
135
136
  optionsRef.current.onMessagesSaved?.(conversationId, ids),
136
- onApprovalRequested: (toolName) => optionsRef.current.onApprovalRequested?.(toolName),
137
137
  });
138
138
  callRef.current = call;
139
139
  await call.start();
140
- }, [getToken, levels]);
140
+ }, [getToken, getAppAssertion, levels]);
141
141
 
142
142
  /** Hang up and forget the call: the room is back to `disconnected`. */
143
143
  const disconnect = useCallback(() => {
package/src/index.ts CHANGED
@@ -3,17 +3,39 @@
3
3
  // Reusable chat UI, voice, and streaming components for Oxy apps
4
4
 
5
5
  // ── Main components ──
6
- export type { AliaChatAgent, AliaChatState } from './components/AliaChatContent';
6
+ export { AliaChatContent } from './components/AliaChatContent';
7
+ export type {
8
+ AliaChatAgentRef,
9
+ AliaChatChrome,
10
+ AliaChatContentProps,
11
+ AliaChatState,
12
+ } from './components/AliaChatContent';
13
+ export { AliaParticipants, joinFlights, useParticipants } from './components/AliaParticipants';
14
+ export type { AliaParticipantsProps, UseParticipantsOptions } from './components/AliaParticipants';
15
+ export { AliaParticipantsHeader } from './components/AliaParticipantsHeader';
16
+ export type { AliaParticipantsHeaderProps } from './components/AliaParticipantsHeader';
17
+ export { ALIA_LEAD, leadOf } from './lib/participants';
18
+ export type { AliaChatParticipants, AliaLead, AliaParticipant } from './lib/participants';
19
+ export { ANCHORS, leadToProfile } from './lib/flights';
20
+ export { parseCatalogueTool, toolRendererKey } from './lib/tool-renderers';
21
+ export type { AliaToolRenderers, AliaToolResult } from './lib/tool-renderers';
22
+ export { AliaAskProvider, useAskAgent } from './components/AliaAskProvider';
23
+ export type { AliaAsk, AliaAskAgents, AliaAskProviderProps } from './components/AliaAskProvider';
24
+ export { AliaSharedConversation } from './components/AliaSharedConversation';
25
+ export type { AliaSharedConversationProps } from './components/AliaSharedConversation';
26
+ export type { AliaAddMenu } from './lib/add-menu';
27
+ export type { AliaVoiceOptions } from './hooks/useChatVoice';
7
28
  export { AliaChatSheet } from './components/AliaChatSheet';
8
29
  export type { AliaChatSheetProps, AliaChatSheetRef } from './components/AliaChatSheet';
9
- export type { AliaTurn } from './components/AliaChatContent';
30
+ export type { AliaTurn } from './hooks/useChatComposer';
10
31
  export { AliaChatScreen } from './components/AliaChatScreen';
11
32
  export type { AliaChatScreenProps } from './components/AliaChatScreen';
12
33
 
13
34
  // ── Chat UI components ──
14
35
  export { ALIA_CHARACTER_RUNTIME_URL } from './lib/character-runtime';
15
36
  export { DEFAULT_LABELS } from './lib/labels';
16
- export type { AliaChatLabels } from './lib/labels';
37
+ export { AliaChatLabelsProvider, useLabels } from './lib/labels';
38
+ export type { AliaChatLabels, VoiceBlockReason } from './lib/labels';
17
39
  export { VoiceModeIcon } from './components/VoiceModeIcon';
18
40
  export { AmbientField, PARALLAX_DURATION, PARALLAX_EASE } from './components/AmbientField';
19
41
  export type { AmbientFieldProps } from './components/AmbientField';
@@ -27,6 +49,16 @@ export { IdentityMark } from './components/IdentityMark';
27
49
  export type { IdentityMarkProps, IdentityMarkState } from './components/IdentityMark';
28
50
  export { AliaMarkdown } from './components/Markdown';
29
51
  export type { AliaMarkdownProps, RenderCodeBlock } from './components/Markdown';
52
+ export { ReplyMarkdown } from './components/ReplyMarkdown';
53
+ export { AliaThread } from './components/thread/AliaThread';
54
+ export type { AliaThreadProps, NewConversationOfferProps } from './components/thread/AliaThread';
55
+ export { ThoughtPanel } from './components/thought/ThoughtPanel';
56
+ export type { ThoughtPanelProps } from './components/thought/ThoughtPanel';
57
+ export type { OutputActions } from './components/thought/FilesAndSources';
58
+ export { CanvasComponent } from './components/canvas/CanvasComponent';
59
+ export { restoreOpenerFocus } from './lib/focus-return';
60
+ export { buildPowerLevelOptions, POWER_LEVEL_ICONS } from './lib/power-level-options';
61
+ export type { DeviceModel } from './lib/power-level-options';
30
62
  export { AliaWelcomeMessage } from './components/AliaWelcomeMessage';
31
63
  export { PlanPreviewCard } from './components/PlanPreviewCard';
32
64
  export { AskedQuestions, QuestionCard } from './components/QuestionCard';
@@ -49,11 +81,8 @@ export {
49
81
  } from './lib/questions';
50
82
  export type { AskedOption, AskedQuestion, QuestionAnswer } from './lib/questions';
51
83
 
52
- // ── Chat hook ──
53
- export { useAliaChat } from './hooks/useAliaChat';
54
- export type { UseAliaChatOptions, UseAliaChatReturn } from './hooks/useAliaChat';
55
- export { AliaChatStreamError } from './lib/chat-stream';
56
- export type { AliaChatFailure } from './lib/chat-stream';
84
+ // ── Conversation runtime: the one data layer under the sheet, the screen and the Alia app ──
85
+ export * from './runtime';
57
86
 
58
87
  // ── Read aloud and dictation (no call, no audio engine) ──
59
88
  export { useTTS } from './hooks/useTTS';
@@ -67,36 +96,21 @@ export type { VoiceErrorCode } from './lib/speech-messages';
67
96
  export { SpeechClipError } from './lib/speech-synthesis';
68
97
  export type { SpeechClipErrorCode } from './lib/speech-synthesis';
69
98
 
70
- // ── Model catalogue (GET /catalogue) ──
71
- // Exported so a consumer building its own picker reads the real models the
72
- // server offers; the SDK itself ships no model identifier.
73
- export {
74
- fetchCatalogue,
75
- parseCatalogue,
76
- resolveSelection,
77
- resolveModelId,
78
- clearCatalogueCache,
79
- } from './lib/catalogue';
80
- export type { Catalogue, CatalogueEntry, ModelSelection, ReasoningEffort } from './lib/catalogue';
81
-
82
99
  // ── Types ──
83
100
  export type {
84
- ChatMessage,
85
- ToolInvocation,
86
101
  AliaAttachment,
87
102
  AliaSuggestion,
88
103
  ResearchProgress,
89
104
  ResearchSource,
90
- PendingPlan,
105
+ MessagePlan,
91
106
  PlanStep,
92
107
  } from './types';
93
108
 
94
- // ── Voice capability contract (the implementation lives in `./voice`) ──
95
- export type {
96
- VoiceSessionComponent,
97
- VoiceSessionProps,
98
- VoiceSessionState,
99
- } from './types';
109
+ // ── Voice: part of the conversation screen; the host brings callbacks (see `AliaVoiceOptions`) ──
110
+ export type { VoiceNotice, VoiceSessionProps, VoiceSessionState } from './types';
111
+ export { AliaVoiceCall } from './components/voice/AliaVoiceCall';
112
+ export { useVoiceLauncher, toastNotice } from './hooks/useVoiceLauncher';
113
+ export type { UseVoiceLauncherOptions, VoiceLauncher } from './hooks/useVoiceLauncher';
100
114
 
101
115
  // ── UI Primitives (NativeWind) ──
102
116
  export { cn, formatFileSize } from './lib/utils';
@@ -6,7 +6,7 @@ type Sources = Readonly<Record<string, AliaAvatarState>>;
6
6
  /**
7
7
  * What Alia is doing, from every place that can know. Two sources feed it and neither owns it:
8
8
  *
9
- * - `local`: the turns running in THIS app, one entry per `useAliaChat` instance (the sheet);
9
+ * - `local`: the turns running in THIS app, one entry per `useConversationThread` instance;
10
10
  * - `remote`: runs the server reports for the person, one entry per agent session, so a button
11
11
  * reacts to work that is not in any open chat.
12
12
  *
@@ -0,0 +1,7 @@
1
+ import type { ComposerPanelAddMenuGroup } from '@oxy.so/bloom/composer-panel';
2
+
3
+ /** The rows of the composer's add menu, and what pressing one does. */
4
+ export interface AliaAddMenu {
5
+ groups: readonly ComposerPanelAddMenuGroup[];
6
+ onSelect: (rowId: string) => void;
7
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * What the thread shows while a call is open: the text messages it held when
3
+ * the call started, then the call's messages.
4
+ *
5
+ * A call message the server has stored is shown AS STORED — the copy a reload
6
+ * or a resume shows, with its tools' arguments and results, which the call's
7
+ * socket never carries. Same id either way (the call names every message by the
8
+ * id it is stored under), so swapping one for the other never adds a message.
9
+ */
10
+
11
+ import type { Message } from '../runtime/types';
12
+ import type { VoiceMessage } from '../types';
13
+
14
+ export function callThread(
15
+ before: readonly Message[],
16
+ call: readonly VoiceMessage[],
17
+ stored: readonly Message[] | undefined,
18
+ ): Message[] {
19
+ const storedById = new Map((stored ?? []).map((message) => [message.id, message]));
20
+ return [...before, ...call.map((message) => storedById.get(message.id) ?? fromCall(message))];
21
+ }
22
+
23
+ function fromCall(message: VoiceMessage): Message {
24
+ return {
25
+ id: message.id,
26
+ role: message.role,
27
+ content: message.content,
28
+ toolInvocations: message.toolInvocations,
29
+ createdAt: new Date(message.timestamp).toISOString(),
30
+ source: 'voice' as const,
31
+ speaker: message.speaker,
32
+ };
33
+ }
@@ -1,4 +1,4 @@
1
- import type { ToolInvocation } from '../types';
1
+ import type { ToolInvocation } from '../runtime/types';
2
2
  import { domainOf } from './domain';
3
3
 
4
4
  export interface CitationSource {
@@ -87,7 +87,7 @@ export function normalizeCitationMarkers(text: string, knownIds?: Iterable<numbe
87
87
 
88
88
  /**
89
89
  * The accessible name of one citation: what a screen reader says for the
90
- * link, and what `CustomMarkdown` puts in the link's title.
90
+ * link, and what `ReplyMarkdown` puts in the link's title.
91
91
  */
92
92
  export function citationLabel(source: CitationSource): string {
93
93
  return `Source ${source.id}: ${source.title}`;
@@ -113,3 +113,80 @@ export function linkifyCitations(text: string, sources: CitationSource[]): strin
113
113
  return `[${n}](${source.url} "${title}")`;
114
114
  });
115
115
  }
116
+
117
+ /** One entry of a rendered references section. */
118
+ export interface ReferenceEntry {
119
+ id: number;
120
+ title: string;
121
+ url: string;
122
+ domain: string;
123
+ }
124
+
125
+ /**
126
+ * Where the references section starts: the API writes `## References` after a
127
+ * rule; the heading is matched loosely so an older answer, or one
128
+ * written in Spanish by the model itself, splits the same way.
129
+ */
130
+ const REFERENCES_HEADING_RE =
131
+ /(?:^|\n)(?:---\s*\n+)?#{1,3}\s*(?:References|Referencias|Sources|Fuentes)\s*:?\s*(?:\n|$)/i;
132
+
133
+ /** `[n] [Title](url)` (new) or `[n] Title` + a URL line (legacy). */
134
+ const REFERENCE_LINE_RE = /^\s*\[(\d{1,3})\]\s*(.*)$/;
135
+ // The title may carry escaped brackets (`React \[Wikipedia\]`), which the
136
+ // API writes for a title that contains them.
137
+ const MARKDOWN_LINK_RE = /^\[((?:\\.|[^\]\\])*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)\s*$/;
138
+ const URL_RE = /https?:\/\/[^\s)]+/;
139
+
140
+ /**
141
+ * Split an answer into its body and the entries of its references section.
142
+ *
143
+ * `references` is `null` when the section is absent or none of its lines
144
+ * parse, in which case the caller renders the whole text as Markdown and
145
+ * nothing is lost. Known sources fill in a title or URL a legacy line lacks.
146
+ */
147
+ export function splitReferences(
148
+ text: string,
149
+ sources: CitationSource[] = [],
150
+ ): { body: string; references: ReferenceEntry[] | null } {
151
+ const match = REFERENCES_HEADING_RE.exec(text);
152
+ if (!match) return { body: text, references: null };
153
+
154
+ const body = text.slice(0, match.index).replace(/\s+$/, '');
155
+ const section = text.slice(match.index + match[0].length);
156
+ const byId = new Map(sources.map((s) => [s.id, s]));
157
+
158
+ const entries: ReferenceEntry[] = [];
159
+ let current: { id: number; rest: string } | null = null;
160
+ const flush = () => {
161
+ if (!current) return;
162
+ const known = byId.get(current.id);
163
+ const rest = current.rest.trim();
164
+ const link = MARKDOWN_LINK_RE.exec(rest);
165
+ let url = link ? link[2] : (URL_RE.exec(rest)?.[0] ?? known?.url ?? '');
166
+ let title = link ? link[1] : rest.replace(URL_RE, '').replace(/\s+/g, ' ').trim();
167
+ if (!url && known) url = known.url;
168
+ if (!title) title = known?.title ?? (url ? domainOf(url, { stripWww: true }) : '');
169
+ if (url)
170
+ entries.push({
171
+ id: current.id,
172
+ title: title.replace(/\\([[\]])/g, '$1'),
173
+ url,
174
+ domain: domainOf(url, { stripWww: true }),
175
+ });
176
+ current = null;
177
+ };
178
+
179
+ for (const line of section.split('\n')) {
180
+ const m = REFERENCE_LINE_RE.exec(line);
181
+ if (m) {
182
+ flush();
183
+ current = { id: Number(m[1]), rest: m[2] };
184
+ } else if (current && line.trim().length > 0) {
185
+ // A legacy entry's URL sits on the indented line after the title.
186
+ current.rest += ` ${line.trim()}`;
187
+ }
188
+ }
189
+ flush();
190
+
191
+ return entries.length > 0 ? { body, references: entries } : { body: text, references: null };
192
+ }
@@ -0,0 +1,23 @@
1
+ import { toast } from '@oxy.so/bloom/toast';
2
+ import * as Clipboard from 'expo-clipboard';
3
+ import type { Translate } from './copy';
4
+
5
+ /**
6
+ * Copy `text`, and say which happened.
7
+ *
8
+ * The result is the clipboard's own: `setStringAsync` resolves `false` on web when the browser
9
+ * refuses (no permission, no focus, an insecure origin) and rejects on native when the module is
10
+ * missing. Either way the answer is `false` and an error toast, and Bloom's copy button, which
11
+ * confirms only on `true`, stays a copy glyph instead of announcing "Copied!" over nothing.
12
+ */
13
+ export async function copyText(text: string, t: Translate): Promise<boolean> {
14
+ let copied: boolean;
15
+ try {
16
+ copied = (await Clipboard.setStringAsync(text)) !== false;
17
+ } catch {
18
+ copied = false;
19
+ }
20
+ if (copied) toast.success(t('chat.copiedToClipboard'));
21
+ else toast.error(t('chat.copyFailed'));
22
+ return copied;
23
+ }
@@ -1,3 +1,5 @@
1
+ import { processMessage } from '../runtime/thread/message-processor';
2
+
1
3
  /** A single part of a multi-part chat message content array. */
2
4
  export interface ContentPart {
3
5
  type: string;
@@ -22,6 +24,14 @@ export function getTextFromContent(content: MessageContent | null | undefined):
22
24
  return String(content || '');
23
25
  }
24
26
 
27
+ /**
28
+ * A message's words as the thread draws them: its text parts, without the tags other platforms
29
+ * use (Telegram's), which a reply written for one can still carry.
30
+ */
31
+ export function messageText(message: { content: MessageContent | null | undefined }): string {
32
+ return processMessage(message.content ? getTextFromContent(message.content) : '', 'app').text;
33
+ }
34
+
25
35
  /**
26
36
  * Extract image URLs from multi-part message content.
27
37
  */