viveworker 0.7.0-beta.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -4
- package/package.json +7 -3
- 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/moltbook-scout-auto.sh +16 -8
- package/scripts/share-cli.mjs +14 -130
- package/scripts/viveworker-bridge.mjs +1696 -223
- package/scripts/viveworker.mjs +27 -6
- package/web/app.css +727 -9
- package/web/app.js +1810 -232
- package/web/i18n.js +207 -17
- package/web/index.html +115 -1
- 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/app.js
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
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
|
+
import { getRoutingTelemetry, routedFetch } from "./remote-pairing/api-router.js?v=20260428-remote-token-refresh";
|
|
2
9
|
|
|
3
10
|
const DESKTOP_BREAKPOINT = 980;
|
|
11
|
+
const APP_BUILD_ID = "20260428-remote-token-refresh";
|
|
4
12
|
const INSTALL_BANNER_DISMISS_KEY = "viveworker-install-banner-dismissed-v2";
|
|
5
13
|
const PUSH_BANNER_DISMISS_KEY = "viveworker-push-banner-dismissed-v1";
|
|
6
14
|
const INITIAL_DETECTED_LOCALE = detectBrowserLocale();
|
|
@@ -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
|
};
|
|
@@ -132,6 +177,302 @@ function hazbasePasskeyHostSupport() {
|
|
|
132
177
|
}
|
|
133
178
|
|
|
134
179
|
const app = document.querySelector("#app");
|
|
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
|
+
}
|
|
314
|
+
|
|
315
|
+
function dismissBootSplash() {
|
|
316
|
+
if (bootSplashDismissed || typeof document === "undefined") {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
bootSplashDismissed = true;
|
|
320
|
+
clearBootSplashHintTimer();
|
|
321
|
+
clearBootSplashDeferredStatusTimer();
|
|
322
|
+
if (typeof window !== "undefined") {
|
|
323
|
+
window.removeEventListener("viveworker:remote-routing-status", handleBootRoutingStatus);
|
|
324
|
+
}
|
|
325
|
+
const splash = document.querySelector("#boot-splash");
|
|
326
|
+
document.body?.classList.add("viveworker-ready");
|
|
327
|
+
if (!splash) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
splash.setAttribute("aria-hidden", "true");
|
|
331
|
+
window.setTimeout(() => {
|
|
332
|
+
splash.remove();
|
|
333
|
+
}, 280);
|
|
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
|
+
}
|
|
135
476
|
const params = new URLSearchParams(window.location.search);
|
|
136
477
|
const initialItem = params.get("item") || "";
|
|
137
478
|
const initialTargetTab = params.get("tab") || "";
|
|
@@ -142,10 +483,14 @@ let didReloadForServiceWorker = false;
|
|
|
142
483
|
let lastViewportMode = isDesktopLayout();
|
|
143
484
|
|
|
144
485
|
boot().catch((error) => {
|
|
145
|
-
const message =
|
|
146
|
-
const hint =
|
|
486
|
+
const message = bootErrorMessage(error);
|
|
487
|
+
const hint = shouldShowNetworkHint(error, message)
|
|
147
488
|
? `<p class="muted">${escapeHtml(L("error.networkHint"))}</p>`
|
|
148
489
|
: "";
|
|
490
|
+
flushBootTrace("boot-error", {
|
|
491
|
+
error: sanitizeBootTraceValue(error?.code || error?.name || message),
|
|
492
|
+
});
|
|
493
|
+
dismissBootSplash();
|
|
149
494
|
app.innerHTML = `
|
|
150
495
|
<main class="onboarding-shell">
|
|
151
496
|
<section class="onboarding-card">
|
|
@@ -158,13 +503,95 @@ boot().catch((error) => {
|
|
|
158
503
|
`;
|
|
159
504
|
});
|
|
160
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
|
+
|
|
161
579
|
async function boot() {
|
|
580
|
+
recordBootTraceEvent("boot-start", {
|
|
581
|
+
url: window.location?.pathname || "/app",
|
|
582
|
+
});
|
|
162
583
|
updateManifestHref(initialPairToken);
|
|
584
|
+
syncVisualViewportMetrics();
|
|
585
|
+
setBootSplashStatus("boot.status.checkingLan", BOOT_SPLASH_STAGE.checking);
|
|
163
586
|
// SW register + update() can take hundreds of ms and does not need to gate
|
|
164
587
|
// first paint. Fire and forget; the `controllerchange` reload handler wired
|
|
165
588
|
// up inside `registerServiceWorker` still picks up new versions.
|
|
166
589
|
registerServiceWorker().catch(() => {});
|
|
590
|
+
window.addEventListener("viveworker:remote-routing-status", handleBootRoutingStatus);
|
|
167
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 });
|
|
168
595
|
window.addEventListener("resize", handleViewportChange, { passive: true });
|
|
169
596
|
window.addEventListener("focus", handlePotentialExternalNavigation, { passive: true });
|
|
170
597
|
window.addEventListener("pageshow", handlePotentialExternalNavigation, { passive: true });
|
|
@@ -173,13 +600,24 @@ async function boot() {
|
|
|
173
600
|
// Single round-trip for session + inbox(pending/completed) + timeline +
|
|
174
601
|
// devices. See `refreshBootstrap` for why we collapsed the boot fan-out.
|
|
175
602
|
await refreshBootstrap();
|
|
603
|
+
if (bootSplashRemoteRouteSeen) {
|
|
604
|
+
setBootRemoteStatusAfterSwitching("boot.status.loadingData", BOOT_SPLASH_STAGE.loading);
|
|
605
|
+
}
|
|
606
|
+
flushBootTrace("bootstrap-complete");
|
|
176
607
|
|
|
177
608
|
if (!state.session?.authenticated && initialPairToken && shouldAutoPairFromBootstrapToken()) {
|
|
178
609
|
try {
|
|
179
|
-
await pair({
|
|
610
|
+
const pairResult = await pair({
|
|
180
611
|
token: initialPairToken,
|
|
181
612
|
temporary: shouldUseTemporaryBootstrapPairing(),
|
|
182
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
|
+
}
|
|
183
621
|
} catch (error) {
|
|
184
622
|
state.pairError = error.message || String(error);
|
|
185
623
|
}
|
|
@@ -211,6 +649,7 @@ async function boot() {
|
|
|
211
649
|
return;
|
|
212
650
|
}
|
|
213
651
|
|
|
652
|
+
await maybeAutoEnrollRemotePairingFromLan();
|
|
214
653
|
await consumePendingNotificationIntent();
|
|
215
654
|
// `?focusPending=claude` marks this tab as the Claude-hook-opened popup:
|
|
216
655
|
// auto-navigate to the newest unresolved Claude pending (plan/question)
|
|
@@ -288,6 +727,7 @@ async function boot() {
|
|
|
288
727
|
refreshInboxDiff(),
|
|
289
728
|
fetchMoltbookScoutStatus(),
|
|
290
729
|
fetchA2aShareStatus(),
|
|
730
|
+
fetchRemotePairingStatus(),
|
|
291
731
|
])
|
|
292
732
|
.then(async () => {
|
|
293
733
|
if (!shouldDeferRenderForActiveInteraction()) {
|
|
@@ -298,6 +738,49 @@ async function boot() {
|
|
|
298
738
|
}, 3000);
|
|
299
739
|
}
|
|
300
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
|
+
|
|
301
784
|
async function registerServiceWorker() {
|
|
302
785
|
if (!("serviceWorker" in navigator)) {
|
|
303
786
|
return;
|
|
@@ -317,6 +800,47 @@ async function registerServiceWorker() {
|
|
|
317
800
|
}
|
|
318
801
|
}
|
|
319
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
|
+
|
|
320
844
|
function handleViewportChange() {
|
|
321
845
|
const nextViewportMode = isDesktopLayout();
|
|
322
846
|
if (nextViewportMode === lastViewportMode) {
|
|
@@ -346,6 +870,7 @@ async function refreshAuthenticatedState() {
|
|
|
346
870
|
await fetchMoltbookScoutStatus();
|
|
347
871
|
await fetchA2aRelayStatus();
|
|
348
872
|
await fetchA2aShareStatus();
|
|
873
|
+
await fetchRemotePairingStatus();
|
|
349
874
|
if (state.currentTab === "settings") {
|
|
350
875
|
await fetchHazbaseStatus();
|
|
351
876
|
}
|
|
@@ -368,11 +893,13 @@ async function refreshAuthenticatedStateRemote() {
|
|
|
368
893
|
fetchMoltbookScoutStatus(),
|
|
369
894
|
fetchA2aRelayStatus(),
|
|
370
895
|
fetchA2aShareStatus(),
|
|
896
|
+
fetchRemotePairingStatus(),
|
|
371
897
|
]);
|
|
372
898
|
}
|
|
373
899
|
|
|
374
900
|
async function refreshSession() {
|
|
375
901
|
state.session = await apiGet("/api/session");
|
|
902
|
+
applyServerAppBuildId(state.session?.webAppBuildId);
|
|
376
903
|
syncPairingTokenState(desiredBootstrapPairingToken());
|
|
377
904
|
applyResolvedLocale();
|
|
378
905
|
}
|
|
@@ -384,8 +911,14 @@ async function refreshSession() {
|
|
|
384
911
|
// iOS PWAs where connection reuse is aggressive. Leaves the diff and
|
|
385
912
|
// external-status probes as separate background phases in `boot()`.
|
|
386
913
|
async function refreshBootstrap() {
|
|
387
|
-
|
|
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" });
|
|
388
920
|
state.session = bootstrap?.session || null;
|
|
921
|
+
applyServerAppBuildId(bootstrap?.appBuildId || state.session?.webAppBuildId);
|
|
389
922
|
syncPairingTokenState(desiredBootstrapPairingToken());
|
|
390
923
|
applyResolvedLocale();
|
|
391
924
|
|
|
@@ -406,7 +939,7 @@ async function refreshBootstrap() {
|
|
|
406
939
|
syncCompletedThreadFilter();
|
|
407
940
|
syncInboxSubtab();
|
|
408
941
|
|
|
409
|
-
state.timeline = bootstrap?.timeline || null;
|
|
942
|
+
state.timeline = await hydrateTimelinePayloadImages(bootstrap?.timeline || null);
|
|
410
943
|
syncTimelineThreadFilter();
|
|
411
944
|
syncTimelineKindFilter();
|
|
412
945
|
|
|
@@ -415,6 +948,12 @@ async function refreshBootstrap() {
|
|
|
415
948
|
state.deviceError = "";
|
|
416
949
|
}
|
|
417
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
|
+
|
|
418
957
|
async function syncDetectedLocalePreference() {
|
|
419
958
|
if (!state.session?.authenticated || !state.session?.deviceId || !state.detectedLocale) {
|
|
420
959
|
return;
|
|
@@ -511,9 +1050,10 @@ function detectBrowserLocale() {
|
|
|
511
1050
|
|
|
512
1051
|
async function refreshPushStatus() {
|
|
513
1052
|
const client = await getClientPushState();
|
|
1053
|
+
const { clientSubscription, ...clientStatus } = client;
|
|
514
1054
|
if (!state.session?.authenticated) {
|
|
515
1055
|
state.pushStatus = {
|
|
516
|
-
...
|
|
1056
|
+
...clientStatus,
|
|
517
1057
|
enabled: false,
|
|
518
1058
|
subscribed: false,
|
|
519
1059
|
serverSubscribed: false,
|
|
@@ -524,16 +1064,34 @@ async function refreshPushStatus() {
|
|
|
524
1064
|
}
|
|
525
1065
|
|
|
526
1066
|
try {
|
|
527
|
-
|
|
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
|
+
}
|
|
528
1086
|
state.pushStatus = {
|
|
529
1087
|
...server,
|
|
530
|
-
...
|
|
1088
|
+
...clientStatus,
|
|
531
1089
|
serverSubscribed: Boolean(server.subscribed),
|
|
532
|
-
subscribed: Boolean(server.subscribed ||
|
|
1090
|
+
subscribed: Boolean(server.subscribed || clientStatus.clientSubscribed),
|
|
533
1091
|
};
|
|
534
1092
|
} catch (error) {
|
|
535
1093
|
state.pushStatus = {
|
|
536
|
-
...
|
|
1094
|
+
...clientStatus,
|
|
537
1095
|
enabled: false,
|
|
538
1096
|
subscribed: false,
|
|
539
1097
|
serverSubscribed: false,
|
|
@@ -568,6 +1126,18 @@ async function fetchA2aRelayStatus() {
|
|
|
568
1126
|
}
|
|
569
1127
|
}
|
|
570
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
|
+
|
|
571
1141
|
async function fetchA2aShareStatus() {
|
|
572
1142
|
if (!state.session?.a2aShareEnabled) {
|
|
573
1143
|
state.a2aShareStatus = null;
|
|
@@ -630,6 +1200,7 @@ async function getClientPushState() {
|
|
|
630
1200
|
"PushManager" in window &&
|
|
631
1201
|
"Notification" in window,
|
|
632
1202
|
clientSubscribed: Boolean(subscription),
|
|
1203
|
+
clientSubscription: subscription ? subscription.toJSON() : null,
|
|
633
1204
|
};
|
|
634
1205
|
}
|
|
635
1206
|
|
|
@@ -670,7 +1241,7 @@ async function refreshInboxDiff() {
|
|
|
670
1241
|
}
|
|
671
1242
|
|
|
672
1243
|
async function refreshTimeline() {
|
|
673
|
-
state.timeline = await apiGet("/api/timeline");
|
|
1244
|
+
state.timeline = await hydrateTimelinePayloadImages(await apiGet("/api/timeline"));
|
|
674
1245
|
syncTimelineThreadFilter();
|
|
675
1246
|
syncTimelineKindFilter();
|
|
676
1247
|
}
|
|
@@ -1158,9 +1729,16 @@ function renderPair() {
|
|
|
1158
1729
|
event.preventDefault();
|
|
1159
1730
|
const form = new FormData(event.currentTarget);
|
|
1160
1731
|
try {
|
|
1161
|
-
await pair({ code: String(form.get("code") || "") });
|
|
1732
|
+
const pairResult = await pair({ code: String(form.get("code") || "") });
|
|
1162
1733
|
state.pairError = "";
|
|
1163
1734
|
state.pairNotice = "";
|
|
1735
|
+
// Best-effort: register this phone's X25519 pubkey with the bridge
|
|
1736
|
+
// so it can reach us via the relay later. Doesn't block the shell
|
|
1737
|
+
// render; failures (older bridge, IndexedDB blocked, …) are swallowed
|
|
1738
|
+
// inside the helper.
|
|
1739
|
+
if (pairResult?.temporaryPairing !== true) {
|
|
1740
|
+
await enrollRemotePairing();
|
|
1741
|
+
}
|
|
1164
1742
|
await refreshSession();
|
|
1165
1743
|
await refreshAuthenticatedState();
|
|
1166
1744
|
await renderShell();
|
|
@@ -1171,6 +1749,7 @@ function renderPair() {
|
|
|
1171
1749
|
});
|
|
1172
1750
|
|
|
1173
1751
|
bindSharedUi(renderPair);
|
|
1752
|
+
requestAnimationFrame(dismissBootSplash);
|
|
1174
1753
|
}
|
|
1175
1754
|
|
|
1176
1755
|
async function pair(payload) {
|
|
@@ -1181,6 +1760,80 @@ async function pair(payload) {
|
|
|
1181
1760
|
return result;
|
|
1182
1761
|
}
|
|
1183
1762
|
|
|
1763
|
+
/**
|
|
1764
|
+
* Best-effort post-pair enrollment: hand the bridge our X25519 static
|
|
1765
|
+
* pubkey + a friendly label, persist the bridge's response in localStorage,
|
|
1766
|
+
* and return the saved record (or null on any failure).
|
|
1767
|
+
*
|
|
1768
|
+
* Always called after a successful LAN pair (regardless of whether the
|
|
1769
|
+
* remote relay is currently ON). Reasoning:
|
|
1770
|
+
*
|
|
1771
|
+
* - Enrollment is idempotent on phonePub server-side; re-pairing the same
|
|
1772
|
+
* phone keeps its existing pairingId and bridge identity.
|
|
1773
|
+
* - Doing it eagerly means flipping the relay toggle later "just works"
|
|
1774
|
+
* without forcing the user to repair-on-LAN to register their key.
|
|
1775
|
+
* - LAN-only basic auth keeps working even if this fails (e.g. older
|
|
1776
|
+
* bridge predating the lan-enroll endpoint, IndexedDB blocked, etc.) —
|
|
1777
|
+
* we explicitly swallow errors and let the caller continue rendering
|
|
1778
|
+
* the authenticated shell.
|
|
1779
|
+
*
|
|
1780
|
+
* Skipped when the surrounding `pair()` returned `temporaryPairing: true`
|
|
1781
|
+
* — temporary sessions are an opt-in shape that doesn't outlive the tab,
|
|
1782
|
+
* so persisting a pairing record would be misleading.
|
|
1783
|
+
*
|
|
1784
|
+
* @param {string} [label] user-visible device label; defaults to a
|
|
1785
|
+
* "LAN paired YYYY-MM-DD" stamp.
|
|
1786
|
+
* @returns {Promise<import("./remote-pairing/pairing-state.js").RemotePairingState | null>}
|
|
1787
|
+
*/
|
|
1788
|
+
async function enrollRemotePairing(label) {
|
|
1789
|
+
try {
|
|
1790
|
+
const keypair = await ensureIdentityKeypair();
|
|
1791
|
+
const phonePubHex = bytesToHex(keypair.pub);
|
|
1792
|
+
const effectiveLabel = (label && String(label).trim()) || buildDefaultEnrollLabel();
|
|
1793
|
+
const response = await apiPost("/api/remote-pairing/lan-enroll", {
|
|
1794
|
+
phonePubHex,
|
|
1795
|
+
label: effectiveLabel,
|
|
1796
|
+
});
|
|
1797
|
+
if (!response || response.ok !== true) {
|
|
1798
|
+
return null;
|
|
1799
|
+
}
|
|
1800
|
+
const saved = saveRemotePairingState({
|
|
1801
|
+
pairingId: response.pairingId,
|
|
1802
|
+
relayToken: response.relayToken,
|
|
1803
|
+
phonePub: response.phonePub,
|
|
1804
|
+
phoneFingerprint: response.phoneFingerprint,
|
|
1805
|
+
bridgePubHex: response.bridgePubHex,
|
|
1806
|
+
bridgeFingerprint: response.bridgeFingerprint,
|
|
1807
|
+
relayUrl: response.relayUrl,
|
|
1808
|
+
label: response.label || "",
|
|
1809
|
+
addedAtMs: Number.isFinite(response.addedAtMs) ? response.addedAtMs : Date.now(),
|
|
1810
|
+
relayTokenUpdatedAtMs: Number.isFinite(response.relayTokenUpdatedAtMs)
|
|
1811
|
+
? response.relayTokenUpdatedAtMs
|
|
1812
|
+
: Date.now(),
|
|
1813
|
+
});
|
|
1814
|
+
if (!saved) {
|
|
1815
|
+
// Storage is full / disabled; the bridge still has the pairing,
|
|
1816
|
+
// so future toggle-on works — just no localStorage cache for the
|
|
1817
|
+
// PWA's own routing decisions.
|
|
1818
|
+
return null;
|
|
1819
|
+
}
|
|
1820
|
+
return loadRemotePairingState();
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
console.warn("[remote-pairing] lan-enroll skipped:", error?.message || error);
|
|
1823
|
+
return null;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function buildDefaultEnrollLabel() {
|
|
1828
|
+
// YYYY-MM-DD so re-enrolls produce stable, sortable labels in the
|
|
1829
|
+
// settings list. (The user can rename in the settings page later.)
|
|
1830
|
+
const now = new Date();
|
|
1831
|
+
const y = now.getFullYear();
|
|
1832
|
+
const m = String(now.getMonth() + 1).padStart(2, "0");
|
|
1833
|
+
const d = String(now.getDate()).padStart(2, "0");
|
|
1834
|
+
return `LAN paired ${y}-${m}-${d}`;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1184
1837
|
async function logout({ revokeCurrentDeviceTrust = false } = {}) {
|
|
1185
1838
|
await apiPost("/api/session/logout", { revokeCurrentDeviceTrust });
|
|
1186
1839
|
resetAuthenticatedState();
|
|
@@ -1208,6 +1861,7 @@ function resetAuthenticatedState() {
|
|
|
1208
1861
|
state.choiceLocalDrafts = {};
|
|
1209
1862
|
clearAllCompletionReplyDrafts();
|
|
1210
1863
|
state.completionReplyDrafts = {};
|
|
1864
|
+
state.completionReplySheetToken = "";
|
|
1211
1865
|
state.settingsSubpage = "";
|
|
1212
1866
|
state.settingsScrollState = null;
|
|
1213
1867
|
state.listScrollState = null;
|
|
@@ -1219,6 +1873,7 @@ function resetAuthenticatedState() {
|
|
|
1219
1873
|
state.deviceError = "";
|
|
1220
1874
|
state.logoutConfirmOpen = false;
|
|
1221
1875
|
state.pairError = "";
|
|
1876
|
+
state.remotePairingDetailsOpen = false;
|
|
1222
1877
|
}
|
|
1223
1878
|
|
|
1224
1879
|
async function revokeTrustedDevice(deviceId) {
|
|
@@ -1240,6 +1895,7 @@ async function revokeTrustedDevice(deviceId) {
|
|
|
1240
1895
|
}
|
|
1241
1896
|
|
|
1242
1897
|
async function renderShell() {
|
|
1898
|
+
syncVisualViewportMetrics();
|
|
1243
1899
|
const desktop = isDesktopLayout();
|
|
1244
1900
|
const shouldShowDetail = state.currentTab !== "settings" && state.currentItem && (desktop || state.detailOpen);
|
|
1245
1901
|
let detail = null;
|
|
@@ -1272,13 +1928,18 @@ async function renderShell() {
|
|
|
1272
1928
|
${renderLogoutConfirmModal()}
|
|
1273
1929
|
${renderHazbaseLogoutConfirmModal()}
|
|
1274
1930
|
</div>
|
|
1931
|
+
${!desktop && detail ? renderCompletionReplySheet(detail) : ""}
|
|
1275
1932
|
`;
|
|
1276
1933
|
|
|
1277
1934
|
bindShellInteractions();
|
|
1935
|
+
if (state.completionReplySheetToken) {
|
|
1936
|
+
resetHorizontalViewportScroll();
|
|
1937
|
+
}
|
|
1278
1938
|
applyPendingDetailScrollReset();
|
|
1279
1939
|
applyPendingListScrollRestore();
|
|
1280
1940
|
applyPendingSettingsSubpageScrollReset();
|
|
1281
1941
|
applyPendingSettingsScrollRestore();
|
|
1942
|
+
requestAnimationFrame(dismissBootSplash);
|
|
1282
1943
|
}
|
|
1283
1944
|
|
|
1284
1945
|
function applyPendingDetailScrollReset() {
|
|
@@ -1341,6 +2002,9 @@ function clearThreadFilterInteraction() {
|
|
|
1341
2002
|
|
|
1342
2003
|
function shouldDeferRenderForActiveInteraction() {
|
|
1343
2004
|
const activeElement = document.activeElement;
|
|
2005
|
+
if (state.completionReplySheetToken) {
|
|
2006
|
+
return true;
|
|
2007
|
+
}
|
|
1344
2008
|
if (
|
|
1345
2009
|
activeElement instanceof HTMLTextAreaElement &&
|
|
1346
2010
|
activeElement.matches("[data-completion-reply-textarea]") &&
|
|
@@ -1708,7 +2372,6 @@ function renderMobileTopBar(detail) {
|
|
|
1708
2372
|
return `
|
|
1709
2373
|
<header class="mobile-topbar">
|
|
1710
2374
|
<div class="mobile-topbar__heading">
|
|
1711
|
-
<span class="eyebrow-pill eyebrow-pill--quiet">${escapeHtml(L("common.appName"))}</span>
|
|
1712
2375
|
<h1 class="mobile-topbar__title">${escapeHtml(meta.title)}</h1>
|
|
1713
2376
|
</div>
|
|
1714
2377
|
</header>
|
|
@@ -1763,7 +2426,9 @@ async function fetchCurrentDetailForItem(itemRef = state.currentItem) {
|
|
|
1763
2426
|
return state.detailOverride.detail;
|
|
1764
2427
|
}
|
|
1765
2428
|
try {
|
|
1766
|
-
const detail = await
|
|
2429
|
+
const detail = await hydrateDetailImages(
|
|
2430
|
+
await apiGet(`/api/items/${encodeURIComponent(itemRef.kind)}/${encodeURIComponent(itemRef.token)}`)
|
|
2431
|
+
);
|
|
1767
2432
|
if (hasLaunchItemIntent(itemRef)) {
|
|
1768
2433
|
state.launchItemIntent.status = "loaded";
|
|
1769
2434
|
}
|
|
@@ -1777,7 +2442,9 @@ async function fetchCurrentDetailForItem(itemRef = state.currentItem) {
|
|
|
1777
2442
|
}
|
|
1778
2443
|
await refreshInbox();
|
|
1779
2444
|
try {
|
|
1780
|
-
const detail = await
|
|
2445
|
+
const detail = await hydrateDetailImages(
|
|
2446
|
+
await apiGet(`/api/items/${encodeURIComponent(itemRef.kind)}/${encodeURIComponent(itemRef.token)}`)
|
|
2447
|
+
);
|
|
1781
2448
|
if (hasLaunchItemIntent(itemRef)) {
|
|
1782
2449
|
state.launchItemIntent.status = "loaded";
|
|
1783
2450
|
}
|
|
@@ -1899,7 +2566,11 @@ function buildActionOutcomeDetail({ kind, title, message }) {
|
|
|
1899
2566
|
|
|
1900
2567
|
function approvalOutcomeMessage(actionUrl, provider) {
|
|
1901
2568
|
const vars = { provider: providerDisplayName(provider) };
|
|
1902
|
-
|
|
2569
|
+
const normalizedUrl = String(actionUrl || "");
|
|
2570
|
+
if (/\/api\/payments\/x402\/hazbase-wallet\/[^/]+\/pay$/u.test(normalizedUrl)) {
|
|
2571
|
+
return L("server.message.paymentSubmitted");
|
|
2572
|
+
}
|
|
2573
|
+
return /\/accept$/u.test(normalizedUrl)
|
|
1903
2574
|
? L("server.message.approvalAccepted", vars)
|
|
1904
2575
|
: L("server.message.approvalRejected", vars);
|
|
1905
2576
|
}
|
|
@@ -2617,6 +3288,8 @@ function timelineEntryStatusLabel(item, isMessageLike) {
|
|
|
2617
3288
|
return L("timeline.status.approved");
|
|
2618
3289
|
case "rejected":
|
|
2619
3290
|
return L("timeline.status.rejected");
|
|
3291
|
+
case "failed":
|
|
3292
|
+
return L("timeline.status.failed");
|
|
2620
3293
|
case "implemented":
|
|
2621
3294
|
return L("timeline.status.implemented");
|
|
2622
3295
|
case "dismissed":
|
|
@@ -3204,7 +3877,7 @@ function buildSettingsContext() {
|
|
|
3204
3877
|
standalone,
|
|
3205
3878
|
supportsPushValue,
|
|
3206
3879
|
permission,
|
|
3207
|
-
subscribed: push.
|
|
3880
|
+
subscribed: push.subscribed === true,
|
|
3208
3881
|
});
|
|
3209
3882
|
|
|
3210
3883
|
return {
|
|
@@ -3230,6 +3903,7 @@ function buildSettingsContext() {
|
|
|
3230
3903
|
a2aRelay: state.a2aRelayStatus,
|
|
3231
3904
|
a2aShare: state.a2aShareStatus,
|
|
3232
3905
|
hazbase: state.hazbaseStatus,
|
|
3906
|
+
remotePairing: state.remotePairingStatus,
|
|
3233
3907
|
};
|
|
3234
3908
|
}
|
|
3235
3909
|
|
|
@@ -3332,6 +4006,195 @@ function collectSettingsDiagnostics({ permission, secureContext, standalone, sup
|
|
|
3332
4006
|
return Array.from(new Set(issues.filter(Boolean)));
|
|
3333
4007
|
}
|
|
3334
4008
|
|
|
4009
|
+
function hasAutoPilotWriteLaneEnabled(session = state.session) {
|
|
4010
|
+
return Boolean(
|
|
4011
|
+
session?.autoPilotTrustedWrites === true ||
|
|
4012
|
+
session?.autoPilotWriteLaneContent === true ||
|
|
4013
|
+
session?.autoPilotWriteLaneUiTests === true ||
|
|
4014
|
+
session?.autoPilotWriteLaneSource === true,
|
|
4015
|
+
);
|
|
4016
|
+
}
|
|
4017
|
+
|
|
4018
|
+
function hasAutoPilotEnabled(session = state.session) {
|
|
4019
|
+
return Boolean(session?.autoPilotTrustedReads === true || hasAutoPilotWriteLaneEnabled(session));
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
function settingsEnabledValue(enabled) {
|
|
4023
|
+
return enabled ? L("common.enabled") : L("common.disabled");
|
|
4024
|
+
}
|
|
4025
|
+
|
|
4026
|
+
function settingsNeedsActionValue() {
|
|
4027
|
+
return L("settings.status.actionNeeded");
|
|
4028
|
+
}
|
|
4029
|
+
|
|
4030
|
+
function settingsDisconnectedValue() {
|
|
4031
|
+
return L("settings.status.disconnected");
|
|
4032
|
+
}
|
|
4033
|
+
|
|
4034
|
+
function settingsSupportedValue(supported) {
|
|
4035
|
+
return supported ? L("settings.status.supported") : L("settings.status.unsupported");
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
function settingsInstalledValue(installed) {
|
|
4039
|
+
return installed ? L("settings.status.installed") : L("settings.status.notInstalled");
|
|
4040
|
+
}
|
|
4041
|
+
|
|
4042
|
+
function settingsNotificationRootValue(context) {
|
|
4043
|
+
if (context.push?.serverSubscribed === true) {
|
|
4044
|
+
return L("common.enabled");
|
|
4045
|
+
}
|
|
4046
|
+
if (context.push?.clientSubscribed === true) {
|
|
4047
|
+
return settingsNeedsActionValue();
|
|
4048
|
+
}
|
|
4049
|
+
if (context.permission === "denied") {
|
|
4050
|
+
return settingsNeedsActionValue();
|
|
4051
|
+
}
|
|
4052
|
+
return L("common.disabled");
|
|
4053
|
+
}
|
|
4054
|
+
|
|
4055
|
+
function notificationPermissionValue(permission) {
|
|
4056
|
+
const key = `settings.permission.${permission || "default"}`;
|
|
4057
|
+
const translated = L(key);
|
|
4058
|
+
return translated === key ? String(permission || "") : translated;
|
|
4059
|
+
}
|
|
4060
|
+
|
|
4061
|
+
function notificationReceiveValue(push) {
|
|
4062
|
+
if (push?.serverSubscribed === true) {
|
|
4063
|
+
return L("common.enabled");
|
|
4064
|
+
}
|
|
4065
|
+
if (push?.clientSubscribed === true) {
|
|
4066
|
+
return L("settings.status.actionNeeded");
|
|
4067
|
+
}
|
|
4068
|
+
return L("common.disabled");
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
function notificationOverallValue(context) {
|
|
4072
|
+
if (context.push?.serverSubscribed === true) {
|
|
4073
|
+
return L("common.enabled");
|
|
4074
|
+
}
|
|
4075
|
+
if (context.permission === "denied") {
|
|
4076
|
+
return L("settings.status.blocked");
|
|
4077
|
+
}
|
|
4078
|
+
if (!context.serverEnabled) {
|
|
4079
|
+
return L("settings.status.notAvailable");
|
|
4080
|
+
}
|
|
4081
|
+
if (!context.supportsPushValue) {
|
|
4082
|
+
return L("settings.status.unsupported");
|
|
4083
|
+
}
|
|
4084
|
+
if (!context.secureContext || !context.standalone || context.permission !== "granted") {
|
|
4085
|
+
return L("settings.status.actionNeeded");
|
|
4086
|
+
}
|
|
4087
|
+
return L("common.disabled");
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4090
|
+
function settingsMoltbookRootValue(context) {
|
|
4091
|
+
return context.moltbookScout?.enabled === true
|
|
4092
|
+
? L("common.enabled")
|
|
4093
|
+
: settingsNeedsActionValue();
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
function settingsA2aRelayRootValue(context) {
|
|
4097
|
+
const relay = context.a2aRelay;
|
|
4098
|
+
if (relay?.enabled === true && relay?.connected === true) {
|
|
4099
|
+
return L("common.enabled");
|
|
4100
|
+
}
|
|
4101
|
+
return settingsDisconnectedValue();
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
function settingsA2aShareRootValue(context) {
|
|
4105
|
+
const share = context.a2aShare;
|
|
4106
|
+
if (share?.enabled === true && !share?.error) {
|
|
4107
|
+
return L("common.enabled");
|
|
4108
|
+
}
|
|
4109
|
+
return settingsNeedsActionValue();
|
|
4110
|
+
}
|
|
4111
|
+
|
|
4112
|
+
function settingsWalletRootValue(context) {
|
|
4113
|
+
if (context.hazbase?.enabled !== true) {
|
|
4114
|
+
return L("common.disabled");
|
|
4115
|
+
}
|
|
4116
|
+
return deriveHazbaseWalletFlow(context.hazbase).coreReady
|
|
4117
|
+
? L("common.enabled")
|
|
4118
|
+
: settingsNeedsActionValue();
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4121
|
+
function isRemotePairingSessionConnected(session) {
|
|
4122
|
+
const value = normalizeClientText(session?.state).toLowerCase();
|
|
4123
|
+
return value === "connected" || value === "open" || value === "running";
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4126
|
+
function isRemotePairingSessionReachable(session) {
|
|
4127
|
+
const value = normalizeClientText(session?.state).toLowerCase();
|
|
4128
|
+
return value === "connected" || value === "open" || value === "running" || value === "opening" || value === "handshaking" || value === "resuming";
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
function remotePairingStatusModel(status, opts = {}) {
|
|
4132
|
+
const enabled = status?.enabled === true;
|
|
4133
|
+
const sessions = Array.isArray(status?.sessions) ? status.sessions : [];
|
|
4134
|
+
const pairings = Array.isArray(status?.pairings) ? status.pairings : [];
|
|
4135
|
+
const usingRelay = opts.usingRelay === true;
|
|
4136
|
+
if (!enabled) {
|
|
4137
|
+
return { key: "disabled", tone: "muted", label: L("settings.remotePairing.status.disabled") };
|
|
4138
|
+
}
|
|
4139
|
+
if (usingRelay && sessions.some(isRemotePairingSessionConnected)) {
|
|
4140
|
+
return { key: "connected", tone: "success", label: L("settings.remotePairing.status.connected") };
|
|
4141
|
+
}
|
|
4142
|
+
if (sessions.some(isRemotePairingSessionReachable)) {
|
|
4143
|
+
return { key: "available", tone: "success", label: L("settings.remotePairing.status.available") };
|
|
4144
|
+
}
|
|
4145
|
+
if (pairings.length > 0) {
|
|
4146
|
+
return { key: "waiting", tone: "warning", label: L("settings.remotePairing.status.waiting") };
|
|
4147
|
+
}
|
|
4148
|
+
return { key: "waiting", tone: "muted", label: L("settings.remotePairing.status.waiting") };
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
function remotePairingAuditTypeLabel(type) {
|
|
4152
|
+
const key = `settings.remotePairing.audit.type.${type || "unknown"}`;
|
|
4153
|
+
const translated = L(key);
|
|
4154
|
+
return translated === key ? L("settings.remotePairing.audit.type.unknown") : translated;
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
function remotePairingAuditOutcomeTone(outcome) {
|
|
4158
|
+
if (outcome === "success") return "success";
|
|
4159
|
+
if (outcome === "failure") return "danger";
|
|
4160
|
+
return "muted";
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
function remotePairingAuditMeta(event) {
|
|
4164
|
+
return [
|
|
4165
|
+
event?.label,
|
|
4166
|
+
event?.phoneFingerprint,
|
|
4167
|
+
event?.relayHost,
|
|
4168
|
+
event?.reason,
|
|
4169
|
+
].filter(Boolean).join(" / ");
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
function renderRemotePairingAudit(events) {
|
|
4173
|
+
const list = Array.isArray(events) ? events.slice(0, 10) : [];
|
|
4174
|
+
if (!list.length) {
|
|
4175
|
+
return `<div class="settings-copy-block"><p class="muted">${escapeHtml(L("settings.remotePairing.audit.empty"))}</p></div>`;
|
|
4176
|
+
}
|
|
4177
|
+
return `
|
|
4178
|
+
<div class="settings-remote-audit-list">
|
|
4179
|
+
${list.map((event) => {
|
|
4180
|
+
const tone = remotePairingAuditOutcomeTone(event?.outcome);
|
|
4181
|
+
const title = remotePairingAuditTypeLabel(event?.type);
|
|
4182
|
+
const meta = remotePairingAuditMeta(event);
|
|
4183
|
+
return `
|
|
4184
|
+
<article class="settings-remote-audit-item">
|
|
4185
|
+
<span class="settings-remote-audit-dot settings-remote-audit-dot--${escapeHtml(tone)}" aria-hidden="true"></span>
|
|
4186
|
+
<div class="settings-remote-audit-body">
|
|
4187
|
+
<p class="settings-remote-audit-title">${escapeHtml(title)}</p>
|
|
4188
|
+
${meta ? `<p class="settings-remote-audit-meta">${escapeHtml(meta)}</p>` : ""}
|
|
4189
|
+
</div>
|
|
4190
|
+
<time class="settings-remote-audit-time">${escapeHtml(formatSettingsTimestamp(event?.atMs))}</time>
|
|
4191
|
+
</article>
|
|
4192
|
+
`;
|
|
4193
|
+
}).join("")}
|
|
4194
|
+
</div>
|
|
4195
|
+
`;
|
|
4196
|
+
}
|
|
4197
|
+
|
|
3335
4198
|
function settingsPageMeta(page) {
|
|
3336
4199
|
switch (page) {
|
|
3337
4200
|
case "notifications":
|
|
@@ -3395,14 +4258,14 @@ function settingsPageMeta(page) {
|
|
|
3395
4258
|
id: "a2aRelay",
|
|
3396
4259
|
title: L("settings.a2aRelay.title"),
|
|
3397
4260
|
description: L("settings.a2aRelay.copy"),
|
|
3398
|
-
icon: "
|
|
4261
|
+
icon: "agent-network",
|
|
3399
4262
|
};
|
|
3400
4263
|
case "a2aShare":
|
|
3401
4264
|
return {
|
|
3402
4265
|
id: "a2aShare",
|
|
3403
4266
|
title: L("settings.a2aShare.title"),
|
|
3404
4267
|
description: L("settings.a2aShare.copy"),
|
|
3405
|
-
icon: "
|
|
4268
|
+
icon: "file-event",
|
|
3406
4269
|
};
|
|
3407
4270
|
case "wallet":
|
|
3408
4271
|
return {
|
|
@@ -3411,6 +4274,13 @@ function settingsPageMeta(page) {
|
|
|
3411
4274
|
description: L("settings.wallet.copy"),
|
|
3412
4275
|
icon: "coin",
|
|
3413
4276
|
};
|
|
4277
|
+
case "remotePairing":
|
|
4278
|
+
return {
|
|
4279
|
+
id: "remotePairing",
|
|
4280
|
+
title: L("settings.remotePairing.title"),
|
|
4281
|
+
description: L("settings.remotePairing.copy"),
|
|
4282
|
+
icon: "remote-connection",
|
|
4283
|
+
};
|
|
3414
4284
|
case "a2aExecutor":
|
|
3415
4285
|
// Executor settings integrated into a2aRelay page — redirect.
|
|
3416
4286
|
return settingsPageMeta("a2aRelay");
|
|
@@ -3426,7 +4296,7 @@ function renderSettingsRoot(context, { mobile }) {
|
|
|
3426
4296
|
page: "notifications",
|
|
3427
4297
|
icon: "notifications",
|
|
3428
4298
|
title: L("settings.notifications.title"),
|
|
3429
|
-
value:
|
|
4299
|
+
value: settingsNotificationRootValue(context),
|
|
3430
4300
|
}),
|
|
3431
4301
|
renderSettingsNavRow({
|
|
3432
4302
|
page: "language",
|
|
@@ -3439,23 +4309,20 @@ function renderSettingsRoot(context, { mobile }) {
|
|
|
3439
4309
|
page: "install",
|
|
3440
4310
|
icon: "homescreen",
|
|
3441
4311
|
title: L("settings.install.title"),
|
|
3442
|
-
value:
|
|
4312
|
+
value: settingsEnabledValue(context.standalone),
|
|
3443
4313
|
})
|
|
3444
4314
|
: "",
|
|
3445
4315
|
renderSettingsNavRow({
|
|
3446
4316
|
page: "awayMode",
|
|
3447
4317
|
icon: "settings",
|
|
3448
4318
|
title: L("settings.awayMode.title"),
|
|
3449
|
-
value: state.session?.claudeAwayMode === true
|
|
4319
|
+
value: settingsEnabledValue(state.session?.claudeAwayMode === true),
|
|
3450
4320
|
}),
|
|
3451
4321
|
renderSettingsNavRow({
|
|
3452
4322
|
page: "autoPilot",
|
|
3453
4323
|
icon: "approval",
|
|
3454
4324
|
title: L("settings.autoPilot.title"),
|
|
3455
|
-
value:
|
|
3456
|
-
state.session?.autoPilotTrustedReads === true || state.session?.autoPilotTrustedWrites === true
|
|
3457
|
-
? L("common.enabled")
|
|
3458
|
-
: L("common.disabled"),
|
|
4325
|
+
value: settingsEnabledValue(hasAutoPilotEnabled()),
|
|
3459
4326
|
}),
|
|
3460
4327
|
].filter(Boolean);
|
|
3461
4328
|
const deviceRows = [
|
|
@@ -3467,6 +4334,12 @@ function renderSettingsRoot(context, { mobile }) {
|
|
|
3467
4334
|
? L("settings.device.count", { count: context.devices.length })
|
|
3468
4335
|
: L("settings.pairing.connected"),
|
|
3469
4336
|
}),
|
|
4337
|
+
state.session?.remotePairingAvailable ? renderSettingsNavRow({
|
|
4338
|
+
page: "remotePairing",
|
|
4339
|
+
icon: "remote-connection",
|
|
4340
|
+
title: L("settings.remotePairing.title"),
|
|
4341
|
+
value: settingsEnabledValue(context.remotePairing?.enabled === true),
|
|
4342
|
+
}) : "",
|
|
3470
4343
|
];
|
|
3471
4344
|
const advancedRows = [
|
|
3472
4345
|
renderSettingsNavRow({
|
|
@@ -3497,31 +4370,26 @@ function renderSettingsRoot(context, { mobile }) {
|
|
|
3497
4370
|
page: "moltbook",
|
|
3498
4371
|
icon: "item",
|
|
3499
4372
|
title: L("settings.moltbook.title"),
|
|
3500
|
-
|
|
3501
|
-
value: context.moltbookScout?.enabled ? L("settings.status.enabled") : L("settings.status.disabled"),
|
|
4373
|
+
value: settingsMoltbookRootValue(context),
|
|
3502
4374
|
}) : "",
|
|
3503
4375
|
state.session?.a2aRelayEnabled ? renderSettingsNavRow({
|
|
3504
4376
|
page: "a2aRelay",
|
|
3505
|
-
icon: "
|
|
4377
|
+
icon: "agent-network",
|
|
3506
4378
|
title: L("settings.a2aRelay.title"),
|
|
3507
|
-
|
|
3508
|
-
value: context.a2aRelay?.connected ? L("settings.status.connected") : L("settings.a2aRelay.status.disconnected"),
|
|
4379
|
+
value: settingsA2aRelayRootValue(context),
|
|
3509
4380
|
}) : "",
|
|
3510
4381
|
state.session?.a2aShareEnabled ? renderSettingsNavRow({
|
|
3511
4382
|
page: "a2aShare",
|
|
3512
|
-
icon: "
|
|
4383
|
+
icon: "file-event",
|
|
3513
4384
|
title: L("settings.a2aShare.title"),
|
|
3514
|
-
|
|
3515
|
-
value: context.a2aShare?.enabled ? L("settings.status.enabled") : L("settings.status.disabled"),
|
|
4385
|
+
value: settingsA2aShareRootValue(context),
|
|
3516
4386
|
}) : "",
|
|
3517
4387
|
context.hazbase?.enabled ? renderSettingsNavRow({
|
|
3518
4388
|
page: "wallet",
|
|
3519
4389
|
icon: "coin",
|
|
3520
4390
|
title: L("settings.wallet.title"),
|
|
3521
|
-
|
|
3522
|
-
value: context
|
|
3523
|
-
? L("settings.hazbase.status.sessionExpired")
|
|
3524
|
-
: context.hazbase?.signedIn ? L("settings.hazbase.status.signedIn") : L("settings.hazbase.status.signedOut"),
|
|
4391
|
+
badge: "beta",
|
|
4392
|
+
value: settingsWalletRootValue(context),
|
|
3525
4393
|
}) : "",
|
|
3526
4394
|
].filter(Boolean)) : ""}
|
|
3527
4395
|
${renderSettingsGroup(L("settings.pairing.title"), deviceRows)}
|
|
@@ -3583,6 +4451,9 @@ function renderSettingsSubpage(context, { mobile }) {
|
|
|
3583
4451
|
case "wallet":
|
|
3584
4452
|
content = renderSettingsWalletPage(context);
|
|
3585
4453
|
break;
|
|
4454
|
+
case "remotePairing":
|
|
4455
|
+
content = renderSettingsRemotePairingPage(context);
|
|
4456
|
+
break;
|
|
3586
4457
|
default:
|
|
3587
4458
|
content = "";
|
|
3588
4459
|
}
|
|
@@ -3597,11 +4468,14 @@ function renderSettingsSubpage(context, { mobile }) {
|
|
|
3597
4468
|
}
|
|
3598
4469
|
|
|
3599
4470
|
function renderSettingsNotificationsPage(context) {
|
|
3600
|
-
const { push, permission,
|
|
4471
|
+
const { push, permission, standalone } = context;
|
|
4472
|
+
const notificationEnabled = push.serverSubscribed === true;
|
|
3601
4473
|
const statusRows = [
|
|
3602
|
-
renderSettingsInfoRow(L("settings.row.status"),
|
|
3603
|
-
renderSettingsInfoRow(L("settings.row.notificationPermission"),
|
|
3604
|
-
|
|
4474
|
+
renderSettingsInfoRow(L("settings.row.status"), notificationOverallValue(context)),
|
|
4475
|
+
renderSettingsInfoRow(L("settings.row.notificationPermission"), notificationPermissionValue(permission), {
|
|
4476
|
+
valueTone: permission === "granted" ? "enabled" : permission === "denied" ? "attention" : "disabled",
|
|
4477
|
+
}),
|
|
4478
|
+
renderSettingsInfoRow(L("settings.row.currentDeviceSubscribed"), notificationReceiveValue(push)),
|
|
3605
4479
|
push.lastSuccessfulDeliveryAtMs
|
|
3606
4480
|
? renderSettingsInfoRow(
|
|
3607
4481
|
L("settings.row.lastSuccessfulDelivery"),
|
|
@@ -3612,12 +4486,6 @@ function renderSettingsNotificationsPage(context) {
|
|
|
3612
4486
|
return `
|
|
3613
4487
|
<div class="settings-page">
|
|
3614
4488
|
${renderSettingsGroup("", statusRows)}
|
|
3615
|
-
${renderSettingsGroup(L("settings.group.advanced"), [
|
|
3616
|
-
renderSettingsInfoRow(L("settings.row.serverWebPush"), serverEnabled ? L("common.enabled") : L("common.disabled")),
|
|
3617
|
-
renderSettingsInfoRow(L("settings.row.secureContext"), secureContext ? L("common.supported") : L("common.notSupported")),
|
|
3618
|
-
renderSettingsInfoRow(L("settings.row.homeScreenApp"), standalone ? L("common.supported") : L("common.notSupported")),
|
|
3619
|
-
renderSettingsInfoRow(L("settings.row.browserSupport"), supportsPushValue ? L("common.supported") : L("common.notSupported")),
|
|
3620
|
-
])}
|
|
3621
4489
|
${state.pushNotice ? `<p class="inline-alert inline-alert--success">${escapeHtml(state.pushNotice)}</p>` : ""}
|
|
3622
4490
|
${state.pushError ? `<p class="inline-alert inline-alert--danger">${escapeHtml(state.pushError)}</p>` : ""}
|
|
3623
4491
|
${renderSettingsActionPanel(renderSettingsNotificationActions({
|
|
@@ -3710,17 +4578,13 @@ function renderSettingsAdvancedPage(context) {
|
|
|
3710
4578
|
${context.diagnostics.map((message) => `<p class="inline-alert">${escapeHtml(message)}</p>`).join("")}
|
|
3711
4579
|
${renderSettingsGroup("", [
|
|
3712
4580
|
renderSettingsInfoRow(L("settings.row.serverWebPush"), context.serverEnabled ? L("common.enabled") : L("common.disabled")),
|
|
3713
|
-
renderSettingsInfoRow(L("settings.row.secureContext"), context.secureContext ? L("common.
|
|
3714
|
-
renderSettingsInfoRow(L("settings.row.homeScreenApp"), context.standalone
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
renderSettingsInfoRow(L("settings.row.
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
L("settings.row.lastSuccessfulDelivery"),
|
|
3721
|
-
new Date(context.push.lastSuccessfulDeliveryAtMs).toLocaleString(state.locale)
|
|
3722
|
-
)
|
|
3723
|
-
: "",
|
|
4581
|
+
renderSettingsInfoRow(L("settings.row.secureContext"), context.secureContext ? L("common.enabled") : L("common.disabled")),
|
|
4582
|
+
renderSettingsInfoRow(L("settings.row.homeScreenApp"), settingsInstalledValue(context.standalone), {
|
|
4583
|
+
valueTone: context.standalone ? "enabled" : "disabled",
|
|
4584
|
+
}),
|
|
4585
|
+
renderSettingsInfoRow(L("settings.row.browserSupport"), settingsSupportedValue(context.supportsPushValue), {
|
|
4586
|
+
valueTone: context.supportsPushValue ? "enabled" : "disabled",
|
|
4587
|
+
}),
|
|
3724
4588
|
renderSettingsInfoRow(L("settings.row.version"), state.appVersion || L("common.unavailable")),
|
|
3725
4589
|
].filter(Boolean), { listClassName: "settings-list settings-list--compact" })}
|
|
3726
4590
|
${versionNotice}
|
|
@@ -3780,15 +4644,48 @@ function renderSettingsGroup(title, rows, options = {}) {
|
|
|
3780
4644
|
`;
|
|
3781
4645
|
}
|
|
3782
4646
|
|
|
3783
|
-
function
|
|
4647
|
+
function settingsNavValueTone(value, explicitTone = "") {
|
|
4648
|
+
if (explicitTone) {
|
|
4649
|
+
return explicitTone;
|
|
4650
|
+
}
|
|
4651
|
+
const text = String(value || "").trim();
|
|
4652
|
+
if (!text) {
|
|
4653
|
+
return "";
|
|
4654
|
+
}
|
|
4655
|
+
if (text === L("common.enabled") || text === L("settings.status.enabled")) {
|
|
4656
|
+
return "enabled";
|
|
4657
|
+
}
|
|
4658
|
+
if (text === L("common.disabled") || text === L("settings.status.disabled")) {
|
|
4659
|
+
return "disabled";
|
|
4660
|
+
}
|
|
4661
|
+
if (text === L("settings.status.actionNeeded") || text === L("settings.status.blocked")) {
|
|
4662
|
+
return "attention";
|
|
4663
|
+
}
|
|
4664
|
+
if (text === L("settings.status.supported") || text === L("settings.status.installed")) {
|
|
4665
|
+
return "enabled";
|
|
4666
|
+
}
|
|
4667
|
+
if (text === L("settings.status.disconnected")) {
|
|
4668
|
+
return "disconnected";
|
|
4669
|
+
}
|
|
4670
|
+
return "";
|
|
4671
|
+
}
|
|
4672
|
+
|
|
4673
|
+
function renderSettingsNavRow({ page, icon, title, badge, subtitle, value, valueTone }) {
|
|
4674
|
+
const tone = settingsNavValueTone(value, valueTone);
|
|
4675
|
+
const valueClass = tone
|
|
4676
|
+
? `settings-row__value settings-row__value--${escapeHtml(tone)}`
|
|
4677
|
+
: "settings-row__value";
|
|
3784
4678
|
return `
|
|
3785
4679
|
<button class="settings-nav-row" type="button" data-settings-subpage="${escapeHtml(page)}">
|
|
3786
4680
|
<span class="settings-row__icon" aria-hidden="true">${renderIcon(icon)}</span>
|
|
3787
4681
|
<span class="settings-row__body">
|
|
3788
|
-
<span class="settings-row__title"
|
|
4682
|
+
<span class="settings-row__title-line">
|
|
4683
|
+
<span class="settings-row__title">${escapeHtml(title)}</span>
|
|
4684
|
+
${badge ? `<span class="settings-row__badge">${escapeHtml(badge)}</span>` : ""}
|
|
4685
|
+
</span>
|
|
3789
4686
|
${subtitle ? `<span class="settings-row__subtitle">${escapeHtml(subtitle)}</span>` : ""}
|
|
3790
4687
|
</span>
|
|
3791
|
-
<span class="
|
|
4688
|
+
<span class="${valueClass}">${escapeHtml(value || "")}</span>
|
|
3792
4689
|
<span class="settings-row__chevron" aria-hidden="true">${renderIcon("chevron-right")}</span>
|
|
3793
4690
|
</button>
|
|
3794
4691
|
`;
|
|
@@ -3800,16 +4697,18 @@ function renderSettingsAwayModePage() {
|
|
|
3800
4697
|
return `
|
|
3801
4698
|
<div class="settings-page">
|
|
3802
4699
|
${renderSettingsGroup("", [`
|
|
3803
|
-
<label class="reply-mode-switch" data-claude-away-toggle>
|
|
4700
|
+
<label class="reply-mode-switch reply-mode-switch--settings" data-claude-away-toggle>
|
|
3804
4701
|
<input type="checkbox" class="reply-mode-switch__input" ${enabled ? "checked" : ""} data-claude-away-checkbox />
|
|
3805
|
-
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3806
4702
|
<span class="reply-mode-switch__copy">
|
|
3807
4703
|
<span class="reply-mode-switch__title">
|
|
3808
4704
|
<span>${escapeHtml(L("settings.claudeAway.title"))}</span>
|
|
3809
|
-
<span class="reply-mode-switch__state">${escapeHtml(stateLabel)}</span>
|
|
3810
4705
|
</span>
|
|
3811
4706
|
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.claudeAway.description"))}</span>
|
|
3812
4707
|
</span>
|
|
4708
|
+
<span class="reply-mode-switch--settings__toggle">
|
|
4709
|
+
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
4710
|
+
<span class="reply-mode-switch__state">${escapeHtml(stateLabel)}</span>
|
|
4711
|
+
</span>
|
|
3813
4712
|
</label>
|
|
3814
4713
|
`])}
|
|
3815
4714
|
<p class="settings-page-copy muted">${escapeHtml(L("settings.awayMode.codexNote"))}</p>
|
|
@@ -3823,24 +4722,25 @@ function renderSettingsAutoPilotPage() {
|
|
|
3823
4722
|
const writeLaneContentEnabled = state.session?.autoPilotWriteLaneContent === true;
|
|
3824
4723
|
const writeLaneUiTestsEnabled = state.session?.autoPilotWriteLaneUiTests === true;
|
|
3825
4724
|
const writeLaneSourceEnabled = state.session?.autoPilotWriteLaneSource === true;
|
|
3826
|
-
const trustedWritesEnabled =
|
|
3827
|
-
writeLaneContentEnabled || writeLaneUiTestsEnabled || writeLaneSourceEnabled || state.session?.autoPilotTrustedWrites === true;
|
|
4725
|
+
const trustedWritesEnabled = hasAutoPilotWriteLaneEnabled();
|
|
3828
4726
|
const trustedWritesStateLabel = trustedWritesEnabled ? L("common.enabled") : L("common.disabled");
|
|
3829
4727
|
const recentEntries = recentAutoPilotEntries();
|
|
3830
4728
|
const suggestions = recentAutoPilotSuggestions();
|
|
3831
4729
|
return `
|
|
3832
4730
|
<div class="settings-page">
|
|
3833
4731
|
${renderSettingsGroup("", [`
|
|
3834
|
-
<label class="reply-mode-switch reply-mode-switch--grouped" data-auto-pilot-toggle>
|
|
4732
|
+
<label class="reply-mode-switch reply-mode-switch--settings reply-mode-switch--grouped" data-auto-pilot-toggle>
|
|
3835
4733
|
<input type="checkbox" class="reply-mode-switch__input" ${trustedReadsEnabled ? "checked" : ""} data-auto-pilot-checkbox />
|
|
3836
|
-
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3837
4734
|
<span class="reply-mode-switch__copy">
|
|
3838
4735
|
<span class="reply-mode-switch__title">
|
|
3839
4736
|
<span>${escapeHtml(L("settings.autoPilot.trustedReadsTitle"))}</span>
|
|
3840
|
-
<span class="reply-mode-switch__state">${escapeHtml(trustedReadsStateLabel)}</span>
|
|
3841
4737
|
</span>
|
|
3842
4738
|
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.autoPilot.trustedReadsDescription"))}</span>
|
|
3843
4739
|
</span>
|
|
4740
|
+
<span class="reply-mode-switch--settings__toggle">
|
|
4741
|
+
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
4742
|
+
<span class="reply-mode-switch__state">${escapeHtml(trustedReadsStateLabel)}</span>
|
|
4743
|
+
</span>
|
|
3844
4744
|
</label>
|
|
3845
4745
|
`, `
|
|
3846
4746
|
<div class="settings-toggle-subhead" role="presentation">
|
|
@@ -3850,40 +4750,46 @@ function renderSettingsAutoPilotPage() {
|
|
|
3850
4750
|
`, `
|
|
3851
4751
|
<div class="settings-toggle-subcopy muted">${escapeHtml(L("settings.autoPilot.trustedWritesDescription"))}</div>
|
|
3852
4752
|
`, `
|
|
3853
|
-
<label class="reply-mode-switch reply-mode-switch--grouped" data-auto-pilot-write-lane="content">
|
|
4753
|
+
<label class="reply-mode-switch reply-mode-switch--settings reply-mode-switch--grouped" data-auto-pilot-write-lane="content">
|
|
3854
4754
|
<input type="checkbox" class="reply-mode-switch__input" ${writeLaneContentEnabled ? "checked" : ""} data-auto-pilot-write-lane-checkbox="content" />
|
|
3855
|
-
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3856
4755
|
<span class="reply-mode-switch__copy">
|
|
3857
4756
|
<span class="reply-mode-switch__title">
|
|
3858
4757
|
<span>${escapeHtml(L("settings.autoPilot.writeLaneContentTitle"))}</span>
|
|
3859
|
-
<span class="reply-mode-switch__state">${escapeHtml(writeLaneContentEnabled ? L("common.enabled") : L("common.disabled"))}</span>
|
|
3860
4758
|
</span>
|
|
3861
4759
|
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.autoPilot.writeLaneContentDescription"))}</span>
|
|
3862
4760
|
</span>
|
|
4761
|
+
<span class="reply-mode-switch--settings__toggle">
|
|
4762
|
+
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
4763
|
+
<span class="reply-mode-switch__state">${escapeHtml(writeLaneContentEnabled ? L("common.enabled") : L("common.disabled"))}</span>
|
|
4764
|
+
</span>
|
|
3863
4765
|
</label>
|
|
3864
4766
|
`, `
|
|
3865
|
-
<label class="reply-mode-switch reply-mode-switch--grouped" data-auto-pilot-write-lane="ui-tests">
|
|
4767
|
+
<label class="reply-mode-switch reply-mode-switch--settings reply-mode-switch--grouped" data-auto-pilot-write-lane="ui-tests">
|
|
3866
4768
|
<input type="checkbox" class="reply-mode-switch__input" ${writeLaneUiTestsEnabled ? "checked" : ""} data-auto-pilot-write-lane-checkbox="ui-tests" />
|
|
3867
|
-
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3868
4769
|
<span class="reply-mode-switch__copy">
|
|
3869
4770
|
<span class="reply-mode-switch__title">
|
|
3870
4771
|
<span>${escapeHtml(L("settings.autoPilot.writeLaneUiTestsTitle"))}</span>
|
|
3871
|
-
<span class="reply-mode-switch__state">${escapeHtml(writeLaneUiTestsEnabled ? L("common.enabled") : L("common.disabled"))}</span>
|
|
3872
4772
|
</span>
|
|
3873
4773
|
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.autoPilot.writeLaneUiTestsDescription"))}</span>
|
|
3874
4774
|
</span>
|
|
4775
|
+
<span class="reply-mode-switch--settings__toggle">
|
|
4776
|
+
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
4777
|
+
<span class="reply-mode-switch__state">${escapeHtml(writeLaneUiTestsEnabled ? L("common.enabled") : L("common.disabled"))}</span>
|
|
4778
|
+
</span>
|
|
3875
4779
|
</label>
|
|
3876
4780
|
`, `
|
|
3877
|
-
<label class="reply-mode-switch reply-mode-switch--grouped" data-auto-pilot-write-lane="source">
|
|
4781
|
+
<label class="reply-mode-switch reply-mode-switch--settings reply-mode-switch--grouped" data-auto-pilot-write-lane="source">
|
|
3878
4782
|
<input type="checkbox" class="reply-mode-switch__input" ${writeLaneSourceEnabled ? "checked" : ""} data-auto-pilot-write-lane-checkbox="source" />
|
|
3879
|
-
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3880
4783
|
<span class="reply-mode-switch__copy">
|
|
3881
4784
|
<span class="reply-mode-switch__title">
|
|
3882
4785
|
<span>${escapeHtml(L("settings.autoPilot.writeLaneSourceTitle"))}</span>
|
|
3883
|
-
<span class="reply-mode-switch__state">${escapeHtml(writeLaneSourceEnabled ? L("common.enabled") : L("common.disabled"))}</span>
|
|
3884
4786
|
</span>
|
|
3885
4787
|
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.autoPilot.writeLaneSourceDescription"))}</span>
|
|
3886
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(writeLaneSourceEnabled ? L("common.enabled") : L("common.disabled"))}</span>
|
|
4792
|
+
</span>
|
|
3887
4793
|
</label>
|
|
3888
4794
|
`], { listClassName: "settings-list settings-list--toggle-group" })}
|
|
3889
4795
|
<p class="settings-page-copy muted">${escapeHtml(L("settings.autoPilot.scopeNote"))}</p>
|
|
@@ -4507,33 +5413,290 @@ function renderSettingsA2aSharePage(context) {
|
|
|
4507
5413
|
${meta ? `<span class="settings-compose-entry__meta muted">${meta}</span>` : ""}
|
|
4508
5414
|
</span>
|
|
4509
5415
|
</div>
|
|
4510
|
-
`;
|
|
4511
|
-
});
|
|
4512
|
-
if (hasMore) {
|
|
4513
|
-
const remaining = items.length - visibleCount;
|
|
4514
|
-
filesList.push(
|
|
4515
|
-
`<button type="button" class="settings-compose-more" data-a2a-share-files-more>${escapeHtml(L("settings.a2aShare.showMore", { count: remaining }))}</button>`
|
|
4516
|
-
);
|
|
4517
|
-
} else if (items.length > PAGE_SIZE) {
|
|
4518
|
-
filesList.push(
|
|
4519
|
-
`<button type="button" class="settings-compose-more" data-a2a-share-files-collapse>${escapeHtml(L("settings.a2aShare.showLess"))}</button>`
|
|
4520
|
-
);
|
|
4521
|
-
}
|
|
5416
|
+
`;
|
|
5417
|
+
});
|
|
5418
|
+
if (hasMore) {
|
|
5419
|
+
const remaining = items.length - visibleCount;
|
|
5420
|
+
filesList.push(
|
|
5421
|
+
`<button type="button" class="settings-compose-more" data-a2a-share-files-more>${escapeHtml(L("settings.a2aShare.showMore", { count: remaining }))}</button>`
|
|
5422
|
+
);
|
|
5423
|
+
} else if (items.length > PAGE_SIZE) {
|
|
5424
|
+
filesList.push(
|
|
5425
|
+
`<button type="button" class="settings-compose-more" data-a2a-share-files-collapse>${escapeHtml(L("settings.a2aShare.showLess"))}</button>`
|
|
5426
|
+
);
|
|
5427
|
+
}
|
|
5428
|
+
|
|
5429
|
+
return `
|
|
5430
|
+
<div class="settings-page">
|
|
5431
|
+
${renderSettingsGroup("", [
|
|
5432
|
+
renderSettingsInfoRow(L("settings.row.a2aShareStatus"), statusLabel),
|
|
5433
|
+
renderSettingsInfoRow(L("settings.row.a2aShareEndpoint"), share.shareHost || share.shareUrl || ""),
|
|
5434
|
+
renderSettingsInfoRow(L("settings.row.a2aShareUserId"), share.userId || ""),
|
|
5435
|
+
])}
|
|
5436
|
+
${renderSettingsGroup(L("settings.a2aShare.storage.title"), storageRows)}
|
|
5437
|
+
${items.length
|
|
5438
|
+
? renderSettingsGroup(L("settings.a2aShare.files.title"), filesList)
|
|
5439
|
+
: renderSettingsGroup(L("settings.a2aShare.files.title"), [
|
|
5440
|
+
`<p class="settings-group__description muted">${escapeHtml(L("settings.a2aShare.files.empty"))}</p>`,
|
|
5441
|
+
])}
|
|
5442
|
+
${share.error ? `<p class="settings-page-copy muted">${escapeHtml(L("settings.a2aShare.error", { reason: share.error }))}</p>` : ""}
|
|
5443
|
+
</div>
|
|
5444
|
+
`;
|
|
5445
|
+
}
|
|
5446
|
+
|
|
5447
|
+
function renderSettingsRemotePairingPage(context) {
|
|
5448
|
+
// Always render — remote pairing is a feature toggle, not a credential-
|
|
5449
|
+
// gated integration. If the bridge hasn't responded with a status payload
|
|
5450
|
+
// yet, fall back to a default-disabled view so the toggle still works.
|
|
5451
|
+
const status = context.remotePairing || {
|
|
5452
|
+
enabled: false,
|
|
5453
|
+
relayUrl: "",
|
|
5454
|
+
configuredRelayUrl: "",
|
|
5455
|
+
identityFingerprint: null,
|
|
5456
|
+
sessions: [],
|
|
5457
|
+
pairings: [],
|
|
5458
|
+
auditEvents: [],
|
|
5459
|
+
};
|
|
5460
|
+
|
|
5461
|
+
const enabled = status.enabled === true;
|
|
5462
|
+
const sessions = Array.isArray(status.sessions) ? status.sessions : [];
|
|
5463
|
+
const pairings = Array.isArray(status.pairings) ? status.pairings : [];
|
|
5464
|
+
const auditEvents = Array.isArray(status.auditEvents) ? status.auditEvents : [];
|
|
5465
|
+
const usingRelay = isRemotePairingUsingRelay();
|
|
5466
|
+
const statusModel = remotePairingStatusModel(status, { usingRelay });
|
|
5467
|
+
const sessionsByPub = new Map(
|
|
5468
|
+
sessions.map((s) => [String(s.phonePub || "").toLowerCase(), s]),
|
|
5469
|
+
);
|
|
5470
|
+
const sessionsByPairingId = new Map(
|
|
5471
|
+
sessions.map((s) => [String(s.pairingId || ""), s]),
|
|
5472
|
+
);
|
|
5473
|
+
const togglePending = state.remotePairingPending === "toggle";
|
|
5474
|
+
const relayPending = state.remotePairingPending === "relayUrl";
|
|
5475
|
+
|
|
5476
|
+
// "This device" — the locally-stored enrollment record (if any). Read
|
|
5477
|
+
// synchronously from localStorage on each render so the badge shows up
|
|
5478
|
+
// immediately after a successful lan-enroll without waiting for state
|
|
5479
|
+
// refresh. Returns null in storage-disabled contexts (Safari private
|
|
5480
|
+
// mode etc.) — the page just renders without the indicator.
|
|
5481
|
+
const localPairing = (() => {
|
|
5482
|
+
try {
|
|
5483
|
+
return loadRemotePairingState();
|
|
5484
|
+
} catch {
|
|
5485
|
+
return null;
|
|
5486
|
+
}
|
|
5487
|
+
})();
|
|
5488
|
+
const localPhonePub = (localPairing?.phonePub || "").toLowerCase();
|
|
5489
|
+
const localRegistered = Boolean(localPairing);
|
|
5490
|
+
|
|
5491
|
+
// Identity fingerprint row — shows the bridge's stable public key short-
|
|
5492
|
+
// form so the human can verify what the phone is pairing against.
|
|
5493
|
+
const fpRow = renderSettingsInfoRow(
|
|
5494
|
+
L("settings.remotePairing.identity.fingerprint"),
|
|
5495
|
+
status.identityFingerprint || L("settings.remotePairing.identity.empty"),
|
|
5496
|
+
);
|
|
5497
|
+
|
|
5498
|
+
const connectionSection = `
|
|
5499
|
+
<section class="settings-remote-connection">
|
|
5500
|
+
<header class="settings-remote-connection__header">
|
|
5501
|
+
<div class="settings-remote-connection__copy">
|
|
5502
|
+
<p class="settings-remote-connection__title">${escapeHtml(L("settings.remotePairing.connection.title"))}</p>
|
|
5503
|
+
</div>
|
|
5504
|
+
<span class="settings-remote-status settings-remote-status--${escapeHtml(statusModel.tone)}">${escapeHtml(statusModel.label)}</span>
|
|
5505
|
+
</header>
|
|
5506
|
+
<label class="reply-mode-switch reply-mode-switch--settings" data-remote-pairing-toggle>
|
|
5507
|
+
<input type="checkbox" class="reply-mode-switch__input"
|
|
5508
|
+
${enabled ? "checked" : ""}
|
|
5509
|
+
${togglePending ? "disabled" : ""}
|
|
5510
|
+
data-remote-pairing-toggle-checkbox />
|
|
5511
|
+
<span class="reply-mode-switch--settings__toggle">
|
|
5512
|
+
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
5513
|
+
<span class="reply-mode-switch__state">${escapeHtml(enabled ? L("settings.claudeAway.on") : L("settings.claudeAway.off"))}</span>
|
|
5514
|
+
</span>
|
|
5515
|
+
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.remotePairing.toggle.description"))}</span>
|
|
5516
|
+
</label>
|
|
5517
|
+
<p class="settings-remote-connection__trust">${escapeHtml(L("settings.remotePairing.security.copy"))}</p>
|
|
5518
|
+
</section>
|
|
5519
|
+
`;
|
|
5520
|
+
|
|
5521
|
+
// Relay URL editor. When empty, the bridge falls back to the compiled-in
|
|
5522
|
+
// default — surfacing that here would require leaking it to the client,
|
|
5523
|
+
// so we just show "" and lean on the placeholder.
|
|
5524
|
+
const relayUrlValue = String(status.configuredRelayUrl || "");
|
|
5525
|
+
const relayUrlSection = `
|
|
5526
|
+
<section class="settings-group">
|
|
5527
|
+
<p class="settings-group__title">${escapeHtml(L("settings.remotePairing.relayUrl.title"))}</p>
|
|
5528
|
+
<div class="settings-input-row">
|
|
5529
|
+
<input type="text"
|
|
5530
|
+
class="settings-input"
|
|
5531
|
+
data-remote-pairing-relay-url-input
|
|
5532
|
+
value="${escapeHtml(relayUrlValue)}"
|
|
5533
|
+
placeholder="${escapeHtml(L("settings.remotePairing.relayUrl.placeholder"))}"
|
|
5534
|
+
${relayPending ? "disabled" : ""}
|
|
5535
|
+
autocomplete="off"
|
|
5536
|
+
spellcheck="false" />
|
|
5537
|
+
<button type="button"
|
|
5538
|
+
class="secondary"
|
|
5539
|
+
data-remote-pairing-relay-url-save
|
|
5540
|
+
${relayPending ? "disabled" : ""}>
|
|
5541
|
+
${escapeHtml(L("settings.remotePairing.relayUrl.save"))}
|
|
5542
|
+
</button>
|
|
5543
|
+
</div>
|
|
5544
|
+
<p class="settings-group__description">${escapeHtml(L("settings.remotePairing.relayUrl.help"))}</p>
|
|
5545
|
+
</section>
|
|
5546
|
+
`;
|
|
5547
|
+
|
|
5548
|
+
// Pairings list — one card per phonePub on disk. "Live" badge if there's
|
|
5549
|
+
// an open session; otherwise show the last-seen timestamp. The phone
|
|
5550
|
+
// currently rendering this page gets an extra "This device" badge so the
|
|
5551
|
+
// user can identify themselves in the list (especially useful when more
|
|
5552
|
+
// than one phone is paired to the same Mac).
|
|
5553
|
+
const otherPairings = localPhonePub
|
|
5554
|
+
? pairings.filter((p) => String(p.phonePub || "").toLowerCase() !== localPhonePub)
|
|
5555
|
+
: pairings;
|
|
5556
|
+
const pairingsRows = otherPairings.length === 0
|
|
5557
|
+
? `<div class="settings-copy-block"><p class="muted">${escapeHtml(L("settings.remotePairing.pairings.empty"))}</p></div>`
|
|
5558
|
+
: `<div class="device-list">
|
|
5559
|
+
${otherPairings.map((p) => {
|
|
5560
|
+
const session = sessionsByPub.get(String(p.phonePub || "").toLowerCase())
|
|
5561
|
+
|| sessionsByPairingId.get(String(p.pairingId || ""));
|
|
5562
|
+
const live = isRemotePairingSessionConnected(session);
|
|
5563
|
+
const lastSeenAtMs = Number(session?.lastSeenAtMs || p.lastSeenAtMs) || 0;
|
|
5564
|
+
const lastSeen = lastSeenAtMs
|
|
5565
|
+
? new Date(lastSeenAtMs).toLocaleString(state.locale)
|
|
5566
|
+
: L("settings.remotePairing.pairings.never");
|
|
5567
|
+
const added = p.addedAtMs
|
|
5568
|
+
? new Date(p.addedAtMs).toLocaleString(state.locale)
|
|
5569
|
+
: L("settings.remotePairing.pairings.never");
|
|
5570
|
+
const pendingThis = state.remotePairingPending === `revoke:${p.phonePub}`;
|
|
5571
|
+
return `
|
|
5572
|
+
<article class="device-card" data-remote-pairing-row="${escapeHtml(p.phonePub)}">
|
|
5573
|
+
<div class="device-card__header">
|
|
5574
|
+
<div class="device-card__title-wrap">
|
|
5575
|
+
<div class="device-card__headline">
|
|
5576
|
+
<span class="device-card__icon" aria-hidden="true">${renderIcon("iphone")}</span>
|
|
5577
|
+
<h3 class="device-card__title">${escapeHtml(p.label || p.phoneFingerprint || p.pairingId)}</h3>
|
|
5578
|
+
</div>
|
|
5579
|
+
<p class="device-card__subtitle">${escapeHtml(p.phoneFingerprint || p.phonePub.slice(0, 16))}</p>
|
|
5580
|
+
</div>
|
|
5581
|
+
<div class="device-card__badges">
|
|
5582
|
+
<span class="device-card__badge ${live ? "device-card__badge--live" : "device-card__badge--offline"}">
|
|
5583
|
+
${escapeHtml(live ? L("settings.remotePairing.pairings.live") : L("settings.remotePairing.pairings.offline"))}
|
|
5584
|
+
</span>
|
|
5585
|
+
</div>
|
|
5586
|
+
</div>
|
|
5587
|
+
<div class="device-card__meta">
|
|
5588
|
+
${renderDeviceMetaRow(L("settings.remotePairing.pairings.added"), added)}
|
|
5589
|
+
${renderDeviceMetaRow(L("settings.remotePairing.pairings.lastSeen"), lastSeen)}
|
|
5590
|
+
</div>
|
|
5591
|
+
<div class="device-card__actions">
|
|
5592
|
+
<button type="button"
|
|
5593
|
+
class="secondary secondary--wide"
|
|
5594
|
+
data-remote-pairing-revoke="${escapeHtml(p.phonePub)}"
|
|
5595
|
+
${pendingThis ? "disabled" : ""}>
|
|
5596
|
+
${escapeHtml(L("settings.remotePairing.pairings.revoke"))}
|
|
5597
|
+
</button>
|
|
5598
|
+
</div>
|
|
5599
|
+
</article>
|
|
5600
|
+
`;
|
|
5601
|
+
}).join("")}
|
|
5602
|
+
</div>`;
|
|
5603
|
+
|
|
5604
|
+
// "This device" group — sits above the pairings list and tells the user
|
|
5605
|
+
// explicitly whether *this phone* (the one rendering the page) is the
|
|
5606
|
+
// enrolled one. Two states:
|
|
5607
|
+
// 1. Enrolled: show the phone fingerprint + a confirmation copy.
|
|
5608
|
+
// 2. Not enrolled: surface a hint that re-pairing on LAN registers
|
|
5609
|
+
// the device. (The hint applies even when other phones are paired
|
|
5610
|
+
// — they're not relevant to this device's relay status.)
|
|
5611
|
+
const thisDeviceSection = (() => {
|
|
5612
|
+
if (localRegistered) {
|
|
5613
|
+
const fingerprint = localPairing.phoneFingerprint
|
|
5614
|
+
|| localPairing.phonePub.slice(0, 16);
|
|
5615
|
+
const rotatePending = state.remotePairingPending === "rotateToken";
|
|
5616
|
+
return renderSettingsGroup(L("settings.remotePairing.thisDevice.title"), [
|
|
5617
|
+
`
|
|
5618
|
+
<div class="device-list">
|
|
5619
|
+
<article class="device-card">
|
|
5620
|
+
<div class="device-card__header">
|
|
5621
|
+
<div class="device-card__title-wrap">
|
|
5622
|
+
<div class="device-card__headline">
|
|
5623
|
+
<span class="device-card__icon" aria-hidden="true">${renderIcon("iphone")}</span>
|
|
5624
|
+
<h3 class="device-card__title">${escapeHtml(L("settings.remotePairing.thisDevice.registeredTitle"))}</h3>
|
|
5625
|
+
</div>
|
|
5626
|
+
<p class="device-card__subtitle">${escapeHtml(fingerprint)}</p>
|
|
5627
|
+
</div>
|
|
5628
|
+
<span class="device-card__badge">${escapeHtml(L("settings.remotePairing.pairings.thisDevice"))}</span>
|
|
5629
|
+
</div>
|
|
5630
|
+
<div class="device-card__meta">
|
|
5631
|
+
${renderDeviceMetaRow(L("settings.remotePairing.status.title"), statusModel.label)}
|
|
5632
|
+
${renderDeviceMetaRow(L("settings.remotePairing.thisDevice.fingerprint"), fingerprint)}
|
|
5633
|
+
</div>
|
|
5634
|
+
<div class="device-card__actions">
|
|
5635
|
+
<button type="button"
|
|
5636
|
+
class="secondary secondary--wide"
|
|
5637
|
+
data-remote-pairing-rotate-token
|
|
5638
|
+
${rotatePending || usingRelay ? "disabled" : ""}>
|
|
5639
|
+
${escapeHtml(L("settings.remotePairing.token.rotate"))}
|
|
5640
|
+
</button>
|
|
5641
|
+
</div>
|
|
5642
|
+
${usingRelay ? `<p class="settings-page-copy muted settings-remote-device-hint">${escapeHtml(L("settings.remotePairing.token.lanOnly"))}</p>` : ""}
|
|
5643
|
+
</article>
|
|
5644
|
+
</div>
|
|
5645
|
+
`,
|
|
5646
|
+
]);
|
|
5647
|
+
}
|
|
5648
|
+
return renderSettingsGroup(L("settings.remotePairing.thisDevice.title"), [
|
|
5649
|
+
`
|
|
5650
|
+
<div class="device-list">
|
|
5651
|
+
<article class="device-card">
|
|
5652
|
+
<div class="device-card__header">
|
|
5653
|
+
<div class="device-card__title-wrap">
|
|
5654
|
+
<div class="device-card__headline">
|
|
5655
|
+
<span class="device-card__icon" aria-hidden="true">${renderIcon("iphone")}</span>
|
|
5656
|
+
<h3 class="device-card__title">${escapeHtml(L("settings.remotePairing.thisDevice.notEnrolledTitle"))}</h3>
|
|
5657
|
+
</div>
|
|
5658
|
+
</div>
|
|
5659
|
+
</div>
|
|
5660
|
+
<div class="device-card__meta">
|
|
5661
|
+
${renderDeviceMetaRow(L("settings.remotePairing.status.title"), L("settings.remotePairing.status.disabled"))}
|
|
5662
|
+
${renderDeviceMetaRow(L("settings.remotePairing.thisDevice.fingerprint"), L("common.unavailable"))}
|
|
5663
|
+
</div>
|
|
5664
|
+
<p class="settings-page-copy muted settings-remote-device-hint">${escapeHtml(L("settings.remotePairing.thisDevice.notEnrolled"))}</p>
|
|
5665
|
+
</article>
|
|
5666
|
+
</div>
|
|
5667
|
+
`,
|
|
5668
|
+
]);
|
|
5669
|
+
})();
|
|
5670
|
+
|
|
5671
|
+
const detailsSection = `
|
|
5672
|
+
<details class="settings-disclosure settings-remote-details" data-remote-pairing-details ${state.remotePairingDetailsOpen ? "open" : ""}>
|
|
5673
|
+
<summary>${escapeHtml(L("settings.remotePairing.details.title"))}</summary>
|
|
5674
|
+
<div class="settings-disclosure__body">
|
|
5675
|
+
${relayUrlSection}
|
|
5676
|
+
${renderSettingsGroup(L("settings.remotePairing.identity.title"), [fpRow])}
|
|
5677
|
+
</div>
|
|
5678
|
+
</details>
|
|
5679
|
+
`;
|
|
5680
|
+
const auditSection = renderSettingsGroup(L("settings.remotePairing.audit.title"), [
|
|
5681
|
+
renderRemotePairingAudit(auditEvents),
|
|
5682
|
+
]);
|
|
5683
|
+
|
|
5684
|
+
const noticeBlock = state.remotePairingNotice
|
|
5685
|
+
? `<p class="settings-page-copy">${escapeHtml(state.remotePairingNotice)}</p>`
|
|
5686
|
+
: "";
|
|
5687
|
+
const errorBlock = state.remotePairingError
|
|
5688
|
+
? `<p class="settings-page-copy danger">${escapeHtml(state.remotePairingError)}</p>`
|
|
5689
|
+
: "";
|
|
4522
5690
|
|
|
4523
5691
|
return `
|
|
4524
5692
|
<div class="settings-page">
|
|
4525
|
-
${
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
])}
|
|
4530
|
-
${
|
|
4531
|
-
${
|
|
4532
|
-
? renderSettingsGroup(L("settings.a2aShare.files.title"), filesList)
|
|
4533
|
-
: renderSettingsGroup(L("settings.a2aShare.files.title"), [
|
|
4534
|
-
`<p class="settings-group__description muted">${escapeHtml(L("settings.a2aShare.files.empty"))}</p>`,
|
|
4535
|
-
])}
|
|
4536
|
-
${share.error ? `<p class="settings-page-copy muted">${escapeHtml(L("settings.a2aShare.error", { reason: share.error }))}</p>` : ""}
|
|
5693
|
+
${noticeBlock}
|
|
5694
|
+
${errorBlock}
|
|
5695
|
+
${connectionSection}
|
|
5696
|
+
${thisDeviceSection}
|
|
5697
|
+
${renderSettingsGroup(L("settings.remotePairing.pairings.otherTitle"), [pairingsRows])}
|
|
5698
|
+
${auditSection}
|
|
5699
|
+
${detailsSection}
|
|
4537
5700
|
</div>
|
|
4538
5701
|
`;
|
|
4539
5702
|
}
|
|
@@ -4565,6 +5728,7 @@ function renderSettingsWalletPage(context) {
|
|
|
4565
5728
|
// longer duplicated.
|
|
4566
5729
|
const guideRows = [
|
|
4567
5730
|
renderHazbaseWalletBanner(flow),
|
|
5731
|
+
renderHazbaseWalletBetaNotice(),
|
|
4568
5732
|
state.hazbaseNotice
|
|
4569
5733
|
? `<div class="settings-copy-block settings-copy-block--compact wallet-flow-message wallet-flow-message--notice"><p>${escapeHtml(state.hazbaseNotice)}</p></div>`
|
|
4570
5734
|
: "",
|
|
@@ -4616,15 +5780,10 @@ function renderSettingsWalletPage(context) {
|
|
|
4616
5780
|
function renderHazbaseWalletStepList(flow) {
|
|
4617
5781
|
const rendered = [];
|
|
4618
5782
|
for (const step of flow.steps) {
|
|
4619
|
-
//
|
|
4620
|
-
//
|
|
4621
|
-
//
|
|
4622
|
-
|
|
4623
|
-
// true completion boundary; the ready banner already keys off
|
|
4624
|
-
// `coreReady = signedIn && hasPasskey && hasBaseSepolia`. Drop this
|
|
4625
|
-
// guard (and restore the opt-in reveal) once mainnet is enabled
|
|
4626
|
-
// server-side.
|
|
4627
|
-
if (step.number === 4) {
|
|
5783
|
+
// Keep the Base mainnet roadmap visible only after the usable Base Sepolia
|
|
5784
|
+
// wallet is ready. Showing Step 4 during email/passkey setup makes the
|
|
5785
|
+
// sequential flow feel like there is another action competing for focus.
|
|
5786
|
+
if (step.number === 4 && step.status === "comingSoon" && !flow.coreReady) {
|
|
4628
5787
|
continue;
|
|
4629
5788
|
}
|
|
4630
5789
|
|
|
@@ -4764,16 +5923,10 @@ function deriveHazbaseWalletFlow(hazbase) {
|
|
|
4764
5923
|
icon: "coin",
|
|
4765
5924
|
title: L("settings.wallet.step.base.title"),
|
|
4766
5925
|
copy: L("settings.wallet.step.base.copy"),
|
|
4767
|
-
detail: baseMainnet?.smartAccountAddress || L("settings.
|
|
5926
|
+
detail: baseMainnet?.smartAccountAddress || L("settings.wallet.step.base.comingSoonDetail"),
|
|
4768
5927
|
monoDetail: Boolean(baseMainnet?.smartAccountAddress),
|
|
4769
|
-
status: hasBaseMainnet ? "complete" :
|
|
4770
|
-
actions:
|
|
4771
|
-
? []
|
|
4772
|
-
: [
|
|
4773
|
-
actionButton("settings.hazbase.action.bootstrapBase", "bootstrap-base", {
|
|
4774
|
-
disabled: !coreReady,
|
|
4775
|
-
}),
|
|
4776
|
-
],
|
|
5928
|
+
status: hasBaseMainnet ? "complete" : "comingSoon",
|
|
5929
|
+
actions: [],
|
|
4777
5930
|
},
|
|
4778
5931
|
],
|
|
4779
5932
|
};
|
|
@@ -4825,14 +5978,29 @@ function renderHazbaseWalletBanner(flow) {
|
|
|
4825
5978
|
`;
|
|
4826
5979
|
}
|
|
4827
5980
|
|
|
5981
|
+
function renderHazbaseWalletBetaNotice() {
|
|
5982
|
+
return `
|
|
5983
|
+
<div class="settings-copy-block settings-copy-block--compact wallet-beta-notice">
|
|
5984
|
+
<p>${escapeHtml(L("settings.wallet.betaNotice"))}</p>
|
|
5985
|
+
</div>
|
|
5986
|
+
`;
|
|
5987
|
+
}
|
|
5988
|
+
|
|
4828
5989
|
function renderHazbaseWalletStepCard(step, { mode = "full" } = {}) {
|
|
4829
5990
|
const statusMeta = {
|
|
4830
5991
|
complete: { label: L("settings.wallet.status.complete"), icon: "completed" },
|
|
4831
5992
|
current: { label: L("settings.wallet.status.current"), icon: "pending" },
|
|
4832
5993
|
locked: { label: L("settings.wallet.status.locked"), icon: "lock" },
|
|
4833
5994
|
optional: { label: L("settings.wallet.status.optional"), icon: "coin" },
|
|
5995
|
+
comingSoon: { label: L("settings.wallet.status.comingSoon"), icon: "coin" },
|
|
4834
5996
|
pending: { label: L("settings.wallet.status.pending"), icon: "pending" },
|
|
4835
5997
|
}[step.status] || { label: L("settings.wallet.status.pending"), icon: "pending" };
|
|
5998
|
+
const statusChipHtml = step.status === "current"
|
|
5999
|
+
? ""
|
|
6000
|
+
: `<span class="wallet-step-card__status wallet-step-card__status--${escapeHtml(step.status)}">
|
|
6001
|
+
<span class="wallet-step-card__status-icon" aria-hidden="true">${renderIcon(statusMeta.icon)}</span>
|
|
6002
|
+
<span>${escapeHtml(statusMeta.label)}</span>
|
|
6003
|
+
</span>`;
|
|
4836
6004
|
|
|
4837
6005
|
if (mode === "compact") {
|
|
4838
6006
|
// Compact row for finished steps. Keeps the check icon + title + one-line
|
|
@@ -4863,10 +6031,7 @@ function renderHazbaseWalletStepCard(step, { mode = "full" } = {}) {
|
|
|
4863
6031
|
<h3 class="wallet-step-card__title">${escapeHtml(step.title)}</h3>
|
|
4864
6032
|
</div>
|
|
4865
6033
|
</div>
|
|
4866
|
-
|
|
4867
|
-
<span class="wallet-step-card__status-icon" aria-hidden="true">${renderIcon(statusMeta.icon)}</span>
|
|
4868
|
-
<span>${escapeHtml(statusMeta.label)}</span>
|
|
4869
|
-
</span>
|
|
6034
|
+
${statusChipHtml}
|
|
4870
6035
|
</div>
|
|
4871
6036
|
<p class="wallet-step-card__copy">${escapeHtml(step.copy)}</p>
|
|
4872
6037
|
<p class="wallet-step-card__detail ${step.monoDetail ? "wallet-step-card__detail--mono" : ""}">${escapeHtml(step.detail)}</p>
|
|
@@ -4998,6 +6163,7 @@ function formatUsdcAtomic(atomic) {
|
|
|
4998
6163
|
}
|
|
4999
6164
|
|
|
5000
6165
|
function renderSettingsInfoRow(label, value, options = {}) {
|
|
6166
|
+
const tone = settingsNavValueTone(value, options.valueTone || "");
|
|
5001
6167
|
const rowClassName = [
|
|
5002
6168
|
"settings-info-row",
|
|
5003
6169
|
options.stacked ? "settings-info-row--stacked" : "",
|
|
@@ -5005,6 +6171,7 @@ function renderSettingsInfoRow(label, value, options = {}) {
|
|
|
5005
6171
|
].filter(Boolean).join(" ");
|
|
5006
6172
|
const valueClassName = [
|
|
5007
6173
|
"settings-info-row__value",
|
|
6174
|
+
tone ? `settings-info-row__value--${tone}` : "",
|
|
5008
6175
|
options.mono ? "settings-info-row__value--mono" : "",
|
|
5009
6176
|
options.valueClassName || "",
|
|
5010
6177
|
].filter(Boolean).join(" ");
|
|
@@ -5051,9 +6218,17 @@ function renderTrustedDeviceCard(device) {
|
|
|
5051
6218
|
const badge = device.currentDevice
|
|
5052
6219
|
? `<span class="device-card__badge">${escapeHtml(L("settings.device.thisDevice"))}</span>`
|
|
5053
6220
|
: "";
|
|
5054
|
-
const
|
|
5055
|
-
?
|
|
5056
|
-
:
|
|
6221
|
+
const actions = device.currentDevice
|
|
6222
|
+
? ""
|
|
6223
|
+
: `
|
|
6224
|
+
<div class="device-card__actions">
|
|
6225
|
+
<button
|
|
6226
|
+
class="secondary secondary--wide"
|
|
6227
|
+
type="button"
|
|
6228
|
+
data-device-revoke="${escapeHtml(device.deviceId)}"
|
|
6229
|
+
>${escapeHtml(L("settings.action.revokeDevice"))}</button>
|
|
6230
|
+
</div>
|
|
6231
|
+
`;
|
|
5057
6232
|
|
|
5058
6233
|
return `
|
|
5059
6234
|
<article class="device-card">
|
|
@@ -5074,14 +6249,7 @@ function renderTrustedDeviceCard(device) {
|
|
|
5074
6249
|
${renderDeviceMetaRow(L("settings.row.pushStatus"), pushLabel)}
|
|
5075
6250
|
${renderDeviceMetaRow(L("settings.row.currentLanguage"), localeLabel)}
|
|
5076
6251
|
</div>
|
|
5077
|
-
|
|
5078
|
-
<button
|
|
5079
|
-
class="secondary secondary--wide"
|
|
5080
|
-
type="button"
|
|
5081
|
-
data-device-revoke="${escapeHtml(device.deviceId)}"
|
|
5082
|
-
data-device-current="${device.currentDevice ? "true" : "false"}"
|
|
5083
|
-
>${escapeHtml(actionLabel)}</button>
|
|
5084
|
-
</div>
|
|
6252
|
+
${actions}
|
|
5085
6253
|
</article>
|
|
5086
6254
|
`;
|
|
5087
6255
|
}
|
|
@@ -5154,8 +6322,9 @@ function renderStandardDetailMobile(detail) {
|
|
|
5154
6322
|
const kindInfo = kindMeta(detail.kind, detail);
|
|
5155
6323
|
const spaciousBodyDetail = TIMELINE_MESSAGE_KINDS.has(detail.kind);
|
|
5156
6324
|
const plainIntro = renderDetailPlainIntro(detail, { mobile: true });
|
|
6325
|
+
const hasCompletionReply = isCompletionReplyAvailable(detail);
|
|
5157
6326
|
return `
|
|
5158
|
-
<div class="mobile-detail-screen">
|
|
6327
|
+
<div class="mobile-detail-screen ${hasCompletionReply ? "mobile-detail-screen--has-reply-dock" : ""}">
|
|
5159
6328
|
<div class="detail-shell detail-shell--mobile">
|
|
5160
6329
|
<div class="mobile-detail-scroll mobile-detail-scroll--detail">
|
|
5161
6330
|
${renderDetailMetaRow(detail, kindInfo, { mobile: true })}
|
|
@@ -5185,10 +6354,10 @@ function renderStandardDetailMobile(detail) {
|
|
|
5185
6354
|
${renderDetailDiffPanel(detail, { mobile: true })}
|
|
5186
6355
|
${renderDetailDiffThreadGroups(detail, { mobile: true })}
|
|
5187
6356
|
${renderDetailFileRefs(detail, { mobile: true })}
|
|
5188
|
-
${renderCompletionReplyComposer(detail, { mobile: true })}
|
|
5189
6357
|
</div>
|
|
5190
6358
|
${detail.readOnly ? "" : renderActionButtons(detail.actions || [], { mobileSticky: true })}
|
|
5191
6359
|
</div>
|
|
6360
|
+
${renderCompletionReplyDock(detail)}
|
|
5192
6361
|
</div>
|
|
5193
6362
|
`;
|
|
5194
6363
|
}
|
|
@@ -5936,7 +7105,7 @@ function renderThreadShareDetail(detail, options = {}) {
|
|
|
5936
7105
|
}
|
|
5937
7106
|
|
|
5938
7107
|
function renderCompletionReplyComposer(detail, options = {}) {
|
|
5939
|
-
if ((detail
|
|
7108
|
+
if (!isCompletionReplyAvailable(detail)) {
|
|
5940
7109
|
return "";
|
|
5941
7110
|
}
|
|
5942
7111
|
|
|
@@ -6110,6 +7279,51 @@ function renderCompletionReplyComposer(detail, options = {}) {
|
|
|
6110
7279
|
`;
|
|
6111
7280
|
}
|
|
6112
7281
|
|
|
7282
|
+
function isCompletionReplyAvailable(detail) {
|
|
7283
|
+
return Boolean(
|
|
7284
|
+
detail &&
|
|
7285
|
+
(detail.kind === "completion" || detail.kind === "assistant_final") &&
|
|
7286
|
+
detail.reply?.enabled === true &&
|
|
7287
|
+
detail.token
|
|
7288
|
+
);
|
|
7289
|
+
}
|
|
7290
|
+
|
|
7291
|
+
function renderCompletionReplyDock(detail) {
|
|
7292
|
+
if (!isCompletionReplyAvailable(detail)) {
|
|
7293
|
+
return "";
|
|
7294
|
+
}
|
|
7295
|
+
const providerVars = { provider: providerDisplayName(detail?.provider) };
|
|
7296
|
+
return `
|
|
7297
|
+
<div class="reply-dock" role="region" aria-label="${escapeHtml(L("reply.eyebrow"))}">
|
|
7298
|
+
<button
|
|
7299
|
+
class="reply-dock__button"
|
|
7300
|
+
type="button"
|
|
7301
|
+
data-open-completion-reply-sheet
|
|
7302
|
+
data-token="${escapeHtml(detail.token)}"
|
|
7303
|
+
>
|
|
7304
|
+
<span class="reply-dock__label">${escapeHtml(L("reply.title", providerVars))}</span>
|
|
7305
|
+
</button>
|
|
7306
|
+
</div>
|
|
7307
|
+
`;
|
|
7308
|
+
}
|
|
7309
|
+
|
|
7310
|
+
function renderCompletionReplySheet(detail) {
|
|
7311
|
+
if (!isCompletionReplyAvailable(detail) || state.completionReplySheetToken !== detail.token) {
|
|
7312
|
+
return "";
|
|
7313
|
+
}
|
|
7314
|
+
const providerVars = { provider: providerDisplayName(detail?.provider) };
|
|
7315
|
+
return `
|
|
7316
|
+
<div class="reply-sheet-backdrop" data-close-completion-reply-sheet aria-hidden="true"></div>
|
|
7317
|
+
<section class="reply-sheet" role="dialog" aria-modal="true" aria-label="${escapeHtml(L("reply.title", providerVars))}">
|
|
7318
|
+
<div class="reply-sheet__handle" aria-hidden="true"></div>
|
|
7319
|
+
<button class="reply-sheet__close" type="button" data-close-completion-reply-sheet aria-label="${escapeHtml(L("common.close"))}">
|
|
7320
|
+
<span aria-hidden="true">×</span>
|
|
7321
|
+
</button>
|
|
7322
|
+
${renderCompletionReplyComposer(detail, { mobile: true, sheet: true })}
|
|
7323
|
+
</section>
|
|
7324
|
+
`;
|
|
7325
|
+
}
|
|
7326
|
+
|
|
6113
7327
|
function renderChoiceQuestions(detail) {
|
|
6114
7328
|
const effectiveAnswers = getEffectiveChoiceDraftAnswers(detail);
|
|
6115
7329
|
return detail.questions
|
|
@@ -6360,10 +7574,27 @@ function renderInstallBanner() {
|
|
|
6360
7574
|
`;
|
|
6361
7575
|
}
|
|
6362
7576
|
|
|
7577
|
+
function renderClientUpdateBanner() {
|
|
7578
|
+
return `
|
|
7579
|
+
<section class="install-banner install-banner--push">
|
|
7580
|
+
<div class="install-banner__copy">
|
|
7581
|
+
<strong>${escapeHtml(L("banner.clientUpdate.title"))}</strong>
|
|
7582
|
+
<p class="muted">${escapeHtml(L("banner.clientUpdate.copy"))}</p>
|
|
7583
|
+
</div>
|
|
7584
|
+
<div class="actions install-banner__actions">
|
|
7585
|
+
<button class="secondary" type="button" data-force-app-refresh>${escapeHtml(L("banner.clientUpdate.action"))}</button>
|
|
7586
|
+
</div>
|
|
7587
|
+
</section>
|
|
7588
|
+
`;
|
|
7589
|
+
}
|
|
7590
|
+
|
|
6363
7591
|
function renderTopBanner() {
|
|
6364
7592
|
if (!isDesktopLayout() && (state.detailOpen || isSettingsSubpageOpen())) {
|
|
6365
7593
|
return "";
|
|
6366
7594
|
}
|
|
7595
|
+
if (state.clientUpdateRequired) {
|
|
7596
|
+
return renderClientUpdateBanner();
|
|
7597
|
+
}
|
|
6367
7598
|
if (shouldShowInstallBanner()) {
|
|
6368
7599
|
return renderInstallBanner();
|
|
6369
7600
|
}
|
|
@@ -6496,20 +7727,7 @@ function renderLogoutConfirmModal() {
|
|
|
6496
7727
|
<strong id="logout-confirm-title">${escapeHtml(L("logout.confirm.title"))}</strong>
|
|
6497
7728
|
<p class="muted">${escapeHtml(L("logout.confirm.copy"))}</p>
|
|
6498
7729
|
</div>
|
|
6499
|
-
<
|
|
6500
|
-
<div class="logout-option__copy">
|
|
6501
|
-
<strong>${escapeHtml(L("logout.confirm.keepTrustedTitle"))}</strong>
|
|
6502
|
-
<p class="muted">${escapeHtml(L("logout.confirm.keepTrustedCopy"))}</p>
|
|
6503
|
-
</div>
|
|
6504
|
-
<button class="primary primary--wide" type="button" data-logout-mode="session">${escapeHtml(L("logout.action.keepTrusted"))}</button>
|
|
6505
|
-
</div>
|
|
6506
|
-
<div class="logout-option logout-option--danger">
|
|
6507
|
-
<div class="logout-option__copy">
|
|
6508
|
-
<strong>${escapeHtml(L("logout.confirm.removeTitle"))}</strong>
|
|
6509
|
-
<p class="muted">${escapeHtml(L("logout.confirm.removeCopy"))}</p>
|
|
6510
|
-
</div>
|
|
6511
|
-
<button class="secondary secondary--wide" type="button" data-logout-mode="revoke">${escapeHtml(L("logout.action.removeDevice"))}</button>
|
|
6512
|
-
</div>
|
|
7730
|
+
<button class="secondary secondary--wide" type="button" data-logout-mode="session">${escapeHtml(L("logout.action.keepTrusted"))}</button>
|
|
6513
7731
|
<button class="ghost ghost--wide" type="button" data-close-logout-confirm>${escapeHtml(L("common.cancel"))}</button>
|
|
6514
7732
|
</section>
|
|
6515
7733
|
</div>
|
|
@@ -6870,24 +8088,56 @@ function bindShellInteractions() {
|
|
|
6870
8088
|
state.pendingActionUrls.delete(actionUrl);
|
|
6871
8089
|
} catch (error) {
|
|
6872
8090
|
state.pendingActionUrls.delete(actionUrl);
|
|
8091
|
+
const approvalFinalized = Boolean(error?.payload?.approvalFinalized);
|
|
6873
8092
|
if (error?.errorKey === "hazbase-session-expired") {
|
|
6874
8093
|
await fetchHazbaseStatus();
|
|
6875
8094
|
}
|
|
6876
|
-
|
|
6877
|
-
|
|
6878
|
-
if (
|
|
6879
|
-
|
|
8095
|
+
if (approvalFinalized) {
|
|
8096
|
+
await refreshAuthenticatedState();
|
|
8097
|
+
if (keepDetailOpen && activeItem?.kind === "approval") {
|
|
8098
|
+
pinActionOutcomeDetail(
|
|
8099
|
+
activeItem,
|
|
8100
|
+
buildActionOutcomeDetail({
|
|
8101
|
+
kind: "approval",
|
|
8102
|
+
title: state.currentDetail?.title,
|
|
8103
|
+
message: L("server.message.paymentFailed", { reason: error.message || String(error) }),
|
|
8104
|
+
})
|
|
8105
|
+
);
|
|
8106
|
+
}
|
|
8107
|
+
} else {
|
|
8108
|
+
// Restore buttons on recoverable failure so the user can retry the same action.
|
|
8109
|
+
for (const sibling of siblingButtons) {
|
|
8110
|
+
if (originalLabels.has(sibling)) {
|
|
8111
|
+
sibling.innerHTML = originalLabels.get(sibling);
|
|
8112
|
+
}
|
|
8113
|
+
sibling.disabled = false;
|
|
8114
|
+
sibling.removeAttribute("aria-busy");
|
|
6880
8115
|
}
|
|
6881
|
-
|
|
6882
|
-
sibling.removeAttribute("aria-busy");
|
|
8116
|
+
button.classList.remove("is-loading");
|
|
6883
8117
|
}
|
|
6884
|
-
button.classList.remove("is-loading");
|
|
6885
8118
|
state.pushError = error.message || String(error);
|
|
6886
8119
|
await renderShell();
|
|
6887
8120
|
}
|
|
6888
8121
|
});
|
|
6889
8122
|
}
|
|
6890
8123
|
|
|
8124
|
+
for (const button of document.querySelectorAll("[data-open-completion-reply-sheet][data-token]")) {
|
|
8125
|
+
button.addEventListener("click", async () => {
|
|
8126
|
+
state.completionReplySheetToken = button.dataset.token || "";
|
|
8127
|
+
resetHorizontalViewportScroll();
|
|
8128
|
+
await renderShell();
|
|
8129
|
+
resetHorizontalViewportScroll();
|
|
8130
|
+
});
|
|
8131
|
+
}
|
|
8132
|
+
|
|
8133
|
+
for (const trigger of document.querySelectorAll("[data-close-completion-reply-sheet]")) {
|
|
8134
|
+
trigger.addEventListener("click", async () => {
|
|
8135
|
+
state.completionReplySheetToken = "";
|
|
8136
|
+
resetHorizontalViewportScroll();
|
|
8137
|
+
await renderShell();
|
|
8138
|
+
});
|
|
8139
|
+
}
|
|
8140
|
+
|
|
6891
8141
|
for (const input of document.querySelectorAll("[data-reply-mode-toggle][data-reply-token]")) {
|
|
6892
8142
|
input.addEventListener("change", async () => {
|
|
6893
8143
|
const token = input.dataset.replyToken || "";
|
|
@@ -7098,14 +8348,51 @@ function bindShellInteractions() {
|
|
|
7098
8348
|
state.session.autoPilotWriteLaneSource = result?.writeLaneSourceEnabled === true;
|
|
7099
8349
|
}
|
|
7100
8350
|
|
|
8351
|
+
function snapshotAutoPilotSettings() {
|
|
8352
|
+
return {
|
|
8353
|
+
trustedReads: state.session?.autoPilotTrustedReads === true,
|
|
8354
|
+
trustedWrites: state.session?.autoPilotTrustedWrites === true,
|
|
8355
|
+
writeLaneContent: state.session?.autoPilotWriteLaneContent === true,
|
|
8356
|
+
writeLaneUiTests: state.session?.autoPilotWriteLaneUiTests === true,
|
|
8357
|
+
writeLaneSource: state.session?.autoPilotWriteLaneSource === true,
|
|
8358
|
+
};
|
|
8359
|
+
}
|
|
8360
|
+
|
|
8361
|
+
function restoreAutoPilotSettings(snapshot) {
|
|
8362
|
+
if (!state.session || !snapshot) {
|
|
8363
|
+
return;
|
|
8364
|
+
}
|
|
8365
|
+
state.session.autoPilotTrustedReads = snapshot.trustedReads === true;
|
|
8366
|
+
state.session.autoPilotTrustedWrites = snapshot.trustedWrites === true;
|
|
8367
|
+
state.session.autoPilotWriteLaneContent = snapshot.writeLaneContent === true;
|
|
8368
|
+
state.session.autoPilotWriteLaneUiTests = snapshot.writeLaneUiTests === true;
|
|
8369
|
+
state.session.autoPilotWriteLaneSource = snapshot.writeLaneSource === true;
|
|
8370
|
+
}
|
|
8371
|
+
|
|
8372
|
+
function reconcileAutoPilotTrustedWrites() {
|
|
8373
|
+
if (!state.session) {
|
|
8374
|
+
return;
|
|
8375
|
+
}
|
|
8376
|
+
state.session.autoPilotTrustedWrites = Boolean(
|
|
8377
|
+
state.session.autoPilotWriteLaneContent === true ||
|
|
8378
|
+
state.session.autoPilotWriteLaneUiTests === true ||
|
|
8379
|
+
state.session.autoPilotWriteLaneSource === true,
|
|
8380
|
+
);
|
|
8381
|
+
}
|
|
8382
|
+
|
|
7101
8383
|
for (const checkbox of document.querySelectorAll("[data-auto-pilot-checkbox]")) {
|
|
7102
8384
|
checkbox.addEventListener("change", async () => {
|
|
7103
8385
|
const next = checkbox.checked === true;
|
|
8386
|
+
const previous = snapshotAutoPilotSettings();
|
|
8387
|
+
if (state.session) {
|
|
8388
|
+
state.session.autoPilotTrustedReads = next;
|
|
8389
|
+
}
|
|
8390
|
+
await renderShell();
|
|
7104
8391
|
try {
|
|
7105
8392
|
const result = await apiPost("/api/settings/auto-pilot", { trustedReadsEnabled: next });
|
|
7106
8393
|
applyAutoPilotSettingsResult(result);
|
|
7107
|
-
await refreshAuthenticatedState();
|
|
7108
8394
|
} catch (error) {
|
|
8395
|
+
restoreAutoPilotSettings(previous);
|
|
7109
8396
|
state.pushError = error.message || String(error);
|
|
7110
8397
|
}
|
|
7111
8398
|
await renderShell();
|
|
@@ -7116,17 +8403,29 @@ function bindShellInteractions() {
|
|
|
7116
8403
|
checkbox.addEventListener("change", async () => {
|
|
7117
8404
|
const next = checkbox.checked === true;
|
|
7118
8405
|
const lane = normalizeClientText(checkbox.getAttribute("data-auto-pilot-write-lane-checkbox") || "");
|
|
8406
|
+
const previous = snapshotAutoPilotSettings();
|
|
7119
8407
|
const payload =
|
|
7120
8408
|
lane === "content"
|
|
7121
8409
|
? { writeLaneContentEnabled: next }
|
|
7122
8410
|
: lane === "ui-tests"
|
|
7123
8411
|
? { writeLaneUiTestsEnabled: next }
|
|
7124
8412
|
: { writeLaneSourceEnabled: next };
|
|
8413
|
+
if (state.session) {
|
|
8414
|
+
if (lane === "content") {
|
|
8415
|
+
state.session.autoPilotWriteLaneContent = next;
|
|
8416
|
+
} else if (lane === "ui-tests") {
|
|
8417
|
+
state.session.autoPilotWriteLaneUiTests = next;
|
|
8418
|
+
} else {
|
|
8419
|
+
state.session.autoPilotWriteLaneSource = next;
|
|
8420
|
+
}
|
|
8421
|
+
reconcileAutoPilotTrustedWrites();
|
|
8422
|
+
}
|
|
8423
|
+
await renderShell();
|
|
7125
8424
|
try {
|
|
7126
8425
|
const result = await apiPost("/api/settings/auto-pilot", payload);
|
|
7127
8426
|
applyAutoPilotSettingsResult(result);
|
|
7128
|
-
await refreshAuthenticatedState();
|
|
7129
8427
|
} catch (error) {
|
|
8428
|
+
restoreAutoPilotSettings(previous);
|
|
7130
8429
|
state.pushError = error.message || String(error);
|
|
7131
8430
|
}
|
|
7132
8431
|
await renderShell();
|
|
@@ -7147,11 +8446,23 @@ function bindShellInteractions() {
|
|
|
7147
8446
|
if (!payload) {
|
|
7148
8447
|
return;
|
|
7149
8448
|
}
|
|
8449
|
+
const previous = snapshotAutoPilotSettings();
|
|
8450
|
+
if (state.session) {
|
|
8451
|
+
if (lane === "content") {
|
|
8452
|
+
state.session.autoPilotWriteLaneContent = true;
|
|
8453
|
+
} else if (lane === "ui_tests") {
|
|
8454
|
+
state.session.autoPilotWriteLaneUiTests = true;
|
|
8455
|
+
} else {
|
|
8456
|
+
state.session.autoPilotWriteLaneSource = true;
|
|
8457
|
+
}
|
|
8458
|
+
reconcileAutoPilotTrustedWrites();
|
|
8459
|
+
}
|
|
8460
|
+
await renderShell();
|
|
7150
8461
|
try {
|
|
7151
8462
|
const result = await apiPost("/api/settings/auto-pilot", payload);
|
|
7152
8463
|
applyAutoPilotSettingsResult(result);
|
|
7153
|
-
await refreshAuthenticatedState();
|
|
7154
8464
|
} catch (error) {
|
|
8465
|
+
restoreAutoPilotSettings(previous);
|
|
7155
8466
|
state.pushError = error.message || String(error);
|
|
7156
8467
|
}
|
|
7157
8468
|
await renderShell();
|
|
@@ -7238,6 +8549,166 @@ function bindShellInteractions() {
|
|
|
7238
8549
|
});
|
|
7239
8550
|
}
|
|
7240
8551
|
|
|
8552
|
+
// ─── Remote pairing (Phase 5c) ─────────────────────────────────────
|
|
8553
|
+
// Optimistic flips with bridge-side reconciliation. The bridge endpoints
|
|
8554
|
+
// hot-restart the orchestrator, so the post-POST `fetchRemotePairingStatus`
|
|
8555
|
+
// call can briefly observe a transient state — that's fine, the next
|
|
8556
|
+
// render will land on the steady state.
|
|
8557
|
+
for (const details of document.querySelectorAll("[data-remote-pairing-details]")) {
|
|
8558
|
+
details.addEventListener("toggle", () => {
|
|
8559
|
+
state.remotePairingDetailsOpen = details.open === true;
|
|
8560
|
+
});
|
|
8561
|
+
}
|
|
8562
|
+
|
|
8563
|
+
for (const checkbox of document.querySelectorAll("[data-remote-pairing-toggle-checkbox]")) {
|
|
8564
|
+
checkbox.addEventListener("change", async () => {
|
|
8565
|
+
const next = checkbox.checked === true;
|
|
8566
|
+
const previous = state.remotePairingStatus?.enabled === true;
|
|
8567
|
+
if (!next && isRemotePairingUsingRelay() && !window.confirm(L("settings.remotePairing.toggle.offConfirm"))) {
|
|
8568
|
+
checkbox.checked = previous;
|
|
8569
|
+
return;
|
|
8570
|
+
}
|
|
8571
|
+
state.remotePairingNotice = "";
|
|
8572
|
+
state.remotePairingError = "";
|
|
8573
|
+
state.remotePairingPending = "toggle";
|
|
8574
|
+
// Optimistic flip — render once with the new value disabled-during-pending,
|
|
8575
|
+
// then reconcile with the bridge.
|
|
8576
|
+
if (state.remotePairingStatus) {
|
|
8577
|
+
state.remotePairingStatus = { ...state.remotePairingStatus, enabled: next };
|
|
8578
|
+
} else {
|
|
8579
|
+
state.remotePairingStatus = {
|
|
8580
|
+
enabled: next,
|
|
8581
|
+
relayUrl: "",
|
|
8582
|
+
configuredRelayUrl: "",
|
|
8583
|
+
identityFingerprint: null,
|
|
8584
|
+
sessions: [],
|
|
8585
|
+
pairings: [],
|
|
8586
|
+
auditEvents: [],
|
|
8587
|
+
};
|
|
8588
|
+
}
|
|
8589
|
+
await renderShell();
|
|
8590
|
+
try {
|
|
8591
|
+
await apiPost("/api/remote-pairing/toggle", { enabled: next });
|
|
8592
|
+
state.remotePairingNotice = next
|
|
8593
|
+
? L("settings.remotePairing.notice.toggleOn")
|
|
8594
|
+
: L("settings.remotePairing.notice.toggleOff");
|
|
8595
|
+
await fetchRemotePairingStatus();
|
|
8596
|
+
} catch (error) {
|
|
8597
|
+
// Roll back to the previous value so the checkbox visually reverts.
|
|
8598
|
+
if (state.remotePairingStatus) {
|
|
8599
|
+
state.remotePairingStatus = { ...state.remotePairingStatus, enabled: previous };
|
|
8600
|
+
}
|
|
8601
|
+
state.remotePairingError = L("settings.remotePairing.error.toggleFailed");
|
|
8602
|
+
} finally {
|
|
8603
|
+
state.remotePairingPending = "";
|
|
8604
|
+
await renderShell();
|
|
8605
|
+
}
|
|
8606
|
+
});
|
|
8607
|
+
}
|
|
8608
|
+
|
|
8609
|
+
for (const button of document.querySelectorAll("[data-remote-pairing-relay-url-save]")) {
|
|
8610
|
+
button.addEventListener("click", async () => {
|
|
8611
|
+
// The input lives on the same page; querySelector grabs the (single)
|
|
8612
|
+
// visible field. If we ever add multiple URL editors we'd need to
|
|
8613
|
+
// scope this to a container — but right now there's exactly one.
|
|
8614
|
+
const input = document.querySelector("[data-remote-pairing-relay-url-input]");
|
|
8615
|
+
const trimmed = (input?.value || "").trim();
|
|
8616
|
+
state.remotePairingNotice = "";
|
|
8617
|
+
state.remotePairingError = "";
|
|
8618
|
+
state.remotePairingPending = "relayUrl";
|
|
8619
|
+
await renderShell();
|
|
8620
|
+
try {
|
|
8621
|
+
await apiPost("/api/remote-pairing/relay-url", { relayUrl: trimmed });
|
|
8622
|
+
state.remotePairingNotice = L("settings.remotePairing.notice.relayUrlSaved");
|
|
8623
|
+
await fetchRemotePairingStatus();
|
|
8624
|
+
} catch (error) {
|
|
8625
|
+
// Distinguish the validation error from a generic save failure so
|
|
8626
|
+
// the user knows whether to fix the URL or retry.
|
|
8627
|
+
state.remotePairingError = error?.errorKey === "invalid-relay-url"
|
|
8628
|
+
? L("settings.remotePairing.error.invalidRelayUrl")
|
|
8629
|
+
: L("settings.remotePairing.error.relayUrlFailed");
|
|
8630
|
+
} finally {
|
|
8631
|
+
state.remotePairingPending = "";
|
|
8632
|
+
await renderShell();
|
|
8633
|
+
}
|
|
8634
|
+
});
|
|
8635
|
+
}
|
|
8636
|
+
|
|
8637
|
+
for (const button of document.querySelectorAll("[data-remote-pairing-rotate-token]")) {
|
|
8638
|
+
button.addEventListener("click", async () => {
|
|
8639
|
+
const local = loadRemotePairingState();
|
|
8640
|
+
if (!local?.phonePub) return;
|
|
8641
|
+
state.remotePairingNotice = "";
|
|
8642
|
+
state.remotePairingError = "";
|
|
8643
|
+
state.remotePairingPending = "rotateToken";
|
|
8644
|
+
await renderShell();
|
|
8645
|
+
try {
|
|
8646
|
+
const response = await apiPost("/api/remote-pairing/rotate-token", { phonePub: local.phonePub });
|
|
8647
|
+
saveRemotePairingState({
|
|
8648
|
+
pairingId: response.pairingId,
|
|
8649
|
+
relayToken: response.relayToken,
|
|
8650
|
+
phonePub: response.phonePub,
|
|
8651
|
+
phoneFingerprint: response.phoneFingerprint,
|
|
8652
|
+
bridgePubHex: response.bridgePubHex,
|
|
8653
|
+
bridgeFingerprint: response.bridgeFingerprint,
|
|
8654
|
+
relayUrl: response.relayUrl,
|
|
8655
|
+
label: response.label || local.label || "",
|
|
8656
|
+
addedAtMs: Number.isFinite(response.addedAtMs) ? response.addedAtMs : local.addedAtMs,
|
|
8657
|
+
relayTokenUpdatedAtMs: Number.isFinite(response.relayTokenUpdatedAtMs)
|
|
8658
|
+
? response.relayTokenUpdatedAtMs
|
|
8659
|
+
: Date.now(),
|
|
8660
|
+
});
|
|
8661
|
+
state.remotePairingNotice = L("settings.remotePairing.notice.tokenRotated");
|
|
8662
|
+
await fetchRemotePairingStatus();
|
|
8663
|
+
} catch (error) {
|
|
8664
|
+
state.remotePairingError = L("settings.remotePairing.error.tokenRotateFailed");
|
|
8665
|
+
} finally {
|
|
8666
|
+
state.remotePairingPending = "";
|
|
8667
|
+
await renderShell();
|
|
8668
|
+
}
|
|
8669
|
+
});
|
|
8670
|
+
}
|
|
8671
|
+
|
|
8672
|
+
for (const button of document.querySelectorAll("[data-remote-pairing-revoke]")) {
|
|
8673
|
+
button.addEventListener("click", async () => {
|
|
8674
|
+
const phonePub = button.dataset.remotePairingRevoke || "";
|
|
8675
|
+
if (!phonePub) return;
|
|
8676
|
+
if (!window.confirm(L("settings.remotePairing.pairings.revokeConfirm"))) return;
|
|
8677
|
+
state.remotePairingNotice = "";
|
|
8678
|
+
state.remotePairingError = "";
|
|
8679
|
+
// Per-pub pending key so the renderer only disables this card's
|
|
8680
|
+
// button, not every revoke button on the page.
|
|
8681
|
+
state.remotePairingPending = `revoke:${phonePub}`;
|
|
8682
|
+
await renderShell();
|
|
8683
|
+
try {
|
|
8684
|
+
await apiPost("/api/remote-pairing/revoke", { phonePub });
|
|
8685
|
+
// If the revoked phone is THIS device, drop the local enrollment
|
|
8686
|
+
// record too — otherwise the "this device" indicator would keep
|
|
8687
|
+
// claiming the (now-revoked) record is live, and a future fetch
|
|
8688
|
+
// routing layer would try to reuse the stale pairingId. Best
|
|
8689
|
+
// effort: storage failures don't change the revoke outcome.
|
|
8690
|
+
try {
|
|
8691
|
+
const local = loadRemotePairingState();
|
|
8692
|
+
if (
|
|
8693
|
+
local
|
|
8694
|
+
&& local.phonePub.toLowerCase() === phonePub.toLowerCase()
|
|
8695
|
+
) {
|
|
8696
|
+
clearRemotePairingState();
|
|
8697
|
+
}
|
|
8698
|
+
} catch {
|
|
8699
|
+
// ignore — same fall-through as savePairingState
|
|
8700
|
+
}
|
|
8701
|
+
state.remotePairingNotice = L("settings.remotePairing.notice.revoked");
|
|
8702
|
+
await fetchRemotePairingStatus();
|
|
8703
|
+
} catch (error) {
|
|
8704
|
+
state.remotePairingError = L("settings.remotePairing.error.revokeFailed");
|
|
8705
|
+
} finally {
|
|
8706
|
+
state.remotePairingPending = "";
|
|
8707
|
+
await renderShell();
|
|
8708
|
+
}
|
|
8709
|
+
});
|
|
8710
|
+
}
|
|
8711
|
+
|
|
7241
8712
|
|
|
7242
8713
|
for (const button of document.querySelectorAll("[data-hazbase-action]")) {
|
|
7243
8714
|
button.addEventListener("click", async () => {
|
|
@@ -7514,24 +8985,13 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7514
8985
|
try {
|
|
7515
8986
|
const decisionBody = { action: submittedAction, editedText };
|
|
7516
8987
|
if (editedTitle) decisionBody.editedTitle = editedTitle;
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
const errBody = await res.json().catch(() => ({}));
|
|
7525
|
-
alert(`Moltbook draft ${submittedAction} failed: ${errBody.error || res.status}`);
|
|
7526
|
-
buttons.forEach((btn) => {
|
|
7527
|
-
btn.disabled = false;
|
|
7528
|
-
btn.classList.remove("is-loading", "is-dimmed");
|
|
7529
|
-
btn.innerHTML = labelCache.get(btn);
|
|
7530
|
-
});
|
|
7531
|
-
if (textarea) textarea.readOnly = false;
|
|
7532
|
-
moltbookDraftForm.dataset.submitting = "";
|
|
7533
|
-
return;
|
|
7534
|
-
}
|
|
8988
|
+
// apiPost routes through LAN-first / relay-fallback and throws on
|
|
8989
|
+
// !ok — the existing catch below handles both transport and HTTP
|
|
8990
|
+
// failures uniformly.
|
|
8991
|
+
await apiPost(
|
|
8992
|
+
`/api/items/moltbook-draft/${encodeURIComponent(token)}/decision`,
|
|
8993
|
+
decisionBody,
|
|
8994
|
+
);
|
|
7535
8995
|
// Mark local detail as resolved so re-render shows "already resolved" immediately.
|
|
7536
8996
|
if (state.currentDetail?.kind === "moltbook_draft") {
|
|
7537
8997
|
state.currentDetail.moltbookDraftEnabled = false;
|
|
@@ -7540,7 +9000,7 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7540
9000
|
await refreshAuthenticatedState();
|
|
7541
9001
|
await renderShell();
|
|
7542
9002
|
} catch (error) {
|
|
7543
|
-
alert(`Moltbook draft
|
|
9003
|
+
alert(`Moltbook draft ${submittedAction} failed: ${error.message}`);
|
|
7544
9004
|
buttons.forEach((btn) => {
|
|
7545
9005
|
btn.disabled = false;
|
|
7546
9006
|
btn.classList.remove("is-loading", "is-dimmed");
|
|
@@ -7591,24 +9051,10 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7591
9051
|
try {
|
|
7592
9052
|
const decisionBody = { action: submittedAction, instruction };
|
|
7593
9053
|
if (executor) decisionBody.executor = executor;
|
|
7594
|
-
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
body: JSON.stringify(decisionBody),
|
|
7599
|
-
});
|
|
7600
|
-
if (!res.ok) {
|
|
7601
|
-
const errBody = await res.json().catch(() => ({}));
|
|
7602
|
-
alert(`A2A task ${submittedAction} failed: ${errBody.error || res.status}`);
|
|
7603
|
-
buttons.forEach((btn) => {
|
|
7604
|
-
btn.disabled = false;
|
|
7605
|
-
btn.classList.remove("is-loading", "is-dimmed");
|
|
7606
|
-
btn.innerHTML = labelCache.get(btn);
|
|
7607
|
-
});
|
|
7608
|
-
if (textarea) textarea.readOnly = false;
|
|
7609
|
-
a2aTaskForm.dataset.submitting = "";
|
|
7610
|
-
return;
|
|
7611
|
-
}
|
|
9054
|
+
await apiPost(
|
|
9055
|
+
`/api/items/a2a-task/${encodeURIComponent(token)}/decision`,
|
|
9056
|
+
decisionBody,
|
|
9057
|
+
);
|
|
7612
9058
|
if (state.currentDetail?.kind === "a2a_task") {
|
|
7613
9059
|
state.currentDetail.a2aTaskEnabled = false;
|
|
7614
9060
|
state.currentDetail.readOnly = true;
|
|
@@ -7616,7 +9062,7 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7616
9062
|
await refreshAuthenticatedState();
|
|
7617
9063
|
await renderShell();
|
|
7618
9064
|
} catch (error) {
|
|
7619
|
-
alert(`A2A task
|
|
9065
|
+
alert(`A2A task ${submittedAction} failed: ${error.message}`);
|
|
7620
9066
|
buttons.forEach((btn) => {
|
|
7621
9067
|
btn.disabled = false;
|
|
7622
9068
|
btn.classList.remove("is-loading", "is-dimmed");
|
|
@@ -7657,24 +9103,10 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7657
9103
|
if (textarea) textarea.readOnly = true;
|
|
7658
9104
|
const editedContent = normalizeClientText(new FormData(threadShareForm).get("shareContent"));
|
|
7659
9105
|
try {
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
body: JSON.stringify({ decision: submittedAction, editedContent }),
|
|
7665
|
-
});
|
|
7666
|
-
if (!res.ok) {
|
|
7667
|
-
const errBody = await res.json().catch(() => ({}));
|
|
7668
|
-
alert(`Thread share ${submittedAction} failed: ${errBody.error || res.status}`);
|
|
7669
|
-
buttons.forEach((btn) => {
|
|
7670
|
-
btn.disabled = false;
|
|
7671
|
-
btn.classList.remove("is-loading", "is-dimmed");
|
|
7672
|
-
btn.innerHTML = labelCache.get(btn);
|
|
7673
|
-
});
|
|
7674
|
-
if (textarea) textarea.readOnly = false;
|
|
7675
|
-
threadShareForm.dataset.submitting = "";
|
|
7676
|
-
return;
|
|
7677
|
-
}
|
|
9106
|
+
await apiPost(
|
|
9107
|
+
`/api/threads/share/${encodeURIComponent(token)}/decision`,
|
|
9108
|
+
{ decision: submittedAction, editedContent },
|
|
9109
|
+
);
|
|
7678
9110
|
if (state.currentDetail?.kind === "thread_share") {
|
|
7679
9111
|
state.currentDetail.threadShareEnabled = false;
|
|
7680
9112
|
state.currentDetail.readOnly = true;
|
|
@@ -7682,7 +9114,7 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7682
9114
|
await refreshAuthenticatedState();
|
|
7683
9115
|
await renderShell();
|
|
7684
9116
|
} catch (error) {
|
|
7685
|
-
alert(`Thread share
|
|
9117
|
+
alert(`Thread share ${submittedAction} failed: ${error.message}`);
|
|
7686
9118
|
buttons.forEach((btn) => {
|
|
7687
9119
|
btn.disabled = false;
|
|
7688
9120
|
btn.classList.remove("is-loading", "is-dimmed");
|
|
@@ -7741,14 +9173,22 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7741
9173
|
await renderShell();
|
|
7742
9174
|
|
|
7743
9175
|
try {
|
|
7744
|
-
const
|
|
7745
|
-
requestBody
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
9176
|
+
const attachments = (draft.attachments || []).filter((attachment) => attachment?.file);
|
|
9177
|
+
let requestBody;
|
|
9178
|
+
if (attachments.length > 0) {
|
|
9179
|
+
requestBody = new FormData();
|
|
9180
|
+
requestBody.set("text", text);
|
|
9181
|
+
requestBody.set("planMode", draft.mode === "plan" ? "true" : "false");
|
|
9182
|
+
requestBody.set("force", draft.confirmOverride === true ? "true" : "false");
|
|
9183
|
+
} else {
|
|
9184
|
+
requestBody = {
|
|
9185
|
+
text,
|
|
9186
|
+
planMode: draft.mode === "plan",
|
|
9187
|
+
force: draft.confirmOverride === true,
|
|
9188
|
+
};
|
|
9189
|
+
}
|
|
9190
|
+
for (const attachment of attachments) {
|
|
9191
|
+
requestBody.append("image", attachment.file, attachment.name || attachment.file.name);
|
|
7752
9192
|
}
|
|
7753
9193
|
const replyKind = replyForm.dataset.replyKind || "completion";
|
|
7754
9194
|
await apiPost(`/api/items/${encodeURIComponent(replyKind)}/${encodeURIComponent(token)}/reply`, requestBody);
|
|
@@ -7907,6 +9347,13 @@ function bindSharedUi(renderFn) {
|
|
|
7907
9347
|
});
|
|
7908
9348
|
}
|
|
7909
9349
|
|
|
9350
|
+
for (const button of document.querySelectorAll("[data-force-app-refresh]")) {
|
|
9351
|
+
button.addEventListener("click", async () => {
|
|
9352
|
+
await forceAppRefreshFromLan();
|
|
9353
|
+
await renderFn();
|
|
9354
|
+
});
|
|
9355
|
+
}
|
|
9356
|
+
|
|
7910
9357
|
for (const button of document.querySelectorAll("[data-install-guide-close]")) {
|
|
7911
9358
|
button.addEventListener("click", async (event) => {
|
|
7912
9359
|
if (button.classList.contains("modal-backdrop")) {
|
|
@@ -8574,6 +10021,10 @@ function renderIcon(name) {
|
|
|
8574
10021
|
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>`;
|
|
8575
10022
|
case "language":
|
|
8576
10023
|
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>`;
|
|
10024
|
+
case "agent-network":
|
|
10025
|
+
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>`;
|
|
10026
|
+
case "remote-connection":
|
|
10027
|
+
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>`;
|
|
8577
10028
|
case "link":
|
|
8578
10029
|
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>`;
|
|
8579
10030
|
case "clip":
|
|
@@ -8724,13 +10175,138 @@ function handleDocumentVisibilityChange() {
|
|
|
8724
10175
|
handlePotentialExternalNavigation();
|
|
8725
10176
|
}
|
|
8726
10177
|
|
|
8727
|
-
async function
|
|
8728
|
-
|
|
10178
|
+
async function hydrateTimelinePayloadImages(payload) {
|
|
10179
|
+
if (!payload || !Array.isArray(payload.entries)) {
|
|
10180
|
+
return payload;
|
|
10181
|
+
}
|
|
10182
|
+
|
|
10183
|
+
const entries = await Promise.all(
|
|
10184
|
+
payload.entries.map(async (entry) => hydrateItemImageUrls(entry))
|
|
10185
|
+
);
|
|
10186
|
+
return { ...payload, entries };
|
|
10187
|
+
}
|
|
10188
|
+
|
|
10189
|
+
async function hydrateDetailImages(detail) {
|
|
10190
|
+
if (!detail || !Array.isArray(detail.imageUrls)) {
|
|
10191
|
+
return detail;
|
|
10192
|
+
}
|
|
10193
|
+
return hydrateItemImageUrls(detail);
|
|
10194
|
+
}
|
|
10195
|
+
|
|
10196
|
+
async function hydrateItemImageUrls(item) {
|
|
10197
|
+
const imageUrls = Array.isArray(item?.imageUrls) ? item.imageUrls.filter(Boolean) : [];
|
|
10198
|
+
if (imageUrls.length === 0) {
|
|
10199
|
+
return item;
|
|
10200
|
+
}
|
|
10201
|
+
|
|
10202
|
+
const hydratedUrls = await Promise.all(imageUrls.map((url) => routedTimelineImageUrl(url)));
|
|
10203
|
+
return {
|
|
10204
|
+
...item,
|
|
10205
|
+
imageUrls: hydratedUrls.filter(Boolean),
|
|
10206
|
+
};
|
|
10207
|
+
}
|
|
10208
|
+
|
|
10209
|
+
async function routedTimelineImageUrl(imageUrl) {
|
|
10210
|
+
const sourceUrl = normalizeClientText(imageUrl);
|
|
10211
|
+
if (!sourceUrl || /^(?:blob:|data:)/iu.test(sourceUrl)) {
|
|
10212
|
+
return sourceUrl;
|
|
10213
|
+
}
|
|
10214
|
+
|
|
10215
|
+
const existing = timelineImageObjectUrlCache.get(sourceUrl);
|
|
10216
|
+
if (existing?.objectUrl) {
|
|
10217
|
+
existing.lastUsedMs = Date.now();
|
|
10218
|
+
return existing.objectUrl;
|
|
10219
|
+
}
|
|
10220
|
+
|
|
10221
|
+
try {
|
|
10222
|
+
const response = await routedFetch(sourceUrl, {
|
|
10223
|
+
credentials: "same-origin",
|
|
10224
|
+
headers: {
|
|
10225
|
+
Accept: "image/*",
|
|
10226
|
+
},
|
|
10227
|
+
});
|
|
10228
|
+
if (!response.ok) {
|
|
10229
|
+
return unavailableTimelineImageDataUrl();
|
|
10230
|
+
}
|
|
10231
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
10232
|
+
const contentType = responseHeader(response.headers, "content-type") || "application/octet-stream";
|
|
10233
|
+
const objectUrl = URL.createObjectURL(new Blob([arrayBuffer], { type: contentType }));
|
|
10234
|
+
timelineImageObjectUrlCache.set(sourceUrl, {
|
|
10235
|
+
objectUrl,
|
|
10236
|
+
lastUsedMs: Date.now(),
|
|
10237
|
+
});
|
|
10238
|
+
pruneTimelineImageObjectUrlCache();
|
|
10239
|
+
return objectUrl;
|
|
10240
|
+
} catch {
|
|
10241
|
+
return unavailableTimelineImageDataUrl();
|
|
10242
|
+
}
|
|
10243
|
+
}
|
|
10244
|
+
|
|
10245
|
+
function unavailableTimelineImageDataUrl() {
|
|
10246
|
+
const title = L("detail.imageUnavailable");
|
|
10247
|
+
const subtitle = L("detail.imageUnavailableHint");
|
|
10248
|
+
const svg = `
|
|
10249
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="640" viewBox="0 0 640 640">
|
|
10250
|
+
<defs>
|
|
10251
|
+
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
|
10252
|
+
<stop offset="0" stop-color="#132026"/>
|
|
10253
|
+
<stop offset="1" stop-color="#0a1116"/>
|
|
10254
|
+
</linearGradient>
|
|
10255
|
+
</defs>
|
|
10256
|
+
<rect width="640" height="640" rx="34" fill="url(#bg)"/>
|
|
10257
|
+
<rect x="24" y="24" width="592" height="592" rx="28" fill="none" stroke="#9cb5c5" stroke-opacity=".18" stroke-width="2"/>
|
|
10258
|
+
<circle cx="320" cy="266" r="50" fill="#26343d"/>
|
|
10259
|
+
<path d="M294 266h52M320 240v52" stroke="#d7e5ed" stroke-width="12" stroke-linecap="round" opacity=".78"/>
|
|
10260
|
+
<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>
|
|
10261
|
+
<text x="320" y="410" text-anchor="middle" fill="#9cb5c5" font-family="Avenir Next, Helvetica, Arial, sans-serif" font-size="22">${escapeSvgText(subtitle)}</text>
|
|
10262
|
+
</svg>
|
|
10263
|
+
`.trim();
|
|
10264
|
+
return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(svg)}`;
|
|
10265
|
+
}
|
|
10266
|
+
|
|
10267
|
+
function escapeSvgText(value) {
|
|
10268
|
+
return normalizeClientText(value)
|
|
10269
|
+
.replace(/&/gu, "&")
|
|
10270
|
+
.replace(/</gu, "<")
|
|
10271
|
+
.replace(/>/gu, ">");
|
|
10272
|
+
}
|
|
10273
|
+
|
|
10274
|
+
function responseHeader(headers, name) {
|
|
10275
|
+
const key = String(name || "").toLowerCase();
|
|
10276
|
+
if (!headers || !key) {
|
|
10277
|
+
return "";
|
|
10278
|
+
}
|
|
10279
|
+
if (typeof headers.get === "function") {
|
|
10280
|
+
return headers.get(key) || "";
|
|
10281
|
+
}
|
|
10282
|
+
return headers[key] || headers[name] || "";
|
|
10283
|
+
}
|
|
10284
|
+
|
|
10285
|
+
function pruneTimelineImageObjectUrlCache() {
|
|
10286
|
+
if (timelineImageObjectUrlCache.size <= MAX_TIMELINE_IMAGE_OBJECT_URLS) {
|
|
10287
|
+
return;
|
|
10288
|
+
}
|
|
10289
|
+
const staleEntries = [...timelineImageObjectUrlCache.entries()]
|
|
10290
|
+
.sort((left, right) => Number(left[1]?.lastUsedMs || 0) - Number(right[1]?.lastUsedMs || 0))
|
|
10291
|
+
.slice(0, Math.max(0, timelineImageObjectUrlCache.size - MAX_TIMELINE_IMAGE_OBJECT_URLS));
|
|
10292
|
+
for (const [sourceUrl, entry] of staleEntries) {
|
|
10293
|
+
if (entry?.objectUrl && typeof URL !== "undefined" && typeof URL.revokeObjectURL === "function") {
|
|
10294
|
+
URL.revokeObjectURL(entry.objectUrl);
|
|
10295
|
+
}
|
|
10296
|
+
timelineImageObjectUrlCache.delete(sourceUrl);
|
|
10297
|
+
}
|
|
10298
|
+
}
|
|
10299
|
+
|
|
10300
|
+
async function apiGet(url, opts = {}) {
|
|
10301
|
+
// routedFetch tries LAN first, then falls back to the relay tunnel when
|
|
10302
|
+
// the phone is off-LAN. Returns a fetch-Response-compatible object so the
|
|
10303
|
+
// rest of this function is identical to a plain `fetch()` call.
|
|
10304
|
+
const response = await routedFetch(url, {
|
|
8729
10305
|
credentials: "same-origin",
|
|
8730
10306
|
headers: {
|
|
8731
10307
|
Accept: "application/json",
|
|
8732
10308
|
},
|
|
8733
|
-
});
|
|
10309
|
+
}, opts);
|
|
8734
10310
|
if (!response.ok) {
|
|
8735
10311
|
const errorInfo = await readError(response);
|
|
8736
10312
|
const error = new Error(errorInfo.message);
|
|
@@ -8744,7 +10320,9 @@ async function apiGet(url) {
|
|
|
8744
10320
|
|
|
8745
10321
|
async function apiPost(url, body) {
|
|
8746
10322
|
const isFormDataBody = typeof FormData !== "undefined" && body instanceof FormData;
|
|
8747
|
-
|
|
10323
|
+
// Keep native FormData for LAN; routedFetch serializes it to multipart
|
|
10324
|
+
// bytes only when the request has to travel through the remote relay.
|
|
10325
|
+
const response = await routedFetch(url, {
|
|
8748
10326
|
method: "POST",
|
|
8749
10327
|
credentials: "same-origin",
|
|
8750
10328
|
headers: isFormDataBody
|