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,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearControlLease,
|
|
3
|
+
ensureControlActionAllowed,
|
|
4
|
+
renewControlLease,
|
|
5
|
+
setControlLease
|
|
6
|
+
} from "./shared-room-state.mjs";
|
|
7
|
+
|
|
8
|
+
export function applyLiveControlAction({
|
|
9
|
+
action = "claim",
|
|
10
|
+
clientId = null,
|
|
11
|
+
existingLease = null,
|
|
12
|
+
owner = null,
|
|
13
|
+
reason = null,
|
|
14
|
+
source = "remote",
|
|
15
|
+
threadId = null,
|
|
16
|
+
ttlMs,
|
|
17
|
+
now = Date.now()
|
|
18
|
+
} = {}) {
|
|
19
|
+
const nextThreadId = String(threadId || "").trim();
|
|
20
|
+
const nextClientId = String(clientId || "").trim() || null;
|
|
21
|
+
const nextSource = source || "remote";
|
|
22
|
+
|
|
23
|
+
if (!nextThreadId) {
|
|
24
|
+
throw new Error("No live session selected.");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (nextSource === "remote" || nextSource === "agent") {
|
|
28
|
+
ensureControlActionAllowed({
|
|
29
|
+
action,
|
|
30
|
+
clientId: nextClientId,
|
|
31
|
+
lease: existingLease,
|
|
32
|
+
source: nextSource,
|
|
33
|
+
threadId: nextThreadId,
|
|
34
|
+
now
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (action === "release") {
|
|
39
|
+
const previousLease = existingLease || null;
|
|
40
|
+
return {
|
|
41
|
+
lease: clearControlLease(existingLease, { threadId: nextThreadId, now }),
|
|
42
|
+
recordEvent: Boolean(previousLease),
|
|
43
|
+
event: previousLease
|
|
44
|
+
? {
|
|
45
|
+
action: "release",
|
|
46
|
+
actor: nextSource,
|
|
47
|
+
actorClientId: nextClientId,
|
|
48
|
+
cause: "released",
|
|
49
|
+
owner: previousLease.owner || null,
|
|
50
|
+
ownerClientId: previousLease.ownerClientId || null,
|
|
51
|
+
ownerLabel: previousLease.ownerLabel || null,
|
|
52
|
+
reason: previousLease.reason || null,
|
|
53
|
+
source: previousLease.source || null,
|
|
54
|
+
threadId: previousLease.threadId || nextThreadId
|
|
55
|
+
}
|
|
56
|
+
: null
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (action === "renew") {
|
|
61
|
+
return {
|
|
62
|
+
lease: renewControlLease({
|
|
63
|
+
clientId: nextClientId,
|
|
64
|
+
lease: existingLease,
|
|
65
|
+
now,
|
|
66
|
+
owner: owner || nextSource,
|
|
67
|
+
reason,
|
|
68
|
+
source: nextSource,
|
|
69
|
+
threadId: nextThreadId,
|
|
70
|
+
ttlMs
|
|
71
|
+
}),
|
|
72
|
+
recordEvent: false,
|
|
73
|
+
event: null
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (action === "claim") {
|
|
78
|
+
const lease = setControlLease({
|
|
79
|
+
clientId: nextClientId,
|
|
80
|
+
now,
|
|
81
|
+
owner: owner || nextSource,
|
|
82
|
+
reason: reason || "compose",
|
|
83
|
+
source: nextSource,
|
|
84
|
+
threadId: nextThreadId,
|
|
85
|
+
ttlMs
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
lease,
|
|
90
|
+
recordEvent: true,
|
|
91
|
+
event: {
|
|
92
|
+
action: "claim",
|
|
93
|
+
actor: nextSource,
|
|
94
|
+
actorClientId: nextClientId,
|
|
95
|
+
cause: "claimed",
|
|
96
|
+
owner: lease.owner || null,
|
|
97
|
+
ownerClientId: lease.ownerClientId || null,
|
|
98
|
+
ownerLabel: lease.ownerLabel || null,
|
|
99
|
+
reason: lease.reason || null,
|
|
100
|
+
source: lease.source || null,
|
|
101
|
+
threadId: lease.threadId || nextThreadId
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new Error(`Unsupported control action: ${action}`);
|
|
107
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
function isOriginEligibleEntry(entry) {
|
|
2
|
+
if (!entry) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (entry.role === "user") {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return entry.role === "assistant" && entry.kind !== "commentary";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createLivePayloadBuilder(deps) {
|
|
14
|
+
const {
|
|
15
|
+
advisoryParticipantForThread,
|
|
16
|
+
ADVISORY_PARTICIPANT_IDS = [],
|
|
17
|
+
bestConversationLabel,
|
|
18
|
+
bestThreadLabel,
|
|
19
|
+
buildBridgeStatus,
|
|
20
|
+
buildParticipant,
|
|
21
|
+
buildSelectedAgentRoomState,
|
|
22
|
+
buildSelectedAttachments,
|
|
23
|
+
buildSelectedCompanionState,
|
|
24
|
+
getPendingInteractionForSelectedThread,
|
|
25
|
+
liveState,
|
|
26
|
+
looksLikeTopicNoise,
|
|
27
|
+
normalizeLane,
|
|
28
|
+
projectLabel,
|
|
29
|
+
pruneAllCompanionWakeups,
|
|
30
|
+
pruneStaleSurfacePresence,
|
|
31
|
+
repoObjective,
|
|
32
|
+
selectedThreadSummary,
|
|
33
|
+
slugifyChannelName,
|
|
34
|
+
summarizeThread,
|
|
35
|
+
trimTopicText
|
|
36
|
+
} = deps;
|
|
37
|
+
|
|
38
|
+
function inferEntryLane(entry, thread) {
|
|
39
|
+
const explicitLane = normalizeLane(entry?.origin);
|
|
40
|
+
if (explicitLane) {
|
|
41
|
+
return explicitLane;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (entry?.role !== "user") {
|
|
45
|
+
return "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (thread?.source === "vscode") {
|
|
49
|
+
return "desktop";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (thread?.source === "cli") {
|
|
53
|
+
return "external";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function participantForEntry(entry, thread) {
|
|
60
|
+
const lane = inferEntryLane(entry, thread);
|
|
61
|
+
|
|
62
|
+
if (entry?.kind === "pending" || entry?.kind === "queued") {
|
|
63
|
+
return buildParticipant("remote");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (entry?.role === "assistant" && entry?.kind === "commentary") {
|
|
67
|
+
return buildParticipant("updates");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (entry?.role === "assistant") {
|
|
71
|
+
return buildParticipant("codex");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (entry?.role === "user") {
|
|
75
|
+
if (lane === "remote") {
|
|
76
|
+
return buildParticipant("remote");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (lane === "desktop") {
|
|
80
|
+
return buildParticipant("desktop");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (lane === "oracle") {
|
|
84
|
+
return buildParticipant("oracle");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (lane === "gemini") {
|
|
88
|
+
return buildParticipant("gemini");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (lane === "external") {
|
|
92
|
+
return buildParticipant("user", {
|
|
93
|
+
label: "external",
|
|
94
|
+
lane: "external",
|
|
95
|
+
token: "user"
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return buildParticipant("user");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (entry?.role === "tool") {
|
|
103
|
+
return buildParticipant("tools");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return buildParticipant("system");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildChannelTopic(snapshot) {
|
|
110
|
+
const thread = snapshot?.thread || null;
|
|
111
|
+
const objective = repoObjective(thread?.cwd || "");
|
|
112
|
+
if (objective && !looksLikeTopicNoise(objective)) {
|
|
113
|
+
return objective;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (thread?.preview && !looksLikeTopicNoise(thread.preview)) {
|
|
117
|
+
return trimTopicText(thread.preview);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const latestConversation = bestConversationLabel(snapshot, 120);
|
|
121
|
+
if (!latestConversation) {
|
|
122
|
+
return "";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return trimTopicText(latestConversation, 120);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildSelectedChannel(snapshot) {
|
|
129
|
+
const thread = snapshot?.thread || selectedThreadSummary() || null;
|
|
130
|
+
const cwd = thread?.cwd || liveState.selectedProjectCwd || process.cwd();
|
|
131
|
+
const label = bestThreadLabel(
|
|
132
|
+
{
|
|
133
|
+
...thread,
|
|
134
|
+
cwd
|
|
135
|
+
},
|
|
136
|
+
snapshot,
|
|
137
|
+
{ selected: true }
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
channelId: thread?.id || liveState.selectedThreadId || null,
|
|
142
|
+
channelLabel: label,
|
|
143
|
+
channelSlug: `#${slugifyChannelName(label)}`,
|
|
144
|
+
source: thread?.source || null,
|
|
145
|
+
serverId: cwd,
|
|
146
|
+
serverLabel: projectLabel(cwd),
|
|
147
|
+
topic: buildChannelTopic(snapshot)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function buildParticipants(snapshot) {
|
|
152
|
+
const participants = new Map();
|
|
153
|
+
const threadId = snapshot?.thread?.id || null;
|
|
154
|
+
|
|
155
|
+
for (const entry of snapshot?.transcript || []) {
|
|
156
|
+
const participant = participantForEntry(entry, snapshot?.thread);
|
|
157
|
+
participants.set(participant.id, participant);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (snapshot?.thread?.source === "vscode" && !participants.has("desktop")) {
|
|
161
|
+
participants.set("desktop", buildParticipant("desktop"));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!participants.has("remote")) {
|
|
165
|
+
participants.set("remote", buildParticipant("remote"));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!participants.has("codex")) {
|
|
169
|
+
participants.set("codex", buildParticipant("codex"));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const advisorId of ADVISORY_PARTICIPANT_IDS) {
|
|
173
|
+
participants.set(advisorId, advisoryParticipantForThread(advisorId, threadId));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return [...participants.values()].sort((a, b) => {
|
|
177
|
+
const orderDelta = (a.sortOrder || 999) - (b.sortOrder || 999);
|
|
178
|
+
if (orderDelta !== 0) {
|
|
179
|
+
return orderDelta;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return String(a.label || a.id || "").localeCompare(String(b.label || b.id || ""));
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function applyTurnOrigins(snapshot) {
|
|
187
|
+
const threadId = snapshot?.thread?.id || null;
|
|
188
|
+
if (!threadId || !Array.isArray(snapshot?.transcript) || snapshot.transcript.length === 0) {
|
|
189
|
+
return snapshot;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const turnOrigins = liveState.turnOriginsByThreadId?.[threadId];
|
|
193
|
+
if (!turnOrigins) {
|
|
194
|
+
return snapshot;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let changed = false;
|
|
198
|
+
const transcript = snapshot.transcript.map((entry) => {
|
|
199
|
+
const origin =
|
|
200
|
+
isOriginEligibleEntry(entry) && entry.turnId
|
|
201
|
+
? turnOrigins[entry.turnId] || null
|
|
202
|
+
: null;
|
|
203
|
+
const currentOrigin = entry.origin || null;
|
|
204
|
+
|
|
205
|
+
if (currentOrigin === origin) {
|
|
206
|
+
return entry;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
changed = true;
|
|
210
|
+
if (!origin) {
|
|
211
|
+
const { origin: _origin, ...rest } = entry;
|
|
212
|
+
return rest;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
...entry,
|
|
217
|
+
origin
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (!changed) {
|
|
222
|
+
return snapshot;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
...snapshot,
|
|
227
|
+
transcript
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function decorateSnapshot(snapshot) {
|
|
232
|
+
const withOrigins = applyTurnOrigins(snapshot);
|
|
233
|
+
const thread = withOrigins?.thread || null;
|
|
234
|
+
|
|
235
|
+
if (!thread || !Array.isArray(withOrigins?.transcript)) {
|
|
236
|
+
return withOrigins;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const transcript = withOrigins.transcript.map((entry) => {
|
|
240
|
+
const lane = inferEntryLane(entry, thread);
|
|
241
|
+
const participant = participantForEntry(entry, thread);
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
...entry,
|
|
245
|
+
lane,
|
|
246
|
+
participant
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const nextSnapshot = {
|
|
251
|
+
...withOrigins,
|
|
252
|
+
transcript
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
...nextSnapshot,
|
|
257
|
+
channel: buildSelectedChannel(nextSnapshot),
|
|
258
|
+
participants: buildParticipants(nextSnapshot)
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function buildLivePayload() {
|
|
263
|
+
pruneStaleSurfacePresence();
|
|
264
|
+
pruneAllCompanionWakeups();
|
|
265
|
+
const selectedThreadSnapshot = decorateSnapshot(liveState.selectedThreadSnapshot);
|
|
266
|
+
const selectedThreadId = selectedThreadSnapshot?.thread?.id || liveState.selectedThreadId || null;
|
|
267
|
+
const selectedAttachments = buildSelectedAttachments(selectedThreadId);
|
|
268
|
+
const selectedCompanion = buildSelectedCompanionState(selectedThreadId);
|
|
269
|
+
const selectedAgentRoom = buildSelectedAgentRoomState(selectedThreadId);
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
pendingInteraction: getPendingInteractionForSelectedThread(),
|
|
273
|
+
participants: selectedThreadSnapshot?.participants || [],
|
|
274
|
+
selectedAgentRoom,
|
|
275
|
+
selectedCompanion,
|
|
276
|
+
selectedAttachments,
|
|
277
|
+
selectedChannel: selectedThreadSnapshot?.channel || buildSelectedChannel(null),
|
|
278
|
+
selectedProjectCwd: liveState.selectedProjectCwd,
|
|
279
|
+
selectionSource: liveState.selectionSource,
|
|
280
|
+
selectedThreadId: liveState.selectedThreadId,
|
|
281
|
+
selectedThreadSnapshot,
|
|
282
|
+
status: buildBridgeStatus(),
|
|
283
|
+
turnDiff: liveState.turnDiff,
|
|
284
|
+
threads: liveState.threads.map(summarizeThread)
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
applyTurnOrigins,
|
|
290
|
+
buildChannelTopic,
|
|
291
|
+
buildLivePayload,
|
|
292
|
+
buildParticipants,
|
|
293
|
+
buildSelectedChannel,
|
|
294
|
+
decorateSnapshot,
|
|
295
|
+
inferEntryLane,
|
|
296
|
+
participantForEntry
|
|
297
|
+
};
|
|
298
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { clearControlLease } from "./shared-room-state.mjs";
|
|
2
|
+
import { applySharedSelectionState } from "./shared-selection-state.mjs";
|
|
3
|
+
|
|
4
|
+
export function applyLiveSelectionTransition(
|
|
5
|
+
state = {},
|
|
6
|
+
{
|
|
7
|
+
cwd = null,
|
|
8
|
+
source = "remote",
|
|
9
|
+
threadId = null,
|
|
10
|
+
threads = []
|
|
11
|
+
} = {},
|
|
12
|
+
{ now = Date.now() } = {}
|
|
13
|
+
) {
|
|
14
|
+
const previousThreadId = state.selectedThreadId || null;
|
|
15
|
+
const selection = applySharedSelectionState(
|
|
16
|
+
{
|
|
17
|
+
selectedProjectCwd: state.selectedProjectCwd || "",
|
|
18
|
+
selectedThreadId: previousThreadId,
|
|
19
|
+
selectedThreadSnapshot: state.selectedThreadSnapshot || null,
|
|
20
|
+
selectionSource: state.selectionSource || "remote",
|
|
21
|
+
turnDiff: state.turnDiff || null,
|
|
22
|
+
writeLock: state.writeLock || null
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
cwd,
|
|
26
|
+
source,
|
|
27
|
+
threadId,
|
|
28
|
+
threads
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const interactionFlow = selection.threadChanged ? null : state.interactionFlow || null;
|
|
33
|
+
const controlLease = selection.threadChanged
|
|
34
|
+
? clearControlLease(state.controlLease || null, { now, threadId: previousThreadId || null })
|
|
35
|
+
: state.controlLease || null;
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
cleared: {
|
|
39
|
+
controlLease: selection.threadChanged && Boolean(state.controlLease),
|
|
40
|
+
interactionFlow: selection.threadChanged && Boolean(state.interactionFlow)
|
|
41
|
+
},
|
|
42
|
+
nextState: {
|
|
43
|
+
...selection.nextState,
|
|
44
|
+
controlLease,
|
|
45
|
+
interactionFlow
|
|
46
|
+
},
|
|
47
|
+
threadChanged: selection.threadChanged
|
|
48
|
+
};
|
|
49
|
+
}
|