dextunnel 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 (76) hide show
  1. package/LICENSE +211 -0
  2. package/README.md +112 -0
  3. package/SECURITY.md +27 -0
  4. package/SUPPORT.md +43 -0
  5. package/package.json +44 -0
  6. package/public/client-shared.js +1831 -0
  7. package/public/favicon.svg +11 -0
  8. package/public/host.html +29 -0
  9. package/public/host.js +2079 -0
  10. package/public/index.html +28 -0
  11. package/public/index.js +98 -0
  12. package/public/live-bridge-lifecycle.js +258 -0
  13. package/public/live-bridge-retry-state.js +61 -0
  14. package/public/live-selection-intent.js +79 -0
  15. package/public/remote-operator-state.js +316 -0
  16. package/public/remote.html +167 -0
  17. package/public/remote.js +3967 -0
  18. package/public/styles.css +2793 -0
  19. package/public/surface-view-state.js +89 -0
  20. package/public/voice-dictation.js +45 -0
  21. package/src/bin/desktop-rehydration-smoke.mjs +111 -0
  22. package/src/bin/dextunnel.mjs +41 -0
  23. package/src/bin/doctor.mjs +48 -0
  24. package/src/bin/launch-attest.mjs +39 -0
  25. package/src/bin/launch-status.mjs +49 -0
  26. package/src/bin/mobile-link-proxy.mjs +221 -0
  27. package/src/bin/mobile-proof.mjs +164 -0
  28. package/src/bin/mobile-transport-smoke.mjs +200 -0
  29. package/src/bin/probe-codex-app-server-write.mjs +36 -0
  30. package/src/bin/probe-codex-app-server.mjs +30 -0
  31. package/src/lib/agent-room-context.mjs +54 -0
  32. package/src/lib/agent-room-runtime.mjs +355 -0
  33. package/src/lib/agent-room-service.mjs +335 -0
  34. package/src/lib/agent-room-state.mjs +406 -0
  35. package/src/lib/agent-room-store.mjs +71 -0
  36. package/src/lib/agent-room-text.mjs +48 -0
  37. package/src/lib/app-server-contract.mjs +66 -0
  38. package/src/lib/app-server-runtime.mjs +60 -0
  39. package/src/lib/attachment-service.mjs +119 -0
  40. package/src/lib/bridge-api-handler.mjs +719 -0
  41. package/src/lib/bridge-runtime-lifecycle.mjs +51 -0
  42. package/src/lib/bridge-status-builder.mjs +60 -0
  43. package/src/lib/codex-app-server-client.mjs +1511 -0
  44. package/src/lib/companion-state.mjs +453 -0
  45. package/src/lib/control-lease-service.mjs +180 -0
  46. package/src/lib/debug-harness-service.mjs +173 -0
  47. package/src/lib/desktop-integration.mjs +146 -0
  48. package/src/lib/desktop-rehydration-smoke.mjs +269 -0
  49. package/src/lib/dextunnel-cli.mjs +122 -0
  50. package/src/lib/discovery-docs.mjs +1321 -0
  51. package/src/lib/fake-codex-app-server-bridge.mjs +340 -0
  52. package/src/lib/install-preflight.mjs +373 -0
  53. package/src/lib/interaction-resolution-service.mjs +185 -0
  54. package/src/lib/interaction-state.mjs +360 -0
  55. package/src/lib/launch-release-bar.mjs +158 -0
  56. package/src/lib/live-control-state.mjs +107 -0
  57. package/src/lib/live-payload-builder.mjs +298 -0
  58. package/src/lib/live-selection-transition-state.mjs +49 -0
  59. package/src/lib/live-transcript-state.mjs +549 -0
  60. package/src/lib/mobile-network-profile.mjs +39 -0
  61. package/src/lib/mock-codex-adapter.mjs +62 -0
  62. package/src/lib/operator-diagnostics.mjs +82 -0
  63. package/src/lib/repo-changes-service.mjs +527 -0
  64. package/src/lib/runtime-config.mjs +106 -0
  65. package/src/lib/selection-state-service.mjs +214 -0
  66. package/src/lib/session-store.mjs +355 -0
  67. package/src/lib/shared-room-state.mjs +473 -0
  68. package/src/lib/shared-selection-state.mjs +40 -0
  69. package/src/lib/sse-hub.mjs +35 -0
  70. package/src/lib/static-surface-service.mjs +71 -0
  71. package/src/lib/surface-access.mjs +189 -0
  72. package/src/lib/surface-presence-service.mjs +118 -0
  73. package/src/lib/surface-request-guard.mjs +52 -0
  74. package/src/lib/thread-sync-state.mjs +536 -0
  75. package/src/lib/watcher-lifecycle.mjs +287 -0
  76. package/src/server.mjs +1446 -0
@@ -0,0 +1,536 @@
1
+ export function createThreadSyncStateService({
2
+ broadcast = () => {},
3
+ buildLivePayload = () => ({}),
4
+ buildLightweightSelectedThreadSnapshot = null,
5
+ clearControlLease = () => {},
6
+ codexAppServer,
7
+ fallbackLiveSourceKinds = ["vscode", "cli"],
8
+ liveState,
9
+ loadThreadAgentRoomState = async () => {},
10
+ mapThreadToCompanionSnapshot,
11
+ nowIso = () => new Date().toISOString(),
12
+ preferredLiveSourceKinds = ["vscode"],
13
+ processCwd = () => process.cwd(),
14
+ snapshotNeedsDeepHydration = () => false,
15
+ selectedTranscriptLimit = 120,
16
+ summarizeThread = (thread) => thread,
17
+ buildSelectedThreadSnapshot = (thread, { limit = selectedTranscriptLimit } = {}) => (
18
+ mapThreadToCompanionSnapshot(thread, { limit })
19
+ ),
20
+ buildThreadSummary = (thread) => summarizeThread(thread),
21
+ readSelectedThread = (threadId) => codexAppServer.readThread(threadId, true),
22
+ } = {}) {
23
+ const threadSummaryCache = new Map();
24
+ const selectedSnapshotCache = new Map();
25
+ const selectedSnapshotWarmers = new Map();
26
+ const preservedTranscriptLimit = Math.max(selectedTranscriptLimit * 2, 24);
27
+ const buildQuickSelectedThreadSnapshot =
28
+ typeof buildLightweightSelectedThreadSnapshot === "function"
29
+ ? buildLightweightSelectedThreadSnapshot
30
+ : null;
31
+
32
+ function pruneCache(cache, maxEntries = 240) {
33
+ while (cache.size > maxEntries) {
34
+ const oldestKey = cache.keys().next().value;
35
+ if (oldestKey == null) {
36
+ break;
37
+ }
38
+ cache.delete(oldestKey);
39
+ }
40
+ }
41
+
42
+ function threadCacheKey(thread) {
43
+ return [
44
+ thread?.id || "",
45
+ thread?.updatedAt || "",
46
+ thread?.path || "",
47
+ thread?.preview || ""
48
+ ].join("|");
49
+ }
50
+
51
+ async function warmSelectedThreadSnapshotForThread(thread, { limit = selectedTranscriptLimit } = {}) {
52
+ if (!thread?.id) {
53
+ return null;
54
+ }
55
+
56
+ const snapshotCacheKey = threadCacheKey(thread);
57
+ const cached = snapshotCacheKey ? selectedSnapshotCache.get(snapshotCacheKey) : null;
58
+ if (cached) {
59
+ return cached;
60
+ }
61
+
62
+ const existing = selectedSnapshotWarmers.get(thread.id);
63
+ if (existing?.cacheKey === snapshotCacheKey) {
64
+ return existing.promise;
65
+ }
66
+
67
+ const promise = Promise.resolve(buildSelectedThreadSnapshot(thread, { limit }))
68
+ .then((snapshot) => {
69
+ if (snapshotCacheKey && snapshot) {
70
+ selectedSnapshotCache.set(snapshotCacheKey, snapshot);
71
+ pruneCache(selectedSnapshotCache, 48);
72
+ }
73
+ return snapshot;
74
+ })
75
+ .finally(() => {
76
+ const current = selectedSnapshotWarmers.get(thread.id);
77
+ if (current?.promise === promise) {
78
+ selectedSnapshotWarmers.delete(thread.id);
79
+ }
80
+ });
81
+
82
+ selectedSnapshotWarmers.set(thread.id, {
83
+ cacheKey: snapshotCacheKey,
84
+ promise
85
+ });
86
+ return promise;
87
+ }
88
+
89
+ function transcriptEntryMergeKey(entry = {}) {
90
+ if (entry?.itemId) {
91
+ return `item:${entry.itemId}`;
92
+ }
93
+
94
+ if (entry?.turnId) {
95
+ return `turn:${entry.turnId}|${entry?.role || ""}|${entry?.kind || ""}`;
96
+ }
97
+
98
+ return "";
99
+ }
100
+
101
+ function normalizeTranscriptText(value = "") {
102
+ return String(value || "").replace(/\s+/g, " ").trim().toLowerCase();
103
+ }
104
+
105
+ function transcriptTimestampBucket(value = "") {
106
+ const ms = new Date(value || 0).getTime();
107
+ if (!Number.isFinite(ms) || ms <= 0) {
108
+ return "";
109
+ }
110
+
111
+ return String(Math.floor(ms / 1000));
112
+ }
113
+
114
+ function transcriptEntrySemanticKey(entry = {}) {
115
+ const text = normalizeTranscriptText(entry?.text || "");
116
+ if (!text) {
117
+ return "";
118
+ }
119
+
120
+ return [
121
+ entry?.role || "",
122
+ entry?.kind || "",
123
+ transcriptTimestampBucket(entry?.timestamp || ""),
124
+ text
125
+ ].join("|");
126
+ }
127
+
128
+ function mergeSelectedThreadSnapshot(previousSnapshot, nextSnapshot) {
129
+ const previousThreadId = previousSnapshot?.thread?.id || null;
130
+ const nextThreadId = nextSnapshot?.thread?.id || null;
131
+ if (!previousThreadId || previousThreadId !== nextThreadId) {
132
+ return nextSnapshot;
133
+ }
134
+
135
+ const previousTranscript = Array.isArray(previousSnapshot?.transcript) ? previousSnapshot.transcript : [];
136
+ const nextTranscript = Array.isArray(nextSnapshot?.transcript) ? nextSnapshot.transcript : [];
137
+ if (previousTranscript.length === 0 || nextTranscript.length === 0) {
138
+ return nextSnapshot;
139
+ }
140
+
141
+ const merged = [];
142
+ const seenIdentityKeys = new Set();
143
+ const seenSemanticKeys = new Set();
144
+ for (const entry of [...previousTranscript, ...nextTranscript]) {
145
+ const identityKey = transcriptEntryMergeKey(entry);
146
+ const semanticKey = transcriptEntrySemanticKey(entry);
147
+ if (
148
+ (identityKey && seenIdentityKeys.has(identityKey)) ||
149
+ (semanticKey && seenSemanticKeys.has(semanticKey))
150
+ ) {
151
+ continue;
152
+ }
153
+
154
+ if (identityKey) {
155
+ seenIdentityKeys.add(identityKey);
156
+ }
157
+ if (semanticKey) {
158
+ seenSemanticKeys.add(semanticKey);
159
+ }
160
+ merged.push(entry);
161
+ }
162
+
163
+ return {
164
+ ...nextSnapshot,
165
+ transcript: merged.slice(-preservedTranscriptLimit),
166
+ transcriptCount: Math.max(
167
+ Number.isFinite(previousSnapshot?.transcriptCount) ? previousSnapshot.transcriptCount : previousTranscript.length,
168
+ Number.isFinite(nextSnapshot?.transcriptCount) ? nextSnapshot.transcriptCount : nextTranscript.length,
169
+ merged.length
170
+ )
171
+ };
172
+ }
173
+
174
+ function setSelectedSnapshotHydrationState(snapshot, transcriptHydrating = false) {
175
+ if (!snapshot) {
176
+ return snapshot;
177
+ }
178
+
179
+ return {
180
+ ...snapshot,
181
+ transcriptHydrating: Boolean(transcriptHydrating),
182
+ thread: snapshot.thread
183
+ ? {
184
+ ...snapshot.thread,
185
+ transcriptHydrating: Boolean(transcriptHydrating)
186
+ }
187
+ : snapshot.thread
188
+ };
189
+ }
190
+
191
+ async function refreshThreadSummary(thread) {
192
+ if (!thread) {
193
+ return;
194
+ }
195
+
196
+ const summaryCacheKey = threadCacheKey(thread);
197
+ const summary = threadSummaryCache.get(summaryCacheKey) || await buildThreadSummary(thread);
198
+ threadSummaryCache.set(summaryCacheKey, summary);
199
+ pruneCache(threadSummaryCache);
200
+ const index = liveState.threads.findIndex((entry) => entry.id === thread.id);
201
+ if (index >= 0) {
202
+ liveState.threads = liveState.threads.map((entry, entryIndex) => (
203
+ entryIndex === index
204
+ ? {
205
+ ...entry,
206
+ ...summary,
207
+ id: entry.id
208
+ }
209
+ : entry
210
+ ));
211
+ } else {
212
+ liveState.threads = [summary, ...liveState.threads];
213
+ }
214
+ }
215
+
216
+ async function commitSelectedThreadSnapshot(thread, snapshot) {
217
+ const previousSnapshot = liveState.selectedThreadSnapshot;
218
+ liveState.selectedThreadSnapshot = thread && snapshot
219
+ ? mergeSelectedThreadSnapshot(previousSnapshot, snapshot)
220
+ : thread
221
+ ? snapshot
222
+ : null;
223
+ liveState.selectedProjectCwd = thread?.cwd || liveState.selectedProjectCwd;
224
+ await refreshThreadSummary(thread);
225
+ liveState.lastSyncAt = nowIso();
226
+ liveState.lastError = null;
227
+ }
228
+
229
+ function hydrateSelectedThreadSnapshotInBackground(thread, requestedThreadId) {
230
+ if (!thread?.id || !requestedThreadId) {
231
+ return;
232
+ }
233
+
234
+ void warmSelectedThreadSnapshotForThread(thread, { limit: selectedTranscriptLimit })
235
+ .then(async (snapshot) => {
236
+ if (liveState.selectedThreadId !== requestedThreadId) {
237
+ return;
238
+ }
239
+
240
+ await commitSelectedThreadSnapshot(
241
+ thread,
242
+ setSelectedSnapshotHydrationState(snapshot, false)
243
+ );
244
+ broadcast("live", buildLivePayload());
245
+ })
246
+ .catch((error) => {
247
+ if (liveState.selectedThreadId !== requestedThreadId) {
248
+ return;
249
+ }
250
+
251
+ if (liveState.selectedThreadSnapshot?.thread?.id === requestedThreadId) {
252
+ liveState.selectedThreadSnapshot = setSelectedSnapshotHydrationState(
253
+ liveState.selectedThreadSnapshot,
254
+ false
255
+ );
256
+ }
257
+ liveState.lastError = error?.message || "Thread refresh failed.";
258
+ broadcast("live", buildLivePayload());
259
+ });
260
+ }
261
+
262
+ function maybePickFallbackSelection() {
263
+ const previousThreadId = liveState.selectedThreadId;
264
+ if (liveState.selectedThreadId && liveState.threads.some((thread) => thread.id === liveState.selectedThreadId)) {
265
+ return;
266
+ }
267
+
268
+ const preferred =
269
+ liveState.threads.find((thread) => thread.cwd === liveState.selectedProjectCwd) ||
270
+ liveState.threads.find((thread) => thread.cwd === processCwd()) ||
271
+ liveState.threads[0] ||
272
+ null;
273
+
274
+ liveState.selectedProjectCwd = preferred?.cwd || processCwd();
275
+ liveState.selectedThreadId = preferred?.id || null;
276
+ if (liveState.selectedThreadId !== previousThreadId) {
277
+ clearControlLease({ broadcastUpdate: false });
278
+ }
279
+ }
280
+
281
+ async function hydrateThreadSummaries(threads = []) {
282
+ if (!Array.isArray(threads) || threads.length === 0) {
283
+ return [];
284
+ }
285
+
286
+ const existingById = new Map(
287
+ (Array.isArray(liveState.threads) ? liveState.threads : [])
288
+ .filter((thread) => thread?.id)
289
+ .map((thread) => [thread.id, thread])
290
+ );
291
+
292
+ const hydrated = await Promise.all(
293
+ threads.map(async (thread) => {
294
+ const base = {
295
+ ...(existingById.get(thread.id) || {}),
296
+ ...(thread || {})
297
+ };
298
+
299
+ try {
300
+ const cacheKey = threadCacheKey(base);
301
+ const cached = threadSummaryCache.get(cacheKey);
302
+ if (cached) {
303
+ return {
304
+ ...base,
305
+ ...cached,
306
+ id: thread.id
307
+ };
308
+ }
309
+
310
+ const summary = await buildThreadSummary(base);
311
+ threadSummaryCache.set(cacheKey, summary);
312
+ pruneCache(threadSummaryCache);
313
+ return {
314
+ ...base,
315
+ ...summary,
316
+ id: thread.id
317
+ };
318
+ } catch {
319
+ return base;
320
+ }
321
+ })
322
+ );
323
+
324
+ return hydrated;
325
+ }
326
+
327
+ function prewarmPriority(thread, index) {
328
+ let score = Math.max(0, 40 - index);
329
+ if (thread?.cwd && thread.cwd === liveState.selectedProjectCwd) {
330
+ score += 120;
331
+ }
332
+ if (thread?.cwd && thread.cwd === processCwd()) {
333
+ score += 80;
334
+ }
335
+ if (thread?.activeTurnId) {
336
+ score += 24;
337
+ }
338
+ if (thread?.source === "vscode") {
339
+ score += 12;
340
+ }
341
+ return score;
342
+ }
343
+
344
+ async function prewarmThreadSnapshots({
345
+ excludeThreadId = null,
346
+ limit = selectedTranscriptLimit,
347
+ maxThreads = 3,
348
+ threads = liveState.threads
349
+ } = {}) {
350
+ const normalizedExcludeThreadId = String(excludeThreadId || "").trim();
351
+ const candidates = (Array.isArray(threads) ? threads : [])
352
+ .map((thread, index) => ({
353
+ index,
354
+ priority: prewarmPriority(thread, index),
355
+ thread
356
+ }))
357
+ .filter(({ thread }) => thread?.id && thread.id !== normalizedExcludeThreadId)
358
+ .sort((a, b) => {
359
+ const priorityDelta = b.priority - a.priority;
360
+ return priorityDelta !== 0 ? priorityDelta : a.index - b.index;
361
+ })
362
+ .slice(0, Math.max(0, Number.parseInt(maxThreads, 10) || 0))
363
+ .map(({ thread }) => thread);
364
+
365
+ for (const thread of candidates) {
366
+ const cacheKey = threadCacheKey(thread);
367
+ if (cacheKey && selectedSnapshotCache.has(cacheKey)) {
368
+ continue;
369
+ }
370
+
371
+ try {
372
+ const hydratedThread = await readSelectedThread(thread.id);
373
+ if (!hydratedThread) {
374
+ continue;
375
+ }
376
+ await warmSelectedThreadSnapshotForThread(hydratedThread, { limit });
377
+ } catch {
378
+ // Background warmers are a best-effort polish path only.
379
+ }
380
+ }
381
+ }
382
+
383
+ async function refreshThreads({ broadcastUpdate = true } = {}) {
384
+ try {
385
+ let threads = await codexAppServer.listThreads({
386
+ cwd: null,
387
+ limit: 60,
388
+ archived: false,
389
+ sourceKinds: preferredLiveSourceKinds
390
+ });
391
+
392
+ if (threads.length === 0) {
393
+ threads = await codexAppServer.listThreads({
394
+ cwd: null,
395
+ limit: 60,
396
+ archived: false,
397
+ sourceKinds: fallbackLiveSourceKinds
398
+ });
399
+ }
400
+
401
+ liveState.threads = await hydrateThreadSummaries(threads);
402
+ maybePickFallbackSelection();
403
+ liveState.lastError = null;
404
+ } catch (error) {
405
+ liveState.lastError = error.message;
406
+ }
407
+
408
+ if (broadcastUpdate) {
409
+ broadcast("live", buildLivePayload());
410
+ }
411
+ }
412
+
413
+ async function refreshSelectedThreadSnapshot({ broadcastUpdate = true } = {}) {
414
+ const requestedThreadId = liveState.selectedThreadId;
415
+
416
+ if (!requestedThreadId) {
417
+ liveState.selectedThreadSnapshot = null;
418
+ liveState.turnDiff = null;
419
+ if (broadcastUpdate) {
420
+ broadcast("live", buildLivePayload());
421
+ }
422
+ return;
423
+ }
424
+
425
+ try {
426
+ const thread = await readSelectedThread(requestedThreadId);
427
+ await loadThreadAgentRoomState(requestedThreadId);
428
+ if (liveState.selectedThreadId !== requestedThreadId) {
429
+ return;
430
+ }
431
+ const snapshotCacheKey = thread ? threadCacheKey(thread) : null;
432
+ const cachedSnapshot = snapshotCacheKey ? selectedSnapshotCache.get(snapshotCacheKey) : null;
433
+ if (cachedSnapshot) {
434
+ await commitSelectedThreadSnapshot(
435
+ thread,
436
+ setSelectedSnapshotHydrationState(cachedSnapshot, false)
437
+ );
438
+ } else if (thread && buildQuickSelectedThreadSnapshot) {
439
+ const quickSnapshot = await buildQuickSelectedThreadSnapshot(thread, {
440
+ limit: selectedTranscriptLimit
441
+ });
442
+ if (liveState.selectedThreadId !== requestedThreadId) {
443
+ return;
444
+ }
445
+
446
+ const needsDeepHydration = snapshotNeedsDeepHydration(quickSnapshot, {
447
+ limit: selectedTranscriptLimit,
448
+ thread
449
+ });
450
+ await commitSelectedThreadSnapshot(
451
+ thread,
452
+ setSelectedSnapshotHydrationState(quickSnapshot, needsDeepHydration)
453
+ );
454
+ if (needsDeepHydration) {
455
+ if (broadcastUpdate) {
456
+ broadcast("live", buildLivePayload());
457
+ }
458
+ hydrateSelectedThreadSnapshotInBackground(thread, requestedThreadId);
459
+ return;
460
+ }
461
+ } else if (thread) {
462
+ const snapshot = await warmSelectedThreadSnapshotForThread(thread, {
463
+ limit: selectedTranscriptLimit
464
+ });
465
+ if (liveState.selectedThreadId !== requestedThreadId) {
466
+ return;
467
+ }
468
+ await commitSelectedThreadSnapshot(
469
+ thread,
470
+ setSelectedSnapshotHydrationState(snapshot, false)
471
+ );
472
+ } else {
473
+ liveState.selectedThreadSnapshot = null;
474
+ }
475
+ } catch (error) {
476
+ liveState.lastError = error.message;
477
+ }
478
+
479
+ if (broadcastUpdate) {
480
+ broadcast("live", buildLivePayload());
481
+ }
482
+ }
483
+
484
+ async function refreshLiveState({ includeThreads = true } = {}) {
485
+ if (includeThreads) {
486
+ await refreshThreads({ broadcastUpdate: false });
487
+ }
488
+ await refreshSelectedThreadSnapshot({ broadcastUpdate: false });
489
+ broadcast("live", buildLivePayload());
490
+ return buildLivePayload();
491
+ }
492
+
493
+ async function createThreadSelectionState({
494
+ cwd = null,
495
+ source = "remote"
496
+ } = {}) {
497
+ const targetCwd = cwd || liveState.selectedProjectCwd || processCwd();
498
+ const createdThread = await codexAppServer.startThread({
499
+ cwd: targetCwd,
500
+ approvalPolicy: "never",
501
+ sandbox: "workspace-write",
502
+ ephemeral: false,
503
+ persistExtendedHistory: true
504
+ });
505
+ const hydratedThread = await codexAppServer.readThread(createdThread.id, true);
506
+ const snapshot = mapThreadToCompanionSnapshot(hydratedThread, { limit: selectedTranscriptLimit });
507
+
508
+ liveState.selectionSource = source;
509
+ liveState.selectedProjectCwd = hydratedThread.cwd || targetCwd;
510
+ liveState.selectedThreadId = hydratedThread.id;
511
+ liveState.selectedThreadSnapshot = snapshot;
512
+ clearControlLease({ broadcastUpdate: false });
513
+ liveState.turnDiff = null;
514
+ liveState.lastSyncAt = nowIso();
515
+ liveState.lastError = null;
516
+ liveState.threads = [
517
+ summarizeThread(hydratedThread),
518
+ ...liveState.threads.filter((thread) => thread.id !== hydratedThread.id)
519
+ ];
520
+
521
+ return {
522
+ snapshot,
523
+ thread: hydratedThread
524
+ };
525
+ }
526
+
527
+ return {
528
+ createThreadSelectionState,
529
+ mergeSelectedThreadSnapshot,
530
+ maybePickFallbackSelection,
531
+ prewarmThreadSnapshots,
532
+ refreshLiveState,
533
+ refreshSelectedThreadSnapshot,
534
+ refreshThreads
535
+ };
536
+ }