viveworker 0.7.0 → 0.8.1
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/.agents/plugins/marketplace.json +20 -0
- package/README.md +115 -4
- package/package.json +13 -3
- package/plugins/viveworker-control-plane/.codex-plugin/plugin.json +49 -0
- package/plugins/viveworker-control-plane/.mcp.json +11 -0
- package/plugins/viveworker-control-plane/assets/viveworker-logo-v2.png +0 -0
- package/plugins/viveworker-control-plane/assets/viveworker-logo-v2.svg +39 -0
- package/plugins/viveworker-control-plane/skills/viveworker-control-plane/SKILL.md +83 -0
- package/scripts/lib/markdown-render.mjs +128 -1
- package/scripts/lib/remote-pairing/README.md +164 -0
- package/scripts/lib/remote-pairing/_browser-bundle-entry.mjs +86 -0
- package/scripts/lib/remote-pairing/audit.mjs +122 -0
- package/scripts/lib/remote-pairing/bridge-relay-client.mjs +737 -0
- package/scripts/lib/remote-pairing/control.mjs +156 -0
- package/scripts/lib/remote-pairing/envelope.mjs +224 -0
- package/scripts/lib/remote-pairing/http-dispatch.mjs +530 -0
- package/scripts/lib/remote-pairing/keys-core.mjs +110 -0
- package/scripts/lib/remote-pairing/keys.mjs +181 -0
- package/scripts/lib/remote-pairing/noise.mjs +436 -0
- package/scripts/lib/remote-pairing/orchestrator.mjs +356 -0
- package/scripts/lib/remote-pairing/pairings.mjs +446 -0
- package/scripts/lib/remote-pairing/rpc.mjs +381 -0
- package/scripts/mcp-server.mjs +891 -0
- package/scripts/moltbook-scout-auto.sh +16 -8
- package/scripts/share-cli.mjs +14 -130
- package/scripts/stats-cli.mjs +683 -0
- package/scripts/viveworker-bridge.mjs +1676 -127
- package/scripts/viveworker.mjs +289 -7
- package/skills/viveworker-control-plane/SKILL.md +104 -0
- package/skills/viveworker-control-plane/agents/openai.yaml +12 -0
- package/templates/CLAUDE.viveworker.md +67 -0
- package/web/app.css +683 -9
- package/web/app.js +1780 -187
- package/web/build-id.js +1 -0
- package/web/i18n.js +187 -9
- package/web/icons/apple-touch-icon.png +0 -0
- package/web/icons/viveworker-icon-192.png +0 -0
- package/web/icons/viveworker-icon-512.png +0 -0
- package/web/icons/viveworker-v-pulse.svg +16 -1
- package/web/index.html +32 -2
- package/web/remote-pairing/api-router.js +873 -0
- package/web/remote-pairing/keys.js +237 -0
- package/web/remote-pairing/pairing-state.js +313 -0
- package/web/remote-pairing/rpc-client.js +765 -0
- package/web/remote-pairing/transport.js +804 -0
- package/web/remote-pairing/wake.js +149 -0
- package/web/remote-pairing-test.html +400 -0
- package/web/remote-pairing.bundle.js +3 -0
- package/web/remote-pairing.bundle.js.LEGAL.txt +15 -0
- package/web/remote-pairing.bundle.js.map +7 -0
- package/web/sw.js +190 -20
- package/web/icons/viveworker-beacon-v.svg +0 -19
- package/web/icons/viveworker-icon-1024.png +0 -0
- package/web/icons/viveworker-v-check.svg +0 -19
package/web/app.js
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import { DEFAULT_LOCALE, SUPPORTED_LOCALES, localeDisplayName, normalizeLocale, resolveLocalePreference, t } from "./i18n.js";
|
|
2
|
+
import { ensureIdentityKeypair, bytesToHex } from "./remote-pairing/keys.js";
|
|
3
|
+
import {
|
|
4
|
+
loadPairingState as loadRemotePairingState,
|
|
5
|
+
savePairingState as saveRemotePairingState,
|
|
6
|
+
clearPairingState as clearRemotePairingState,
|
|
7
|
+
} from "./remote-pairing/pairing-state.js";
|
|
8
|
+
const APP_BUILD_ID = "__VIVEWORKER_APP_BUILD_ID__";
|
|
9
|
+
const { getRoutingTelemetry, routedFetch } = await import(`./remote-pairing/api-router.js?v=${encodeURIComponent(APP_BUILD_ID)}`);
|
|
2
10
|
|
|
3
11
|
const DESKTOP_BREAKPOINT = 980;
|
|
4
12
|
const INSTALL_BANNER_DISMISS_KEY = "viveworker-install-banner-dismissed-v2";
|
|
@@ -12,6 +20,24 @@ const THREAD_FILTER_INTERACTION_DEFER_MS = 8000;
|
|
|
12
20
|
const MAX_COMPLETION_REPLY_IMAGE_COUNT = 4;
|
|
13
21
|
const NOTIFICATION_INTENT_CACHE = "viveworker-notification-intent-v1";
|
|
14
22
|
const NOTIFICATION_INTENT_PATH = "/__viveworker_notification_intent__";
|
|
23
|
+
const MAX_TIMELINE_IMAGE_OBJECT_URLS = 80;
|
|
24
|
+
const REMOTE_PAIRING_STATE_STORAGE_KEY = "viveworker.remote-pairing.state";
|
|
25
|
+
const REMOTE_PAIRING_STATE_SCHEMA_VERSION = 2;
|
|
26
|
+
const REMOTE_PAIRING_STATE_LEGACY_SCHEMA_VERSION = 1;
|
|
27
|
+
const BOOT_SPLASH_SLOW_HINT_MS = 10000;
|
|
28
|
+
const BOOT_SPLASH_REMOTE_SWITCHING_MIN_MS = 650;
|
|
29
|
+
const BOOTSTRAP_REMOTE_TIMEOUT_MS = 12_000;
|
|
30
|
+
const REMOTE_PAIRING_TOKEN_REFRESH_MS = 30 * 24 * 60 * 60 * 1000;
|
|
31
|
+
const BOOT_TRACE_MAX_EVENTS = 90;
|
|
32
|
+
const BOOT_TRACE_MAX_VALUE_LENGTH = 120;
|
|
33
|
+
const BOOT_SPLASH_STAGE = Object.freeze({
|
|
34
|
+
initial: -1,
|
|
35
|
+
checking: 0,
|
|
36
|
+
switching: 1,
|
|
37
|
+
establishing: 2,
|
|
38
|
+
loading: 3,
|
|
39
|
+
});
|
|
40
|
+
const timelineImageObjectUrlCache = new Map();
|
|
15
41
|
|
|
16
42
|
const state = {
|
|
17
43
|
session: null,
|
|
@@ -51,6 +77,7 @@ const state = {
|
|
|
51
77
|
detailDiffExpanded: {},
|
|
52
78
|
choiceLocalDrafts: {},
|
|
53
79
|
completionReplyDrafts: {},
|
|
80
|
+
completionReplySheetToken: "",
|
|
54
81
|
pendingActionUrls: new Set(),
|
|
55
82
|
pairError: "",
|
|
56
83
|
pairNotice: "",
|
|
@@ -60,6 +87,21 @@ const state = {
|
|
|
60
87
|
a2aRelayStatus: null,
|
|
61
88
|
a2aShareStatus: null,
|
|
62
89
|
a2aShareRecentExpanded: 0,
|
|
90
|
+
// Remote-pairing relay snapshot — { enabled, relayUrl, configuredRelayUrl,
|
|
91
|
+
// identityFingerprint, sessions: [...], pairings: [...] } | null.
|
|
92
|
+
// Populated by fetchRemotePairingStatus() on settings page open and after
|
|
93
|
+
// toggle/revoke actions. Null until first fetch completes.
|
|
94
|
+
remotePairingStatus: null,
|
|
95
|
+
// Notice / error flashes for the remote-pairing settings page (consumed
|
|
96
|
+
// and cleared after a single render, mirroring `pushNotice` / `pushError`
|
|
97
|
+
// from the push UI).
|
|
98
|
+
remotePairingNotice: "",
|
|
99
|
+
remotePairingError: "",
|
|
100
|
+
// Pending action key for in-flight toggle / revoke / relay-url save —
|
|
101
|
+
// used to disable buttons while the round-trip is in progress so a fast
|
|
102
|
+
// double-click doesn't fire two POSTs.
|
|
103
|
+
remotePairingPending: "",
|
|
104
|
+
remotePairingDetailsOpen: false,
|
|
63
105
|
hazbaseStatus: null,
|
|
64
106
|
hazbaseNotice: "",
|
|
65
107
|
hazbaseError: "",
|
|
@@ -106,6 +148,9 @@ const state = {
|
|
|
106
148
|
defaultLocale: DEFAULT_LOCALE,
|
|
107
149
|
supportedLocales: [...SUPPORTED_LOCALES],
|
|
108
150
|
appVersion: "",
|
|
151
|
+
appBuildId: APP_BUILD_ID,
|
|
152
|
+
serverAppBuildId: "",
|
|
153
|
+
clientUpdateRequired: false,
|
|
109
154
|
versionStatus: null,
|
|
110
155
|
versionStatusError: "",
|
|
111
156
|
};
|
|
@@ -133,12 +178,150 @@ function hazbasePasskeyHostSupport() {
|
|
|
133
178
|
|
|
134
179
|
const app = document.querySelector("#app");
|
|
135
180
|
let bootSplashDismissed = false;
|
|
181
|
+
let bootSplashHintTimer = null;
|
|
182
|
+
let bootSplashHintVisible = false;
|
|
183
|
+
let bootSplashDeferredStatusTimer = null;
|
|
184
|
+
let bootSplashRemoteRouteSeen = false;
|
|
185
|
+
let bootSplashRemoteSwitchingShownAtMs = 0;
|
|
186
|
+
let bootSplashStatusStage = BOOT_SPLASH_STAGE.initial;
|
|
187
|
+
let bootSplashPendingStatusStage = BOOT_SPLASH_STAGE.initial;
|
|
188
|
+
const bootTraceStartedAtMs = Date.now();
|
|
189
|
+
const bootTraceStartPerfMs = bootTraceNow();
|
|
190
|
+
const bootTraceId = makeBootTraceId();
|
|
191
|
+
let bootTraceEvents = [];
|
|
192
|
+
let bootTraceClosed = false;
|
|
193
|
+
let bootTraceSent = false;
|
|
194
|
+
|
|
195
|
+
if (typeof window !== "undefined") {
|
|
196
|
+
window.__viveworkerAppBuild = APP_BUILD_ID;
|
|
197
|
+
window.__viveworkerBootTraceId = bootTraceId;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function bootTraceNow() {
|
|
201
|
+
return typeof performance !== "undefined" && typeof performance.now === "function"
|
|
202
|
+
? performance.now()
|
|
203
|
+
: Date.now();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function makeBootTraceId() {
|
|
207
|
+
const randomPart = Math.random().toString(36).slice(2, 10);
|
|
208
|
+
return `${Date.now().toString(36)}-${randomPart}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function sanitizeBootTraceValue(value) {
|
|
212
|
+
if (value === null || value === undefined) {
|
|
213
|
+
return "";
|
|
214
|
+
}
|
|
215
|
+
return String(value).slice(0, BOOT_TRACE_MAX_VALUE_LENGTH);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function sanitizeBootTraceUrl(value) {
|
|
219
|
+
const raw = sanitizeBootTraceValue(value);
|
|
220
|
+
if (!raw) {
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
const base = window.location?.origin || "https://localhost";
|
|
225
|
+
const url = new URL(raw, base);
|
|
226
|
+
return url.pathname;
|
|
227
|
+
} catch {
|
|
228
|
+
return raw.split("?")[0].slice(0, BOOT_TRACE_MAX_VALUE_LENGTH);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function recordBootTraceEvent(type, detail = {}) {
|
|
233
|
+
if (bootTraceClosed) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const event = {
|
|
237
|
+
type: sanitizeBootTraceValue(type),
|
|
238
|
+
tMs: Math.max(0, Math.round(bootTraceNow() - bootTraceStartPerfMs)),
|
|
239
|
+
};
|
|
240
|
+
const phase = sanitizeBootTraceValue(detail.phase);
|
|
241
|
+
const url = sanitizeBootTraceUrl(detail.url);
|
|
242
|
+
const stateValue = sanitizeBootTraceValue(detail.state);
|
|
243
|
+
const previousState = sanitizeBootTraceValue(detail.previousState);
|
|
244
|
+
const reason = sanitizeBootTraceValue(detail.reason);
|
|
245
|
+
if (phase) event.phase = phase;
|
|
246
|
+
if (url) event.url = url;
|
|
247
|
+
if (stateValue) event.state = stateValue;
|
|
248
|
+
if (previousState) event.previousState = previousState;
|
|
249
|
+
if (reason) event.reason = reason;
|
|
250
|
+
if (detail.sticky === true || detail.sticky === false) event.sticky = detail.sticky;
|
|
251
|
+
if (Number.isFinite(Number(detail.code))) event.code = Number(detail.code);
|
|
252
|
+
if (detail.resumed === true || detail.resumed === false) event.resumed = detail.resumed;
|
|
253
|
+
bootTraceEvents.push(event);
|
|
254
|
+
if (bootTraceEvents.length > BOOT_TRACE_MAX_EVENTS) {
|
|
255
|
+
bootTraceEvents = bootTraceEvents.slice(-BOOT_TRACE_MAX_EVENTS);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function flushBootTrace(reason, extra = {}) {
|
|
260
|
+
if (bootTraceSent) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
bootTraceSent = true;
|
|
264
|
+
bootTraceClosed = true;
|
|
265
|
+
const payload = {
|
|
266
|
+
traceId: bootTraceId,
|
|
267
|
+
reason: sanitizeBootTraceValue(reason),
|
|
268
|
+
appBuildId: APP_BUILD_ID,
|
|
269
|
+
locale: state.locale || DEFAULT_LOCALE,
|
|
270
|
+
startedAtMs: bootTraceStartedAtMs,
|
|
271
|
+
totalMs: Math.max(0, Math.round(bootTraceNow() - bootTraceStartPerfMs)),
|
|
272
|
+
remoteRouteSeen: bootSplashRemoteRouteSeen,
|
|
273
|
+
finalStage: bootSplashStatusStage,
|
|
274
|
+
userAgent: navigator.userAgent || "",
|
|
275
|
+
events: bootTraceEvents,
|
|
276
|
+
...extra,
|
|
277
|
+
};
|
|
278
|
+
queueMicrotask(() => {
|
|
279
|
+
routedFetch("/api/remote-pairing/boot-trace", {
|
|
280
|
+
method: "POST",
|
|
281
|
+
credentials: "same-origin",
|
|
282
|
+
headers: {
|
|
283
|
+
"Content-Type": "application/json",
|
|
284
|
+
Accept: "application/json",
|
|
285
|
+
},
|
|
286
|
+
body: JSON.stringify(payload),
|
|
287
|
+
}, { suppressRoutingStatus: true }).catch(() => {});
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function syncVisualViewportMetrics() {
|
|
292
|
+
if (typeof document === "undefined") {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const root = document.documentElement;
|
|
296
|
+
const viewport = window.visualViewport;
|
|
297
|
+
const width = viewport?.width || window.innerWidth || root.clientWidth || 0;
|
|
298
|
+
const left = viewport?.offsetLeft || 0;
|
|
299
|
+
root.style.setProperty("--visual-viewport-width", `${Math.max(0, width)}px`);
|
|
300
|
+
root.style.setProperty("--visual-viewport-left", `${Math.max(0, left)}px`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function resetHorizontalViewportScroll() {
|
|
304
|
+
syncVisualViewportMetrics();
|
|
305
|
+
const scrollingElement = document.scrollingElement || document.documentElement;
|
|
306
|
+
if (scrollingElement) {
|
|
307
|
+
scrollingElement.scrollLeft = 0;
|
|
308
|
+
}
|
|
309
|
+
document.documentElement.scrollLeft = 0;
|
|
310
|
+
document.body.scrollLeft = 0;
|
|
311
|
+
syncVisualViewportMetrics();
|
|
312
|
+
window.requestAnimationFrame?.(syncVisualViewportMetrics);
|
|
313
|
+
}
|
|
136
314
|
|
|
137
315
|
function dismissBootSplash() {
|
|
138
316
|
if (bootSplashDismissed || typeof document === "undefined") {
|
|
139
317
|
return;
|
|
140
318
|
}
|
|
141
319
|
bootSplashDismissed = true;
|
|
320
|
+
clearBootSplashHintTimer();
|
|
321
|
+
clearBootSplashDeferredStatusTimer();
|
|
322
|
+
if (typeof window !== "undefined") {
|
|
323
|
+
window.removeEventListener("viveworker:remote-routing-status", handleBootRoutingStatus);
|
|
324
|
+
}
|
|
142
325
|
const splash = document.querySelector("#boot-splash");
|
|
143
326
|
document.body?.classList.add("viveworker-ready");
|
|
144
327
|
if (!splash) {
|
|
@@ -149,6 +332,147 @@ function dismissBootSplash() {
|
|
|
149
332
|
splash.remove();
|
|
150
333
|
}, 280);
|
|
151
334
|
}
|
|
335
|
+
|
|
336
|
+
function setBootSplashStatus(key, stage = bootSplashStatusStage) {
|
|
337
|
+
if (bootSplashDismissed || typeof document === "undefined") {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
if (stage < bootSplashStatusStage) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
const message = L(key);
|
|
344
|
+
const status = document.querySelector("#boot-splash-status");
|
|
345
|
+
const splash = document.querySelector("#boot-splash");
|
|
346
|
+
if (status && status.textContent !== message) {
|
|
347
|
+
status.textContent = message;
|
|
348
|
+
}
|
|
349
|
+
if (splash) {
|
|
350
|
+
splash.setAttribute("aria-label", `${L("common.appName")} ${message}`);
|
|
351
|
+
}
|
|
352
|
+
if (bootSplashRemoteRouteSeen) {
|
|
353
|
+
ensureBootSplashHintTimer();
|
|
354
|
+
}
|
|
355
|
+
bootSplashStatusStage = Math.max(bootSplashStatusStage, stage);
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function ensureBootSplashHintTimer() {
|
|
360
|
+
if (bootSplashHintTimer != null || bootSplashHintVisible || bootSplashDismissed) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
bootSplashHintTimer = window.setTimeout(() => {
|
|
364
|
+
bootSplashHintTimer = null;
|
|
365
|
+
showBootSplashHint();
|
|
366
|
+
}, BOOT_SPLASH_SLOW_HINT_MS);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function clearBootSplashHintTimer() {
|
|
370
|
+
if (bootSplashHintTimer != null) {
|
|
371
|
+
window.clearTimeout(bootSplashHintTimer);
|
|
372
|
+
bootSplashHintTimer = null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function clearBootSplashDeferredStatusTimer() {
|
|
377
|
+
if (bootSplashDeferredStatusTimer != null) {
|
|
378
|
+
window.clearTimeout(bootSplashDeferredStatusTimer);
|
|
379
|
+
bootSplashDeferredStatusTimer = null;
|
|
380
|
+
}
|
|
381
|
+
bootSplashPendingStatusStage = BOOT_SPLASH_STAGE.initial;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function showBootSplashHint() {
|
|
385
|
+
if (bootSplashDismissed || bootSplashHintVisible || typeof document === "undefined") {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const hint = document.querySelector("#boot-splash-hint");
|
|
389
|
+
if (!hint) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
bootSplashHintVisible = true;
|
|
393
|
+
hint.textContent = L("boot.status.slowHint");
|
|
394
|
+
hint.hidden = false;
|
|
395
|
+
if (typeof window.requestAnimationFrame === "function") {
|
|
396
|
+
window.requestAnimationFrame(() => {
|
|
397
|
+
hint.classList.add("is-visible");
|
|
398
|
+
});
|
|
399
|
+
} else {
|
|
400
|
+
hint.classList.add("is-visible");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function bootSplashNow() {
|
|
405
|
+
return typeof performance !== "undefined" && typeof performance.now === "function"
|
|
406
|
+
? performance.now()
|
|
407
|
+
: Date.now();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function showBootRemoteSwitchingStatus() {
|
|
411
|
+
bootSplashRemoteRouteSeen = true;
|
|
412
|
+
const effectiveStage = Math.max(bootSplashStatusStage, bootSplashPendingStatusStage);
|
|
413
|
+
if (effectiveStage >= BOOT_SPLASH_STAGE.switching) {
|
|
414
|
+
ensureBootSplashHintTimer();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
bootSplashRemoteSwitchingShownAtMs = bootSplashNow();
|
|
418
|
+
clearBootSplashDeferredStatusTimer();
|
|
419
|
+
setBootSplashStatus("boot.status.switchingRemote", BOOT_SPLASH_STAGE.switching);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function setBootRemoteStatusAfterSwitching(key, stage) {
|
|
423
|
+
bootSplashRemoteRouteSeen = true;
|
|
424
|
+
const effectiveStage = Math.max(bootSplashStatusStage, bootSplashPendingStatusStage);
|
|
425
|
+
if (stage <= effectiveStage) {
|
|
426
|
+
ensureBootSplashHintTimer();
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (bootSplashStatusStage < BOOT_SPLASH_STAGE.switching) {
|
|
430
|
+
showBootRemoteSwitchingStatus();
|
|
431
|
+
}
|
|
432
|
+
const elapsedMs = bootSplashRemoteSwitchingShownAtMs > 0
|
|
433
|
+
? bootSplashNow() - bootSplashRemoteSwitchingShownAtMs
|
|
434
|
+
: BOOT_SPLASH_REMOTE_SWITCHING_MIN_MS;
|
|
435
|
+
const delayMs = Math.max(0, BOOT_SPLASH_REMOTE_SWITCHING_MIN_MS - elapsedMs);
|
|
436
|
+
clearBootSplashDeferredStatusTimer();
|
|
437
|
+
if (delayMs <= 0) {
|
|
438
|
+
setBootSplashStatus(key, stage);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
bootSplashDeferredStatusTimer = window.setTimeout(() => {
|
|
442
|
+
bootSplashDeferredStatusTimer = null;
|
|
443
|
+
bootSplashPendingStatusStage = BOOT_SPLASH_STAGE.initial;
|
|
444
|
+
setBootSplashStatus(key, stage);
|
|
445
|
+
}, delayMs);
|
|
446
|
+
bootSplashPendingStatusStage = stage;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function handleBootRoutingStatus(event) {
|
|
450
|
+
if (bootSplashDismissed) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
recordBootTraceEvent("route", event?.detail || {});
|
|
454
|
+
const phase = event?.detail?.phase || "";
|
|
455
|
+
switch (phase) {
|
|
456
|
+
case "lan-checking":
|
|
457
|
+
setBootSplashStatus("boot.status.checkingLan", BOOT_SPLASH_STAGE.checking);
|
|
458
|
+
break;
|
|
459
|
+
case "lan-failed":
|
|
460
|
+
case "remote-switching":
|
|
461
|
+
showBootRemoteSwitchingStatus();
|
|
462
|
+
break;
|
|
463
|
+
case "remote-connecting":
|
|
464
|
+
setBootRemoteStatusAfterSwitching("boot.status.establishingRemote", BOOT_SPLASH_STAGE.establishing);
|
|
465
|
+
break;
|
|
466
|
+
case "lan-connected":
|
|
467
|
+
// Same-LAN startup is fast and already covered by the checking message.
|
|
468
|
+
break;
|
|
469
|
+
case "remote-connected":
|
|
470
|
+
setBootRemoteStatusAfterSwitching("boot.status.loadingData", BOOT_SPLASH_STAGE.loading);
|
|
471
|
+
break;
|
|
472
|
+
default:
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
152
476
|
const params = new URLSearchParams(window.location.search);
|
|
153
477
|
const initialItem = params.get("item") || "";
|
|
154
478
|
const initialTargetTab = params.get("tab") || "";
|
|
@@ -159,10 +483,13 @@ let didReloadForServiceWorker = false;
|
|
|
159
483
|
let lastViewportMode = isDesktopLayout();
|
|
160
484
|
|
|
161
485
|
boot().catch((error) => {
|
|
162
|
-
const message =
|
|
163
|
-
const hint =
|
|
486
|
+
const message = bootErrorMessage(error);
|
|
487
|
+
const hint = shouldShowNetworkHint(error, message)
|
|
164
488
|
? `<p class="muted">${escapeHtml(L("error.networkHint"))}</p>`
|
|
165
489
|
: "";
|
|
490
|
+
flushBootTrace("boot-error", {
|
|
491
|
+
error: sanitizeBootTraceValue(error?.code || error?.name || message),
|
|
492
|
+
});
|
|
166
493
|
dismissBootSplash();
|
|
167
494
|
app.innerHTML = `
|
|
168
495
|
<main class="onboarding-shell">
|
|
@@ -176,13 +503,95 @@ boot().catch((error) => {
|
|
|
176
503
|
`;
|
|
177
504
|
});
|
|
178
505
|
|
|
506
|
+
function bootErrorMessage(error) {
|
|
507
|
+
if (isRemotePairingEnrollmentRequired(error)) {
|
|
508
|
+
return L("error.remotePairingNeedsLanRefresh");
|
|
509
|
+
}
|
|
510
|
+
if (isRemotePairingUnavailable(error)) {
|
|
511
|
+
return L("error.remotePairingUnavailable");
|
|
512
|
+
}
|
|
513
|
+
return error?.message || String(error);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function shouldShowNetworkHint(error, message) {
|
|
517
|
+
if (isRemotePairingEnrollmentRequired(error) || isRemotePairingUnavailable(error)) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
return /Load failed|Failed to fetch|NetworkError|fetch/i.test(message);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function isRemotePairingEnrollmentRequired(error) {
|
|
524
|
+
return error?.code === "remote-pairing-enrollment-required" ||
|
|
525
|
+
error?.name === "RemotePairingEnrollmentRequiredError";
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function isRemotePairingUnavailable(error) {
|
|
529
|
+
return error?.code === "remote-pairing-unavailable" ||
|
|
530
|
+
error?.code === "remote-pairing-unreachable" ||
|
|
531
|
+
error?.name === "RemotePairingUnavailableError" ||
|
|
532
|
+
error?.name === "RpcTimeoutError" ||
|
|
533
|
+
error?.name === "RpcTransportError" ||
|
|
534
|
+
error?.name === "RpcTransportFailedError";
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function inspectRemotePairingStateForEnrollment() {
|
|
538
|
+
let store;
|
|
539
|
+
try {
|
|
540
|
+
store = globalThis.localStorage ?? null;
|
|
541
|
+
} catch {
|
|
542
|
+
return { status: "storage-unavailable", needsEnrollment: false };
|
|
543
|
+
}
|
|
544
|
+
if (!store) {
|
|
545
|
+
return { status: "storage-unavailable", needsEnrollment: false };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
let raw;
|
|
549
|
+
try {
|
|
550
|
+
raw = store.getItem(REMOTE_PAIRING_STATE_STORAGE_KEY);
|
|
551
|
+
} catch {
|
|
552
|
+
return { status: "storage-unavailable", needsEnrollment: false };
|
|
553
|
+
}
|
|
554
|
+
if (!raw) {
|
|
555
|
+
return { status: "missing", needsEnrollment: true };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
let parsed;
|
|
559
|
+
try {
|
|
560
|
+
parsed = JSON.parse(raw);
|
|
561
|
+
} catch {
|
|
562
|
+
return { status: "malformed", needsEnrollment: true };
|
|
563
|
+
}
|
|
564
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
565
|
+
return { status: "malformed", needsEnrollment: true };
|
|
566
|
+
}
|
|
567
|
+
if (parsed.version === REMOTE_PAIRING_STATE_LEGACY_SCHEMA_VERSION) {
|
|
568
|
+
return { status: "legacy-v1", needsEnrollment: true };
|
|
569
|
+
}
|
|
570
|
+
if (parsed.version !== REMOTE_PAIRING_STATE_SCHEMA_VERSION) {
|
|
571
|
+
return { status: "unsupported-version", needsEnrollment: true };
|
|
572
|
+
}
|
|
573
|
+
if (typeof parsed.relayToken !== "string" || parsed.relayToken.length === 0) {
|
|
574
|
+
return { status: "missing-token", needsEnrollment: true };
|
|
575
|
+
}
|
|
576
|
+
return { status: "ready", needsEnrollment: false };
|
|
577
|
+
}
|
|
578
|
+
|
|
179
579
|
async function boot() {
|
|
580
|
+
recordBootTraceEvent("boot-start", {
|
|
581
|
+
url: window.location?.pathname || "/app",
|
|
582
|
+
});
|
|
180
583
|
updateManifestHref(initialPairToken);
|
|
584
|
+
syncVisualViewportMetrics();
|
|
585
|
+
setBootSplashStatus("boot.status.checkingLan", BOOT_SPLASH_STAGE.checking);
|
|
181
586
|
// SW register + update() can take hundreds of ms and does not need to gate
|
|
182
587
|
// first paint. Fire and forget; the `controllerchange` reload handler wired
|
|
183
588
|
// up inside `registerServiceWorker` still picks up new versions.
|
|
184
589
|
registerServiceWorker().catch(() => {});
|
|
590
|
+
window.addEventListener("viveworker:remote-routing-status", handleBootRoutingStatus);
|
|
185
591
|
navigator.serviceWorker?.addEventListener("message", handleServiceWorkerMessage);
|
|
592
|
+
window.addEventListener("resize", syncVisualViewportMetrics, { passive: true });
|
|
593
|
+
window.visualViewport?.addEventListener("resize", syncVisualViewportMetrics, { passive: true });
|
|
594
|
+
window.visualViewport?.addEventListener("scroll", syncVisualViewportMetrics, { passive: true });
|
|
186
595
|
window.addEventListener("resize", handleViewportChange, { passive: true });
|
|
187
596
|
window.addEventListener("focus", handlePotentialExternalNavigation, { passive: true });
|
|
188
597
|
window.addEventListener("pageshow", handlePotentialExternalNavigation, { passive: true });
|
|
@@ -191,13 +600,24 @@ async function boot() {
|
|
|
191
600
|
// Single round-trip for session + inbox(pending/completed) + timeline +
|
|
192
601
|
// devices. See `refreshBootstrap` for why we collapsed the boot fan-out.
|
|
193
602
|
await refreshBootstrap();
|
|
603
|
+
if (bootSplashRemoteRouteSeen) {
|
|
604
|
+
setBootRemoteStatusAfterSwitching("boot.status.loadingData", BOOT_SPLASH_STAGE.loading);
|
|
605
|
+
}
|
|
606
|
+
flushBootTrace("bootstrap-complete");
|
|
194
607
|
|
|
195
608
|
if (!state.session?.authenticated && initialPairToken && shouldAutoPairFromBootstrapToken()) {
|
|
196
609
|
try {
|
|
197
|
-
await pair({
|
|
610
|
+
const pairResult = await pair({
|
|
198
611
|
token: initialPairToken,
|
|
199
612
|
temporary: shouldUseTemporaryBootstrapPairing(),
|
|
200
613
|
});
|
|
614
|
+
// Mirror the manual #pair-form path: register with the bridge so
|
|
615
|
+
// off-LAN reconnect via the relay works without a second pair step.
|
|
616
|
+
// Skipped when the bridge graced this tab a temporary session
|
|
617
|
+
// (Safari non-PWA bootstrap), since those don't survive the tab.
|
|
618
|
+
if (pairResult?.temporaryPairing !== true) {
|
|
619
|
+
await enrollRemotePairing();
|
|
620
|
+
}
|
|
201
621
|
} catch (error) {
|
|
202
622
|
state.pairError = error.message || String(error);
|
|
203
623
|
}
|
|
@@ -229,6 +649,7 @@ async function boot() {
|
|
|
229
649
|
return;
|
|
230
650
|
}
|
|
231
651
|
|
|
652
|
+
await maybeAutoEnrollRemotePairingFromLan();
|
|
232
653
|
await consumePendingNotificationIntent();
|
|
233
654
|
// `?focusPending=claude` marks this tab as the Claude-hook-opened popup:
|
|
234
655
|
// auto-navigate to the newest unresolved Claude pending (plan/question)
|
|
@@ -306,6 +727,7 @@ async function boot() {
|
|
|
306
727
|
refreshInboxDiff(),
|
|
307
728
|
fetchMoltbookScoutStatus(),
|
|
308
729
|
fetchA2aShareStatus(),
|
|
730
|
+
fetchRemotePairingStatus(),
|
|
309
731
|
])
|
|
310
732
|
.then(async () => {
|
|
311
733
|
if (!shouldDeferRenderForActiveInteraction()) {
|
|
@@ -316,6 +738,49 @@ async function boot() {
|
|
|
316
738
|
}, 3000);
|
|
317
739
|
}
|
|
318
740
|
|
|
741
|
+
async function maybeAutoEnrollRemotePairingFromLan() {
|
|
742
|
+
if (!state.session?.authenticated) {
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
const pairingState = inspectRemotePairingStateForEnrollment();
|
|
746
|
+
const telemetry = getRoutingTelemetry();
|
|
747
|
+
if (!telemetry || telemetry.lanOk <= 0) {
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
if (!pairingState.needsEnrollment) {
|
|
751
|
+
const record = pairingState.record || loadRemotePairingState();
|
|
752
|
+
if (!shouldRefreshRemotePairingTokenFromLan(record)) {
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return enrollRemotePairing();
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function shouldRefreshRemotePairingTokenFromLan(record) {
|
|
760
|
+
if (!record) {
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
const updatedAt = Number(record.relayTokenUpdatedAtMs) || 0;
|
|
764
|
+
if (!updatedAt) {
|
|
765
|
+
return true;
|
|
766
|
+
}
|
|
767
|
+
return Date.now() - updatedAt >= REMOTE_PAIRING_TOKEN_REFRESH_MS;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function isRemotePairingUsingRelay() {
|
|
771
|
+
const telemetry = getRoutingTelemetry();
|
|
772
|
+
if (!telemetry) {
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
if (telemetry.lastRoute === "lan") {
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
if (telemetry.lastRoute === "relay") {
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
return telemetry.relayOk > 0 && Number(telemetry.stickyRelayUntilMs || 0) > Date.now();
|
|
782
|
+
}
|
|
783
|
+
|
|
319
784
|
async function registerServiceWorker() {
|
|
320
785
|
if (!("serviceWorker" in navigator)) {
|
|
321
786
|
return;
|
|
@@ -335,6 +800,47 @@ async function registerServiceWorker() {
|
|
|
335
800
|
}
|
|
336
801
|
}
|
|
337
802
|
|
|
803
|
+
async function forceAppRefreshFromLan() {
|
|
804
|
+
try {
|
|
805
|
+
const healthInit = {
|
|
806
|
+
cache: "no-store",
|
|
807
|
+
credentials: "same-origin",
|
|
808
|
+
headers: { Accept: "application/json" },
|
|
809
|
+
};
|
|
810
|
+
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
|
|
811
|
+
healthInit.signal = AbortSignal.timeout(2500);
|
|
812
|
+
}
|
|
813
|
+
await fetch("/health", healthInit);
|
|
814
|
+
} catch {
|
|
815
|
+
state.pushError = L("error.clientUpdateNeedsLan");
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
try {
|
|
820
|
+
const registration = state.serviceWorkerRegistration || await navigator.serviceWorker?.getRegistration?.();
|
|
821
|
+
await registration?.update?.();
|
|
822
|
+
} catch {
|
|
823
|
+
// Reload below is still useful; the next boot can retry SW update.
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
try {
|
|
827
|
+
if (typeof caches !== "undefined") {
|
|
828
|
+
const keys = await caches.keys();
|
|
829
|
+
await Promise.all(
|
|
830
|
+
keys
|
|
831
|
+
.filter((key) => /^viveworker-v/.test(key))
|
|
832
|
+
.map((key) => caches.delete(key))
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
} catch {
|
|
836
|
+
// Cache deletion is best-effort; network-first app routes still help.
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const nextUrl = new URL(window.location.href);
|
|
840
|
+
nextUrl.searchParams.set("appRefresh", String(Date.now()));
|
|
841
|
+
window.location.replace(nextUrl.toString());
|
|
842
|
+
}
|
|
843
|
+
|
|
338
844
|
function handleViewportChange() {
|
|
339
845
|
const nextViewportMode = isDesktopLayout();
|
|
340
846
|
if (nextViewportMode === lastViewportMode) {
|
|
@@ -364,6 +870,7 @@ async function refreshAuthenticatedState() {
|
|
|
364
870
|
await fetchMoltbookScoutStatus();
|
|
365
871
|
await fetchA2aRelayStatus();
|
|
366
872
|
await fetchA2aShareStatus();
|
|
873
|
+
await fetchRemotePairingStatus();
|
|
367
874
|
if (state.currentTab === "settings") {
|
|
368
875
|
await fetchHazbaseStatus();
|
|
369
876
|
}
|
|
@@ -386,11 +893,13 @@ async function refreshAuthenticatedStateRemote() {
|
|
|
386
893
|
fetchMoltbookScoutStatus(),
|
|
387
894
|
fetchA2aRelayStatus(),
|
|
388
895
|
fetchA2aShareStatus(),
|
|
896
|
+
fetchRemotePairingStatus(),
|
|
389
897
|
]);
|
|
390
898
|
}
|
|
391
899
|
|
|
392
900
|
async function refreshSession() {
|
|
393
901
|
state.session = await apiGet("/api/session");
|
|
902
|
+
applyServerAppBuildId(state.session?.webAppBuildId);
|
|
394
903
|
syncPairingTokenState(desiredBootstrapPairingToken());
|
|
395
904
|
applyResolvedLocale();
|
|
396
905
|
}
|
|
@@ -402,8 +911,14 @@ async function refreshSession() {
|
|
|
402
911
|
// iOS PWAs where connection reuse is aggressive. Leaves the diff and
|
|
403
912
|
// external-status probes as separate background phases in `boot()`.
|
|
404
913
|
async function refreshBootstrap() {
|
|
405
|
-
|
|
914
|
+
recordBootTraceEvent("bootstrap-start", { url: "/api/bootstrap" });
|
|
915
|
+
const bootstrap = await apiGet("/api/bootstrap", {
|
|
916
|
+
timeoutMs: BOOTSTRAP_REMOTE_TIMEOUT_MS,
|
|
917
|
+
preferRelayError: true,
|
|
918
|
+
});
|
|
919
|
+
recordBootTraceEvent("bootstrap-response", { url: "/api/bootstrap" });
|
|
406
920
|
state.session = bootstrap?.session || null;
|
|
921
|
+
applyServerAppBuildId(bootstrap?.appBuildId || state.session?.webAppBuildId);
|
|
407
922
|
syncPairingTokenState(desiredBootstrapPairingToken());
|
|
408
923
|
applyResolvedLocale();
|
|
409
924
|
|
|
@@ -424,7 +939,7 @@ async function refreshBootstrap() {
|
|
|
424
939
|
syncCompletedThreadFilter();
|
|
425
940
|
syncInboxSubtab();
|
|
426
941
|
|
|
427
|
-
state.timeline = bootstrap?.timeline || null;
|
|
942
|
+
state.timeline = await hydrateTimelinePayloadImages(bootstrap?.timeline || null);
|
|
428
943
|
syncTimelineThreadFilter();
|
|
429
944
|
syncTimelineKindFilter();
|
|
430
945
|
|
|
@@ -433,6 +948,12 @@ async function refreshBootstrap() {
|
|
|
433
948
|
state.deviceError = "";
|
|
434
949
|
}
|
|
435
950
|
|
|
951
|
+
function applyServerAppBuildId(value) {
|
|
952
|
+
const buildId = normalizeClientText(value);
|
|
953
|
+
state.serverAppBuildId = buildId;
|
|
954
|
+
state.clientUpdateRequired = Boolean(buildId && buildId !== APP_BUILD_ID);
|
|
955
|
+
}
|
|
956
|
+
|
|
436
957
|
async function syncDetectedLocalePreference() {
|
|
437
958
|
if (!state.session?.authenticated || !state.session?.deviceId || !state.detectedLocale) {
|
|
438
959
|
return;
|
|
@@ -529,9 +1050,10 @@ function detectBrowserLocale() {
|
|
|
529
1050
|
|
|
530
1051
|
async function refreshPushStatus() {
|
|
531
1052
|
const client = await getClientPushState();
|
|
1053
|
+
const { clientSubscription, ...clientStatus } = client;
|
|
532
1054
|
if (!state.session?.authenticated) {
|
|
533
1055
|
state.pushStatus = {
|
|
534
|
-
...
|
|
1056
|
+
...clientStatus,
|
|
535
1057
|
enabled: false,
|
|
536
1058
|
subscribed: false,
|
|
537
1059
|
serverSubscribed: false,
|
|
@@ -542,16 +1064,34 @@ async function refreshPushStatus() {
|
|
|
542
1064
|
}
|
|
543
1065
|
|
|
544
1066
|
try {
|
|
545
|
-
|
|
1067
|
+
let server = await apiGet("/api/push/status");
|
|
1068
|
+
if (
|
|
1069
|
+
server?.enabled === true &&
|
|
1070
|
+
server?.subscribed !== true &&
|
|
1071
|
+
clientSubscription &&
|
|
1072
|
+
clientStatus.notificationPermission === "granted"
|
|
1073
|
+
) {
|
|
1074
|
+
try {
|
|
1075
|
+
await apiPost("/api/push/subscribe", {
|
|
1076
|
+
subscription: clientSubscription,
|
|
1077
|
+
userAgent: navigator.userAgent,
|
|
1078
|
+
standalone: isStandaloneMode(),
|
|
1079
|
+
});
|
|
1080
|
+
server = await apiGet("/api/push/status");
|
|
1081
|
+
} catch {
|
|
1082
|
+
// Best effort: if the browser still has a local subscription, the
|
|
1083
|
+
// status row can reflect that while the enable action repairs it.
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
546
1086
|
state.pushStatus = {
|
|
547
1087
|
...server,
|
|
548
|
-
...
|
|
1088
|
+
...clientStatus,
|
|
549
1089
|
serverSubscribed: Boolean(server.subscribed),
|
|
550
|
-
subscribed: Boolean(server.subscribed ||
|
|
1090
|
+
subscribed: Boolean(server.subscribed || clientStatus.clientSubscribed),
|
|
551
1091
|
};
|
|
552
1092
|
} catch (error) {
|
|
553
1093
|
state.pushStatus = {
|
|
554
|
-
...
|
|
1094
|
+
...clientStatus,
|
|
555
1095
|
enabled: false,
|
|
556
1096
|
subscribed: false,
|
|
557
1097
|
serverSubscribed: false,
|
|
@@ -586,6 +1126,18 @@ async function fetchA2aRelayStatus() {
|
|
|
586
1126
|
}
|
|
587
1127
|
}
|
|
588
1128
|
|
|
1129
|
+
async function fetchRemotePairingStatus() {
|
|
1130
|
+
if (!state.session?.remotePairingAvailable) {
|
|
1131
|
+
state.remotePairingStatus = null;
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
try {
|
|
1135
|
+
state.remotePairingStatus = await apiGet("/api/remote-pairing/status");
|
|
1136
|
+
} catch {
|
|
1137
|
+
state.remotePairingStatus = null;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
589
1141
|
async function fetchA2aShareStatus() {
|
|
590
1142
|
if (!state.session?.a2aShareEnabled) {
|
|
591
1143
|
state.a2aShareStatus = null;
|
|
@@ -648,6 +1200,7 @@ async function getClientPushState() {
|
|
|
648
1200
|
"PushManager" in window &&
|
|
649
1201
|
"Notification" in window,
|
|
650
1202
|
clientSubscribed: Boolean(subscription),
|
|
1203
|
+
clientSubscription: subscription ? subscription.toJSON() : null,
|
|
651
1204
|
};
|
|
652
1205
|
}
|
|
653
1206
|
|
|
@@ -688,7 +1241,7 @@ async function refreshInboxDiff() {
|
|
|
688
1241
|
}
|
|
689
1242
|
|
|
690
1243
|
async function refreshTimeline() {
|
|
691
|
-
state.timeline = await apiGet("/api/timeline");
|
|
1244
|
+
state.timeline = await hydrateTimelinePayloadImages(await apiGet("/api/timeline"));
|
|
692
1245
|
syncTimelineThreadFilter();
|
|
693
1246
|
syncTimelineKindFilter();
|
|
694
1247
|
}
|
|
@@ -872,6 +1425,7 @@ function normalizeProviderClient(value) {
|
|
|
872
1425
|
if (normalized === "moltbook") return "moltbook";
|
|
873
1426
|
if (normalized === "a2a") return "a2a";
|
|
874
1427
|
if (normalized === "viveworker") return "viveworker";
|
|
1428
|
+
if (normalized === "mcp") return "mcp";
|
|
875
1429
|
return "codex";
|
|
876
1430
|
}
|
|
877
1431
|
|
|
@@ -881,6 +1435,7 @@ function providerDisplayName(provider) {
|
|
|
881
1435
|
if (p === "moltbook") return "Moltbook";
|
|
882
1436
|
if (p === "a2a") return "A2A";
|
|
883
1437
|
if (p === "viveworker") return L("common.appName");
|
|
1438
|
+
if (p === "mcp") return "MCP";
|
|
884
1439
|
return L("common.codex");
|
|
885
1440
|
}
|
|
886
1441
|
|
|
@@ -1176,9 +1731,16 @@ function renderPair() {
|
|
|
1176
1731
|
event.preventDefault();
|
|
1177
1732
|
const form = new FormData(event.currentTarget);
|
|
1178
1733
|
try {
|
|
1179
|
-
await pair({ code: String(form.get("code") || "") });
|
|
1734
|
+
const pairResult = await pair({ code: String(form.get("code") || "") });
|
|
1180
1735
|
state.pairError = "";
|
|
1181
1736
|
state.pairNotice = "";
|
|
1737
|
+
// Best-effort: register this phone's X25519 pubkey with the bridge
|
|
1738
|
+
// so it can reach us via the relay later. Doesn't block the shell
|
|
1739
|
+
// render; failures (older bridge, IndexedDB blocked, …) are swallowed
|
|
1740
|
+
// inside the helper.
|
|
1741
|
+
if (pairResult?.temporaryPairing !== true) {
|
|
1742
|
+
await enrollRemotePairing();
|
|
1743
|
+
}
|
|
1182
1744
|
await refreshSession();
|
|
1183
1745
|
await refreshAuthenticatedState();
|
|
1184
1746
|
await renderShell();
|
|
@@ -1200,6 +1762,80 @@ async function pair(payload) {
|
|
|
1200
1762
|
return result;
|
|
1201
1763
|
}
|
|
1202
1764
|
|
|
1765
|
+
/**
|
|
1766
|
+
* Best-effort post-pair enrollment: hand the bridge our X25519 static
|
|
1767
|
+
* pubkey + a friendly label, persist the bridge's response in localStorage,
|
|
1768
|
+
* and return the saved record (or null on any failure).
|
|
1769
|
+
*
|
|
1770
|
+
* Always called after a successful LAN pair (regardless of whether the
|
|
1771
|
+
* remote relay is currently ON). Reasoning:
|
|
1772
|
+
*
|
|
1773
|
+
* - Enrollment is idempotent on phonePub server-side; re-pairing the same
|
|
1774
|
+
* phone keeps its existing pairingId and bridge identity.
|
|
1775
|
+
* - Doing it eagerly means flipping the relay toggle later "just works"
|
|
1776
|
+
* without forcing the user to repair-on-LAN to register their key.
|
|
1777
|
+
* - LAN-only basic auth keeps working even if this fails (e.g. older
|
|
1778
|
+
* bridge predating the lan-enroll endpoint, IndexedDB blocked, etc.) —
|
|
1779
|
+
* we explicitly swallow errors and let the caller continue rendering
|
|
1780
|
+
* the authenticated shell.
|
|
1781
|
+
*
|
|
1782
|
+
* Skipped when the surrounding `pair()` returned `temporaryPairing: true`
|
|
1783
|
+
* — temporary sessions are an opt-in shape that doesn't outlive the tab,
|
|
1784
|
+
* so persisting a pairing record would be misleading.
|
|
1785
|
+
*
|
|
1786
|
+
* @param {string} [label] user-visible device label; defaults to a
|
|
1787
|
+
* "LAN paired YYYY-MM-DD" stamp.
|
|
1788
|
+
* @returns {Promise<import("./remote-pairing/pairing-state.js").RemotePairingState | null>}
|
|
1789
|
+
*/
|
|
1790
|
+
async function enrollRemotePairing(label) {
|
|
1791
|
+
try {
|
|
1792
|
+
const keypair = await ensureIdentityKeypair();
|
|
1793
|
+
const phonePubHex = bytesToHex(keypair.pub);
|
|
1794
|
+
const effectiveLabel = (label && String(label).trim()) || buildDefaultEnrollLabel();
|
|
1795
|
+
const response = await apiPost("/api/remote-pairing/lan-enroll", {
|
|
1796
|
+
phonePubHex,
|
|
1797
|
+
label: effectiveLabel,
|
|
1798
|
+
});
|
|
1799
|
+
if (!response || response.ok !== true) {
|
|
1800
|
+
return null;
|
|
1801
|
+
}
|
|
1802
|
+
const saved = saveRemotePairingState({
|
|
1803
|
+
pairingId: response.pairingId,
|
|
1804
|
+
relayToken: response.relayToken,
|
|
1805
|
+
phonePub: response.phonePub,
|
|
1806
|
+
phoneFingerprint: response.phoneFingerprint,
|
|
1807
|
+
bridgePubHex: response.bridgePubHex,
|
|
1808
|
+
bridgeFingerprint: response.bridgeFingerprint,
|
|
1809
|
+
relayUrl: response.relayUrl,
|
|
1810
|
+
label: response.label || "",
|
|
1811
|
+
addedAtMs: Number.isFinite(response.addedAtMs) ? response.addedAtMs : Date.now(),
|
|
1812
|
+
relayTokenUpdatedAtMs: Number.isFinite(response.relayTokenUpdatedAtMs)
|
|
1813
|
+
? response.relayTokenUpdatedAtMs
|
|
1814
|
+
: Date.now(),
|
|
1815
|
+
});
|
|
1816
|
+
if (!saved) {
|
|
1817
|
+
// Storage is full / disabled; the bridge still has the pairing,
|
|
1818
|
+
// so future toggle-on works — just no localStorage cache for the
|
|
1819
|
+
// PWA's own routing decisions.
|
|
1820
|
+
return null;
|
|
1821
|
+
}
|
|
1822
|
+
return loadRemotePairingState();
|
|
1823
|
+
} catch (error) {
|
|
1824
|
+
console.warn("[remote-pairing] lan-enroll skipped:", error?.message || error);
|
|
1825
|
+
return null;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
function buildDefaultEnrollLabel() {
|
|
1830
|
+
// YYYY-MM-DD so re-enrolls produce stable, sortable labels in the
|
|
1831
|
+
// settings list. (The user can rename in the settings page later.)
|
|
1832
|
+
const now = new Date();
|
|
1833
|
+
const y = now.getFullYear();
|
|
1834
|
+
const m = String(now.getMonth() + 1).padStart(2, "0");
|
|
1835
|
+
const d = String(now.getDate()).padStart(2, "0");
|
|
1836
|
+
return `LAN paired ${y}-${m}-${d}`;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1203
1839
|
async function logout({ revokeCurrentDeviceTrust = false } = {}) {
|
|
1204
1840
|
await apiPost("/api/session/logout", { revokeCurrentDeviceTrust });
|
|
1205
1841
|
resetAuthenticatedState();
|
|
@@ -1227,6 +1863,7 @@ function resetAuthenticatedState() {
|
|
|
1227
1863
|
state.choiceLocalDrafts = {};
|
|
1228
1864
|
clearAllCompletionReplyDrafts();
|
|
1229
1865
|
state.completionReplyDrafts = {};
|
|
1866
|
+
state.completionReplySheetToken = "";
|
|
1230
1867
|
state.settingsSubpage = "";
|
|
1231
1868
|
state.settingsScrollState = null;
|
|
1232
1869
|
state.listScrollState = null;
|
|
@@ -1238,6 +1875,7 @@ function resetAuthenticatedState() {
|
|
|
1238
1875
|
state.deviceError = "";
|
|
1239
1876
|
state.logoutConfirmOpen = false;
|
|
1240
1877
|
state.pairError = "";
|
|
1878
|
+
state.remotePairingDetailsOpen = false;
|
|
1241
1879
|
}
|
|
1242
1880
|
|
|
1243
1881
|
async function revokeTrustedDevice(deviceId) {
|
|
@@ -1258,7 +1896,49 @@ async function revokeTrustedDevice(deviceId) {
|
|
|
1258
1896
|
await renderShell();
|
|
1259
1897
|
}
|
|
1260
1898
|
|
|
1899
|
+
/**
|
|
1900
|
+
* `renderShell()` rebuilds the entire `#app` subtree by reassigning
|
|
1901
|
+
* `innerHTML`, which destroys every <pre> element in the DOM — including
|
|
1902
|
+
* any horizontal scroll position the user dragged into a wide code block.
|
|
1903
|
+
* On a polling interval, that means a long line of code keeps snapping
|
|
1904
|
+
* back to the start while the reader is mid-line.
|
|
1905
|
+
*
|
|
1906
|
+
* `snapshotCodeBlockScrolls()` records each `.markdown pre`'s scrollLeft
|
|
1907
|
+
* keyed by its trimmed textContent (so the key survives a fresh render
|
|
1908
|
+
* regardless of position in the DOM tree). `restoreCodeBlockScrolls()`
|
|
1909
|
+
* walks the new DOM and restores any scrollLeft we still have a key for.
|
|
1910
|
+
*
|
|
1911
|
+
* Content-keyed matching is intentional: if the underlying code text
|
|
1912
|
+
* changes mid-scroll, the new <pre> is logically different and we let it
|
|
1913
|
+
* start at scrollLeft=0 rather than landing the reader somewhere unrelated.
|
|
1914
|
+
*/
|
|
1915
|
+
function snapshotCodeBlockScrolls() {
|
|
1916
|
+
if (typeof document === "undefined") return null;
|
|
1917
|
+
const blocks = document.querySelectorAll(".markdown pre");
|
|
1918
|
+
if (blocks.length === 0) return null;
|
|
1919
|
+
const map = new Map();
|
|
1920
|
+
for (const pre of blocks) {
|
|
1921
|
+
const key = pre.textContent ? pre.textContent.trim() : "";
|
|
1922
|
+
if (!key) continue;
|
|
1923
|
+
if (pre.scrollLeft === 0 && pre.scrollTop === 0) continue;
|
|
1924
|
+
map.set(key, { scrollLeft: pre.scrollLeft, scrollTop: pre.scrollTop });
|
|
1925
|
+
}
|
|
1926
|
+
return map.size > 0 ? map : null;
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
function restoreCodeBlockScrolls(snapshot) {
|
|
1930
|
+
if (!snapshot || typeof document === "undefined") return;
|
|
1931
|
+
for (const pre of document.querySelectorAll(".markdown pre")) {
|
|
1932
|
+
const key = pre.textContent ? pre.textContent.trim() : "";
|
|
1933
|
+
const saved = key ? snapshot.get(key) : null;
|
|
1934
|
+
if (!saved) continue;
|
|
1935
|
+
if (saved.scrollLeft) pre.scrollLeft = saved.scrollLeft;
|
|
1936
|
+
if (saved.scrollTop) pre.scrollTop = saved.scrollTop;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1261
1940
|
async function renderShell() {
|
|
1941
|
+
syncVisualViewportMetrics();
|
|
1262
1942
|
const desktop = isDesktopLayout();
|
|
1263
1943
|
const shouldShowDetail = state.currentTab !== "settings" && state.currentItem && (desktop || state.detailOpen);
|
|
1264
1944
|
let detail = null;
|
|
@@ -1277,6 +1957,8 @@ async function renderShell() {
|
|
|
1277
1957
|
.filter(Boolean)
|
|
1278
1958
|
.join(" ");
|
|
1279
1959
|
|
|
1960
|
+
const codeBlockScrollSnapshot = snapshotCodeBlockScrolls();
|
|
1961
|
+
|
|
1280
1962
|
app.innerHTML = `
|
|
1281
1963
|
<div class="${shellClassName}">
|
|
1282
1964
|
${desktop ? renderDesktopHeader(detail) : renderMobileTopBar(detail)}
|
|
@@ -1291,13 +1973,21 @@ async function renderShell() {
|
|
|
1291
1973
|
${renderLogoutConfirmModal()}
|
|
1292
1974
|
${renderHazbaseLogoutConfirmModal()}
|
|
1293
1975
|
</div>
|
|
1976
|
+
${!desktop && detail ? renderCompletionReplySheet(detail) : ""}
|
|
1294
1977
|
`;
|
|
1295
1978
|
|
|
1296
1979
|
bindShellInteractions();
|
|
1980
|
+
if (state.completionReplySheetToken) {
|
|
1981
|
+
resetHorizontalViewportScroll();
|
|
1982
|
+
}
|
|
1297
1983
|
applyPendingDetailScrollReset();
|
|
1298
1984
|
applyPendingListScrollRestore();
|
|
1299
1985
|
applyPendingSettingsSubpageScrollReset();
|
|
1300
1986
|
applyPendingSettingsScrollRestore();
|
|
1987
|
+
// Reapply any horizontal scroll the user dragged into a code block before
|
|
1988
|
+
// this re-render. Done after the imperative scroll resets above so they
|
|
1989
|
+
// can't fight each other.
|
|
1990
|
+
restoreCodeBlockScrolls(codeBlockScrollSnapshot);
|
|
1301
1991
|
requestAnimationFrame(dismissBootSplash);
|
|
1302
1992
|
}
|
|
1303
1993
|
|
|
@@ -1361,6 +2051,9 @@ function clearThreadFilterInteraction() {
|
|
|
1361
2051
|
|
|
1362
2052
|
function shouldDeferRenderForActiveInteraction() {
|
|
1363
2053
|
const activeElement = document.activeElement;
|
|
2054
|
+
if (state.completionReplySheetToken) {
|
|
2055
|
+
return true;
|
|
2056
|
+
}
|
|
1364
2057
|
if (
|
|
1365
2058
|
activeElement instanceof HTMLTextAreaElement &&
|
|
1366
2059
|
activeElement.matches("[data-completion-reply-textarea]") &&
|
|
@@ -1728,7 +2421,6 @@ function renderMobileTopBar(detail) {
|
|
|
1728
2421
|
return `
|
|
1729
2422
|
<header class="mobile-topbar">
|
|
1730
2423
|
<div class="mobile-topbar__heading">
|
|
1731
|
-
<span class="eyebrow-pill eyebrow-pill--quiet">${escapeHtml(L("common.appName"))}</span>
|
|
1732
2424
|
<h1 class="mobile-topbar__title">${escapeHtml(meta.title)}</h1>
|
|
1733
2425
|
</div>
|
|
1734
2426
|
</header>
|
|
@@ -1783,7 +2475,9 @@ async function fetchCurrentDetailForItem(itemRef = state.currentItem) {
|
|
|
1783
2475
|
return state.detailOverride.detail;
|
|
1784
2476
|
}
|
|
1785
2477
|
try {
|
|
1786
|
-
const detail = await
|
|
2478
|
+
const detail = await hydrateDetailImages(
|
|
2479
|
+
await apiGet(`/api/items/${encodeURIComponent(itemRef.kind)}/${encodeURIComponent(itemRef.token)}`)
|
|
2480
|
+
);
|
|
1787
2481
|
if (hasLaunchItemIntent(itemRef)) {
|
|
1788
2482
|
state.launchItemIntent.status = "loaded";
|
|
1789
2483
|
}
|
|
@@ -1797,7 +2491,9 @@ async function fetchCurrentDetailForItem(itemRef = state.currentItem) {
|
|
|
1797
2491
|
}
|
|
1798
2492
|
await refreshInbox();
|
|
1799
2493
|
try {
|
|
1800
|
-
const detail = await
|
|
2494
|
+
const detail = await hydrateDetailImages(
|
|
2495
|
+
await apiGet(`/api/items/${encodeURIComponent(itemRef.kind)}/${encodeURIComponent(itemRef.token)}`)
|
|
2496
|
+
);
|
|
1801
2497
|
if (hasLaunchItemIntent(itemRef)) {
|
|
1802
2498
|
state.launchItemIntent.status = "loaded";
|
|
1803
2499
|
}
|
|
@@ -3230,7 +3926,7 @@ function buildSettingsContext() {
|
|
|
3230
3926
|
standalone,
|
|
3231
3927
|
supportsPushValue,
|
|
3232
3928
|
permission,
|
|
3233
|
-
subscribed: push.
|
|
3929
|
+
subscribed: push.subscribed === true,
|
|
3234
3930
|
});
|
|
3235
3931
|
|
|
3236
3932
|
return {
|
|
@@ -3256,6 +3952,7 @@ function buildSettingsContext() {
|
|
|
3256
3952
|
a2aRelay: state.a2aRelayStatus,
|
|
3257
3953
|
a2aShare: state.a2aShareStatus,
|
|
3258
3954
|
hazbase: state.hazbaseStatus,
|
|
3955
|
+
remotePairing: state.remotePairingStatus,
|
|
3259
3956
|
};
|
|
3260
3957
|
}
|
|
3261
3958
|
|
|
@@ -3358,6 +4055,195 @@ function collectSettingsDiagnostics({ permission, secureContext, standalone, sup
|
|
|
3358
4055
|
return Array.from(new Set(issues.filter(Boolean)));
|
|
3359
4056
|
}
|
|
3360
4057
|
|
|
4058
|
+
function hasAutoPilotWriteLaneEnabled(session = state.session) {
|
|
4059
|
+
return Boolean(
|
|
4060
|
+
session?.autoPilotTrustedWrites === true ||
|
|
4061
|
+
session?.autoPilotWriteLaneContent === true ||
|
|
4062
|
+
session?.autoPilotWriteLaneUiTests === true ||
|
|
4063
|
+
session?.autoPilotWriteLaneSource === true,
|
|
4064
|
+
);
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
function hasAutoPilotEnabled(session = state.session) {
|
|
4068
|
+
return Boolean(session?.autoPilotTrustedReads === true || hasAutoPilotWriteLaneEnabled(session));
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
function settingsEnabledValue(enabled) {
|
|
4072
|
+
return enabled ? L("common.enabled") : L("common.disabled");
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
function settingsNeedsActionValue() {
|
|
4076
|
+
return L("settings.status.actionNeeded");
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4079
|
+
function settingsDisconnectedValue() {
|
|
4080
|
+
return L("settings.status.disconnected");
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
function settingsSupportedValue(supported) {
|
|
4084
|
+
return supported ? L("settings.status.supported") : L("settings.status.unsupported");
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
function settingsInstalledValue(installed) {
|
|
4088
|
+
return installed ? L("settings.status.installed") : L("settings.status.notInstalled");
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
function settingsNotificationRootValue(context) {
|
|
4092
|
+
if (context.push?.serverSubscribed === true) {
|
|
4093
|
+
return L("common.enabled");
|
|
4094
|
+
}
|
|
4095
|
+
if (context.push?.clientSubscribed === true) {
|
|
4096
|
+
return settingsNeedsActionValue();
|
|
4097
|
+
}
|
|
4098
|
+
if (context.permission === "denied") {
|
|
4099
|
+
return settingsNeedsActionValue();
|
|
4100
|
+
}
|
|
4101
|
+
return L("common.disabled");
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
function notificationPermissionValue(permission) {
|
|
4105
|
+
const key = `settings.permission.${permission || "default"}`;
|
|
4106
|
+
const translated = L(key);
|
|
4107
|
+
return translated === key ? String(permission || "") : translated;
|
|
4108
|
+
}
|
|
4109
|
+
|
|
4110
|
+
function notificationReceiveValue(push) {
|
|
4111
|
+
if (push?.serverSubscribed === true) {
|
|
4112
|
+
return L("common.enabled");
|
|
4113
|
+
}
|
|
4114
|
+
if (push?.clientSubscribed === true) {
|
|
4115
|
+
return L("settings.status.actionNeeded");
|
|
4116
|
+
}
|
|
4117
|
+
return L("common.disabled");
|
|
4118
|
+
}
|
|
4119
|
+
|
|
4120
|
+
function notificationOverallValue(context) {
|
|
4121
|
+
if (context.push?.serverSubscribed === true) {
|
|
4122
|
+
return L("common.enabled");
|
|
4123
|
+
}
|
|
4124
|
+
if (context.permission === "denied") {
|
|
4125
|
+
return L("settings.status.blocked");
|
|
4126
|
+
}
|
|
4127
|
+
if (!context.serverEnabled) {
|
|
4128
|
+
return L("settings.status.notAvailable");
|
|
4129
|
+
}
|
|
4130
|
+
if (!context.supportsPushValue) {
|
|
4131
|
+
return L("settings.status.unsupported");
|
|
4132
|
+
}
|
|
4133
|
+
if (!context.secureContext || !context.standalone || context.permission !== "granted") {
|
|
4134
|
+
return L("settings.status.actionNeeded");
|
|
4135
|
+
}
|
|
4136
|
+
return L("common.disabled");
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
function settingsMoltbookRootValue(context) {
|
|
4140
|
+
return context.moltbookScout?.enabled === true
|
|
4141
|
+
? L("common.enabled")
|
|
4142
|
+
: settingsNeedsActionValue();
|
|
4143
|
+
}
|
|
4144
|
+
|
|
4145
|
+
function settingsA2aRelayRootValue(context) {
|
|
4146
|
+
const relay = context.a2aRelay;
|
|
4147
|
+
if (relay?.enabled === true && relay?.connected === true) {
|
|
4148
|
+
return L("common.enabled");
|
|
4149
|
+
}
|
|
4150
|
+
return settingsDisconnectedValue();
|
|
4151
|
+
}
|
|
4152
|
+
|
|
4153
|
+
function settingsA2aShareRootValue(context) {
|
|
4154
|
+
const share = context.a2aShare;
|
|
4155
|
+
if (share?.enabled === true && !share?.error) {
|
|
4156
|
+
return L("common.enabled");
|
|
4157
|
+
}
|
|
4158
|
+
return settingsNeedsActionValue();
|
|
4159
|
+
}
|
|
4160
|
+
|
|
4161
|
+
function settingsWalletRootValue(context) {
|
|
4162
|
+
if (context.hazbase?.enabled !== true) {
|
|
4163
|
+
return L("common.disabled");
|
|
4164
|
+
}
|
|
4165
|
+
return deriveHazbaseWalletFlow(context.hazbase).coreReady
|
|
4166
|
+
? L("common.enabled")
|
|
4167
|
+
: settingsNeedsActionValue();
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
function isRemotePairingSessionConnected(session) {
|
|
4171
|
+
const value = normalizeClientText(session?.state).toLowerCase();
|
|
4172
|
+
return value === "connected" || value === "open" || value === "running";
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
function isRemotePairingSessionReachable(session) {
|
|
4176
|
+
const value = normalizeClientText(session?.state).toLowerCase();
|
|
4177
|
+
return value === "connected" || value === "open" || value === "running" || value === "opening" || value === "handshaking" || value === "resuming";
|
|
4178
|
+
}
|
|
4179
|
+
|
|
4180
|
+
function remotePairingStatusModel(status, opts = {}) {
|
|
4181
|
+
const enabled = status?.enabled === true;
|
|
4182
|
+
const sessions = Array.isArray(status?.sessions) ? status.sessions : [];
|
|
4183
|
+
const pairings = Array.isArray(status?.pairings) ? status.pairings : [];
|
|
4184
|
+
const usingRelay = opts.usingRelay === true;
|
|
4185
|
+
if (!enabled) {
|
|
4186
|
+
return { key: "disabled", tone: "muted", label: L("settings.remotePairing.status.disabled") };
|
|
4187
|
+
}
|
|
4188
|
+
if (usingRelay && sessions.some(isRemotePairingSessionConnected)) {
|
|
4189
|
+
return { key: "connected", tone: "success", label: L("settings.remotePairing.status.connected") };
|
|
4190
|
+
}
|
|
4191
|
+
if (sessions.some(isRemotePairingSessionReachable)) {
|
|
4192
|
+
return { key: "available", tone: "success", label: L("settings.remotePairing.status.available") };
|
|
4193
|
+
}
|
|
4194
|
+
if (pairings.length > 0) {
|
|
4195
|
+
return { key: "waiting", tone: "warning", label: L("settings.remotePairing.status.waiting") };
|
|
4196
|
+
}
|
|
4197
|
+
return { key: "waiting", tone: "muted", label: L("settings.remotePairing.status.waiting") };
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
function remotePairingAuditTypeLabel(type) {
|
|
4201
|
+
const key = `settings.remotePairing.audit.type.${type || "unknown"}`;
|
|
4202
|
+
const translated = L(key);
|
|
4203
|
+
return translated === key ? L("settings.remotePairing.audit.type.unknown") : translated;
|
|
4204
|
+
}
|
|
4205
|
+
|
|
4206
|
+
function remotePairingAuditOutcomeTone(outcome) {
|
|
4207
|
+
if (outcome === "success") return "success";
|
|
4208
|
+
if (outcome === "failure") return "danger";
|
|
4209
|
+
return "muted";
|
|
4210
|
+
}
|
|
4211
|
+
|
|
4212
|
+
function remotePairingAuditMeta(event) {
|
|
4213
|
+
return [
|
|
4214
|
+
event?.label,
|
|
4215
|
+
event?.phoneFingerprint,
|
|
4216
|
+
event?.relayHost,
|
|
4217
|
+
event?.reason,
|
|
4218
|
+
].filter(Boolean).join(" / ");
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
function renderRemotePairingAudit(events) {
|
|
4222
|
+
const list = Array.isArray(events) ? events.slice(0, 10) : [];
|
|
4223
|
+
if (!list.length) {
|
|
4224
|
+
return `<div class="settings-copy-block"><p class="muted">${escapeHtml(L("settings.remotePairing.audit.empty"))}</p></div>`;
|
|
4225
|
+
}
|
|
4226
|
+
return `
|
|
4227
|
+
<div class="settings-remote-audit-list">
|
|
4228
|
+
${list.map((event) => {
|
|
4229
|
+
const tone = remotePairingAuditOutcomeTone(event?.outcome);
|
|
4230
|
+
const title = remotePairingAuditTypeLabel(event?.type);
|
|
4231
|
+
const meta = remotePairingAuditMeta(event);
|
|
4232
|
+
return `
|
|
4233
|
+
<article class="settings-remote-audit-item">
|
|
4234
|
+
<span class="settings-remote-audit-dot settings-remote-audit-dot--${escapeHtml(tone)}" aria-hidden="true"></span>
|
|
4235
|
+
<div class="settings-remote-audit-body">
|
|
4236
|
+
<p class="settings-remote-audit-title">${escapeHtml(title)}</p>
|
|
4237
|
+
${meta ? `<p class="settings-remote-audit-meta">${escapeHtml(meta)}</p>` : ""}
|
|
4238
|
+
</div>
|
|
4239
|
+
<time class="settings-remote-audit-time">${escapeHtml(formatSettingsTimestamp(event?.atMs))}</time>
|
|
4240
|
+
</article>
|
|
4241
|
+
`;
|
|
4242
|
+
}).join("")}
|
|
4243
|
+
</div>
|
|
4244
|
+
`;
|
|
4245
|
+
}
|
|
4246
|
+
|
|
3361
4247
|
function settingsPageMeta(page) {
|
|
3362
4248
|
switch (page) {
|
|
3363
4249
|
case "notifications":
|
|
@@ -3421,14 +4307,14 @@ function settingsPageMeta(page) {
|
|
|
3421
4307
|
id: "a2aRelay",
|
|
3422
4308
|
title: L("settings.a2aRelay.title"),
|
|
3423
4309
|
description: L("settings.a2aRelay.copy"),
|
|
3424
|
-
icon: "
|
|
4310
|
+
icon: "agent-network",
|
|
3425
4311
|
};
|
|
3426
4312
|
case "a2aShare":
|
|
3427
4313
|
return {
|
|
3428
4314
|
id: "a2aShare",
|
|
3429
4315
|
title: L("settings.a2aShare.title"),
|
|
3430
4316
|
description: L("settings.a2aShare.copy"),
|
|
3431
|
-
icon: "
|
|
4317
|
+
icon: "file-event",
|
|
3432
4318
|
};
|
|
3433
4319
|
case "wallet":
|
|
3434
4320
|
return {
|
|
@@ -3437,6 +4323,13 @@ function settingsPageMeta(page) {
|
|
|
3437
4323
|
description: L("settings.wallet.copy"),
|
|
3438
4324
|
icon: "coin",
|
|
3439
4325
|
};
|
|
4326
|
+
case "remotePairing":
|
|
4327
|
+
return {
|
|
4328
|
+
id: "remotePairing",
|
|
4329
|
+
title: L("settings.remotePairing.title"),
|
|
4330
|
+
description: L("settings.remotePairing.copy"),
|
|
4331
|
+
icon: "remote-connection",
|
|
4332
|
+
};
|
|
3440
4333
|
case "a2aExecutor":
|
|
3441
4334
|
// Executor settings integrated into a2aRelay page — redirect.
|
|
3442
4335
|
return settingsPageMeta("a2aRelay");
|
|
@@ -3452,7 +4345,7 @@ function renderSettingsRoot(context, { mobile }) {
|
|
|
3452
4345
|
page: "notifications",
|
|
3453
4346
|
icon: "notifications",
|
|
3454
4347
|
title: L("settings.notifications.title"),
|
|
3455
|
-
value:
|
|
4348
|
+
value: settingsNotificationRootValue(context),
|
|
3456
4349
|
}),
|
|
3457
4350
|
renderSettingsNavRow({
|
|
3458
4351
|
page: "language",
|
|
@@ -3465,23 +4358,20 @@ function renderSettingsRoot(context, { mobile }) {
|
|
|
3465
4358
|
page: "install",
|
|
3466
4359
|
icon: "homescreen",
|
|
3467
4360
|
title: L("settings.install.title"),
|
|
3468
|
-
value:
|
|
4361
|
+
value: settingsEnabledValue(context.standalone),
|
|
3469
4362
|
})
|
|
3470
4363
|
: "",
|
|
3471
4364
|
renderSettingsNavRow({
|
|
3472
4365
|
page: "awayMode",
|
|
3473
4366
|
icon: "settings",
|
|
3474
4367
|
title: L("settings.awayMode.title"),
|
|
3475
|
-
value: state.session?.claudeAwayMode === true
|
|
4368
|
+
value: settingsEnabledValue(state.session?.claudeAwayMode === true),
|
|
3476
4369
|
}),
|
|
3477
4370
|
renderSettingsNavRow({
|
|
3478
4371
|
page: "autoPilot",
|
|
3479
4372
|
icon: "approval",
|
|
3480
4373
|
title: L("settings.autoPilot.title"),
|
|
3481
|
-
value:
|
|
3482
|
-
state.session?.autoPilotTrustedReads === true || state.session?.autoPilotTrustedWrites === true
|
|
3483
|
-
? L("common.enabled")
|
|
3484
|
-
: L("common.disabled"),
|
|
4374
|
+
value: settingsEnabledValue(hasAutoPilotEnabled()),
|
|
3485
4375
|
}),
|
|
3486
4376
|
].filter(Boolean);
|
|
3487
4377
|
const deviceRows = [
|
|
@@ -3493,6 +4383,12 @@ function renderSettingsRoot(context, { mobile }) {
|
|
|
3493
4383
|
? L("settings.device.count", { count: context.devices.length })
|
|
3494
4384
|
: L("settings.pairing.connected"),
|
|
3495
4385
|
}),
|
|
4386
|
+
state.session?.remotePairingAvailable ? renderSettingsNavRow({
|
|
4387
|
+
page: "remotePairing",
|
|
4388
|
+
icon: "remote-connection",
|
|
4389
|
+
title: L("settings.remotePairing.title"),
|
|
4390
|
+
value: settingsEnabledValue(context.remotePairing?.enabled === true),
|
|
4391
|
+
}) : "",
|
|
3496
4392
|
];
|
|
3497
4393
|
const advancedRows = [
|
|
3498
4394
|
renderSettingsNavRow({
|
|
@@ -3523,32 +4419,26 @@ function renderSettingsRoot(context, { mobile }) {
|
|
|
3523
4419
|
page: "moltbook",
|
|
3524
4420
|
icon: "item",
|
|
3525
4421
|
title: L("settings.moltbook.title"),
|
|
3526
|
-
|
|
3527
|
-
value: context.moltbookScout?.enabled ? L("settings.status.enabled") : L("settings.status.disabled"),
|
|
4422
|
+
value: settingsMoltbookRootValue(context),
|
|
3528
4423
|
}) : "",
|
|
3529
4424
|
state.session?.a2aRelayEnabled ? renderSettingsNavRow({
|
|
3530
4425
|
page: "a2aRelay",
|
|
3531
|
-
icon: "
|
|
4426
|
+
icon: "agent-network",
|
|
3532
4427
|
title: L("settings.a2aRelay.title"),
|
|
3533
|
-
|
|
3534
|
-
value: context.a2aRelay?.connected ? L("settings.status.connected") : L("settings.a2aRelay.status.disconnected"),
|
|
4428
|
+
value: settingsA2aRelayRootValue(context),
|
|
3535
4429
|
}) : "",
|
|
3536
4430
|
state.session?.a2aShareEnabled ? renderSettingsNavRow({
|
|
3537
4431
|
page: "a2aShare",
|
|
3538
|
-
icon: "
|
|
4432
|
+
icon: "file-event",
|
|
3539
4433
|
title: L("settings.a2aShare.title"),
|
|
3540
|
-
|
|
3541
|
-
value: context.a2aShare?.enabled ? L("settings.status.enabled") : L("settings.status.disabled"),
|
|
4434
|
+
value: settingsA2aShareRootValue(context),
|
|
3542
4435
|
}) : "",
|
|
3543
4436
|
context.hazbase?.enabled ? renderSettingsNavRow({
|
|
3544
4437
|
page: "wallet",
|
|
3545
4438
|
icon: "coin",
|
|
3546
4439
|
title: L("settings.wallet.title"),
|
|
3547
4440
|
badge: "beta",
|
|
3548
|
-
|
|
3549
|
-
value: context.hazbase?.sessionInvalid
|
|
3550
|
-
? L("settings.hazbase.status.sessionExpired")
|
|
3551
|
-
: context.hazbase?.signedIn ? L("settings.hazbase.status.signedIn") : L("settings.hazbase.status.signedOut"),
|
|
4441
|
+
value: settingsWalletRootValue(context),
|
|
3552
4442
|
}) : "",
|
|
3553
4443
|
].filter(Boolean)) : ""}
|
|
3554
4444
|
${renderSettingsGroup(L("settings.pairing.title"), deviceRows)}
|
|
@@ -3610,6 +4500,9 @@ function renderSettingsSubpage(context, { mobile }) {
|
|
|
3610
4500
|
case "wallet":
|
|
3611
4501
|
content = renderSettingsWalletPage(context);
|
|
3612
4502
|
break;
|
|
4503
|
+
case "remotePairing":
|
|
4504
|
+
content = renderSettingsRemotePairingPage(context);
|
|
4505
|
+
break;
|
|
3613
4506
|
default:
|
|
3614
4507
|
content = "";
|
|
3615
4508
|
}
|
|
@@ -3624,11 +4517,14 @@ function renderSettingsSubpage(context, { mobile }) {
|
|
|
3624
4517
|
}
|
|
3625
4518
|
|
|
3626
4519
|
function renderSettingsNotificationsPage(context) {
|
|
3627
|
-
const { push, permission,
|
|
4520
|
+
const { push, permission, standalone } = context;
|
|
4521
|
+
const notificationEnabled = push.serverSubscribed === true;
|
|
3628
4522
|
const statusRows = [
|
|
3629
|
-
renderSettingsInfoRow(L("settings.row.status"),
|
|
3630
|
-
renderSettingsInfoRow(L("settings.row.notificationPermission"),
|
|
3631
|
-
|
|
4523
|
+
renderSettingsInfoRow(L("settings.row.status"), notificationOverallValue(context)),
|
|
4524
|
+
renderSettingsInfoRow(L("settings.row.notificationPermission"), notificationPermissionValue(permission), {
|
|
4525
|
+
valueTone: permission === "granted" ? "enabled" : permission === "denied" ? "attention" : "disabled",
|
|
4526
|
+
}),
|
|
4527
|
+
renderSettingsInfoRow(L("settings.row.currentDeviceSubscribed"), notificationReceiveValue(push)),
|
|
3632
4528
|
push.lastSuccessfulDeliveryAtMs
|
|
3633
4529
|
? renderSettingsInfoRow(
|
|
3634
4530
|
L("settings.row.lastSuccessfulDelivery"),
|
|
@@ -3639,12 +4535,6 @@ function renderSettingsNotificationsPage(context) {
|
|
|
3639
4535
|
return `
|
|
3640
4536
|
<div class="settings-page">
|
|
3641
4537
|
${renderSettingsGroup("", statusRows)}
|
|
3642
|
-
${renderSettingsGroup(L("settings.group.advanced"), [
|
|
3643
|
-
renderSettingsInfoRow(L("settings.row.serverWebPush"), serverEnabled ? L("common.enabled") : L("common.disabled")),
|
|
3644
|
-
renderSettingsInfoRow(L("settings.row.secureContext"), secureContext ? L("common.supported") : L("common.notSupported")),
|
|
3645
|
-
renderSettingsInfoRow(L("settings.row.homeScreenApp"), standalone ? L("common.supported") : L("common.notSupported")),
|
|
3646
|
-
renderSettingsInfoRow(L("settings.row.browserSupport"), supportsPushValue ? L("common.supported") : L("common.notSupported")),
|
|
3647
|
-
])}
|
|
3648
4538
|
${state.pushNotice ? `<p class="inline-alert inline-alert--success">${escapeHtml(state.pushNotice)}</p>` : ""}
|
|
3649
4539
|
${state.pushError ? `<p class="inline-alert inline-alert--danger">${escapeHtml(state.pushError)}</p>` : ""}
|
|
3650
4540
|
${renderSettingsActionPanel(renderSettingsNotificationActions({
|
|
@@ -3737,17 +4627,13 @@ function renderSettingsAdvancedPage(context) {
|
|
|
3737
4627
|
${context.diagnostics.map((message) => `<p class="inline-alert">${escapeHtml(message)}</p>`).join("")}
|
|
3738
4628
|
${renderSettingsGroup("", [
|
|
3739
4629
|
renderSettingsInfoRow(L("settings.row.serverWebPush"), context.serverEnabled ? L("common.enabled") : L("common.disabled")),
|
|
3740
|
-
renderSettingsInfoRow(L("settings.row.secureContext"), context.secureContext ? L("common.
|
|
3741
|
-
renderSettingsInfoRow(L("settings.row.homeScreenApp"), context.standalone
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
renderSettingsInfoRow(L("settings.row.
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
L("settings.row.lastSuccessfulDelivery"),
|
|
3748
|
-
new Date(context.push.lastSuccessfulDeliveryAtMs).toLocaleString(state.locale)
|
|
3749
|
-
)
|
|
3750
|
-
: "",
|
|
4630
|
+
renderSettingsInfoRow(L("settings.row.secureContext"), context.secureContext ? L("common.enabled") : L("common.disabled")),
|
|
4631
|
+
renderSettingsInfoRow(L("settings.row.homeScreenApp"), settingsInstalledValue(context.standalone), {
|
|
4632
|
+
valueTone: context.standalone ? "enabled" : "disabled",
|
|
4633
|
+
}),
|
|
4634
|
+
renderSettingsInfoRow(L("settings.row.browserSupport"), settingsSupportedValue(context.supportsPushValue), {
|
|
4635
|
+
valueTone: context.supportsPushValue ? "enabled" : "disabled",
|
|
4636
|
+
}),
|
|
3751
4637
|
renderSettingsInfoRow(L("settings.row.version"), state.appVersion || L("common.unavailable")),
|
|
3752
4638
|
].filter(Boolean), { listClassName: "settings-list settings-list--compact" })}
|
|
3753
4639
|
${versionNotice}
|
|
@@ -3807,7 +4693,37 @@ function renderSettingsGroup(title, rows, options = {}) {
|
|
|
3807
4693
|
`;
|
|
3808
4694
|
}
|
|
3809
4695
|
|
|
3810
|
-
function
|
|
4696
|
+
function settingsNavValueTone(value, explicitTone = "") {
|
|
4697
|
+
if (explicitTone) {
|
|
4698
|
+
return explicitTone;
|
|
4699
|
+
}
|
|
4700
|
+
const text = String(value || "").trim();
|
|
4701
|
+
if (!text) {
|
|
4702
|
+
return "";
|
|
4703
|
+
}
|
|
4704
|
+
if (text === L("common.enabled") || text === L("settings.status.enabled")) {
|
|
4705
|
+
return "enabled";
|
|
4706
|
+
}
|
|
4707
|
+
if (text === L("common.disabled") || text === L("settings.status.disabled")) {
|
|
4708
|
+
return "disabled";
|
|
4709
|
+
}
|
|
4710
|
+
if (text === L("settings.status.actionNeeded") || text === L("settings.status.blocked")) {
|
|
4711
|
+
return "attention";
|
|
4712
|
+
}
|
|
4713
|
+
if (text === L("settings.status.supported") || text === L("settings.status.installed")) {
|
|
4714
|
+
return "enabled";
|
|
4715
|
+
}
|
|
4716
|
+
if (text === L("settings.status.disconnected")) {
|
|
4717
|
+
return "disconnected";
|
|
4718
|
+
}
|
|
4719
|
+
return "";
|
|
4720
|
+
}
|
|
4721
|
+
|
|
4722
|
+
function renderSettingsNavRow({ page, icon, title, badge, subtitle, value, valueTone }) {
|
|
4723
|
+
const tone = settingsNavValueTone(value, valueTone);
|
|
4724
|
+
const valueClass = tone
|
|
4725
|
+
? `settings-row__value settings-row__value--${escapeHtml(tone)}`
|
|
4726
|
+
: "settings-row__value";
|
|
3811
4727
|
return `
|
|
3812
4728
|
<button class="settings-nav-row" type="button" data-settings-subpage="${escapeHtml(page)}">
|
|
3813
4729
|
<span class="settings-row__icon" aria-hidden="true">${renderIcon(icon)}</span>
|
|
@@ -3818,7 +4734,7 @@ function renderSettingsNavRow({ page, icon, title, badge, subtitle, value }) {
|
|
|
3818
4734
|
</span>
|
|
3819
4735
|
${subtitle ? `<span class="settings-row__subtitle">${escapeHtml(subtitle)}</span>` : ""}
|
|
3820
4736
|
</span>
|
|
3821
|
-
<span class="
|
|
4737
|
+
<span class="${valueClass}">${escapeHtml(value || "")}</span>
|
|
3822
4738
|
<span class="settings-row__chevron" aria-hidden="true">${renderIcon("chevron-right")}</span>
|
|
3823
4739
|
</button>
|
|
3824
4740
|
`;
|
|
@@ -3830,16 +4746,18 @@ function renderSettingsAwayModePage() {
|
|
|
3830
4746
|
return `
|
|
3831
4747
|
<div class="settings-page">
|
|
3832
4748
|
${renderSettingsGroup("", [`
|
|
3833
|
-
<label class="reply-mode-switch" data-claude-away-toggle>
|
|
4749
|
+
<label class="reply-mode-switch reply-mode-switch--settings" data-claude-away-toggle>
|
|
3834
4750
|
<input type="checkbox" class="reply-mode-switch__input" ${enabled ? "checked" : ""} data-claude-away-checkbox />
|
|
3835
|
-
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3836
4751
|
<span class="reply-mode-switch__copy">
|
|
3837
4752
|
<span class="reply-mode-switch__title">
|
|
3838
4753
|
<span>${escapeHtml(L("settings.claudeAway.title"))}</span>
|
|
3839
|
-
<span class="reply-mode-switch__state">${escapeHtml(stateLabel)}</span>
|
|
3840
4754
|
</span>
|
|
3841
4755
|
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.claudeAway.description"))}</span>
|
|
3842
4756
|
</span>
|
|
4757
|
+
<span class="reply-mode-switch--settings__toggle">
|
|
4758
|
+
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
4759
|
+
<span class="reply-mode-switch__state">${escapeHtml(stateLabel)}</span>
|
|
4760
|
+
</span>
|
|
3843
4761
|
</label>
|
|
3844
4762
|
`])}
|
|
3845
4763
|
<p class="settings-page-copy muted">${escapeHtml(L("settings.awayMode.codexNote"))}</p>
|
|
@@ -3853,24 +4771,25 @@ function renderSettingsAutoPilotPage() {
|
|
|
3853
4771
|
const writeLaneContentEnabled = state.session?.autoPilotWriteLaneContent === true;
|
|
3854
4772
|
const writeLaneUiTestsEnabled = state.session?.autoPilotWriteLaneUiTests === true;
|
|
3855
4773
|
const writeLaneSourceEnabled = state.session?.autoPilotWriteLaneSource === true;
|
|
3856
|
-
const trustedWritesEnabled =
|
|
3857
|
-
writeLaneContentEnabled || writeLaneUiTestsEnabled || writeLaneSourceEnabled || state.session?.autoPilotTrustedWrites === true;
|
|
4774
|
+
const trustedWritesEnabled = hasAutoPilotWriteLaneEnabled();
|
|
3858
4775
|
const trustedWritesStateLabel = trustedWritesEnabled ? L("common.enabled") : L("common.disabled");
|
|
3859
4776
|
const recentEntries = recentAutoPilotEntries();
|
|
3860
4777
|
const suggestions = recentAutoPilotSuggestions();
|
|
3861
4778
|
return `
|
|
3862
4779
|
<div class="settings-page">
|
|
3863
4780
|
${renderSettingsGroup("", [`
|
|
3864
|
-
<label class="reply-mode-switch reply-mode-switch--grouped" data-auto-pilot-toggle>
|
|
4781
|
+
<label class="reply-mode-switch reply-mode-switch--settings reply-mode-switch--grouped" data-auto-pilot-toggle>
|
|
3865
4782
|
<input type="checkbox" class="reply-mode-switch__input" ${trustedReadsEnabled ? "checked" : ""} data-auto-pilot-checkbox />
|
|
3866
|
-
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3867
4783
|
<span class="reply-mode-switch__copy">
|
|
3868
4784
|
<span class="reply-mode-switch__title">
|
|
3869
4785
|
<span>${escapeHtml(L("settings.autoPilot.trustedReadsTitle"))}</span>
|
|
3870
|
-
<span class="reply-mode-switch__state">${escapeHtml(trustedReadsStateLabel)}</span>
|
|
3871
4786
|
</span>
|
|
3872
4787
|
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.autoPilot.trustedReadsDescription"))}</span>
|
|
3873
4788
|
</span>
|
|
4789
|
+
<span class="reply-mode-switch--settings__toggle">
|
|
4790
|
+
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
4791
|
+
<span class="reply-mode-switch__state">${escapeHtml(trustedReadsStateLabel)}</span>
|
|
4792
|
+
</span>
|
|
3874
4793
|
</label>
|
|
3875
4794
|
`, `
|
|
3876
4795
|
<div class="settings-toggle-subhead" role="presentation">
|
|
@@ -3880,40 +4799,46 @@ function renderSettingsAutoPilotPage() {
|
|
|
3880
4799
|
`, `
|
|
3881
4800
|
<div class="settings-toggle-subcopy muted">${escapeHtml(L("settings.autoPilot.trustedWritesDescription"))}</div>
|
|
3882
4801
|
`, `
|
|
3883
|
-
<label class="reply-mode-switch reply-mode-switch--grouped" data-auto-pilot-write-lane="content">
|
|
4802
|
+
<label class="reply-mode-switch reply-mode-switch--settings reply-mode-switch--grouped" data-auto-pilot-write-lane="content">
|
|
3884
4803
|
<input type="checkbox" class="reply-mode-switch__input" ${writeLaneContentEnabled ? "checked" : ""} data-auto-pilot-write-lane-checkbox="content" />
|
|
3885
|
-
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3886
4804
|
<span class="reply-mode-switch__copy">
|
|
3887
4805
|
<span class="reply-mode-switch__title">
|
|
3888
4806
|
<span>${escapeHtml(L("settings.autoPilot.writeLaneContentTitle"))}</span>
|
|
3889
|
-
<span class="reply-mode-switch__state">${escapeHtml(writeLaneContentEnabled ? L("common.enabled") : L("common.disabled"))}</span>
|
|
3890
4807
|
</span>
|
|
3891
4808
|
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.autoPilot.writeLaneContentDescription"))}</span>
|
|
3892
4809
|
</span>
|
|
4810
|
+
<span class="reply-mode-switch--settings__toggle">
|
|
4811
|
+
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
4812
|
+
<span class="reply-mode-switch__state">${escapeHtml(writeLaneContentEnabled ? L("common.enabled") : L("common.disabled"))}</span>
|
|
4813
|
+
</span>
|
|
3893
4814
|
</label>
|
|
3894
4815
|
`, `
|
|
3895
|
-
<label class="reply-mode-switch reply-mode-switch--grouped" data-auto-pilot-write-lane="ui-tests">
|
|
4816
|
+
<label class="reply-mode-switch reply-mode-switch--settings reply-mode-switch--grouped" data-auto-pilot-write-lane="ui-tests">
|
|
3896
4817
|
<input type="checkbox" class="reply-mode-switch__input" ${writeLaneUiTestsEnabled ? "checked" : ""} data-auto-pilot-write-lane-checkbox="ui-tests" />
|
|
3897
|
-
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3898
4818
|
<span class="reply-mode-switch__copy">
|
|
3899
4819
|
<span class="reply-mode-switch__title">
|
|
3900
4820
|
<span>${escapeHtml(L("settings.autoPilot.writeLaneUiTestsTitle"))}</span>
|
|
3901
|
-
<span class="reply-mode-switch__state">${escapeHtml(writeLaneUiTestsEnabled ? L("common.enabled") : L("common.disabled"))}</span>
|
|
3902
4821
|
</span>
|
|
3903
4822
|
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.autoPilot.writeLaneUiTestsDescription"))}</span>
|
|
3904
4823
|
</span>
|
|
4824
|
+
<span class="reply-mode-switch--settings__toggle">
|
|
4825
|
+
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
4826
|
+
<span class="reply-mode-switch__state">${escapeHtml(writeLaneUiTestsEnabled ? L("common.enabled") : L("common.disabled"))}</span>
|
|
4827
|
+
</span>
|
|
3905
4828
|
</label>
|
|
3906
4829
|
`, `
|
|
3907
|
-
<label class="reply-mode-switch reply-mode-switch--grouped" data-auto-pilot-write-lane="source">
|
|
4830
|
+
<label class="reply-mode-switch reply-mode-switch--settings reply-mode-switch--grouped" data-auto-pilot-write-lane="source">
|
|
3908
4831
|
<input type="checkbox" class="reply-mode-switch__input" ${writeLaneSourceEnabled ? "checked" : ""} data-auto-pilot-write-lane-checkbox="source" />
|
|
3909
|
-
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3910
4832
|
<span class="reply-mode-switch__copy">
|
|
3911
4833
|
<span class="reply-mode-switch__title">
|
|
3912
4834
|
<span>${escapeHtml(L("settings.autoPilot.writeLaneSourceTitle"))}</span>
|
|
3913
|
-
<span class="reply-mode-switch__state">${escapeHtml(writeLaneSourceEnabled ? L("common.enabled") : L("common.disabled"))}</span>
|
|
3914
4835
|
</span>
|
|
3915
4836
|
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.autoPilot.writeLaneSourceDescription"))}</span>
|
|
3916
4837
|
</span>
|
|
4838
|
+
<span class="reply-mode-switch--settings__toggle">
|
|
4839
|
+
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
4840
|
+
<span class="reply-mode-switch__state">${escapeHtml(writeLaneSourceEnabled ? L("common.enabled") : L("common.disabled"))}</span>
|
|
4841
|
+
</span>
|
|
3917
4842
|
</label>
|
|
3918
4843
|
`], { listClassName: "settings-list settings-list--toggle-group" })}
|
|
3919
4844
|
<p class="settings-page-copy muted">${escapeHtml(L("settings.autoPilot.scopeNote"))}</p>
|
|
@@ -4537,33 +5462,290 @@ function renderSettingsA2aSharePage(context) {
|
|
|
4537
5462
|
${meta ? `<span class="settings-compose-entry__meta muted">${meta}</span>` : ""}
|
|
4538
5463
|
</span>
|
|
4539
5464
|
</div>
|
|
4540
|
-
`;
|
|
4541
|
-
});
|
|
4542
|
-
if (hasMore) {
|
|
4543
|
-
const remaining = items.length - visibleCount;
|
|
4544
|
-
filesList.push(
|
|
4545
|
-
`<button type="button" class="settings-compose-more" data-a2a-share-files-more>${escapeHtml(L("settings.a2aShare.showMore", { count: remaining }))}</button>`
|
|
4546
|
-
);
|
|
4547
|
-
} else if (items.length > PAGE_SIZE) {
|
|
4548
|
-
filesList.push(
|
|
4549
|
-
`<button type="button" class="settings-compose-more" data-a2a-share-files-collapse>${escapeHtml(L("settings.a2aShare.showLess"))}</button>`
|
|
4550
|
-
);
|
|
4551
|
-
}
|
|
5465
|
+
`;
|
|
5466
|
+
});
|
|
5467
|
+
if (hasMore) {
|
|
5468
|
+
const remaining = items.length - visibleCount;
|
|
5469
|
+
filesList.push(
|
|
5470
|
+
`<button type="button" class="settings-compose-more" data-a2a-share-files-more>${escapeHtml(L("settings.a2aShare.showMore", { count: remaining }))}</button>`
|
|
5471
|
+
);
|
|
5472
|
+
} else if (items.length > PAGE_SIZE) {
|
|
5473
|
+
filesList.push(
|
|
5474
|
+
`<button type="button" class="settings-compose-more" data-a2a-share-files-collapse>${escapeHtml(L("settings.a2aShare.showLess"))}</button>`
|
|
5475
|
+
);
|
|
5476
|
+
}
|
|
5477
|
+
|
|
5478
|
+
return `
|
|
5479
|
+
<div class="settings-page">
|
|
5480
|
+
${renderSettingsGroup("", [
|
|
5481
|
+
renderSettingsInfoRow(L("settings.row.a2aShareStatus"), statusLabel),
|
|
5482
|
+
renderSettingsInfoRow(L("settings.row.a2aShareEndpoint"), share.shareHost || share.shareUrl || ""),
|
|
5483
|
+
renderSettingsInfoRow(L("settings.row.a2aShareUserId"), share.userId || ""),
|
|
5484
|
+
])}
|
|
5485
|
+
${renderSettingsGroup(L("settings.a2aShare.storage.title"), storageRows)}
|
|
5486
|
+
${items.length
|
|
5487
|
+
? renderSettingsGroup(L("settings.a2aShare.files.title"), filesList)
|
|
5488
|
+
: renderSettingsGroup(L("settings.a2aShare.files.title"), [
|
|
5489
|
+
`<p class="settings-group__description muted">${escapeHtml(L("settings.a2aShare.files.empty"))}</p>`,
|
|
5490
|
+
])}
|
|
5491
|
+
${share.error ? `<p class="settings-page-copy muted">${escapeHtml(L("settings.a2aShare.error", { reason: share.error }))}</p>` : ""}
|
|
5492
|
+
</div>
|
|
5493
|
+
`;
|
|
5494
|
+
}
|
|
5495
|
+
|
|
5496
|
+
function renderSettingsRemotePairingPage(context) {
|
|
5497
|
+
// Always render — remote pairing is a feature toggle, not a credential-
|
|
5498
|
+
// gated integration. If the bridge hasn't responded with a status payload
|
|
5499
|
+
// yet, fall back to a default-disabled view so the toggle still works.
|
|
5500
|
+
const status = context.remotePairing || {
|
|
5501
|
+
enabled: false,
|
|
5502
|
+
relayUrl: "",
|
|
5503
|
+
configuredRelayUrl: "",
|
|
5504
|
+
identityFingerprint: null,
|
|
5505
|
+
sessions: [],
|
|
5506
|
+
pairings: [],
|
|
5507
|
+
auditEvents: [],
|
|
5508
|
+
};
|
|
5509
|
+
|
|
5510
|
+
const enabled = status.enabled === true;
|
|
5511
|
+
const sessions = Array.isArray(status.sessions) ? status.sessions : [];
|
|
5512
|
+
const pairings = Array.isArray(status.pairings) ? status.pairings : [];
|
|
5513
|
+
const auditEvents = Array.isArray(status.auditEvents) ? status.auditEvents : [];
|
|
5514
|
+
const usingRelay = isRemotePairingUsingRelay();
|
|
5515
|
+
const statusModel = remotePairingStatusModel(status, { usingRelay });
|
|
5516
|
+
const sessionsByPub = new Map(
|
|
5517
|
+
sessions.map((s) => [String(s.phonePub || "").toLowerCase(), s]),
|
|
5518
|
+
);
|
|
5519
|
+
const sessionsByPairingId = new Map(
|
|
5520
|
+
sessions.map((s) => [String(s.pairingId || ""), s]),
|
|
5521
|
+
);
|
|
5522
|
+
const togglePending = state.remotePairingPending === "toggle";
|
|
5523
|
+
const relayPending = state.remotePairingPending === "relayUrl";
|
|
5524
|
+
|
|
5525
|
+
// "This device" — the locally-stored enrollment record (if any). Read
|
|
5526
|
+
// synchronously from localStorage on each render so the badge shows up
|
|
5527
|
+
// immediately after a successful lan-enroll without waiting for state
|
|
5528
|
+
// refresh. Returns null in storage-disabled contexts (Safari private
|
|
5529
|
+
// mode etc.) — the page just renders without the indicator.
|
|
5530
|
+
const localPairing = (() => {
|
|
5531
|
+
try {
|
|
5532
|
+
return loadRemotePairingState();
|
|
5533
|
+
} catch {
|
|
5534
|
+
return null;
|
|
5535
|
+
}
|
|
5536
|
+
})();
|
|
5537
|
+
const localPhonePub = (localPairing?.phonePub || "").toLowerCase();
|
|
5538
|
+
const localRegistered = Boolean(localPairing);
|
|
5539
|
+
|
|
5540
|
+
// Identity fingerprint row — shows the bridge's stable public key short-
|
|
5541
|
+
// form so the human can verify what the phone is pairing against.
|
|
5542
|
+
const fpRow = renderSettingsInfoRow(
|
|
5543
|
+
L("settings.remotePairing.identity.fingerprint"),
|
|
5544
|
+
status.identityFingerprint || L("settings.remotePairing.identity.empty"),
|
|
5545
|
+
);
|
|
5546
|
+
|
|
5547
|
+
const connectionSection = `
|
|
5548
|
+
<section class="settings-remote-connection">
|
|
5549
|
+
<header class="settings-remote-connection__header">
|
|
5550
|
+
<div class="settings-remote-connection__copy">
|
|
5551
|
+
<p class="settings-remote-connection__title">${escapeHtml(L("settings.remotePairing.connection.title"))}</p>
|
|
5552
|
+
</div>
|
|
5553
|
+
<span class="settings-remote-status settings-remote-status--${escapeHtml(statusModel.tone)}">${escapeHtml(statusModel.label)}</span>
|
|
5554
|
+
</header>
|
|
5555
|
+
<label class="reply-mode-switch reply-mode-switch--settings" data-remote-pairing-toggle>
|
|
5556
|
+
<input type="checkbox" class="reply-mode-switch__input"
|
|
5557
|
+
${enabled ? "checked" : ""}
|
|
5558
|
+
${togglePending ? "disabled" : ""}
|
|
5559
|
+
data-remote-pairing-toggle-checkbox />
|
|
5560
|
+
<span class="reply-mode-switch--settings__toggle">
|
|
5561
|
+
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
5562
|
+
<span class="reply-mode-switch__state">${escapeHtml(enabled ? L("settings.claudeAway.on") : L("settings.claudeAway.off"))}</span>
|
|
5563
|
+
</span>
|
|
5564
|
+
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.remotePairing.toggle.description"))}</span>
|
|
5565
|
+
</label>
|
|
5566
|
+
<p class="settings-remote-connection__trust">${escapeHtml(L("settings.remotePairing.security.copy"))}</p>
|
|
5567
|
+
</section>
|
|
5568
|
+
`;
|
|
5569
|
+
|
|
5570
|
+
// Relay URL editor. When empty, the bridge falls back to the compiled-in
|
|
5571
|
+
// default — surfacing that here would require leaking it to the client,
|
|
5572
|
+
// so we just show "" and lean on the placeholder.
|
|
5573
|
+
const relayUrlValue = String(status.configuredRelayUrl || "");
|
|
5574
|
+
const relayUrlSection = `
|
|
5575
|
+
<section class="settings-group">
|
|
5576
|
+
<p class="settings-group__title">${escapeHtml(L("settings.remotePairing.relayUrl.title"))}</p>
|
|
5577
|
+
<div class="settings-input-row">
|
|
5578
|
+
<input type="text"
|
|
5579
|
+
class="settings-input"
|
|
5580
|
+
data-remote-pairing-relay-url-input
|
|
5581
|
+
value="${escapeHtml(relayUrlValue)}"
|
|
5582
|
+
placeholder="${escapeHtml(L("settings.remotePairing.relayUrl.placeholder"))}"
|
|
5583
|
+
${relayPending ? "disabled" : ""}
|
|
5584
|
+
autocomplete="off"
|
|
5585
|
+
spellcheck="false" />
|
|
5586
|
+
<button type="button"
|
|
5587
|
+
class="secondary"
|
|
5588
|
+
data-remote-pairing-relay-url-save
|
|
5589
|
+
${relayPending ? "disabled" : ""}>
|
|
5590
|
+
${escapeHtml(L("settings.remotePairing.relayUrl.save"))}
|
|
5591
|
+
</button>
|
|
5592
|
+
</div>
|
|
5593
|
+
<p class="settings-group__description">${escapeHtml(L("settings.remotePairing.relayUrl.help"))}</p>
|
|
5594
|
+
</section>
|
|
5595
|
+
`;
|
|
5596
|
+
|
|
5597
|
+
// Pairings list — one card per phonePub on disk. "Live" badge if there's
|
|
5598
|
+
// an open session; otherwise show the last-seen timestamp. The phone
|
|
5599
|
+
// currently rendering this page gets an extra "This device" badge so the
|
|
5600
|
+
// user can identify themselves in the list (especially useful when more
|
|
5601
|
+
// than one phone is paired to the same Mac).
|
|
5602
|
+
const otherPairings = localPhonePub
|
|
5603
|
+
? pairings.filter((p) => String(p.phonePub || "").toLowerCase() !== localPhonePub)
|
|
5604
|
+
: pairings;
|
|
5605
|
+
const pairingsRows = otherPairings.length === 0
|
|
5606
|
+
? `<div class="settings-copy-block"><p class="muted">${escapeHtml(L("settings.remotePairing.pairings.empty"))}</p></div>`
|
|
5607
|
+
: `<div class="device-list">
|
|
5608
|
+
${otherPairings.map((p) => {
|
|
5609
|
+
const session = sessionsByPub.get(String(p.phonePub || "").toLowerCase())
|
|
5610
|
+
|| sessionsByPairingId.get(String(p.pairingId || ""));
|
|
5611
|
+
const live = isRemotePairingSessionConnected(session);
|
|
5612
|
+
const lastSeenAtMs = Number(session?.lastSeenAtMs || p.lastSeenAtMs) || 0;
|
|
5613
|
+
const lastSeen = lastSeenAtMs
|
|
5614
|
+
? new Date(lastSeenAtMs).toLocaleString(state.locale)
|
|
5615
|
+
: L("settings.remotePairing.pairings.never");
|
|
5616
|
+
const added = p.addedAtMs
|
|
5617
|
+
? new Date(p.addedAtMs).toLocaleString(state.locale)
|
|
5618
|
+
: L("settings.remotePairing.pairings.never");
|
|
5619
|
+
const pendingThis = state.remotePairingPending === `revoke:${p.phonePub}`;
|
|
5620
|
+
return `
|
|
5621
|
+
<article class="device-card" data-remote-pairing-row="${escapeHtml(p.phonePub)}">
|
|
5622
|
+
<div class="device-card__header">
|
|
5623
|
+
<div class="device-card__title-wrap">
|
|
5624
|
+
<div class="device-card__headline">
|
|
5625
|
+
<span class="device-card__icon" aria-hidden="true">${renderIcon("iphone")}</span>
|
|
5626
|
+
<h3 class="device-card__title">${escapeHtml(p.label || p.phoneFingerprint || p.pairingId)}</h3>
|
|
5627
|
+
</div>
|
|
5628
|
+
<p class="device-card__subtitle">${escapeHtml(p.phoneFingerprint || p.phonePub.slice(0, 16))}</p>
|
|
5629
|
+
</div>
|
|
5630
|
+
<div class="device-card__badges">
|
|
5631
|
+
<span class="device-card__badge ${live ? "device-card__badge--live" : "device-card__badge--offline"}">
|
|
5632
|
+
${escapeHtml(live ? L("settings.remotePairing.pairings.live") : L("settings.remotePairing.pairings.offline"))}
|
|
5633
|
+
</span>
|
|
5634
|
+
</div>
|
|
5635
|
+
</div>
|
|
5636
|
+
<div class="device-card__meta">
|
|
5637
|
+
${renderDeviceMetaRow(L("settings.remotePairing.pairings.added"), added)}
|
|
5638
|
+
${renderDeviceMetaRow(L("settings.remotePairing.pairings.lastSeen"), lastSeen)}
|
|
5639
|
+
</div>
|
|
5640
|
+
<div class="device-card__actions">
|
|
5641
|
+
<button type="button"
|
|
5642
|
+
class="secondary secondary--wide"
|
|
5643
|
+
data-remote-pairing-revoke="${escapeHtml(p.phonePub)}"
|
|
5644
|
+
${pendingThis ? "disabled" : ""}>
|
|
5645
|
+
${escapeHtml(L("settings.remotePairing.pairings.revoke"))}
|
|
5646
|
+
</button>
|
|
5647
|
+
</div>
|
|
5648
|
+
</article>
|
|
5649
|
+
`;
|
|
5650
|
+
}).join("")}
|
|
5651
|
+
</div>`;
|
|
5652
|
+
|
|
5653
|
+
// "This device" group — sits above the pairings list and tells the user
|
|
5654
|
+
// explicitly whether *this phone* (the one rendering the page) is the
|
|
5655
|
+
// enrolled one. Two states:
|
|
5656
|
+
// 1. Enrolled: show the phone fingerprint + a confirmation copy.
|
|
5657
|
+
// 2. Not enrolled: surface a hint that re-pairing on LAN registers
|
|
5658
|
+
// the device. (The hint applies even when other phones are paired
|
|
5659
|
+
// — they're not relevant to this device's relay status.)
|
|
5660
|
+
const thisDeviceSection = (() => {
|
|
5661
|
+
if (localRegistered) {
|
|
5662
|
+
const fingerprint = localPairing.phoneFingerprint
|
|
5663
|
+
|| localPairing.phonePub.slice(0, 16);
|
|
5664
|
+
const rotatePending = state.remotePairingPending === "rotateToken";
|
|
5665
|
+
return renderSettingsGroup(L("settings.remotePairing.thisDevice.title"), [
|
|
5666
|
+
`
|
|
5667
|
+
<div class="device-list">
|
|
5668
|
+
<article class="device-card">
|
|
5669
|
+
<div class="device-card__header">
|
|
5670
|
+
<div class="device-card__title-wrap">
|
|
5671
|
+
<div class="device-card__headline">
|
|
5672
|
+
<span class="device-card__icon" aria-hidden="true">${renderIcon("iphone")}</span>
|
|
5673
|
+
<h3 class="device-card__title">${escapeHtml(L("settings.remotePairing.thisDevice.registeredTitle"))}</h3>
|
|
5674
|
+
</div>
|
|
5675
|
+
<p class="device-card__subtitle">${escapeHtml(fingerprint)}</p>
|
|
5676
|
+
</div>
|
|
5677
|
+
<span class="device-card__badge">${escapeHtml(L("settings.remotePairing.pairings.thisDevice"))}</span>
|
|
5678
|
+
</div>
|
|
5679
|
+
<div class="device-card__meta">
|
|
5680
|
+
${renderDeviceMetaRow(L("settings.remotePairing.status.title"), statusModel.label)}
|
|
5681
|
+
${renderDeviceMetaRow(L("settings.remotePairing.thisDevice.fingerprint"), fingerprint)}
|
|
5682
|
+
</div>
|
|
5683
|
+
<div class="device-card__actions">
|
|
5684
|
+
<button type="button"
|
|
5685
|
+
class="secondary secondary--wide"
|
|
5686
|
+
data-remote-pairing-rotate-token
|
|
5687
|
+
${rotatePending || usingRelay ? "disabled" : ""}>
|
|
5688
|
+
${escapeHtml(L("settings.remotePairing.token.rotate"))}
|
|
5689
|
+
</button>
|
|
5690
|
+
</div>
|
|
5691
|
+
${usingRelay ? `<p class="settings-page-copy muted settings-remote-device-hint">${escapeHtml(L("settings.remotePairing.token.lanOnly"))}</p>` : ""}
|
|
5692
|
+
</article>
|
|
5693
|
+
</div>
|
|
5694
|
+
`,
|
|
5695
|
+
]);
|
|
5696
|
+
}
|
|
5697
|
+
return renderSettingsGroup(L("settings.remotePairing.thisDevice.title"), [
|
|
5698
|
+
`
|
|
5699
|
+
<div class="device-list">
|
|
5700
|
+
<article class="device-card">
|
|
5701
|
+
<div class="device-card__header">
|
|
5702
|
+
<div class="device-card__title-wrap">
|
|
5703
|
+
<div class="device-card__headline">
|
|
5704
|
+
<span class="device-card__icon" aria-hidden="true">${renderIcon("iphone")}</span>
|
|
5705
|
+
<h3 class="device-card__title">${escapeHtml(L("settings.remotePairing.thisDevice.notEnrolledTitle"))}</h3>
|
|
5706
|
+
</div>
|
|
5707
|
+
</div>
|
|
5708
|
+
</div>
|
|
5709
|
+
<div class="device-card__meta">
|
|
5710
|
+
${renderDeviceMetaRow(L("settings.remotePairing.status.title"), L("settings.remotePairing.status.disabled"))}
|
|
5711
|
+
${renderDeviceMetaRow(L("settings.remotePairing.thisDevice.fingerprint"), L("common.unavailable"))}
|
|
5712
|
+
</div>
|
|
5713
|
+
<p class="settings-page-copy muted settings-remote-device-hint">${escapeHtml(L("settings.remotePairing.thisDevice.notEnrolled"))}</p>
|
|
5714
|
+
</article>
|
|
5715
|
+
</div>
|
|
5716
|
+
`,
|
|
5717
|
+
]);
|
|
5718
|
+
})();
|
|
5719
|
+
|
|
5720
|
+
const detailsSection = `
|
|
5721
|
+
<details class="settings-disclosure settings-remote-details" data-remote-pairing-details ${state.remotePairingDetailsOpen ? "open" : ""}>
|
|
5722
|
+
<summary>${escapeHtml(L("settings.remotePairing.details.title"))}</summary>
|
|
5723
|
+
<div class="settings-disclosure__body">
|
|
5724
|
+
${relayUrlSection}
|
|
5725
|
+
${renderSettingsGroup(L("settings.remotePairing.identity.title"), [fpRow])}
|
|
5726
|
+
</div>
|
|
5727
|
+
</details>
|
|
5728
|
+
`;
|
|
5729
|
+
const auditSection = renderSettingsGroup(L("settings.remotePairing.audit.title"), [
|
|
5730
|
+
renderRemotePairingAudit(auditEvents),
|
|
5731
|
+
]);
|
|
5732
|
+
|
|
5733
|
+
const noticeBlock = state.remotePairingNotice
|
|
5734
|
+
? `<p class="settings-page-copy">${escapeHtml(state.remotePairingNotice)}</p>`
|
|
5735
|
+
: "";
|
|
5736
|
+
const errorBlock = state.remotePairingError
|
|
5737
|
+
? `<p class="settings-page-copy danger">${escapeHtml(state.remotePairingError)}</p>`
|
|
5738
|
+
: "";
|
|
4552
5739
|
|
|
4553
5740
|
return `
|
|
4554
5741
|
<div class="settings-page">
|
|
4555
|
-
${
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
])}
|
|
4560
|
-
${
|
|
4561
|
-
${
|
|
4562
|
-
? renderSettingsGroup(L("settings.a2aShare.files.title"), filesList)
|
|
4563
|
-
: renderSettingsGroup(L("settings.a2aShare.files.title"), [
|
|
4564
|
-
`<p class="settings-group__description muted">${escapeHtml(L("settings.a2aShare.files.empty"))}</p>`,
|
|
4565
|
-
])}
|
|
4566
|
-
${share.error ? `<p class="settings-page-copy muted">${escapeHtml(L("settings.a2aShare.error", { reason: share.error }))}</p>` : ""}
|
|
5742
|
+
${noticeBlock}
|
|
5743
|
+
${errorBlock}
|
|
5744
|
+
${connectionSection}
|
|
5745
|
+
${thisDeviceSection}
|
|
5746
|
+
${renderSettingsGroup(L("settings.remotePairing.pairings.otherTitle"), [pairingsRows])}
|
|
5747
|
+
${auditSection}
|
|
5748
|
+
${detailsSection}
|
|
4567
5749
|
</div>
|
|
4568
5750
|
`;
|
|
4569
5751
|
}
|
|
@@ -5030,6 +6212,7 @@ function formatUsdcAtomic(atomic) {
|
|
|
5030
6212
|
}
|
|
5031
6213
|
|
|
5032
6214
|
function renderSettingsInfoRow(label, value, options = {}) {
|
|
6215
|
+
const tone = settingsNavValueTone(value, options.valueTone || "");
|
|
5033
6216
|
const rowClassName = [
|
|
5034
6217
|
"settings-info-row",
|
|
5035
6218
|
options.stacked ? "settings-info-row--stacked" : "",
|
|
@@ -5037,6 +6220,7 @@ function renderSettingsInfoRow(label, value, options = {}) {
|
|
|
5037
6220
|
].filter(Boolean).join(" ");
|
|
5038
6221
|
const valueClassName = [
|
|
5039
6222
|
"settings-info-row__value",
|
|
6223
|
+
tone ? `settings-info-row__value--${tone}` : "",
|
|
5040
6224
|
options.mono ? "settings-info-row__value--mono" : "",
|
|
5041
6225
|
options.valueClassName || "",
|
|
5042
6226
|
].filter(Boolean).join(" ");
|
|
@@ -5083,9 +6267,17 @@ function renderTrustedDeviceCard(device) {
|
|
|
5083
6267
|
const badge = device.currentDevice
|
|
5084
6268
|
? `<span class="device-card__badge">${escapeHtml(L("settings.device.thisDevice"))}</span>`
|
|
5085
6269
|
: "";
|
|
5086
|
-
const
|
|
5087
|
-
?
|
|
5088
|
-
:
|
|
6270
|
+
const actions = device.currentDevice
|
|
6271
|
+
? ""
|
|
6272
|
+
: `
|
|
6273
|
+
<div class="device-card__actions">
|
|
6274
|
+
<button
|
|
6275
|
+
class="secondary secondary--wide"
|
|
6276
|
+
type="button"
|
|
6277
|
+
data-device-revoke="${escapeHtml(device.deviceId)}"
|
|
6278
|
+
>${escapeHtml(L("settings.action.revokeDevice"))}</button>
|
|
6279
|
+
</div>
|
|
6280
|
+
`;
|
|
5089
6281
|
|
|
5090
6282
|
return `
|
|
5091
6283
|
<article class="device-card">
|
|
@@ -5106,14 +6298,7 @@ function renderTrustedDeviceCard(device) {
|
|
|
5106
6298
|
${renderDeviceMetaRow(L("settings.row.pushStatus"), pushLabel)}
|
|
5107
6299
|
${renderDeviceMetaRow(L("settings.row.currentLanguage"), localeLabel)}
|
|
5108
6300
|
</div>
|
|
5109
|
-
|
|
5110
|
-
<button
|
|
5111
|
-
class="secondary secondary--wide"
|
|
5112
|
-
type="button"
|
|
5113
|
-
data-device-revoke="${escapeHtml(device.deviceId)}"
|
|
5114
|
-
data-device-current="${device.currentDevice ? "true" : "false"}"
|
|
5115
|
-
>${escapeHtml(actionLabel)}</button>
|
|
5116
|
-
</div>
|
|
6301
|
+
${actions}
|
|
5117
6302
|
</article>
|
|
5118
6303
|
`;
|
|
5119
6304
|
}
|
|
@@ -5186,8 +6371,9 @@ function renderStandardDetailMobile(detail) {
|
|
|
5186
6371
|
const kindInfo = kindMeta(detail.kind, detail);
|
|
5187
6372
|
const spaciousBodyDetail = TIMELINE_MESSAGE_KINDS.has(detail.kind);
|
|
5188
6373
|
const plainIntro = renderDetailPlainIntro(detail, { mobile: true });
|
|
6374
|
+
const hasCompletionReply = isCompletionReplyAvailable(detail);
|
|
5189
6375
|
return `
|
|
5190
|
-
<div class="mobile-detail-screen">
|
|
6376
|
+
<div class="mobile-detail-screen ${hasCompletionReply ? "mobile-detail-screen--has-reply-dock" : ""}">
|
|
5191
6377
|
<div class="detail-shell detail-shell--mobile">
|
|
5192
6378
|
<div class="mobile-detail-scroll mobile-detail-scroll--detail">
|
|
5193
6379
|
${renderDetailMetaRow(detail, kindInfo, { mobile: true })}
|
|
@@ -5217,10 +6403,10 @@ function renderStandardDetailMobile(detail) {
|
|
|
5217
6403
|
${renderDetailDiffPanel(detail, { mobile: true })}
|
|
5218
6404
|
${renderDetailDiffThreadGroups(detail, { mobile: true })}
|
|
5219
6405
|
${renderDetailFileRefs(detail, { mobile: true })}
|
|
5220
|
-
${renderCompletionReplyComposer(detail, { mobile: true })}
|
|
5221
6406
|
</div>
|
|
5222
6407
|
${detail.readOnly ? "" : renderActionButtons(detail.actions || [], { mobileSticky: true })}
|
|
5223
6408
|
</div>
|
|
6409
|
+
${renderCompletionReplyDock(detail)}
|
|
5224
6410
|
</div>
|
|
5225
6411
|
`;
|
|
5226
6412
|
}
|
|
@@ -5968,7 +7154,7 @@ function renderThreadShareDetail(detail, options = {}) {
|
|
|
5968
7154
|
}
|
|
5969
7155
|
|
|
5970
7156
|
function renderCompletionReplyComposer(detail, options = {}) {
|
|
5971
|
-
if ((detail
|
|
7157
|
+
if (!isCompletionReplyAvailable(detail)) {
|
|
5972
7158
|
return "";
|
|
5973
7159
|
}
|
|
5974
7160
|
|
|
@@ -6142,6 +7328,51 @@ function renderCompletionReplyComposer(detail, options = {}) {
|
|
|
6142
7328
|
`;
|
|
6143
7329
|
}
|
|
6144
7330
|
|
|
7331
|
+
function isCompletionReplyAvailable(detail) {
|
|
7332
|
+
return Boolean(
|
|
7333
|
+
detail &&
|
|
7334
|
+
(detail.kind === "completion" || detail.kind === "assistant_final") &&
|
|
7335
|
+
detail.reply?.enabled === true &&
|
|
7336
|
+
detail.token
|
|
7337
|
+
);
|
|
7338
|
+
}
|
|
7339
|
+
|
|
7340
|
+
function renderCompletionReplyDock(detail) {
|
|
7341
|
+
if (!isCompletionReplyAvailable(detail)) {
|
|
7342
|
+
return "";
|
|
7343
|
+
}
|
|
7344
|
+
const providerVars = { provider: providerDisplayName(detail?.provider) };
|
|
7345
|
+
return `
|
|
7346
|
+
<div class="reply-dock" role="region" aria-label="${escapeHtml(L("reply.eyebrow"))}">
|
|
7347
|
+
<button
|
|
7348
|
+
class="reply-dock__button"
|
|
7349
|
+
type="button"
|
|
7350
|
+
data-open-completion-reply-sheet
|
|
7351
|
+
data-token="${escapeHtml(detail.token)}"
|
|
7352
|
+
>
|
|
7353
|
+
<span class="reply-dock__label">${escapeHtml(L("reply.title", providerVars))}</span>
|
|
7354
|
+
</button>
|
|
7355
|
+
</div>
|
|
7356
|
+
`;
|
|
7357
|
+
}
|
|
7358
|
+
|
|
7359
|
+
function renderCompletionReplySheet(detail) {
|
|
7360
|
+
if (!isCompletionReplyAvailable(detail) || state.completionReplySheetToken !== detail.token) {
|
|
7361
|
+
return "";
|
|
7362
|
+
}
|
|
7363
|
+
const providerVars = { provider: providerDisplayName(detail?.provider) };
|
|
7364
|
+
return `
|
|
7365
|
+
<div class="reply-sheet-backdrop" data-close-completion-reply-sheet aria-hidden="true"></div>
|
|
7366
|
+
<section class="reply-sheet" role="dialog" aria-modal="true" aria-label="${escapeHtml(L("reply.title", providerVars))}">
|
|
7367
|
+
<div class="reply-sheet__handle" aria-hidden="true"></div>
|
|
7368
|
+
<button class="reply-sheet__close" type="button" data-close-completion-reply-sheet aria-label="${escapeHtml(L("common.close"))}">
|
|
7369
|
+
<span aria-hidden="true">×</span>
|
|
7370
|
+
</button>
|
|
7371
|
+
${renderCompletionReplyComposer(detail, { mobile: true, sheet: true })}
|
|
7372
|
+
</section>
|
|
7373
|
+
`;
|
|
7374
|
+
}
|
|
7375
|
+
|
|
6145
7376
|
function renderChoiceQuestions(detail) {
|
|
6146
7377
|
const effectiveAnswers = getEffectiveChoiceDraftAnswers(detail);
|
|
6147
7378
|
return detail.questions
|
|
@@ -6392,10 +7623,27 @@ function renderInstallBanner() {
|
|
|
6392
7623
|
`;
|
|
6393
7624
|
}
|
|
6394
7625
|
|
|
7626
|
+
function renderClientUpdateBanner() {
|
|
7627
|
+
return `
|
|
7628
|
+
<section class="install-banner install-banner--push">
|
|
7629
|
+
<div class="install-banner__copy">
|
|
7630
|
+
<strong>${escapeHtml(L("banner.clientUpdate.title"))}</strong>
|
|
7631
|
+
<p class="muted">${escapeHtml(L("banner.clientUpdate.copy"))}</p>
|
|
7632
|
+
</div>
|
|
7633
|
+
<div class="actions install-banner__actions">
|
|
7634
|
+
<button class="secondary" type="button" data-force-app-refresh>${escapeHtml(L("banner.clientUpdate.action"))}</button>
|
|
7635
|
+
</div>
|
|
7636
|
+
</section>
|
|
7637
|
+
`;
|
|
7638
|
+
}
|
|
7639
|
+
|
|
6395
7640
|
function renderTopBanner() {
|
|
6396
7641
|
if (!isDesktopLayout() && (state.detailOpen || isSettingsSubpageOpen())) {
|
|
6397
7642
|
return "";
|
|
6398
7643
|
}
|
|
7644
|
+
if (state.clientUpdateRequired) {
|
|
7645
|
+
return renderClientUpdateBanner();
|
|
7646
|
+
}
|
|
6399
7647
|
if (shouldShowInstallBanner()) {
|
|
6400
7648
|
return renderInstallBanner();
|
|
6401
7649
|
}
|
|
@@ -6922,6 +8170,23 @@ function bindShellInteractions() {
|
|
|
6922
8170
|
});
|
|
6923
8171
|
}
|
|
6924
8172
|
|
|
8173
|
+
for (const button of document.querySelectorAll("[data-open-completion-reply-sheet][data-token]")) {
|
|
8174
|
+
button.addEventListener("click", async () => {
|
|
8175
|
+
state.completionReplySheetToken = button.dataset.token || "";
|
|
8176
|
+
resetHorizontalViewportScroll();
|
|
8177
|
+
await renderShell();
|
|
8178
|
+
resetHorizontalViewportScroll();
|
|
8179
|
+
});
|
|
8180
|
+
}
|
|
8181
|
+
|
|
8182
|
+
for (const trigger of document.querySelectorAll("[data-close-completion-reply-sheet]")) {
|
|
8183
|
+
trigger.addEventListener("click", async () => {
|
|
8184
|
+
state.completionReplySheetToken = "";
|
|
8185
|
+
resetHorizontalViewportScroll();
|
|
8186
|
+
await renderShell();
|
|
8187
|
+
});
|
|
8188
|
+
}
|
|
8189
|
+
|
|
6925
8190
|
for (const input of document.querySelectorAll("[data-reply-mode-toggle][data-reply-token]")) {
|
|
6926
8191
|
input.addEventListener("change", async () => {
|
|
6927
8192
|
const token = input.dataset.replyToken || "";
|
|
@@ -7132,14 +8397,51 @@ function bindShellInteractions() {
|
|
|
7132
8397
|
state.session.autoPilotWriteLaneSource = result?.writeLaneSourceEnabled === true;
|
|
7133
8398
|
}
|
|
7134
8399
|
|
|
8400
|
+
function snapshotAutoPilotSettings() {
|
|
8401
|
+
return {
|
|
8402
|
+
trustedReads: state.session?.autoPilotTrustedReads === true,
|
|
8403
|
+
trustedWrites: state.session?.autoPilotTrustedWrites === true,
|
|
8404
|
+
writeLaneContent: state.session?.autoPilotWriteLaneContent === true,
|
|
8405
|
+
writeLaneUiTests: state.session?.autoPilotWriteLaneUiTests === true,
|
|
8406
|
+
writeLaneSource: state.session?.autoPilotWriteLaneSource === true,
|
|
8407
|
+
};
|
|
8408
|
+
}
|
|
8409
|
+
|
|
8410
|
+
function restoreAutoPilotSettings(snapshot) {
|
|
8411
|
+
if (!state.session || !snapshot) {
|
|
8412
|
+
return;
|
|
8413
|
+
}
|
|
8414
|
+
state.session.autoPilotTrustedReads = snapshot.trustedReads === true;
|
|
8415
|
+
state.session.autoPilotTrustedWrites = snapshot.trustedWrites === true;
|
|
8416
|
+
state.session.autoPilotWriteLaneContent = snapshot.writeLaneContent === true;
|
|
8417
|
+
state.session.autoPilotWriteLaneUiTests = snapshot.writeLaneUiTests === true;
|
|
8418
|
+
state.session.autoPilotWriteLaneSource = snapshot.writeLaneSource === true;
|
|
8419
|
+
}
|
|
8420
|
+
|
|
8421
|
+
function reconcileAutoPilotTrustedWrites() {
|
|
8422
|
+
if (!state.session) {
|
|
8423
|
+
return;
|
|
8424
|
+
}
|
|
8425
|
+
state.session.autoPilotTrustedWrites = Boolean(
|
|
8426
|
+
state.session.autoPilotWriteLaneContent === true ||
|
|
8427
|
+
state.session.autoPilotWriteLaneUiTests === true ||
|
|
8428
|
+
state.session.autoPilotWriteLaneSource === true,
|
|
8429
|
+
);
|
|
8430
|
+
}
|
|
8431
|
+
|
|
7135
8432
|
for (const checkbox of document.querySelectorAll("[data-auto-pilot-checkbox]")) {
|
|
7136
8433
|
checkbox.addEventListener("change", async () => {
|
|
7137
8434
|
const next = checkbox.checked === true;
|
|
8435
|
+
const previous = snapshotAutoPilotSettings();
|
|
8436
|
+
if (state.session) {
|
|
8437
|
+
state.session.autoPilotTrustedReads = next;
|
|
8438
|
+
}
|
|
8439
|
+
await renderShell();
|
|
7138
8440
|
try {
|
|
7139
8441
|
const result = await apiPost("/api/settings/auto-pilot", { trustedReadsEnabled: next });
|
|
7140
8442
|
applyAutoPilotSettingsResult(result);
|
|
7141
|
-
await refreshAuthenticatedState();
|
|
7142
8443
|
} catch (error) {
|
|
8444
|
+
restoreAutoPilotSettings(previous);
|
|
7143
8445
|
state.pushError = error.message || String(error);
|
|
7144
8446
|
}
|
|
7145
8447
|
await renderShell();
|
|
@@ -7150,17 +8452,29 @@ function bindShellInteractions() {
|
|
|
7150
8452
|
checkbox.addEventListener("change", async () => {
|
|
7151
8453
|
const next = checkbox.checked === true;
|
|
7152
8454
|
const lane = normalizeClientText(checkbox.getAttribute("data-auto-pilot-write-lane-checkbox") || "");
|
|
8455
|
+
const previous = snapshotAutoPilotSettings();
|
|
7153
8456
|
const payload =
|
|
7154
8457
|
lane === "content"
|
|
7155
8458
|
? { writeLaneContentEnabled: next }
|
|
7156
8459
|
: lane === "ui-tests"
|
|
7157
8460
|
? { writeLaneUiTestsEnabled: next }
|
|
7158
8461
|
: { writeLaneSourceEnabled: next };
|
|
8462
|
+
if (state.session) {
|
|
8463
|
+
if (lane === "content") {
|
|
8464
|
+
state.session.autoPilotWriteLaneContent = next;
|
|
8465
|
+
} else if (lane === "ui-tests") {
|
|
8466
|
+
state.session.autoPilotWriteLaneUiTests = next;
|
|
8467
|
+
} else {
|
|
8468
|
+
state.session.autoPilotWriteLaneSource = next;
|
|
8469
|
+
}
|
|
8470
|
+
reconcileAutoPilotTrustedWrites();
|
|
8471
|
+
}
|
|
8472
|
+
await renderShell();
|
|
7159
8473
|
try {
|
|
7160
8474
|
const result = await apiPost("/api/settings/auto-pilot", payload);
|
|
7161
8475
|
applyAutoPilotSettingsResult(result);
|
|
7162
|
-
await refreshAuthenticatedState();
|
|
7163
8476
|
} catch (error) {
|
|
8477
|
+
restoreAutoPilotSettings(previous);
|
|
7164
8478
|
state.pushError = error.message || String(error);
|
|
7165
8479
|
}
|
|
7166
8480
|
await renderShell();
|
|
@@ -7181,11 +8495,23 @@ function bindShellInteractions() {
|
|
|
7181
8495
|
if (!payload) {
|
|
7182
8496
|
return;
|
|
7183
8497
|
}
|
|
8498
|
+
const previous = snapshotAutoPilotSettings();
|
|
8499
|
+
if (state.session) {
|
|
8500
|
+
if (lane === "content") {
|
|
8501
|
+
state.session.autoPilotWriteLaneContent = true;
|
|
8502
|
+
} else if (lane === "ui_tests") {
|
|
8503
|
+
state.session.autoPilotWriteLaneUiTests = true;
|
|
8504
|
+
} else {
|
|
8505
|
+
state.session.autoPilotWriteLaneSource = true;
|
|
8506
|
+
}
|
|
8507
|
+
reconcileAutoPilotTrustedWrites();
|
|
8508
|
+
}
|
|
8509
|
+
await renderShell();
|
|
7184
8510
|
try {
|
|
7185
8511
|
const result = await apiPost("/api/settings/auto-pilot", payload);
|
|
7186
8512
|
applyAutoPilotSettingsResult(result);
|
|
7187
|
-
await refreshAuthenticatedState();
|
|
7188
8513
|
} catch (error) {
|
|
8514
|
+
restoreAutoPilotSettings(previous);
|
|
7189
8515
|
state.pushError = error.message || String(error);
|
|
7190
8516
|
}
|
|
7191
8517
|
await renderShell();
|
|
@@ -7272,6 +8598,166 @@ function bindShellInteractions() {
|
|
|
7272
8598
|
});
|
|
7273
8599
|
}
|
|
7274
8600
|
|
|
8601
|
+
// ─── Remote pairing (Phase 5c) ─────────────────────────────────────
|
|
8602
|
+
// Optimistic flips with bridge-side reconciliation. The bridge endpoints
|
|
8603
|
+
// hot-restart the orchestrator, so the post-POST `fetchRemotePairingStatus`
|
|
8604
|
+
// call can briefly observe a transient state — that's fine, the next
|
|
8605
|
+
// render will land on the steady state.
|
|
8606
|
+
for (const details of document.querySelectorAll("[data-remote-pairing-details]")) {
|
|
8607
|
+
details.addEventListener("toggle", () => {
|
|
8608
|
+
state.remotePairingDetailsOpen = details.open === true;
|
|
8609
|
+
});
|
|
8610
|
+
}
|
|
8611
|
+
|
|
8612
|
+
for (const checkbox of document.querySelectorAll("[data-remote-pairing-toggle-checkbox]")) {
|
|
8613
|
+
checkbox.addEventListener("change", async () => {
|
|
8614
|
+
const next = checkbox.checked === true;
|
|
8615
|
+
const previous = state.remotePairingStatus?.enabled === true;
|
|
8616
|
+
if (!next && isRemotePairingUsingRelay() && !window.confirm(L("settings.remotePairing.toggle.offConfirm"))) {
|
|
8617
|
+
checkbox.checked = previous;
|
|
8618
|
+
return;
|
|
8619
|
+
}
|
|
8620
|
+
state.remotePairingNotice = "";
|
|
8621
|
+
state.remotePairingError = "";
|
|
8622
|
+
state.remotePairingPending = "toggle";
|
|
8623
|
+
// Optimistic flip — render once with the new value disabled-during-pending,
|
|
8624
|
+
// then reconcile with the bridge.
|
|
8625
|
+
if (state.remotePairingStatus) {
|
|
8626
|
+
state.remotePairingStatus = { ...state.remotePairingStatus, enabled: next };
|
|
8627
|
+
} else {
|
|
8628
|
+
state.remotePairingStatus = {
|
|
8629
|
+
enabled: next,
|
|
8630
|
+
relayUrl: "",
|
|
8631
|
+
configuredRelayUrl: "",
|
|
8632
|
+
identityFingerprint: null,
|
|
8633
|
+
sessions: [],
|
|
8634
|
+
pairings: [],
|
|
8635
|
+
auditEvents: [],
|
|
8636
|
+
};
|
|
8637
|
+
}
|
|
8638
|
+
await renderShell();
|
|
8639
|
+
try {
|
|
8640
|
+
await apiPost("/api/remote-pairing/toggle", { enabled: next });
|
|
8641
|
+
state.remotePairingNotice = next
|
|
8642
|
+
? L("settings.remotePairing.notice.toggleOn")
|
|
8643
|
+
: L("settings.remotePairing.notice.toggleOff");
|
|
8644
|
+
await fetchRemotePairingStatus();
|
|
8645
|
+
} catch (error) {
|
|
8646
|
+
// Roll back to the previous value so the checkbox visually reverts.
|
|
8647
|
+
if (state.remotePairingStatus) {
|
|
8648
|
+
state.remotePairingStatus = { ...state.remotePairingStatus, enabled: previous };
|
|
8649
|
+
}
|
|
8650
|
+
state.remotePairingError = L("settings.remotePairing.error.toggleFailed");
|
|
8651
|
+
} finally {
|
|
8652
|
+
state.remotePairingPending = "";
|
|
8653
|
+
await renderShell();
|
|
8654
|
+
}
|
|
8655
|
+
});
|
|
8656
|
+
}
|
|
8657
|
+
|
|
8658
|
+
for (const button of document.querySelectorAll("[data-remote-pairing-relay-url-save]")) {
|
|
8659
|
+
button.addEventListener("click", async () => {
|
|
8660
|
+
// The input lives on the same page; querySelector grabs the (single)
|
|
8661
|
+
// visible field. If we ever add multiple URL editors we'd need to
|
|
8662
|
+
// scope this to a container — but right now there's exactly one.
|
|
8663
|
+
const input = document.querySelector("[data-remote-pairing-relay-url-input]");
|
|
8664
|
+
const trimmed = (input?.value || "").trim();
|
|
8665
|
+
state.remotePairingNotice = "";
|
|
8666
|
+
state.remotePairingError = "";
|
|
8667
|
+
state.remotePairingPending = "relayUrl";
|
|
8668
|
+
await renderShell();
|
|
8669
|
+
try {
|
|
8670
|
+
await apiPost("/api/remote-pairing/relay-url", { relayUrl: trimmed });
|
|
8671
|
+
state.remotePairingNotice = L("settings.remotePairing.notice.relayUrlSaved");
|
|
8672
|
+
await fetchRemotePairingStatus();
|
|
8673
|
+
} catch (error) {
|
|
8674
|
+
// Distinguish the validation error from a generic save failure so
|
|
8675
|
+
// the user knows whether to fix the URL or retry.
|
|
8676
|
+
state.remotePairingError = error?.errorKey === "invalid-relay-url"
|
|
8677
|
+
? L("settings.remotePairing.error.invalidRelayUrl")
|
|
8678
|
+
: L("settings.remotePairing.error.relayUrlFailed");
|
|
8679
|
+
} finally {
|
|
8680
|
+
state.remotePairingPending = "";
|
|
8681
|
+
await renderShell();
|
|
8682
|
+
}
|
|
8683
|
+
});
|
|
8684
|
+
}
|
|
8685
|
+
|
|
8686
|
+
for (const button of document.querySelectorAll("[data-remote-pairing-rotate-token]")) {
|
|
8687
|
+
button.addEventListener("click", async () => {
|
|
8688
|
+
const local = loadRemotePairingState();
|
|
8689
|
+
if (!local?.phonePub) return;
|
|
8690
|
+
state.remotePairingNotice = "";
|
|
8691
|
+
state.remotePairingError = "";
|
|
8692
|
+
state.remotePairingPending = "rotateToken";
|
|
8693
|
+
await renderShell();
|
|
8694
|
+
try {
|
|
8695
|
+
const response = await apiPost("/api/remote-pairing/rotate-token", { phonePub: local.phonePub });
|
|
8696
|
+
saveRemotePairingState({
|
|
8697
|
+
pairingId: response.pairingId,
|
|
8698
|
+
relayToken: response.relayToken,
|
|
8699
|
+
phonePub: response.phonePub,
|
|
8700
|
+
phoneFingerprint: response.phoneFingerprint,
|
|
8701
|
+
bridgePubHex: response.bridgePubHex,
|
|
8702
|
+
bridgeFingerprint: response.bridgeFingerprint,
|
|
8703
|
+
relayUrl: response.relayUrl,
|
|
8704
|
+
label: response.label || local.label || "",
|
|
8705
|
+
addedAtMs: Number.isFinite(response.addedAtMs) ? response.addedAtMs : local.addedAtMs,
|
|
8706
|
+
relayTokenUpdatedAtMs: Number.isFinite(response.relayTokenUpdatedAtMs)
|
|
8707
|
+
? response.relayTokenUpdatedAtMs
|
|
8708
|
+
: Date.now(),
|
|
8709
|
+
});
|
|
8710
|
+
state.remotePairingNotice = L("settings.remotePairing.notice.tokenRotated");
|
|
8711
|
+
await fetchRemotePairingStatus();
|
|
8712
|
+
} catch (error) {
|
|
8713
|
+
state.remotePairingError = L("settings.remotePairing.error.tokenRotateFailed");
|
|
8714
|
+
} finally {
|
|
8715
|
+
state.remotePairingPending = "";
|
|
8716
|
+
await renderShell();
|
|
8717
|
+
}
|
|
8718
|
+
});
|
|
8719
|
+
}
|
|
8720
|
+
|
|
8721
|
+
for (const button of document.querySelectorAll("[data-remote-pairing-revoke]")) {
|
|
8722
|
+
button.addEventListener("click", async () => {
|
|
8723
|
+
const phonePub = button.dataset.remotePairingRevoke || "";
|
|
8724
|
+
if (!phonePub) return;
|
|
8725
|
+
if (!window.confirm(L("settings.remotePairing.pairings.revokeConfirm"))) return;
|
|
8726
|
+
state.remotePairingNotice = "";
|
|
8727
|
+
state.remotePairingError = "";
|
|
8728
|
+
// Per-pub pending key so the renderer only disables this card's
|
|
8729
|
+
// button, not every revoke button on the page.
|
|
8730
|
+
state.remotePairingPending = `revoke:${phonePub}`;
|
|
8731
|
+
await renderShell();
|
|
8732
|
+
try {
|
|
8733
|
+
await apiPost("/api/remote-pairing/revoke", { phonePub });
|
|
8734
|
+
// If the revoked phone is THIS device, drop the local enrollment
|
|
8735
|
+
// record too — otherwise the "this device" indicator would keep
|
|
8736
|
+
// claiming the (now-revoked) record is live, and a future fetch
|
|
8737
|
+
// routing layer would try to reuse the stale pairingId. Best
|
|
8738
|
+
// effort: storage failures don't change the revoke outcome.
|
|
8739
|
+
try {
|
|
8740
|
+
const local = loadRemotePairingState();
|
|
8741
|
+
if (
|
|
8742
|
+
local
|
|
8743
|
+
&& local.phonePub.toLowerCase() === phonePub.toLowerCase()
|
|
8744
|
+
) {
|
|
8745
|
+
clearRemotePairingState();
|
|
8746
|
+
}
|
|
8747
|
+
} catch {
|
|
8748
|
+
// ignore — same fall-through as savePairingState
|
|
8749
|
+
}
|
|
8750
|
+
state.remotePairingNotice = L("settings.remotePairing.notice.revoked");
|
|
8751
|
+
await fetchRemotePairingStatus();
|
|
8752
|
+
} catch (error) {
|
|
8753
|
+
state.remotePairingError = L("settings.remotePairing.error.revokeFailed");
|
|
8754
|
+
} finally {
|
|
8755
|
+
state.remotePairingPending = "";
|
|
8756
|
+
await renderShell();
|
|
8757
|
+
}
|
|
8758
|
+
});
|
|
8759
|
+
}
|
|
8760
|
+
|
|
7275
8761
|
|
|
7276
8762
|
for (const button of document.querySelectorAll("[data-hazbase-action]")) {
|
|
7277
8763
|
button.addEventListener("click", async () => {
|
|
@@ -7548,24 +9034,13 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7548
9034
|
try {
|
|
7549
9035
|
const decisionBody = { action: submittedAction, editedText };
|
|
7550
9036
|
if (editedTitle) decisionBody.editedTitle = editedTitle;
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
const errBody = await res.json().catch(() => ({}));
|
|
7559
|
-
alert(`Moltbook draft ${submittedAction} failed: ${errBody.error || res.status}`);
|
|
7560
|
-
buttons.forEach((btn) => {
|
|
7561
|
-
btn.disabled = false;
|
|
7562
|
-
btn.classList.remove("is-loading", "is-dimmed");
|
|
7563
|
-
btn.innerHTML = labelCache.get(btn);
|
|
7564
|
-
});
|
|
7565
|
-
if (textarea) textarea.readOnly = false;
|
|
7566
|
-
moltbookDraftForm.dataset.submitting = "";
|
|
7567
|
-
return;
|
|
7568
|
-
}
|
|
9037
|
+
// apiPost routes through LAN-first / relay-fallback and throws on
|
|
9038
|
+
// !ok — the existing catch below handles both transport and HTTP
|
|
9039
|
+
// failures uniformly.
|
|
9040
|
+
await apiPost(
|
|
9041
|
+
`/api/items/moltbook-draft/${encodeURIComponent(token)}/decision`,
|
|
9042
|
+
decisionBody,
|
|
9043
|
+
);
|
|
7569
9044
|
// Mark local detail as resolved so re-render shows "already resolved" immediately.
|
|
7570
9045
|
if (state.currentDetail?.kind === "moltbook_draft") {
|
|
7571
9046
|
state.currentDetail.moltbookDraftEnabled = false;
|
|
@@ -7574,7 +9049,7 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7574
9049
|
await refreshAuthenticatedState();
|
|
7575
9050
|
await renderShell();
|
|
7576
9051
|
} catch (error) {
|
|
7577
|
-
alert(`Moltbook draft
|
|
9052
|
+
alert(`Moltbook draft ${submittedAction} failed: ${error.message}`);
|
|
7578
9053
|
buttons.forEach((btn) => {
|
|
7579
9054
|
btn.disabled = false;
|
|
7580
9055
|
btn.classList.remove("is-loading", "is-dimmed");
|
|
@@ -7625,24 +9100,10 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7625
9100
|
try {
|
|
7626
9101
|
const decisionBody = { action: submittedAction, instruction };
|
|
7627
9102
|
if (executor) decisionBody.executor = executor;
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
body: JSON.stringify(decisionBody),
|
|
7633
|
-
});
|
|
7634
|
-
if (!res.ok) {
|
|
7635
|
-
const errBody = await res.json().catch(() => ({}));
|
|
7636
|
-
alert(`A2A task ${submittedAction} failed: ${errBody.error || res.status}`);
|
|
7637
|
-
buttons.forEach((btn) => {
|
|
7638
|
-
btn.disabled = false;
|
|
7639
|
-
btn.classList.remove("is-loading", "is-dimmed");
|
|
7640
|
-
btn.innerHTML = labelCache.get(btn);
|
|
7641
|
-
});
|
|
7642
|
-
if (textarea) textarea.readOnly = false;
|
|
7643
|
-
a2aTaskForm.dataset.submitting = "";
|
|
7644
|
-
return;
|
|
7645
|
-
}
|
|
9103
|
+
await apiPost(
|
|
9104
|
+
`/api/items/a2a-task/${encodeURIComponent(token)}/decision`,
|
|
9105
|
+
decisionBody,
|
|
9106
|
+
);
|
|
7646
9107
|
if (state.currentDetail?.kind === "a2a_task") {
|
|
7647
9108
|
state.currentDetail.a2aTaskEnabled = false;
|
|
7648
9109
|
state.currentDetail.readOnly = true;
|
|
@@ -7650,7 +9111,7 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7650
9111
|
await refreshAuthenticatedState();
|
|
7651
9112
|
await renderShell();
|
|
7652
9113
|
} catch (error) {
|
|
7653
|
-
alert(`A2A task
|
|
9114
|
+
alert(`A2A task ${submittedAction} failed: ${error.message}`);
|
|
7654
9115
|
buttons.forEach((btn) => {
|
|
7655
9116
|
btn.disabled = false;
|
|
7656
9117
|
btn.classList.remove("is-loading", "is-dimmed");
|
|
@@ -7691,24 +9152,10 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7691
9152
|
if (textarea) textarea.readOnly = true;
|
|
7692
9153
|
const editedContent = normalizeClientText(new FormData(threadShareForm).get("shareContent"));
|
|
7693
9154
|
try {
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
body: JSON.stringify({ decision: submittedAction, editedContent }),
|
|
7699
|
-
});
|
|
7700
|
-
if (!res.ok) {
|
|
7701
|
-
const errBody = await res.json().catch(() => ({}));
|
|
7702
|
-
alert(`Thread share ${submittedAction} failed: ${errBody.error || res.status}`);
|
|
7703
|
-
buttons.forEach((btn) => {
|
|
7704
|
-
btn.disabled = false;
|
|
7705
|
-
btn.classList.remove("is-loading", "is-dimmed");
|
|
7706
|
-
btn.innerHTML = labelCache.get(btn);
|
|
7707
|
-
});
|
|
7708
|
-
if (textarea) textarea.readOnly = false;
|
|
7709
|
-
threadShareForm.dataset.submitting = "";
|
|
7710
|
-
return;
|
|
7711
|
-
}
|
|
9155
|
+
await apiPost(
|
|
9156
|
+
`/api/threads/share/${encodeURIComponent(token)}/decision`,
|
|
9157
|
+
{ decision: submittedAction, editedContent },
|
|
9158
|
+
);
|
|
7712
9159
|
if (state.currentDetail?.kind === "thread_share") {
|
|
7713
9160
|
state.currentDetail.threadShareEnabled = false;
|
|
7714
9161
|
state.currentDetail.readOnly = true;
|
|
@@ -7716,7 +9163,7 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7716
9163
|
await refreshAuthenticatedState();
|
|
7717
9164
|
await renderShell();
|
|
7718
9165
|
} catch (error) {
|
|
7719
|
-
alert(`Thread share
|
|
9166
|
+
alert(`Thread share ${submittedAction} failed: ${error.message}`);
|
|
7720
9167
|
buttons.forEach((btn) => {
|
|
7721
9168
|
btn.disabled = false;
|
|
7722
9169
|
btn.classList.remove("is-loading", "is-dimmed");
|
|
@@ -7775,14 +9222,22 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7775
9222
|
await renderShell();
|
|
7776
9223
|
|
|
7777
9224
|
try {
|
|
7778
|
-
const
|
|
7779
|
-
requestBody
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
9225
|
+
const attachments = (draft.attachments || []).filter((attachment) => attachment?.file);
|
|
9226
|
+
let requestBody;
|
|
9227
|
+
if (attachments.length > 0) {
|
|
9228
|
+
requestBody = new FormData();
|
|
9229
|
+
requestBody.set("text", text);
|
|
9230
|
+
requestBody.set("planMode", draft.mode === "plan" ? "true" : "false");
|
|
9231
|
+
requestBody.set("force", draft.confirmOverride === true ? "true" : "false");
|
|
9232
|
+
} else {
|
|
9233
|
+
requestBody = {
|
|
9234
|
+
text,
|
|
9235
|
+
planMode: draft.mode === "plan",
|
|
9236
|
+
force: draft.confirmOverride === true,
|
|
9237
|
+
};
|
|
9238
|
+
}
|
|
9239
|
+
for (const attachment of attachments) {
|
|
9240
|
+
requestBody.append("image", attachment.file, attachment.name || attachment.file.name);
|
|
7786
9241
|
}
|
|
7787
9242
|
const replyKind = replyForm.dataset.replyKind || "completion";
|
|
7788
9243
|
await apiPost(`/api/items/${encodeURIComponent(replyKind)}/${encodeURIComponent(token)}/reply`, requestBody);
|
|
@@ -7941,6 +9396,13 @@ function bindSharedUi(renderFn) {
|
|
|
7941
9396
|
});
|
|
7942
9397
|
}
|
|
7943
9398
|
|
|
9399
|
+
for (const button of document.querySelectorAll("[data-force-app-refresh]")) {
|
|
9400
|
+
button.addEventListener("click", async () => {
|
|
9401
|
+
await forceAppRefreshFromLan();
|
|
9402
|
+
await renderFn();
|
|
9403
|
+
});
|
|
9404
|
+
}
|
|
9405
|
+
|
|
7944
9406
|
for (const button of document.querySelectorAll("[data-install-guide-close]")) {
|
|
7945
9407
|
button.addEventListener("click", async (event) => {
|
|
7946
9408
|
if (button.classList.contains("modal-backdrop")) {
|
|
@@ -8608,6 +10070,10 @@ function renderIcon(name) {
|
|
|
8608
10070
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round"><rect x="7.2" y="2.8" width="9.6" height="18.4" rx="2.4"/><path d="M10 6.7h4"/><circle cx="12" cy="17.6" r="0.7" fill="currentColor" stroke="none"/></svg>`;
|
|
8609
10071
|
case "language":
|
|
8610
10072
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3.5c3.8 0 7 3.8 7 8.5s-3.2 8.5-7 8.5-7-3.8-7-8.5 3.2-8.5 7-8.5Z"/><path d="M5.8 9h12.4"/><path d="M5.8 15h12.4"/><path d="M12 3.8c1.9 2 3 4.9 3 8.2s-1.1 6.2-3 8.2c-1.9-2-3-4.9-3-8.2s1.1-6.2 3-8.2Z"/></svg>`;
|
|
10073
|
+
case "agent-network":
|
|
10074
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round"><circle cx="6.8" cy="7.2" r="2.7"/><circle cx="17.2" cy="7.2" r="2.7"/><circle cx="12" cy="17" r="3"/><path d="M9.2 8.8 11 14.1"/><path d="m14.8 8.8-1.9 5.3"/><path d="M9.5 7.2h5"/></svg>`;
|
|
10075
|
+
case "remote-connection":
|
|
10076
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="12" width="14" height="7" rx="2"/><path d="M9 19h6"/><path d="M12 12v7"/><path d="M8.2 8.8a5.4 5.4 0 0 1 7.6 0"/><path d="M10.2 6.3a8.2 8.2 0 0 1 3.6 0"/><path d="M11.2 9.8a1.2 1.2 0 0 1 1.6 0"/></svg>`;
|
|
8611
10077
|
case "link":
|
|
8612
10078
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M10.4 13.6 8.3 15.7a3 3 0 0 1-4.2-4.2l2.8-2.8a3 3 0 0 1 4.2 0"/><path d="m13.6 10.4 2.1-2.1a3 3 0 1 1 4.2 4.2l-2.8 2.8a3 3 0 0 1-4.2 0"/><path d="m9.5 14.5 5-5"/></svg>`;
|
|
8613
10079
|
case "clip":
|
|
@@ -8758,13 +10224,138 @@ function handleDocumentVisibilityChange() {
|
|
|
8758
10224
|
handlePotentialExternalNavigation();
|
|
8759
10225
|
}
|
|
8760
10226
|
|
|
8761
|
-
async function
|
|
8762
|
-
|
|
10227
|
+
async function hydrateTimelinePayloadImages(payload) {
|
|
10228
|
+
if (!payload || !Array.isArray(payload.entries)) {
|
|
10229
|
+
return payload;
|
|
10230
|
+
}
|
|
10231
|
+
|
|
10232
|
+
const entries = await Promise.all(
|
|
10233
|
+
payload.entries.map(async (entry) => hydrateItemImageUrls(entry))
|
|
10234
|
+
);
|
|
10235
|
+
return { ...payload, entries };
|
|
10236
|
+
}
|
|
10237
|
+
|
|
10238
|
+
async function hydrateDetailImages(detail) {
|
|
10239
|
+
if (!detail || !Array.isArray(detail.imageUrls)) {
|
|
10240
|
+
return detail;
|
|
10241
|
+
}
|
|
10242
|
+
return hydrateItemImageUrls(detail);
|
|
10243
|
+
}
|
|
10244
|
+
|
|
10245
|
+
async function hydrateItemImageUrls(item) {
|
|
10246
|
+
const imageUrls = Array.isArray(item?.imageUrls) ? item.imageUrls.filter(Boolean) : [];
|
|
10247
|
+
if (imageUrls.length === 0) {
|
|
10248
|
+
return item;
|
|
10249
|
+
}
|
|
10250
|
+
|
|
10251
|
+
const hydratedUrls = await Promise.all(imageUrls.map((url) => routedTimelineImageUrl(url)));
|
|
10252
|
+
return {
|
|
10253
|
+
...item,
|
|
10254
|
+
imageUrls: hydratedUrls.filter(Boolean),
|
|
10255
|
+
};
|
|
10256
|
+
}
|
|
10257
|
+
|
|
10258
|
+
async function routedTimelineImageUrl(imageUrl) {
|
|
10259
|
+
const sourceUrl = normalizeClientText(imageUrl);
|
|
10260
|
+
if (!sourceUrl || /^(?:blob:|data:)/iu.test(sourceUrl)) {
|
|
10261
|
+
return sourceUrl;
|
|
10262
|
+
}
|
|
10263
|
+
|
|
10264
|
+
const existing = timelineImageObjectUrlCache.get(sourceUrl);
|
|
10265
|
+
if (existing?.objectUrl) {
|
|
10266
|
+
existing.lastUsedMs = Date.now();
|
|
10267
|
+
return existing.objectUrl;
|
|
10268
|
+
}
|
|
10269
|
+
|
|
10270
|
+
try {
|
|
10271
|
+
const response = await routedFetch(sourceUrl, {
|
|
10272
|
+
credentials: "same-origin",
|
|
10273
|
+
headers: {
|
|
10274
|
+
Accept: "image/*",
|
|
10275
|
+
},
|
|
10276
|
+
});
|
|
10277
|
+
if (!response.ok) {
|
|
10278
|
+
return unavailableTimelineImageDataUrl();
|
|
10279
|
+
}
|
|
10280
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
10281
|
+
const contentType = responseHeader(response.headers, "content-type") || "application/octet-stream";
|
|
10282
|
+
const objectUrl = URL.createObjectURL(new Blob([arrayBuffer], { type: contentType }));
|
|
10283
|
+
timelineImageObjectUrlCache.set(sourceUrl, {
|
|
10284
|
+
objectUrl,
|
|
10285
|
+
lastUsedMs: Date.now(),
|
|
10286
|
+
});
|
|
10287
|
+
pruneTimelineImageObjectUrlCache();
|
|
10288
|
+
return objectUrl;
|
|
10289
|
+
} catch {
|
|
10290
|
+
return unavailableTimelineImageDataUrl();
|
|
10291
|
+
}
|
|
10292
|
+
}
|
|
10293
|
+
|
|
10294
|
+
function unavailableTimelineImageDataUrl() {
|
|
10295
|
+
const title = L("detail.imageUnavailable");
|
|
10296
|
+
const subtitle = L("detail.imageUnavailableHint");
|
|
10297
|
+
const svg = `
|
|
10298
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="640" viewBox="0 0 640 640">
|
|
10299
|
+
<defs>
|
|
10300
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
10301
|
+
<stop offset="0" stop-color="#132026"/>
|
|
10302
|
+
<stop offset="1" stop-color="#0a1116"/>
|
|
10303
|
+
</linearGradient>
|
|
10304
|
+
</defs>
|
|
10305
|
+
<rect width="640" height="640" rx="34" fill="url(#bg)"/>
|
|
10306
|
+
<rect x="24" y="24" width="592" height="592" rx="28" fill="none" stroke="#9cb5c5" stroke-opacity=".18" stroke-width="2"/>
|
|
10307
|
+
<circle cx="320" cy="266" r="50" fill="#26343d"/>
|
|
10308
|
+
<path d="M294 266h52M320 240v52" stroke="#d7e5ed" stroke-width="12" stroke-linecap="round" opacity=".78"/>
|
|
10309
|
+
<text x="320" y="360" text-anchor="middle" fill="#d7e5ed" font-family="Avenir Next, Helvetica, Arial, sans-serif" font-size="31" font-weight="700">${escapeSvgText(title)}</text>
|
|
10310
|
+
<text x="320" y="410" text-anchor="middle" fill="#9cb5c5" font-family="Avenir Next, Helvetica, Arial, sans-serif" font-size="22">${escapeSvgText(subtitle)}</text>
|
|
10311
|
+
</svg>
|
|
10312
|
+
`.trim();
|
|
10313
|
+
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`;
|
|
10314
|
+
}
|
|
10315
|
+
|
|
10316
|
+
function escapeSvgText(value) {
|
|
10317
|
+
return normalizeClientText(value)
|
|
10318
|
+
.replace(/&/gu, "&")
|
|
10319
|
+
.replace(/</gu, "<")
|
|
10320
|
+
.replace(/>/gu, ">");
|
|
10321
|
+
}
|
|
10322
|
+
|
|
10323
|
+
function responseHeader(headers, name) {
|
|
10324
|
+
const key = String(name || "").toLowerCase();
|
|
10325
|
+
if (!headers || !key) {
|
|
10326
|
+
return "";
|
|
10327
|
+
}
|
|
10328
|
+
if (typeof headers.get === "function") {
|
|
10329
|
+
return headers.get(key) || "";
|
|
10330
|
+
}
|
|
10331
|
+
return headers[key] || headers[name] || "";
|
|
10332
|
+
}
|
|
10333
|
+
|
|
10334
|
+
function pruneTimelineImageObjectUrlCache() {
|
|
10335
|
+
if (timelineImageObjectUrlCache.size <= MAX_TIMELINE_IMAGE_OBJECT_URLS) {
|
|
10336
|
+
return;
|
|
10337
|
+
}
|
|
10338
|
+
const staleEntries = [...timelineImageObjectUrlCache.entries()]
|
|
10339
|
+
.sort((left, right) => Number(left[1]?.lastUsedMs || 0) - Number(right[1]?.lastUsedMs || 0))
|
|
10340
|
+
.slice(0, Math.max(0, timelineImageObjectUrlCache.size - MAX_TIMELINE_IMAGE_OBJECT_URLS));
|
|
10341
|
+
for (const [sourceUrl, entry] of staleEntries) {
|
|
10342
|
+
if (entry?.objectUrl && typeof URL !== "undefined" && typeof URL.revokeObjectURL === "function") {
|
|
10343
|
+
URL.revokeObjectURL(entry.objectUrl);
|
|
10344
|
+
}
|
|
10345
|
+
timelineImageObjectUrlCache.delete(sourceUrl);
|
|
10346
|
+
}
|
|
10347
|
+
}
|
|
10348
|
+
|
|
10349
|
+
async function apiGet(url, opts = {}) {
|
|
10350
|
+
// routedFetch tries LAN first, then falls back to the relay tunnel when
|
|
10351
|
+
// the phone is off-LAN. Returns a fetch-Response-compatible object so the
|
|
10352
|
+
// rest of this function is identical to a plain `fetch()` call.
|
|
10353
|
+
const response = await routedFetch(url, {
|
|
8763
10354
|
credentials: "same-origin",
|
|
8764
10355
|
headers: {
|
|
8765
10356
|
Accept: "application/json",
|
|
8766
10357
|
},
|
|
8767
|
-
});
|
|
10358
|
+
}, opts);
|
|
8768
10359
|
if (!response.ok) {
|
|
8769
10360
|
const errorInfo = await readError(response);
|
|
8770
10361
|
const error = new Error(errorInfo.message);
|
|
@@ -8778,7 +10369,9 @@ async function apiGet(url) {
|
|
|
8778
10369
|
|
|
8779
10370
|
async function apiPost(url, body) {
|
|
8780
10371
|
const isFormDataBody = typeof FormData !== "undefined" && body instanceof FormData;
|
|
8781
|
-
|
|
10372
|
+
// Keep native FormData for LAN; routedFetch serializes it to multipart
|
|
10373
|
+
// bytes only when the request has to travel through the remote relay.
|
|
10374
|
+
const response = await routedFetch(url, {
|
|
8782
10375
|
method: "POST",
|
|
8783
10376
|
credentials: "same-origin",
|
|
8784
10377
|
headers: isFormDataBody
|