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.
- package/LICENSE +211 -0
- package/README.md +112 -0
- package/SECURITY.md +27 -0
- package/SUPPORT.md +43 -0
- package/package.json +44 -0
- package/public/client-shared.js +1831 -0
- package/public/favicon.svg +11 -0
- package/public/host.html +29 -0
- package/public/host.js +2079 -0
- package/public/index.html +28 -0
- package/public/index.js +98 -0
- package/public/live-bridge-lifecycle.js +258 -0
- package/public/live-bridge-retry-state.js +61 -0
- package/public/live-selection-intent.js +79 -0
- package/public/remote-operator-state.js +316 -0
- package/public/remote.html +167 -0
- package/public/remote.js +3967 -0
- package/public/styles.css +2793 -0
- package/public/surface-view-state.js +89 -0
- package/public/voice-dictation.js +45 -0
- package/src/bin/desktop-rehydration-smoke.mjs +111 -0
- package/src/bin/dextunnel.mjs +41 -0
- package/src/bin/doctor.mjs +48 -0
- package/src/bin/launch-attest.mjs +39 -0
- package/src/bin/launch-status.mjs +49 -0
- package/src/bin/mobile-link-proxy.mjs +221 -0
- package/src/bin/mobile-proof.mjs +164 -0
- package/src/bin/mobile-transport-smoke.mjs +200 -0
- package/src/bin/probe-codex-app-server-write.mjs +36 -0
- package/src/bin/probe-codex-app-server.mjs +30 -0
- package/src/lib/agent-room-context.mjs +54 -0
- package/src/lib/agent-room-runtime.mjs +355 -0
- package/src/lib/agent-room-service.mjs +335 -0
- package/src/lib/agent-room-state.mjs +406 -0
- package/src/lib/agent-room-store.mjs +71 -0
- package/src/lib/agent-room-text.mjs +48 -0
- package/src/lib/app-server-contract.mjs +66 -0
- package/src/lib/app-server-runtime.mjs +60 -0
- package/src/lib/attachment-service.mjs +119 -0
- package/src/lib/bridge-api-handler.mjs +719 -0
- package/src/lib/bridge-runtime-lifecycle.mjs +51 -0
- package/src/lib/bridge-status-builder.mjs +60 -0
- package/src/lib/codex-app-server-client.mjs +1511 -0
- package/src/lib/companion-state.mjs +453 -0
- package/src/lib/control-lease-service.mjs +180 -0
- package/src/lib/debug-harness-service.mjs +173 -0
- package/src/lib/desktop-integration.mjs +146 -0
- package/src/lib/desktop-rehydration-smoke.mjs +269 -0
- package/src/lib/dextunnel-cli.mjs +122 -0
- package/src/lib/discovery-docs.mjs +1321 -0
- package/src/lib/fake-codex-app-server-bridge.mjs +340 -0
- package/src/lib/install-preflight.mjs +373 -0
- package/src/lib/interaction-resolution-service.mjs +185 -0
- package/src/lib/interaction-state.mjs +360 -0
- package/src/lib/launch-release-bar.mjs +158 -0
- package/src/lib/live-control-state.mjs +107 -0
- package/src/lib/live-payload-builder.mjs +298 -0
- package/src/lib/live-selection-transition-state.mjs +49 -0
- package/src/lib/live-transcript-state.mjs +549 -0
- package/src/lib/mobile-network-profile.mjs +39 -0
- package/src/lib/mock-codex-adapter.mjs +62 -0
- package/src/lib/operator-diagnostics.mjs +82 -0
- package/src/lib/repo-changes-service.mjs +527 -0
- package/src/lib/runtime-config.mjs +106 -0
- package/src/lib/selection-state-service.mjs +214 -0
- package/src/lib/session-store.mjs +355 -0
- package/src/lib/shared-room-state.mjs +473 -0
- package/src/lib/shared-selection-state.mjs +40 -0
- package/src/lib/sse-hub.mjs +35 -0
- package/src/lib/static-surface-service.mjs +71 -0
- package/src/lib/surface-access.mjs +189 -0
- package/src/lib/surface-presence-service.mjs +118 -0
- package/src/lib/surface-request-guard.mjs +52 -0
- package/src/lib/thread-sync-state.mjs +536 -0
- package/src/lib/watcher-lifecycle.mjs +287 -0
- 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
|
+
}
|