viveworker 0.7.0 → 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 +1324 -103
- package/scripts/viveworker.mjs +27 -6
- package/web/app.css +634 -9
- package/web/app.js +1731 -187
- package/web/i18n.js +187 -9
- package/web/index.html +32 -2
- package/web/remote-pairing/api-router.js +873 -0
- package/web/remote-pairing/keys.js +237 -0
- package/web/remote-pairing/pairing-state.js +313 -0
- package/web/remote-pairing/rpc-client.js +765 -0
- package/web/remote-pairing/transport.js +804 -0
- package/web/remote-pairing/wake.js +149 -0
- package/web/remote-pairing-test.html +400 -0
- package/web/remote-pairing.bundle.js +3 -0
- package/web/remote-pairing.bundle.js.LEGAL.txt +15 -0
- package/web/remote-pairing.bundle.js.map +7 -0
- package/web/sw.js +190 -20
package/web/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
|
};
|
|
@@ -133,12 +178,150 @@ function hazbasePasskeyHostSupport() {
|
|
|
133
178
|
|
|
134
179
|
const app = document.querySelector("#app");
|
|
135
180
|
let bootSplashDismissed = false;
|
|
181
|
+
let bootSplashHintTimer = null;
|
|
182
|
+
let bootSplashHintVisible = false;
|
|
183
|
+
let bootSplashDeferredStatusTimer = null;
|
|
184
|
+
let bootSplashRemoteRouteSeen = false;
|
|
185
|
+
let bootSplashRemoteSwitchingShownAtMs = 0;
|
|
186
|
+
let bootSplashStatusStage = BOOT_SPLASH_STAGE.initial;
|
|
187
|
+
let bootSplashPendingStatusStage = BOOT_SPLASH_STAGE.initial;
|
|
188
|
+
const bootTraceStartedAtMs = Date.now();
|
|
189
|
+
const bootTraceStartPerfMs = bootTraceNow();
|
|
190
|
+
const bootTraceId = makeBootTraceId();
|
|
191
|
+
let bootTraceEvents = [];
|
|
192
|
+
let bootTraceClosed = false;
|
|
193
|
+
let bootTraceSent = false;
|
|
194
|
+
|
|
195
|
+
if (typeof window !== "undefined") {
|
|
196
|
+
window.__viveworkerAppBuild = APP_BUILD_ID;
|
|
197
|
+
window.__viveworkerBootTraceId = bootTraceId;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function bootTraceNow() {
|
|
201
|
+
return typeof performance !== "undefined" && typeof performance.now === "function"
|
|
202
|
+
? performance.now()
|
|
203
|
+
: Date.now();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function makeBootTraceId() {
|
|
207
|
+
const randomPart = Math.random().toString(36).slice(2, 10);
|
|
208
|
+
return `${Date.now().toString(36)}-${randomPart}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function sanitizeBootTraceValue(value) {
|
|
212
|
+
if (value === null || value === undefined) {
|
|
213
|
+
return "";
|
|
214
|
+
}
|
|
215
|
+
return String(value).slice(0, BOOT_TRACE_MAX_VALUE_LENGTH);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function sanitizeBootTraceUrl(value) {
|
|
219
|
+
const raw = sanitizeBootTraceValue(value);
|
|
220
|
+
if (!raw) {
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
const base = window.location?.origin || "https://localhost";
|
|
225
|
+
const url = new URL(raw, base);
|
|
226
|
+
return url.pathname;
|
|
227
|
+
} catch {
|
|
228
|
+
return raw.split("?")[0].slice(0, BOOT_TRACE_MAX_VALUE_LENGTH);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function recordBootTraceEvent(type, detail = {}) {
|
|
233
|
+
if (bootTraceClosed) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const event = {
|
|
237
|
+
type: sanitizeBootTraceValue(type),
|
|
238
|
+
tMs: Math.max(0, Math.round(bootTraceNow() - bootTraceStartPerfMs)),
|
|
239
|
+
};
|
|
240
|
+
const phase = sanitizeBootTraceValue(detail.phase);
|
|
241
|
+
const url = sanitizeBootTraceUrl(detail.url);
|
|
242
|
+
const stateValue = sanitizeBootTraceValue(detail.state);
|
|
243
|
+
const previousState = sanitizeBootTraceValue(detail.previousState);
|
|
244
|
+
const reason = sanitizeBootTraceValue(detail.reason);
|
|
245
|
+
if (phase) event.phase = phase;
|
|
246
|
+
if (url) event.url = url;
|
|
247
|
+
if (stateValue) event.state = stateValue;
|
|
248
|
+
if (previousState) event.previousState = previousState;
|
|
249
|
+
if (reason) event.reason = reason;
|
|
250
|
+
if (detail.sticky === true || detail.sticky === false) event.sticky = detail.sticky;
|
|
251
|
+
if (Number.isFinite(Number(detail.code))) event.code = Number(detail.code);
|
|
252
|
+
if (detail.resumed === true || detail.resumed === false) event.resumed = detail.resumed;
|
|
253
|
+
bootTraceEvents.push(event);
|
|
254
|
+
if (bootTraceEvents.length > BOOT_TRACE_MAX_EVENTS) {
|
|
255
|
+
bootTraceEvents = bootTraceEvents.slice(-BOOT_TRACE_MAX_EVENTS);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function flushBootTrace(reason, extra = {}) {
|
|
260
|
+
if (bootTraceSent) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
bootTraceSent = true;
|
|
264
|
+
bootTraceClosed = true;
|
|
265
|
+
const payload = {
|
|
266
|
+
traceId: bootTraceId,
|
|
267
|
+
reason: sanitizeBootTraceValue(reason),
|
|
268
|
+
appBuildId: APP_BUILD_ID,
|
|
269
|
+
locale: state.locale || DEFAULT_LOCALE,
|
|
270
|
+
startedAtMs: bootTraceStartedAtMs,
|
|
271
|
+
totalMs: Math.max(0, Math.round(bootTraceNow() - bootTraceStartPerfMs)),
|
|
272
|
+
remoteRouteSeen: bootSplashRemoteRouteSeen,
|
|
273
|
+
finalStage: bootSplashStatusStage,
|
|
274
|
+
userAgent: navigator.userAgent || "",
|
|
275
|
+
events: bootTraceEvents,
|
|
276
|
+
...extra,
|
|
277
|
+
};
|
|
278
|
+
queueMicrotask(() => {
|
|
279
|
+
routedFetch("/api/remote-pairing/boot-trace", {
|
|
280
|
+
method: "POST",
|
|
281
|
+
credentials: "same-origin",
|
|
282
|
+
headers: {
|
|
283
|
+
"Content-Type": "application/json",
|
|
284
|
+
Accept: "application/json",
|
|
285
|
+
},
|
|
286
|
+
body: JSON.stringify(payload),
|
|
287
|
+
}, { suppressRoutingStatus: true }).catch(() => {});
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function syncVisualViewportMetrics() {
|
|
292
|
+
if (typeof document === "undefined") {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const root = document.documentElement;
|
|
296
|
+
const viewport = window.visualViewport;
|
|
297
|
+
const width = viewport?.width || window.innerWidth || root.clientWidth || 0;
|
|
298
|
+
const left = viewport?.offsetLeft || 0;
|
|
299
|
+
root.style.setProperty("--visual-viewport-width", `${Math.max(0, width)}px`);
|
|
300
|
+
root.style.setProperty("--visual-viewport-left", `${Math.max(0, left)}px`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function resetHorizontalViewportScroll() {
|
|
304
|
+
syncVisualViewportMetrics();
|
|
305
|
+
const scrollingElement = document.scrollingElement || document.documentElement;
|
|
306
|
+
if (scrollingElement) {
|
|
307
|
+
scrollingElement.scrollLeft = 0;
|
|
308
|
+
}
|
|
309
|
+
document.documentElement.scrollLeft = 0;
|
|
310
|
+
document.body.scrollLeft = 0;
|
|
311
|
+
syncVisualViewportMetrics();
|
|
312
|
+
window.requestAnimationFrame?.(syncVisualViewportMetrics);
|
|
313
|
+
}
|
|
136
314
|
|
|
137
315
|
function dismissBootSplash() {
|
|
138
316
|
if (bootSplashDismissed || typeof document === "undefined") {
|
|
139
317
|
return;
|
|
140
318
|
}
|
|
141
319
|
bootSplashDismissed = true;
|
|
320
|
+
clearBootSplashHintTimer();
|
|
321
|
+
clearBootSplashDeferredStatusTimer();
|
|
322
|
+
if (typeof window !== "undefined") {
|
|
323
|
+
window.removeEventListener("viveworker:remote-routing-status", handleBootRoutingStatus);
|
|
324
|
+
}
|
|
142
325
|
const splash = document.querySelector("#boot-splash");
|
|
143
326
|
document.body?.classList.add("viveworker-ready");
|
|
144
327
|
if (!splash) {
|
|
@@ -149,6 +332,147 @@ function dismissBootSplash() {
|
|
|
149
332
|
splash.remove();
|
|
150
333
|
}, 280);
|
|
151
334
|
}
|
|
335
|
+
|
|
336
|
+
function setBootSplashStatus(key, stage = bootSplashStatusStage) {
|
|
337
|
+
if (bootSplashDismissed || typeof document === "undefined") {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
if (stage < bootSplashStatusStage) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
const message = L(key);
|
|
344
|
+
const status = document.querySelector("#boot-splash-status");
|
|
345
|
+
const splash = document.querySelector("#boot-splash");
|
|
346
|
+
if (status && status.textContent !== message) {
|
|
347
|
+
status.textContent = message;
|
|
348
|
+
}
|
|
349
|
+
if (splash) {
|
|
350
|
+
splash.setAttribute("aria-label", `${L("common.appName")} ${message}`);
|
|
351
|
+
}
|
|
352
|
+
if (bootSplashRemoteRouteSeen) {
|
|
353
|
+
ensureBootSplashHintTimer();
|
|
354
|
+
}
|
|
355
|
+
bootSplashStatusStage = Math.max(bootSplashStatusStage, stage);
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function ensureBootSplashHintTimer() {
|
|
360
|
+
if (bootSplashHintTimer != null || bootSplashHintVisible || bootSplashDismissed) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
bootSplashHintTimer = window.setTimeout(() => {
|
|
364
|
+
bootSplashHintTimer = null;
|
|
365
|
+
showBootSplashHint();
|
|
366
|
+
}, BOOT_SPLASH_SLOW_HINT_MS);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function clearBootSplashHintTimer() {
|
|
370
|
+
if (bootSplashHintTimer != null) {
|
|
371
|
+
window.clearTimeout(bootSplashHintTimer);
|
|
372
|
+
bootSplashHintTimer = null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function clearBootSplashDeferredStatusTimer() {
|
|
377
|
+
if (bootSplashDeferredStatusTimer != null) {
|
|
378
|
+
window.clearTimeout(bootSplashDeferredStatusTimer);
|
|
379
|
+
bootSplashDeferredStatusTimer = null;
|
|
380
|
+
}
|
|
381
|
+
bootSplashPendingStatusStage = BOOT_SPLASH_STAGE.initial;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function showBootSplashHint() {
|
|
385
|
+
if (bootSplashDismissed || bootSplashHintVisible || typeof document === "undefined") {
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const hint = document.querySelector("#boot-splash-hint");
|
|
389
|
+
if (!hint) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
bootSplashHintVisible = true;
|
|
393
|
+
hint.textContent = L("boot.status.slowHint");
|
|
394
|
+
hint.hidden = false;
|
|
395
|
+
if (typeof window.requestAnimationFrame === "function") {
|
|
396
|
+
window.requestAnimationFrame(() => {
|
|
397
|
+
hint.classList.add("is-visible");
|
|
398
|
+
});
|
|
399
|
+
} else {
|
|
400
|
+
hint.classList.add("is-visible");
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function bootSplashNow() {
|
|
405
|
+
return typeof performance !== "undefined" && typeof performance.now === "function"
|
|
406
|
+
? performance.now()
|
|
407
|
+
: Date.now();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function showBootRemoteSwitchingStatus() {
|
|
411
|
+
bootSplashRemoteRouteSeen = true;
|
|
412
|
+
const effectiveStage = Math.max(bootSplashStatusStage, bootSplashPendingStatusStage);
|
|
413
|
+
if (effectiveStage >= BOOT_SPLASH_STAGE.switching) {
|
|
414
|
+
ensureBootSplashHintTimer();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
bootSplashRemoteSwitchingShownAtMs = bootSplashNow();
|
|
418
|
+
clearBootSplashDeferredStatusTimer();
|
|
419
|
+
setBootSplashStatus("boot.status.switchingRemote", BOOT_SPLASH_STAGE.switching);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function setBootRemoteStatusAfterSwitching(key, stage) {
|
|
423
|
+
bootSplashRemoteRouteSeen = true;
|
|
424
|
+
const effectiveStage = Math.max(bootSplashStatusStage, bootSplashPendingStatusStage);
|
|
425
|
+
if (stage <= effectiveStage) {
|
|
426
|
+
ensureBootSplashHintTimer();
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (bootSplashStatusStage < BOOT_SPLASH_STAGE.switching) {
|
|
430
|
+
showBootRemoteSwitchingStatus();
|
|
431
|
+
}
|
|
432
|
+
const elapsedMs = bootSplashRemoteSwitchingShownAtMs > 0
|
|
433
|
+
? bootSplashNow() - bootSplashRemoteSwitchingShownAtMs
|
|
434
|
+
: BOOT_SPLASH_REMOTE_SWITCHING_MIN_MS;
|
|
435
|
+
const delayMs = Math.max(0, BOOT_SPLASH_REMOTE_SWITCHING_MIN_MS - elapsedMs);
|
|
436
|
+
clearBootSplashDeferredStatusTimer();
|
|
437
|
+
if (delayMs <= 0) {
|
|
438
|
+
setBootSplashStatus(key, stage);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
bootSplashDeferredStatusTimer = window.setTimeout(() => {
|
|
442
|
+
bootSplashDeferredStatusTimer = null;
|
|
443
|
+
bootSplashPendingStatusStage = BOOT_SPLASH_STAGE.initial;
|
|
444
|
+
setBootSplashStatus(key, stage);
|
|
445
|
+
}, delayMs);
|
|
446
|
+
bootSplashPendingStatusStage = stage;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function handleBootRoutingStatus(event) {
|
|
450
|
+
if (bootSplashDismissed) {
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
recordBootTraceEvent("route", event?.detail || {});
|
|
454
|
+
const phase = event?.detail?.phase || "";
|
|
455
|
+
switch (phase) {
|
|
456
|
+
case "lan-checking":
|
|
457
|
+
setBootSplashStatus("boot.status.checkingLan", BOOT_SPLASH_STAGE.checking);
|
|
458
|
+
break;
|
|
459
|
+
case "lan-failed":
|
|
460
|
+
case "remote-switching":
|
|
461
|
+
showBootRemoteSwitchingStatus();
|
|
462
|
+
break;
|
|
463
|
+
case "remote-connecting":
|
|
464
|
+
setBootRemoteStatusAfterSwitching("boot.status.establishingRemote", BOOT_SPLASH_STAGE.establishing);
|
|
465
|
+
break;
|
|
466
|
+
case "lan-connected":
|
|
467
|
+
// Same-LAN startup is fast and already covered by the checking message.
|
|
468
|
+
break;
|
|
469
|
+
case "remote-connected":
|
|
470
|
+
setBootRemoteStatusAfterSwitching("boot.status.loadingData", BOOT_SPLASH_STAGE.loading);
|
|
471
|
+
break;
|
|
472
|
+
default:
|
|
473
|
+
break;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
152
476
|
const params = new URLSearchParams(window.location.search);
|
|
153
477
|
const initialItem = params.get("item") || "";
|
|
154
478
|
const initialTargetTab = params.get("tab") || "";
|
|
@@ -159,10 +483,13 @@ let didReloadForServiceWorker = false;
|
|
|
159
483
|
let lastViewportMode = isDesktopLayout();
|
|
160
484
|
|
|
161
485
|
boot().catch((error) => {
|
|
162
|
-
const message =
|
|
163
|
-
const hint =
|
|
486
|
+
const message = bootErrorMessage(error);
|
|
487
|
+
const hint = shouldShowNetworkHint(error, message)
|
|
164
488
|
? `<p class="muted">${escapeHtml(L("error.networkHint"))}</p>`
|
|
165
489
|
: "";
|
|
490
|
+
flushBootTrace("boot-error", {
|
|
491
|
+
error: sanitizeBootTraceValue(error?.code || error?.name || message),
|
|
492
|
+
});
|
|
166
493
|
dismissBootSplash();
|
|
167
494
|
app.innerHTML = `
|
|
168
495
|
<main class="onboarding-shell">
|
|
@@ -176,13 +503,95 @@ boot().catch((error) => {
|
|
|
176
503
|
`;
|
|
177
504
|
});
|
|
178
505
|
|
|
506
|
+
function bootErrorMessage(error) {
|
|
507
|
+
if (isRemotePairingEnrollmentRequired(error)) {
|
|
508
|
+
return L("error.remotePairingNeedsLanRefresh");
|
|
509
|
+
}
|
|
510
|
+
if (isRemotePairingUnavailable(error)) {
|
|
511
|
+
return L("error.remotePairingUnavailable");
|
|
512
|
+
}
|
|
513
|
+
return error?.message || String(error);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function shouldShowNetworkHint(error, message) {
|
|
517
|
+
if (isRemotePairingEnrollmentRequired(error) || isRemotePairingUnavailable(error)) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
return /Load failed|Failed to fetch|NetworkError|fetch/i.test(message);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function isRemotePairingEnrollmentRequired(error) {
|
|
524
|
+
return error?.code === "remote-pairing-enrollment-required" ||
|
|
525
|
+
error?.name === "RemotePairingEnrollmentRequiredError";
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function isRemotePairingUnavailable(error) {
|
|
529
|
+
return error?.code === "remote-pairing-unavailable" ||
|
|
530
|
+
error?.code === "remote-pairing-unreachable" ||
|
|
531
|
+
error?.name === "RemotePairingUnavailableError" ||
|
|
532
|
+
error?.name === "RpcTimeoutError" ||
|
|
533
|
+
error?.name === "RpcTransportError" ||
|
|
534
|
+
error?.name === "RpcTransportFailedError";
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function inspectRemotePairingStateForEnrollment() {
|
|
538
|
+
let store;
|
|
539
|
+
try {
|
|
540
|
+
store = globalThis.localStorage ?? null;
|
|
541
|
+
} catch {
|
|
542
|
+
return { status: "storage-unavailable", needsEnrollment: false };
|
|
543
|
+
}
|
|
544
|
+
if (!store) {
|
|
545
|
+
return { status: "storage-unavailable", needsEnrollment: false };
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
let raw;
|
|
549
|
+
try {
|
|
550
|
+
raw = store.getItem(REMOTE_PAIRING_STATE_STORAGE_KEY);
|
|
551
|
+
} catch {
|
|
552
|
+
return { status: "storage-unavailable", needsEnrollment: false };
|
|
553
|
+
}
|
|
554
|
+
if (!raw) {
|
|
555
|
+
return { status: "missing", needsEnrollment: true };
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
let parsed;
|
|
559
|
+
try {
|
|
560
|
+
parsed = JSON.parse(raw);
|
|
561
|
+
} catch {
|
|
562
|
+
return { status: "malformed", needsEnrollment: true };
|
|
563
|
+
}
|
|
564
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
565
|
+
return { status: "malformed", needsEnrollment: true };
|
|
566
|
+
}
|
|
567
|
+
if (parsed.version === REMOTE_PAIRING_STATE_LEGACY_SCHEMA_VERSION) {
|
|
568
|
+
return { status: "legacy-v1", needsEnrollment: true };
|
|
569
|
+
}
|
|
570
|
+
if (parsed.version !== REMOTE_PAIRING_STATE_SCHEMA_VERSION) {
|
|
571
|
+
return { status: "unsupported-version", needsEnrollment: true };
|
|
572
|
+
}
|
|
573
|
+
if (typeof parsed.relayToken !== "string" || parsed.relayToken.length === 0) {
|
|
574
|
+
return { status: "missing-token", needsEnrollment: true };
|
|
575
|
+
}
|
|
576
|
+
return { status: "ready", needsEnrollment: false };
|
|
577
|
+
}
|
|
578
|
+
|
|
179
579
|
async function boot() {
|
|
580
|
+
recordBootTraceEvent("boot-start", {
|
|
581
|
+
url: window.location?.pathname || "/app",
|
|
582
|
+
});
|
|
180
583
|
updateManifestHref(initialPairToken);
|
|
584
|
+
syncVisualViewportMetrics();
|
|
585
|
+
setBootSplashStatus("boot.status.checkingLan", BOOT_SPLASH_STAGE.checking);
|
|
181
586
|
// SW register + update() can take hundreds of ms and does not need to gate
|
|
182
587
|
// first paint. Fire and forget; the `controllerchange` reload handler wired
|
|
183
588
|
// up inside `registerServiceWorker` still picks up new versions.
|
|
184
589
|
registerServiceWorker().catch(() => {});
|
|
590
|
+
window.addEventListener("viveworker:remote-routing-status", handleBootRoutingStatus);
|
|
185
591
|
navigator.serviceWorker?.addEventListener("message", handleServiceWorkerMessage);
|
|
592
|
+
window.addEventListener("resize", syncVisualViewportMetrics, { passive: true });
|
|
593
|
+
window.visualViewport?.addEventListener("resize", syncVisualViewportMetrics, { passive: true });
|
|
594
|
+
window.visualViewport?.addEventListener("scroll", syncVisualViewportMetrics, { passive: true });
|
|
186
595
|
window.addEventListener("resize", handleViewportChange, { passive: true });
|
|
187
596
|
window.addEventListener("focus", handlePotentialExternalNavigation, { passive: true });
|
|
188
597
|
window.addEventListener("pageshow", handlePotentialExternalNavigation, { passive: true });
|
|
@@ -191,13 +600,24 @@ async function boot() {
|
|
|
191
600
|
// Single round-trip for session + inbox(pending/completed) + timeline +
|
|
192
601
|
// devices. See `refreshBootstrap` for why we collapsed the boot fan-out.
|
|
193
602
|
await refreshBootstrap();
|
|
603
|
+
if (bootSplashRemoteRouteSeen) {
|
|
604
|
+
setBootRemoteStatusAfterSwitching("boot.status.loadingData", BOOT_SPLASH_STAGE.loading);
|
|
605
|
+
}
|
|
606
|
+
flushBootTrace("bootstrap-complete");
|
|
194
607
|
|
|
195
608
|
if (!state.session?.authenticated && initialPairToken && shouldAutoPairFromBootstrapToken()) {
|
|
196
609
|
try {
|
|
197
|
-
await pair({
|
|
610
|
+
const pairResult = await pair({
|
|
198
611
|
token: initialPairToken,
|
|
199
612
|
temporary: shouldUseTemporaryBootstrapPairing(),
|
|
200
613
|
});
|
|
614
|
+
// Mirror the manual #pair-form path: register with the bridge so
|
|
615
|
+
// off-LAN reconnect via the relay works without a second pair step.
|
|
616
|
+
// Skipped when the bridge graced this tab a temporary session
|
|
617
|
+
// (Safari non-PWA bootstrap), since those don't survive the tab.
|
|
618
|
+
if (pairResult?.temporaryPairing !== true) {
|
|
619
|
+
await enrollRemotePairing();
|
|
620
|
+
}
|
|
201
621
|
} catch (error) {
|
|
202
622
|
state.pairError = error.message || String(error);
|
|
203
623
|
}
|
|
@@ -229,6 +649,7 @@ async function boot() {
|
|
|
229
649
|
return;
|
|
230
650
|
}
|
|
231
651
|
|
|
652
|
+
await maybeAutoEnrollRemotePairingFromLan();
|
|
232
653
|
await consumePendingNotificationIntent();
|
|
233
654
|
// `?focusPending=claude` marks this tab as the Claude-hook-opened popup:
|
|
234
655
|
// auto-navigate to the newest unresolved Claude pending (plan/question)
|
|
@@ -306,6 +727,7 @@ async function boot() {
|
|
|
306
727
|
refreshInboxDiff(),
|
|
307
728
|
fetchMoltbookScoutStatus(),
|
|
308
729
|
fetchA2aShareStatus(),
|
|
730
|
+
fetchRemotePairingStatus(),
|
|
309
731
|
])
|
|
310
732
|
.then(async () => {
|
|
311
733
|
if (!shouldDeferRenderForActiveInteraction()) {
|
|
@@ -316,6 +738,49 @@ async function boot() {
|
|
|
316
738
|
}, 3000);
|
|
317
739
|
}
|
|
318
740
|
|
|
741
|
+
async function maybeAutoEnrollRemotePairingFromLan() {
|
|
742
|
+
if (!state.session?.authenticated) {
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
const pairingState = inspectRemotePairingStateForEnrollment();
|
|
746
|
+
const telemetry = getRoutingTelemetry();
|
|
747
|
+
if (!telemetry || telemetry.lanOk <= 0) {
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
if (!pairingState.needsEnrollment) {
|
|
751
|
+
const record = pairingState.record || loadRemotePairingState();
|
|
752
|
+
if (!shouldRefreshRemotePairingTokenFromLan(record)) {
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return enrollRemotePairing();
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function shouldRefreshRemotePairingTokenFromLan(record) {
|
|
760
|
+
if (!record) {
|
|
761
|
+
return false;
|
|
762
|
+
}
|
|
763
|
+
const updatedAt = Number(record.relayTokenUpdatedAtMs) || 0;
|
|
764
|
+
if (!updatedAt) {
|
|
765
|
+
return true;
|
|
766
|
+
}
|
|
767
|
+
return Date.now() - updatedAt >= REMOTE_PAIRING_TOKEN_REFRESH_MS;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function isRemotePairingUsingRelay() {
|
|
771
|
+
const telemetry = getRoutingTelemetry();
|
|
772
|
+
if (!telemetry) {
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
if (telemetry.lastRoute === "lan") {
|
|
776
|
+
return false;
|
|
777
|
+
}
|
|
778
|
+
if (telemetry.lastRoute === "relay") {
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
return telemetry.relayOk > 0 && Number(telemetry.stickyRelayUntilMs || 0) > Date.now();
|
|
782
|
+
}
|
|
783
|
+
|
|
319
784
|
async function registerServiceWorker() {
|
|
320
785
|
if (!("serviceWorker" in navigator)) {
|
|
321
786
|
return;
|
|
@@ -335,6 +800,47 @@ async function registerServiceWorker() {
|
|
|
335
800
|
}
|
|
336
801
|
}
|
|
337
802
|
|
|
803
|
+
async function forceAppRefreshFromLan() {
|
|
804
|
+
try {
|
|
805
|
+
const healthInit = {
|
|
806
|
+
cache: "no-store",
|
|
807
|
+
credentials: "same-origin",
|
|
808
|
+
headers: { Accept: "application/json" },
|
|
809
|
+
};
|
|
810
|
+
if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
|
|
811
|
+
healthInit.signal = AbortSignal.timeout(2500);
|
|
812
|
+
}
|
|
813
|
+
await fetch("/health", healthInit);
|
|
814
|
+
} catch {
|
|
815
|
+
state.pushError = L("error.clientUpdateNeedsLan");
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
try {
|
|
820
|
+
const registration = state.serviceWorkerRegistration || await navigator.serviceWorker?.getRegistration?.();
|
|
821
|
+
await registration?.update?.();
|
|
822
|
+
} catch {
|
|
823
|
+
// Reload below is still useful; the next boot can retry SW update.
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
try {
|
|
827
|
+
if (typeof caches !== "undefined") {
|
|
828
|
+
const keys = await caches.keys();
|
|
829
|
+
await Promise.all(
|
|
830
|
+
keys
|
|
831
|
+
.filter((key) => /^viveworker-v/.test(key))
|
|
832
|
+
.map((key) => caches.delete(key))
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
} catch {
|
|
836
|
+
// Cache deletion is best-effort; network-first app routes still help.
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const nextUrl = new URL(window.location.href);
|
|
840
|
+
nextUrl.searchParams.set("appRefresh", String(Date.now()));
|
|
841
|
+
window.location.replace(nextUrl.toString());
|
|
842
|
+
}
|
|
843
|
+
|
|
338
844
|
function handleViewportChange() {
|
|
339
845
|
const nextViewportMode = isDesktopLayout();
|
|
340
846
|
if (nextViewportMode === lastViewportMode) {
|
|
@@ -364,6 +870,7 @@ async function refreshAuthenticatedState() {
|
|
|
364
870
|
await fetchMoltbookScoutStatus();
|
|
365
871
|
await fetchA2aRelayStatus();
|
|
366
872
|
await fetchA2aShareStatus();
|
|
873
|
+
await fetchRemotePairingStatus();
|
|
367
874
|
if (state.currentTab === "settings") {
|
|
368
875
|
await fetchHazbaseStatus();
|
|
369
876
|
}
|
|
@@ -386,11 +893,13 @@ async function refreshAuthenticatedStateRemote() {
|
|
|
386
893
|
fetchMoltbookScoutStatus(),
|
|
387
894
|
fetchA2aRelayStatus(),
|
|
388
895
|
fetchA2aShareStatus(),
|
|
896
|
+
fetchRemotePairingStatus(),
|
|
389
897
|
]);
|
|
390
898
|
}
|
|
391
899
|
|
|
392
900
|
async function refreshSession() {
|
|
393
901
|
state.session = await apiGet("/api/session");
|
|
902
|
+
applyServerAppBuildId(state.session?.webAppBuildId);
|
|
394
903
|
syncPairingTokenState(desiredBootstrapPairingToken());
|
|
395
904
|
applyResolvedLocale();
|
|
396
905
|
}
|
|
@@ -402,8 +911,14 @@ async function refreshSession() {
|
|
|
402
911
|
// iOS PWAs where connection reuse is aggressive. Leaves the diff and
|
|
403
912
|
// external-status probes as separate background phases in `boot()`.
|
|
404
913
|
async function refreshBootstrap() {
|
|
405
|
-
|
|
914
|
+
recordBootTraceEvent("bootstrap-start", { url: "/api/bootstrap" });
|
|
915
|
+
const bootstrap = await apiGet("/api/bootstrap", {
|
|
916
|
+
timeoutMs: BOOTSTRAP_REMOTE_TIMEOUT_MS,
|
|
917
|
+
preferRelayError: true,
|
|
918
|
+
});
|
|
919
|
+
recordBootTraceEvent("bootstrap-response", { url: "/api/bootstrap" });
|
|
406
920
|
state.session = bootstrap?.session || null;
|
|
921
|
+
applyServerAppBuildId(bootstrap?.appBuildId || state.session?.webAppBuildId);
|
|
407
922
|
syncPairingTokenState(desiredBootstrapPairingToken());
|
|
408
923
|
applyResolvedLocale();
|
|
409
924
|
|
|
@@ -424,7 +939,7 @@ async function refreshBootstrap() {
|
|
|
424
939
|
syncCompletedThreadFilter();
|
|
425
940
|
syncInboxSubtab();
|
|
426
941
|
|
|
427
|
-
state.timeline = bootstrap?.timeline || null;
|
|
942
|
+
state.timeline = await hydrateTimelinePayloadImages(bootstrap?.timeline || null);
|
|
428
943
|
syncTimelineThreadFilter();
|
|
429
944
|
syncTimelineKindFilter();
|
|
430
945
|
|
|
@@ -433,6 +948,12 @@ async function refreshBootstrap() {
|
|
|
433
948
|
state.deviceError = "";
|
|
434
949
|
}
|
|
435
950
|
|
|
951
|
+
function applyServerAppBuildId(value) {
|
|
952
|
+
const buildId = normalizeClientText(value);
|
|
953
|
+
state.serverAppBuildId = buildId;
|
|
954
|
+
state.clientUpdateRequired = Boolean(buildId && buildId !== APP_BUILD_ID);
|
|
955
|
+
}
|
|
956
|
+
|
|
436
957
|
async function syncDetectedLocalePreference() {
|
|
437
958
|
if (!state.session?.authenticated || !state.session?.deviceId || !state.detectedLocale) {
|
|
438
959
|
return;
|
|
@@ -529,9 +1050,10 @@ function detectBrowserLocale() {
|
|
|
529
1050
|
|
|
530
1051
|
async function refreshPushStatus() {
|
|
531
1052
|
const client = await getClientPushState();
|
|
1053
|
+
const { clientSubscription, ...clientStatus } = client;
|
|
532
1054
|
if (!state.session?.authenticated) {
|
|
533
1055
|
state.pushStatus = {
|
|
534
|
-
...
|
|
1056
|
+
...clientStatus,
|
|
535
1057
|
enabled: false,
|
|
536
1058
|
subscribed: false,
|
|
537
1059
|
serverSubscribed: false,
|
|
@@ -542,16 +1064,34 @@ async function refreshPushStatus() {
|
|
|
542
1064
|
}
|
|
543
1065
|
|
|
544
1066
|
try {
|
|
545
|
-
|
|
1067
|
+
let server = await apiGet("/api/push/status");
|
|
1068
|
+
if (
|
|
1069
|
+
server?.enabled === true &&
|
|
1070
|
+
server?.subscribed !== true &&
|
|
1071
|
+
clientSubscription &&
|
|
1072
|
+
clientStatus.notificationPermission === "granted"
|
|
1073
|
+
) {
|
|
1074
|
+
try {
|
|
1075
|
+
await apiPost("/api/push/subscribe", {
|
|
1076
|
+
subscription: clientSubscription,
|
|
1077
|
+
userAgent: navigator.userAgent,
|
|
1078
|
+
standalone: isStandaloneMode(),
|
|
1079
|
+
});
|
|
1080
|
+
server = await apiGet("/api/push/status");
|
|
1081
|
+
} catch {
|
|
1082
|
+
// Best effort: if the browser still has a local subscription, the
|
|
1083
|
+
// status row can reflect that while the enable action repairs it.
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
546
1086
|
state.pushStatus = {
|
|
547
1087
|
...server,
|
|
548
|
-
...
|
|
1088
|
+
...clientStatus,
|
|
549
1089
|
serverSubscribed: Boolean(server.subscribed),
|
|
550
|
-
subscribed: Boolean(server.subscribed ||
|
|
1090
|
+
subscribed: Boolean(server.subscribed || clientStatus.clientSubscribed),
|
|
551
1091
|
};
|
|
552
1092
|
} catch (error) {
|
|
553
1093
|
state.pushStatus = {
|
|
554
|
-
...
|
|
1094
|
+
...clientStatus,
|
|
555
1095
|
enabled: false,
|
|
556
1096
|
subscribed: false,
|
|
557
1097
|
serverSubscribed: false,
|
|
@@ -586,6 +1126,18 @@ async function fetchA2aRelayStatus() {
|
|
|
586
1126
|
}
|
|
587
1127
|
}
|
|
588
1128
|
|
|
1129
|
+
async function fetchRemotePairingStatus() {
|
|
1130
|
+
if (!state.session?.remotePairingAvailable) {
|
|
1131
|
+
state.remotePairingStatus = null;
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
try {
|
|
1135
|
+
state.remotePairingStatus = await apiGet("/api/remote-pairing/status");
|
|
1136
|
+
} catch {
|
|
1137
|
+
state.remotePairingStatus = null;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
589
1141
|
async function fetchA2aShareStatus() {
|
|
590
1142
|
if (!state.session?.a2aShareEnabled) {
|
|
591
1143
|
state.a2aShareStatus = null;
|
|
@@ -648,6 +1200,7 @@ async function getClientPushState() {
|
|
|
648
1200
|
"PushManager" in window &&
|
|
649
1201
|
"Notification" in window,
|
|
650
1202
|
clientSubscribed: Boolean(subscription),
|
|
1203
|
+
clientSubscription: subscription ? subscription.toJSON() : null,
|
|
651
1204
|
};
|
|
652
1205
|
}
|
|
653
1206
|
|
|
@@ -688,7 +1241,7 @@ async function refreshInboxDiff() {
|
|
|
688
1241
|
}
|
|
689
1242
|
|
|
690
1243
|
async function refreshTimeline() {
|
|
691
|
-
state.timeline = await apiGet("/api/timeline");
|
|
1244
|
+
state.timeline = await hydrateTimelinePayloadImages(await apiGet("/api/timeline"));
|
|
692
1245
|
syncTimelineThreadFilter();
|
|
693
1246
|
syncTimelineKindFilter();
|
|
694
1247
|
}
|
|
@@ -1176,9 +1729,16 @@ function renderPair() {
|
|
|
1176
1729
|
event.preventDefault();
|
|
1177
1730
|
const form = new FormData(event.currentTarget);
|
|
1178
1731
|
try {
|
|
1179
|
-
await pair({ code: String(form.get("code") || "") });
|
|
1732
|
+
const pairResult = await pair({ code: String(form.get("code") || "") });
|
|
1180
1733
|
state.pairError = "";
|
|
1181
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
|
+
}
|
|
1182
1742
|
await refreshSession();
|
|
1183
1743
|
await refreshAuthenticatedState();
|
|
1184
1744
|
await renderShell();
|
|
@@ -1200,6 +1760,80 @@ async function pair(payload) {
|
|
|
1200
1760
|
return result;
|
|
1201
1761
|
}
|
|
1202
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
|
+
|
|
1203
1837
|
async function logout({ revokeCurrentDeviceTrust = false } = {}) {
|
|
1204
1838
|
await apiPost("/api/session/logout", { revokeCurrentDeviceTrust });
|
|
1205
1839
|
resetAuthenticatedState();
|
|
@@ -1227,6 +1861,7 @@ function resetAuthenticatedState() {
|
|
|
1227
1861
|
state.choiceLocalDrafts = {};
|
|
1228
1862
|
clearAllCompletionReplyDrafts();
|
|
1229
1863
|
state.completionReplyDrafts = {};
|
|
1864
|
+
state.completionReplySheetToken = "";
|
|
1230
1865
|
state.settingsSubpage = "";
|
|
1231
1866
|
state.settingsScrollState = null;
|
|
1232
1867
|
state.listScrollState = null;
|
|
@@ -1238,6 +1873,7 @@ function resetAuthenticatedState() {
|
|
|
1238
1873
|
state.deviceError = "";
|
|
1239
1874
|
state.logoutConfirmOpen = false;
|
|
1240
1875
|
state.pairError = "";
|
|
1876
|
+
state.remotePairingDetailsOpen = false;
|
|
1241
1877
|
}
|
|
1242
1878
|
|
|
1243
1879
|
async function revokeTrustedDevice(deviceId) {
|
|
@@ -1259,6 +1895,7 @@ async function revokeTrustedDevice(deviceId) {
|
|
|
1259
1895
|
}
|
|
1260
1896
|
|
|
1261
1897
|
async function renderShell() {
|
|
1898
|
+
syncVisualViewportMetrics();
|
|
1262
1899
|
const desktop = isDesktopLayout();
|
|
1263
1900
|
const shouldShowDetail = state.currentTab !== "settings" && state.currentItem && (desktop || state.detailOpen);
|
|
1264
1901
|
let detail = null;
|
|
@@ -1291,9 +1928,13 @@ async function renderShell() {
|
|
|
1291
1928
|
${renderLogoutConfirmModal()}
|
|
1292
1929
|
${renderHazbaseLogoutConfirmModal()}
|
|
1293
1930
|
</div>
|
|
1931
|
+
${!desktop && detail ? renderCompletionReplySheet(detail) : ""}
|
|
1294
1932
|
`;
|
|
1295
1933
|
|
|
1296
1934
|
bindShellInteractions();
|
|
1935
|
+
if (state.completionReplySheetToken) {
|
|
1936
|
+
resetHorizontalViewportScroll();
|
|
1937
|
+
}
|
|
1297
1938
|
applyPendingDetailScrollReset();
|
|
1298
1939
|
applyPendingListScrollRestore();
|
|
1299
1940
|
applyPendingSettingsSubpageScrollReset();
|
|
@@ -1361,6 +2002,9 @@ function clearThreadFilterInteraction() {
|
|
|
1361
2002
|
|
|
1362
2003
|
function shouldDeferRenderForActiveInteraction() {
|
|
1363
2004
|
const activeElement = document.activeElement;
|
|
2005
|
+
if (state.completionReplySheetToken) {
|
|
2006
|
+
return true;
|
|
2007
|
+
}
|
|
1364
2008
|
if (
|
|
1365
2009
|
activeElement instanceof HTMLTextAreaElement &&
|
|
1366
2010
|
activeElement.matches("[data-completion-reply-textarea]") &&
|
|
@@ -1728,7 +2372,6 @@ function renderMobileTopBar(detail) {
|
|
|
1728
2372
|
return `
|
|
1729
2373
|
<header class="mobile-topbar">
|
|
1730
2374
|
<div class="mobile-topbar__heading">
|
|
1731
|
-
<span class="eyebrow-pill eyebrow-pill--quiet">${escapeHtml(L("common.appName"))}</span>
|
|
1732
2375
|
<h1 class="mobile-topbar__title">${escapeHtml(meta.title)}</h1>
|
|
1733
2376
|
</div>
|
|
1734
2377
|
</header>
|
|
@@ -1783,7 +2426,9 @@ async function fetchCurrentDetailForItem(itemRef = state.currentItem) {
|
|
|
1783
2426
|
return state.detailOverride.detail;
|
|
1784
2427
|
}
|
|
1785
2428
|
try {
|
|
1786
|
-
const detail = await
|
|
2429
|
+
const detail = await hydrateDetailImages(
|
|
2430
|
+
await apiGet(`/api/items/${encodeURIComponent(itemRef.kind)}/${encodeURIComponent(itemRef.token)}`)
|
|
2431
|
+
);
|
|
1787
2432
|
if (hasLaunchItemIntent(itemRef)) {
|
|
1788
2433
|
state.launchItemIntent.status = "loaded";
|
|
1789
2434
|
}
|
|
@@ -1797,7 +2442,9 @@ async function fetchCurrentDetailForItem(itemRef = state.currentItem) {
|
|
|
1797
2442
|
}
|
|
1798
2443
|
await refreshInbox();
|
|
1799
2444
|
try {
|
|
1800
|
-
const detail = await
|
|
2445
|
+
const detail = await hydrateDetailImages(
|
|
2446
|
+
await apiGet(`/api/items/${encodeURIComponent(itemRef.kind)}/${encodeURIComponent(itemRef.token)}`)
|
|
2447
|
+
);
|
|
1801
2448
|
if (hasLaunchItemIntent(itemRef)) {
|
|
1802
2449
|
state.launchItemIntent.status = "loaded";
|
|
1803
2450
|
}
|
|
@@ -3230,7 +3877,7 @@ function buildSettingsContext() {
|
|
|
3230
3877
|
standalone,
|
|
3231
3878
|
supportsPushValue,
|
|
3232
3879
|
permission,
|
|
3233
|
-
subscribed: push.
|
|
3880
|
+
subscribed: push.subscribed === true,
|
|
3234
3881
|
});
|
|
3235
3882
|
|
|
3236
3883
|
return {
|
|
@@ -3256,6 +3903,7 @@ function buildSettingsContext() {
|
|
|
3256
3903
|
a2aRelay: state.a2aRelayStatus,
|
|
3257
3904
|
a2aShare: state.a2aShareStatus,
|
|
3258
3905
|
hazbase: state.hazbaseStatus,
|
|
3906
|
+
remotePairing: state.remotePairingStatus,
|
|
3259
3907
|
};
|
|
3260
3908
|
}
|
|
3261
3909
|
|
|
@@ -3358,6 +4006,195 @@ function collectSettingsDiagnostics({ permission, secureContext, standalone, sup
|
|
|
3358
4006
|
return Array.from(new Set(issues.filter(Boolean)));
|
|
3359
4007
|
}
|
|
3360
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
|
+
|
|
3361
4198
|
function settingsPageMeta(page) {
|
|
3362
4199
|
switch (page) {
|
|
3363
4200
|
case "notifications":
|
|
@@ -3421,14 +4258,14 @@ function settingsPageMeta(page) {
|
|
|
3421
4258
|
id: "a2aRelay",
|
|
3422
4259
|
title: L("settings.a2aRelay.title"),
|
|
3423
4260
|
description: L("settings.a2aRelay.copy"),
|
|
3424
|
-
icon: "
|
|
4261
|
+
icon: "agent-network",
|
|
3425
4262
|
};
|
|
3426
4263
|
case "a2aShare":
|
|
3427
4264
|
return {
|
|
3428
4265
|
id: "a2aShare",
|
|
3429
4266
|
title: L("settings.a2aShare.title"),
|
|
3430
4267
|
description: L("settings.a2aShare.copy"),
|
|
3431
|
-
icon: "
|
|
4268
|
+
icon: "file-event",
|
|
3432
4269
|
};
|
|
3433
4270
|
case "wallet":
|
|
3434
4271
|
return {
|
|
@@ -3437,6 +4274,13 @@ function settingsPageMeta(page) {
|
|
|
3437
4274
|
description: L("settings.wallet.copy"),
|
|
3438
4275
|
icon: "coin",
|
|
3439
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
|
+
};
|
|
3440
4284
|
case "a2aExecutor":
|
|
3441
4285
|
// Executor settings integrated into a2aRelay page — redirect.
|
|
3442
4286
|
return settingsPageMeta("a2aRelay");
|
|
@@ -3452,7 +4296,7 @@ function renderSettingsRoot(context, { mobile }) {
|
|
|
3452
4296
|
page: "notifications",
|
|
3453
4297
|
icon: "notifications",
|
|
3454
4298
|
title: L("settings.notifications.title"),
|
|
3455
|
-
value:
|
|
4299
|
+
value: settingsNotificationRootValue(context),
|
|
3456
4300
|
}),
|
|
3457
4301
|
renderSettingsNavRow({
|
|
3458
4302
|
page: "language",
|
|
@@ -3465,23 +4309,20 @@ function renderSettingsRoot(context, { mobile }) {
|
|
|
3465
4309
|
page: "install",
|
|
3466
4310
|
icon: "homescreen",
|
|
3467
4311
|
title: L("settings.install.title"),
|
|
3468
|
-
value:
|
|
4312
|
+
value: settingsEnabledValue(context.standalone),
|
|
3469
4313
|
})
|
|
3470
4314
|
: "",
|
|
3471
4315
|
renderSettingsNavRow({
|
|
3472
4316
|
page: "awayMode",
|
|
3473
4317
|
icon: "settings",
|
|
3474
4318
|
title: L("settings.awayMode.title"),
|
|
3475
|
-
value: state.session?.claudeAwayMode === true
|
|
4319
|
+
value: settingsEnabledValue(state.session?.claudeAwayMode === true),
|
|
3476
4320
|
}),
|
|
3477
4321
|
renderSettingsNavRow({
|
|
3478
4322
|
page: "autoPilot",
|
|
3479
4323
|
icon: "approval",
|
|
3480
4324
|
title: L("settings.autoPilot.title"),
|
|
3481
|
-
value:
|
|
3482
|
-
state.session?.autoPilotTrustedReads === true || state.session?.autoPilotTrustedWrites === true
|
|
3483
|
-
? L("common.enabled")
|
|
3484
|
-
: L("common.disabled"),
|
|
4325
|
+
value: settingsEnabledValue(hasAutoPilotEnabled()),
|
|
3485
4326
|
}),
|
|
3486
4327
|
].filter(Boolean);
|
|
3487
4328
|
const deviceRows = [
|
|
@@ -3493,6 +4334,12 @@ function renderSettingsRoot(context, { mobile }) {
|
|
|
3493
4334
|
? L("settings.device.count", { count: context.devices.length })
|
|
3494
4335
|
: L("settings.pairing.connected"),
|
|
3495
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
|
+
}) : "",
|
|
3496
4343
|
];
|
|
3497
4344
|
const advancedRows = [
|
|
3498
4345
|
renderSettingsNavRow({
|
|
@@ -3523,32 +4370,26 @@ function renderSettingsRoot(context, { mobile }) {
|
|
|
3523
4370
|
page: "moltbook",
|
|
3524
4371
|
icon: "item",
|
|
3525
4372
|
title: L("settings.moltbook.title"),
|
|
3526
|
-
|
|
3527
|
-
value: context.moltbookScout?.enabled ? L("settings.status.enabled") : L("settings.status.disabled"),
|
|
4373
|
+
value: settingsMoltbookRootValue(context),
|
|
3528
4374
|
}) : "",
|
|
3529
4375
|
state.session?.a2aRelayEnabled ? renderSettingsNavRow({
|
|
3530
4376
|
page: "a2aRelay",
|
|
3531
|
-
icon: "
|
|
4377
|
+
icon: "agent-network",
|
|
3532
4378
|
title: L("settings.a2aRelay.title"),
|
|
3533
|
-
|
|
3534
|
-
value: context.a2aRelay?.connected ? L("settings.status.connected") : L("settings.a2aRelay.status.disconnected"),
|
|
4379
|
+
value: settingsA2aRelayRootValue(context),
|
|
3535
4380
|
}) : "",
|
|
3536
4381
|
state.session?.a2aShareEnabled ? renderSettingsNavRow({
|
|
3537
4382
|
page: "a2aShare",
|
|
3538
|
-
icon: "
|
|
4383
|
+
icon: "file-event",
|
|
3539
4384
|
title: L("settings.a2aShare.title"),
|
|
3540
|
-
|
|
3541
|
-
value: context.a2aShare?.enabled ? L("settings.status.enabled") : L("settings.status.disabled"),
|
|
4385
|
+
value: settingsA2aShareRootValue(context),
|
|
3542
4386
|
}) : "",
|
|
3543
4387
|
context.hazbase?.enabled ? renderSettingsNavRow({
|
|
3544
4388
|
page: "wallet",
|
|
3545
4389
|
icon: "coin",
|
|
3546
4390
|
title: L("settings.wallet.title"),
|
|
3547
4391
|
badge: "beta",
|
|
3548
|
-
|
|
3549
|
-
value: context.hazbase?.sessionInvalid
|
|
3550
|
-
? L("settings.hazbase.status.sessionExpired")
|
|
3551
|
-
: context.hazbase?.signedIn ? L("settings.hazbase.status.signedIn") : L("settings.hazbase.status.signedOut"),
|
|
4392
|
+
value: settingsWalletRootValue(context),
|
|
3552
4393
|
}) : "",
|
|
3553
4394
|
].filter(Boolean)) : ""}
|
|
3554
4395
|
${renderSettingsGroup(L("settings.pairing.title"), deviceRows)}
|
|
@@ -3610,6 +4451,9 @@ function renderSettingsSubpage(context, { mobile }) {
|
|
|
3610
4451
|
case "wallet":
|
|
3611
4452
|
content = renderSettingsWalletPage(context);
|
|
3612
4453
|
break;
|
|
4454
|
+
case "remotePairing":
|
|
4455
|
+
content = renderSettingsRemotePairingPage(context);
|
|
4456
|
+
break;
|
|
3613
4457
|
default:
|
|
3614
4458
|
content = "";
|
|
3615
4459
|
}
|
|
@@ -3624,11 +4468,14 @@ function renderSettingsSubpage(context, { mobile }) {
|
|
|
3624
4468
|
}
|
|
3625
4469
|
|
|
3626
4470
|
function renderSettingsNotificationsPage(context) {
|
|
3627
|
-
const { push, permission,
|
|
4471
|
+
const { push, permission, standalone } = context;
|
|
4472
|
+
const notificationEnabled = push.serverSubscribed === true;
|
|
3628
4473
|
const statusRows = [
|
|
3629
|
-
renderSettingsInfoRow(L("settings.row.status"),
|
|
3630
|
-
renderSettingsInfoRow(L("settings.row.notificationPermission"),
|
|
3631
|
-
|
|
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)),
|
|
3632
4479
|
push.lastSuccessfulDeliveryAtMs
|
|
3633
4480
|
? renderSettingsInfoRow(
|
|
3634
4481
|
L("settings.row.lastSuccessfulDelivery"),
|
|
@@ -3639,12 +4486,6 @@ function renderSettingsNotificationsPage(context) {
|
|
|
3639
4486
|
return `
|
|
3640
4487
|
<div class="settings-page">
|
|
3641
4488
|
${renderSettingsGroup("", statusRows)}
|
|
3642
|
-
${renderSettingsGroup(L("settings.group.advanced"), [
|
|
3643
|
-
renderSettingsInfoRow(L("settings.row.serverWebPush"), serverEnabled ? L("common.enabled") : L("common.disabled")),
|
|
3644
|
-
renderSettingsInfoRow(L("settings.row.secureContext"), secureContext ? L("common.supported") : L("common.notSupported")),
|
|
3645
|
-
renderSettingsInfoRow(L("settings.row.homeScreenApp"), standalone ? L("common.supported") : L("common.notSupported")),
|
|
3646
|
-
renderSettingsInfoRow(L("settings.row.browserSupport"), supportsPushValue ? L("common.supported") : L("common.notSupported")),
|
|
3647
|
-
])}
|
|
3648
4489
|
${state.pushNotice ? `<p class="inline-alert inline-alert--success">${escapeHtml(state.pushNotice)}</p>` : ""}
|
|
3649
4490
|
${state.pushError ? `<p class="inline-alert inline-alert--danger">${escapeHtml(state.pushError)}</p>` : ""}
|
|
3650
4491
|
${renderSettingsActionPanel(renderSettingsNotificationActions({
|
|
@@ -3737,17 +4578,13 @@ function renderSettingsAdvancedPage(context) {
|
|
|
3737
4578
|
${context.diagnostics.map((message) => `<p class="inline-alert">${escapeHtml(message)}</p>`).join("")}
|
|
3738
4579
|
${renderSettingsGroup("", [
|
|
3739
4580
|
renderSettingsInfoRow(L("settings.row.serverWebPush"), context.serverEnabled ? L("common.enabled") : L("common.disabled")),
|
|
3740
|
-
renderSettingsInfoRow(L("settings.row.secureContext"), context.secureContext ? L("common.
|
|
3741
|
-
renderSettingsInfoRow(L("settings.row.homeScreenApp"), context.standalone
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
renderSettingsInfoRow(L("settings.row.
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
L("settings.row.lastSuccessfulDelivery"),
|
|
3748
|
-
new Date(context.push.lastSuccessfulDeliveryAtMs).toLocaleString(state.locale)
|
|
3749
|
-
)
|
|
3750
|
-
: "",
|
|
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
|
+
}),
|
|
3751
4588
|
renderSettingsInfoRow(L("settings.row.version"), state.appVersion || L("common.unavailable")),
|
|
3752
4589
|
].filter(Boolean), { listClassName: "settings-list settings-list--compact" })}
|
|
3753
4590
|
${versionNotice}
|
|
@@ -3807,7 +4644,37 @@ function renderSettingsGroup(title, rows, options = {}) {
|
|
|
3807
4644
|
`;
|
|
3808
4645
|
}
|
|
3809
4646
|
|
|
3810
|
-
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";
|
|
3811
4678
|
return `
|
|
3812
4679
|
<button class="settings-nav-row" type="button" data-settings-subpage="${escapeHtml(page)}">
|
|
3813
4680
|
<span class="settings-row__icon" aria-hidden="true">${renderIcon(icon)}</span>
|
|
@@ -3818,7 +4685,7 @@ function renderSettingsNavRow({ page, icon, title, badge, subtitle, value }) {
|
|
|
3818
4685
|
</span>
|
|
3819
4686
|
${subtitle ? `<span class="settings-row__subtitle">${escapeHtml(subtitle)}</span>` : ""}
|
|
3820
4687
|
</span>
|
|
3821
|
-
<span class="
|
|
4688
|
+
<span class="${valueClass}">${escapeHtml(value || "")}</span>
|
|
3822
4689
|
<span class="settings-row__chevron" aria-hidden="true">${renderIcon("chevron-right")}</span>
|
|
3823
4690
|
</button>
|
|
3824
4691
|
`;
|
|
@@ -3830,16 +4697,18 @@ function renderSettingsAwayModePage() {
|
|
|
3830
4697
|
return `
|
|
3831
4698
|
<div class="settings-page">
|
|
3832
4699
|
${renderSettingsGroup("", [`
|
|
3833
|
-
<label class="reply-mode-switch" data-claude-away-toggle>
|
|
4700
|
+
<label class="reply-mode-switch reply-mode-switch--settings" data-claude-away-toggle>
|
|
3834
4701
|
<input type="checkbox" class="reply-mode-switch__input" ${enabled ? "checked" : ""} data-claude-away-checkbox />
|
|
3835
|
-
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3836
4702
|
<span class="reply-mode-switch__copy">
|
|
3837
4703
|
<span class="reply-mode-switch__title">
|
|
3838
4704
|
<span>${escapeHtml(L("settings.claudeAway.title"))}</span>
|
|
3839
|
-
<span class="reply-mode-switch__state">${escapeHtml(stateLabel)}</span>
|
|
3840
4705
|
</span>
|
|
3841
4706
|
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.claudeAway.description"))}</span>
|
|
3842
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>
|
|
3843
4712
|
</label>
|
|
3844
4713
|
`])}
|
|
3845
4714
|
<p class="settings-page-copy muted">${escapeHtml(L("settings.awayMode.codexNote"))}</p>
|
|
@@ -3853,24 +4722,25 @@ function renderSettingsAutoPilotPage() {
|
|
|
3853
4722
|
const writeLaneContentEnabled = state.session?.autoPilotWriteLaneContent === true;
|
|
3854
4723
|
const writeLaneUiTestsEnabled = state.session?.autoPilotWriteLaneUiTests === true;
|
|
3855
4724
|
const writeLaneSourceEnabled = state.session?.autoPilotWriteLaneSource === true;
|
|
3856
|
-
const trustedWritesEnabled =
|
|
3857
|
-
writeLaneContentEnabled || writeLaneUiTestsEnabled || writeLaneSourceEnabled || state.session?.autoPilotTrustedWrites === true;
|
|
4725
|
+
const trustedWritesEnabled = hasAutoPilotWriteLaneEnabled();
|
|
3858
4726
|
const trustedWritesStateLabel = trustedWritesEnabled ? L("common.enabled") : L("common.disabled");
|
|
3859
4727
|
const recentEntries = recentAutoPilotEntries();
|
|
3860
4728
|
const suggestions = recentAutoPilotSuggestions();
|
|
3861
4729
|
return `
|
|
3862
4730
|
<div class="settings-page">
|
|
3863
4731
|
${renderSettingsGroup("", [`
|
|
3864
|
-
<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>
|
|
3865
4733
|
<input type="checkbox" class="reply-mode-switch__input" ${trustedReadsEnabled ? "checked" : ""} data-auto-pilot-checkbox />
|
|
3866
|
-
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3867
4734
|
<span class="reply-mode-switch__copy">
|
|
3868
4735
|
<span class="reply-mode-switch__title">
|
|
3869
4736
|
<span>${escapeHtml(L("settings.autoPilot.trustedReadsTitle"))}</span>
|
|
3870
|
-
<span class="reply-mode-switch__state">${escapeHtml(trustedReadsStateLabel)}</span>
|
|
3871
4737
|
</span>
|
|
3872
4738
|
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.autoPilot.trustedReadsDescription"))}</span>
|
|
3873
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>
|
|
3874
4744
|
</label>
|
|
3875
4745
|
`, `
|
|
3876
4746
|
<div class="settings-toggle-subhead" role="presentation">
|
|
@@ -3880,40 +4750,46 @@ function renderSettingsAutoPilotPage() {
|
|
|
3880
4750
|
`, `
|
|
3881
4751
|
<div class="settings-toggle-subcopy muted">${escapeHtml(L("settings.autoPilot.trustedWritesDescription"))}</div>
|
|
3882
4752
|
`, `
|
|
3883
|
-
<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">
|
|
3884
4754
|
<input type="checkbox" class="reply-mode-switch__input" ${writeLaneContentEnabled ? "checked" : ""} data-auto-pilot-write-lane-checkbox="content" />
|
|
3885
|
-
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3886
4755
|
<span class="reply-mode-switch__copy">
|
|
3887
4756
|
<span class="reply-mode-switch__title">
|
|
3888
4757
|
<span>${escapeHtml(L("settings.autoPilot.writeLaneContentTitle"))}</span>
|
|
3889
|
-
<span class="reply-mode-switch__state">${escapeHtml(writeLaneContentEnabled ? L("common.enabled") : L("common.disabled"))}</span>
|
|
3890
4758
|
</span>
|
|
3891
4759
|
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.autoPilot.writeLaneContentDescription"))}</span>
|
|
3892
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>
|
|
3893
4765
|
</label>
|
|
3894
4766
|
`, `
|
|
3895
|
-
<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">
|
|
3896
4768
|
<input type="checkbox" class="reply-mode-switch__input" ${writeLaneUiTestsEnabled ? "checked" : ""} data-auto-pilot-write-lane-checkbox="ui-tests" />
|
|
3897
|
-
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3898
4769
|
<span class="reply-mode-switch__copy">
|
|
3899
4770
|
<span class="reply-mode-switch__title">
|
|
3900
4771
|
<span>${escapeHtml(L("settings.autoPilot.writeLaneUiTestsTitle"))}</span>
|
|
3901
|
-
<span class="reply-mode-switch__state">${escapeHtml(writeLaneUiTestsEnabled ? L("common.enabled") : L("common.disabled"))}</span>
|
|
3902
4772
|
</span>
|
|
3903
4773
|
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.autoPilot.writeLaneUiTestsDescription"))}</span>
|
|
3904
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>
|
|
3905
4779
|
</label>
|
|
3906
4780
|
`, `
|
|
3907
|
-
<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">
|
|
3908
4782
|
<input type="checkbox" class="reply-mode-switch__input" ${writeLaneSourceEnabled ? "checked" : ""} data-auto-pilot-write-lane-checkbox="source" />
|
|
3909
|
-
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3910
4783
|
<span class="reply-mode-switch__copy">
|
|
3911
4784
|
<span class="reply-mode-switch__title">
|
|
3912
4785
|
<span>${escapeHtml(L("settings.autoPilot.writeLaneSourceTitle"))}</span>
|
|
3913
|
-
<span class="reply-mode-switch__state">${escapeHtml(writeLaneSourceEnabled ? L("common.enabled") : L("common.disabled"))}</span>
|
|
3914
4786
|
</span>
|
|
3915
4787
|
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.autoPilot.writeLaneSourceDescription"))}</span>
|
|
3916
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>
|
|
3917
4793
|
</label>
|
|
3918
4794
|
`], { listClassName: "settings-list settings-list--toggle-group" })}
|
|
3919
4795
|
<p class="settings-page-copy muted">${escapeHtml(L("settings.autoPilot.scopeNote"))}</p>
|
|
@@ -4537,33 +5413,290 @@ function renderSettingsA2aSharePage(context) {
|
|
|
4537
5413
|
${meta ? `<span class="settings-compose-entry__meta muted">${meta}</span>` : ""}
|
|
4538
5414
|
</span>
|
|
4539
5415
|
</div>
|
|
4540
|
-
`;
|
|
4541
|
-
});
|
|
4542
|
-
if (hasMore) {
|
|
4543
|
-
const remaining = items.length - visibleCount;
|
|
4544
|
-
filesList.push(
|
|
4545
|
-
`<button type="button" class="settings-compose-more" data-a2a-share-files-more>${escapeHtml(L("settings.a2aShare.showMore", { count: remaining }))}</button>`
|
|
4546
|
-
);
|
|
4547
|
-
} else if (items.length > PAGE_SIZE) {
|
|
4548
|
-
filesList.push(
|
|
4549
|
-
`<button type="button" class="settings-compose-more" data-a2a-share-files-collapse>${escapeHtml(L("settings.a2aShare.showLess"))}</button>`
|
|
4550
|
-
);
|
|
4551
|
-
}
|
|
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
|
+
: "";
|
|
4552
5690
|
|
|
4553
5691
|
return `
|
|
4554
5692
|
<div class="settings-page">
|
|
4555
|
-
${
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
])}
|
|
4560
|
-
${
|
|
4561
|
-
${
|
|
4562
|
-
? renderSettingsGroup(L("settings.a2aShare.files.title"), filesList)
|
|
4563
|
-
: renderSettingsGroup(L("settings.a2aShare.files.title"), [
|
|
4564
|
-
`<p class="settings-group__description muted">${escapeHtml(L("settings.a2aShare.files.empty"))}</p>`,
|
|
4565
|
-
])}
|
|
4566
|
-
${share.error ? `<p class="settings-page-copy muted">${escapeHtml(L("settings.a2aShare.error", { reason: share.error }))}</p>` : ""}
|
|
5693
|
+
${noticeBlock}
|
|
5694
|
+
${errorBlock}
|
|
5695
|
+
${connectionSection}
|
|
5696
|
+
${thisDeviceSection}
|
|
5697
|
+
${renderSettingsGroup(L("settings.remotePairing.pairings.otherTitle"), [pairingsRows])}
|
|
5698
|
+
${auditSection}
|
|
5699
|
+
${detailsSection}
|
|
4567
5700
|
</div>
|
|
4568
5701
|
`;
|
|
4569
5702
|
}
|
|
@@ -5030,6 +6163,7 @@ function formatUsdcAtomic(atomic) {
|
|
|
5030
6163
|
}
|
|
5031
6164
|
|
|
5032
6165
|
function renderSettingsInfoRow(label, value, options = {}) {
|
|
6166
|
+
const tone = settingsNavValueTone(value, options.valueTone || "");
|
|
5033
6167
|
const rowClassName = [
|
|
5034
6168
|
"settings-info-row",
|
|
5035
6169
|
options.stacked ? "settings-info-row--stacked" : "",
|
|
@@ -5037,6 +6171,7 @@ function renderSettingsInfoRow(label, value, options = {}) {
|
|
|
5037
6171
|
].filter(Boolean).join(" ");
|
|
5038
6172
|
const valueClassName = [
|
|
5039
6173
|
"settings-info-row__value",
|
|
6174
|
+
tone ? `settings-info-row__value--${tone}` : "",
|
|
5040
6175
|
options.mono ? "settings-info-row__value--mono" : "",
|
|
5041
6176
|
options.valueClassName || "",
|
|
5042
6177
|
].filter(Boolean).join(" ");
|
|
@@ -5083,9 +6218,17 @@ function renderTrustedDeviceCard(device) {
|
|
|
5083
6218
|
const badge = device.currentDevice
|
|
5084
6219
|
? `<span class="device-card__badge">${escapeHtml(L("settings.device.thisDevice"))}</span>`
|
|
5085
6220
|
: "";
|
|
5086
|
-
const
|
|
5087
|
-
?
|
|
5088
|
-
:
|
|
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
|
+
`;
|
|
5089
6232
|
|
|
5090
6233
|
return `
|
|
5091
6234
|
<article class="device-card">
|
|
@@ -5106,14 +6249,7 @@ function renderTrustedDeviceCard(device) {
|
|
|
5106
6249
|
${renderDeviceMetaRow(L("settings.row.pushStatus"), pushLabel)}
|
|
5107
6250
|
${renderDeviceMetaRow(L("settings.row.currentLanguage"), localeLabel)}
|
|
5108
6251
|
</div>
|
|
5109
|
-
|
|
5110
|
-
<button
|
|
5111
|
-
class="secondary secondary--wide"
|
|
5112
|
-
type="button"
|
|
5113
|
-
data-device-revoke="${escapeHtml(device.deviceId)}"
|
|
5114
|
-
data-device-current="${device.currentDevice ? "true" : "false"}"
|
|
5115
|
-
>${escapeHtml(actionLabel)}</button>
|
|
5116
|
-
</div>
|
|
6252
|
+
${actions}
|
|
5117
6253
|
</article>
|
|
5118
6254
|
`;
|
|
5119
6255
|
}
|
|
@@ -5186,8 +6322,9 @@ function renderStandardDetailMobile(detail) {
|
|
|
5186
6322
|
const kindInfo = kindMeta(detail.kind, detail);
|
|
5187
6323
|
const spaciousBodyDetail = TIMELINE_MESSAGE_KINDS.has(detail.kind);
|
|
5188
6324
|
const plainIntro = renderDetailPlainIntro(detail, { mobile: true });
|
|
6325
|
+
const hasCompletionReply = isCompletionReplyAvailable(detail);
|
|
5189
6326
|
return `
|
|
5190
|
-
<div class="mobile-detail-screen">
|
|
6327
|
+
<div class="mobile-detail-screen ${hasCompletionReply ? "mobile-detail-screen--has-reply-dock" : ""}">
|
|
5191
6328
|
<div class="detail-shell detail-shell--mobile">
|
|
5192
6329
|
<div class="mobile-detail-scroll mobile-detail-scroll--detail">
|
|
5193
6330
|
${renderDetailMetaRow(detail, kindInfo, { mobile: true })}
|
|
@@ -5217,10 +6354,10 @@ function renderStandardDetailMobile(detail) {
|
|
|
5217
6354
|
${renderDetailDiffPanel(detail, { mobile: true })}
|
|
5218
6355
|
${renderDetailDiffThreadGroups(detail, { mobile: true })}
|
|
5219
6356
|
${renderDetailFileRefs(detail, { mobile: true })}
|
|
5220
|
-
${renderCompletionReplyComposer(detail, { mobile: true })}
|
|
5221
6357
|
</div>
|
|
5222
6358
|
${detail.readOnly ? "" : renderActionButtons(detail.actions || [], { mobileSticky: true })}
|
|
5223
6359
|
</div>
|
|
6360
|
+
${renderCompletionReplyDock(detail)}
|
|
5224
6361
|
</div>
|
|
5225
6362
|
`;
|
|
5226
6363
|
}
|
|
@@ -5968,7 +7105,7 @@ function renderThreadShareDetail(detail, options = {}) {
|
|
|
5968
7105
|
}
|
|
5969
7106
|
|
|
5970
7107
|
function renderCompletionReplyComposer(detail, options = {}) {
|
|
5971
|
-
if ((detail
|
|
7108
|
+
if (!isCompletionReplyAvailable(detail)) {
|
|
5972
7109
|
return "";
|
|
5973
7110
|
}
|
|
5974
7111
|
|
|
@@ -6142,6 +7279,51 @@ function renderCompletionReplyComposer(detail, options = {}) {
|
|
|
6142
7279
|
`;
|
|
6143
7280
|
}
|
|
6144
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
|
+
|
|
6145
7327
|
function renderChoiceQuestions(detail) {
|
|
6146
7328
|
const effectiveAnswers = getEffectiveChoiceDraftAnswers(detail);
|
|
6147
7329
|
return detail.questions
|
|
@@ -6392,10 +7574,27 @@ function renderInstallBanner() {
|
|
|
6392
7574
|
`;
|
|
6393
7575
|
}
|
|
6394
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
|
+
|
|
6395
7591
|
function renderTopBanner() {
|
|
6396
7592
|
if (!isDesktopLayout() && (state.detailOpen || isSettingsSubpageOpen())) {
|
|
6397
7593
|
return "";
|
|
6398
7594
|
}
|
|
7595
|
+
if (state.clientUpdateRequired) {
|
|
7596
|
+
return renderClientUpdateBanner();
|
|
7597
|
+
}
|
|
6399
7598
|
if (shouldShowInstallBanner()) {
|
|
6400
7599
|
return renderInstallBanner();
|
|
6401
7600
|
}
|
|
@@ -6922,6 +8121,23 @@ function bindShellInteractions() {
|
|
|
6922
8121
|
});
|
|
6923
8122
|
}
|
|
6924
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
|
+
|
|
6925
8141
|
for (const input of document.querySelectorAll("[data-reply-mode-toggle][data-reply-token]")) {
|
|
6926
8142
|
input.addEventListener("change", async () => {
|
|
6927
8143
|
const token = input.dataset.replyToken || "";
|
|
@@ -7132,14 +8348,51 @@ function bindShellInteractions() {
|
|
|
7132
8348
|
state.session.autoPilotWriteLaneSource = result?.writeLaneSourceEnabled === true;
|
|
7133
8349
|
}
|
|
7134
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
|
+
|
|
7135
8383
|
for (const checkbox of document.querySelectorAll("[data-auto-pilot-checkbox]")) {
|
|
7136
8384
|
checkbox.addEventListener("change", async () => {
|
|
7137
8385
|
const next = checkbox.checked === true;
|
|
8386
|
+
const previous = snapshotAutoPilotSettings();
|
|
8387
|
+
if (state.session) {
|
|
8388
|
+
state.session.autoPilotTrustedReads = next;
|
|
8389
|
+
}
|
|
8390
|
+
await renderShell();
|
|
7138
8391
|
try {
|
|
7139
8392
|
const result = await apiPost("/api/settings/auto-pilot", { trustedReadsEnabled: next });
|
|
7140
8393
|
applyAutoPilotSettingsResult(result);
|
|
7141
|
-
await refreshAuthenticatedState();
|
|
7142
8394
|
} catch (error) {
|
|
8395
|
+
restoreAutoPilotSettings(previous);
|
|
7143
8396
|
state.pushError = error.message || String(error);
|
|
7144
8397
|
}
|
|
7145
8398
|
await renderShell();
|
|
@@ -7150,17 +8403,29 @@ function bindShellInteractions() {
|
|
|
7150
8403
|
checkbox.addEventListener("change", async () => {
|
|
7151
8404
|
const next = checkbox.checked === true;
|
|
7152
8405
|
const lane = normalizeClientText(checkbox.getAttribute("data-auto-pilot-write-lane-checkbox") || "");
|
|
8406
|
+
const previous = snapshotAutoPilotSettings();
|
|
7153
8407
|
const payload =
|
|
7154
8408
|
lane === "content"
|
|
7155
8409
|
? { writeLaneContentEnabled: next }
|
|
7156
8410
|
: lane === "ui-tests"
|
|
7157
8411
|
? { writeLaneUiTestsEnabled: next }
|
|
7158
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();
|
|
7159
8424
|
try {
|
|
7160
8425
|
const result = await apiPost("/api/settings/auto-pilot", payload);
|
|
7161
8426
|
applyAutoPilotSettingsResult(result);
|
|
7162
|
-
await refreshAuthenticatedState();
|
|
7163
8427
|
} catch (error) {
|
|
8428
|
+
restoreAutoPilotSettings(previous);
|
|
7164
8429
|
state.pushError = error.message || String(error);
|
|
7165
8430
|
}
|
|
7166
8431
|
await renderShell();
|
|
@@ -7181,11 +8446,23 @@ function bindShellInteractions() {
|
|
|
7181
8446
|
if (!payload) {
|
|
7182
8447
|
return;
|
|
7183
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();
|
|
7184
8461
|
try {
|
|
7185
8462
|
const result = await apiPost("/api/settings/auto-pilot", payload);
|
|
7186
8463
|
applyAutoPilotSettingsResult(result);
|
|
7187
|
-
await refreshAuthenticatedState();
|
|
7188
8464
|
} catch (error) {
|
|
8465
|
+
restoreAutoPilotSettings(previous);
|
|
7189
8466
|
state.pushError = error.message || String(error);
|
|
7190
8467
|
}
|
|
7191
8468
|
await renderShell();
|
|
@@ -7272,6 +8549,166 @@ function bindShellInteractions() {
|
|
|
7272
8549
|
});
|
|
7273
8550
|
}
|
|
7274
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
|
+
|
|
7275
8712
|
|
|
7276
8713
|
for (const button of document.querySelectorAll("[data-hazbase-action]")) {
|
|
7277
8714
|
button.addEventListener("click", async () => {
|
|
@@ -7548,24 +8985,13 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7548
8985
|
try {
|
|
7549
8986
|
const decisionBody = { action: submittedAction, editedText };
|
|
7550
8987
|
if (editedTitle) decisionBody.editedTitle = editedTitle;
|
|
7551
|
-
|
|
7552
|
-
|
|
7553
|
-
|
|
7554
|
-
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
const errBody = await res.json().catch(() => ({}));
|
|
7559
|
-
alert(`Moltbook draft ${submittedAction} failed: ${errBody.error || res.status}`);
|
|
7560
|
-
buttons.forEach((btn) => {
|
|
7561
|
-
btn.disabled = false;
|
|
7562
|
-
btn.classList.remove("is-loading", "is-dimmed");
|
|
7563
|
-
btn.innerHTML = labelCache.get(btn);
|
|
7564
|
-
});
|
|
7565
|
-
if (textarea) textarea.readOnly = false;
|
|
7566
|
-
moltbookDraftForm.dataset.submitting = "";
|
|
7567
|
-
return;
|
|
7568
|
-
}
|
|
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
|
+
);
|
|
7569
8995
|
// Mark local detail as resolved so re-render shows "already resolved" immediately.
|
|
7570
8996
|
if (state.currentDetail?.kind === "moltbook_draft") {
|
|
7571
8997
|
state.currentDetail.moltbookDraftEnabled = false;
|
|
@@ -7574,7 +9000,7 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7574
9000
|
await refreshAuthenticatedState();
|
|
7575
9001
|
await renderShell();
|
|
7576
9002
|
} catch (error) {
|
|
7577
|
-
alert(`Moltbook draft
|
|
9003
|
+
alert(`Moltbook draft ${submittedAction} failed: ${error.message}`);
|
|
7578
9004
|
buttons.forEach((btn) => {
|
|
7579
9005
|
btn.disabled = false;
|
|
7580
9006
|
btn.classList.remove("is-loading", "is-dimmed");
|
|
@@ -7625,24 +9051,10 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7625
9051
|
try {
|
|
7626
9052
|
const decisionBody = { action: submittedAction, instruction };
|
|
7627
9053
|
if (executor) decisionBody.executor = executor;
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
body: JSON.stringify(decisionBody),
|
|
7633
|
-
});
|
|
7634
|
-
if (!res.ok) {
|
|
7635
|
-
const errBody = await res.json().catch(() => ({}));
|
|
7636
|
-
alert(`A2A task ${submittedAction} failed: ${errBody.error || res.status}`);
|
|
7637
|
-
buttons.forEach((btn) => {
|
|
7638
|
-
btn.disabled = false;
|
|
7639
|
-
btn.classList.remove("is-loading", "is-dimmed");
|
|
7640
|
-
btn.innerHTML = labelCache.get(btn);
|
|
7641
|
-
});
|
|
7642
|
-
if (textarea) textarea.readOnly = false;
|
|
7643
|
-
a2aTaskForm.dataset.submitting = "";
|
|
7644
|
-
return;
|
|
7645
|
-
}
|
|
9054
|
+
await apiPost(
|
|
9055
|
+
`/api/items/a2a-task/${encodeURIComponent(token)}/decision`,
|
|
9056
|
+
decisionBody,
|
|
9057
|
+
);
|
|
7646
9058
|
if (state.currentDetail?.kind === "a2a_task") {
|
|
7647
9059
|
state.currentDetail.a2aTaskEnabled = false;
|
|
7648
9060
|
state.currentDetail.readOnly = true;
|
|
@@ -7650,7 +9062,7 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7650
9062
|
await refreshAuthenticatedState();
|
|
7651
9063
|
await renderShell();
|
|
7652
9064
|
} catch (error) {
|
|
7653
|
-
alert(`A2A task
|
|
9065
|
+
alert(`A2A task ${submittedAction} failed: ${error.message}`);
|
|
7654
9066
|
buttons.forEach((btn) => {
|
|
7655
9067
|
btn.disabled = false;
|
|
7656
9068
|
btn.classList.remove("is-loading", "is-dimmed");
|
|
@@ -7691,24 +9103,10 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7691
9103
|
if (textarea) textarea.readOnly = true;
|
|
7692
9104
|
const editedContent = normalizeClientText(new FormData(threadShareForm).get("shareContent"));
|
|
7693
9105
|
try {
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
body: JSON.stringify({ decision: submittedAction, editedContent }),
|
|
7699
|
-
});
|
|
7700
|
-
if (!res.ok) {
|
|
7701
|
-
const errBody = await res.json().catch(() => ({}));
|
|
7702
|
-
alert(`Thread share ${submittedAction} failed: ${errBody.error || res.status}`);
|
|
7703
|
-
buttons.forEach((btn) => {
|
|
7704
|
-
btn.disabled = false;
|
|
7705
|
-
btn.classList.remove("is-loading", "is-dimmed");
|
|
7706
|
-
btn.innerHTML = labelCache.get(btn);
|
|
7707
|
-
});
|
|
7708
|
-
if (textarea) textarea.readOnly = false;
|
|
7709
|
-
threadShareForm.dataset.submitting = "";
|
|
7710
|
-
return;
|
|
7711
|
-
}
|
|
9106
|
+
await apiPost(
|
|
9107
|
+
`/api/threads/share/${encodeURIComponent(token)}/decision`,
|
|
9108
|
+
{ decision: submittedAction, editedContent },
|
|
9109
|
+
);
|
|
7712
9110
|
if (state.currentDetail?.kind === "thread_share") {
|
|
7713
9111
|
state.currentDetail.threadShareEnabled = false;
|
|
7714
9112
|
state.currentDetail.readOnly = true;
|
|
@@ -7716,7 +9114,7 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7716
9114
|
await refreshAuthenticatedState();
|
|
7717
9115
|
await renderShell();
|
|
7718
9116
|
} catch (error) {
|
|
7719
|
-
alert(`Thread share
|
|
9117
|
+
alert(`Thread share ${submittedAction} failed: ${error.message}`);
|
|
7720
9118
|
buttons.forEach((btn) => {
|
|
7721
9119
|
btn.disabled = false;
|
|
7722
9120
|
btn.classList.remove("is-loading", "is-dimmed");
|
|
@@ -7775,14 +9173,22 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
7775
9173
|
await renderShell();
|
|
7776
9174
|
|
|
7777
9175
|
try {
|
|
7778
|
-
const
|
|
7779
|
-
requestBody
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
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);
|
|
7786
9192
|
}
|
|
7787
9193
|
const replyKind = replyForm.dataset.replyKind || "completion";
|
|
7788
9194
|
await apiPost(`/api/items/${encodeURIComponent(replyKind)}/${encodeURIComponent(token)}/reply`, requestBody);
|
|
@@ -7941,6 +9347,13 @@ function bindSharedUi(renderFn) {
|
|
|
7941
9347
|
});
|
|
7942
9348
|
}
|
|
7943
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
|
+
|
|
7944
9357
|
for (const button of document.querySelectorAll("[data-install-guide-close]")) {
|
|
7945
9358
|
button.addEventListener("click", async (event) => {
|
|
7946
9359
|
if (button.classList.contains("modal-backdrop")) {
|
|
@@ -8608,6 +10021,10 @@ function renderIcon(name) {
|
|
|
8608
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>`;
|
|
8609
10022
|
case "language":
|
|
8610
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>`;
|
|
8611
10028
|
case "link":
|
|
8612
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>`;
|
|
8613
10030
|
case "clip":
|
|
@@ -8758,13 +10175,138 @@ function handleDocumentVisibilityChange() {
|
|
|
8758
10175
|
handlePotentialExternalNavigation();
|
|
8759
10176
|
}
|
|
8760
10177
|
|
|
8761
|
-
async function
|
|
8762
|
-
|
|
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, {
|
|
8763
10305
|
credentials: "same-origin",
|
|
8764
10306
|
headers: {
|
|
8765
10307
|
Accept: "application/json",
|
|
8766
10308
|
},
|
|
8767
|
-
});
|
|
10309
|
+
}, opts);
|
|
8768
10310
|
if (!response.ok) {
|
|
8769
10311
|
const errorInfo = await readError(response);
|
|
8770
10312
|
const error = new Error(errorInfo.message);
|
|
@@ -8778,7 +10320,9 @@ async function apiGet(url) {
|
|
|
8778
10320
|
|
|
8779
10321
|
async function apiPost(url, body) {
|
|
8780
10322
|
const isFormDataBody = typeof FormData !== "undefined" && body instanceof FormData;
|
|
8781
|
-
|
|
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, {
|
|
8782
10326
|
method: "POST",
|
|
8783
10327
|
credentials: "same-origin",
|
|
8784
10328
|
headers: isFormDataBody
|