@parlor/client 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +20 -2
  3. package/dist/action-client.d.ts +40 -0
  4. package/dist/action-client.d.ts.map +1 -0
  5. package/dist/action-client.js +83 -0
  6. package/dist/action-client.js.map +1 -0
  7. package/dist/index.d.ts +8 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +6 -0
  10. package/dist/index.js.map +1 -0
  11. package/dist/plugin-client.d.ts +11 -0
  12. package/dist/plugin-client.d.ts.map +1 -0
  13. package/dist/plugin-client.js +14 -0
  14. package/dist/plugin-client.js.map +1 -0
  15. package/dist/query-keys.d.ts +3 -0
  16. package/dist/query-keys.d.ts.map +1 -0
  17. package/dist/query-keys.js +5 -0
  18. package/dist/query-keys.js.map +1 -0
  19. package/dist/react/agent-ui/activity-state.d.ts +20 -0
  20. package/dist/react/agent-ui/activity-state.d.ts.map +1 -0
  21. package/dist/react/agent-ui/activity-state.js +14 -0
  22. package/dist/react/agent-ui/activity-state.js.map +1 -0
  23. package/dist/react/agent-ui/chat-activity.d.ts +13 -0
  24. package/dist/react/agent-ui/chat-activity.d.ts.map +1 -0
  25. package/dist/react/agent-ui/chat-activity.js +26 -0
  26. package/dist/react/agent-ui/chat-activity.js.map +1 -0
  27. package/dist/react/agent-ui/chat-confirm.d.ts +28 -0
  28. package/dist/react/agent-ui/chat-confirm.d.ts.map +1 -0
  29. package/dist/react/agent-ui/chat-confirm.js +140 -0
  30. package/dist/react/agent-ui/chat-confirm.js.map +1 -0
  31. package/dist/react/agent-ui/chat.d.ts +7 -0
  32. package/dist/react/agent-ui/chat.d.ts.map +1 -0
  33. package/dist/react/agent-ui/chat.js +7 -0
  34. package/dist/react/agent-ui/chat.js.map +1 -0
  35. package/dist/react/agent-ui/dev.d.ts +12 -0
  36. package/dist/react/agent-ui/dev.d.ts.map +1 -0
  37. package/dist/react/agent-ui/dev.js +80 -0
  38. package/dist/react/agent-ui/dev.js.map +1 -0
  39. package/dist/react/agent-ui/runtime.d.ts +12 -0
  40. package/dist/react/agent-ui/runtime.d.ts.map +1 -0
  41. package/dist/react/agent-ui/runtime.js +50 -0
  42. package/dist/react/agent-ui/runtime.js.map +1 -0
  43. package/dist/react/agent-ui/use-agent-activity.d.ts +3 -0
  44. package/dist/react/agent-ui/use-agent-activity.d.ts.map +1 -0
  45. package/dist/react/agent-ui/use-agent-activity.js +7 -0
  46. package/dist/react/agent-ui/use-agent-activity.js.map +1 -0
  47. package/dist/react/agent-ui/use-agent-confirmations.d.ts +8 -0
  48. package/dist/react/agent-ui/use-agent-confirmations.d.ts.map +1 -0
  49. package/dist/react/agent-ui/use-agent-confirmations.js +12 -0
  50. package/dist/react/agent-ui/use-agent-confirmations.js.map +1 -0
  51. package/dist/react/agent-ui/use-agent-view.d.ts +11 -0
  52. package/dist/react/agent-ui/use-agent-view.d.ts.map +1 -0
  53. package/dist/react/agent-ui/use-agent-view.js +25 -0
  54. package/dist/react/agent-ui/use-agent-view.js.map +1 -0
  55. package/dist/react/chat/ask-fields.d.ts +13 -0
  56. package/dist/react/chat/ask-fields.d.ts.map +1 -0
  57. package/dist/react/chat/ask-fields.js +46 -0
  58. package/dist/react/chat/ask-fields.js.map +1 -0
  59. package/dist/react/chat/chat-ask.d.ts +32 -0
  60. package/dist/react/chat/chat-ask.d.ts.map +1 -0
  61. package/dist/react/chat/chat-ask.js +69 -0
  62. package/dist/react/chat/chat-ask.js.map +1 -0
  63. package/dist/react/chat/chat-composer.d.ts +18 -0
  64. package/dist/react/chat/chat-composer.d.ts.map +1 -0
  65. package/dist/react/chat/chat-composer.js +81 -0
  66. package/dist/react/chat/chat-composer.js.map +1 -0
  67. package/dist/react/chat/chat-root.d.ts +14 -0
  68. package/dist/react/chat/chat-root.d.ts.map +1 -0
  69. package/dist/react/chat/chat-root.js +21 -0
  70. package/dist/react/chat/chat-root.js.map +1 -0
  71. package/dist/react/chat/chat-thread.d.ts +18 -0
  72. package/dist/react/chat/chat-thread.d.ts.map +1 -0
  73. package/dist/react/chat/chat-thread.js +69 -0
  74. package/dist/react/chat/chat-thread.js.map +1 -0
  75. package/dist/react/chat/context.d.ts +6 -0
  76. package/dist/react/chat/context.d.ts.map +1 -0
  77. package/dist/react/chat/context.js +24 -0
  78. package/dist/react/chat/context.js.map +1 -0
  79. package/dist/react/chat/create.d.ts +15 -0
  80. package/dist/react/chat/create.d.ts.map +1 -0
  81. package/dist/react/chat/create.js +23 -0
  82. package/dist/react/chat/create.js.map +1 -0
  83. package/dist/react/chat/open-turns.d.ts +10 -0
  84. package/dist/react/chat/open-turns.d.ts.map +1 -0
  85. package/dist/react/chat/open-turns.js +60 -0
  86. package/dist/react/chat/open-turns.js.map +1 -0
  87. package/dist/react/chat/runtime.d.ts +156 -0
  88. package/dist/react/chat/runtime.d.ts.map +1 -0
  89. package/dist/react/chat/runtime.js +928 -0
  90. package/dist/react/chat/runtime.js.map +1 -0
  91. package/dist/react/chat/use-chat.d.ts +4 -0
  92. package/dist/react/chat/use-chat.d.ts.map +1 -0
  93. package/dist/react/chat/use-chat.js +22 -0
  94. package/dist/react/chat/use-chat.js.map +1 -0
  95. package/dist/react/chat/use-device-setup.d.ts +9 -0
  96. package/dist/react/chat/use-device-setup.d.ts.map +1 -0
  97. package/dist/react/chat/use-device-setup.js +17 -0
  98. package/dist/react/chat/use-device-setup.js.map +1 -0
  99. package/dist/react/hooks.d.ts +11 -0
  100. package/dist/react/hooks.d.ts.map +1 -0
  101. package/dist/react/hooks.js +32 -0
  102. package/dist/react/hooks.js.map +1 -0
  103. package/dist/react/index.d.ts +20 -0
  104. package/dist/react/index.d.ts.map +1 -0
  105. package/dist/react/index.js +13 -0
  106. package/dist/react/index.js.map +1 -0
  107. package/dist/react/provider.d.ts +38 -0
  108. package/dist/react/provider.d.ts.map +1 -0
  109. package/dist/react/provider.js +126 -0
  110. package/dist/react/provider.js.map +1 -0
  111. package/dist/react/retry.d.ts +5 -0
  112. package/dist/react/retry.d.ts.map +1 -0
  113. package/dist/react/retry.js +22 -0
  114. package/dist/react/retry.js.map +1 -0
  115. package/dist/react/sync-bridge.d.ts +25 -0
  116. package/dist/react/sync-bridge.d.ts.map +1 -0
  117. package/dist/react/sync-bridge.js +65 -0
  118. package/dist/react/sync-bridge.js.map +1 -0
  119. package/dist/react/sync.d.ts +12 -0
  120. package/dist/react/sync.d.ts.map +1 -0
  121. package/dist/react/sync.js +36 -0
  122. package/dist/react/sync.js.map +1 -0
  123. package/dist/react/use-capability.d.ts +6 -0
  124. package/dist/react/use-capability.d.ts.map +1 -0
  125. package/dist/react/use-capability.js +21 -0
  126. package/dist/react/use-capability.js.map +1 -0
  127. package/dist/react/use-devices.d.ts +34 -0
  128. package/dist/react/use-devices.d.ts.map +1 -0
  129. package/dist/react/use-devices.js +93 -0
  130. package/dist/react/use-devices.js.map +1 -0
  131. package/dist/react/use-link-device.d.ts +42 -0
  132. package/dist/react/use-link-device.d.ts.map +1 -0
  133. package/dist/react/use-link-device.js +58 -0
  134. package/dist/react/use-link-device.js.map +1 -0
  135. package/dist/register.d.ts +7 -0
  136. package/dist/register.d.ts.map +1 -0
  137. package/dist/register.js +2 -0
  138. package/dist/register.js.map +1 -0
  139. package/dist/session.d.ts +121 -0
  140. package/dist/session.d.ts.map +1 -0
  141. package/dist/session.js +5 -0
  142. package/dist/session.js.map +1 -0
  143. package/package.json +62 -3
@@ -0,0 +1,928 @@
1
+ import { CHAT_CHANNEL, CHAT_TAG, foldThread, HOSTS_TAG, isPromptTooLong, parseChatFrame, PROMPT_LIMIT_MESSAGE, } from "@parlor/chat";
2
+ import { ActionError, asId, tagsOverlap, } from "@parlor/core";
3
+ import { generateClientId } from "../../action-client.js";
4
+ import { applyFrame, mergeSnapshot, NO_OPEN_TURNS, pruneOpen, } from "./open-turns.js";
5
+ export const HOST_GRANTS_TAG = "parlor_auth_hosts";
6
+ export const DEVICES_TAG = "parlor_auth_devices";
7
+ const LINKED = { status: "linked" };
8
+ const CHECKING = { status: "checking" };
9
+ const READ_RETRY_BASE_MS = 1_000;
10
+ const READ_RETRY_MAX_MS = 30_000;
11
+ const ACTIVITY_PING_MS = 60_000;
12
+ const UNFIXABLE_BY_RETRY = new Set([
13
+ "not_found",
14
+ "unauthenticated",
15
+ "forbidden",
16
+ ]);
17
+ const retryable = (error) => !(error instanceof ActionError && UNFIXABLE_BY_RETRY.has(error.code));
18
+ const mint = (kind) => asId(kind, generateClientId());
19
+ const isActive = (turn) => turn.state === "queued" || turn.state === "running";
20
+ const exceedsPromptLimit = (error) => error.issues?.some((issue) => issue.code === "too_big" && issue.path.join(".") === "prompt.text") ?? false;
21
+ class NeedsDevice extends Error {
22
+ name = "NeedsDevice";
23
+ }
24
+ /** Prompts are signed with presence this long before the computer's deadline, for clock skew. */
25
+ const PRESENCE_MARGIN_MS = 10 * 60_000;
26
+ const LONGEST_TIMEOUT_MS = 2 ** 31 - 1;
27
+ const readable = (error) => {
28
+ if (error instanceof NeedsDevice)
29
+ return error.message;
30
+ if (error instanceof Error && error.name === "NotAllowedError") {
31
+ return "Face ID or Touch ID was cancelled.";
32
+ }
33
+ if (!(error instanceof ActionError))
34
+ return "Could not reach the server.";
35
+ if (error.issues === undefined)
36
+ return error.message;
37
+ return exceedsPromptLimit(error)
38
+ ? PROMPT_LIMIT_MESSAGE
39
+ : "That message could not be sent. Try again.";
40
+ };
41
+ const bySeq = (a, b) => a.seq - b.seq;
42
+ export function createChatRuntime(deps) {
43
+ const { calls, agentUi } = deps;
44
+ const listeners = new Set();
45
+ let threadId = deps.threadId === undefined ? undefined : asId("thread", deps.threadId);
46
+ let loaded = "no";
47
+ let thread;
48
+ let threads = [];
49
+ let olderThreads = [];
50
+ let moreThreads = false;
51
+ let hasOlder = false;
52
+ let loadingOlder = false;
53
+ let hosts = [];
54
+ let items = new Map();
55
+ let turns = [];
56
+ let open = NO_OPEN_TURNS;
57
+ let beforeLoad = [];
58
+ let draft = "";
59
+ let failure;
60
+ let chosen;
61
+ let sending = false;
62
+ let pendingSend;
63
+ let startedNew = false;
64
+ let connect = {
65
+ kind: "command",
66
+ command: `npx @parlor/agent-host connect ${deps.appOrigin}`,
67
+ };
68
+ let stopWatching;
69
+ let watched;
70
+ let running = false;
71
+ let channelOpen = false;
72
+ let staleAfterLoad = false;
73
+ let dirty = false;
74
+ const staleReads = new Set();
75
+ let unfixable = false;
76
+ let retryTimer;
77
+ let retryDelay = READ_RETRY_BASE_MS;
78
+ let lastPing;
79
+ let pingRefused = false;
80
+ let generation = 0;
81
+ let device;
82
+ let linkInfo;
83
+ let linking;
84
+ let linkBusy = false;
85
+ let linkError;
86
+ let linkExpired = false;
87
+ let linkTimer;
88
+ let presenceTimer;
89
+ let setup;
90
+ let afterSetup;
91
+ let explainers = 0;
92
+ const resends = new Map();
93
+ const answerIds = new Map();
94
+ const stopIds = new Map();
95
+ const refusedLast = (hostId, deviceId) => {
96
+ const last = [...turns].reverse().find((turn) => turn.agent.hostId === hostId);
97
+ if (last?.failure !== "device_rejected")
98
+ return false;
99
+ return [...items.values()].some((item) => item.turnId === last.turnId &&
100
+ item.kind === "prompt" &&
101
+ item.body.signed?.deviceId === deviceId);
102
+ };
103
+ /**
104
+ * When the computer behind `agentKey` wants this device's next prompt signed with presence:
105
+ * by its report, or now if it refused this device's last prompt, whatever the clocks say.
106
+ */
107
+ const presenceDueAt = (agentKey) => {
108
+ const deviceId = device?.id ?? linkInfo?.deviceId;
109
+ if (deviceId === undefined)
110
+ return undefined;
111
+ const hostId = agentKey.split("/")[0];
112
+ const due = hosts.find((host) => host.id === hostId)?.presenceDue?.[deviceId];
113
+ if (due === undefined)
114
+ return undefined;
115
+ return refusedLast(hostId, deviceId) ? 0 : due - PRESENCE_MARGIN_MS;
116
+ };
117
+ const choices = () => hosts.flatMap((host) => host.agents.map((agent) => ({
118
+ key: `${host.id}/${agent.id}`,
119
+ name: agent.name,
120
+ hostName: host.name,
121
+ online: host.online,
122
+ ready: agent.ready,
123
+ problem: agent.problem,
124
+ })));
125
+ const waitOn = (reason, hostId) => ({
126
+ reason,
127
+ hostId,
128
+ hostName: hosts.find((host) => host.id === hostId)?.name ?? "your computer",
129
+ });
130
+ const linkOf = () => {
131
+ if (!deps.auth?.linkState)
132
+ return LINKED;
133
+ const reporting = hosts.filter((host) => host.online && host.devices !== undefined);
134
+ if (reporting.length === 0)
135
+ return LINKED;
136
+ if (!linkInfo)
137
+ return CHECKING;
138
+ const { deviceId } = linkInfo;
139
+ if (linkInfo.pairing ||
140
+ linkInfo.vouched ||
141
+ (deviceId !== undefined &&
142
+ reporting.some((host) => host.devices?.includes(deviceId)))) {
143
+ return LINKED;
144
+ }
145
+ const listing = hosts.filter((host) => host.devices !== undefined);
146
+ return {
147
+ status: "unlinked",
148
+ hostName: reporting[0].name,
149
+ code: linking?.code,
150
+ qr: linking?.qr,
151
+ command: linking && `npx @parlor/agent-host link ${deps.appOrigin} ${linking.code}`,
152
+ expiresAt: linking?.expiresAt,
153
+ expired: linkExpired,
154
+ onlyComputer: listing.every((host) => host.devices.every((id) => id === deviceId)),
155
+ busy: linkBusy,
156
+ error: linkError,
157
+ };
158
+ };
159
+ const waitOf = () => {
160
+ const approving = turns.find(awaitsApproval);
161
+ if (approving)
162
+ return waitOn("approval", approving.agent.hostId);
163
+ const offline = turns.find((turn) => turn.state === "queued" &&
164
+ hosts.some((host) => host.id === turn.agent.hostId && !host.online));
165
+ return offline && waitOn("offline", offline.agent.hostId);
166
+ };
167
+ const compute = () => {
168
+ const agents = choices();
169
+ const wanted = chosen ??
170
+ (thread?.agent ? `${thread.agent.hostId}/${thread.agent.agentId}` : undefined);
171
+ const agent = agents.find((choice) => choice.key === wanted) ??
172
+ agents.find((choice) => choice.online && choice.ready) ??
173
+ agents[0];
174
+ const wait = loaded === "yes" && !sending ? waitOf() : undefined;
175
+ const due = agent && presenceDueAt(agent.key);
176
+ const status = loaded === "failed"
177
+ ? "unavailable"
178
+ : loaded === "no"
179
+ ? "loading"
180
+ : sending
181
+ ? "sending"
182
+ : wait
183
+ ? "waiting"
184
+ : turns.some(isActive) || open.turns.size > 0
185
+ ? "running"
186
+ : "idle";
187
+ return {
188
+ status,
189
+ wait,
190
+ thread,
191
+ threads: [
192
+ ...threads,
193
+ ...olderThreads.filter((older) => !threads.some((shown) => shown.id === older.id)),
194
+ ],
195
+ moreThreads,
196
+ hasOlder,
197
+ loadingOlder,
198
+ items: foldThread({
199
+ items: [...items.values()].sort(bySeq),
200
+ open: [...open.turns.values()],
201
+ turns,
202
+ confirmations: agentUi.confirmations.get().filter((c) => c.threadId === threadId),
203
+ deviceId: device?.id,
204
+ }),
205
+ hosts,
206
+ agents,
207
+ agent,
208
+ connect,
209
+ draft,
210
+ error: failure,
211
+ setup,
212
+ link: linkOf(),
213
+ presenceDue: due !== undefined && Date.now() >= due,
214
+ };
215
+ };
216
+ /** Re-emits once the chosen computer's presence deadline arrives, so the hint shows unprompted. */
217
+ const watchPresence = () => {
218
+ clearTimeout(presenceTimer);
219
+ presenceTimer = undefined;
220
+ const due = running && state.agent ? presenceDueAt(state.agent.key) : undefined;
221
+ if (due === undefined || state.presenceDue)
222
+ return;
223
+ presenceTimer = setTimeout(emit, Math.min(due - Date.now(), LONGEST_TIMEOUT_MS));
224
+ };
225
+ let state = compute();
226
+ const emit = () => {
227
+ state = compute();
228
+ watchPresence();
229
+ for (const listener of [...listeners])
230
+ listener();
231
+ };
232
+ const watch = () => {
233
+ if (watched === threadId)
234
+ return;
235
+ stopWatching?.();
236
+ watched = threadId;
237
+ stopWatching = threadId === undefined ? undefined : agentUi.watch(threadId);
238
+ };
239
+ const absorb = (page) => {
240
+ items = new Map(items);
241
+ for (const item of page.items)
242
+ items.set(item.id, item);
243
+ if (page.turns) {
244
+ const fresh = new Map(page.turns.map((turn) => [turn.turnId, turn]));
245
+ turns = [...turns.filter((turn) => !fresh.has(turn.turnId)), ...page.turns];
246
+ }
247
+ open = pruneOpen(open, items, turns);
248
+ };
249
+ const oldestSeq = () => items.size === 0
250
+ ? undefined
251
+ : Math.min(...[...items.values()].map((item) => item.seq));
252
+ const takeThreads = (snapshot) => {
253
+ threads = snapshot.threads;
254
+ if (olderThreads.length === 0)
255
+ moreThreads = snapshot.moreThreads === true;
256
+ };
257
+ const load = async (target) => {
258
+ const epoch = generation;
259
+ const snapshot = await calls.call("open", target ? { threadId: target } : {});
260
+ if (epoch !== generation)
261
+ return;
262
+ staleReads.clear();
263
+ takeThreads(snapshot);
264
+ hosts = snapshot.hosts;
265
+ const page = startedNew && target === undefined ? undefined : snapshot.page;
266
+ thread = page?.thread;
267
+ threadId = page?.thread?.id ?? target;
268
+ if (page) {
269
+ const oldest = oldestSeq();
270
+ absorb(page);
271
+ const first = page.items[0];
272
+ if (oldest === undefined || (first !== undefined && first.seq <= oldest)) {
273
+ hasOlder = page.hasMore;
274
+ }
275
+ }
276
+ open = mergeSnapshot(open, channelOpen && page ? page.open : []);
277
+ const replay = beforeLoad;
278
+ beforeLoad = [];
279
+ loaded = "yes";
280
+ retryDelay = READ_RETRY_BASE_MS;
281
+ for (const frame of replay) {
282
+ if (frame.threadId === threadId)
283
+ open = applyFrame(open, frame);
284
+ }
285
+ open = pruneOpen(open, items, turns);
286
+ watch();
287
+ };
288
+ const refreshLists = async () => {
289
+ const epoch = generation;
290
+ const fresh = await calls.call("open", {});
291
+ if (epoch !== generation || threadId !== undefined)
292
+ return;
293
+ takeThreads(fresh);
294
+ hosts = fresh.hosts;
295
+ emit();
296
+ };
297
+ const refreshHosts = async () => {
298
+ const fresh = await calls.call("hosts", {});
299
+ hosts = fresh;
300
+ emit();
301
+ };
302
+ const readLists = async () => {
303
+ if (threadId === undefined && !startedNew && draft.trim() === "") {
304
+ await load(undefined);
305
+ emit();
306
+ }
307
+ else
308
+ await refreshLists();
309
+ };
310
+ const staleReaders = {
311
+ lists: readLists,
312
+ hosts: refreshHosts,
313
+ };
314
+ const cancelRetry = () => {
315
+ if (retryTimer !== undefined)
316
+ globalThis.clearTimeout(retryTimer);
317
+ retryTimer = undefined;
318
+ };
319
+ const clearRetry = () => {
320
+ cancelRetry();
321
+ retryDelay = READ_RETRY_BASE_MS;
322
+ };
323
+ const scheduleRetry = () => {
324
+ if (!running || retryTimer !== undefined)
325
+ return;
326
+ const wait = retryDelay;
327
+ retryDelay = Math.min(retryDelay * 2, READ_RETRY_MAX_MS);
328
+ retryTimer = globalThis.setTimeout(() => {
329
+ retryTimer = undefined;
330
+ void mend();
331
+ }, wait);
332
+ };
333
+ const settled = () => {
334
+ if (!dirty && staleReads.size === 0)
335
+ clearRetry();
336
+ };
337
+ const rereadStale = async (kind) => {
338
+ const epoch = generation;
339
+ try {
340
+ await staleReaders[kind]();
341
+ staleReads.delete(kind);
342
+ settled();
343
+ }
344
+ catch (error) {
345
+ if (epoch !== generation)
346
+ return;
347
+ staleReads.add(kind);
348
+ if (retryable(error))
349
+ scheduleRetry();
350
+ }
351
+ };
352
+ let refreshing = false;
353
+ let refreshAgain = false;
354
+ const refresh = async () => {
355
+ if (refreshing) {
356
+ refreshAgain = true;
357
+ return;
358
+ }
359
+ refreshing = true;
360
+ try {
361
+ do {
362
+ refreshAgain = false;
363
+ if (loaded !== "yes" || threadId === undefined)
364
+ break;
365
+ const epoch = generation;
366
+ const target = threadId;
367
+ if (turns.some(isActive)) {
368
+ await load(target);
369
+ }
370
+ else {
371
+ const lastSeq = Math.max(0, ...[...items.values()].map((item) => item.seq));
372
+ const fresh = await calls.call("items", {
373
+ threadId: target,
374
+ afterSeq: lastSeq,
375
+ });
376
+ if (epoch !== generation)
377
+ break;
378
+ absorb({ items: fresh });
379
+ const known = new Set(turns.map((turn) => turn.turnId));
380
+ if (fresh.some((item) => !known.has(item.turnId)))
381
+ await load(target);
382
+ }
383
+ dirty = false;
384
+ settled();
385
+ emit();
386
+ } while (refreshAgain);
387
+ }
388
+ catch (error) {
389
+ refreshAgain = false;
390
+ dirty = true;
391
+ if (retryable(error))
392
+ scheduleRetry();
393
+ }
394
+ finally {
395
+ refreshing = false;
396
+ }
397
+ };
398
+ const reporting = async (command) => {
399
+ try {
400
+ await command();
401
+ }
402
+ catch (error) {
403
+ failure = readable(error);
404
+ emit();
405
+ throw error;
406
+ }
407
+ if (failure !== undefined) {
408
+ failure = undefined;
409
+ emit();
410
+ }
411
+ };
412
+ const awaitsApproval = (turn) => {
413
+ if (!isActive(turn))
414
+ return false;
415
+ let waiting = false;
416
+ for (const item of [...items.values()].sort(bySeq)) {
417
+ if (item.turnId !== turn.turnId)
418
+ continue;
419
+ if (item.kind === "event" && item.body.event === "device_pending")
420
+ waiting = true;
421
+ else if (item.author !== "user")
422
+ waiting = false;
423
+ }
424
+ return waiting;
425
+ };
426
+ const deviceOf = async () => {
427
+ const found = await deps.auth?.device?.();
428
+ device = found ?? device;
429
+ return found;
430
+ };
431
+ const refreshLink = async () => {
432
+ if (!deps.auth?.linkState)
433
+ return;
434
+ try {
435
+ linkInfo = await deps.auth.linkState();
436
+ }
437
+ catch {
438
+ return;
439
+ }
440
+ if (linking && linking.deviceId !== linkInfo.deviceId)
441
+ linking = undefined;
442
+ emit();
443
+ };
444
+ const whenSetUp = async (action) => {
445
+ if (explainers > 0 &&
446
+ device === undefined &&
447
+ (await deps.auth?.deviceSetup?.()) === "needed") {
448
+ afterSetup = action;
449
+ setup = { step: "explain" };
450
+ emit();
451
+ return;
452
+ }
453
+ await action();
454
+ };
455
+ const signPrompt = async (prompt) => {
456
+ const own = await deviceOf();
457
+ if (!own)
458
+ return undefined;
459
+ const due = presenceDueAt(`${prompt.agent.hostId}/${prompt.agent.agentId}`);
460
+ return due !== undefined && Date.now() >= due
461
+ ? own.signPrompt(prompt, { presence: true })
462
+ : own.signPrompt(prompt);
463
+ };
464
+ const submit = async (attempt, agentKey, settle) => {
465
+ const [hostId, agentId] = agentKey.split("/");
466
+ const epoch = generation;
467
+ sending = true;
468
+ emit();
469
+ try {
470
+ await reporting(async () => {
471
+ const agent = { hostId: asId("host", hostId), agentId };
472
+ const signed = deps.auth?.device
473
+ ? await signPrompt({
474
+ threadId: attempt.threadId,
475
+ turnId: attempt.turnId,
476
+ agent,
477
+ text: attempt.text,
478
+ })
479
+ : undefined;
480
+ await calls.call("send", {
481
+ threadId: attempt.threadId,
482
+ turnId: attempt.turnId,
483
+ itemId: attempt.itemId,
484
+ clientId: deps.clientId,
485
+ prompt: { text: attempt.text, agent, ...(signed ? { signed } : {}) },
486
+ });
487
+ settle();
488
+ if (epoch !== generation)
489
+ return;
490
+ startedNew = false;
491
+ threadId = attempt.threadId;
492
+ watch();
493
+ await load(threadId);
494
+ });
495
+ }
496
+ finally {
497
+ sending = false;
498
+ emit();
499
+ }
500
+ };
501
+ const activeTurn = () => [...turns].reverse().find(isActive);
502
+ const switchTo = (target, blank) => {
503
+ generation += 1;
504
+ threadId = target;
505
+ startedNew = blank;
506
+ thread = undefined;
507
+ items = new Map();
508
+ turns = [];
509
+ hasOlder = false;
510
+ open = NO_OPEN_TURNS;
511
+ beforeLoad = [];
512
+ pendingSend = undefined;
513
+ stopIds.clear();
514
+ answerIds.clear();
515
+ dirty = false;
516
+ staleReads.clear();
517
+ clearRetry();
518
+ watch();
519
+ if (blank && loaded === "yes") {
520
+ emit();
521
+ return;
522
+ }
523
+ loaded = "no";
524
+ emit();
525
+ void loadFirst().then(emit);
526
+ };
527
+ const runtime = {
528
+ subscribe(listener) {
529
+ listeners.add(listener);
530
+ return () => listeners.delete(listener);
531
+ },
532
+ get: () => state,
533
+ setDraft(text) {
534
+ draft = text;
535
+ emit();
536
+ announceActivity();
537
+ },
538
+ async send(text = draft) {
539
+ announceActivity();
540
+ const prompt = text.trim();
541
+ const target = state.agent;
542
+ if (!prompt || isPromptTooLong(prompt) || !target || sending || loaded !== "yes") {
543
+ return;
544
+ }
545
+ const retrying = pendingSend?.text === prompt &&
546
+ pendingSend.agent === target.key &&
547
+ pendingSend.openedIn === threadId;
548
+ const attempt = retrying && pendingSend
549
+ ? pendingSend
550
+ : (pendingSend = {
551
+ text: prompt,
552
+ agent: target.key,
553
+ openedIn: threadId,
554
+ threadId: threadId ?? mint("thread"),
555
+ turnId: mint("turn"),
556
+ itemId: mint("item"),
557
+ });
558
+ await whenSetUp(() => submit(attempt, target.key, () => {
559
+ pendingSend = undefined;
560
+ if (draft.trim() === prompt)
561
+ draft = "";
562
+ }));
563
+ },
564
+ async loadOlder() {
565
+ const target = threadId;
566
+ const before = oldestSeq();
567
+ if (target === undefined || before === undefined || !hasOlder || loadingOlder)
568
+ return;
569
+ const epoch = generation;
570
+ loadingOlder = true;
571
+ emit();
572
+ try {
573
+ await reporting(async () => {
574
+ const older = await calls.call("history", {
575
+ threadId: target,
576
+ beforeSeq: before,
577
+ });
578
+ if (epoch !== generation)
579
+ return;
580
+ absorb(older);
581
+ hasOlder = older.hasMore;
582
+ });
583
+ }
584
+ finally {
585
+ loadingOlder = false;
586
+ emit();
587
+ }
588
+ },
589
+ async loadMoreThreads() {
590
+ const last = olderThreads.at(-1) ?? threads.at(-1);
591
+ if (!last || !moreThreads || loadingOlder)
592
+ return;
593
+ loadingOlder = true;
594
+ emit();
595
+ try {
596
+ await reporting(async () => {
597
+ const next = await calls.call("threadsBefore", {
598
+ before: { updatedAt: last.updatedAt, id: last.id },
599
+ });
600
+ olderThreads = [...olderThreads, ...next.threads];
601
+ moreThreads = next.hasMore;
602
+ });
603
+ }
604
+ finally {
605
+ loadingOlder = false;
606
+ emit();
607
+ }
608
+ },
609
+ async resend(promptItemId) {
610
+ const prompt = state.items.find((item) => item.kind === "prompt" && item.id === promptItemId);
611
+ if (prompt?.kind !== "prompt" || sending || loaded !== "yes" || !threadId)
612
+ return;
613
+ const key = `${prompt.agent.hostId}/${prompt.agent.agentId}`;
614
+ let attempt = resends.get(prompt.id);
615
+ if (!attempt) {
616
+ attempt = {
617
+ text: prompt.text,
618
+ agent: key,
619
+ openedIn: threadId,
620
+ threadId,
621
+ turnId: mint("turn"),
622
+ itemId: mint("item"),
623
+ };
624
+ resends.set(prompt.id, attempt);
625
+ }
626
+ await whenSetUp(() => submit(attempt, key, () => resends.delete(prompt.id)));
627
+ },
628
+ async stop() {
629
+ const turn = activeTurn();
630
+ if (!turn || threadId === undefined)
631
+ return;
632
+ const itemId = stopIds.get(turn.turnId) ?? mint("item");
633
+ stopIds.set(turn.turnId, itemId);
634
+ const target = threadId;
635
+ await reporting(async () => {
636
+ await calls.call("cancel", { threadId: target, turnId: turn.turnId, itemId });
637
+ await refresh();
638
+ });
639
+ },
640
+ async answer(askId, choice, values) {
641
+ const ask = state.items.find((item) => item.kind === "ask" && item.id === askId);
642
+ if (!ask || ask.kind !== "ask" || ask.answer.state !== "open" || !threadId)
643
+ return;
644
+ if (ask.approval === "computer") {
645
+ throw new Error("This question is answered on your computer.");
646
+ }
647
+ const itemId = answerIds.get(askId) ?? mint("item");
648
+ answerIds.set(askId, itemId);
649
+ const target = threadId;
650
+ const toSign = {
651
+ threadId: target,
652
+ askId: ask.id,
653
+ choice,
654
+ ...(values ? { values } : {}),
655
+ };
656
+ await whenSetUp(() => reporting(async () => {
657
+ const found = deps.auth?.device ? await deviceOf() : undefined;
658
+ if (ask.approval === "presence" && !found) {
659
+ throw new NeedsDevice("This question needs a device that can confirm it is you.");
660
+ }
661
+ const signed = await found?.signAnswer(toSign, {
662
+ presence: ask.approval === "presence",
663
+ });
664
+ await calls.call("answer", {
665
+ threadId: target,
666
+ turnId: ask.turnId,
667
+ itemId,
668
+ answer: {
669
+ askId: ask.id,
670
+ choice,
671
+ ...(values ? { values } : {}),
672
+ ...(signed ? { signed } : {}),
673
+ },
674
+ });
675
+ answerIds.delete(askId);
676
+ await refresh();
677
+ }));
678
+ },
679
+ async setUpDevice({ passkey }) {
680
+ if (setup === undefined || setup.step === "passkey")
681
+ return;
682
+ setup = { step: "passkey" };
683
+ emit();
684
+ try {
685
+ device = (await deps.auth?.device?.({ presence: passkey })) ?? device;
686
+ }
687
+ catch (error) {
688
+ if (error instanceof Error && error.name === "PresenceSetupCancelled") {
689
+ setup = { step: "cancelled" };
690
+ emit();
691
+ return;
692
+ }
693
+ setup = undefined;
694
+ afterSetup = undefined;
695
+ failure = readable(error);
696
+ emit();
697
+ return;
698
+ }
699
+ const waiting = afterSetup;
700
+ setup = undefined;
701
+ afterSetup = undefined;
702
+ emit();
703
+ await waiting?.();
704
+ },
705
+ dismissSetup() {
706
+ setup = undefined;
707
+ afterSetup = undefined;
708
+ emit();
709
+ },
710
+ async startLink(options) {
711
+ if (!deps.auth?.startLink || linkBusy)
712
+ return;
713
+ linkBusy = true;
714
+ linkError = undefined;
715
+ emit();
716
+ try {
717
+ const started = await deps.auth.startLink(options);
718
+ linking = {
719
+ deviceId: started.deviceId,
720
+ code: started.code,
721
+ qr: `${deps.appOrigin}/link#${new URLSearchParams({ d: started.deviceId, c: started.commitment })}`,
722
+ expiresAt: started.expiresAt,
723
+ };
724
+ linkExpired = false;
725
+ clearTimeout(linkTimer);
726
+ linkTimer = setTimeout(() => {
727
+ linking = undefined;
728
+ linkExpired = true;
729
+ emit();
730
+ }, Math.min(Math.max(0, Date.parse(started.expiresAt) - Date.now()), LONGEST_TIMEOUT_MS));
731
+ linkInfo = {
732
+ vouched: false,
733
+ pairing: false,
734
+ ...linkInfo,
735
+ deviceId: started.deviceId,
736
+ };
737
+ }
738
+ catch (error) {
739
+ linkError =
740
+ error instanceof Error && error.name === "PresenceSetupCancelled"
741
+ ? "Face ID or Touch ID was cancelled. Try again, or link without it."
742
+ : readable(error);
743
+ }
744
+ finally {
745
+ linkBusy = false;
746
+ emit();
747
+ }
748
+ },
749
+ explainSetup() {
750
+ explainers += 1;
751
+ return () => {
752
+ explainers -= 1;
753
+ if (explainers === 0 && setup !== undefined)
754
+ runtime.dismissSetup();
755
+ };
756
+ },
757
+ choose(agentKey) {
758
+ chosen = agentKey;
759
+ emit();
760
+ },
761
+ openThread(target) {
762
+ switchTo(target === undefined ? undefined : asId("thread", target), false);
763
+ },
764
+ newThread() {
765
+ switchTo(undefined, true);
766
+ },
767
+ start() {
768
+ running = true;
769
+ const detachChannel = deps.addChannel({
770
+ event: CHAT_CHANNEL,
771
+ opened: () => {
772
+ channelOpen = true;
773
+ if (loaded === "yes" && turns.some(isActive) && !refreshing)
774
+ void refresh();
775
+ },
776
+ receive(data) {
777
+ const frame = parseChatFrame(data);
778
+ if (!frame)
779
+ return;
780
+ if (loaded === "no") {
781
+ beforeLoad.push(frame);
782
+ return;
783
+ }
784
+ if (loaded !== "yes" || frame.threadId !== threadId)
785
+ return;
786
+ open = applyFrame(open, frame);
787
+ emit();
788
+ },
789
+ closed: () => {
790
+ channelOpen = false;
791
+ },
792
+ });
793
+ const offTags = deps.onSyncTags((tags) => {
794
+ const mine = tags.some((tag) => tagsOverlap(tag, threadId === undefined ? [CHAT_TAG] : [CHAT_TAG, threadId]));
795
+ const computers = tags.some((tag) => tagsOverlap(tag, [HOSTS_TAG]) || tagsOverlap(tag, [HOST_GRANTS_TAG]));
796
+ if (tags.some((tag) => tagsOverlap(tag, [DEVICES_TAG])))
797
+ void refreshLink();
798
+ if (loaded === "failed" && (mine || computers)) {
799
+ cancelRetry();
800
+ void loadFirst().then(emit);
801
+ }
802
+ else if (loaded === "no") {
803
+ if (mine || computers)
804
+ staleAfterLoad = true;
805
+ }
806
+ else {
807
+ if (mine && threadId !== undefined)
808
+ void refresh();
809
+ else if (mine)
810
+ void rereadStale("lists");
811
+ if (computers)
812
+ void rereadStale("hosts");
813
+ }
814
+ });
815
+ const offResume = deps.onResume(() => {
816
+ if (loaded === "failed") {
817
+ if (!unfixable) {
818
+ cancelRetry();
819
+ void loadFirst().then(emit);
820
+ }
821
+ }
822
+ else if (loaded === "no")
823
+ staleAfterLoad = true;
824
+ else
825
+ void refresh();
826
+ });
827
+ const offNegotiated = deps.onNegotiated((transport) => {
828
+ if (transport === "poll")
829
+ announceActivity();
830
+ });
831
+ const offConfirmations = agentUi.confirmations.subscribe(emit);
832
+ emit();
833
+ announceActivity();
834
+ void initial();
835
+ return () => {
836
+ running = false;
837
+ channelOpen = false;
838
+ generation += 1;
839
+ clearRetry();
840
+ clearTimeout(presenceTimer);
841
+ presenceTimer = undefined;
842
+ clearTimeout(linkTimer);
843
+ linkTimer = undefined;
844
+ detachChannel();
845
+ offTags();
846
+ offResume();
847
+ offNegotiated();
848
+ offConfirmations();
849
+ stopWatching?.();
850
+ stopWatching = undefined;
851
+ watched = undefined;
852
+ };
853
+ },
854
+ };
855
+ async function mend() {
856
+ if (loaded === "failed")
857
+ await loadFirst();
858
+ else if (loaded === "yes") {
859
+ if (dirty)
860
+ await refresh();
861
+ for (const kind of [...staleReads])
862
+ await rereadStale(kind);
863
+ }
864
+ emit();
865
+ }
866
+ function announceActivity() {
867
+ if (!running || pingRefused || deps.transport() !== "poll")
868
+ return;
869
+ if (deps.isHidden?.() === true)
870
+ return;
871
+ const now = Date.now();
872
+ if (lastPing !== undefined && now - lastPing < ACTIVITY_PING_MS)
873
+ return;
874
+ lastPing = now;
875
+ calls.call("userActive", {}).catch((error) => {
876
+ if (error instanceof ActionError && error.code === "not_found")
877
+ pingRefused = true;
878
+ });
879
+ }
880
+ async function loadFirst() {
881
+ try {
882
+ await load(threadId);
883
+ unfixable = false;
884
+ }
885
+ catch (error) {
886
+ if (running) {
887
+ loaded = "failed";
888
+ unfixable = !retryable(error);
889
+ if (!unfixable)
890
+ scheduleRetry();
891
+ }
892
+ return;
893
+ }
894
+ if (staleAfterLoad) {
895
+ staleAfterLoad = false;
896
+ await refresh();
897
+ }
898
+ }
899
+ async function initial() {
900
+ void deps.localHost.then((handle) => {
901
+ const authorizeHost = deps.auth?.authorizeHost;
902
+ if (!handle.available || !authorizeHost || !running)
903
+ return;
904
+ connect = {
905
+ kind: "here",
906
+ start: async () => {
907
+ const own = await deps.auth?.device?.();
908
+ const outcome = await handle.api.connect({
909
+ appOrigin: deps.appOrigin,
910
+ authorize: authorizeHost,
911
+ ...(own ? { device: own.keys } : {}),
912
+ });
913
+ if (!outcome.connected)
914
+ throw new Error(`Connecting failed: ${outcome.reason}`);
915
+ await refreshHosts();
916
+ await refreshLink();
917
+ },
918
+ };
919
+ emit();
920
+ });
921
+ void refreshLink();
922
+ await loadFirst();
923
+ announceActivity();
924
+ emit();
925
+ }
926
+ return runtime;
927
+ }
928
+ //# sourceMappingURL=runtime.js.map