@alia.onl/sdk 10.1.0 → 11.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 (64) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +24 -0
  3. package/package.json +5 -7
  4. package/src/components/AliaAvatar.tsx +66 -0
  5. package/src/components/AliaChatContent.tsx +235 -112
  6. package/src/components/AliaChatHeader.tsx +118 -0
  7. package/src/components/AliaChatMessageList.tsx +171 -260
  8. package/src/components/AliaChatScreen.tsx +23 -24
  9. package/src/components/AliaChatSheet.tsx +70 -316
  10. package/src/components/AliaWelcomeMessage.tsx +47 -50
  11. package/src/components/AmbientField.tsx +577 -0
  12. package/src/components/IdentityMark.tsx +10 -2
  13. package/src/components/Markdown.tsx +116 -40
  14. package/src/components/PlanPreviewCard.tsx +71 -55
  15. package/src/components/ThinkingBlock.tsx +47 -0
  16. package/src/components/VoiceModeIcon.tsx +10 -0
  17. package/src/components/voice/VoiceControls.tsx +22 -73
  18. package/src/components/voice/VoiceSession.tsx +2 -1
  19. package/src/hooks/useAliaChat.ts +22 -15
  20. package/src/hooks/useAmbientWave.ts +35 -5
  21. package/src/hooks/useSoundEffects.ts +14 -11
  22. package/src/hooks/useSpeechToText.ts +9 -2
  23. package/src/hooks/useTTS.ts +163 -156
  24. package/src/hooks/useVoiceRoom.ts +228 -170
  25. package/src/index.ts +18 -6
  26. package/src/lib/catalogue.ts +17 -4
  27. package/src/lib/chat-stream.ts +25 -7
  28. package/src/lib/chat-transport.ts +2 -1
  29. package/src/lib/citations.ts +63 -0
  30. package/src/lib/content-utils.ts +3 -2
  31. package/src/lib/image-data-url.ts +15 -0
  32. package/src/lib/keyboard.tsx +1 -1
  33. package/src/lib/labels.tsx +81 -0
  34. package/src/lib/speech-recognition.native.ts +15 -4
  35. package/src/lib/speech-recognition.ts +27 -7
  36. package/src/lib/task-labels.ts +67 -0
  37. package/src/lib/tool-call-status.ts +28 -0
  38. package/src/lib/tool-registry.ts +14 -14
  39. package/src/lib/voice-text.ts +12 -6
  40. package/src/lib/voice-turn.ts +4 -1
  41. package/src/lib/work-log-copy.ts +61 -0
  42. package/src/lib/work-log.ts +378 -0
  43. package/src/tools.ts +11 -0
  44. package/src/types.ts +5 -2
  45. package/src/voice.ts +12 -4
  46. package/src/components/Reasoning.tsx +0 -152
  47. package/src/components/ResearchProgressCard.tsx +0 -159
  48. package/src/components/ThinkingIndicator.tsx +0 -98
  49. package/src/components/ui/button.tsx +0 -104
  50. package/src/components/ui/chat-text-input.tsx +0 -185
  51. package/src/components/ui/dropdown-menu.tsx +0 -2
  52. package/src/components/ui/dropdown-menu.web.tsx +0 -383
  53. package/src/components/ui/prompt-input/actions.tsx +0 -28
  54. package/src/components/ui/prompt-input/add-menu.tsx +0 -83
  55. package/src/components/ui/prompt-input/attachments.tsx +0 -181
  56. package/src/components/ui/prompt-input/autocomplete.tsx +0 -168
  57. package/src/components/ui/prompt-input/context.tsx +0 -71
  58. package/src/components/ui/prompt-input/mic-button.tsx +0 -53
  59. package/src/components/ui/prompt-input/prompt-input.tsx +0 -279
  60. package/src/components/ui/prompt-input/submit-button.tsx +0 -50
  61. package/src/components/ui/prompt-input/textarea.tsx +0 -80
  62. package/src/components/ui/text.tsx +0 -29
  63. package/src/components/voice/AudioWaveVisualizer.tsx +0 -444
  64. package/src/components/voice/VoiceOverlay.tsx +0 -75
@@ -1,3 +1,4 @@
1
+ import type { MessageContent } from './content-utils';
1
2
  import {
2
3
  AliaChatStreamError,
3
4
  consumeAliaChatStream,
@@ -20,7 +21,7 @@ export interface AliaChatRequest {
20
21
  readonly url: string;
21
22
  /** `publisher/model`; omitted, the server's default model answers. */
22
23
  readonly model?: string;
23
- readonly messages: ReadonlyArray<{ readonly role: string; readonly content: string }>;
24
+ readonly messages: ReadonlyArray<{ readonly role: string; readonly content: MessageContent }>;
24
25
  /**
25
26
  * `'voice'` asks for an answer meant to be heard: short, conversational, no
26
27
  * formatting (the API layers its voice response profile over the turn's
@@ -0,0 +1,63 @@
1
+ import type { ToolInvocation } from '../types';
2
+
3
+ export interface CitationSource {
4
+ /** The citation number, as it appears in `[n]`. */
5
+ id: number;
6
+ url: string;
7
+ title: string;
8
+ domain: string;
9
+ }
10
+
11
+ interface PersistedResearchSource {
12
+ id?: unknown;
13
+ url?: unknown;
14
+ title?: unknown;
15
+ }
16
+
17
+ function domainOf(url: string): string {
18
+ try {
19
+ return new URL(url).hostname.replace(/^www\./, '');
20
+ } catch {
21
+ return url;
22
+ }
23
+ }
24
+
25
+ /**
26
+ * The numbered sources of a research answer.
27
+ *
28
+ * Only `deepResearch` invocations carry NUMBERED sources; a `webSearch` result has URLs but
29
+ * no `[n]` to stand for them, so it contributes nothing here. The first invocation to claim a
30
+ * number wins, which is also the only one there is.
31
+ */
32
+ export function extractCitationSources(
33
+ toolInvocations?: readonly ToolInvocation[],
34
+ /** `researchProgress.sources` of a live research answer, before it is persisted. */
35
+ liveSources?: readonly PersistedResearchSource[] | null,
36
+ ): CitationSource[] {
37
+ const byId = new Map<number, CitationSource>();
38
+ const take = (raw: PersistedResearchSource) => {
39
+ if (!raw || typeof raw !== 'object') return;
40
+ const id = typeof raw.id === 'number' ? raw.id : Number(raw.id);
41
+ const url = typeof raw.url === 'string' ? raw.url : '';
42
+ if (!Number.isInteger(id) || id < 1 || url.length === 0 || byId.has(id)) return;
43
+ const title =
44
+ typeof raw.title === 'string' && raw.title.trim().length > 0
45
+ ? raw.title.trim()
46
+ : domainOf(url);
47
+ byId.set(id, { id, url, title, domain: domainOf(url) });
48
+ };
49
+
50
+ for (const inv of toolInvocations ?? []) {
51
+ if (inv.toolName !== 'deepResearch' || inv.state !== 'result') continue;
52
+ const result = inv.result;
53
+ const sources: unknown =
54
+ result !== null && typeof result === 'object'
55
+ ? (result as { sources?: unknown }).sources
56
+ : undefined;
57
+ if (!Array.isArray(sources)) continue;
58
+ for (const raw of sources as PersistedResearchSource[]) take(raw);
59
+ }
60
+ for (const raw of liveSources ?? []) take(raw);
61
+
62
+ return [...byId.values()].sort((a, b) => a.id - b.id);
63
+ }
@@ -28,8 +28,9 @@ export function getTextFromContent(content: MessageContent | null | undefined):
28
28
  export function getImagesFromContent(content: MessageContent | null | undefined): string[] {
29
29
  if (!Array.isArray(content)) return [];
30
30
  return content
31
- .filter((p): p is ContentPart & { image_url: { url: string } } =>
32
- p.type === 'image_url' && typeof p.image_url?.url === 'string',
31
+ .filter(
32
+ (p): p is ContentPart & { image_url: { url: string } } =>
33
+ p.type === 'image_url' && typeof p.image_url?.url === 'string',
33
34
  )
34
35
  .map((p) => p.image_url.url);
35
36
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Reads a picked image (a `file://` URI on native, a `blob:` URL on web) into a
3
+ * `data:` URL, the one form the chat API accepts as an `image_url` part without
4
+ * the server having to reach the device.
5
+ */
6
+ export async function imageUriToDataUrl(uri: string): Promise<string> {
7
+ if (uri.startsWith('data:')) return uri;
8
+ const blob = await (await fetch(uri)).blob();
9
+ return await new Promise<string>((resolve, reject) => {
10
+ const reader = new FileReader();
11
+ reader.onerror = () => reject(reader.error ?? new Error('Could not read the image'));
12
+ reader.onload = () => resolve(String(reader.result));
13
+ reader.readAsDataURL(blob);
14
+ });
15
+ }
@@ -18,7 +18,7 @@ type KeyboardAwareScrollViewProps = ScrollViewProps & {
18
18
  const KeyboardAwareScrollView = React.forwardRef<ScrollView, KeyboardAwareScrollViewProps>(
19
19
  ({ bottomOffset, disableScrollOnKeyboardHide, enabled, extraKeyboardSpace, ...props }, ref) => (
20
20
  <ScrollView ref={ref} {...props} />
21
- )
21
+ ),
22
22
  );
23
23
  KeyboardAwareScrollView.displayName = 'KeyboardAwareScrollView';
24
24
 
@@ -0,0 +1,81 @@
1
+ import { createContext, type ReactNode, useContext, useMemo } from 'react';
2
+ import { englishWorkLog, type Translate } from './work-log-copy';
3
+
4
+ /** Every string the chat UI draws itself. Bloom's own copy follows `BloomProvider locale`. */
5
+ export interface AliaChatLabels {
6
+ // Header
7
+ clear: string;
8
+ close: string;
9
+ chatActions: string;
10
+ // Composer
11
+ placeholder: string;
12
+ addMenuGroup: string;
13
+ addPhotos: string;
14
+ startVoice: string;
15
+ voiceFailed: string;
16
+ // Messages
17
+ copy: string;
18
+ copied: string;
19
+ edit: string;
20
+ save: string;
21
+ cancel: string;
22
+ readAloud: string;
23
+ stopReading: string;
24
+ // Plan
25
+ planTitle: string;
26
+ planApprove: string;
27
+ planCancel: string;
28
+ planApproved: string;
29
+ planCancelled: string;
30
+ // Reasoning
31
+ reasoning: string;
32
+ thoughtFor: (seconds: number) => string;
33
+ // The turn's work log
34
+ /** The words of the tool log, keyed as the Alia app keys them (see `work-log-copy.ts`). */
35
+ workLog: Translate;
36
+ // Research
37
+ }
38
+
39
+ export const DEFAULT_LABELS: AliaChatLabels = {
40
+ clear: 'Clear',
41
+ close: 'Close',
42
+ chatActions: 'Chat actions',
43
+ placeholder: 'Ask Alia...',
44
+ addMenuGroup: 'Add',
45
+ addPhotos: 'Photos',
46
+ startVoice: 'Start voice',
47
+ voiceFailed: "Couldn't start the voice call",
48
+ copy: 'Copy',
49
+ copied: 'Copied',
50
+ edit: 'Edit',
51
+ save: 'Save',
52
+ cancel: 'Cancel',
53
+ readAloud: 'Read aloud',
54
+ stopReading: 'Stop reading',
55
+ planTitle: "Here's my plan",
56
+ planApprove: 'Approve',
57
+ planCancel: 'Cancel',
58
+ planApproved: 'Plan approved',
59
+ planCancelled: 'Plan cancelled',
60
+ reasoning: 'Reasoning',
61
+ thoughtFor: (seconds) => `Thought for ${seconds} seconds`,
62
+ workLog: englishWorkLog,
63
+ };
64
+
65
+ const LabelsContext = createContext<AliaChatLabels>(DEFAULT_LABELS);
66
+
67
+ /** The chat's labels: the defaults, with whatever the host overrides. */
68
+ export function AliaChatLabelsProvider({
69
+ labels,
70
+ children,
71
+ }: {
72
+ labels?: Partial<AliaChatLabels>;
73
+ children: ReactNode;
74
+ }) {
75
+ const value = useMemo(() => ({ ...DEFAULT_LABELS, ...labels }), [labels]);
76
+ return <LabelsContext.Provider value={value}>{children}</LabelsContext.Provider>;
77
+ }
78
+
79
+ export function useLabels(): AliaChatLabels {
80
+ return useContext(LabelsContext);
81
+ }
@@ -16,7 +16,10 @@
16
16
  * listeners.
17
17
  */
18
18
 
19
- import type { ExpoSpeechRecognitionErrorCode, ExpoSpeechRecognitionModule as NativeModule } from 'expo-speech-recognition';
19
+ import type {
20
+ ExpoSpeechRecognitionErrorCode,
21
+ ExpoSpeechRecognitionModule as NativeModule,
22
+ } from 'expo-speech-recognition';
20
23
  import {
21
24
  clampLevel,
22
25
  matchSpeechLocale,
@@ -68,7 +71,10 @@ export async function requestSpeechRecognitionPermission(): Promise<SpeechRecogn
68
71
  const permission = await module.requestPermissionsAsync();
69
72
  return permission.granted ? null : { code: 'not-allowed', detail: permission.status };
70
73
  } catch (error: unknown) {
71
- return { code: 'other', detail: error instanceof Error ? error.message : 'permission request failed' };
74
+ return {
75
+ code: 'other',
76
+ detail: error instanceof Error ? error.message : 'permission request failed',
77
+ };
72
78
  }
73
79
  }
74
80
 
@@ -134,7 +140,8 @@ export async function chooseSpeechRecognizer(lang: string): Promise<SpeechRecogn
134
140
  for (const service of order) {
135
141
  let offered: string[];
136
142
  try {
137
- offered = (await module.getSupportedLocales({ androidRecognitionServicePackage: service })).locales;
143
+ offered = (await module.getSupportedLocales({ androidRecognitionServicePackage: service }))
144
+ .locales;
138
145
  } catch {
139
146
  continue;
140
147
  }
@@ -209,7 +216,11 @@ export function startSpeechRecognition(
209
216
  ...(options.echoCancellation ? { iosVoiceProcessingEnabled: true } : {}),
210
217
  });
211
218
  } catch (error: unknown) {
212
- if (!ended) handlers.onError({ code: 'other', detail: error instanceof Error ? error.message : 'start failed' });
219
+ if (!ended)
220
+ handlers.onError({
221
+ code: 'other',
222
+ detail: error instanceof Error ? error.message : 'start failed',
223
+ });
213
224
  finish();
214
225
  }
215
226
 
@@ -38,11 +38,25 @@ import type {
38
38
  // Declared here rather than taken from lib.dom: TypeScript's DOM library does
39
39
  // not ship the prefixed constructor, and the unprefixed one is not everywhere.
40
40
 
41
- interface WebSpeechAlternative { readonly transcript: string }
42
- interface WebSpeechResult { readonly isFinal: boolean; readonly length: number; readonly [index: number]: WebSpeechAlternative }
43
- interface WebSpeechResultList { readonly length: number; readonly [index: number]: WebSpeechResult }
44
- interface WebSpeechResultEvent { readonly results: WebSpeechResultList }
45
- interface WebSpeechErrorEvent { readonly error: string; readonly message?: string }
41
+ interface WebSpeechAlternative {
42
+ readonly transcript: string;
43
+ }
44
+ interface WebSpeechResult {
45
+ readonly isFinal: boolean;
46
+ readonly length: number;
47
+ readonly [index: number]: WebSpeechAlternative;
48
+ }
49
+ interface WebSpeechResultList {
50
+ readonly length: number;
51
+ readonly [index: number]: WebSpeechResult;
52
+ }
53
+ interface WebSpeechResultEvent {
54
+ readonly results: WebSpeechResultList;
55
+ }
56
+ interface WebSpeechErrorEvent {
57
+ readonly error: string;
58
+ readonly message?: string;
59
+ }
46
60
 
47
61
  interface WebSpeechRecognition {
48
62
  lang: string;
@@ -95,7 +109,12 @@ export async function requestSpeechRecognitionPermission(): Promise<SpeechRecogn
95
109
 
96
110
  function domErrorName(error: unknown): string {
97
111
  // A DOMException is not always `instanceof Error`, so read `.name` off the object.
98
- if (typeof error === 'object' && error !== null && 'name' in error && typeof error.name === 'string') {
112
+ if (
113
+ typeof error === 'object' &&
114
+ error !== null &&
115
+ 'name' in error &&
116
+ typeof error.name === 'string'
117
+ ) {
99
118
  return error.name;
100
119
  }
101
120
  return '';
@@ -149,7 +168,8 @@ const LEVEL_INTERVAL_MS = 100;
149
168
 
150
169
  /** An analyser on the microphone, polled on a clock. Returns its teardown. */
151
170
  function meterStream(stream: MediaStream, onLevel: (level: number) => void): () => void {
152
- const AudioContextCtor = (globalThis as unknown as { AudioContext?: typeof AudioContext }).AudioContext;
171
+ const AudioContextCtor = (globalThis as unknown as { AudioContext?: typeof AudioContext })
172
+ .AudioContext;
153
173
  if (AudioContextCtor === undefined) return () => undefined;
154
174
  let context: AudioContext;
155
175
  try {
@@ -0,0 +1,67 @@
1
+ import { englishWorkLog, type Translate } from './work-log-copy';
2
+
3
+ /** Tools that have a name of their own in the catalogue. Others are named from their id. */
4
+ const NAMED_PILL = new Set([
5
+ 'browse',
6
+ 'webSearch',
7
+ 'webScraper',
8
+ 'generateFile',
9
+ 'sendTelegramMessage',
10
+ 'sendSlack',
11
+ 'sendDiscord',
12
+ 'sendSignal',
13
+ 'sendEmail',
14
+ 'delegateToAgent',
15
+ 'askAgent',
16
+ // The API's app router: the model opening a connected app whose tools did not all fit in
17
+ // one request.
18
+ 'useApps',
19
+ 'agentSearch',
20
+ 'userMemory',
21
+ 'shellExec',
22
+ 'fileEdit',
23
+ 'codeInterpreter',
24
+ ]);
25
+
26
+ /** Tools whose finished label differs from the running one; the rest read the same. */
27
+ const NAMED_DONE = new Set([
28
+ 'browse',
29
+ 'webScraper',
30
+ 'generateFile',
31
+ 'delegateToAgent',
32
+ 'askAgent',
33
+ 'useApps',
34
+ 'agentSearch',
35
+ 'userMemory',
36
+ 'shellExec',
37
+ 'fileEdit',
38
+ 'codeInterpreter',
39
+ ]);
40
+
41
+ /** A tool's short label, in the reader's language when a translator is given. */
42
+ export function getToolPillLabel(toolName: string, t: Translate = englishWorkLog): string {
43
+ if (NAMED_PILL.has(toolName)) return t(`tasks.tool.pill.${toolName}`);
44
+
45
+ // Oxy service tools: oxy_serviceName__toolName → "ServiceName"
46
+ if (toolName.startsWith('oxy_')) {
47
+ const parts = toolName.replace('oxy_', '').split('__');
48
+ return (parts[0] ?? '').replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
49
+ }
50
+
51
+ // Fallback: camelCase → "Camel Case"
52
+ return toolName
53
+ .replace(/([A-Z])/g, ' $1')
54
+ .replace(/^./, (c) => c.toUpperCase())
55
+ .trim();
56
+ }
57
+
58
+ /**
59
+ * The finished form of a tool's label, for a task whose call has returned (`TaskList` swaps
60
+ * `runningTitle` for this once the task's steps land). Labels that are already names
61
+ * ("Telegram", "Email") read the same either way, so they fall back to the running label.
62
+ */
63
+ export function getToolDoneLabel(toolName: string, t: Translate = englishWorkLog): string {
64
+ return NAMED_DONE.has(toolName)
65
+ ? t(`tasks.tool.done.${toolName}`)
66
+ : getToolPillLabel(toolName, t);
67
+ }
@@ -0,0 +1,28 @@
1
+ import type { ToolInvocation } from '../types';
2
+
3
+ /**
4
+ * Where a tool call stands:
5
+ * - `running`: still in flight in a live turn.
6
+ * - `done`: it returned a result without an error.
7
+ * - `error`: it returned, and the result carries an `error`.
8
+ * - `interrupted`: it never returned and its turn is over — a stop, a failure, or a
9
+ * persisted turn cut short. It must not read as running.
10
+ */
11
+ export type ToolCallStatus = 'running' | 'done' | 'error' | 'interrupted';
12
+
13
+ export function toolCallStatus(
14
+ inv: Pick<ToolInvocation, 'state' | 'result'>,
15
+ live: boolean,
16
+ ): ToolCallStatus {
17
+ if (inv.state !== 'result') return live ? 'running' : 'interrupted';
18
+ const result: unknown = inv.result;
19
+ if (
20
+ result !== null &&
21
+ typeof result === 'object' &&
22
+ 'error' in result &&
23
+ Boolean((result as { error?: unknown }).error)
24
+ ) {
25
+ return 'error';
26
+ }
27
+ return 'done';
28
+ }
@@ -5,27 +5,27 @@ interface ToolDefinition {
5
5
 
6
6
  const TOOL_REGISTRY: Record<string, ToolDefinition> = {
7
7
  // Search
8
- webSearch: { label: 'Searching the web', category: 'search' },
9
- scrapeURL: { label: 'Reading URL', category: 'search' },
10
- getTimeline: { label: 'Getting timeline', category: 'search' },
11
- searchKnowledgeBase: { label: 'Searching knowledge base', category: 'search' },
12
- webScraper: { label: 'Reading web page', category: 'search' },
13
- browse: { label: 'Browsing the web', category: 'search' },
8
+ webSearch: { label: 'Searching the web', category: 'search' },
9
+ scrapeURL: { label: 'Reading URL', category: 'search' },
10
+ getTimeline: { label: 'Getting timeline', category: 'search' },
11
+ searchKnowledgeBase: { label: 'Searching knowledge base', category: 'search' },
12
+ webScraper: { label: 'Reading web page', category: 'search' },
13
+ browse: { label: 'Browsing the web', category: 'search' },
14
14
 
15
15
  // Communication
16
- sendWhatsAppMessage: { label: 'Sending WhatsApp message', category: 'communication' },
17
- getWhatsAppChats: { label: 'Loading WhatsApp chats', category: 'communication' },
16
+ sendWhatsAppMessage: { label: 'Sending WhatsApp message', category: 'communication' },
17
+ getWhatsAppChats: { label: 'Loading WhatsApp chats', category: 'communication' },
18
18
  getWhatsAppMessages: { label: 'Reading WhatsApp messages', category: 'communication' },
19
- sendTelegramMessage: { label: 'Sending Telegram message', category: 'communication' },
19
+ sendTelegramMessage: { label: 'Sending Telegram message', category: 'communication' },
20
20
 
21
21
  // Utility
22
- getCurrentDate: { label: 'Getting current date', category: 'utility' },
23
- generateFile: { label: 'Generating file', category: 'utility' },
22
+ getCurrentDate: { label: 'Getting current date', category: 'utility' },
23
+ generateFile: { label: 'Generating file', category: 'utility' },
24
24
 
25
25
  // Memory
26
- saveUserMemory: { label: 'Saving to memory', category: 'memory' },
27
- updateUserPreferences: { label: 'Updating preferences', category: 'memory' },
28
- updateUserContext: { label: 'Updating context', category: 'memory' },
26
+ saveUserMemory: { label: 'Saving to memory', category: 'memory' },
27
+ updateUserPreferences: { label: 'Updating preferences', category: 'memory' },
28
+ updateUserContext: { label: 'Updating context', category: 'memory' },
29
29
  };
30
30
 
31
31
  /**
@@ -11,12 +11,11 @@
11
11
 
12
12
  const TAG = String.raw`ALIA_TITLE|TITLE|TÍTULO|TITRE|TITOLO|TITEL|ЗАГОЛОВОК`;
13
13
 
14
- const TITLE_STRIP_RE = new RegExp(
15
- String.raw`\[(${TAG})\].*?\[\/\1\]|<(${TAG})>.*?<\/\2>`, 'gi',
16
- );
14
+ const TITLE_STRIP_RE = new RegExp(String.raw`\[(${TAG})\].*?\[\/\1\]|<(${TAG})>.*?<\/\2>`, 'gi');
17
15
 
18
16
  const TITLE_PARTIAL_RE = new RegExp(
19
- String.raw`\[(${TAG})\].*?(\[\/\1\])?$|<(${TAG})>.*?(<\/\3>)?$`, 'si',
17
+ String.raw`\[(${TAG})\].*?(\[\/\1\])?$|<(${TAG})>.*?(<\/\3>)?$`,
18
+ 'si',
20
19
  );
21
20
 
22
21
  /**
@@ -31,7 +30,10 @@ export function stripTitleTags(content: string, { trim = true }: { trim?: boolea
31
30
  }
32
31
 
33
32
  /** The same, for an answer still streaming: an opened tag hides the rest. */
34
- export function stripTitleTagsPartial(content: string, { trim = true }: { trim?: boolean } = {}): string {
33
+ export function stripTitleTagsPartial(
34
+ content: string,
35
+ { trim = true }: { trim?: boolean } = {},
36
+ ): string {
35
37
  const stripped = content.replace(TITLE_STRIP_RE, '').replace(TITLE_PARTIAL_RE, '');
36
38
  return trim ? stripped.trim() : stripped;
37
39
  }
@@ -106,7 +108,11 @@ export function takeSpeechChunks(
106
108
  rest = '';
107
109
  } else if (rest.length > MAX_RUN_CHARS) {
108
110
  const window = rest.slice(0, MAX_RUN_CHARS);
109
- const pause = Math.max(window.lastIndexOf(', '), window.lastIndexOf('; '), window.lastIndexOf(': '));
111
+ const pause = Math.max(
112
+ window.lastIndexOf(', '),
113
+ window.lastIndexOf('; '),
114
+ window.lastIndexOf(': '),
115
+ );
110
116
  const cut = pause > 0 ? pause + 2 : Math.max(window.lastIndexOf(' '), 1);
111
117
  sentences.push(rest.slice(0, cut));
112
118
  rest = rest.slice(cut);
@@ -43,7 +43,10 @@ export interface VoiceTurn {
43
43
  export type VoiceTurnSender = (turn: VoiceTurn) => Promise<void>;
44
44
 
45
45
  interface LinkedClientFactory {
46
- createLinkedClient(config: { baseURL: string }): { client: AuthenticatedResponseClient; dispose(): void };
46
+ createLinkedClient(config: { baseURL: string }): {
47
+ client: AuthenticatedResponseClient;
48
+ dispose(): void;
49
+ };
47
50
  }
48
51
 
49
52
  export interface AliaVoiceTurnSenderOptions {
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The words of the work log (`work-log.ts`), keyed the way the Alia app's own catalogue keys
3
+ * them, so the app hands its translator straight in. A host without one gets English.
4
+ */
5
+ export type Translate = (key: string, params?: Record<string, unknown>) => string;
6
+
7
+ type Entry = string | { one: string; other: string };
8
+
9
+ const CATALOGUE: Record<string, Entry> = {
10
+ 'chat.bloom.researched': 'Researched',
11
+ 'chat.bloom.searchCount': { one: '1 search', other: '{{count}} searches' },
12
+ 'chat.bloom.searchedWeb': 'Searched the web for',
13
+ 'chat.bloom.searchFailed': 'Search failed',
14
+ 'chat.bloom.noResults': 'No results',
15
+ 'chat.bloom.resultCount': { one: '1 result', other: '{{count}} results' },
16
+ 'chat.bloom.visited': 'Visited',
17
+ 'chat.bloom.researching': 'Researching',
18
+ 'chat.bloom.sourceCount': { one: '1 source', other: '{{count}} sources' },
19
+ 'chat.bloom.createdFile': 'Created',
20
+ 'chat.bloom.writingFile': 'Writing',
21
+ 'chat.bloom.taskInput': 'With',
22
+ 'chat.bloom.taskFailed': 'Returned an error',
23
+ 'chat.bloom.taskStopped': "Didn't finish",
24
+ 'tasks.tool.pill.browse': 'Browsing',
25
+ 'tasks.tool.pill.webSearch': 'Web search',
26
+ 'tasks.tool.pill.webScraper': 'Reading page',
27
+ 'tasks.tool.pill.generateFile': 'Generating file',
28
+ 'tasks.tool.pill.sendTelegramMessage': 'Telegram',
29
+ 'tasks.tool.pill.sendSlack': 'Slack',
30
+ 'tasks.tool.pill.sendDiscord': 'Discord',
31
+ 'tasks.tool.pill.sendSignal': 'Signal',
32
+ 'tasks.tool.pill.sendEmail': 'Email',
33
+ 'tasks.tool.pill.delegateToAgent': 'Delegating',
34
+ 'tasks.tool.pill.askAgent': 'Asking an agent',
35
+ 'tasks.tool.pill.useApps': 'Opening apps',
36
+ 'tasks.tool.pill.agentSearch': 'Searching agents',
37
+ 'tasks.tool.pill.userMemory': 'Remembering',
38
+ 'tasks.tool.pill.shellExec': 'Running command',
39
+ 'tasks.tool.pill.fileEdit': 'Editing file',
40
+ 'tasks.tool.pill.codeInterpreter': 'Running code',
41
+ 'tasks.tool.done.browse': 'Browsed',
42
+ 'tasks.tool.done.webScraper': 'Read page',
43
+ 'tasks.tool.done.generateFile': 'Generated file',
44
+ 'tasks.tool.done.delegateToAgent': 'Delegated',
45
+ 'tasks.tool.done.askAgent': 'Asked an agent',
46
+ 'tasks.tool.done.useApps': 'Opened apps',
47
+ 'tasks.tool.done.agentSearch': 'Searched agents',
48
+ 'tasks.tool.done.userMemory': 'Remembered',
49
+ 'tasks.tool.done.shellExec': 'Ran command',
50
+ 'tasks.tool.done.fileEdit': 'Edited file',
51
+ 'tasks.tool.done.codeInterpreter': 'Ran code',
52
+ };
53
+
54
+ /** The default translator: the English above, with `{{count}}` plurals. */
55
+ export const englishWorkLog: Translate = (key, params) => {
56
+ const entry = CATALOGUE[key];
57
+ if (entry === undefined) return key;
58
+ const count = typeof params?.count === 'number' ? params.count : undefined;
59
+ const text = typeof entry === 'string' ? entry : count === 1 ? entry.one : entry.other;
60
+ return text.replace('{{count}}', String(count ?? ''));
61
+ };