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,28 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta http-equiv="refresh" content="0; url=/">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<title>Dextunnel</title>
|
|
8
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
|
9
|
+
<link rel="stylesheet" href="/styles.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body class="landing-shell">
|
|
12
|
+
<main class="landing-grid">
|
|
13
|
+
<section class="panel hero-panel">
|
|
14
|
+
<p class="eyebrow">Dextunnel / remote</p>
|
|
15
|
+
<h1>Opening the Dextunnel remote.</h1>
|
|
16
|
+
<p class="lede">
|
|
17
|
+
The browser landing page has been retired. Dextunnel now opens the main remote surface at the root URL.
|
|
18
|
+
</p>
|
|
19
|
+
<div class="hero-actions">
|
|
20
|
+
<a class="cta" href="/">Open remote</a>
|
|
21
|
+
</div>
|
|
22
|
+
</section>
|
|
23
|
+
</main>
|
|
24
|
+
<script>
|
|
25
|
+
window.location.replace("/");
|
|
26
|
+
</script>
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|
package/public/index.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const nodes = {
|
|
2
|
+
badge: document.querySelector("#preflight-badge"),
|
|
3
|
+
checks: document.querySelector("#preflight-checks"),
|
|
4
|
+
nextSteps: document.querySelector("#preflight-next-steps"),
|
|
5
|
+
refresh: document.querySelector("#preflight-refresh"),
|
|
6
|
+
summary: document.querySelector("#preflight-summary")
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function setBadge(status, label) {
|
|
10
|
+
if (!nodes.badge) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
nodes.badge.textContent = label;
|
|
15
|
+
nodes.badge.className = "landing-status-badge";
|
|
16
|
+
nodes.badge.classList.add(`is-${status}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function setSummary(message) {
|
|
20
|
+
if (nodes.summary) {
|
|
21
|
+
nodes.summary.textContent = message;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderChecks(checks) {
|
|
26
|
+
if (!nodes.checks) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
nodes.checks.textContent = "";
|
|
31
|
+
for (const check of checks || []) {
|
|
32
|
+
const article = document.createElement("article");
|
|
33
|
+
article.className = `preflight-card is-${check.severity || "warning"}`;
|
|
34
|
+
|
|
35
|
+
const label = document.createElement("p");
|
|
36
|
+
label.className = "preflight-card-label";
|
|
37
|
+
label.textContent = check.label || "Check";
|
|
38
|
+
|
|
39
|
+
const detail = document.createElement("p");
|
|
40
|
+
detail.className = "preflight-card-detail";
|
|
41
|
+
detail.textContent = check.detail || "";
|
|
42
|
+
|
|
43
|
+
article.append(label, detail);
|
|
44
|
+
nodes.checks.append(article);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderNextSteps(steps) {
|
|
49
|
+
if (!nodes.nextSteps) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
nodes.nextSteps.textContent = "";
|
|
54
|
+
for (const step of steps || []) {
|
|
55
|
+
const item = document.createElement("li");
|
|
56
|
+
item.textContent = step;
|
|
57
|
+
nodes.nextSteps.append(item);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function loadPreflight() {
|
|
62
|
+
setBadge("loading", "Checking");
|
|
63
|
+
setSummary("Running a local Dextunnel preflight...");
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const response = await fetch("/api/preflight?warmup=1", {
|
|
67
|
+
headers: {
|
|
68
|
+
Accept: "application/json"
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
throw new Error(`Preflight returned HTTP ${response.status}.`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const payload = await response.json();
|
|
76
|
+
const status = payload.status || "warning";
|
|
77
|
+
setBadge(status, status === "ready" ? "Ready" : status === "error" ? "Needs attention" : "Almost there");
|
|
78
|
+
setSummary(payload.summary || "Preflight complete.");
|
|
79
|
+
renderChecks(payload.checks || []);
|
|
80
|
+
renderNextSteps(payload.nextSteps || []);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
setBadge("error", "Unavailable");
|
|
83
|
+
setSummary(String(error?.message || error || "Failed to load the Dextunnel preflight."));
|
|
84
|
+
renderChecks([
|
|
85
|
+
{
|
|
86
|
+
detail: "Try reloading this page or run npm run doctor in the repo root.",
|
|
87
|
+
label: "Preflight",
|
|
88
|
+
severity: "error"
|
|
89
|
+
}
|
|
90
|
+
]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
nodes.refresh?.addEventListener("click", () => {
|
|
95
|
+
void loadPreflight();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
void loadPreflight();
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import {
|
|
2
|
+
planBootstrapRetry,
|
|
3
|
+
planStreamRecovery,
|
|
4
|
+
reconnectStreamState
|
|
5
|
+
} from "./live-bridge-retry-state.js";
|
|
6
|
+
|
|
7
|
+
export function createLiveBridgeLifecycleState({
|
|
8
|
+
bootstrapRetryBaseMs,
|
|
9
|
+
streamRecoveryBaseMs
|
|
10
|
+
} = {}) {
|
|
11
|
+
return {
|
|
12
|
+
bootstrapPromise: null,
|
|
13
|
+
bootstrapRetryBackoffMs: bootstrapRetryBaseMs,
|
|
14
|
+
bootstrapRetryTimer: null,
|
|
15
|
+
refreshAfterStreamReconnect: false,
|
|
16
|
+
refreshPromise: null,
|
|
17
|
+
stream: null,
|
|
18
|
+
streamRecoveryBackoffMs: streamRecoveryBaseMs,
|
|
19
|
+
streamRecoveryTimer: null,
|
|
20
|
+
streamState: "connecting"
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createLiveBridgeLifecycle({
|
|
25
|
+
state,
|
|
26
|
+
bootstrapRetry,
|
|
27
|
+
streamRecovery,
|
|
28
|
+
createEventSource,
|
|
29
|
+
createTimeout = (callback, delay) => window.setTimeout(callback, delay),
|
|
30
|
+
clearCreatedTimeout = (timer) => window.clearTimeout(timer),
|
|
31
|
+
getHasLiveState,
|
|
32
|
+
getVisible,
|
|
33
|
+
onBootstrapError,
|
|
34
|
+
onBootstrapStart,
|
|
35
|
+
onBootstrapSuccess,
|
|
36
|
+
onLive,
|
|
37
|
+
onRender = () => {},
|
|
38
|
+
onSnapshot,
|
|
39
|
+
onStreamError,
|
|
40
|
+
onStreamOpen,
|
|
41
|
+
requestBootstrap,
|
|
42
|
+
requestRefresh,
|
|
43
|
+
streamUrl
|
|
44
|
+
}) {
|
|
45
|
+
if (!state) {
|
|
46
|
+
throw new Error("createLiveBridgeLifecycle requires a mutable state object.");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const lifecycle = {
|
|
50
|
+
bootstrap,
|
|
51
|
+
closeStream,
|
|
52
|
+
ensureStream,
|
|
53
|
+
resetBootstrapRetry,
|
|
54
|
+
resetStreamRecovery,
|
|
55
|
+
resumeVisible,
|
|
56
|
+
refresh
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function resetStreamRecovery() {
|
|
60
|
+
if (state.streamRecoveryTimer) {
|
|
61
|
+
clearCreatedTimeout(state.streamRecoveryTimer);
|
|
62
|
+
state.streamRecoveryTimer = null;
|
|
63
|
+
}
|
|
64
|
+
state.streamRecoveryBackoffMs = streamRecovery.baseMs;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resetBootstrapRetry() {
|
|
68
|
+
if (state.bootstrapRetryTimer) {
|
|
69
|
+
clearCreatedTimeout(state.bootstrapRetryTimer);
|
|
70
|
+
state.bootstrapRetryTimer = null;
|
|
71
|
+
}
|
|
72
|
+
state.bootstrapRetryBackoffMs = bootstrapRetry.baseMs;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function closeStream() {
|
|
76
|
+
if (!state.stream) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
state.stream.close();
|
|
81
|
+
state.stream = null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function scheduleBootstrapRetry() {
|
|
85
|
+
const plan = planBootstrapRetry({
|
|
86
|
+
backoffMs: state.bootstrapRetryBackoffMs,
|
|
87
|
+
baseMs: bootstrapRetry.baseMs,
|
|
88
|
+
hasTimer: Boolean(state.bootstrapRetryTimer),
|
|
89
|
+
isVisible: getVisible(),
|
|
90
|
+
maxMs: bootstrapRetry.maxMs
|
|
91
|
+
});
|
|
92
|
+
if (!plan.schedule) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
state.bootstrapRetryBackoffMs = plan.nextBackoffMs;
|
|
97
|
+
state.bootstrapRetryTimer = createTimeout(() => {
|
|
98
|
+
state.bootstrapRetryTimer = null;
|
|
99
|
+
if (getVisible()) {
|
|
100
|
+
void lifecycle.bootstrap({ retrying: true });
|
|
101
|
+
lifecycle.ensureStream();
|
|
102
|
+
}
|
|
103
|
+
}, plan.delay);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function ensureStream({ force = false } = {}) {
|
|
107
|
+
if (state.stream && !force) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (force) {
|
|
112
|
+
closeStream();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
state.streamState = reconnectStreamState({ hasLiveState: getHasLiveState() });
|
|
116
|
+
|
|
117
|
+
const nextStream = createEventSource(streamUrl);
|
|
118
|
+
state.stream = nextStream;
|
|
119
|
+
|
|
120
|
+
nextStream.addEventListener("snapshot", (event) => {
|
|
121
|
+
if (state.stream !== nextStream) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
onSnapshot(JSON.parse(event.data));
|
|
126
|
+
onRender();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
nextStream.addEventListener("open", () => {
|
|
130
|
+
if (state.stream !== nextStream) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
state.streamState = "live";
|
|
135
|
+
resetStreamRecovery();
|
|
136
|
+
onStreamOpen?.();
|
|
137
|
+
if (state.refreshAfterStreamReconnect) {
|
|
138
|
+
state.refreshAfterStreamReconnect = false;
|
|
139
|
+
void lifecycle.refresh({ background: true });
|
|
140
|
+
}
|
|
141
|
+
onRender();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
nextStream.addEventListener("live", (event) => {
|
|
145
|
+
if (state.stream !== nextStream) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
onLive(JSON.parse(event.data));
|
|
150
|
+
onRender();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
nextStream.addEventListener("error", () => {
|
|
154
|
+
if (state.stream !== nextStream) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
state.streamState = reconnectStreamState({ hasLiveState: getHasLiveState() });
|
|
159
|
+
closeStream();
|
|
160
|
+
onStreamError?.();
|
|
161
|
+
scheduleStreamRecovery();
|
|
162
|
+
onRender();
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function scheduleStreamRecovery() {
|
|
167
|
+
const plan = planStreamRecovery({
|
|
168
|
+
backoffMs: state.streamRecoveryBackoffMs,
|
|
169
|
+
baseMs: streamRecovery.baseMs,
|
|
170
|
+
hasLiveState: getHasLiveState(),
|
|
171
|
+
hasTimer: Boolean(state.streamRecoveryTimer),
|
|
172
|
+
isVisible: getVisible(),
|
|
173
|
+
maxMs: streamRecovery.maxMs
|
|
174
|
+
});
|
|
175
|
+
if (!plan.schedule) {
|
|
176
|
+
state.streamState = plan.streamState;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
state.streamState = plan.streamState;
|
|
181
|
+
state.streamRecoveryBackoffMs = plan.nextBackoffMs;
|
|
182
|
+
state.streamRecoveryTimer = createTimeout(() => {
|
|
183
|
+
state.streamRecoveryTimer = null;
|
|
184
|
+
if (!getVisible()) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
state.refreshAfterStreamReconnect = plan.followupAction === "refresh";
|
|
189
|
+
lifecycle.ensureStream({ force: true });
|
|
190
|
+
if (plan.followupAction === "bootstrap") {
|
|
191
|
+
void lifecycle.bootstrap({ retrying: true });
|
|
192
|
+
}
|
|
193
|
+
}, plan.delay);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function bootstrap({ retrying = false } = {}) {
|
|
197
|
+
if (state.bootstrapPromise) {
|
|
198
|
+
return state.bootstrapPromise;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
onBootstrapStart?.({ retrying });
|
|
202
|
+
onRender();
|
|
203
|
+
|
|
204
|
+
state.bootstrapPromise = requestBootstrap()
|
|
205
|
+
.then(({ snapshot, live }) => {
|
|
206
|
+
onBootstrapSuccess({ live, retrying, snapshot });
|
|
207
|
+
state.streamState = "live";
|
|
208
|
+
resetBootstrapRetry();
|
|
209
|
+
onRender();
|
|
210
|
+
return live;
|
|
211
|
+
})
|
|
212
|
+
.catch((error) => {
|
|
213
|
+
state.streamState = reconnectStreamState({ hasLiveState: getHasLiveState() });
|
|
214
|
+
onBootstrapError?.({ error, retrying });
|
|
215
|
+
onRender();
|
|
216
|
+
scheduleBootstrapRetry();
|
|
217
|
+
return null;
|
|
218
|
+
})
|
|
219
|
+
.finally(() => {
|
|
220
|
+
state.bootstrapPromise = null;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
return state.bootstrapPromise;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function refresh({ background = false } = {}) {
|
|
227
|
+
if (state.refreshPromise) {
|
|
228
|
+
return state.refreshPromise;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
state.refreshPromise = Promise.resolve()
|
|
232
|
+
.then(() => requestRefresh({ background }))
|
|
233
|
+
.catch((error) => {
|
|
234
|
+
if (background) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
throw error;
|
|
238
|
+
})
|
|
239
|
+
.finally(() => {
|
|
240
|
+
state.refreshPromise = null;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return state.refreshPromise;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function resumeVisible() {
|
|
247
|
+
if (!getVisible()) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
lifecycle.ensureStream();
|
|
252
|
+
if (!getHasLiveState()) {
|
|
253
|
+
void lifecycle.bootstrap({ retrying: true });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return lifecycle;
|
|
258
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export function nextBackoffDelay(currentMs, { baseMs, maxMs }) {
|
|
2
|
+
const delay = Number.isFinite(currentMs) && currentMs > 0 ? currentMs : baseMs;
|
|
3
|
+
return {
|
|
4
|
+
delay,
|
|
5
|
+
nextMs: Math.min(delay * 2, maxMs)
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function shouldScheduleRetry({ hasTimer = false, isVisible = true } = {}) {
|
|
10
|
+
return !hasTimer && Boolean(isVisible);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function reconnectStreamState({ hasLiveState = false } = {}) {
|
|
14
|
+
return hasLiveState ? "recovering" : "connecting";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function planBootstrapRetry({
|
|
18
|
+
backoffMs,
|
|
19
|
+
baseMs,
|
|
20
|
+
maxMs,
|
|
21
|
+
hasTimer = false,
|
|
22
|
+
isVisible = true
|
|
23
|
+
} = {}) {
|
|
24
|
+
if (!shouldScheduleRetry({ hasTimer, isVisible })) {
|
|
25
|
+
return {
|
|
26
|
+
schedule: false
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const next = nextBackoffDelay(backoffMs, { baseMs, maxMs });
|
|
31
|
+
return {
|
|
32
|
+
delay: next.delay,
|
|
33
|
+
nextBackoffMs: next.nextMs,
|
|
34
|
+
schedule: true
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function planStreamRecovery({
|
|
39
|
+
backoffMs,
|
|
40
|
+
baseMs,
|
|
41
|
+
maxMs,
|
|
42
|
+
hasLiveState = false,
|
|
43
|
+
hasTimer = false,
|
|
44
|
+
isVisible = true
|
|
45
|
+
} = {}) {
|
|
46
|
+
if (!shouldScheduleRetry({ hasTimer, isVisible })) {
|
|
47
|
+
return {
|
|
48
|
+
schedule: false,
|
|
49
|
+
streamState: reconnectStreamState({ hasLiveState })
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const next = nextBackoffDelay(backoffMs, { baseMs, maxMs });
|
|
54
|
+
return {
|
|
55
|
+
delay: next.delay,
|
|
56
|
+
followupAction: hasLiveState ? "refresh" : "bootstrap",
|
|
57
|
+
nextBackoffMs: next.nextMs,
|
|
58
|
+
schedule: true,
|
|
59
|
+
streamState: reconnectStreamState({ hasLiveState })
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export function createSelectionIntent({
|
|
2
|
+
cwd = null,
|
|
3
|
+
projectLabel = "",
|
|
4
|
+
source = "remote",
|
|
5
|
+
threadId = null,
|
|
6
|
+
threadLabel = ""
|
|
7
|
+
} = {}) {
|
|
8
|
+
return {
|
|
9
|
+
projectLabel: String(projectLabel || "").trim(),
|
|
10
|
+
requestedCwd: String(cwd || "").trim(),
|
|
11
|
+
requestedThreadId: String(threadId || "").trim(),
|
|
12
|
+
source: String(source || "remote").trim() || "remote",
|
|
13
|
+
threadLabel: String(threadLabel || "").trim()
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function selectionIntentSatisfied(intent = null, liveState = null) {
|
|
18
|
+
if (!intent) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const requestedThreadId = String(intent.requestedThreadId || "").trim();
|
|
23
|
+
const requestedCwd = String(intent.requestedCwd || "").trim();
|
|
24
|
+
const selectedThreadId = String(liveState?.selectedThreadId || "").trim();
|
|
25
|
+
const selectedCwd = String(liveState?.selectedProjectCwd || "").trim();
|
|
26
|
+
|
|
27
|
+
if (requestedThreadId) {
|
|
28
|
+
return selectedThreadId === requestedThreadId;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (requestedCwd) {
|
|
32
|
+
return selectedCwd === requestedCwd;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function selectionIntentMessage(intent = null, fallback = "Switching shared room...") {
|
|
39
|
+
if (!intent) {
|
|
40
|
+
return fallback;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (intent.threadLabel) {
|
|
44
|
+
return `Switching to ${intent.threadLabel}...`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (intent.projectLabel) {
|
|
48
|
+
return `Switching to ${intent.projectLabel}...`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return fallback;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function selectionIntentTitle(intent = null) {
|
|
55
|
+
if (!intent) {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const threadLabel = String(intent.threadLabel || "").trim();
|
|
60
|
+
if (!threadLabel) {
|
|
61
|
+
return "";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const normalized = threadLabel
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
67
|
+
.replace(/^-+|-+$/g, "");
|
|
68
|
+
|
|
69
|
+
return normalized ? `#${normalized}` : "";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function reconcileSelectionIntent(intent = null, liveState = null) {
|
|
73
|
+
const settled = selectionIntentSatisfied(intent, liveState);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
intent: settled ? null : intent,
|
|
77
|
+
settled
|
|
78
|
+
};
|
|
79
|
+
}
|