@k2b/cloud 0.23.0 → 0.24.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 (45) hide show
  1. package/package.json +2 -2
  2. package/src/_internal/capabilities.ts +230 -75
  3. package/src/_internal/define-app.ts +12 -3
  4. package/src/_internal/fixtures/filesv2-manifest-cloud-v0.29.0.json +887 -0
  5. package/src/_internal/page-responses.ts +1 -1
  6. package/src/_internal/registry.ts +78 -3
  7. package/src/ai/chat/blocks.tsx +2 -1
  8. package/src/ai/chat/messages.ts +6 -0
  9. package/src/ai/client/controller.ts +65 -20
  10. package/src/ai/client/transport.ts +5 -5
  11. package/src/ai/code-mode-skill.ts +2 -2
  12. package/src/ai/index.ts +0 -7
  13. package/src/ai/live-events.ts +1 -262
  14. package/src/ai/live.ts +31 -2
  15. package/src/ai/migrate.ts +35 -16
  16. package/src/ai/runtime.ts +1 -6
  17. package/src/ai/solid.ts +0 -5
  18. package/src/api/help.ts +2 -2
  19. package/src/api/pwa-phone.ts +10 -3
  20. package/src/api/search/schemas.ts +34 -0
  21. package/src/api/search.ts +118 -50
  22. package/src/browser/CloudResourceSearch.browser-harness.tsx +11 -0
  23. package/src/browser/CloudResourceSearch.tsx +208 -80
  24. package/src/browser/resource-search-messages.ts +15 -2
  25. package/src/browser/search-stream.ts +151 -0
  26. package/src/contracts/capabilities.ts +89 -24
  27. package/src/contracts/capability-compatibility.ts +28 -14
  28. package/src/contracts/file-provider.ts +158 -0
  29. package/src/contracts/index.ts +1 -0
  30. package/src/events/live-engine.ts +11 -7
  31. package/src/events/live.ts +32 -32
  32. package/src/server/help.ts +3 -3
  33. package/src/services/index.ts +1 -1
  34. package/src/services/outbox.ts +29 -115
  35. package/src/services/pdf/markdown.ts +22 -2
  36. package/src/services/pwa-devices.ts +74 -8
  37. package/src/shared/markdown/formula.ts +122 -26
  38. package/src/shared/markdown/index.ts +27 -14
  39. package/src/ssr/PwaLayout.tsx +2 -1
  40. package/src/styles/resource-search.css +19 -0
  41. package/src/ai/client/live-connection.ts +0 -141
  42. package/src/ai/live-messages.ts +0 -45
  43. package/src/ai/live-outbox.ts +0 -102
  44. package/src/ai/live-routes.ts +0 -412
  45. package/src/shared/markdown/extensions/info-blocks.ts +0 -79
@@ -1,5 +1,4 @@
1
1
  import { z } from "zod";
2
- import type { AiStreamEvent } from "./protocol";
3
2
 
4
3
  export const AI_INVALIDATION_DOMAINS = [
5
4
  "conversation-list",
@@ -17,8 +16,8 @@ export const AiInvalidationDomainSchema = z.enum(AI_INVALIDATION_DOMAINS);
17
16
  export type AiInvalidationDomain = z.infer<typeof AiInvalidationDomainSchema>;
18
17
 
19
18
  export const AiResourceIdSchema = z.string().regex(/^[0-9A-Za-z]{6}$/);
20
- export const AiLiveCursorSchema = z.string().regex(/^s6t\.[a-zA-Z0-9_-]+\.\d+$/);
21
19
 
20
+ /** The data of one AI live update on the `user` channel of `/api/ai/live`: which views of the user's AI data changed. */
22
21
  export const AiInvalidationSchema = z
23
22
  .object({
24
23
  type: z.literal("ai.invalidated"),
@@ -31,263 +30,3 @@ export const AiInvalidationSchema = z
31
30
  .strict();
32
31
 
33
32
  export type AiInvalidation = z.infer<typeof AiInvalidationSchema>;
34
-
35
- export const AI_LIVE_WS_TYPE = {
36
- subscribe: "ai.live.subscribe",
37
- ready: "ai.live.ready",
38
- event: "ai.live.event",
39
- scopeChanged: "ai.live.scope_changed",
40
- revoked: "ai.live.revoked",
41
- error: "ai.live.error",
42
- turnSubscribe: "ai.turn.subscribe",
43
- turnUnsubscribe: "ai.turn.unsubscribe",
44
- turnEvent: "ai.turn.event",
45
- turnError: "ai.turn.error",
46
- } as const;
47
-
48
- const AiLiveSubscribeMessageSchema = z
49
- .object({
50
- type: z.literal(AI_LIVE_WS_TYPE.subscribe),
51
- payload: z.object({ fromCursor: AiLiveCursorSchema.nullable(), recover: z.boolean() }).strict(),
52
- })
53
- .strict();
54
-
55
- const AiTurnSubscribeMessageSchema = z
56
- .object({
57
- type: z.literal(AI_LIVE_WS_TYPE.turnSubscribe),
58
- payload: z.object({ conversationId: AiResourceIdSchema }).strict(),
59
- })
60
- .strict();
61
-
62
- const AiTurnUnsubscribeMessageSchema = z
63
- .object({
64
- type: z.literal(AI_LIVE_WS_TYPE.turnUnsubscribe),
65
- payload: z.object({ conversationId: AiResourceIdSchema }).strict(),
66
- })
67
- .strict();
68
-
69
- export const AiLiveClientMessageSchema = z.discriminatedUnion("type", [
70
- AiLiveSubscribeMessageSchema,
71
- AiTurnSubscribeMessageSchema,
72
- AiTurnUnsubscribeMessageSchema,
73
- ]);
74
- export type AiLiveClientMessage = z.infer<typeof AiLiveClientMessageSchema>;
75
-
76
- const AiLiveRevocationCodeSchema = z.enum(["login_required", "access_denied"]);
77
- const AiLiveErrorCodeSchema = z.enum(["invalid_json", "invalid_message", "backpressure", "internal_error", "stream_failed"]);
78
- const AiTurnErrorCodeSchema = z.enum(["not_found", "access_denied", "stream_failed"]);
79
-
80
- export type AiLiveRevocationCode = z.infer<typeof AiLiveRevocationCodeSchema>;
81
- export type AiLiveErrorCode = z.infer<typeof AiLiveErrorCodeSchema>;
82
- export type AiTurnErrorCode = z.infer<typeof AiTurnErrorCodeSchema>;
83
-
84
- const AiTurnBlockSchema = z.discriminatedUnion("kind", [
85
- z.object({ id: z.string(), kind: z.literal("text"), text: z.string() }).strict(),
86
- z.object({ id: z.string(), kind: z.literal("thinking"), text: z.string() }).strict(),
87
- z
88
- .object({
89
- id: z.string(),
90
- kind: z.literal("steer_message"),
91
- steerId: z.string(),
92
- text: z.string(),
93
- status: z.enum(["pending", "consumed", "failed"]),
94
- })
95
- .strict(),
96
- z.object({ id: z.string(), kind: z.literal("steer_applied"), steerId: z.string() }).strict(),
97
- z
98
- .object({
99
- id: z.string(),
100
- kind: z.literal("tool"),
101
- callId: z.string(),
102
- name: z.string(),
103
- args: z.unknown().optional(),
104
- status: z.enum(["running", "awaiting_approval", "awaiting_client", "completed", "failed", "rejected"]),
105
- result: z.unknown().optional(),
106
- isError: z.boolean().optional(),
107
- approval: z.unknown().optional(),
108
- frontendMode: z.enum(["client", "client_view", "client_interaction"]).optional(),
109
- presentation: z.unknown().optional(),
110
- })
111
- .strict(),
112
- z
113
- .object({
114
- id: z.string(),
115
- kind: z.literal("compaction"),
116
- status: z.enum(["running", "completed", "skipped", "failed"]),
117
- result: z.unknown().optional(),
118
- })
119
- .strict(),
120
- ]);
121
-
122
- const AiConversationSchema = z
123
- .object({
124
- id: AiResourceIdSchema,
125
- shortId: AiResourceIdSchema,
126
- title: z.string(),
127
- titleSource: z.enum(["default", "auto", "user"]),
128
- description: z.string(),
129
- descriptionSource: z.enum(["default", "auto", "user"]),
130
- keywords: z.array(z.string()),
131
- pinnedAt: z.string().nullable(),
132
- archivedAt: z.string().nullable(),
133
- done: z.boolean().nullable(),
134
- isDone: z.boolean(),
135
- lastUsedAt: z.string(),
136
- runStatus: z.enum(["idle", "queued", "running", "needs_attention", "waiting_for_browser", "failed"]),
137
- runError: z.string().nullable(),
138
- unreadCompletion: z.boolean(),
139
- projectId: z.string().nullable(),
140
- draft: z.object({ content: z.array(z.unknown()), revision: z.number().int(), updatedAt: z.string().nullable() }).strict(),
141
- createdByUserId: z.string().nullable(),
142
- createdAt: z.string(),
143
- updatedAt: z.string(),
144
- })
145
- .strict();
146
-
147
- const AiStoredMessageSchema = z
148
- .object({
149
- id: AiResourceIdSchema,
150
- shortId: AiResourceIdSchema,
151
- conversationId: AiResourceIdSchema,
152
- seq: z.number().int().nonnegative(),
153
- kind: z.enum(["message", "summary"]),
154
- message: z.unknown(),
155
- loopId: AiResourceIdSchema.nullable(),
156
- modelProfileId: z.string().nullable(),
157
- providerModel: z.string().nullable(),
158
- usage: z.unknown().nullable(),
159
- stopReason: z.string().nullable(),
160
- loopAggregate: z.unknown().nullable(),
161
- loopDoneReason: z.string().nullable(),
162
- compactedAt: z.string().nullable(),
163
- meta: z.unknown().nullable(),
164
- createdAt: z.string(),
165
- })
166
- .strict();
167
-
168
- const AiTurnSnapshotSchema = z
169
- .object({
170
- turnId: AiResourceIdSchema,
171
- attempt: z.number().int().nonnegative(),
172
- status: z.enum(["queued", "running", "waiting_for_action", "completed", "failed", "aborted"]),
173
- seq: z.number().int().nonnegative(),
174
- blocks: z.array(AiTurnBlockSchema),
175
- modelProfileId: z.string().nullable(),
176
- createdAt: z.string(),
177
- })
178
- .strict();
179
-
180
- const AiWireEventBaseSchema = {
181
- v: z.literal(1),
182
- conversationId: AiResourceIdSchema,
183
- turnId: AiResourceIdSchema,
184
- attempt: z.number().int().nonnegative(),
185
- seq: z.number().int().nonnegative(),
186
- } as const;
187
-
188
- const AiStreamEventWireSchema = z.union([
189
- z
190
- .object({
191
- type: z.literal("state"),
192
- conversation: AiConversationSchema,
193
- messages: z.array(AiStoredMessageSchema),
194
- hasMoreMessages: z.boolean().optional(),
195
- activeTurn: AiTurnSnapshotSchema.nullable(),
196
- })
197
- .strict(),
198
- z
199
- .object({
200
- ...AiWireEventBaseSchema,
201
- type: z.literal("turn_started"),
202
- modelProfileId: z.string(),
203
- providerModel: z.string(),
204
- blocks: z.array(AiTurnBlockSchema).optional(),
205
- })
206
- .strict(),
207
- z.object({ ...AiWireEventBaseSchema, type: z.literal("block_set"), block: AiTurnBlockSchema }).strict(),
208
- z
209
- .object({
210
- ...AiWireEventBaseSchema,
211
- type: z.literal("block_delta"),
212
- blockId: z.string(),
213
- blockKind: z.enum(["text", "thinking"]),
214
- delta: z.string(),
215
- })
216
- .strict(),
217
- z
218
- .object({
219
- ...AiWireEventBaseSchema,
220
- type: z.literal("turn_finished"),
221
- status: z.enum(["completed", "failed", "aborted"]),
222
- error: z.string().nullable(),
223
- messages: z.array(AiStoredMessageSchema).optional(),
224
- })
225
- .strict(),
226
- ]);
227
-
228
- export const AiStreamEventSchema = z.custom<AiStreamEvent>((value) => AiStreamEventWireSchema.safeParse(value).success);
229
-
230
- export const AiLiveServerMessageSchema = z.discriminatedUnion("type", [
231
- z
232
- .object({
233
- type: z.literal(AI_LIVE_WS_TYPE.ready),
234
- payload: z.object({ cursor: AiLiveCursorSchema, recovered: z.boolean() }).strict(),
235
- })
236
- .strict(),
237
- z
238
- .object({
239
- type: z.literal(AI_LIVE_WS_TYPE.event),
240
- payload: z.object({ cursor: AiLiveCursorSchema, event: AiInvalidationSchema }).strict(),
241
- })
242
- .strict(),
243
- z
244
- .object({
245
- type: z.literal(AI_LIVE_WS_TYPE.scopeChanged),
246
- payload: z.object({ at: z.string().datetime() }).strict(),
247
- })
248
- .strict(),
249
- z
250
- .object({
251
- type: z.literal(AI_LIVE_WS_TYPE.revoked),
252
- payload: z.object({ code: AiLiveRevocationCodeSchema, message: z.string().min(1).max(500) }).strict(),
253
- })
254
- .strict(),
255
- z
256
- .object({
257
- type: z.literal(AI_LIVE_WS_TYPE.error),
258
- payload: z.object({ code: AiLiveErrorCodeSchema, message: z.string().min(1).max(500) }).strict(),
259
- })
260
- .strict(),
261
- z
262
- .object({
263
- type: z.literal(AI_LIVE_WS_TYPE.turnEvent),
264
- payload: z.object({ conversationId: AiResourceIdSchema, event: AiStreamEventSchema }).strict(),
265
- })
266
- .strict(),
267
- z
268
- .object({
269
- type: z.literal(AI_LIVE_WS_TYPE.turnError),
270
- payload: z.object({ conversationId: AiResourceIdSchema, code: AiTurnErrorCodeSchema, message: z.string().min(1).max(500) }).strict(),
271
- })
272
- .strict(),
273
- ]);
274
-
275
- export type AiLiveServerMessage = z.infer<typeof AiLiveServerMessageSchema>;
276
-
277
- export const aiTurnEventMessage = (conversationId: string, event: AiStreamEvent): AiLiveServerMessage => ({
278
- type: AI_LIVE_WS_TYPE.turnEvent,
279
- payload: { conversationId, event },
280
- });
281
-
282
- export const parseAiLiveServerMessage = (raw: string): AiLiveServerMessage | null => {
283
- try {
284
- const parsed = AiLiveServerMessageSchema.safeParse(JSON.parse(raw));
285
- if (!parsed.success) return null;
286
- if (parsed.data.type !== AI_LIVE_WS_TYPE.turnEvent) return parsed.data;
287
- const eventConversationId =
288
- parsed.data.payload.event.type === "state" ? parsed.data.payload.event.conversation.id : parsed.data.payload.event.conversationId;
289
- return eventConversationId === parsed.data.payload.conversationId ? parsed.data : null;
290
- } catch {
291
- return null;
292
- }
293
- };
package/src/ai/live.ts CHANGED
@@ -1,2 +1,31 @@
1
- export { latestAiInvalidationCursor } from "./live-outbox";
2
- export { type AiLiveRoutes, type AiLiveRoutesConfig, aiLiveRoutes } from "./live-routes";
1
+ import { z } from "zod";
2
+ import { defineLive, type LiveViewer } from "../events/live";
3
+ import { AiInvalidationSchema } from "./live-events";
4
+
5
+ /**
6
+ * Live updates of a user's AI data. Core publishes and serves them at
7
+ * `/api/ai/live`; the AI's database triggers write them, keyed `u:<user ID>`,
8
+ * in the transaction of every change. Other applications, such as Assistant,
9
+ * read `cursor()` before their snapshot.
10
+ */
11
+ export const aiLive = defineLive({ appId: "core", event: AiInvalidationSchema });
12
+
13
+ /** The user an AI request acts for: the user, or the user a credential is delegated by. */
14
+ const ownerOf = (viewer: LiveViewer): string | null => (viewer.accessSubject.type === "user" ? viewer.accessSubject.userId : null);
15
+
16
+ /**
17
+ * `user` follows the viewer's own AI data: conversations, their files, tasks and
18
+ * dictations, and the Projects the viewer can read. Its single key admits only
19
+ * that user; who receives a Project change was decided when it was written.
20
+ */
21
+ export const aiLiveChannels = {
22
+ user: {
23
+ scope: z.object({}).strict(),
24
+ keys: async (_scope: unknown, viewer: LiveViewer) => {
25
+ const owner = ownerOf(viewer);
26
+ return owner ? [`u:${owner}`] : null;
27
+ },
28
+ authorize: async (key: string, viewers: readonly LiveViewer[]) =>
29
+ new Set(viewers.filter((viewer) => `u:${ownerOf(viewer)}` === key).map((viewer) => viewer.id)),
30
+ },
31
+ };
package/src/ai/migrate.ts CHANGED
@@ -13,6 +13,14 @@ const backfillAiShortIds = async (
13
13
  };
14
14
 
15
15
  export const migrateCloudAi = async (): Promise<void> => {
16
+ // AI writes its live updates to Core's platform outbox, in the transaction of every
17
+ // AI write: without events.enqueue(), every one of them would fail.
18
+ const [outbox] = await sql<{ ready: boolean }[]>`
19
+ SELECT to_regprocedure('events.enqueue(uuid,text,text,text,jsonb,text)') IS NOT NULL AS ready
20
+ `;
21
+ if (!outbox?.ready) {
22
+ throw new Error("AI live updates write to events.outbox, which does not exist. Run Core's events migration before the AI migration.");
23
+ }
16
24
  await sql`CREATE SCHEMA IF NOT EXISTS ai`.simple();
17
25
  console.log(" ✓ ai schema");
18
26
 
@@ -1722,10 +1730,12 @@ export const migrateCloudAi = async (): Promise<void> => {
1722
1730
  WHERE project_id IS NOT NULL AND archived_at IS NULL
1723
1731
  `.simple();
1724
1732
 
1725
- // Durable Realtime UI invalidations. Triggers live at the persistence seam so
1726
- // route, worker, tool, enrichment, and scheduler writes cannot bypass them.
1727
- // The outbox row commits or rolls back with the authoritative domain write;
1728
- // publishing remains an after-commit runtime responsibility.
1733
+ // Live updates. Triggers live at the persistence seam so route, worker, tool,
1734
+ // enrichment, and scheduler writes cannot bypass them. They write to Core's
1735
+ // platform outbox in the transaction of the domain write (see the start of this migration).
1736
+
1737
+ // The previous outbox. Nothing writes it anymore; an older Core that still runs during
1738
+ // an update finds it empty. The cleanup release drops it with the topic cloud-ai-invalidations.
1729
1739
  await sql`
1730
1740
  CREATE TABLE IF NOT EXISTS ai.live_invalidation_outbox (
1731
1741
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -1786,6 +1796,10 @@ export const migrateCloudAi = async (): Promise<void> => {
1786
1796
  DROP FUNCTION IF EXISTS ai.enqueue_live_for_user(UUID, TEXT, UUID, TEXT, TEXT, TEXT[])
1787
1797
  `.simple();
1788
1798
 
1799
+ // Every AI live update goes through this function: one update for one user,
1800
+ // keyed `u:<user>`, on Core's live topic. Its data is an `AiInvalidation`.
1801
+ // No coalesce key: the last payload would win and drop the domains of earlier ones.
1802
+ // Gateway health reports a body without events.enqueue: an older Core restored its own.
1789
1803
  await sql`
1790
1804
  CREATE OR REPLACE FUNCTION ai.enqueue_live_for_user(
1791
1805
  p_change_id UUID,
@@ -1796,11 +1810,23 @@ export const migrateCloudAi = async (): Promise<void> => {
1796
1810
  ) RETURNS void AS $$
1797
1811
  BEGIN
1798
1812
  IF p_user_id IS NULL OR cardinality(p_domains) = 0 THEN RETURN; END IF;
1799
- INSERT INTO ai.live_invalidation_outbox (
1800
- change_id, audience_user_id, conversation_short_id, project_short_id, domains
1801
- ) VALUES (
1802
- p_change_id, p_user_id, p_conversation_short_id, p_project_short_id,
1803
- ARRAY(SELECT DISTINCT domain FROM unnest(p_domains) domain ORDER BY domain)
1813
+ PERFORM events.enqueue(
1814
+ gen_random_uuid(),
1815
+ 'core',
1816
+ 'live',
1817
+ 'u:' || p_user_id::text,
1818
+ jsonb_build_object(
1819
+ 'v', 1,
1820
+ 'k', 'u:' || p_user_id::text,
1821
+ 'd', jsonb_build_object(
1822
+ 'type', 'ai.invalidated',
1823
+ 'changeId', p_change_id,
1824
+ 'conversationId', p_conversation_short_id,
1825
+ 'projectId', p_project_short_id,
1826
+ 'domains', to_jsonb(ARRAY(SELECT DISTINCT domain FROM unnest(p_domains) domain ORDER BY domain)),
1827
+ 'at', to_char(now() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')
1828
+ )
1829
+ )
1804
1830
  );
1805
1831
  END
1806
1832
  $$ LANGUAGE plpgsql
@@ -2213,13 +2239,6 @@ export const migrateCloudAi = async (): Promise<void> => {
2213
2239
  )`.simple();
2214
2240
  await sql`CREATE INDEX IF NOT EXISTS ai_dictations_pending ON ai.dictations(conversation_id, created_at, id) WHERE disposition = 'pending'`.simple();
2215
2241
  await sql`CREATE INDEX IF NOT EXISTS ai_dictations_recovery ON ai.dictations(status, next_attempt_at, lease_until) WHERE status IN ('queued', 'running')`.simple();
2216
- await sql.begin(async (tx) => {
2217
- await tx`ALTER TABLE ai.live_invalidation_outbox DROP CONSTRAINT IF EXISTS ai_live_invalidation_domains_check`;
2218
- await tx`ALTER TABLE ai.live_invalidation_outbox ADD CONSTRAINT ai_live_invalidation_domains_check CHECK (
2219
- domains <@ ARRAY['conversation-list', 'conversation-detail', 'conversation-sources', 'conversation-files', 'conversation-tasks',
2220
- 'conversation-dictations', 'project-list', 'project-detail', 'project-context']::text[] AND cardinality(domains) > 0
2221
- )`;
2222
- });
2223
2242
  await sql`DROP TRIGGER IF EXISTS ai_live_dictations_changed ON ai.dictations`.simple();
2224
2243
  await sql`DROP TRIGGER IF EXISTS ai_live_dictations_state_changed ON ai.dictations`.simple();
2225
2244
  await sql`CREATE TRIGGER ai_live_dictations_changed AFTER INSERT OR DELETE ON ai.dictations
package/src/ai/runtime.ts CHANGED
@@ -12,7 +12,6 @@ import { aiChatAccessSubject, selectAssistantAiModelId } from "./assistant-model
12
12
  import { startAiDictationRuntime } from "./dictation-runtime";
13
13
  import { AiTurnExecutor } from "./executor";
14
14
  import { canonicalizeAiConversationAttachments, snapshotAiConversationFiles } from "./file-context";
15
- import { startAiInvalidationRuntime, stopAiInvalidationRuntime } from "./live-outbox";
16
15
  import { drainQueuedMessages } from "./message-queue";
17
16
  import { aiQuotas } from "./quotas";
18
17
  import { parseAiResourceMarker } from "./resource-markers";
@@ -577,7 +576,6 @@ export const startAiRuntime = (
577
576
  },
578
577
  };
579
578
  running = state;
580
- startAiInvalidationRuntime();
581
579
  log.info("AI runtime started", { workerId: AI_WORKER_ID, concurrency });
582
580
  return releaseAiRuntime(listener);
583
581
  };
@@ -591,10 +589,7 @@ const releaseAiRuntime = (listener: ((event: AiTurnFinalizedEvent) => Promise<vo
591
589
  if (!state) return;
592
590
  if (listener) state.listeners.delete(listener);
593
591
  refCount = Math.max(0, refCount - 1);
594
- if (refCount === 0) {
595
- state.stop();
596
- void stopAiInvalidationRuntime();
597
- }
592
+ if (refCount === 0) state.stop();
598
593
  };
599
594
  };
600
595
 
package/src/ai/solid.ts CHANGED
@@ -7,11 +7,6 @@ export {
7
7
  createAiChatController,
8
8
  } from "./client/controller";
9
9
  export { conversationFileSource } from "./client/file-source";
10
- export {
11
- type AiLiveConnection,
12
- type CreateAiLiveConnectionOptions,
13
- createAiLiveConnection,
14
- } from "./client/live-connection";
15
10
  export {
16
11
  type AiActiveTurn,
17
12
  type AiChatProjection,
package/src/api/help.ts CHANGED
@@ -8,7 +8,7 @@ import { helpApiMessages } from "./help-messages";
8
8
  export type HelpRouteDependencies = {
9
9
  help?: HelpReaderFactory;
10
10
  authenticate?: MiddlewareHandler<AuthContext>;
11
- renderMarkdown?: (markdown: string) => string;
11
+ renderMarkdown?: (markdown: string, locale: string) => string;
12
12
  };
13
13
 
14
14
  export const createHelpRoutes = (dependencies: HelpRouteDependencies = {}) => {
@@ -34,7 +34,7 @@ export const createHelpRoutes = (dependencies: HelpRouteDependencies = {}) => {
34
34
  id: document.documentId,
35
35
  title: document.title,
36
36
  markdown: document.markdown,
37
- html: (dependencies.renderMarkdown ?? renderHelpMarkdown)(document.markdown),
37
+ html: (dependencies.renderMarkdown ?? renderHelpMarkdown)(document.markdown, document.locale),
38
38
  };
39
39
  return c.json(payload);
40
40
  });
@@ -2,8 +2,8 @@ import { type Context, Hono } from "hono";
2
2
  import { deleteCookie, getCookie, setCookie } from "hono/cookie";
3
3
  import { env } from "../config/env";
4
4
  import { PWA_AUTH_PATH, PWA_COOKIES, PWA_LIMITS, PWA_SCOPE, PwaClaimSchema, type PwaLaunchState, PwaRenameSchema } from "../contracts/pwa";
5
- import { type AuthContext, auth, rateLimit, v } from "../server";
6
- import { type PwaCredentials, PwaError, pwaDevices } from "../services/pwa-devices";
5
+ import { type AuthContext, auth, getLocale, rateLimit, v } from "../server";
6
+ import { type PwaCredentials, type PwaDeviceNotifier, PwaError, pwaDevices } from "../services/pwa-devices";
7
7
  import { defaultShellAvailable, handlePwaError, type PwaRouteOptions, pwaErrorResponse, pwaInvalidRequest, pwaTransport } from "./pwa";
8
8
 
9
9
  const PAIRING_COOKIE_PATH = `${PWA_AUTH_PATH}/pairings`;
@@ -58,12 +58,17 @@ export const launchTarget = (to: string | undefined): string => {
58
58
 
59
59
  const stateUrl = (state: PwaLaunchState) => `${PWA_SCOPE}?pwa=${state}`;
60
60
 
61
+ export type PwaPhoneRouteOptions = PwaRouteOptions & {
62
+ /** Sends the "new phone paired" notice; completion awaits it before the phone gets its credentials. */
63
+ notify: PwaDeviceNotifier;
64
+ };
65
+
61
66
  /**
62
67
  * Phone side of the mobile app (preview), mounted by Core at `/pwa/_auth` before its page
63
68
  * catch-all. The device key and the completion secret use cookie paths below `/pwa/_auth`,
64
69
  * which only Core serves; other applications see only the 24-hour app session.
65
70
  */
66
- export const createPwaPhoneRoutes = (options: PwaRouteOptions = {}) => {
71
+ export const createPwaPhoneRoutes = (options: PwaPhoneRouteOptions) => {
67
72
  const service = options.service ?? pwaDevices;
68
73
  const shellAvailable = options.shellAvailable ?? defaultShellAvailable;
69
74
  const limit = rateLimit({ keyBy: "ip" });
@@ -102,6 +107,8 @@ export const createPwaPhoneRoutes = (options: PwaRouteOptions = {}) => {
102
107
  deviceKey: getCookie(c, PWA_COOKIES.device),
103
108
  appSession: await appSession(c),
104
109
  webUserId: web?.user.id ?? null,
110
+ locale: getLocale(c),
111
+ notify: options.notify,
105
112
  });
106
113
  if (result.state === "waiting") return c.json(result, 202);
107
114
  if (result.credentials) setAppCredentials(c, result.credentials);
@@ -85,6 +85,40 @@ export const SearchResponseSchema = z.object({
85
85
  failedApps: z.array(z.string()).optional(),
86
86
  });
87
87
 
88
+ /** Media type a client sends in `Accept` to receive one NDJSON line per app as soon as it finishes. */
89
+ export const SEARCH_STREAM_CONTENT_TYPE = "application/x-ndjson";
90
+
91
+ export const SearchProviderStatusSchema = z.enum(["ok", "empty", "timeout", "error"]);
92
+
93
+ /**
94
+ * One line of a streamed search: `start` names the apps that search, one `provider` line follows per app in the
95
+ * order the apps finish, and `done` closes the search. Apps without a line before `done` do not exist: every app
96
+ * named in `start` gets exactly one line.
97
+ */
98
+ export const SearchStreamLineSchema = z.discriminatedUnion("type", [
99
+ z.object({
100
+ type: z.literal("start"),
101
+ query: z.string(),
102
+ apps: z.array(SearchAppSchema),
103
+ providers: z.array(z.string()),
104
+ unsupportedTags: z.array(z.string()).optional(),
105
+ }),
106
+ z.object({
107
+ type: z.literal("provider"),
108
+ provider: z.string(),
109
+ status: SearchProviderStatusSchema,
110
+ results: z.array(SearchItemSchema),
111
+ ms: z.number().int().nonnegative(),
112
+ }),
113
+ z.object({
114
+ type: z.literal("done"),
115
+ status: z.enum(["complete", "partial"]),
116
+ count: z.number().int().nonnegative(),
117
+ }),
118
+ ]);
119
+
88
120
  export type SearchApp = z.infer<typeof SearchAppSchema>;
89
121
  export type SearchItem = z.infer<typeof SearchItemSchema>;
90
122
  export type SearchResponse = z.infer<typeof SearchResponseSchema>;
123
+ export type SearchProviderStatus = z.infer<typeof SearchProviderStatusSchema>;
124
+ export type SearchStreamLine = z.infer<typeof SearchStreamLineSchema>;