@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
@@ -0,0 +1,123 @@
1
+ import { useOxy } from '@oxy.so/services/ui/client';
2
+ import { useMemo } from 'react';
3
+ import { useAliaRuntime } from './config';
4
+
5
+ /**
6
+ * A refusal or failure from Alia's API, shaped so the helpers in
7
+ * `error-utils.ts` read it: `status`, and the parsed body under
8
+ * `response.data`.
9
+ */
10
+ export class AliaApiError extends Error {
11
+ readonly status: number;
12
+ readonly response: { status: number; data: unknown };
13
+
14
+ constructor(status: number, data: unknown, message?: string) {
15
+ super(message ?? `Alia request failed with status ${status}.`);
16
+ this.name = 'AliaApiError';
17
+ this.status = status;
18
+ this.response = { status, data };
19
+ }
20
+ }
21
+
22
+ export interface AliaApiResult<T> {
23
+ status: number;
24
+ data: T;
25
+ }
26
+
27
+ export interface AliaApiRequest {
28
+ params?: Record<string, string | number | undefined>;
29
+ body?: unknown;
30
+ signal?: AbortSignal;
31
+ }
32
+
33
+ /**
34
+ * Alia's REST API for the signed-in person. Authentication, the preflight
35
+ * refresh and the single 401 replay belong to the linked Oxy client; this
36
+ * layer never reads or writes a bearer itself.
37
+ */
38
+ export interface AliaApi {
39
+ get<T>(path: string, request?: AliaApiRequest): Promise<AliaApiResult<T>>;
40
+ post<T>(path: string, body?: unknown, request?: AliaApiRequest): Promise<AliaApiResult<T>>;
41
+ patch<T>(path: string, body?: unknown, request?: AliaApiRequest): Promise<AliaApiResult<T>>;
42
+ delete<T>(path: string, request?: AliaApiRequest): Promise<AliaApiResult<T>>;
43
+ }
44
+
45
+ function withParams(path: string, params: AliaApiRequest['params']): string {
46
+ if (!params) return path;
47
+ const search = new URLSearchParams();
48
+ for (const [key, value] of Object.entries(params)) {
49
+ if (value !== undefined) search.append(key, String(value));
50
+ }
51
+ const query = search.toString();
52
+ return query === '' ? path : `${path}${path.includes('?') ? '&' : '?'}${query}`;
53
+ }
54
+
55
+ async function readBody(response: Response): Promise<unknown> {
56
+ const text = await response.text().catch(() => '');
57
+ if (text === '') return undefined;
58
+ try {
59
+ return JSON.parse(text);
60
+ } catch {
61
+ return text;
62
+ }
63
+ }
64
+
65
+ /** The structural slice of `OxyServices` the API layer needs. */
66
+ export interface LinkableOxy {
67
+ createLinkedClient(config: { baseURL: string }): {
68
+ client: {
69
+ requestAuthenticatedResponse(config: {
70
+ method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
71
+ url: string;
72
+ body?: string;
73
+ headers?: Record<string, string>;
74
+ signal?: AbortSignal;
75
+ fetch?: typeof fetch;
76
+ }): Promise<Response>;
77
+ };
78
+ dispose(): void;
79
+ };
80
+ }
81
+
82
+ /** Build the API over an Oxy client for `apiUrl`. Exposed for tests and for non-React callers. */
83
+ export function createAliaApi(oxyServices: LinkableOxy, apiUrl: string): AliaApi {
84
+ async function send<T>(
85
+ method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
86
+ path: string,
87
+ request: AliaApiRequest = {},
88
+ ): Promise<AliaApiResult<T>> {
89
+ const linked = oxyServices.createLinkedClient({ baseURL: apiUrl });
90
+ try {
91
+ const response = await linked.client.requestAuthenticatedResponse({
92
+ method,
93
+ url: withParams(path, request.params),
94
+ ...(request.body === undefined
95
+ ? {}
96
+ : {
97
+ body: JSON.stringify(request.body),
98
+ headers: { 'Content-Type': 'application/json' },
99
+ }),
100
+ ...(request.signal ? { signal: request.signal } : {}),
101
+ });
102
+ const data = await readBody(response);
103
+ if (!response.ok) throw new AliaApiError(response.status, data);
104
+ return { status: response.status, data: data as T };
105
+ } finally {
106
+ linked.dispose();
107
+ }
108
+ }
109
+
110
+ return {
111
+ get: (path, request) => send('GET', path, request),
112
+ post: (path, body, request) => send('POST', path, { ...request, body: body ?? {} }),
113
+ patch: (path, body, request) => send('PATCH', path, { ...request, body: body ?? {} }),
114
+ delete: (path, request) => send('DELETE', path, request),
115
+ };
116
+ }
117
+
118
+ /** Alia's REST API for the nearest runtime's `apiUrl`, as the signed-in person. */
119
+ export function useAliaApi(): AliaApi {
120
+ const { oxyServices } = useOxy();
121
+ const { apiUrl } = useAliaRuntime();
122
+ return useMemo(() => createAliaApi(oxyServices as LinkableOxy, apiUrl), [oxyServices, apiUrl]);
123
+ }
@@ -0,0 +1,96 @@
1
+ import type { AliaAttachment } from '../types';
2
+
3
+ export type MessageContentPart =
4
+ | { type: 'text'; text: string }
5
+ | { type: 'image_url'; image_url: { url: string } }
6
+ | { type: 'file'; file: { file_id: string; filename: string; mime: string } };
7
+
8
+ export interface BuiltMessageContent {
9
+ content: string | MessageContentPart[];
10
+ /**
11
+ * Attachments the request does NOT carry, so the caller can say so.
12
+ *
13
+ * Never empty silently: everything here was visibly attached in the composer,
14
+ * and a file that appears attached and then is not sent is the failure #608
15
+ * §1.6 is about — a visible state that does not correspond to a real
16
+ * capability.
17
+ */
18
+ dropped: DroppedAttachment[];
19
+ }
20
+
21
+ export interface DroppedAttachment {
22
+ name: string;
23
+ /**
24
+ * `unsupported` — the chat has no way to send a document (no upload was given).
25
+ * `unreadable` — an image whose bytes could not be read off the device, or a
26
+ * document that could not be uploaded.
27
+ */
28
+ reason: 'unsupported' | 'unreadable';
29
+ }
30
+
31
+ /** Puts a document in the person's Oxy Files and answers its id. */
32
+ export type UploadDocument = (attachment: AliaAttachment) => Promise<string>;
33
+
34
+ /**
35
+ * The request body for a turn that carries attachments, and an account of what
36
+ * it could not carry.
37
+ *
38
+ * A photo travels inline, as an `image_url` part with its `data:` URL. A
39
+ * document travels as a `file` part naming an Oxy Files id
40
+ * (`docs/agent-embedding.md` §5): it is uploaded to the person's Files first,
41
+ * with `upload`, and a chat given no `upload` cannot send documents and says so.
42
+ */
43
+ export async function buildMessageContent(
44
+ text: string,
45
+ attachments: AliaAttachment[],
46
+ upload?: UploadDocument,
47
+ ): Promise<BuiltMessageContent> {
48
+ const dropped: DroppedAttachment[] = [];
49
+
50
+ const usable = attachments.filter((att) => {
51
+ if (!att.uri) return false;
52
+ if (att.type !== 'image' && upload === undefined) {
53
+ dropped.push({ name: att.name, reason: 'unsupported' });
54
+ return false;
55
+ }
56
+ return true;
57
+ });
58
+
59
+ if (usable.length === 0) return { content: text, dropped };
60
+
61
+ const parts: MessageContentPart[] = [{ type: 'text', text }];
62
+
63
+ // The readers need the platform's file access, loaded on the first attachment: a turn of plain
64
+ // text (and everything that imports this module) never touches it.
65
+ const { imageUriToDataUrl } = await import('../lib/image-data-url');
66
+ for (const att of usable) {
67
+ if (att.type !== 'image' && upload !== undefined) {
68
+ try {
69
+ const fileId = await upload(att);
70
+ parts.push({
71
+ type: 'file',
72
+ file: { file_id: fileId, filename: att.name, mime: att.mimeType },
73
+ });
74
+ } catch {
75
+ dropped.push({ name: att.name, reason: 'unreadable' });
76
+ }
77
+ continue;
78
+ }
79
+ try {
80
+ parts.push({
81
+ type: 'image_url',
82
+ image_url: { url: await imageUriToDataUrl(att.uri, att.mimeType) },
83
+ });
84
+ } catch {
85
+ // Reported rather than skipped. A picture that did not arrive is worth
86
+ // a sentence; it used to be a `continue`.
87
+ dropped.push({ name: att.name, reason: 'unreadable' });
88
+ }
89
+ }
90
+
91
+ // Every attachment failed, so the parts array is a text part and nothing
92
+ // else — which is just the text, said in a more expensive way.
93
+ if (parts.length === 1) return { content: text, dropped };
94
+
95
+ return { content: parts, dropped };
96
+ }
@@ -0,0 +1,270 @@
1
+ import { useMemo } from 'react';
2
+ import { create } from 'zustand';
3
+ import type { AliaAttachment } from '../types';
4
+
5
+ /**
6
+ * What a composer is holding before it is sent: the text, the attachments and
7
+ * the turn's own choices (connector, skills). One record per account and
8
+ * composer, and the only place any of it lives.
9
+ *
10
+ * ## Why this replaced three owners
11
+ *
12
+ * The text was `useState` inside each mounted chat screen, the attachments
13
+ * were ONE global list in `global-store.ts`, and a draft handed from elsewhere
14
+ * (a failed send, the memory settings) travelled through a third slot,
15
+ * `composerDraft`, that every mounted screen read on every render and matched
16
+ * against its own id with a sequence number. The drawer keeps each visited
17
+ * chat mounted, so the attachment list was on every one of them at once — a
18
+ * picture added in one chat was sitting in the composer of the next — and
19
+ * nothing reset any of it when the account changed, so profile B was handed
20
+ * profile A's half-written message and files (#608 §4).
21
+ *
22
+ * Now each composer names its draft by {@link DraftTarget}, reads only that
23
+ * one, and a hand-off is a write to the target's record: there is nothing to
24
+ * broadcast and nothing to "consume".
25
+ *
26
+ * ## Accounts
27
+ *
28
+ * The store holds the drafts of ONE account, the one bound by the app layout
29
+ * (`bindAccount`). Moving to a different account, or signing out, drops every
30
+ * draft and releases what their attachments were holding. The one exception
31
+ * is signing IN from signed-out: the drafts carry over, because they were
32
+ * typed by the person now signing in — "continue without an account" leaves
33
+ * a draft that must still be there after the sign-in its send opened (#608
34
+ * §3.2).
35
+ *
36
+ * Every write names the account it was made for ({@link DraftAddress}). A
37
+ * write for an account that is no longer bound — a send that fails after the
38
+ * switch, a picker that resolves after it — is refused and its attachments
39
+ * released, so a slow answer for A cannot land in B's composer.
40
+ *
41
+ * ## Not persisted
42
+ *
43
+ * A picture is a `data:` or `blob:` URL; writing megabytes of base64 into
44
+ * AsyncStorage on every keystroke is not a draft feature worth its cost, and a
45
+ * `blob:` URL does not survive a reload anyway.
46
+ */
47
+
48
+ /**
49
+ * Which composer: a conversation's id, `null` for the new-chat screen (ghost
50
+ * included — it is the same screen), or a `surface:` name for a screen that
51
+ * asks Alia something outside a chat.
52
+ */
53
+ export type DraftTarget = string | null;
54
+
55
+ /** A draft's owner at the time the address was taken. */
56
+ export interface DraftAddress {
57
+ account: string | null;
58
+ target: DraftTarget;
59
+ }
60
+
61
+ export interface ComposerDraft {
62
+ text: string;
63
+ attachments: AliaAttachment[];
64
+ /** The connector chosen for the next turn. */
65
+ mcpServerId: string | null;
66
+ /** The skills chosen for the next turn. */
67
+ skillNames: string[];
68
+ /**
69
+ * Set while the composer is rewriting a turn already sent rather than
70
+ * writing a new one: its send replaces that turn and everything after it.
71
+ * Absent for an ordinary draft.
72
+ */
73
+ editing?: DraftEdit;
74
+ }
75
+
76
+ export type DraftTurn = Pick<ComposerDraft, 'mcpServerId' | 'skillNames'>;
77
+
78
+ /** What the composer held before an edit took it over, for a cancel to give back. */
79
+ export type DraftBeforeEdit = Pick<ComposerDraft, 'text' | 'mcpServerId' | 'skillNames'>;
80
+
81
+ export interface DraftEdit {
82
+ /** The user turn being rewritten. */
83
+ messageId: string;
84
+ before: DraftBeforeEdit;
85
+ }
86
+
87
+ /** The turn an edit loads into the composer: its words and the options it was sent with. */
88
+ export interface EditedTurn extends DraftTurn {
89
+ messageId: string;
90
+ text: string;
91
+ }
92
+
93
+ export const EMPTY_DRAFT: ComposerDraft = Object.freeze({
94
+ text: '',
95
+ attachments: [],
96
+ mcpServerId: null,
97
+ skillNames: [],
98
+ }) as ComposerDraft;
99
+
100
+ /**
101
+ * Give back a temporary URL, if this one is temporary.
102
+ *
103
+ * On web both pickers — and a dropped document — hand back
104
+ * `URL.createObjectURL(file)`, which pins the file's bytes in the page until
105
+ * it is revoked. `data:` and `file://` URLs are plain strings: nothing to give
106
+ * back.
107
+ */
108
+ export function releaseAttachmentUri(uri: string | undefined): void {
109
+ if (uri === undefined || !uri.startsWith('blob:')) return;
110
+ if (typeof URL === 'undefined' || typeof URL.revokeObjectURL !== 'function') return;
111
+ URL.revokeObjectURL(uri);
112
+ }
113
+
114
+ export function releaseAttachments(attachments: readonly AliaAttachment[]): void {
115
+ for (const attachment of attachments) releaseAttachmentUri(attachment.uri);
116
+ }
117
+
118
+ const keyOf = (target: DraftTarget) => target ?? '';
119
+
120
+ interface DraftStoreState {
121
+ /** The account whose drafts these are; `null` while signed out. */
122
+ account: string | null;
123
+ drafts: Record<string, ComposerDraft>;
124
+
125
+ bindAccount: (userId: string | null) => void;
126
+ /** The address of `target` under the account bound now. */
127
+ address: (target: DraftTarget) => DraftAddress;
128
+
129
+ setText: (address: DraftAddress, text: string) => void;
130
+ addAttachment: (address: DraftAddress, attachment: AliaAttachment) => void;
131
+ /** Takes the tile away and releases its URL. */
132
+ removeAttachment: (address: DraftAddress, id: string) => void;
133
+ /** Change the turn's connector and skills, from what the draft holds now. */
134
+ updateTurn: (address: DraftAddress, change: (turn: DraftTurn) => DraftTurn) => void;
135
+ /**
136
+ * Put a draft back — a send that failed, or text another screen hands to a
137
+ * composer. The fields given replace the draft's; attachments given are put
138
+ * back in front of any added since, which are kept.
139
+ */
140
+ restore: (address: DraftAddress, draft: Partial<ComposerDraft>) => void;
141
+ /**
142
+ * Empty the draft for a send. The attachments are NOT released: the send
143
+ * still needs them, and a failed one hands them back through `restore`.
144
+ */
145
+ clear: (address: DraftAddress) => void;
146
+ /**
147
+ * Load a sent turn into the composer to be rewritten. What the composer held
148
+ * is kept aside (attachments stay where they are), and a second edit started
149
+ * before the first is sent or cancelled keeps the ORIGINAL draft aside, not
150
+ * the first edit's text.
151
+ */
152
+ startEdit: (address: DraftAddress, turn: EditedTurn) => void;
153
+ /** Leave edit mode and give the composer back what it held before. */
154
+ cancelEdit: (address: DraftAddress) => void;
155
+ }
156
+
157
+ export const useComposerDraftStore = create<DraftStoreState>((set, get) => {
158
+ /** Apply `change` to the target's draft, if the address is still current. */
159
+ const write = (
160
+ address: DraftAddress,
161
+ change: (draft: ComposerDraft) => ComposerDraft | null,
162
+ rejected?: () => void,
163
+ ) => {
164
+ if (address.account !== get().account) {
165
+ rejected?.();
166
+ return;
167
+ }
168
+ set((state) => {
169
+ const key = keyOf(address.target);
170
+ const next = change(state.drafts[key] ?? EMPTY_DRAFT);
171
+ const drafts = { ...state.drafts };
172
+ if (next === null) delete drafts[key];
173
+ else drafts[key] = next;
174
+ return { drafts };
175
+ });
176
+ };
177
+
178
+ return {
179
+ account: null,
180
+ drafts: {},
181
+
182
+ bindAccount: (userId) => {
183
+ const { account, drafts } = get();
184
+ if (userId === account) return;
185
+ // Signed out -> signed in: the same person, carrying what they typed.
186
+ if (account === null) {
187
+ set({ account: userId });
188
+ return;
189
+ }
190
+ for (const draft of Object.values(drafts)) releaseAttachments(draft.attachments);
191
+ set({ account: userId, drafts: {} });
192
+ },
193
+
194
+ address: (target) => ({ account: get().account, target }),
195
+
196
+ setText: (address, text) => write(address, (draft) => ({ ...draft, text })),
197
+
198
+ addAttachment: (address, attachment) =>
199
+ write(
200
+ address,
201
+ (draft) => ({ ...draft, attachments: [...draft.attachments, attachment] }),
202
+ () => releaseAttachmentUri(attachment.uri),
203
+ ),
204
+
205
+ removeAttachment: (address, id) =>
206
+ write(address, (draft) => {
207
+ const removed = draft.attachments.find((a) => a.id === id);
208
+ if (removed === undefined) return draft;
209
+ releaseAttachmentUri(removed.uri);
210
+ return { ...draft, attachments: draft.attachments.filter((a) => a.id !== id) };
211
+ }),
212
+
213
+ updateTurn: (address, change) =>
214
+ write(address, (draft) => ({
215
+ ...draft,
216
+ ...change({ mcpServerId: draft.mcpServerId, skillNames: draft.skillNames }),
217
+ })),
218
+
219
+ restore: (address, restored) =>
220
+ write(
221
+ address,
222
+ (draft) => {
223
+ const returning = restored.attachments ?? [];
224
+ const ids = new Set(returning.map((a) => a.id));
225
+ return {
226
+ ...draft,
227
+ ...restored,
228
+ attachments: [...returning, ...draft.attachments.filter((a) => !ids.has(a.id))],
229
+ };
230
+ },
231
+ () => releaseAttachments(restored.attachments ?? []),
232
+ ),
233
+
234
+ clear: (address) => write(address, () => null),
235
+
236
+ startEdit: (address, turn) =>
237
+ write(address, (draft) => ({
238
+ ...draft,
239
+ text: turn.text,
240
+ mcpServerId: turn.mcpServerId,
241
+ skillNames: turn.skillNames,
242
+ editing: {
243
+ messageId: turn.messageId,
244
+ before: draft.editing?.before ?? {
245
+ text: draft.text,
246
+ mcpServerId: draft.mcpServerId,
247
+ skillNames: draft.skillNames,
248
+ },
249
+ },
250
+ })),
251
+
252
+ cancelEdit: (address) =>
253
+ write(address, (draft) => {
254
+ if (draft.editing === undefined) return draft;
255
+ const { editing, ...rest } = draft;
256
+ return { ...rest, ...editing.before };
257
+ }),
258
+ };
259
+ });
260
+
261
+ /** The draft of `target`, as a component reads it. */
262
+ export function useComposerDraft(target: DraftTarget): ComposerDraft {
263
+ return useComposerDraftStore((state) => state.drafts[keyOf(target)] ?? EMPTY_DRAFT);
264
+ }
265
+
266
+ /** The address of `target` under the account bound now, as a component takes it. */
267
+ export function useDraftAddress(target: DraftTarget): DraftAddress {
268
+ const account = useComposerDraftStore((state) => state.account);
269
+ return useMemo(() => ({ account, target }), [account, target]);
270
+ }
@@ -0,0 +1,186 @@
1
+ import { createContext, type ReactNode, useContext, useMemo } from 'react';
2
+ import { ALIA_API_URL } from '../lib/origins';
3
+ import type { AgentRun, Message } from './types';
4
+
5
+ /**
6
+ * Where a signed-out session keeps its conversations, per account. The host
7
+ * binds it to the signed-in account; unbound it reads nothing and writes
8
+ * nothing. `AccountScopedKey` in the Alia app is one.
9
+ */
10
+ export interface OfflineConversationStore {
11
+ getItem(): Promise<string | null>;
12
+ setItem(value: string): Promise<void>;
13
+ }
14
+
15
+ /** An artifact a tool produced (a generated file, a canvas), for the host to show beside the thread. */
16
+ export interface AliaArtifact {
17
+ id: string;
18
+ type: string;
19
+ content: unknown;
20
+ title: string;
21
+ timestamp: number;
22
+ }
23
+
24
+ /** The usage frame a stream ends with: the balance left and, sometimes, a spending warning. */
25
+ export interface AliaUsageFrame {
26
+ credits_remaining?: number;
27
+ credit_warning?: {
28
+ level?: string;
29
+ daysRemaining?: unknown;
30
+ todaySpend?: unknown;
31
+ avgDailySpend?: unknown;
32
+ currentModelMultiplier?: unknown;
33
+ };
34
+ [key: string]: unknown;
35
+ }
36
+
37
+ /** What the request carries that the host's own controls decide, read at send time. */
38
+ export interface AliaTurnRequest {
39
+ /** Only `false` is sent: `true` is the server's default. */
40
+ webSearch?: boolean;
41
+ deepResearch?: boolean;
42
+ }
43
+
44
+ /** Why the thread asks its host to show the thought panel. */
45
+ export type ThoughtOpening =
46
+ /** The reader pressed a turn's "worked for" line or a sources row. */
47
+ | 'pressed'
48
+ /** A running turn began using tools; offered only while the host has nothing else open. */
49
+ | 'automatic';
50
+
51
+ /**
52
+ * How the person speaks and likes to be spoken to: read-aloud, dictation and calls all use it.
53
+ * Each is the server's or the platform's own default when absent.
54
+ */
55
+ export interface SpeechPreferences {
56
+ voice?: 'male' | 'female';
57
+ tone?: 'brief' | 'chill' | 'default';
58
+ /** The language spoken and dictated, a BCP 47 tag; the platform's own when absent. */
59
+ language?: string;
60
+ }
61
+
62
+ /**
63
+ * Everything the conversation runtime needs from the app around it, as typed
64
+ * callbacks. Every member is optional: a host that omits one gets the plain
65
+ * behaviour (no navigation, no artifacts panel, no credits). The Alia app and
66
+ * the sheet mount the SAME runtime and differ only in what they put here.
67
+ */
68
+ export interface AliaRuntimeHost {
69
+ /** Device facts sent as `X-Device-Info`, so the model's `getDeviceInfo` tool can answer. */
70
+ deviceInfo?: () => Promise<unknown>;
71
+ /** The modes the host's composer controls own; read when the turn is sent, not when it is built. */
72
+ turnRequest?: () => AliaTurnRequest;
73
+ /** A turn is about to be sent from the composer or a retry. */
74
+ onTurnStart?: () => void;
75
+ /** Real answer text just arrived (throttled): a haptic tick. */
76
+ onOutput?: () => void;
77
+ /** The stream's usage frame: credits left and the spending warning. */
78
+ onUsage?: (usage: AliaUsageFrame) => void;
79
+ /** A tool finished; `name` is the tool, `output` its structural result. */
80
+ onToolResult?: (result: {
81
+ toolCallId: string;
82
+ name: string | undefined;
83
+ output: unknown;
84
+ }) => void;
85
+ /** A tool produced something worth its own panel. */
86
+ onArtifact?: (artifact: AliaArtifact) => void;
87
+ /** What this turn put in the context window (`alia.context`), raw. */
88
+ onContext?: (conversationId: string | null, payload: unknown) => void;
89
+ /**
90
+ * An agent's feature gate refused what the person tried (`feature_entitlement_required`, or a
91
+ * locked row of the attach menu): `key` is the feature, e.g. `attachments.documents`. The host
92
+ * shows its own upsell; the person's words stay in the composer.
93
+ */
94
+ onFeatureLocked?: (key: string) => void;
95
+ /** A delegated agent started a run of its own for this turn. */
96
+ onAgentTurn?: (turn: AgentRun & { conversationId: string | null }) => void;
97
+ /** The agent's row in the host's list, ahead of the server: your line, then her answer. */
98
+ onTurnPreview?: (agentId: string | null | undefined, text: string) => void;
99
+ /**
100
+ * Messages an agent writes into the open conversation by itself (a result, a check-in).
101
+ * Returns the unsubscribe.
102
+ */
103
+ subscribeConversationMessages?: (
104
+ conversationId: string,
105
+ listener: (message: Message) => void,
106
+ ) => () => void;
107
+ /**
108
+ * Show where the work of a turn is read — the host's right panel — now that the thread selected
109
+ * one (`useThoughtStore`; the panel is `ThoughtPanel`). Return `false` to decline an
110
+ * `automatic` opening because something else is open. A host without it has no thought panel:
111
+ * the thread draws the "worked for" line as text and never opens one.
112
+ */
113
+ showThought?: (opening: ThoughtOpening) => boolean;
114
+ /** A scheduled-task card in the thread was pressed: show that task. Omitted, the card is not pressable. */
115
+ openAutomation?: (automationId: string) => void;
116
+ /** The URL of a site's cached icon for the work log's sources, or nothing for Bloom's quiet dot. */
117
+ faviconUrl?: (url: string) => string | null | undefined;
118
+ /** The runtime created or chose a conversation: show it. Omitted, the host shows nothing new. */
119
+ openConversation?: (conversationId: string) => void;
120
+ /** The open conversation is gone (deleted elsewhere, or a first send failed): leave it. */
121
+ closeConversation?: () => void;
122
+ }
123
+
124
+ export interface AliaRuntimeConfig {
125
+ /** Alia's API origin. Every route the SDK calls hangs off it, under `/v1` (`routes.ts`). */
126
+ apiUrl: string;
127
+ /** The offline copy of the conversations, when the host keeps one. */
128
+ offline: OfflineConversationStore | null;
129
+ /**
130
+ * The `fetch` a turn is streamed with. React Native's own cannot stream a
131
+ * body; Expo's (`expo/fetch`) can. Omitted, the platform's.
132
+ */
133
+ streamFetch: typeof fetch | undefined;
134
+ /** How read-aloud, dictation and calls speak; omitted, the server's and the platform's defaults. */
135
+ speech: SpeechPreferences;
136
+ host: AliaRuntimeHost;
137
+ }
138
+
139
+ export interface AliaRuntimeProviderProps extends Partial<Omit<AliaRuntimeConfig, 'host'>> {
140
+ host?: AliaRuntimeHost;
141
+ children: ReactNode;
142
+ }
143
+
144
+ const DEFAULT_CONFIG: AliaRuntimeConfig = {
145
+ apiUrl: ALIA_API_URL,
146
+ offline: null,
147
+ streamFetch: undefined,
148
+ speech: {},
149
+ host: {},
150
+ };
151
+
152
+ const AliaRuntimeContext = createContext<AliaRuntimeConfig>(DEFAULT_CONFIG);
153
+
154
+ /**
155
+ * Configure the conversation runtime for everything below. Providers nest and
156
+ * merge: what a nearer one leaves out, the outer one (or the default) answers.
157
+ * The sheet and the screen mount one from their own props; the Alia app mounts
158
+ * one above its routes, so its sidebar and its conversation read the same
159
+ * conversations through the same hooks.
160
+ */
161
+ export function AliaRuntimeProvider({
162
+ apiUrl,
163
+ offline,
164
+ streamFetch,
165
+ speech,
166
+ host,
167
+ children,
168
+ }: AliaRuntimeProviderProps) {
169
+ const parent = useContext(AliaRuntimeContext);
170
+ const value = useMemo<AliaRuntimeConfig>(() => {
171
+ const nextApiUrl = apiUrl ?? parent.apiUrl;
172
+ return {
173
+ apiUrl: nextApiUrl,
174
+ offline: offline === undefined ? parent.offline : offline,
175
+ streamFetch: streamFetch ?? parent.streamFetch,
176
+ speech: speech ?? parent.speech,
177
+ host: host === undefined ? parent.host : { ...parent.host, ...host },
178
+ };
179
+ }, [apiUrl, offline, streamFetch, speech, host, parent]);
180
+ return <AliaRuntimeContext.Provider value={value}>{children}</AliaRuntimeContext.Provider>;
181
+ }
182
+
183
+ /** The runtime's configuration: the nearest provider's, or the defaults. */
184
+ export function useAliaRuntime(): AliaRuntimeConfig {
185
+ return useContext(AliaRuntimeContext);
186
+ }