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,473 @@
|
|
|
1
|
+
function isoAt(nowMs) {
|
|
2
|
+
return new Date(nowMs).toISOString();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function normalizeSurfaceName(value) {
|
|
6
|
+
const nextSurface = String(value || "").trim().toLowerCase();
|
|
7
|
+
if (nextSurface === "host" || nextSurface === "agent" || nextSurface === "remote") {
|
|
8
|
+
return nextSurface;
|
|
9
|
+
}
|
|
10
|
+
return "remote";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isWriterSurface(surface) {
|
|
14
|
+
const nextSurface = normalizeSurfaceName(surface);
|
|
15
|
+
return nextSurface === "remote" || nextSurface === "agent";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function shortClientId(value) {
|
|
19
|
+
const normalized = String(value || "")
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9]+/g, "");
|
|
22
|
+
if (!normalized) {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return normalized.length > 4 ? normalized.slice(-4) : normalized;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function surfaceActorLabel({ surface = "", clientId = null } = {}) {
|
|
30
|
+
const nextSurface = normalizeSurfaceName(surface);
|
|
31
|
+
const base =
|
|
32
|
+
nextSurface === "host"
|
|
33
|
+
? "Host"
|
|
34
|
+
: nextSurface === "agent"
|
|
35
|
+
? "Agent"
|
|
36
|
+
: "Remote";
|
|
37
|
+
const suffix = shortClientId(clientId);
|
|
38
|
+
return suffix ? `${base} ${suffix}` : base;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function countSurfacePresence(surfacePresenceByClientId = {}, threadId, surface) {
|
|
42
|
+
const nextThreadId = String(threadId || "").trim();
|
|
43
|
+
const nextSurface = normalizeSurfaceName(surface);
|
|
44
|
+
if (!nextThreadId || !nextSurface) {
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let count = 0;
|
|
49
|
+
for (const presence of Object.values(surfacePresenceByClientId || {})) {
|
|
50
|
+
if (!presence?.threadId || presence.threadId !== nextThreadId) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (normalizeSurfaceName(presence.surface) !== nextSurface) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
count += 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return count;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function surfacePresenceState(presence) {
|
|
65
|
+
if (!presence) {
|
|
66
|
+
return "detached";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (presence.visible && presence.focused && presence.engaged) {
|
|
70
|
+
return "active";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (presence.visible && presence.focused) {
|
|
74
|
+
return "open";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (presence.visible) {
|
|
78
|
+
return "viewing";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return "background";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function upsertSurfacePresence(surfacePresenceByClientId = {}, payload = {}, { updatedAt = isoAt(Date.now()) } = {}) {
|
|
85
|
+
const clientId = String(payload.clientId || "").trim();
|
|
86
|
+
const threadId = String(payload.threadId || "").trim();
|
|
87
|
+
if (!clientId || !threadId) {
|
|
88
|
+
return {
|
|
89
|
+
changed: false,
|
|
90
|
+
nextPresenceByClientId: surfacePresenceByClientId,
|
|
91
|
+
nextPresence: null,
|
|
92
|
+
previousPresence: null
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const nextPresence = {
|
|
97
|
+
clientId,
|
|
98
|
+
engaged: Boolean(payload.engaged),
|
|
99
|
+
focused: Boolean(payload.focused),
|
|
100
|
+
label: normalizeSurfaceName(payload.surface),
|
|
101
|
+
surface: normalizeSurfaceName(payload.surface),
|
|
102
|
+
threadId,
|
|
103
|
+
updatedAt,
|
|
104
|
+
visible: payload.visible !== false
|
|
105
|
+
};
|
|
106
|
+
const previousPresence = surfacePresenceByClientId[clientId] || null;
|
|
107
|
+
|
|
108
|
+
const changed =
|
|
109
|
+
!previousPresence ||
|
|
110
|
+
previousPresence.surface !== nextPresence.surface ||
|
|
111
|
+
previousPresence.threadId !== nextPresence.threadId ||
|
|
112
|
+
previousPresence.visible !== nextPresence.visible ||
|
|
113
|
+
previousPresence.focused !== nextPresence.focused ||
|
|
114
|
+
previousPresence.engaged !== nextPresence.engaged;
|
|
115
|
+
|
|
116
|
+
if (!changed) {
|
|
117
|
+
return {
|
|
118
|
+
changed: false,
|
|
119
|
+
nextPresenceByClientId: surfacePresenceByClientId,
|
|
120
|
+
nextPresence,
|
|
121
|
+
previousPresence
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
changed: true,
|
|
127
|
+
nextPresence,
|
|
128
|
+
nextPresenceByClientId: {
|
|
129
|
+
...surfacePresenceByClientId,
|
|
130
|
+
[clientId]: nextPresence
|
|
131
|
+
},
|
|
132
|
+
previousPresence
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function removeSurfacePresence(surfacePresenceByClientId = {}, clientId) {
|
|
137
|
+
const id = String(clientId || "").trim();
|
|
138
|
+
const previousPresence = id ? surfacePresenceByClientId[id] || null : null;
|
|
139
|
+
if (!id || !previousPresence) {
|
|
140
|
+
return {
|
|
141
|
+
changed: false,
|
|
142
|
+
nextPresenceByClientId: surfacePresenceByClientId,
|
|
143
|
+
previousPresence: null
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const nextPresenceByClientId = {
|
|
148
|
+
...surfacePresenceByClientId
|
|
149
|
+
};
|
|
150
|
+
delete nextPresenceByClientId[id];
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
changed: true,
|
|
154
|
+
nextPresenceByClientId,
|
|
155
|
+
previousPresence
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function applySurfacePresenceUpdate(
|
|
160
|
+
surfacePresenceByClientId = {},
|
|
161
|
+
payload = {},
|
|
162
|
+
{ now = Date.now(), selectedThreadId = "" } = {}
|
|
163
|
+
) {
|
|
164
|
+
const updatedAt = isoAt(now);
|
|
165
|
+
const clientId = String(payload.clientId || "").trim();
|
|
166
|
+
const previousPresence = clientId ? surfacePresenceByClientId[clientId] || null : null;
|
|
167
|
+
const previousThreadId = String(previousPresence?.threadId || "").trim();
|
|
168
|
+
const previousSurface = normalizeSurfaceName(previousPresence?.surface);
|
|
169
|
+
const previousCount =
|
|
170
|
+
previousThreadId && previousSurface
|
|
171
|
+
? countSurfacePresence(surfacePresenceByClientId, previousThreadId, previousSurface)
|
|
172
|
+
: 0;
|
|
173
|
+
const nextThreadId = String(payload.threadId || selectedThreadId || "").trim();
|
|
174
|
+
const nextSurface = normalizeSurfaceName(payload.surface);
|
|
175
|
+
const nextCountBefore =
|
|
176
|
+
!payload.detach && nextThreadId && nextSurface
|
|
177
|
+
? countSurfacePresence(surfacePresenceByClientId, nextThreadId, nextSurface)
|
|
178
|
+
: 0;
|
|
179
|
+
|
|
180
|
+
const updateResult = payload.detach
|
|
181
|
+
? removeSurfacePresence(surfacePresenceByClientId, payload.clientId)
|
|
182
|
+
: upsertSurfacePresence(surfacePresenceByClientId, { ...payload, threadId: nextThreadId }, { updatedAt });
|
|
183
|
+
|
|
184
|
+
const events = [];
|
|
185
|
+
|
|
186
|
+
if (updateResult.changed) {
|
|
187
|
+
if (payload.detach) {
|
|
188
|
+
if (previousThreadId && previousSurface && previousCount === 1) {
|
|
189
|
+
events.push({
|
|
190
|
+
action: "detach",
|
|
191
|
+
at: updatedAt,
|
|
192
|
+
cause: "closed",
|
|
193
|
+
surface: previousSurface,
|
|
194
|
+
threadId: previousThreadId
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
if (
|
|
199
|
+
previousPresence &&
|
|
200
|
+
previousThreadId &&
|
|
201
|
+
previousSurface &&
|
|
202
|
+
(previousThreadId !== nextThreadId || previousSurface !== nextSurface) &&
|
|
203
|
+
previousCount === 1
|
|
204
|
+
) {
|
|
205
|
+
events.push({
|
|
206
|
+
action: "detach",
|
|
207
|
+
at: updatedAt,
|
|
208
|
+
cause: "moved",
|
|
209
|
+
surface: previousSurface,
|
|
210
|
+
threadId: previousThreadId
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (nextThreadId && nextSurface) {
|
|
215
|
+
const nextCountAfter = countSurfacePresence(updateResult.nextPresenceByClientId, nextThreadId, nextSurface);
|
|
216
|
+
if (nextCountBefore === 0 && nextCountAfter > 0) {
|
|
217
|
+
events.push({
|
|
218
|
+
action: "attach",
|
|
219
|
+
at: updatedAt,
|
|
220
|
+
cause: previousPresence ? "moved" : "opened",
|
|
221
|
+
surface: nextSurface,
|
|
222
|
+
threadId: nextThreadId
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
changed: updateResult.changed,
|
|
231
|
+
events,
|
|
232
|
+
nextPresenceByClientId: updateResult.nextPresenceByClientId,
|
|
233
|
+
nextPresence: updateResult.nextPresence || null,
|
|
234
|
+
previousPresence: updateResult.previousPresence || null
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function pruneStaleSurfacePresence(surfacePresenceByClientId = {}, { now = Date.now(), staleMs = 0 } = {}) {
|
|
239
|
+
const current = surfacePresenceByClientId || {};
|
|
240
|
+
let changed = false;
|
|
241
|
+
const nextPresenceByClientId = {};
|
|
242
|
+
const events = [];
|
|
243
|
+
|
|
244
|
+
for (const [clientId, presence] of Object.entries(current)) {
|
|
245
|
+
const updatedAtMs = new Date(presence.updatedAt || 0).getTime();
|
|
246
|
+
if (!updatedAtMs || now - updatedAtMs > staleMs) {
|
|
247
|
+
const threadId = String(presence?.threadId || "").trim();
|
|
248
|
+
const surface = normalizeSurfaceName(presence?.surface);
|
|
249
|
+
if (threadId && countSurfacePresence(current, threadId, surface) === 1) {
|
|
250
|
+
events.push({
|
|
251
|
+
action: "detach",
|
|
252
|
+
at: isoAt(now),
|
|
253
|
+
cause: "stale",
|
|
254
|
+
surface,
|
|
255
|
+
threadId
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
changed = true;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
nextPresenceByClientId[clientId] = presence;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
changed,
|
|
267
|
+
events,
|
|
268
|
+
nextPresenceByClientId: changed ? nextPresenceByClientId : current
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function buildSelectedAttachments(surfacePresenceByClientId = {}, threadId = null) {
|
|
273
|
+
const nextThreadId = String(threadId || "").trim();
|
|
274
|
+
if (!nextThreadId) {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const grouped = new Map();
|
|
279
|
+
for (const presence of Object.values(surfacePresenceByClientId || {})) {
|
|
280
|
+
if (!presence?.threadId || presence.threadId !== nextThreadId) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const surface = normalizeSurfaceName(presence.surface);
|
|
285
|
+
const current = grouped.get(surface);
|
|
286
|
+
if (!current) {
|
|
287
|
+
grouped.set(surface, {
|
|
288
|
+
count: 1,
|
|
289
|
+
label: surface,
|
|
290
|
+
state: surfacePresenceState(presence),
|
|
291
|
+
surface,
|
|
292
|
+
updatedAt: presence.updatedAt || null
|
|
293
|
+
});
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const currentUpdatedAtMs = new Date(current.updatedAt || 0).getTime();
|
|
298
|
+
const nextUpdatedAtMs = new Date(presence.updatedAt || 0).getTime();
|
|
299
|
+
grouped.set(surface, {
|
|
300
|
+
...current,
|
|
301
|
+
count: current.count + 1,
|
|
302
|
+
state: nextUpdatedAtMs >= currentUpdatedAtMs ? surfacePresenceState(presence) : current.state,
|
|
303
|
+
updatedAt: nextUpdatedAtMs >= currentUpdatedAtMs ? presence.updatedAt || null : current.updatedAt
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return [...grouped.values()]
|
|
308
|
+
.sort((a, b) => {
|
|
309
|
+
const order = { remote: 10, host: 20 };
|
|
310
|
+
const delta = (order[a.surface] || 99) - (order[b.surface] || 99);
|
|
311
|
+
if (delta !== 0) {
|
|
312
|
+
return delta;
|
|
313
|
+
}
|
|
314
|
+
return String(a.label || "").localeCompare(String(b.label || ""));
|
|
315
|
+
})
|
|
316
|
+
.map(({ updatedAt: _updatedAt, ...attachment }) => attachment);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function setControlLease({
|
|
320
|
+
clientId = null,
|
|
321
|
+
owner = "remote",
|
|
322
|
+
reason = "compose",
|
|
323
|
+
source = "remote",
|
|
324
|
+
threadId = null,
|
|
325
|
+
ttlMs,
|
|
326
|
+
now = Date.now()
|
|
327
|
+
} = {}) {
|
|
328
|
+
if (!threadId) {
|
|
329
|
+
throw new Error("No live session selected.");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
acquiredAt: isoAt(now),
|
|
334
|
+
expiresAt: isoAt(now + ttlMs),
|
|
335
|
+
ownerClientId: clientId ? String(clientId).trim() : null,
|
|
336
|
+
ownerLabel: surfaceActorLabel({ surface: owner || source, clientId }),
|
|
337
|
+
owner,
|
|
338
|
+
reason,
|
|
339
|
+
source,
|
|
340
|
+
threadId
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
export function getControlLeaseForThread(lease, threadId = null, { now = Date.now() } = {}) {
|
|
345
|
+
if (!lease) {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (lease.expiresAt && new Date(lease.expiresAt).getTime() <= now) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (threadId && lease.threadId !== threadId) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return lease;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function clearControlLease(lease, { threadId = null, now = Date.now() } = {}) {
|
|
361
|
+
const current = getControlLeaseForThread(lease, null, { now });
|
|
362
|
+
if (!current) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (threadId && current.threadId !== threadId) {
|
|
367
|
+
return current;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function renewControlLease({
|
|
374
|
+
lease,
|
|
375
|
+
clientId = null,
|
|
376
|
+
owner = null,
|
|
377
|
+
reason = null,
|
|
378
|
+
source = null,
|
|
379
|
+
threadId = null,
|
|
380
|
+
ttlMs,
|
|
381
|
+
now = Date.now()
|
|
382
|
+
} = {}) {
|
|
383
|
+
const current = getControlLeaseForThread(lease, threadId, { now });
|
|
384
|
+
if (!current) {
|
|
385
|
+
throw new Error("Remote control is not active.");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const nextClientId = clientId ? String(clientId).trim() : current.ownerClientId || null;
|
|
389
|
+
const nextOwner = owner || current.owner || "remote";
|
|
390
|
+
const nextSource = source || current.source || "remote";
|
|
391
|
+
|
|
392
|
+
return {
|
|
393
|
+
...current,
|
|
394
|
+
expiresAt: isoAt(now + ttlMs),
|
|
395
|
+
ownerClientId: nextClientId,
|
|
396
|
+
ownerLabel: surfaceActorLabel({
|
|
397
|
+
surface: nextOwner || nextSource,
|
|
398
|
+
clientId: nextClientId
|
|
399
|
+
}),
|
|
400
|
+
owner: nextOwner,
|
|
401
|
+
reason: reason || current.reason || "compose",
|
|
402
|
+
source: nextSource
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export function ensureControlActionAllowed({
|
|
407
|
+
action = "claim",
|
|
408
|
+
lease,
|
|
409
|
+
threadId,
|
|
410
|
+
source = "remote",
|
|
411
|
+
clientId = null,
|
|
412
|
+
now = Date.now()
|
|
413
|
+
} = {}) {
|
|
414
|
+
const current = getControlLeaseForThread(lease, threadId, { now });
|
|
415
|
+
const nextSource = normalizeSurfaceName(source);
|
|
416
|
+
if (!current || !isWriterSurface(nextSource)) {
|
|
417
|
+
return current;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const nextClientId = String(clientId || "").trim();
|
|
421
|
+
const currentOwner = normalizeSurfaceName(current.owner || current.source || "");
|
|
422
|
+
const currentOwnerLabel =
|
|
423
|
+
current.ownerLabel || surfaceActorLabel({ surface: current.owner || current.source, clientId: current.ownerClientId });
|
|
424
|
+
|
|
425
|
+
if (currentOwner === nextSource) {
|
|
426
|
+
if (nextClientId && current.ownerClientId && current.ownerClientId === nextClientId) {
|
|
427
|
+
return current;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
throw new Error(`Another ${nextSource} surface currently holds control for this channel.`);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
throw new Error(`${currentOwnerLabel} currently holds control for this channel.`);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function ensureRemoteControlLease({
|
|
437
|
+
lease,
|
|
438
|
+
threadId,
|
|
439
|
+
source = "remote",
|
|
440
|
+
clientId = null,
|
|
441
|
+
ttlMs,
|
|
442
|
+
now = Date.now()
|
|
443
|
+
} = {}) {
|
|
444
|
+
ensureControlActionAllowed({
|
|
445
|
+
action: "renew",
|
|
446
|
+
lease,
|
|
447
|
+
threadId,
|
|
448
|
+
source,
|
|
449
|
+
clientId,
|
|
450
|
+
now
|
|
451
|
+
});
|
|
452
|
+
const current = getControlLeaseForThread(lease, threadId, { now });
|
|
453
|
+
const nextSource = normalizeSurfaceName(source);
|
|
454
|
+
if (!current || current.owner !== source) {
|
|
455
|
+
throw new Error(`Take control before sending from the ${nextSource}.`);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const nextClientId = String(clientId || "").trim();
|
|
459
|
+
if (nextClientId && current.ownerClientId && current.ownerClientId !== nextClientId) {
|
|
460
|
+
throw new Error(`Another ${nextSource} surface currently holds control for this channel.`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return renewControlLease({
|
|
464
|
+
clientId: nextClientId || current.ownerClientId || null,
|
|
465
|
+
lease: current,
|
|
466
|
+
now,
|
|
467
|
+
owner: current.owner,
|
|
468
|
+
reason: current.reason || "compose",
|
|
469
|
+
source: current.source || source,
|
|
470
|
+
threadId,
|
|
471
|
+
ttlMs
|
|
472
|
+
});
|
|
473
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function applySharedSelectionState(
|
|
2
|
+
state = {},
|
|
3
|
+
{
|
|
4
|
+
cwd = null,
|
|
5
|
+
source = "remote",
|
|
6
|
+
threadId = null,
|
|
7
|
+
threads = []
|
|
8
|
+
} = {}
|
|
9
|
+
) {
|
|
10
|
+
const previousThreadId = state.selectedThreadId || null;
|
|
11
|
+
const nextSelectedProjectCwd = cwd || state.selectedProjectCwd || "";
|
|
12
|
+
|
|
13
|
+
let nextSelectedThreadId = previousThreadId;
|
|
14
|
+
if (threadId) {
|
|
15
|
+
nextSelectedThreadId = threadId;
|
|
16
|
+
} else if (cwd) {
|
|
17
|
+
const nextThread = (threads || []).find((candidate) => candidate.cwd === cwd) || null;
|
|
18
|
+
nextSelectedThreadId = nextThread?.id || null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const threadChanged = nextSelectedThreadId !== previousThreadId;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
nextState: {
|
|
25
|
+
selectedProjectCwd: nextSelectedProjectCwd,
|
|
26
|
+
selectedThreadId: nextSelectedThreadId,
|
|
27
|
+
selectionSource: source,
|
|
28
|
+
selectedThreadSnapshot: threadChanged ? null : state.selectedThreadSnapshot || null,
|
|
29
|
+
turnDiff:
|
|
30
|
+
state.turnDiff && state.turnDiff.threadId === nextSelectedThreadId
|
|
31
|
+
? state.turnDiff
|
|
32
|
+
: null,
|
|
33
|
+
writeLock:
|
|
34
|
+
state.writeLock && state.writeLock.threadId === nextSelectedThreadId
|
|
35
|
+
? state.writeLock
|
|
36
|
+
: null
|
|
37
|
+
},
|
|
38
|
+
threadChanged
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function createSseHub() {
|
|
2
|
+
const clients = new Set();
|
|
3
|
+
|
|
4
|
+
function writeEvent(res, event, payload) {
|
|
5
|
+
res.write(`event: ${event}\n`);
|
|
6
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
broadcast(event, payload) {
|
|
11
|
+
for (const res of clients) {
|
|
12
|
+
writeEvent(res, event, payload);
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
close(res) {
|
|
16
|
+
clients.delete(res);
|
|
17
|
+
},
|
|
18
|
+
open(res, initialEvents = [], headers = {}) {
|
|
19
|
+
res.writeHead(200, {
|
|
20
|
+
"Cache-Control": "no-store",
|
|
21
|
+
Connection: "keep-alive",
|
|
22
|
+
"Content-Type": "text/event-stream",
|
|
23
|
+
...headers
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
for (const entry of initialEvents) {
|
|
27
|
+
if (!entry?.event) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
writeEvent(res, entry.event, entry.payload);
|
|
31
|
+
}
|
|
32
|
+
clients.add(res);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import { injectSurfaceBootstrap } from "./surface-access.mjs";
|
|
5
|
+
import { canServeSurfaceBootstrap } from "./surface-request-guard.mjs";
|
|
6
|
+
|
|
7
|
+
export function createStaticSurfaceService({
|
|
8
|
+
exposeHostSurface = false,
|
|
9
|
+
issueSurfaceBootstrap,
|
|
10
|
+
mimeTypes = {},
|
|
11
|
+
publicDir,
|
|
12
|
+
readFileFn = readFile,
|
|
13
|
+
sendJson
|
|
14
|
+
} = {}) {
|
|
15
|
+
if (!publicDir) {
|
|
16
|
+
throw new Error("createStaticSurfaceService requires a publicDir.");
|
|
17
|
+
}
|
|
18
|
+
if (typeof issueSurfaceBootstrap !== "function") {
|
|
19
|
+
throw new Error("createStaticSurfaceService requires issueSurfaceBootstrap.");
|
|
20
|
+
}
|
|
21
|
+
if (typeof sendJson !== "function") {
|
|
22
|
+
throw new Error("createStaticSurfaceService requires sendJson.");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function serveStatic(req, res, pathname) {
|
|
26
|
+
const relativePath = pathname === "/" ? "remote.html" : pathname.slice(1);
|
|
27
|
+
const filePath = path.join(publicDir, relativePath);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const ext = path.extname(filePath);
|
|
31
|
+
const shouldInjectBootstrap =
|
|
32
|
+
relativePath === "remote.html" || relativePath === "host.html";
|
|
33
|
+
if (
|
|
34
|
+
shouldInjectBootstrap &&
|
|
35
|
+
!canServeSurfaceBootstrap({
|
|
36
|
+
exposeHostSurface,
|
|
37
|
+
localAddress: req.socket?.localAddress || "",
|
|
38
|
+
pathname,
|
|
39
|
+
remoteAddress: req.socket?.remoteAddress || ""
|
|
40
|
+
})
|
|
41
|
+
) {
|
|
42
|
+
sendJson(res, 403, {
|
|
43
|
+
error: "Host surface is restricted to loopback unless DEXTUNNEL_EXPOSE_HOST_SURFACE is enabled."
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const data = shouldInjectBootstrap
|
|
49
|
+
? injectSurfaceBootstrap(
|
|
50
|
+
await readFileFn(filePath, "utf8"),
|
|
51
|
+
issueSurfaceBootstrap(relativePath === "host.html" ? "host" : "remote")
|
|
52
|
+
)
|
|
53
|
+
: await readFileFn(filePath);
|
|
54
|
+
res.writeHead(200, {
|
|
55
|
+
"Cache-Control": "no-store, max-age=0",
|
|
56
|
+
"Content-Type": mimeTypes[ext] || "application/octet-stream",
|
|
57
|
+
Pragma: "no-cache"
|
|
58
|
+
});
|
|
59
|
+
res.end(data);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
sendJson(res, 404, {
|
|
62
|
+
error: `Not found: ${relativePath}`,
|
|
63
|
+
detail: error.message
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
serveStatic
|
|
70
|
+
};
|
|
71
|
+
}
|