@alia.onl/sdk 15.7.0 → 19.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.
Files changed (150) hide show
  1. package/CHANGELOG.md +255 -12
  2. package/README.md +241 -155
  3. package/package.json +17 -5
  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/csp.ts +50 -0
  43. package/src/hooks/useAliaActivity.ts +3 -3
  44. package/src/hooks/useAtBottom.ts +51 -0
  45. package/src/hooks/useAttachMenu.ts +94 -0
  46. package/src/hooks/useChatComposer.ts +299 -0
  47. package/src/hooks/useChatVoice.ts +129 -0
  48. package/src/hooks/useDictation.ts +2 -3
  49. package/src/hooks/useDocumentPicker.ts +3 -29
  50. package/src/hooks/useFailedTurnToast.ts +43 -0
  51. package/src/hooks/useMessageVote.ts +50 -0
  52. package/src/hooks/useOpenThought.ts +79 -0
  53. package/src/hooks/usePendingQuestions.ts +2 -2
  54. package/src/hooks/useReadAloud.ts +92 -0
  55. package/src/hooks/useShareHandler.ts +24 -0
  56. package/src/hooks/useSoundEffects.ts +1 -1
  57. package/src/hooks/useTTS.ts +2 -3
  58. package/src/hooks/useThoughtScope.ts +59 -0
  59. package/src/hooks/useThreadRows.ts +145 -0
  60. package/src/hooks/useThreadView.ts +68 -0
  61. package/src/hooks/useTimelineWindow.ts +256 -0
  62. package/src/hooks/useVoiceLauncher.ts +180 -0
  63. package/src/hooks/useVoiceRoom.ts +2 -6
  64. package/src/index.ts +43 -29
  65. package/src/lib/activity-store.ts +1 -1
  66. package/src/lib/add-menu.ts +7 -0
  67. package/src/lib/call-thread.ts +33 -0
  68. package/src/lib/character-runtime.ts +6 -4
  69. package/src/lib/citations.ts +79 -2
  70. package/src/lib/clipboard.ts +23 -0
  71. package/src/lib/content-utils.ts +10 -0
  72. package/src/lib/copy-en.ts +284 -0
  73. package/src/lib/copy.ts +32 -0
  74. package/src/lib/document-picker.ts +31 -0
  75. package/src/lib/flights.ts +22 -13
  76. package/src/lib/focus-return.ts +66 -0
  77. package/src/lib/keyboard.native.tsx +19 -0
  78. package/src/lib/keyboard.tsx +9 -0
  79. package/src/lib/labels.tsx +69 -17
  80. package/src/lib/origins.ts +15 -0
  81. package/src/lib/participants.ts +86 -0
  82. package/src/lib/power-level-options.ts +66 -0
  83. package/src/lib/questions.ts +2 -2
  84. package/src/lib/task-labels.ts +4 -4
  85. package/src/lib/tool-call-status.ts +1 -1
  86. package/src/lib/tool-icons.ts +36 -0
  87. package/src/lib/tool-renderers.tsx +48 -0
  88. package/src/lib/turn-state.ts +2 -2
  89. package/src/lib/voice-audio-engine.ts +14 -158
  90. package/src/lib/voice-call-request.ts +49 -0
  91. package/src/lib/voice-call.ts +76 -34
  92. package/src/lib/voice-controls-labels.ts +31 -0
  93. package/src/lib/voice-protocol.ts +32 -14
  94. package/src/lib/voice-worklets.ts +19 -0
  95. package/src/lib/work-log.ts +6 -6
  96. package/src/runtime/agents.ts +151 -0
  97. package/src/runtime/api.ts +123 -0
  98. package/src/runtime/attachment-utils.ts +96 -0
  99. package/src/runtime/composer-draft-store.ts +270 -0
  100. package/src/runtime/config.tsx +186 -0
  101. package/src/runtime/conversation-store.ts +49 -0
  102. package/src/runtime/conversations.ts +557 -0
  103. package/src/runtime/error-utils.ts +104 -0
  104. package/src/runtime/host-tools.ts +140 -0
  105. package/src/runtime/index.ts +113 -0
  106. package/src/runtime/message-history.ts +124 -0
  107. package/src/runtime/model.ts +39 -0
  108. package/src/runtime/oxy-files.ts +37 -0
  109. package/src/runtime/power-levels.ts +66 -0
  110. package/src/runtime/query-keys.ts +34 -0
  111. package/src/runtime/random-uuid.ts +25 -0
  112. package/src/runtime/routes.ts +51 -0
  113. package/src/runtime/sharing.ts +165 -0
  114. package/src/runtime/sse-frame-reader.ts +88 -0
  115. package/src/runtime/stream-events.ts +146 -0
  116. package/src/runtime/stream-outcome.ts +21 -0
  117. package/src/runtime/thought-store.ts +103 -0
  118. package/src/runtime/thread/agent-activity.ts +261 -0
  119. package/src/runtime/thread/automation-labels.ts +79 -0
  120. package/src/runtime/thread/conversation-export.ts +135 -0
  121. package/src/runtime/thread/message-days.ts +120 -0
  122. package/src/runtime/thread/message-processor.ts +93 -0
  123. package/src/runtime/thread/task-utils.ts +11 -0
  124. package/src/runtime/thread/thought-utils.ts +752 -0
  125. package/src/runtime/thread/thread-history.ts +139 -0
  126. package/src/runtime/thread/timeline.ts +202 -0
  127. package/src/runtime/thread/title-tags.ts +8 -0
  128. package/src/runtime/thread/tool-cards.ts +383 -0
  129. package/src/runtime/turn-failure.ts +108 -0
  130. package/src/runtime/types.ts +148 -0
  131. package/src/runtime/usage-limit-error.ts +44 -0
  132. package/src/runtime/use-agent-activity.ts +80 -0
  133. package/src/runtime/use-conversation-stream.ts +1427 -0
  134. package/src/runtime/use-conversation-thread.ts +556 -0
  135. package/src/runtime/use-start-conversation.ts +107 -0
  136. package/src/runtime/use-thread-history.ts +64 -0
  137. package/src/runtime/use-thread-search.ts +95 -0
  138. package/src/runtime/use-turn-edit.ts +99 -0
  139. package/src/tools.ts +2 -2
  140. package/src/types.ts +18 -47
  141. package/worklets/voice-processors.js +152 -0
  142. package/src/components/AliaChatMessageList.tsx +0 -532
  143. package/src/components/ThinkingBlock.tsx +0 -43
  144. package/src/hooks/useAliaChat.ts +0 -573
  145. package/src/lib/api-url.ts +0 -2
  146. package/src/lib/catalogue.ts +0 -236
  147. package/src/lib/chat-stream.ts +0 -655
  148. package/src/lib/chat-transport.ts +0 -98
  149. package/src/lib/delegation.ts +0 -34
  150. package/src/lib/work-log-copy.ts +0 -61
@@ -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
  */
@@ -0,0 +1,284 @@
1
+ /**
2
+ * The chat's own English, keyed the way the Alia app's catalogue keys it, so the app hands its
3
+ * translator in (`labels.t`) and a host without one reads English. Generated from the app's
4
+ * `locales/en.json` and kept by hand from here: a key a component asks for belongs here.
5
+ */
6
+ export type CopyEntry = string | { zero?: string; one?: string; other: string };
7
+
8
+ export const COPY_EN: Readonly<Record<string, CopyEntry>> = {
9
+ 'automations.cron.dailyAt': 'Daily at {{time}}',
10
+ 'automations.cron.dayPlural.0': 'Sundays',
11
+ 'automations.cron.dayPlural.1': 'Mondays',
12
+ 'automations.cron.dayPlural.2': 'Tuesdays',
13
+ 'automations.cron.dayPlural.3': 'Wednesdays',
14
+ 'automations.cron.dayPlural.4': 'Thursdays',
15
+ 'automations.cron.dayPlural.5': 'Fridays',
16
+ 'automations.cron.dayPlural.6': 'Saturdays',
17
+ 'automations.cron.daysAt': '{{days}} at {{time}}',
18
+ 'automations.cron.everyHour': 'Every hour',
19
+ 'automations.cron.everyHours': 'Every {{count}} hours',
20
+ 'automations.cron.everyMinutes': { one: 'Every minute', other: 'Every {{count}} minutes' },
21
+ 'automations.cron.weekdaysAt': 'Weekdays at {{time}}',
22
+ 'automations.cron.weekendsAt': 'Weekends at {{time}}',
23
+ 'automations.trigger.anyApp': 'Any app',
24
+ 'automations.trigger.anyEvent': 'Any event',
25
+ 'automations.trigger.manual': 'Manual request',
26
+ 'automations.trigger.unscheduled': 'Unscheduled',
27
+ 'chat.agentRun.by': 'by %{name}',
28
+ 'chat.agentRun.complete': 'Task complete',
29
+ 'chat.agentRun.completed': 'Completed',
30
+ 'chat.agentRun.credits': '%{count} credits',
31
+ 'chat.agentRun.error': 'Error',
32
+ 'chat.agentRun.failed': 'Failed',
33
+ 'chat.agentRun.planItems': '%{done}/%{total} plan items',
34
+ 'chat.agentRun.planSummary': 'Plan summary',
35
+ 'chat.agentRun.steps': '%{count} steps',
36
+ 'chat.agentRun.taskComplete': 'Task complete',
37
+ 'chat.agentRun.taskFailed': 'Task failed',
38
+ 'chat.agentRun.unknown': 'Unknown',
39
+ 'chat.agentRun.working': 'Agent working',
40
+ 'chat.backToLatest': 'Back to the latest',
41
+ 'chat.blockFailed': "This part of the answer couldn't be shown.",
42
+ 'chat.bloom.context.freeSpace': 'Free space',
43
+ 'chat.bloom.context.mcp': 'MCP tools',
44
+ 'chat.bloom.context.memory': 'Memory',
45
+ 'chat.bloom.context.messages': 'Messages',
46
+ 'chat.bloom.context.skills': 'Skills',
47
+ 'chat.bloom.context.system': 'System prompt',
48
+ 'chat.bloom.context.title': 'Context window',
49
+ 'chat.bloom.context.tools': 'Tools',
50
+ 'chat.bloom.createdFile': 'Created',
51
+ 'chat.bloom.limits.close': 'Close credits panel',
52
+ 'chat.bloom.limits.dailyFree': 'Daily free credits',
53
+ 'chat.bloom.limits.planUsage': 'Plan usage limits',
54
+ 'chat.bloom.limits.resetsDaily': 'Resets every 24 hours',
55
+ 'chat.bloom.limits.resetsInHours': 'Resets in {{hours}} hr {{minutes}} min',
56
+ 'chat.bloom.limits.resetsInMinutes': 'Resets in {{minutes}} min',
57
+ 'chat.bloom.limits.resetsSoon': 'Resets soon',
58
+ 'chat.bloom.limits.window': '{{hours}}-hour limit',
59
+ 'chat.bloom.limits.windowFresh': 'Resets {{hours}} hours after your next message',
60
+ 'chat.bloom.noResults': 'No results',
61
+ 'chat.bloom.researched': 'Researched',
62
+ 'chat.bloom.researching': 'Researching',
63
+ 'chat.bloom.resultCount': { one: '1 result', other: '{{count}} results' },
64
+ 'chat.bloom.scrollToLatest': 'Scroll to latest messages',
65
+ 'chat.bloom.searchCount': { one: '1 search', other: '{{count}} searches' },
66
+ 'chat.bloom.searchFailed': 'Search failed',
67
+ 'chat.bloom.searchedWeb': 'Searched the web for',
68
+ 'chat.bloom.sourceCount': { one: '1 source', other: '{{count}} sources' },
69
+ 'chat.bloom.taskFailed': 'Returned an error',
70
+ 'chat.bloom.taskInput': 'With',
71
+ 'chat.bloom.taskStopped': "Didn't finish",
72
+ 'chat.bloom.visited': 'Visited',
73
+ 'chat.bloom.writingFile': 'Writing',
74
+ 'chat.copied': 'Copied!',
75
+ 'chat.copiedToClipboard': 'Copied to clipboard',
76
+ 'chat.copy': 'Copy',
77
+ 'chat.copyFailed': "Couldn't copy to the clipboard",
78
+ 'chat.dislike': 'Dislike',
79
+ 'chat.edit': 'Edit',
80
+ 'chat.feedbackFailed': "Your feedback couldn't be saved.",
81
+ 'chat.like': 'Like',
82
+ 'chat.loadingHistory': 'Loading earlier messages…',
83
+ 'chat.newConversationAccept': 'Start a new one',
84
+ 'chat.newConversationDismiss': 'Not now',
85
+ 'chat.newConversationOffer': 'Start a new conversation?',
86
+ 'chat.newStretch': 'New conversation',
87
+ 'chat.readAloud': 'Read Aloud',
88
+ 'chat.readAloudFailed': "Couldn't read this aloud",
89
+ 'chat.regenerate': 'Regenerate',
90
+ 'chat.retry': 'Retry',
91
+ 'chat.richBlocks.source': 'Source: {{source}}',
92
+ 'chat.searchThreadAgent': 'Agent',
93
+ 'chat.searchThreadEmpty': 'Nothing matched.',
94
+ 'chat.searchThreadFailed': 'The search could not be run.',
95
+ 'chat.searchThreadHint':
96
+ 'Search everything you have said to this agent, across every conversation.',
97
+ 'chat.searchThreadPlaceholder': 'Search this thread',
98
+ 'chat.searchThreadSearching': 'Searching…',
99
+ 'chat.searchThreadYou': 'You',
100
+ 'chat.sources': 'Sources',
101
+ 'chat.stopReading': 'Stop reading',
102
+ 'chat.thanksFeedback': 'Thanks for your feedback!',
103
+ 'chat.thinking': 'Thinking',
104
+ 'chat.today': 'Today',
105
+ 'chat.toolCard.openTask': 'Open scheduled task %{objective}',
106
+ 'chat.toolCard.paused': 'Paused',
107
+ 'chat.toolCard.scheduled': 'Scheduled',
108
+ 'chat.turnActions': 'Message actions',
109
+ 'chat.consultFailed': "%{name} couldn't answer.",
110
+ 'chat.shareFailed': "Couldn't create the share link.",
111
+ 'chat.sharedFailed': "Couldn't load this conversation.",
112
+ 'chat.sharedGone': 'This shared conversation is no longer available.',
113
+ 'chat.turnFailed': "Alia couldn't answer right now.",
114
+ 'chat.turnFailedRetryHint': 'Try again.',
115
+ 'chat.turnInterrupted': 'Alia was interrupted before finishing.',
116
+ 'chat.working': 'Working',
117
+ 'chat.yesterday': 'Yesterday',
118
+ 'common.close': 'Close',
119
+ 'composer.cancelEdit': 'Cancel',
120
+ 'composer.editPlaceholder': 'Edit your message',
121
+ 'composer.editingMessage': 'Editing message',
122
+ 'faircoin.indexed':
123
+ 'Indexed price from the WFAIR/USDC pool on Base — FairCoin has no exchange listing',
124
+ 'faircoin.liquidity': 'Liquidity',
125
+ 'faircoin.marketCap': 'Market cap',
126
+ 'faircoin.name': 'FairCoin',
127
+ 'faircoin.noChart': 'Only one sample so far — not enough to draw a line.',
128
+ 'faircoin.noPrice': 'No price',
129
+ 'faircoin.noPriceHint': 'The WFAIR/USDC pool had no indexed price when this was asked.',
130
+ 'faircoin.noSamples': 'No samples for this period yet.',
131
+ 'faircoin.range.1y': '1Y',
132
+ 'faircoin.range.24h': '24h',
133
+ 'faircoin.range.30d': '30D',
134
+ 'faircoin.range.7d': '7D',
135
+ 'faircoin.range.all': 'All',
136
+ 'faircoin.scale.billion': 'B',
137
+ 'faircoin.scale.million': 'M',
138
+ 'faircoin.scale.thousand': 'K',
139
+ 'faircoin.scale.trillion': 'T',
140
+ 'faircoin.source': 'From %{source}',
141
+ 'faircoin.symbol': 'FAIR',
142
+ 'faircoin.unavailable': 'No history for %{ranges}',
143
+ 'faircoin.updated': 'Updated %{time}',
144
+ 'faircoin.volume': '24h volume',
145
+ 'market.dayHigh': 'Day high',
146
+ 'market.dayLow': 'Day low',
147
+ 'market.marketCap': 'Market cap',
148
+ 'market.noChart': 'Not enough history to chart this range',
149
+ 'market.noPrice': 'No quote available',
150
+ 'market.open': 'Open',
151
+ 'market.range.1D': '1D',
152
+ 'market.range.1M': '1M',
153
+ 'market.range.1Y': '1Y',
154
+ 'market.range.5D': '5D',
155
+ 'market.range.5Y': '5Y',
156
+ 'market.range.6M': '6M',
157
+ 'market.range.MAX': 'MAX',
158
+ 'market.range.YTD': 'YTD',
159
+ 'market.volume': 'Volume (24h)',
160
+ 'panels.canvas.image': 'Image',
161
+ 'panels.canvas.unsupported': 'Unsupported component type: {{type}}',
162
+ 'panels.chart.share': 'Share',
163
+ 'panels.chart.value': 'Value',
164
+ 'panels.code.copied': 'Code copied',
165
+ 'panels.code.copy': 'Copy code',
166
+ 'panels.form.enter': 'Enter {{field}}',
167
+ 'panels.form.select': 'Select…',
168
+ 'panels.form.submit': 'Submit',
169
+ 'powerLevels.auto.description': 'Picks the right level for each message',
170
+ 'powerLevels.auto.label': 'Auto',
171
+ 'powerLevels.high.description': 'Thinks harder on tougher questions',
172
+ 'powerLevels.high.label': 'High',
173
+ 'powerLevels.instant.description': 'Fastest and cheapest, for quick answers',
174
+ 'powerLevels.instant.label': 'Instant',
175
+ 'powerLevels.medium.description': 'Quick and capable, for everyday tasks',
176
+ 'powerLevels.medium.label': 'Medium',
177
+ 'powerLevels.onDevice': 'On %{device}',
178
+ 'powerLevels.pro.description': 'Frontier models; slower and costs more',
179
+ 'powerLevels.pro.label': 'Pro',
180
+ 'powerLevels.ultra.description': 'Most capable; slowest and costs the most',
181
+ 'powerLevels.ultra.label': 'Ultra',
182
+ 'powerLevels.xhigh.description': 'Thinks even harder; a little slower',
183
+ 'powerLevels.xhigh.label': 'XHigh',
184
+ 'tasks.tool.done.agentSearch': 'Searched agents',
185
+ 'tasks.tool.done.browse': 'Browsed',
186
+ 'tasks.tool.done.codeInterpreter': 'Ran code',
187
+ 'tasks.tool.done.consultAgent': 'Consulted an agent',
188
+ 'tasks.tool.done.fileEdit': 'Edited file',
189
+ 'tasks.tool.done.generateFile': 'Generated file',
190
+ 'tasks.tool.done.shellExec': 'Ran command',
191
+ 'tasks.tool.done.loadTools': 'Loaded tools',
192
+ 'tasks.tool.done.userMemory': 'Remembered',
193
+ 'tasks.tool.done.webScraper': 'Read page',
194
+ 'tasks.tool.pill.agentSearch': 'Searching agents',
195
+ 'tasks.tool.pill.browse': 'Browsing',
196
+ 'tasks.tool.pill.codeInterpreter': 'Running code',
197
+ 'tasks.tool.pill.consultAgent': 'Consulting an agent',
198
+ 'tasks.tool.pill.fileEdit': 'Editing file',
199
+ 'tasks.tool.pill.generateFile': 'Generating file',
200
+ 'tasks.tool.pill.sendDiscord': 'Discord',
201
+ 'tasks.tool.pill.sendEmail': 'Email',
202
+ 'tasks.tool.pill.sendSignal': 'Signal',
203
+ 'tasks.tool.pill.sendSlack': 'Slack',
204
+ 'tasks.tool.pill.sendTelegramMessage': 'Telegram',
205
+ 'tasks.tool.pill.shellExec': 'Running command',
206
+ 'tasks.tool.pill.loadTools': 'Loading tools',
207
+ 'tasks.tool.pill.userMemory': 'Remembering',
208
+ 'tasks.tool.pill.webScraper': 'Reading page',
209
+ 'tasks.tool.pill.webSearch': 'Web search',
210
+ 'thought.activity': 'Activity',
211
+ 'thought.audit.agent': 'Agent: {{name}}',
212
+ 'thought.audit.fileGenerated': 'File generated',
213
+ 'thought.audit.plan': 'Plan',
214
+ 'thought.audit.planSteps': { one: '1 step', other: '{{count}} steps' },
215
+ 'thought.audit.researchComplete': 'Research complete',
216
+ 'thought.audit.researchIncomplete': 'Research incomplete',
217
+ 'thought.audit.researchPhase.decomposing': 'Research: breaking down the question',
218
+ 'thought.audit.researchPhase.finalizing': 'Research: finishing up',
219
+ 'thought.audit.researchPhase.follow_up': 'Research: following up',
220
+ 'thought.audit.researchPhase.reading': 'Research: reading articles',
221
+ 'thought.audit.researchPhase.searching': 'Research: searching sources',
222
+ 'thought.audit.researchPhase.synthesizing': 'Research: synthesizing findings',
223
+ 'thought.audit.researching': 'Researching',
224
+ 'thought.cancelled': 'Stopped',
225
+ 'thought.done': 'Done',
226
+ 'thought.failed': 'Failed',
227
+ 'thought.failedAfter': 'Failed after {{elapsed}}',
228
+ 'thought.filesAndSources': 'Files and sources',
229
+ 'thought.input': 'Input',
230
+ 'thought.loadFailed': "This conversation couldn't be loaded",
231
+ 'thought.loading': 'Loading this conversation…',
232
+ 'thought.messageGone': 'This message is no longer in the conversation',
233
+ 'thought.noActivity': 'No actions recorded yet',
234
+ 'thought.noOutputs': 'No files generated',
235
+ 'thought.noSources': 'No sources used',
236
+ 'thought.noSteps': 'No steps to display',
237
+ 'thought.openOutput': 'Open {{name}} in the canvas',
238
+ 'thought.output': 'Output',
239
+ 'thought.outputs': 'Outputs',
240
+ 'thought.reasoning': 'Reasoning',
241
+ 'thought.references': 'References',
242
+ 'thought.sourceLabel': 'Source {{n}}: {{title}}',
243
+ 'thought.sources': 'Sources',
244
+ 'thought.steps': 'Steps',
245
+ 'thought.stoppedAfter': 'Stopped after {{elapsed}}',
246
+ 'thought.thinking': 'Thinking',
247
+ 'thought.thinkingFor': 'Thinking for {{seconds}}s...',
248
+ 'thought.thoughtFor': 'Thought for {{seconds}} seconds',
249
+ 'thought.title': 'Thought',
250
+ 'thought.tool.browse': 'Browsing the web',
251
+ 'thought.tool.generateFile': 'Generating file',
252
+ 'thought.tool.getCurrentDate': 'Getting current date',
253
+ 'thought.tool.getTimeline': 'Getting timeline',
254
+ 'thought.tool.getWhatsAppChats': 'Loading WhatsApp chats',
255
+ 'thought.tool.getWhatsAppMessages': 'Reading WhatsApp messages',
256
+ 'thought.tool.saveUserMemory': 'Saving to memory',
257
+ 'thought.tool.scrapeURL': 'Reading URL',
258
+ 'thought.tool.searchKnowledgeBase': 'Searching knowledge base',
259
+ 'thought.tool.sendTelegramMessage': 'Sending Telegram message',
260
+ 'thought.tool.sendWhatsAppMessage': 'Sending WhatsApp message',
261
+ 'thought.tool.updateUserContext': 'Updating context',
262
+ 'thought.tool.updateUserPreferences': 'Updating preferences',
263
+ 'thought.tool.webScraper': 'Reading web page',
264
+ 'thought.tool.webSearch': 'Searching the web',
265
+ 'thought.viewDetails': 'View details',
266
+ 'thought.websites': 'Websites',
267
+ 'thought.worked': 'Worked',
268
+ 'thought.workedFor': 'Worked for {{elapsed}}',
269
+ 'thought.working': 'Working…',
270
+ 'thought.workingFor': 'Working for {{elapsed}}…',
271
+ 'thought.writing': 'Writing',
272
+ 'usageLimit.inputDisabledPlaceholder': 'Usage limit reached',
273
+ 'weather.condition.clear': 'Clear',
274
+ 'weather.condition.cloudy': 'Cloudy',
275
+ 'weather.condition.drizzle': 'Drizzle',
276
+ 'weather.condition.fog': 'Fog',
277
+ 'weather.condition.partly-cloudy': 'Partly cloudy',
278
+ 'weather.condition.rain': 'Rain',
279
+ 'weather.condition.showers': 'Showers',
280
+ 'weather.condition.snow': 'Snow',
281
+ 'weather.condition.snow-showers': 'Snow showers',
282
+ 'weather.condition.thunderstorm': 'Thunderstorm',
283
+ 'weather.toggleUnit': 'Switch temperature unit',
284
+ };
@@ -0,0 +1,32 @@
1
+ import { COPY_EN, type CopyEntry } from './copy-en';
2
+
3
+ /**
4
+ * A translator keyed the way the Alia app's own catalogue keys its strings, so the app hands its
5
+ * `t` straight in (`labels.t`). The words the chat draws that are not one fixed `labels` field —
6
+ * the thread, its cards, the thought panel, the work log — are asked for by key.
7
+ */
8
+ export type Translate = (key: string, params?: Record<string, unknown>) => string;
9
+
10
+ function entryFor(entry: CopyEntry, count: number | undefined): string {
11
+ if (typeof entry === 'string') return entry;
12
+ if (count === 0 && entry.zero !== undefined) return entry.zero;
13
+ if (count === 1 && entry.one !== undefined) return entry.one;
14
+ return entry.other;
15
+ }
16
+
17
+ /** `{{name}}` and `%{name}` both name a parameter, as in the app's catalogue. */
18
+ const PLACEHOLDER = /\{\{\s*(\w+)\s*\}\}|%\{(\w+)\}/g;
19
+
20
+ /**
21
+ * The default translator: the English in `copy-en.ts`, with `{{count}}` plurals and parameters.
22
+ * A key it does not know answers itself, so a missing string is visible rather than blank.
23
+ */
24
+ export const englishCopy: Translate = (key, params) => {
25
+ const entry = COPY_EN[key];
26
+ if (entry === undefined) return key;
27
+ const count = typeof params?.count === 'number' ? params.count : undefined;
28
+ return entryFor(entry, count).replace(PLACEHOLDER, (placeholder, a?: string, b?: string) => {
29
+ const value = params?.[(a ?? b) as string];
30
+ return value === undefined ? placeholder : String(value);
31
+ });
32
+ };
@@ -0,0 +1,31 @@
1
+ import * as DocumentPicker from 'expo-document-picker';
2
+
3
+ export type DocumentPickerResult = {
4
+ uri: string;
5
+ name: string;
6
+ size: number;
7
+ mimeType: string;
8
+ };
9
+
10
+ /** The platform's document picker, several at once; nothing when the person cancels or it fails. */
11
+ export async function pickDocuments(): Promise<DocumentPickerResult[] | undefined> {
12
+ try {
13
+ const result = await DocumentPicker.getDocumentAsync({
14
+ type: '*/*',
15
+ multiple: true,
16
+ copyToCacheDirectory: true,
17
+ });
18
+
19
+ if (result.canceled === false && result.assets && result.assets.length > 0) {
20
+ return result.assets.map((asset) => ({
21
+ uri: asset.uri,
22
+ name: asset.name,
23
+ size: asset.size || 0,
24
+ mimeType: asset.mimeType || 'application/octet-stream',
25
+ }));
26
+ }
27
+ } catch (error) {
28
+ console.warn('Failed to pick document:', error);
29
+ }
30
+ return undefined;
31
+ }
@@ -1,24 +1,33 @@
1
1
  import { createContext, useContext } from 'react';
2
2
  import type { AvatarFlights, AvatarFlightSpec } from '@oxy.so/bloom/avatar-flight';
3
3
 
4
- import { ALIA_AVATAR } from '../components/AliaAvatar';
4
+ import type { AliaParticipant } from './participants';
5
5
 
6
- /** Where Alia's avatar is in the chat: large in the greeting, then small in the header's profile. */
7
- export const ALIA_ANCHORS = { welcome: 'alia:welcome', profile: 'alia:profile' } as const;
6
+ /**
7
+ * Where the conversation's avatars are: the lead's, large in the greeting; everybody's, small in
8
+ * the header's participants; and a consulted agent's, beside the reply it is writing.
9
+ */
10
+ export const ANCHORS = {
11
+ welcome: 'lead:welcome',
12
+ profile: 'participants',
13
+ reply: (agentId: string) => `reply:${agentId}`,
14
+ } as const;
8
15
 
9
- /** The greeting's avatar flies up into the header's profile as the conversation begins. */
10
- export const ALIA_TO_PROFILE: AvatarFlightSpec = {
11
- key: 'alia',
12
- character: ALIA_AVATAR,
13
- from: ALIA_ANCHORS.welcome,
14
- to: ALIA_ANCHORS.profile,
15
- duration: 800,
16
- };
16
+ /** The greeting's avatar flies up into the header's participants as the conversation begins. */
17
+ export function leadToProfile(lead: AliaParticipant): AvatarFlightSpec {
18
+ return {
19
+ key: 'lead',
20
+ character: lead.character,
21
+ from: ANCHORS.welcome,
22
+ to: ANCHORS.profile,
23
+ duration: 800,
24
+ };
25
+ }
17
26
 
18
27
  const FlightsContext = createContext<AvatarFlights | null>(null);
19
28
 
20
- /** The chat's avatar flights, for the greeting and the header that share them. */
29
+ /** The chat's avatar flights, for the greeting, the thread and the header that share them. */
21
30
  export const AliaFlightsProvider = FlightsContext.Provider;
22
31
 
23
- /** `null` outside a chat: the greeting and the header then draw their avatars without flying. */
32
+ /** `null` outside a chat: the greeting, the replies and the header then draw their avatars without flying. */
24
33
  export const useAliaFlights = () => useContext(FlightsContext);
@@ -0,0 +1,66 @@
1
+ import { Platform } from 'react-native';
2
+
3
+ /**
4
+ * Where keyboard focus goes back to when the execution panel closes.
5
+ *
6
+ * The panel is opened from a row in the conversation — a tool step, the
7
+ * sources row, the reasoning trigger — and on the web the desktop rail is not
8
+ * a dialog: nothing moves focus into it and nothing would move it back. So a
9
+ * screen-reader or keyboard user who opened it and pressed Escape was left
10
+ * with focus wherever the browser had put it, which after a click on a
11
+ * `Pressable` is `body` in Safari. The opener is remembered here at open time
12
+ * and focused again at close.
13
+ *
14
+ * One slot, module-level: there is one panel, and the store's own selection
15
+ * already says which message it is about. A DOM element in the store would
16
+ * also be a DOM element in a persisted store's state tree, which it must not
17
+ * be.
18
+ */
19
+ type Focusable = { focus: () => void };
20
+
21
+ let opener: Focusable | null = null;
22
+
23
+ function isFocusable(value: unknown): value is Focusable {
24
+ return (
25
+ typeof value === 'object' &&
26
+ value !== null &&
27
+ typeof (value as { focus?: unknown }).focus === 'function'
28
+ );
29
+ }
30
+
31
+ /**
32
+ * Remember what opened the panel. A caller with a ref to its own control
33
+ * passes it; one without (a memoised row whose ref would be one more prop)
34
+ * lets this read the element that is focused right now, which on the web is
35
+ * the button that was just activated by keyboard, and the button or `body`
36
+ * after a pointer press depending on the browser. Nothing is remembered on
37
+ * native, where focus does not work this way.
38
+ */
39
+ export function rememberOpener(candidate?: unknown): void {
40
+ if (isFocusable(candidate)) {
41
+ opener = candidate;
42
+ return;
43
+ }
44
+ if (
45
+ Platform.OS === 'web' &&
46
+ typeof document !== 'undefined' &&
47
+ isFocusable(document.activeElement)
48
+ ) {
49
+ opener = document.activeElement;
50
+ return;
51
+ }
52
+ opener = null;
53
+ }
54
+
55
+ /** Focus the remembered opener, once, and forget it. */
56
+ export function restoreOpenerFocus(): void {
57
+ const target = opener;
58
+ opener = null;
59
+ if (target === null) return;
60
+ try {
61
+ target.focus();
62
+ } catch {
63
+ // An element that has since left the document cannot take focus; there
64
+ // is nowhere better to send it.
65
+ }
66
+ }
@@ -1,6 +1,25 @@
1
1
  // Native: re-export react-native-keyboard-controller components
2
+ import Animated, { useAnimatedStyle } from 'react-native-reanimated';
3
+ import { useReanimatedKeyboardAnimation } from 'react-native-keyboard-controller';
4
+
2
5
  export {
3
6
  KeyboardAwareScrollView,
4
7
  KeyboardAvoidingView,
5
8
  KeyboardProvider,
6
9
  } from 'react-native-keyboard-controller';
10
+
11
+ /**
12
+ * The room under a bottom-anchored control for the gesture bar, while the gesture bar is what lies
13
+ * under it.
14
+ *
15
+ * With the keyboard up, a keyboard-avoiding host already ends at the keyboard's top edge, and the
16
+ * keyboard covers the gesture bar. Keeping this room then counts the bar twice: the chat's composer
17
+ * floated a bar's height above the keyboard, with the transcript showing through the gap (Pixel 8a).
18
+ * It follows the keyboard's own animation, so the control settles onto the keyboard as it rises
19
+ * instead of jumping after.
20
+ */
21
+ export function KeyboardSafeAreaFloor({ inset }: { inset: number }) {
22
+ const { progress } = useReanimatedKeyboardAnimation();
23
+ const style = useAnimatedStyle(() => ({ height: inset * (1 - progress.value) }), [inset]);
24
+ return <Animated.View style={style} />;
25
+ }
@@ -5,6 +5,7 @@ import {
5
5
  type ScrollViewProps,
6
6
  KeyboardAvoidingView as RNKeyboardAvoidingView,
7
7
  type KeyboardAvoidingViewProps as RNKeyboardAvoidingViewProps,
8
+ View,
8
9
  } from 'react-native';
9
10
 
10
11
  // Accept native-only props so shared components don't cause TS errors
@@ -41,3 +42,11 @@ export { KeyboardAwareScrollView, KeyboardAvoidingView };
41
42
  export function KeyboardProvider({ children }: { children: React.ReactNode }) {
42
43
  return <>{children}</>;
43
44
  }
45
+
46
+ /**
47
+ * The room under a bottom-anchored control for the system's bottom bar. The web's keyboard resizes
48
+ * the page rather than covering it, so that room never has to give way to it (see the native file).
49
+ */
50
+ export function KeyboardSafeAreaFloor({ inset }: { inset: number }) {
51
+ return <View style={{ height: inset }} />;
52
+ }