viveworker 0.7.0-beta.1 → 0.7.0-beta.3
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 +39 -0
- package/package.json +3 -1
- package/scripts/share-cli.mjs +554 -3
- package/scripts/viveworker-bridge.mjs +3395 -218
- package/viveworker.env.example +8 -0
- package/web/app.css +916 -109
- package/web/app.js +1762 -43
- package/web/hazbase-passkey.js +107 -0
- package/web/i18n.js +276 -0
- package/web/sw.js +33 -15
package/web/app.js
CHANGED
|
@@ -6,6 +6,8 @@ const PUSH_BANNER_DISMISS_KEY = "viveworker-push-banner-dismissed-v1";
|
|
|
6
6
|
const INITIAL_DETECTED_LOCALE = detectBrowserLocale();
|
|
7
7
|
const TIMELINE_MESSAGE_KINDS = new Set(["user_message", "assistant_commentary", "assistant_final"]);
|
|
8
8
|
const TIMELINE_OPERATIONAL_KINDS = new Set(["approval", "plan", "plan_ready", "choice"]);
|
|
9
|
+
const EXTERNAL_TARGET_TABS = new Set(["inbox", "timeline", "diff"]);
|
|
10
|
+
const EXTERNAL_TARGET_INBOX_SUBTABS = new Set(["pending", "completed"]);
|
|
9
11
|
const THREAD_FILTER_INTERACTION_DEFER_MS = 8000;
|
|
10
12
|
const MAX_COMPLETION_REPLY_IMAGE_COUNT = 4;
|
|
11
13
|
const NOTIFICATION_INTENT_CACHE = "viveworker-notification-intent-v1";
|
|
@@ -14,6 +16,12 @@ const NOTIFICATION_INTENT_PATH = "/__viveworker_notification_intent__";
|
|
|
14
16
|
const state = {
|
|
15
17
|
session: null,
|
|
16
18
|
inbox: null,
|
|
19
|
+
// Flips to true after the first /api/inbox/diff response resolves
|
|
20
|
+
// (success OR failure). Until then the Code/diff tab shows skeleton
|
|
21
|
+
// shimmer cards instead of the "no entries" empty state, because the
|
|
22
|
+
// initial diff scan spawns git subprocesses per tracked repo and can
|
|
23
|
+
// take 1–3 seconds — the empty state was misleading during that window.
|
|
24
|
+
inboxDiffLoaded: false,
|
|
17
25
|
timeline: null,
|
|
18
26
|
devices: [],
|
|
19
27
|
currentTab: "inbox",
|
|
@@ -52,6 +60,34 @@ const state = {
|
|
|
52
60
|
a2aRelayStatus: null,
|
|
53
61
|
a2aShareStatus: null,
|
|
54
62
|
a2aShareRecentExpanded: 0,
|
|
63
|
+
hazbaseStatus: null,
|
|
64
|
+
hazbaseNotice: "",
|
|
65
|
+
hazbaseError: "",
|
|
66
|
+
// Session-only flag: once the Sepolia wallet is ready, the mainnet step
|
|
67
|
+
// is hidden behind a subtle opt-in link (most beta users won't activate
|
|
68
|
+
// it). Flipping this reveals the full mainnet step card for the rest of
|
|
69
|
+
// the session. Not persisted — reopening Wallet next visit starts hidden
|
|
70
|
+
// again, which is the desired default for the closed beta.
|
|
71
|
+
hazbaseMainnetOptIn: false,
|
|
72
|
+
// Sign-in OTP flow is a two-step send→verify dance. Showing both
|
|
73
|
+
// buttons side-by-side confused users; they clicked "Verify" before
|
|
74
|
+
// ever requesting a code. We gate the verify button behind a
|
|
75
|
+
// successful send by tracking that a code was just issued in this
|
|
76
|
+
// session (plus the email it was sent to, so verify doesn't re-prompt).
|
|
77
|
+
// Both reset on successful verify (or on page reload — losing the flag
|
|
78
|
+
// just means the user re-clicks "send", which is idempotent on the
|
|
79
|
+
// server side). Email + code are also mirrored here so re-renders
|
|
80
|
+
// triggered by polls/notices don't wipe what the user was typing —
|
|
81
|
+
// the <input value="..."> attributes read these back.
|
|
82
|
+
hazbaseOtpRequested: false,
|
|
83
|
+
hazbaseOtpEmail: "",
|
|
84
|
+
hazbaseOtpCode: "",
|
|
85
|
+
// Wallet logout is reversible in principle (same account + same passkey
|
|
86
|
+
// re-derives the same smart account address), but it still wipes the
|
|
87
|
+
// current session + any in-flight approvals and forces an OTP round-trip
|
|
88
|
+
// to recover — cheap to gate behind an explicit confirm. Session-only
|
|
89
|
+
// flag; flipped by the signOut button and cleared on confirm/cancel.
|
|
90
|
+
hazbaseLogoutConfirmOpen: false,
|
|
55
91
|
a2aTaskExecutorPick: "codex",
|
|
56
92
|
pushNotice: "",
|
|
57
93
|
pushError: "",
|
|
@@ -70,13 +106,36 @@ const state = {
|
|
|
70
106
|
defaultLocale: DEFAULT_LOCALE,
|
|
71
107
|
supportedLocales: [...SUPPORTED_LOCALES],
|
|
72
108
|
appVersion: "",
|
|
109
|
+
versionStatus: null,
|
|
110
|
+
versionStatusError: "",
|
|
73
111
|
};
|
|
74
112
|
|
|
75
113
|
let detailLoadSequence = 0;
|
|
114
|
+
let hazbasePasskeyModulePromise = null;
|
|
115
|
+
|
|
116
|
+
async function loadHazbasePasskeyModule() {
|
|
117
|
+
if (!hazbasePasskeyModulePromise) {
|
|
118
|
+
hazbasePasskeyModulePromise = import("./hazbase-passkey.js");
|
|
119
|
+
}
|
|
120
|
+
return hazbasePasskeyModulePromise;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function hazbasePasskeyHostSupport() {
|
|
124
|
+
const protocol = normalizeClientText(window.location?.protocol || "");
|
|
125
|
+
const hostname = normalizeClientText(window.location?.hostname || "").toLowerCase();
|
|
126
|
+
const eligible = protocol === "https:" && Boolean(hostname) && hostname.endsWith(".local");
|
|
127
|
+
return {
|
|
128
|
+
eligible,
|
|
129
|
+
hostname,
|
|
130
|
+
detail: eligible ? "" : L("settings.hazbase.passkey.localHostRequired"),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
76
133
|
|
|
77
134
|
const app = document.querySelector("#app");
|
|
78
135
|
const params = new URLSearchParams(window.location.search);
|
|
79
136
|
const initialItem = params.get("item") || "";
|
|
137
|
+
const initialTargetTab = params.get("tab") || "";
|
|
138
|
+
const initialTargetSubtab = params.get("subtab") || "";
|
|
80
139
|
const initialPairToken = params.get("pairToken") || "";
|
|
81
140
|
const initialFocusPending = params.get("focusPending") || "";
|
|
82
141
|
let didReloadForServiceWorker = false;
|
|
@@ -101,14 +160,19 @@ boot().catch((error) => {
|
|
|
101
160
|
|
|
102
161
|
async function boot() {
|
|
103
162
|
updateManifestHref(initialPairToken);
|
|
104
|
-
|
|
163
|
+
// SW register + update() can take hundreds of ms and does not need to gate
|
|
164
|
+
// first paint. Fire and forget; the `controllerchange` reload handler wired
|
|
165
|
+
// up inside `registerServiceWorker` still picks up new versions.
|
|
166
|
+
registerServiceWorker().catch(() => {});
|
|
105
167
|
navigator.serviceWorker?.addEventListener("message", handleServiceWorkerMessage);
|
|
106
168
|
window.addEventListener("resize", handleViewportChange, { passive: true });
|
|
107
169
|
window.addEventListener("focus", handlePotentialExternalNavigation, { passive: true });
|
|
108
170
|
window.addEventListener("pageshow", handlePotentialExternalNavigation, { passive: true });
|
|
109
171
|
document.addEventListener("visibilitychange", handleDocumentVisibilityChange);
|
|
110
172
|
|
|
111
|
-
|
|
173
|
+
// Single round-trip for session + inbox(pending/completed) + timeline +
|
|
174
|
+
// devices. See `refreshBootstrap` for why we collapsed the boot fan-out.
|
|
175
|
+
await refreshBootstrap();
|
|
112
176
|
|
|
113
177
|
if (!state.session?.authenticated && initialPairToken && shouldAutoPairFromBootstrapToken()) {
|
|
114
178
|
try {
|
|
@@ -119,17 +183,19 @@ async function boot() {
|
|
|
119
183
|
} catch (error) {
|
|
120
184
|
state.pairError = error.message || String(error);
|
|
121
185
|
}
|
|
122
|
-
await
|
|
186
|
+
await refreshBootstrap();
|
|
123
187
|
}
|
|
124
188
|
|
|
125
189
|
syncPairingTokenState(desiredBootstrapPairingToken());
|
|
126
190
|
|
|
127
191
|
const parsedInitialItem = parseItemRef(initialItem);
|
|
128
192
|
if (parsedInitialItem) {
|
|
193
|
+
const targetTab = sanitizeExternalTargetTab(initialTargetTab) || tabForItemKind(parsedInitialItem.kind, state.currentTab);
|
|
194
|
+
const targetSubtab = sanitizeExternalTargetInboxSubtab(initialTargetSubtab);
|
|
129
195
|
state.currentItem = parsedInitialItem;
|
|
130
|
-
state.currentTab =
|
|
196
|
+
state.currentTab = targetTab;
|
|
131
197
|
if (state.currentTab === "inbox") {
|
|
132
|
-
state.inboxSubtab = inboxSubtabForItemKind(parsedInitialItem.kind);
|
|
198
|
+
state.inboxSubtab = inboxSubtabForItemKind(parsedInitialItem.kind, targetSubtab);
|
|
133
199
|
}
|
|
134
200
|
state.detailOpen = true;
|
|
135
201
|
if (isFastPathItemRef(parsedInitialItem)) {
|
|
@@ -146,8 +212,6 @@ async function boot() {
|
|
|
146
212
|
}
|
|
147
213
|
|
|
148
214
|
await consumePendingNotificationIntent();
|
|
149
|
-
await syncDetectedLocalePreference();
|
|
150
|
-
await refreshAuthenticatedState();
|
|
151
215
|
// `?focusPending=claude` marks this tab as the Claude-hook-opened popup:
|
|
152
216
|
// auto-navigate to the newest unresolved Claude pending (plan/question)
|
|
153
217
|
// detail view — but only when the user is not already in the middle of
|
|
@@ -156,9 +220,37 @@ async function boot() {
|
|
|
156
220
|
if (initialFocusPending === "claude" && !state.currentItem) {
|
|
157
221
|
state.claudePopupMode = true;
|
|
158
222
|
}
|
|
159
|
-
|
|
223
|
+
|
|
224
|
+
// Bootstrap already populated session + inbox(pending/completed) +
|
|
225
|
+
// timeline + devices, so the shell renders with real data on first
|
|
226
|
+
// paint. No null-state fallback needed here.
|
|
160
227
|
ensureCurrentSelection();
|
|
228
|
+
maybeAutoFocusClaudePending();
|
|
161
229
|
await renderShell();
|
|
230
|
+
refreshVersionStatusForTechnicalPage();
|
|
231
|
+
|
|
232
|
+
// Diff fetch runs as a background phase because `/api/inbox/diff`
|
|
233
|
+
// spawns `git` subprocesses per tracked repo and can stall for several
|
|
234
|
+
// seconds. The Code/diff tab lights up when this resolves.
|
|
235
|
+
refreshInboxDiff()
|
|
236
|
+
.then(async () => {
|
|
237
|
+
if (!shouldDeferRenderForActiveInteraction()) {
|
|
238
|
+
await renderShell();
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
.catch(() => {});
|
|
242
|
+
|
|
243
|
+
// Remote status probes (push/moltbook/a2a-relay/a2a-share — the share
|
|
244
|
+
// worker round-trip alone can block up to 10s). Re-renders when it
|
|
245
|
+
// resolves.
|
|
246
|
+
refreshAuthenticatedStateRemote()
|
|
247
|
+
.then(async () => {
|
|
248
|
+
if (!shouldDeferRenderForActiveInteraction()) {
|
|
249
|
+
await renderShell();
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
.catch(() => {});
|
|
253
|
+
syncDetectedLocalePreference().catch(() => {});
|
|
162
254
|
|
|
163
255
|
setInterval(async () => {
|
|
164
256
|
if (!state.session?.authenticated) {
|
|
@@ -168,11 +260,41 @@ async function boot() {
|
|
|
168
260
|
if (consumedNotificationIntent) {
|
|
169
261
|
return;
|
|
170
262
|
}
|
|
171
|
-
|
|
263
|
+
// Split the poll tick into a fast local fan-out and a slow background
|
|
264
|
+
// fan-out. The fast calls are all in-memory bridge lookups (inbox,
|
|
265
|
+
// timeline, devices, push status, relay status) and typically resolve in
|
|
266
|
+
// under ~100 ms over localhost; rendering immediately after them keeps
|
|
267
|
+
// new user_message entries reaching the timeline within one scan tick.
|
|
268
|
+
// The slow calls — /api/inbox/diff (git subprocesses), moltbook scout
|
|
269
|
+
// status (cross-origin to moltbook.com on cache miss), and especially
|
|
270
|
+
// /api/share/status (10 s upstream timeout to share.viveworker.com) —
|
|
271
|
+
// used to gate the render on every poll, so a single share-worker
|
|
272
|
+
// cache miss stalled timeline updates for up to 10 seconds. Now they
|
|
273
|
+
// run in the background and trigger a second render when they resolve.
|
|
274
|
+
await Promise.all([
|
|
275
|
+
refreshInbox(),
|
|
276
|
+
refreshTimeline(),
|
|
277
|
+
refreshDevices(),
|
|
278
|
+
refreshPushStatus(),
|
|
279
|
+
fetchA2aRelayStatus(),
|
|
280
|
+
]);
|
|
281
|
+
ensureCurrentSelection();
|
|
172
282
|
maybeAutoFocusClaudePending();
|
|
173
283
|
if (!shouldDeferRenderForActiveInteraction()) {
|
|
174
284
|
await renderShell();
|
|
175
285
|
}
|
|
286
|
+
|
|
287
|
+
Promise.allSettled([
|
|
288
|
+
refreshInboxDiff(),
|
|
289
|
+
fetchMoltbookScoutStatus(),
|
|
290
|
+
fetchA2aShareStatus(),
|
|
291
|
+
])
|
|
292
|
+
.then(async () => {
|
|
293
|
+
if (!shouldDeferRenderForActiveInteraction()) {
|
|
294
|
+
await renderShell();
|
|
295
|
+
}
|
|
296
|
+
})
|
|
297
|
+
.catch(() => {});
|
|
176
298
|
}, 3000);
|
|
177
299
|
}
|
|
178
300
|
|
|
@@ -215,22 +337,84 @@ function handleViewportChange() {
|
|
|
215
337
|
}
|
|
216
338
|
|
|
217
339
|
async function refreshAuthenticatedState() {
|
|
218
|
-
|
|
340
|
+
// Fire the two inbox halves in parallel so the diff's git subprocesses
|
|
341
|
+
// don't serialize behind pending+completed (or vice versa).
|
|
342
|
+
await Promise.all([refreshInbox(), refreshInboxDiff()]);
|
|
219
343
|
await refreshTimeline();
|
|
220
344
|
await refreshDevices();
|
|
221
345
|
await refreshPushStatus();
|
|
222
346
|
await fetchMoltbookScoutStatus();
|
|
223
347
|
await fetchA2aRelayStatus();
|
|
224
348
|
await fetchA2aShareStatus();
|
|
349
|
+
if (state.currentTab === "settings") {
|
|
350
|
+
await fetchHazbaseStatus();
|
|
351
|
+
}
|
|
352
|
+
ensureCurrentSelection();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Boot-time split: the first paint should not wait on cross-origin status
|
|
356
|
+
// probes. `Local` covers in-memory bridge lookups (fast). `Remote` covers
|
|
357
|
+
// everything that can stall on an external service (push config,
|
|
358
|
+
// moltbook/a2a worker calls — the a2a share worker has a 10s timeout) and
|
|
359
|
+
// runs in the background after the shell renders.
|
|
360
|
+
async function refreshAuthenticatedStateLocal() {
|
|
361
|
+
await Promise.all([refreshInbox(), refreshTimeline(), refreshDevices()]);
|
|
225
362
|
ensureCurrentSelection();
|
|
226
363
|
}
|
|
227
364
|
|
|
365
|
+
async function refreshAuthenticatedStateRemote() {
|
|
366
|
+
await Promise.allSettled([
|
|
367
|
+
refreshPushStatus(),
|
|
368
|
+
fetchMoltbookScoutStatus(),
|
|
369
|
+
fetchA2aRelayStatus(),
|
|
370
|
+
fetchA2aShareStatus(),
|
|
371
|
+
]);
|
|
372
|
+
}
|
|
373
|
+
|
|
228
374
|
async function refreshSession() {
|
|
229
375
|
state.session = await apiGet("/api/session");
|
|
230
376
|
syncPairingTokenState(desiredBootstrapPairingToken());
|
|
231
377
|
applyResolvedLocale();
|
|
232
378
|
}
|
|
233
379
|
|
|
380
|
+
// One-shot boot fetch: hits `/api/bootstrap` which bundles session,
|
|
381
|
+
// inbox (pending + completed), timeline, and devices into a single
|
|
382
|
+
// HTTPS round-trip. Saves 3 additional TLS handshakes versus calling
|
|
383
|
+
// the four endpoints in parallel, which is the dominant boot cost on
|
|
384
|
+
// iOS PWAs where connection reuse is aggressive. Leaves the diff and
|
|
385
|
+
// external-status probes as separate background phases in `boot()`.
|
|
386
|
+
async function refreshBootstrap() {
|
|
387
|
+
const bootstrap = await apiGet("/api/bootstrap");
|
|
388
|
+
state.session = bootstrap?.session || null;
|
|
389
|
+
syncPairingTokenState(desiredBootstrapPairingToken());
|
|
390
|
+
applyResolvedLocale();
|
|
391
|
+
|
|
392
|
+
if (!state.session?.authenticated) {
|
|
393
|
+
state.devices = [];
|
|
394
|
+
state.deviceError = "";
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const fastInbox = bootstrap?.inbox || {};
|
|
399
|
+
const previousDiff = Array.isArray(state.inbox?.diff) ? state.inbox.diff : [];
|
|
400
|
+
state.inbox = {
|
|
401
|
+
pending: Array.isArray(fastInbox.pending) ? fastInbox.pending : [],
|
|
402
|
+
completed: Array.isArray(fastInbox.completed) ? fastInbox.completed : [],
|
|
403
|
+
diff: previousDiff,
|
|
404
|
+
};
|
|
405
|
+
syncDiffThreadFilter();
|
|
406
|
+
syncCompletedThreadFilter();
|
|
407
|
+
syncInboxSubtab();
|
|
408
|
+
|
|
409
|
+
state.timeline = bootstrap?.timeline || null;
|
|
410
|
+
syncTimelineThreadFilter();
|
|
411
|
+
syncTimelineKindFilter();
|
|
412
|
+
|
|
413
|
+
const devicesPayload = bootstrap?.devices;
|
|
414
|
+
state.devices = Array.isArray(devicesPayload?.devices) ? devicesPayload.devices : [];
|
|
415
|
+
state.deviceError = "";
|
|
416
|
+
}
|
|
417
|
+
|
|
234
418
|
async function syncDetectedLocalePreference() {
|
|
235
419
|
if (!state.session?.authenticated || !state.session?.deviceId || !state.detectedLocale) {
|
|
236
420
|
return;
|
|
@@ -248,16 +432,47 @@ async function syncDetectedLocalePreference() {
|
|
|
248
432
|
applyResolvedLocale();
|
|
249
433
|
}
|
|
250
434
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
435
|
+
// Two-phase override: flip state.session + applyResolvedLocale synchronously
|
|
436
|
+
// so the caller can renderShell() in the new language before the POST
|
|
437
|
+
// round-trip, then reconcile with the server response. On failure the
|
|
438
|
+
// previous session snapshot is restored so the UI rolls back.
|
|
439
|
+
function applyLocaleOverrideOptimistically(nextLocale) {
|
|
440
|
+
if (!state.session) return null;
|
|
441
|
+
const previousSession = { ...state.session };
|
|
442
|
+
const optimistic = resolveLocalePreference({
|
|
443
|
+
overrideLocale: nextLocale || "",
|
|
444
|
+
detectedLocale: state.session?.deviceDetectedLocale || state.detectedLocale,
|
|
445
|
+
defaultLocale: state.session?.defaultLocale || DEFAULT_LOCALE,
|
|
446
|
+
fallbackLocale: DEFAULT_LOCALE,
|
|
255
447
|
});
|
|
256
448
|
state.session = {
|
|
257
449
|
...state.session,
|
|
258
|
-
|
|
450
|
+
deviceOverrideLocale: nextLocale || "",
|
|
451
|
+
locale: optimistic.locale,
|
|
452
|
+
localeSource: optimistic.source,
|
|
259
453
|
};
|
|
260
454
|
applyResolvedLocale();
|
|
455
|
+
return previousSession;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function persistLocaleOverride(nextLocale, previousSession) {
|
|
459
|
+
try {
|
|
460
|
+
const result = await apiPost("/api/session/locale", {
|
|
461
|
+
detectedLocale: state.detectedLocale,
|
|
462
|
+
overrideLocale: nextLocale || null,
|
|
463
|
+
});
|
|
464
|
+
state.session = {
|
|
465
|
+
...state.session,
|
|
466
|
+
...result,
|
|
467
|
+
};
|
|
468
|
+
applyResolvedLocale();
|
|
469
|
+
} catch (error) {
|
|
470
|
+
if (previousSession) {
|
|
471
|
+
state.session = previousSession;
|
|
472
|
+
applyResolvedLocale();
|
|
473
|
+
}
|
|
474
|
+
throw error;
|
|
475
|
+
}
|
|
261
476
|
}
|
|
262
477
|
|
|
263
478
|
function applyResolvedLocale() {
|
|
@@ -280,6 +495,8 @@ function L(key, vars = {}) {
|
|
|
280
495
|
return t(state.locale || DEFAULT_LOCALE, key, vars);
|
|
281
496
|
}
|
|
282
497
|
|
|
498
|
+
|
|
499
|
+
|
|
283
500
|
function detectBrowserLocale() {
|
|
284
501
|
if (Array.isArray(navigator.languages) && navigator.languages.length > 0) {
|
|
285
502
|
for (const value of navigator.languages) {
|
|
@@ -363,6 +580,38 @@ async function fetchA2aShareStatus() {
|
|
|
363
580
|
}
|
|
364
581
|
}
|
|
365
582
|
|
|
583
|
+
|
|
584
|
+
async function fetchHazbaseStatus() {
|
|
585
|
+
try {
|
|
586
|
+
state.hazbaseStatus = await apiGet("/api/hazbase/status");
|
|
587
|
+
} catch {
|
|
588
|
+
state.hazbaseStatus = null;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function fetchVersionStatus() {
|
|
593
|
+
try {
|
|
594
|
+
state.versionStatus = await apiGet("/api/version/status");
|
|
595
|
+
state.versionStatusError = "";
|
|
596
|
+
} catch (error) {
|
|
597
|
+
state.versionStatus = null;
|
|
598
|
+
state.versionStatusError = error.message || String(error);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function refreshVersionStatusForTechnicalPage() {
|
|
603
|
+
if (state.currentTab !== "settings" || state.settingsSubpage !== "advanced") {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
fetchVersionStatus()
|
|
607
|
+
.then(async () => {
|
|
608
|
+
if (state.currentTab === "settings" && state.settingsSubpage === "advanced" && !shouldDeferRenderForActiveInteraction()) {
|
|
609
|
+
await renderShell();
|
|
610
|
+
}
|
|
611
|
+
})
|
|
612
|
+
.catch(() => {});
|
|
613
|
+
}
|
|
614
|
+
|
|
366
615
|
async function getClientPushState() {
|
|
367
616
|
const registration = state.serviceWorkerRegistration || (await navigator.serviceWorker?.ready.catch(() => null));
|
|
368
617
|
if (registration) {
|
|
@@ -385,12 +634,41 @@ async function getClientPushState() {
|
|
|
385
634
|
}
|
|
386
635
|
|
|
387
636
|
async function refreshInbox() {
|
|
388
|
-
|
|
637
|
+
const fast = await apiGet("/api/inbox");
|
|
638
|
+
// `/api/inbox` now returns only `{ pending, completed }`. The `diff`
|
|
639
|
+
// half lives at `/api/inbox/diff` because it spawns `git` subprocesses
|
|
640
|
+
// server-side and was blocking first paint of the completed/pending
|
|
641
|
+
// lists. Preserve whatever diff entries we already have in memory so
|
|
642
|
+
// polling doesn't wipe the diff tab between diff refetches.
|
|
643
|
+
const previousDiff = Array.isArray(state.inbox?.diff) ? state.inbox.diff : [];
|
|
644
|
+
state.inbox = {
|
|
645
|
+
pending: Array.isArray(fast?.pending) ? fast.pending : [],
|
|
646
|
+
completed: Array.isArray(fast?.completed) ? fast.completed : [],
|
|
647
|
+
diff: previousDiff,
|
|
648
|
+
};
|
|
389
649
|
syncDiffThreadFilter();
|
|
390
650
|
syncCompletedThreadFilter();
|
|
391
651
|
syncInboxSubtab();
|
|
392
652
|
}
|
|
393
653
|
|
|
654
|
+
async function refreshInboxDiff() {
|
|
655
|
+
try {
|
|
656
|
+
const response = await apiGet("/api/inbox/diff");
|
|
657
|
+
const previous = state.inbox || { pending: [], completed: [] };
|
|
658
|
+
state.inbox = {
|
|
659
|
+
pending: Array.isArray(previous.pending) ? previous.pending : [],
|
|
660
|
+
completed: Array.isArray(previous.completed) ? previous.completed : [],
|
|
661
|
+
diff: Array.isArray(response?.diff) ? response.diff : [],
|
|
662
|
+
};
|
|
663
|
+
syncDiffThreadFilter();
|
|
664
|
+
syncInboxSubtab();
|
|
665
|
+
} finally {
|
|
666
|
+
// Always flip off the skeleton — a persistent error shouldn't leave the
|
|
667
|
+
// Code tab perpetually shimmering. The next poll cycle will retry.
|
|
668
|
+
state.inboxDiffLoaded = true;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
394
672
|
async function refreshTimeline() {
|
|
395
673
|
state.timeline = await apiGet("/api/timeline");
|
|
396
674
|
syncTimelineThreadFilter();
|
|
@@ -466,9 +744,9 @@ function allInboxEntries() {
|
|
|
466
744
|
return [];
|
|
467
745
|
}
|
|
468
746
|
return [
|
|
469
|
-
...state.inbox.pending.map((item) => ({ item, status: "pending" })),
|
|
747
|
+
...(Array.isArray(state.inbox.pending) ? state.inbox.pending.map((item) => ({ item, status: "pending" })) : []),
|
|
470
748
|
...(Array.isArray(state.inbox.diff) ? state.inbox.diff.map((item) => ({ item, status: "diff" })) : []),
|
|
471
|
-
...state.inbox.completed.map((item) => ({ item, status: "completed" })),
|
|
749
|
+
...(Array.isArray(state.inbox.completed) ? state.inbox.completed.map((item) => ({ item, status: "completed" })) : []),
|
|
472
750
|
];
|
|
473
751
|
}
|
|
474
752
|
|
|
@@ -565,7 +843,7 @@ function listInboxEntries() {
|
|
|
565
843
|
if (state.inboxSubtab === "completed") {
|
|
566
844
|
return filteredCompletedEntries().map((item) => ({ item, status: "completed" }));
|
|
567
845
|
}
|
|
568
|
-
return state.inbox.pending
|
|
846
|
+
return (Array.isArray(state.inbox.pending) ? state.inbox.pending : [])
|
|
569
847
|
.filter((item) => entryMatchesProviderFilter(item))
|
|
570
848
|
.map((item) => ({ item, status: "pending" }));
|
|
571
849
|
}
|
|
@@ -575,6 +853,7 @@ function normalizeProviderClient(value) {
|
|
|
575
853
|
if (normalized === "claude") return "claude";
|
|
576
854
|
if (normalized === "moltbook") return "moltbook";
|
|
577
855
|
if (normalized === "a2a") return "a2a";
|
|
856
|
+
if (normalized === "viveworker") return "viveworker";
|
|
578
857
|
return "codex";
|
|
579
858
|
}
|
|
580
859
|
|
|
@@ -583,6 +862,7 @@ function providerDisplayName(provider) {
|
|
|
583
862
|
if (p === "claude") return L("common.claude");
|
|
584
863
|
if (p === "moltbook") return "Moltbook";
|
|
585
864
|
if (p === "a2a") return "A2A";
|
|
865
|
+
if (p === "viveworker") return L("common.appName");
|
|
586
866
|
return L("common.codex");
|
|
587
867
|
}
|
|
588
868
|
|
|
@@ -914,6 +1194,10 @@ async function logout({ revokeCurrentDeviceTrust = false } = {}) {
|
|
|
914
1194
|
function resetAuthenticatedState() {
|
|
915
1195
|
state.session = null;
|
|
916
1196
|
state.inbox = null;
|
|
1197
|
+
// Reset the diff-loaded flag so the next sign-in shows the skeleton
|
|
1198
|
+
// again during the fresh /api/inbox/diff fetch instead of flashing the
|
|
1199
|
+
// empty state from the previous session's terminal value.
|
|
1200
|
+
state.inboxDiffLoaded = false;
|
|
917
1201
|
state.timeline = null;
|
|
918
1202
|
state.devices = [];
|
|
919
1203
|
state.currentItem = null;
|
|
@@ -978,6 +1262,7 @@ async function renderShell() {
|
|
|
978
1262
|
<div class="${shellClassName}">
|
|
979
1263
|
${desktop ? renderDesktopHeader(detail) : renderMobileTopBar(detail)}
|
|
980
1264
|
${renderTopBanner()}
|
|
1265
|
+
${renderGlobalErrorBanner()}
|
|
981
1266
|
<main class="app-main">
|
|
982
1267
|
${desktop ? renderDesktopWorkspace(detail) : renderMobileWorkspace(detail)}
|
|
983
1268
|
</main>
|
|
@@ -985,6 +1270,7 @@ async function renderShell() {
|
|
|
985
1270
|
${renderImageViewerModal()}
|
|
986
1271
|
${renderInstallGuideModal()}
|
|
987
1272
|
${renderLogoutConfirmModal()}
|
|
1273
|
+
${renderHazbaseLogoutConfirmModal()}
|
|
988
1274
|
</div>
|
|
989
1275
|
`;
|
|
990
1276
|
|
|
@@ -1086,6 +1372,17 @@ function shouldDeferRenderForActiveInteraction() {
|
|
|
1086
1372
|
) {
|
|
1087
1373
|
return true;
|
|
1088
1374
|
}
|
|
1375
|
+
if (
|
|
1376
|
+
activeElement instanceof HTMLInputElement &&
|
|
1377
|
+
activeElement.matches("[data-hazbase-input]")
|
|
1378
|
+
) {
|
|
1379
|
+
// User is mid-edit on the wallet sign-in email/OTP field. A poll tick
|
|
1380
|
+
// that re-renders here would blur the input and interrupt typing
|
|
1381
|
+
// (state mirroring restores value + caret position, but the blur
|
|
1382
|
+
// itself is still jarring, especially on iOS where it also dismisses
|
|
1383
|
+
// the keyboard).
|
|
1384
|
+
return true;
|
|
1385
|
+
}
|
|
1089
1386
|
if (
|
|
1090
1387
|
activeElement instanceof HTMLSelectElement &&
|
|
1091
1388
|
activeElement.matches("[data-timeline-thread-select], [data-diff-thread-select], [data-completed-thread-select]")
|
|
@@ -1937,6 +2234,12 @@ function renderCompletedCompletionCard(entry, sourceTab) {
|
|
|
1937
2234
|
const timestampLabel = formatTimelineTimestamp(item.createdAtMs);
|
|
1938
2235
|
const pillLabel = item.kind === "completion" ? L("common.task") : kindInfo.label;
|
|
1939
2236
|
const pillTone = item.kind === "completion" ? "completion" : kindInfo.tone;
|
|
2237
|
+
const compactSummary = normalizeClientText(summaryText);
|
|
2238
|
+
const useThreadAsPrimaryTitle = Boolean(threadLabel) && /^\d+$/u.test(compactSummary);
|
|
2239
|
+
const titleText = useThreadAsPrimaryTitle ? threadLabel : (summaryText || L("common.untitledItem"));
|
|
2240
|
+
const secondarySummaryHtml = useThreadAsPrimaryTitle
|
|
2241
|
+
? `<p class="item-card__summary">${escapeHtml(summaryText)}</p>`
|
|
2242
|
+
: "";
|
|
1940
2243
|
|
|
1941
2244
|
return `
|
|
1942
2245
|
<button
|
|
@@ -1958,7 +2261,8 @@ function renderCompletedCompletionCard(entry, sourceTab) {
|
|
|
1958
2261
|
</div>
|
|
1959
2262
|
<div class="item-card__content">
|
|
1960
2263
|
${threadLabel ? `<p class="item-card__thread">${escapeHtml(threadLabel)}</p>` : ""}
|
|
1961
|
-
<h3 class="item-card__title">${escapeHtml(
|
|
2264
|
+
<h3 class="item-card__title">${escapeHtml(titleText)}</h3>
|
|
2265
|
+
${secondarySummaryHtml}
|
|
1962
2266
|
</div>
|
|
1963
2267
|
</button>
|
|
1964
2268
|
`;
|
|
@@ -2007,9 +2311,16 @@ function renderTimelinePanel({ entries, desktop }) {
|
|
|
2007
2311
|
function renderDiffPanel({ entries, desktop }) {
|
|
2008
2312
|
const meta = tabMeta("diff");
|
|
2009
2313
|
const listClassName = desktop ? "diff-list diff-list--desktop" : "diff-list";
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2314
|
+
let bodyHtml;
|
|
2315
|
+
if (entries.length) {
|
|
2316
|
+
bodyHtml = `<div class="${listClassName}">${entries.map((entry) => renderDiffEntry(entry)).join("")}</div>`;
|
|
2317
|
+
} else if (!state.inboxDiffLoaded) {
|
|
2318
|
+
// First /api/inbox/diff still in flight — show shimmer cards so the
|
|
2319
|
+
// user sees something is happening instead of "no entries".
|
|
2320
|
+
bodyHtml = renderDiffSkeleton(listClassName);
|
|
2321
|
+
} else {
|
|
2322
|
+
bodyHtml = renderEmptyList("diff");
|
|
2323
|
+
}
|
|
2013
2324
|
|
|
2014
2325
|
if (!desktop) {
|
|
2015
2326
|
return `
|
|
@@ -2038,6 +2349,31 @@ function renderDiffPanel({ entries, desktop }) {
|
|
|
2038
2349
|
`;
|
|
2039
2350
|
}
|
|
2040
2351
|
|
|
2352
|
+
// Placeholder shimmer shown while the first /api/inbox/diff response is
|
|
2353
|
+
// pending. Shape mirrors `.diff-entry` so the tab doesn't visually jump when
|
|
2354
|
+
// real entries land. Three cards with decreasing prominence is enough to
|
|
2355
|
+
// signal activity without pretending to be a specific number of results.
|
|
2356
|
+
function renderDiffSkeleton(listClassName) {
|
|
2357
|
+
const card = `
|
|
2358
|
+
<div class="diff-entry diff-entry--skeleton" aria-hidden="true">
|
|
2359
|
+
<div class="diff-entry__header">
|
|
2360
|
+
<span class="diff-skeleton-line diff-skeleton-line--thread"></span>
|
|
2361
|
+
<span class="diff-skeleton-line diff-skeleton-line--time"></span>
|
|
2362
|
+
</div>
|
|
2363
|
+
<span class="diff-skeleton-line diff-skeleton-line--title"></span>
|
|
2364
|
+
<div class="diff-entry__files">
|
|
2365
|
+
<span class="diff-skeleton-chip"></span>
|
|
2366
|
+
<span class="diff-skeleton-chip diff-skeleton-chip--narrow"></span>
|
|
2367
|
+
</div>
|
|
2368
|
+
</div>
|
|
2369
|
+
`;
|
|
2370
|
+
return `
|
|
2371
|
+
<div class="${listClassName}" role="status" aria-busy="true" aria-label="${escapeHtml(L("common.loading"))}">
|
|
2372
|
+
${card}${card}${card}
|
|
2373
|
+
</div>
|
|
2374
|
+
`;
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2041
2377
|
function timelineThreadsForProvider(provider) {
|
|
2042
2378
|
const entries = Array.isArray(state.timeline?.entries) ? state.timeline.entries : [];
|
|
2043
2379
|
const byThread = new Map();
|
|
@@ -2893,6 +3229,7 @@ function buildSettingsContext() {
|
|
|
2893
3229
|
moltbookScout: state.moltbookScoutStatus,
|
|
2894
3230
|
a2aRelay: state.a2aRelayStatus,
|
|
2895
3231
|
a2aShare: state.a2aShareStatus,
|
|
3232
|
+
hazbase: state.hazbaseStatus,
|
|
2896
3233
|
};
|
|
2897
3234
|
}
|
|
2898
3235
|
|
|
@@ -3039,6 +3376,13 @@ function settingsPageMeta(page) {
|
|
|
3039
3376
|
description: L("settings.awayMode.copy"),
|
|
3040
3377
|
icon: "settings",
|
|
3041
3378
|
};
|
|
3379
|
+
case "autoPilot":
|
|
3380
|
+
return {
|
|
3381
|
+
id: "autoPilot",
|
|
3382
|
+
title: L("settings.autoPilot.title"),
|
|
3383
|
+
description: L("settings.autoPilot.copy"),
|
|
3384
|
+
icon: "approval",
|
|
3385
|
+
};
|
|
3042
3386
|
case "moltbook":
|
|
3043
3387
|
return {
|
|
3044
3388
|
id: "moltbook",
|
|
@@ -3060,6 +3404,13 @@ function settingsPageMeta(page) {
|
|
|
3060
3404
|
description: L("settings.a2aShare.copy"),
|
|
3061
3405
|
icon: "link",
|
|
3062
3406
|
};
|
|
3407
|
+
case "wallet":
|
|
3408
|
+
return {
|
|
3409
|
+
id: "wallet",
|
|
3410
|
+
title: L("settings.wallet.title"),
|
|
3411
|
+
description: L("settings.wallet.copy"),
|
|
3412
|
+
icon: "coin",
|
|
3413
|
+
};
|
|
3063
3414
|
case "a2aExecutor":
|
|
3064
3415
|
// Executor settings integrated into a2aRelay page — redirect.
|
|
3065
3416
|
return settingsPageMeta("a2aRelay");
|
|
@@ -3097,6 +3448,15 @@ function renderSettingsRoot(context, { mobile }) {
|
|
|
3097
3448
|
title: L("settings.awayMode.title"),
|
|
3098
3449
|
value: state.session?.claudeAwayMode === true ? L("settings.claudeAway.on") : L("settings.claudeAway.off"),
|
|
3099
3450
|
}),
|
|
3451
|
+
renderSettingsNavRow({
|
|
3452
|
+
page: "autoPilot",
|
|
3453
|
+
icon: "approval",
|
|
3454
|
+
title: L("settings.autoPilot.title"),
|
|
3455
|
+
value:
|
|
3456
|
+
state.session?.autoPilotTrustedReads === true || state.session?.autoPilotTrustedWrites === true
|
|
3457
|
+
? L("common.enabled")
|
|
3458
|
+
: L("common.disabled"),
|
|
3459
|
+
}),
|
|
3100
3460
|
].filter(Boolean);
|
|
3101
3461
|
const deviceRows = [
|
|
3102
3462
|
renderSettingsNavRow({
|
|
@@ -3132,7 +3492,7 @@ function renderSettingsRoot(context, { mobile }) {
|
|
|
3132
3492
|
`
|
|
3133
3493
|
}
|
|
3134
3494
|
${renderSettingsGroup(L("settings.group.general"), generalRows)}
|
|
3135
|
-
${(state.session?.moltbookEnabled || state.session?.a2aRelayEnabled || state.session?.a2aShareEnabled) ? renderSettingsGroup(L("settings.group.integrations"), [
|
|
3495
|
+
${(state.session?.moltbookEnabled || state.session?.a2aRelayEnabled || state.session?.a2aShareEnabled || context.hazbase?.enabled) ? renderSettingsGroup(L("settings.group.integrations"), [
|
|
3136
3496
|
state.session?.moltbookEnabled ? renderSettingsNavRow({
|
|
3137
3497
|
page: "moltbook",
|
|
3138
3498
|
icon: "item",
|
|
@@ -3154,6 +3514,15 @@ function renderSettingsRoot(context, { mobile }) {
|
|
|
3154
3514
|
subtitle: L("settings.a2aShare.subtitle"),
|
|
3155
3515
|
value: context.a2aShare?.enabled ? L("settings.status.enabled") : L("settings.status.disabled"),
|
|
3156
3516
|
}) : "",
|
|
3517
|
+
context.hazbase?.enabled ? renderSettingsNavRow({
|
|
3518
|
+
page: "wallet",
|
|
3519
|
+
icon: "coin",
|
|
3520
|
+
title: L("settings.wallet.title"),
|
|
3521
|
+
subtitle: L("settings.wallet.subtitle"),
|
|
3522
|
+
value: context.hazbase?.sessionInvalid
|
|
3523
|
+
? L("settings.hazbase.status.sessionExpired")
|
|
3524
|
+
: context.hazbase?.signedIn ? L("settings.hazbase.status.signedIn") : L("settings.hazbase.status.signedOut"),
|
|
3525
|
+
}) : "",
|
|
3157
3526
|
].filter(Boolean)) : ""}
|
|
3158
3527
|
${renderSettingsGroup(L("settings.pairing.title"), deviceRows)}
|
|
3159
3528
|
${renderSettingsGroup(L("settings.group.advanced"), advancedRows)}
|
|
@@ -3198,6 +3567,9 @@ function renderSettingsSubpage(context, { mobile }) {
|
|
|
3198
3567
|
case "awayMode":
|
|
3199
3568
|
content = renderSettingsAwayModePage();
|
|
3200
3569
|
break;
|
|
3570
|
+
case "autoPilot":
|
|
3571
|
+
content = renderSettingsAutoPilotPage();
|
|
3572
|
+
break;
|
|
3201
3573
|
case "moltbook":
|
|
3202
3574
|
content = renderSettingsMoltbookPage(context);
|
|
3203
3575
|
break;
|
|
@@ -3208,6 +3580,9 @@ function renderSettingsSubpage(context, { mobile }) {
|
|
|
3208
3580
|
case "a2aShare":
|
|
3209
3581
|
content = renderSettingsA2aSharePage(context);
|
|
3210
3582
|
break;
|
|
3583
|
+
case "wallet":
|
|
3584
|
+
content = renderSettingsWalletPage(context);
|
|
3585
|
+
break;
|
|
3211
3586
|
default:
|
|
3212
3587
|
content = "";
|
|
3213
3588
|
}
|
|
@@ -3329,6 +3704,7 @@ function renderSettingsDevicePage(context) {
|
|
|
3329
3704
|
}
|
|
3330
3705
|
|
|
3331
3706
|
function renderSettingsAdvancedPage(context) {
|
|
3707
|
+
const versionNotice = renderVersionUpdateNotice();
|
|
3332
3708
|
return `
|
|
3333
3709
|
<div class="settings-page">
|
|
3334
3710
|
${context.diagnostics.map((message) => `<p class="inline-alert">${escapeHtml(message)}</p>`).join("")}
|
|
@@ -3347,10 +3723,28 @@ function renderSettingsAdvancedPage(context) {
|
|
|
3347
3723
|
: "",
|
|
3348
3724
|
renderSettingsInfoRow(L("settings.row.version"), state.appVersion || L("common.unavailable")),
|
|
3349
3725
|
].filter(Boolean), { listClassName: "settings-list settings-list--compact" })}
|
|
3726
|
+
${versionNotice}
|
|
3350
3727
|
</div>
|
|
3351
3728
|
`;
|
|
3352
3729
|
}
|
|
3353
3730
|
|
|
3731
|
+
function renderVersionUpdateNotice() {
|
|
3732
|
+
const status = state.versionStatus;
|
|
3733
|
+
if (!status?.updateAvailable || !status.latestVersion) {
|
|
3734
|
+
return "";
|
|
3735
|
+
}
|
|
3736
|
+
return `
|
|
3737
|
+
<section class="settings-copy-block settings-copy-block--compact settings-update-notice">
|
|
3738
|
+
<p class="settings-update-notice__title">${escapeHtml(L("settings.updateAvailable.title"))}</p>
|
|
3739
|
+
<p class="muted">${escapeHtml(L("settings.updateAvailable.copy", {
|
|
3740
|
+
current: status.currentVersion || state.appVersion || "",
|
|
3741
|
+
latest: status.latestVersion,
|
|
3742
|
+
}))}</p>
|
|
3743
|
+
<code class="settings-update-notice__command">npx viveworker update</code>
|
|
3744
|
+
</section>
|
|
3745
|
+
`;
|
|
3746
|
+
}
|
|
3747
|
+
|
|
3354
3748
|
function renderSettingsNotificationActions({ push, canEnable, standalone }) {
|
|
3355
3749
|
if (push.serverSubscribed) {
|
|
3356
3750
|
return `
|
|
@@ -3423,6 +3817,456 @@ function renderSettingsAwayModePage() {
|
|
|
3423
3817
|
`;
|
|
3424
3818
|
}
|
|
3425
3819
|
|
|
3820
|
+
function renderSettingsAutoPilotPage() {
|
|
3821
|
+
const trustedReadsEnabled = state.session?.autoPilotTrustedReads === true;
|
|
3822
|
+
const trustedReadsStateLabel = trustedReadsEnabled ? L("common.enabled") : L("common.disabled");
|
|
3823
|
+
const writeLaneContentEnabled = state.session?.autoPilotWriteLaneContent === true;
|
|
3824
|
+
const writeLaneUiTestsEnabled = state.session?.autoPilotWriteLaneUiTests === true;
|
|
3825
|
+
const writeLaneSourceEnabled = state.session?.autoPilotWriteLaneSource === true;
|
|
3826
|
+
const trustedWritesEnabled =
|
|
3827
|
+
writeLaneContentEnabled || writeLaneUiTestsEnabled || writeLaneSourceEnabled || state.session?.autoPilotTrustedWrites === true;
|
|
3828
|
+
const trustedWritesStateLabel = trustedWritesEnabled ? L("common.enabled") : L("common.disabled");
|
|
3829
|
+
const recentEntries = recentAutoPilotEntries();
|
|
3830
|
+
const suggestions = recentAutoPilotSuggestions();
|
|
3831
|
+
return `
|
|
3832
|
+
<div class="settings-page">
|
|
3833
|
+
${renderSettingsGroup("", [`
|
|
3834
|
+
<label class="reply-mode-switch reply-mode-switch--grouped" data-auto-pilot-toggle>
|
|
3835
|
+
<input type="checkbox" class="reply-mode-switch__input" ${trustedReadsEnabled ? "checked" : ""} data-auto-pilot-checkbox />
|
|
3836
|
+
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3837
|
+
<span class="reply-mode-switch__copy">
|
|
3838
|
+
<span class="reply-mode-switch__title">
|
|
3839
|
+
<span>${escapeHtml(L("settings.autoPilot.trustedReadsTitle"))}</span>
|
|
3840
|
+
<span class="reply-mode-switch__state">${escapeHtml(trustedReadsStateLabel)}</span>
|
|
3841
|
+
</span>
|
|
3842
|
+
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.autoPilot.trustedReadsDescription"))}</span>
|
|
3843
|
+
</span>
|
|
3844
|
+
</label>
|
|
3845
|
+
`, `
|
|
3846
|
+
<div class="settings-toggle-subhead" role="presentation">
|
|
3847
|
+
<span class="settings-toggle-subhead__title">${escapeHtml(L("settings.autoPilot.trustedWritesTitle"))}</span>
|
|
3848
|
+
<span class="settings-toggle-subhead__state">${escapeHtml(trustedWritesStateLabel)}</span>
|
|
3849
|
+
</div>
|
|
3850
|
+
`, `
|
|
3851
|
+
<div class="settings-toggle-subcopy muted">${escapeHtml(L("settings.autoPilot.trustedWritesDescription"))}</div>
|
|
3852
|
+
`, `
|
|
3853
|
+
<label class="reply-mode-switch reply-mode-switch--grouped" data-auto-pilot-write-lane="content">
|
|
3854
|
+
<input type="checkbox" class="reply-mode-switch__input" ${writeLaneContentEnabled ? "checked" : ""} data-auto-pilot-write-lane-checkbox="content" />
|
|
3855
|
+
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3856
|
+
<span class="reply-mode-switch__copy">
|
|
3857
|
+
<span class="reply-mode-switch__title">
|
|
3858
|
+
<span>${escapeHtml(L("settings.autoPilot.writeLaneContentTitle"))}</span>
|
|
3859
|
+
<span class="reply-mode-switch__state">${escapeHtml(writeLaneContentEnabled ? L("common.enabled") : L("common.disabled"))}</span>
|
|
3860
|
+
</span>
|
|
3861
|
+
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.autoPilot.writeLaneContentDescription"))}</span>
|
|
3862
|
+
</span>
|
|
3863
|
+
</label>
|
|
3864
|
+
`, `
|
|
3865
|
+
<label class="reply-mode-switch reply-mode-switch--grouped" data-auto-pilot-write-lane="ui-tests">
|
|
3866
|
+
<input type="checkbox" class="reply-mode-switch__input" ${writeLaneUiTestsEnabled ? "checked" : ""} data-auto-pilot-write-lane-checkbox="ui-tests" />
|
|
3867
|
+
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3868
|
+
<span class="reply-mode-switch__copy">
|
|
3869
|
+
<span class="reply-mode-switch__title">
|
|
3870
|
+
<span>${escapeHtml(L("settings.autoPilot.writeLaneUiTestsTitle"))}</span>
|
|
3871
|
+
<span class="reply-mode-switch__state">${escapeHtml(writeLaneUiTestsEnabled ? L("common.enabled") : L("common.disabled"))}</span>
|
|
3872
|
+
</span>
|
|
3873
|
+
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.autoPilot.writeLaneUiTestsDescription"))}</span>
|
|
3874
|
+
</span>
|
|
3875
|
+
</label>
|
|
3876
|
+
`, `
|
|
3877
|
+
<label class="reply-mode-switch reply-mode-switch--grouped" data-auto-pilot-write-lane="source">
|
|
3878
|
+
<input type="checkbox" class="reply-mode-switch__input" ${writeLaneSourceEnabled ? "checked" : ""} data-auto-pilot-write-lane-checkbox="source" />
|
|
3879
|
+
<span class="reply-mode-switch__track" aria-hidden="true"><span class="reply-mode-switch__thumb"></span></span>
|
|
3880
|
+
<span class="reply-mode-switch__copy">
|
|
3881
|
+
<span class="reply-mode-switch__title">
|
|
3882
|
+
<span>${escapeHtml(L("settings.autoPilot.writeLaneSourceTitle"))}</span>
|
|
3883
|
+
<span class="reply-mode-switch__state">${escapeHtml(writeLaneSourceEnabled ? L("common.enabled") : L("common.disabled"))}</span>
|
|
3884
|
+
</span>
|
|
3885
|
+
<span class="reply-mode-switch__hint">${escapeHtml(L("settings.autoPilot.writeLaneSourceDescription"))}</span>
|
|
3886
|
+
</span>
|
|
3887
|
+
</label>
|
|
3888
|
+
`], { listClassName: "settings-list settings-list--toggle-group" })}
|
|
3889
|
+
<p class="settings-page-copy muted">${escapeHtml(L("settings.autoPilot.scopeNote"))}</p>
|
|
3890
|
+
${
|
|
3891
|
+
suggestions.length
|
|
3892
|
+
? renderSettingsGroup(
|
|
3893
|
+
L("settings.autoPilot.suggestionsTitle"),
|
|
3894
|
+
suggestions.map((suggestion) => renderSettingsAutoPilotSuggestion(suggestion))
|
|
3895
|
+
)
|
|
3896
|
+
: ""
|
|
3897
|
+
}
|
|
3898
|
+
${
|
|
3899
|
+
recentEntries.length
|
|
3900
|
+
? renderSettingsGroup(
|
|
3901
|
+
L("settings.autoPilot.recentTitle"),
|
|
3902
|
+
recentEntries.map((item) => renderSettingsAutoPilotRecentEntry(item))
|
|
3903
|
+
)
|
|
3904
|
+
: `
|
|
3905
|
+
<section class="settings-group">
|
|
3906
|
+
<p class="settings-group__title">${escapeHtml(L("settings.autoPilot.recentTitle"))}</p>
|
|
3907
|
+
<div class="settings-copy-block settings-copy-block--compact">
|
|
3908
|
+
<p class="muted">${escapeHtml(L("settings.autoPilot.recentEmpty"))}</p>
|
|
3909
|
+
</div>
|
|
3910
|
+
</section>
|
|
3911
|
+
`
|
|
3912
|
+
}
|
|
3913
|
+
</div>
|
|
3914
|
+
`;
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3917
|
+
function recentAutoPilotEntries(limit = 5) {
|
|
3918
|
+
const entries = Array.isArray(state.timeline?.entries) ? state.timeline.entries : [];
|
|
3919
|
+
return entries
|
|
3920
|
+
.filter((entry) => isAutoPilotApprovalEntry(entry))
|
|
3921
|
+
.sort((a, b) => (Number(b.createdAtMs) || 0) - (Number(a.createdAtMs) || 0))
|
|
3922
|
+
.slice(0, limit);
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
function isAutoPilotApprovalEntry(entry) {
|
|
3926
|
+
const stableId = normalizeClientText(entry?.stableId || "");
|
|
3927
|
+
return (
|
|
3928
|
+
normalizeClientText(entry?.kind || "") === "approval" &&
|
|
3929
|
+
normalizeClientText(entry?.outcome || "") === "approved" &&
|
|
3930
|
+
(stableId.endsWith(":autopilot") || stableId.includes(":autopilot-write"))
|
|
3931
|
+
);
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
function autoPilotEntryMode(item) {
|
|
3935
|
+
const stableId = normalizeClientText(item?.stableId || "");
|
|
3936
|
+
return stableId.includes(":autopilot-write") ? "write" : "read";
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
function autoPilotEntryWriteLane(item) {
|
|
3940
|
+
const stableId = normalizeClientText(item?.stableId || "");
|
|
3941
|
+
const match = stableId.match(/:autopilot-write:([a-z_-]+)$/u);
|
|
3942
|
+
return normalizeClientText(match?.[1] || "");
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
function isManualApprovedWriteEntry(entry) {
|
|
3946
|
+
const stableId = normalizeClientText(entry?.stableId || "");
|
|
3947
|
+
return (
|
|
3948
|
+
normalizeClientText(entry?.kind || "") === "approval" &&
|
|
3949
|
+
normalizeClientText(entry?.outcome || "") === "approved" &&
|
|
3950
|
+
!stableId.includes(":autopilot") &&
|
|
3951
|
+
normalizeClientFileRefs(entry?.fileRefs).length > 0 &&
|
|
3952
|
+
normalizeClientText(entry?.diffText || "").length > 0
|
|
3953
|
+
);
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
function autoPilotDeniedWritePathClient(fileRef) {
|
|
3957
|
+
const normalized = normalizeClientText(fileRef || "");
|
|
3958
|
+
if (!normalized) {
|
|
3959
|
+
return true;
|
|
3960
|
+
}
|
|
3961
|
+
const lower = normalized.toLowerCase();
|
|
3962
|
+
const segments = lower.split(/[\\/]+/u).filter(Boolean);
|
|
3963
|
+
const basename = segments[segments.length - 1] || "";
|
|
3964
|
+
if (
|
|
3965
|
+
segments.some((segment) => [".ssh", ".aws", ".gnupg", ".azure", ".kube", ".github", ".gitlab", ".terraform", ".claude", ".husky", ".vscode"].includes(segment))
|
|
3966
|
+
) {
|
|
3967
|
+
return true;
|
|
3968
|
+
}
|
|
3969
|
+
if (basename === ".npmrc" || basename === ".netrc" || basename === ".env" || basename.startsWith(".env.")) {
|
|
3970
|
+
return true;
|
|
3971
|
+
}
|
|
3972
|
+
if (basename.endsWith(".pem") || basename.endsWith(".key") || basename.endsWith(".p12") || basename.endsWith(".pfx")) {
|
|
3973
|
+
return true;
|
|
3974
|
+
}
|
|
3975
|
+
if (
|
|
3976
|
+
[
|
|
3977
|
+
"package.json",
|
|
3978
|
+
"package-lock.json",
|
|
3979
|
+
"pnpm-lock.yaml",
|
|
3980
|
+
"yarn.lock",
|
|
3981
|
+
"bun.lockb",
|
|
3982
|
+
"cargo.toml",
|
|
3983
|
+
"cargo.lock",
|
|
3984
|
+
"gemfile",
|
|
3985
|
+
"gemfile.lock",
|
|
3986
|
+
"podfile",
|
|
3987
|
+
"podfile.lock",
|
|
3988
|
+
"composer.json",
|
|
3989
|
+
"composer.lock",
|
|
3990
|
+
"pipfile",
|
|
3991
|
+
"pipfile.lock",
|
|
3992
|
+
"poetry.lock",
|
|
3993
|
+
"requirements.txt",
|
|
3994
|
+
"dockerfile",
|
|
3995
|
+
"wrangler.toml",
|
|
3996
|
+
"tsconfig.json",
|
|
3997
|
+
"tsconfig.tsbuildinfo",
|
|
3998
|
+
].includes(basename)
|
|
3999
|
+
) {
|
|
4000
|
+
return true;
|
|
4001
|
+
}
|
|
4002
|
+
return /^id_[a-z0-9._-]+$/iu.test(basename) || basename.includes("secret") || basename.includes("credential");
|
|
4003
|
+
}
|
|
4004
|
+
|
|
4005
|
+
function autoPilotContentWritePathClient(fileRef) {
|
|
4006
|
+
const normalized = normalizeClientText(fileRef || "");
|
|
4007
|
+
if (!normalized || autoPilotDeniedWritePathClient(normalized)) {
|
|
4008
|
+
return false;
|
|
4009
|
+
}
|
|
4010
|
+
const lower = normalized.toLowerCase();
|
|
4011
|
+
const segments = lower.split(/[\\/]+/u).filter(Boolean);
|
|
4012
|
+
const basename = segments[segments.length - 1] || "";
|
|
4013
|
+
const extension = basename.includes(".") ? `.${basename.split(".").pop().toLowerCase()}` : "";
|
|
4014
|
+
const basenameWithoutExtension = extension ? basename.slice(0, -extension.length) : basename;
|
|
4015
|
+
if ([".md", ".mdx", ".txt", ".rst", ".adoc"].includes(extension)) {
|
|
4016
|
+
return true;
|
|
4017
|
+
}
|
|
4018
|
+
if (["license", "notice", "copying", "readme", "changelog", "contributing"].includes(basenameWithoutExtension.toLowerCase())) {
|
|
4019
|
+
return true;
|
|
4020
|
+
}
|
|
4021
|
+
if (segments.includes("i18n") && [".js", ".ts", ".json", ".yaml", ".yml"].includes(extension)) {
|
|
4022
|
+
return true;
|
|
4023
|
+
}
|
|
4024
|
+
if (segments.includes("messages") && extension === ".json") {
|
|
4025
|
+
return true;
|
|
4026
|
+
}
|
|
4027
|
+
return false;
|
|
4028
|
+
}
|
|
4029
|
+
|
|
4030
|
+
function autoPilotUiTestsWritePathClient(fileRef) {
|
|
4031
|
+
const normalized = normalizeClientText(fileRef || "");
|
|
4032
|
+
if (!normalized || autoPilotDeniedWritePathClient(normalized)) {
|
|
4033
|
+
return false;
|
|
4034
|
+
}
|
|
4035
|
+
const lower = normalized.toLowerCase();
|
|
4036
|
+
const segments = lower.split(/[\\/]+/u).filter(Boolean);
|
|
4037
|
+
const basename = segments[segments.length - 1] || "";
|
|
4038
|
+
const extension = basename.includes(".") ? `.${basename.split(".").pop().toLowerCase()}` : "";
|
|
4039
|
+
if ([".css", ".scss", ".sass", ".less", ".styl", ".pcss"].includes(extension)) {
|
|
4040
|
+
return true;
|
|
4041
|
+
}
|
|
4042
|
+
if (segments.includes("__tests__") || /\.(test|spec)\.[cm]?[jt]sx?$/u.test(basename)) {
|
|
4043
|
+
return true;
|
|
4044
|
+
}
|
|
4045
|
+
if ((segments.includes("web") || segments.includes("components")) && [".js", ".jsx", ".ts", ".tsx", ".html"].includes(extension)) {
|
|
4046
|
+
return true;
|
|
4047
|
+
}
|
|
4048
|
+
return false;
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
function autoPilotSourceWritePathClient(fileRef) {
|
|
4052
|
+
const normalized = normalizeClientText(fileRef || "");
|
|
4053
|
+
if (!normalized || autoPilotDeniedWritePathClient(normalized)) {
|
|
4054
|
+
return false;
|
|
4055
|
+
}
|
|
4056
|
+
if (autoPilotContentWritePathClient(normalized) || autoPilotUiTestsWritePathClient(normalized)) {
|
|
4057
|
+
return false;
|
|
4058
|
+
}
|
|
4059
|
+
return /\.(?:[cm]?[jt]sx?)$/u.test(normalized);
|
|
4060
|
+
}
|
|
4061
|
+
|
|
4062
|
+
function diffAddedLinesClient(diffText) {
|
|
4063
|
+
return String(diffText || "")
|
|
4064
|
+
.replace(/\r\n/gu, "\n")
|
|
4065
|
+
.split("\n")
|
|
4066
|
+
.filter((line) => line.startsWith("+") && !line.startsWith("+++"))
|
|
4067
|
+
.map((line) => line.slice(1));
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
function addedDiffLinesContainClient(diffText, pattern) {
|
|
4071
|
+
return diffAddedLinesClient(diffText).some((line) => pattern.test(line));
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
function hasUnsafeUiOrTestWriteDiffClient(diffText) {
|
|
4075
|
+
const patterns = [
|
|
4076
|
+
/\bprocess\.env\b/u,
|
|
4077
|
+
/\b(?:child_process|spawn|exec|execFile|fork)\b/u,
|
|
4078
|
+
/\bfs\.(?:write|append|rm|unlink|rename|chmod|chown|copyFile|cp)\b/u,
|
|
4079
|
+
/\b(?:fetch|axios|XMLHttpRequest)\s*\(/u,
|
|
4080
|
+
/\bcrypto\b/u,
|
|
4081
|
+
/\b(?:secret|token|password|privateKey|credential)\b/iu,
|
|
4082
|
+
];
|
|
4083
|
+
return (
|
|
4084
|
+
addedDiffLinesContainClient(diffText, /^\s*(?:import\s|export\s+.*\s+from\s)/u) ||
|
|
4085
|
+
addedDiffLinesContainClient(diffText, /\brequire\s*\(/u) ||
|
|
4086
|
+
patterns.some((pattern) => addedDiffLinesContainClient(diffText, pattern))
|
|
4087
|
+
);
|
|
4088
|
+
}
|
|
4089
|
+
|
|
4090
|
+
function hasUnsafeSourceWriteDiffClient(diffText) {
|
|
4091
|
+
const patterns = [
|
|
4092
|
+
/\bprocess\.env\b/u,
|
|
4093
|
+
/\b(?:child_process|spawn|exec|execFile|fork)\b/u,
|
|
4094
|
+
/\bfs\.(?:write|append|rm|unlink|rename|chmod|chown|copyFile|cp)\b/u,
|
|
4095
|
+
/\b(?:fetch|axios|XMLHttpRequest)\s*\(/u,
|
|
4096
|
+
/\b(?:net|tls|dgram|http2?)\b/u,
|
|
4097
|
+
/\bcrypto\b/u,
|
|
4098
|
+
/\b(?:secret|token|password|privateKey|credential)\b/iu,
|
|
4099
|
+
];
|
|
4100
|
+
return (
|
|
4101
|
+
addedDiffLinesContainClient(diffText, /^\s*(?:import\s|export\s+.*\s+from\s)/u) ||
|
|
4102
|
+
addedDiffLinesContainClient(diffText, /\brequire\s*\(/u) ||
|
|
4103
|
+
patterns.some((pattern) => addedDiffLinesContainClient(diffText, pattern))
|
|
4104
|
+
);
|
|
4105
|
+
}
|
|
4106
|
+
|
|
4107
|
+
function classifyManualWriteLaneSuggestion(entry) {
|
|
4108
|
+
const fileRefs = normalizeClientFileRefs(entry?.fileRefs);
|
|
4109
|
+
const diffText = normalizeClientText(entry?.diffText || "");
|
|
4110
|
+
const added = Math.max(0, Number(entry?.diffAddedLines) || 0);
|
|
4111
|
+
const removed = Math.max(0, Number(entry?.diffRemovedLines) || 0);
|
|
4112
|
+
const totalChangedLines = added + removed;
|
|
4113
|
+
if (!fileRefs.length || !diffText || totalChangedLines === 0) {
|
|
4114
|
+
return "";
|
|
4115
|
+
}
|
|
4116
|
+
if (/^(?:new file mode|deleted file mode|rename from|rename to|old mode|new mode|similarity index|dissimilarity index|GIT binary patch|Binary files )/mu.test(diffText)) {
|
|
4117
|
+
return "";
|
|
4118
|
+
}
|
|
4119
|
+
if (
|
|
4120
|
+
fileRefs.length >= 1 &&
|
|
4121
|
+
fileRefs.length <= 3 &&
|
|
4122
|
+
totalChangedLines <= 120 &&
|
|
4123
|
+
fileRefs.every((fileRef) => autoPilotContentWritePathClient(fileRef))
|
|
4124
|
+
) {
|
|
4125
|
+
return "content";
|
|
4126
|
+
}
|
|
4127
|
+
if (
|
|
4128
|
+
fileRefs.length >= 1 &&
|
|
4129
|
+
fileRefs.length <= 2 &&
|
|
4130
|
+
totalChangedLines <= 80 &&
|
|
4131
|
+
fileRefs.every((fileRef) => autoPilotUiTestsWritePathClient(fileRef)) &&
|
|
4132
|
+
!hasUnsafeUiOrTestWriteDiffClient(diffText)
|
|
4133
|
+
) {
|
|
4134
|
+
return "ui_tests";
|
|
4135
|
+
}
|
|
4136
|
+
if (
|
|
4137
|
+
fileRefs.length === 1 &&
|
|
4138
|
+
totalChangedLines <= 40 &&
|
|
4139
|
+
fileRefs.every((fileRef) => autoPilotSourceWritePathClient(fileRef)) &&
|
|
4140
|
+
!hasUnsafeSourceWriteDiffClient(diffText)
|
|
4141
|
+
) {
|
|
4142
|
+
return "source";
|
|
4143
|
+
}
|
|
4144
|
+
return "";
|
|
4145
|
+
}
|
|
4146
|
+
|
|
4147
|
+
function isWriteLaneEnabled(lane) {
|
|
4148
|
+
return lane === "content"
|
|
4149
|
+
? state.session?.autoPilotWriteLaneContent === true
|
|
4150
|
+
: lane === "ui_tests"
|
|
4151
|
+
? state.session?.autoPilotWriteLaneUiTests === true
|
|
4152
|
+
: state.session?.autoPilotWriteLaneSource === true;
|
|
4153
|
+
}
|
|
4154
|
+
|
|
4155
|
+
function recentAutoPilotSuggestions(limit = 40, minCount = 3) {
|
|
4156
|
+
const entries = Array.isArray(state.timeline?.entries) ? state.timeline.entries : [];
|
|
4157
|
+
const counts = new Map();
|
|
4158
|
+
for (const entry of entries
|
|
4159
|
+
.filter((item) => isManualApprovedWriteEntry(item))
|
|
4160
|
+
.sort((a, b) => (Number(b.createdAtMs) || 0) - (Number(a.createdAtMs) || 0))
|
|
4161
|
+
.slice(0, limit)) {
|
|
4162
|
+
const lane = classifyManualWriteLaneSuggestion(entry);
|
|
4163
|
+
if (!lane || isWriteLaneEnabled(lane)) {
|
|
4164
|
+
continue;
|
|
4165
|
+
}
|
|
4166
|
+
counts.set(lane, (counts.get(lane) || 0) + 1);
|
|
4167
|
+
}
|
|
4168
|
+
return ["content", "ui_tests", "source"]
|
|
4169
|
+
.map((lane) => ({ lane, count: counts.get(lane) || 0 }))
|
|
4170
|
+
.filter((item) => item.count >= minCount);
|
|
4171
|
+
}
|
|
4172
|
+
|
|
4173
|
+
function firstMarkdownCodeFence(text) {
|
|
4174
|
+
const match = String(text || "").match(/```(?:\w+)?\n([\s\S]*?)\n```/u);
|
|
4175
|
+
return normalizeClientText(match?.[1] || "");
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
function truncateUiText(value, maxGlyphs = 92) {
|
|
4179
|
+
const normalized = normalizeClientText(value || "");
|
|
4180
|
+
if (!normalized) {
|
|
4181
|
+
return "";
|
|
4182
|
+
}
|
|
4183
|
+
const glyphs = Array.from(normalized);
|
|
4184
|
+
return glyphs.length > maxGlyphs ? `${glyphs.slice(0, maxGlyphs).join("")}…` : normalized;
|
|
4185
|
+
}
|
|
4186
|
+
|
|
4187
|
+
function autoPilotEntryHeadline(item) {
|
|
4188
|
+
const mode = autoPilotEntryMode(item);
|
|
4189
|
+
if (mode === "read") {
|
|
4190
|
+
return truncateUiText(firstMarkdownCodeFence(item?.messageText || "") || item?.summary || item?.title || "");
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
const fileRefs = normalizeClientFileRefs(item?.fileRefs);
|
|
4194
|
+
const primaryRef = fileRefs[0] || item?.summary || item?.title || "";
|
|
4195
|
+
const extraCount = Math.max(0, fileRefs.length - 1);
|
|
4196
|
+
const stats =
|
|
4197
|
+
Number.isFinite(Number(item?.diffAddedLines)) && Number.isFinite(Number(item?.diffRemovedLines))
|
|
4198
|
+
? ` (+${Math.max(0, Number(item?.diffAddedLines) || 0)} / -${Math.max(0, Number(item?.diffRemovedLines) || 0)})`
|
|
4199
|
+
: "";
|
|
4200
|
+
return truncateUiText(`${primaryRef}${extraCount > 0 ? ` +${extraCount}` : ""}${stats}`);
|
|
4201
|
+
}
|
|
4202
|
+
|
|
4203
|
+
function renderSettingsAutoPilotRecentEntry(item) {
|
|
4204
|
+
const mode = autoPilotEntryMode(item);
|
|
4205
|
+
const badgeLabel = mode === "write" ? L("settings.autoPilot.recentWrite") : L("settings.autoPilot.recentRead");
|
|
4206
|
+
const badgeClass = mode === "write" ? "settings-compose-badge--write" : "settings-compose-badge--read";
|
|
4207
|
+
const iconName = mode === "write" ? "file-event" : "approval";
|
|
4208
|
+
const iconToneClass = mode === "write" ? "settings-icon-entry__icon--write" : "settings-icon-entry__icon--read";
|
|
4209
|
+
const headline = autoPilotEntryHeadline(item) || L("common.untitledItem");
|
|
4210
|
+
const threadLabel = resolvedThreadLabel(item?.threadId || "", item?.threadLabel || "");
|
|
4211
|
+
const laneLabel = ({
|
|
4212
|
+
content: L("settings.autoPilot.recentContent"),
|
|
4213
|
+
"ui_tests": L("settings.autoPilot.recentUiTests"),
|
|
4214
|
+
source: L("settings.autoPilot.recentSource"),
|
|
4215
|
+
}[autoPilotEntryWriteLane(item)] || "");
|
|
4216
|
+
const metaParts = [providerDisplayName(item?.provider), formatTimelineTimestamp(item?.createdAtMs)].filter(Boolean);
|
|
4217
|
+
|
|
4218
|
+
return `
|
|
4219
|
+
<button
|
|
4220
|
+
type="button"
|
|
4221
|
+
class="settings-compose-entry settings-icon-entry settings-autopilot-entry"
|
|
4222
|
+
data-open-item-kind="${escapeHtml(item.kind)}"
|
|
4223
|
+
data-open-item-token="${escapeHtml(item.token)}"
|
|
4224
|
+
data-source-tab="timeline"
|
|
4225
|
+
>
|
|
4226
|
+
<span class="settings-icon-entry__icon ${iconToneClass}" aria-hidden="true">${renderIcon(iconName)}</span>
|
|
4227
|
+
<span class="settings-icon-entry__body">
|
|
4228
|
+
<span class="settings-icon-entry__title-row">
|
|
4229
|
+
<span class="settings-compose-entry__title">${escapeHtml(headline)}</span>
|
|
4230
|
+
<span class="settings-compose-badge ${badgeClass}">${escapeHtml(badgeLabel)}</span>
|
|
4231
|
+
${mode === "write" && laneLabel ? `<span class="settings-compose-badge settings-compose-badge--lane">${escapeHtml(laneLabel)}</span>` : ""}
|
|
4232
|
+
</span>
|
|
4233
|
+
<span class="settings-autopilot-entry__meta">${escapeHtml(metaParts.join(" · "))}</span>
|
|
4234
|
+
${threadLabel ? `<span class="settings-autopilot-entry__thread">${escapeHtml(threadLabel)}</span>` : ""}
|
|
4235
|
+
</span>
|
|
4236
|
+
</button>
|
|
4237
|
+
`;
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
function renderSettingsAutoPilotSuggestion({ lane, count }) {
|
|
4241
|
+
const title =
|
|
4242
|
+
lane === "content"
|
|
4243
|
+
? L("settings.autoPilot.suggestionContentTitle")
|
|
4244
|
+
: lane === "ui_tests"
|
|
4245
|
+
? L("settings.autoPilot.suggestionUiTestsTitle")
|
|
4246
|
+
: L("settings.autoPilot.suggestionSourceTitle");
|
|
4247
|
+
const body =
|
|
4248
|
+
lane === "content"
|
|
4249
|
+
? L("settings.autoPilot.suggestionContentBody", { count })
|
|
4250
|
+
: lane === "ui_tests"
|
|
4251
|
+
? L("settings.autoPilot.suggestionUiTestsBody", { count })
|
|
4252
|
+
: L("settings.autoPilot.suggestionSourceBody", { count });
|
|
4253
|
+
return `
|
|
4254
|
+
<div class="settings-suggestion-card">
|
|
4255
|
+
<div class="settings-suggestion-card__header">
|
|
4256
|
+
<div>
|
|
4257
|
+
<p class="settings-suggestion-card__title">${escapeHtml(title)}</p>
|
|
4258
|
+
<p class="settings-suggestion-card__body">${escapeHtml(body)}</p>
|
|
4259
|
+
</div>
|
|
4260
|
+
<button
|
|
4261
|
+
type="button"
|
|
4262
|
+
class="primary settings-suggestion-card__action"
|
|
4263
|
+
data-auto-pilot-suggest-lane="${escapeHtml(lane)}"
|
|
4264
|
+
>${escapeHtml(L("settings.autoPilot.suggestionEnable"))}</button>
|
|
4265
|
+
</div>
|
|
4266
|
+
</div>
|
|
4267
|
+
`;
|
|
4268
|
+
}
|
|
4269
|
+
|
|
3426
4270
|
function renderSettingsMoltbookPage(context) {
|
|
3427
4271
|
const scout = context.moltbookScout;
|
|
3428
4272
|
if (!scout?.enabled) {
|
|
@@ -3694,6 +4538,406 @@ function renderSettingsA2aSharePage(context) {
|
|
|
3694
4538
|
`;
|
|
3695
4539
|
}
|
|
3696
4540
|
|
|
4541
|
+
function renderSettingsWalletPage(context) {
|
|
4542
|
+
const hazbase = context.hazbase || { enabled: false };
|
|
4543
|
+
if (!hazbase?.enabled) {
|
|
4544
|
+
return `
|
|
4545
|
+
<div class="settings-page">
|
|
4546
|
+
<p class="settings-page-copy muted">${escapeHtml(L("settings.wallet.unavailable"))}</p>
|
|
4547
|
+
</div>
|
|
4548
|
+
`;
|
|
4549
|
+
}
|
|
4550
|
+
const flow = deriveHazbaseWalletFlow(hazbase);
|
|
4551
|
+
|
|
4552
|
+
// Progressive disclosure. Previous layout rendered the full status summary
|
|
4553
|
+
// plus all four full-sized step cards at once; new account flows were
|
|
4554
|
+
// dominated by locked-looking cards and it was hard to tell which step was
|
|
4555
|
+
// actionable. Now we:
|
|
4556
|
+
// - skip `locked` steps entirely (still-unreachable actions add noise),
|
|
4557
|
+
// - collapse `complete` steps to a compact one-line row that keeps the
|
|
4558
|
+
// verified value visible (email / address) without a full card,
|
|
4559
|
+
// - keep the single `current`/`pending` step in the full action card so
|
|
4560
|
+
// the CTA is unambiguous,
|
|
4561
|
+
// - hide the optional mainnet step behind a subtle opt-in link until the
|
|
4562
|
+
// user explicitly requests it (see `state.hazbaseMainnetOptIn`).
|
|
4563
|
+
// The compact rows also replace the former "Current status" summary group
|
|
4564
|
+
// above the flow, so signed-in email / passkey state / addresses are no
|
|
4565
|
+
// longer duplicated.
|
|
4566
|
+
const guideRows = [
|
|
4567
|
+
renderHazbaseWalletBanner(flow),
|
|
4568
|
+
state.hazbaseNotice
|
|
4569
|
+
? `<div class="settings-copy-block settings-copy-block--compact wallet-flow-message wallet-flow-message--notice"><p>${escapeHtml(state.hazbaseNotice)}</p></div>`
|
|
4570
|
+
: "",
|
|
4571
|
+
state.hazbaseError
|
|
4572
|
+
? `<div class="settings-copy-block settings-copy-block--compact wallet-flow-message wallet-flow-message--error"><p>${escapeHtml(state.hazbaseError)}</p></div>`
|
|
4573
|
+
: "",
|
|
4574
|
+
renderHazbaseWalletStepList(flow),
|
|
4575
|
+
].filter(Boolean);
|
|
4576
|
+
const canRefreshSession = Boolean(hazbase.sessionInvalid);
|
|
4577
|
+
const advancedActions = canRefreshSession || hazbase.signedIn
|
|
4578
|
+
? [
|
|
4579
|
+
canRefreshSession
|
|
4580
|
+
? `<button class="secondary secondary--wide" type="button" data-hazbase-action="refresh-session">${escapeHtml(L("settings.hazbase.action.refreshSession"))}</button>`
|
|
4581
|
+
: "",
|
|
4582
|
+
hazbase.signedIn
|
|
4583
|
+
? `<button class="secondary secondary--wide" type="button" data-hazbase-action="logout">${escapeHtml(L("settings.hazbase.action.signOut"))}</button>`
|
|
4584
|
+
: "",
|
|
4585
|
+
].filter(Boolean).join("")
|
|
4586
|
+
: "";
|
|
4587
|
+
// Render the wallet flow without `renderSettingsGroup`'s `.settings-list`
|
|
4588
|
+
// wrapper. The banner (`.settings-copy-block`), notice/error blocks, and
|
|
4589
|
+
// each step card (`.wallet-step-card`) already have their own rounded
|
|
4590
|
+
// frame — wrapping them in another `.settings-list` produced a visible
|
|
4591
|
+
// "box inside a box" nest. The title (`.settings-group__title`) still
|
|
4592
|
+
// sits above a flat stack of sibling cards via `.wallet-setup-stack`.
|
|
4593
|
+
const flowTitle = L("settings.wallet.flow.title");
|
|
4594
|
+
// Brand attribution. The wallet stack (factory + validator + bundler +
|
|
4595
|
+
// paymaster) is provided by hazBase; the link points to their LP so
|
|
4596
|
+
// curious users can discover what's running the signing path.
|
|
4597
|
+
const poweredBy = `
|
|
4598
|
+
<p class="wallet-powered-by muted">
|
|
4599
|
+
powered by <a href="https://lp.hazbase.com" target="_blank" rel="noopener noreferrer">hazBase</a>
|
|
4600
|
+
</p>
|
|
4601
|
+
`;
|
|
4602
|
+
return `
|
|
4603
|
+
<div class="settings-page">
|
|
4604
|
+
<section class="settings-group">
|
|
4605
|
+
${flowTitle ? `<p class="settings-group__title">${escapeHtml(flowTitle)}</p>` : ""}
|
|
4606
|
+
<div class="wallet-setup-stack">
|
|
4607
|
+
${guideRows.join("")}
|
|
4608
|
+
</div>
|
|
4609
|
+
</section>
|
|
4610
|
+
${advancedActions ? renderSettingsActionPanel(advancedActions, L("settings.wallet.advanced.title")) : ""}
|
|
4611
|
+
${poweredBy}
|
|
4612
|
+
</div>
|
|
4613
|
+
`;
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4616
|
+
function renderHazbaseWalletStepList(flow) {
|
|
4617
|
+
const rendered = [];
|
|
4618
|
+
for (const step of flow.steps) {
|
|
4619
|
+
// Step 4 (Base mainnet) is hidden while hazbase's closed beta keeps
|
|
4620
|
+
// chainId 8453 out of `WALLET_GATEWAY_CHAINS_JSON` — calling bootstrap
|
|
4621
|
+
// against mainnet currently returns `unsupported_chain`, so there's no
|
|
4622
|
+
// path for the user to complete the step. Step 3 (Base Sepolia) is the
|
|
4623
|
+
// true completion boundary; the ready banner already keys off
|
|
4624
|
+
// `coreReady = signedIn && hasPasskey && hasBaseSepolia`. Drop this
|
|
4625
|
+
// guard (and restore the opt-in reveal) once mainnet is enabled
|
|
4626
|
+
// server-side.
|
|
4627
|
+
if (step.number === 4) {
|
|
4628
|
+
continue;
|
|
4629
|
+
}
|
|
4630
|
+
|
|
4631
|
+
// Locked steps don't render. Revealing them only adds grayed-out
|
|
4632
|
+
// placeholders below the active step; users mistake the placeholder
|
|
4633
|
+
// status chips for inactive buttons.
|
|
4634
|
+
if (step.status === "locked") {
|
|
4635
|
+
continue;
|
|
4636
|
+
}
|
|
4637
|
+
|
|
4638
|
+
const mode = step.status === "complete" ? "compact" : "full";
|
|
4639
|
+
rendered.push(renderHazbaseWalletStepCard(step, { mode }));
|
|
4640
|
+
}
|
|
4641
|
+
return `<div class="wallet-step-list">${rendered.join("")}</div>`;
|
|
4642
|
+
}
|
|
4643
|
+
|
|
4644
|
+
function renderHazbaseWalletMainnetOptIn() {
|
|
4645
|
+
return `
|
|
4646
|
+
<button class="wallet-mainnet-optin" type="button" data-hazbase-action="mainnet-opt-in">
|
|
4647
|
+
<span class="wallet-mainnet-optin__body">
|
|
4648
|
+
<span class="wallet-mainnet-optin__label">${escapeHtml(L("settings.wallet.mainnet.optIn"))}</span>
|
|
4649
|
+
<span class="wallet-mainnet-optin__hint muted">${escapeHtml(L("settings.wallet.mainnet.optInHint"))}</span>
|
|
4650
|
+
</span>
|
|
4651
|
+
<span class="wallet-mainnet-optin__chevron" aria-hidden="true">→</span>
|
|
4652
|
+
</button>
|
|
4653
|
+
`;
|
|
4654
|
+
}
|
|
4655
|
+
|
|
4656
|
+
function deriveHazbaseWalletFlow(hazbase) {
|
|
4657
|
+
const accounts = Array.isArray(hazbase.accounts) ? hazbase.accounts : [];
|
|
4658
|
+
const baseSepolia = accounts.find((entry) => Number(entry.chainId) === 84532) || null;
|
|
4659
|
+
const baseMainnet = accounts.find((entry) => Number(entry.chainId) === 8453) || null;
|
|
4660
|
+
const sessionInvalid = Boolean(hazbase.sessionInvalid);
|
|
4661
|
+
const signedIn = Boolean(hazbase.signedIn) && !sessionInvalid;
|
|
4662
|
+
const passkeyHost = hazbasePasskeyHostSupport();
|
|
4663
|
+
const hasPasskey = Boolean(hazbase.credentialId || hazbase.deviceBindingId);
|
|
4664
|
+
const hasBaseSepolia = Boolean(baseSepolia?.smartAccountAddress);
|
|
4665
|
+
const hasBaseMainnet = Boolean(baseMainnet?.smartAccountAddress);
|
|
4666
|
+
const coreReady = signedIn && hasPasskey && hasBaseSepolia;
|
|
4667
|
+
|
|
4668
|
+
const actionButton = (labelKey, action, { primary = false, disabled = false } = {}) => `
|
|
4669
|
+
<button
|
|
4670
|
+
class="${primary ? "primary" : "secondary"} ${primary ? "primary--wide" : "secondary--wide"}"
|
|
4671
|
+
type="button"
|
|
4672
|
+
data-hazbase-action="${action}"
|
|
4673
|
+
${disabled ? "disabled" : ""}
|
|
4674
|
+
>${escapeHtml(L(labelKey))}</button>
|
|
4675
|
+
`;
|
|
4676
|
+
|
|
4677
|
+
return {
|
|
4678
|
+
hasPasskey,
|
|
4679
|
+
baseSepolia,
|
|
4680
|
+
baseMainnet,
|
|
4681
|
+
sessionInvalid,
|
|
4682
|
+
coreReady,
|
|
4683
|
+
steps: [
|
|
4684
|
+
{
|
|
4685
|
+
number: 1,
|
|
4686
|
+
icon: "approval",
|
|
4687
|
+
title: sessionInvalid ? L("settings.wallet.step.refreshSession.title") : L("settings.wallet.step.signIn.title"),
|
|
4688
|
+
copy: sessionInvalid ? L("settings.wallet.step.refreshSession.copy") : L("settings.wallet.step.signIn.copy"),
|
|
4689
|
+
detail: signedIn
|
|
4690
|
+
? hazbase.email || L("settings.hazbase.status.signedIn")
|
|
4691
|
+
: state.hazbaseOtpRequested
|
|
4692
|
+
? L("settings.hazbase.status.otpAwaitingVerify")
|
|
4693
|
+
: sessionInvalid
|
|
4694
|
+
? L("settings.hazbase.status.sessionExpired")
|
|
4695
|
+
: L("settings.hazbase.status.signedOut"),
|
|
4696
|
+
status: signedIn ? "complete" : "current",
|
|
4697
|
+
// Inline form: email input (always visible pre-sign-in) and the
|
|
4698
|
+
// one-time password input (revealed after a successful send).
|
|
4699
|
+
// Putting the field immediately above its submit button is the
|
|
4700
|
+
// whole point — users don't have to guess "what does this button
|
|
4701
|
+
// ask me?" before clicking.
|
|
4702
|
+
form: signedIn
|
|
4703
|
+
? ""
|
|
4704
|
+
: renderHazbaseSignInForm({
|
|
4705
|
+
email: state.hazbaseOtpEmail || hazbase.email || "",
|
|
4706
|
+
otpRequested: Boolean(state.hazbaseOtpRequested),
|
|
4707
|
+
code: state.hazbaseOtpCode || "",
|
|
4708
|
+
}),
|
|
4709
|
+
// Sequential flow: before a code is requested, only the primary
|
|
4710
|
+
// "send" action is visible. After a successful send we swap the
|
|
4711
|
+
// primary CTA to "verify" and demote "send" to a quieter "resend"
|
|
4712
|
+
// option in case the email never arrives. This keeps the user's
|
|
4713
|
+
// next move unambiguous.
|
|
4714
|
+
actions: signedIn
|
|
4715
|
+
? []
|
|
4716
|
+
: state.hazbaseOtpRequested
|
|
4717
|
+
? [
|
|
4718
|
+
actionButton("settings.hazbase.action.verifyOtp", "verify-otp", { primary: true }),
|
|
4719
|
+
actionButton("settings.hazbase.action.resendOtp", "request-otp"),
|
|
4720
|
+
]
|
|
4721
|
+
: [
|
|
4722
|
+
actionButton(sessionInvalid ? "settings.hazbase.action.refreshSession" : "settings.hazbase.action.requestOtp", "request-otp", { primary: true }),
|
|
4723
|
+
],
|
|
4724
|
+
},
|
|
4725
|
+
{
|
|
4726
|
+
number: 2,
|
|
4727
|
+
icon: "lock",
|
|
4728
|
+
title: L("settings.wallet.step.passkey.title"),
|
|
4729
|
+
copy: L("settings.wallet.step.passkey.copy"),
|
|
4730
|
+
detail: hasPasskey
|
|
4731
|
+
? L("settings.hazbase.passkey.ready")
|
|
4732
|
+
: passkeyHost.eligible
|
|
4733
|
+
? L("settings.hazbase.passkey.missing")
|
|
4734
|
+
: passkeyHost.detail,
|
|
4735
|
+
status: hasPasskey ? "complete" : signedIn ? "current" : "locked",
|
|
4736
|
+
actions: hasPasskey
|
|
4737
|
+
? []
|
|
4738
|
+
: [
|
|
4739
|
+
actionButton("settings.hazbase.action.registerPasskey", "register-passkey", {
|
|
4740
|
+
primary: signedIn && passkeyHost.eligible,
|
|
4741
|
+
disabled: !signedIn || !passkeyHost.eligible,
|
|
4742
|
+
}),
|
|
4743
|
+
],
|
|
4744
|
+
},
|
|
4745
|
+
{
|
|
4746
|
+
number: 3,
|
|
4747
|
+
icon: "coin",
|
|
4748
|
+
title: L("settings.wallet.step.baseSepolia.title"),
|
|
4749
|
+
copy: L("settings.wallet.step.baseSepolia.copy"),
|
|
4750
|
+
detail: baseSepolia?.smartAccountAddress || L("settings.hazbase.wallet.missing"),
|
|
4751
|
+
monoDetail: Boolean(baseSepolia?.smartAccountAddress),
|
|
4752
|
+
status: hasBaseSepolia ? "complete" : signedIn && hasPasskey ? "current" : "locked",
|
|
4753
|
+
actions: hasBaseSepolia
|
|
4754
|
+
? []
|
|
4755
|
+
: [
|
|
4756
|
+
actionButton("settings.hazbase.action.bootstrapBaseSepolia", "bootstrap-base-sepolia", {
|
|
4757
|
+
primary: signedIn && hasPasskey,
|
|
4758
|
+
disabled: !signedIn || !hasPasskey,
|
|
4759
|
+
}),
|
|
4760
|
+
],
|
|
4761
|
+
},
|
|
4762
|
+
{
|
|
4763
|
+
number: 4,
|
|
4764
|
+
icon: "coin",
|
|
4765
|
+
title: L("settings.wallet.step.base.title"),
|
|
4766
|
+
copy: L("settings.wallet.step.base.copy"),
|
|
4767
|
+
detail: baseMainnet?.smartAccountAddress || L("settings.hazbase.wallet.missing"),
|
|
4768
|
+
monoDetail: Boolean(baseMainnet?.smartAccountAddress),
|
|
4769
|
+
status: hasBaseMainnet ? "complete" : coreReady ? "optional" : "locked",
|
|
4770
|
+
actions: hasBaseMainnet
|
|
4771
|
+
? []
|
|
4772
|
+
: [
|
|
4773
|
+
actionButton("settings.hazbase.action.bootstrapBase", "bootstrap-base", {
|
|
4774
|
+
disabled: !coreReady,
|
|
4775
|
+
}),
|
|
4776
|
+
],
|
|
4777
|
+
},
|
|
4778
|
+
],
|
|
4779
|
+
};
|
|
4780
|
+
}
|
|
4781
|
+
|
|
4782
|
+
function renderHazbaseWalletBanner(flow) {
|
|
4783
|
+
const title = flow.sessionInvalid
|
|
4784
|
+
? L("settings.wallet.sessionExpired.title")
|
|
4785
|
+
: flow.coreReady ? L("settings.wallet.ready.title") : L("settings.wallet.flow.banner.title");
|
|
4786
|
+
const className = flow.coreReady
|
|
4787
|
+
? "settings-copy-block settings-copy-block--stacked wallet-flow-banner wallet-flow-banner--ready"
|
|
4788
|
+
: flow.sessionInvalid
|
|
4789
|
+
? "settings-copy-block settings-copy-block--stacked wallet-flow-banner wallet-flow-banner--session-expired"
|
|
4790
|
+
: "settings-copy-block settings-copy-block--stacked wallet-flow-banner";
|
|
4791
|
+
// Ready state: surface the smart-account address prominently so the user
|
|
4792
|
+
// can see at a glance which wallet agents will use as `--pay-to` when
|
|
4793
|
+
// gating paid shares / A2A payouts. The address is the single most
|
|
4794
|
+
// actionable fact on this page once setup is complete — muted filler
|
|
4795
|
+
// copy buries it.
|
|
4796
|
+
const payoutAddress = flow.baseSepolia?.smartAccountAddress || "";
|
|
4797
|
+
const copyLabel = L("settings.wallet.ready.copyAddress");
|
|
4798
|
+
const body = flow.coreReady && payoutAddress
|
|
4799
|
+
? `
|
|
4800
|
+
<p class="wallet-flow-banner__copy muted">${escapeHtml(L("settings.wallet.ready.payoutIntro"))}</p>
|
|
4801
|
+
<button
|
|
4802
|
+
class="wallet-flow-banner__address"
|
|
4803
|
+
type="button"
|
|
4804
|
+
data-wallet-address-copy="${escapeHtml(payoutAddress)}"
|
|
4805
|
+
aria-label="${escapeHtml(copyLabel)}: ${escapeHtml(payoutAddress)}"
|
|
4806
|
+
>
|
|
4807
|
+
<span class="wallet-flow-banner__address-text">${escapeHtml(payoutAddress)}</span>
|
|
4808
|
+
<span class="wallet-flow-banner__address-icon-slot">
|
|
4809
|
+
<span class="wallet-flow-banner__address-icon wallet-flow-banner__address-icon--copy" aria-hidden="true">${renderIcon("copy")}</span>
|
|
4810
|
+
<span class="wallet-flow-banner__address-icon wallet-flow-banner__address-icon--check" aria-hidden="true">${renderIcon("check")}</span>
|
|
4811
|
+
</span>
|
|
4812
|
+
</button>
|
|
4813
|
+
`
|
|
4814
|
+
: `<p class="wallet-flow-banner__copy muted">${escapeHtml(
|
|
4815
|
+
flow.sessionInvalid
|
|
4816
|
+
? L("settings.wallet.sessionExpired.copy")
|
|
4817
|
+
: flow.coreReady ? L("settings.wallet.ready.copy") : L("settings.wallet.flow.copy"),
|
|
4818
|
+
)}</p>`;
|
|
4819
|
+
return `
|
|
4820
|
+
<div class="${className}">
|
|
4821
|
+
<p class="wallet-flow-banner__eyebrow">${escapeHtml(L("settings.hazbase.title"))}</p>
|
|
4822
|
+
<p class="wallet-flow-banner__title">${escapeHtml(title)}</p>
|
|
4823
|
+
${body}
|
|
4824
|
+
</div>
|
|
4825
|
+
`;
|
|
4826
|
+
}
|
|
4827
|
+
|
|
4828
|
+
function renderHazbaseWalletStepCard(step, { mode = "full" } = {}) {
|
|
4829
|
+
const statusMeta = {
|
|
4830
|
+
complete: { label: L("settings.wallet.status.complete"), icon: "completed" },
|
|
4831
|
+
current: { label: L("settings.wallet.status.current"), icon: "pending" },
|
|
4832
|
+
locked: { label: L("settings.wallet.status.locked"), icon: "lock" },
|
|
4833
|
+
optional: { label: L("settings.wallet.status.optional"), icon: "coin" },
|
|
4834
|
+
pending: { label: L("settings.wallet.status.pending"), icon: "pending" },
|
|
4835
|
+
}[step.status] || { label: L("settings.wallet.status.pending"), icon: "pending" };
|
|
4836
|
+
|
|
4837
|
+
if (mode === "compact") {
|
|
4838
|
+
// Compact row for finished steps. Keeps the check icon + title + one-line
|
|
4839
|
+
// detail visible (so the user can scan what's done at a glance) but
|
|
4840
|
+
// drops the copy/actions to stop the page from being four blocks tall.
|
|
4841
|
+
const detailClass = step.monoDetail
|
|
4842
|
+
? "wallet-step-card__compact-detail wallet-step-card__compact-detail--mono"
|
|
4843
|
+
: "wallet-step-card__compact-detail";
|
|
4844
|
+
return `
|
|
4845
|
+
<div class="wallet-step-card wallet-step-card--compact wallet-step-card--${escapeHtml(step.status)}">
|
|
4846
|
+
<span class="wallet-step-card__compact-icon" aria-hidden="true">${renderIcon(statusMeta.icon)}</span>
|
|
4847
|
+
<div class="wallet-step-card__compact-body">
|
|
4848
|
+
<p class="wallet-step-card__compact-title">${escapeHtml(step.title)}</p>
|
|
4849
|
+
${step.detail ? `<p class="${detailClass}">${escapeHtml(step.detail)}</p>` : ""}
|
|
4850
|
+
</div>
|
|
4851
|
+
<span class="wallet-step-card__compact-status" aria-hidden="true">${escapeHtml(statusMeta.label)}</span>
|
|
4852
|
+
</div>
|
|
4853
|
+
`;
|
|
4854
|
+
}
|
|
4855
|
+
|
|
4856
|
+
return `
|
|
4857
|
+
<article class="wallet-step-card wallet-step-card--${escapeHtml(step.status)}">
|
|
4858
|
+
<div class="wallet-step-card__header">
|
|
4859
|
+
<div class="wallet-step-card__headline">
|
|
4860
|
+
<span class="wallet-step-card__icon" aria-hidden="true">${renderIcon(step.icon)}</span>
|
|
4861
|
+
<div class="wallet-step-card__title-wrap">
|
|
4862
|
+
<p class="wallet-step-card__eyebrow">${escapeHtml(L("settings.wallet.stepNumber", { count: step.number }))}</p>
|
|
4863
|
+
<h3 class="wallet-step-card__title">${escapeHtml(step.title)}</h3>
|
|
4864
|
+
</div>
|
|
4865
|
+
</div>
|
|
4866
|
+
<span class="wallet-step-card__status wallet-step-card__status--${escapeHtml(step.status)}">
|
|
4867
|
+
<span class="wallet-step-card__status-icon" aria-hidden="true">${renderIcon(statusMeta.icon)}</span>
|
|
4868
|
+
<span>${escapeHtml(statusMeta.label)}</span>
|
|
4869
|
+
</span>
|
|
4870
|
+
</div>
|
|
4871
|
+
<p class="wallet-step-card__copy">${escapeHtml(step.copy)}</p>
|
|
4872
|
+
<p class="wallet-step-card__detail ${step.monoDetail ? "wallet-step-card__detail--mono" : ""}">${escapeHtml(step.detail)}</p>
|
|
4873
|
+
${step.form || ""}
|
|
4874
|
+
${step.actions.length ? `<div class="wallet-step-card__actions">${step.actions.join("")}</div>` : ""}
|
|
4875
|
+
</article>
|
|
4876
|
+
`;
|
|
4877
|
+
}
|
|
4878
|
+
|
|
4879
|
+
// Sign-in form sits inside step 1 when the user hasn't signed in yet.
|
|
4880
|
+
// Email field is always rendered (editable so users can correct a typo
|
|
4881
|
+
// and resend); OTP field only appears once a code has been issued. Both
|
|
4882
|
+
// inputs carry value="..." sourced from state so a poll-triggered
|
|
4883
|
+
// re-render doesn't wipe what the user was typing mid-edit.
|
|
4884
|
+
function renderHazbaseSignInForm({ email, otpRequested, code }) {
|
|
4885
|
+
// Once the OTP has been sent we lock the email field so accidental edits
|
|
4886
|
+
// can't invalidate the pending code. A small "change email" link is shown
|
|
4887
|
+
// as the intentional escape hatch — clicking it reverts the form to the
|
|
4888
|
+
// pre-sent state (code discarded, email stays for easy typo recovery).
|
|
4889
|
+
const emailLocked = Boolean(otpRequested);
|
|
4890
|
+
const emailLabelRow = emailLocked
|
|
4891
|
+
? `<span class="wallet-step-card__field-label-row">
|
|
4892
|
+
<span class="wallet-step-card__field-label">${escapeHtml(L("settings.hazbase.field.emailLabel"))}</span>
|
|
4893
|
+
<button
|
|
4894
|
+
type="button"
|
|
4895
|
+
class="wallet-step-card__field-link"
|
|
4896
|
+
data-hazbase-action="change-email"
|
|
4897
|
+
>${escapeHtml(L("settings.hazbase.action.changeEmail"))}</button>
|
|
4898
|
+
</span>`
|
|
4899
|
+
: `<span class="wallet-step-card__field-label">${escapeHtml(L("settings.hazbase.field.emailLabel"))}</span>`;
|
|
4900
|
+
const emailField = `
|
|
4901
|
+
<label class="wallet-step-card__field">
|
|
4902
|
+
${emailLabelRow}
|
|
4903
|
+
<input
|
|
4904
|
+
type="email"
|
|
4905
|
+
class="wallet-step-card__field-input${emailLocked ? " wallet-step-card__field-input--locked" : ""}"
|
|
4906
|
+
data-hazbase-input="otp-email"
|
|
4907
|
+
value="${escapeHtml(email || "")}"
|
|
4908
|
+
placeholder="${escapeHtml(L("settings.hazbase.field.emailPlaceholder"))}"
|
|
4909
|
+
autocomplete="email"
|
|
4910
|
+
inputmode="email"
|
|
4911
|
+
autocapitalize="off"
|
|
4912
|
+
autocorrect="off"
|
|
4913
|
+
spellcheck="false"
|
|
4914
|
+
${emailLocked ? "disabled aria-disabled=\"true\"" : ""}
|
|
4915
|
+
/>
|
|
4916
|
+
</label>
|
|
4917
|
+
`;
|
|
4918
|
+
const otpField = otpRequested
|
|
4919
|
+
? `
|
|
4920
|
+
<label class="wallet-step-card__field">
|
|
4921
|
+
<span class="wallet-step-card__field-label">${escapeHtml(L("settings.hazbase.field.otpLabel"))}</span>
|
|
4922
|
+
<input
|
|
4923
|
+
type="text"
|
|
4924
|
+
class="wallet-step-card__field-input wallet-step-card__field-input--mono"
|
|
4925
|
+
data-hazbase-input="otp-code"
|
|
4926
|
+
value="${escapeHtml(code || "")}"
|
|
4927
|
+
placeholder="${escapeHtml(L("settings.hazbase.field.otpPlaceholder"))}"
|
|
4928
|
+
autocomplete="one-time-code"
|
|
4929
|
+
inputmode="numeric"
|
|
4930
|
+
autocapitalize="off"
|
|
4931
|
+
autocorrect="off"
|
|
4932
|
+
spellcheck="false"
|
|
4933
|
+
maxlength="12"
|
|
4934
|
+
/>
|
|
4935
|
+
</label>
|
|
4936
|
+
`
|
|
4937
|
+
: "";
|
|
4938
|
+
return `<div class="wallet-step-card__form">${emailField}${otpField}</div>`;
|
|
4939
|
+
}
|
|
4940
|
+
|
|
3697
4941
|
function formatRelativeAge(ms) {
|
|
3698
4942
|
if (!Number.isFinite(ms) || ms < 0) return "";
|
|
3699
4943
|
// Intl.RelativeTimeFormat with numeric:"auto" gives us locale-aware phrasing
|
|
@@ -3754,8 +4998,16 @@ function formatUsdcAtomic(atomic) {
|
|
|
3754
4998
|
}
|
|
3755
4999
|
|
|
3756
5000
|
function renderSettingsInfoRow(label, value, options = {}) {
|
|
3757
|
-
const rowClassName = [
|
|
3758
|
-
|
|
5001
|
+
const rowClassName = [
|
|
5002
|
+
"settings-info-row",
|
|
5003
|
+
options.stacked ? "settings-info-row--stacked" : "",
|
|
5004
|
+
options.rowClassName || "",
|
|
5005
|
+
].filter(Boolean).join(" ");
|
|
5006
|
+
const valueClassName = [
|
|
5007
|
+
"settings-info-row__value",
|
|
5008
|
+
options.mono ? "settings-info-row__value--mono" : "",
|
|
5009
|
+
options.valueClassName || "",
|
|
5010
|
+
].filter(Boolean).join(" ");
|
|
3759
5011
|
const displayValue = options.rawValue ? value : escapeHtml(value);
|
|
3760
5012
|
return `
|
|
3761
5013
|
<div class="${rowClassName}">
|
|
@@ -3868,6 +5120,7 @@ function renderStandardDetailDesktop(detail) {
|
|
|
3868
5120
|
<h2 class="detail-title detail-title--desktop">${renderDetailTitle(detail)}</h2>
|
|
3869
5121
|
${detail.readOnly || detail.kind === "approval" || detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "thread_share" ? "" : renderDetailLead(detail, kindInfo)}
|
|
3870
5122
|
${renderPreviousContextCard(detail)}
|
|
5123
|
+
${renderAutoPilotManualReview(detail)}
|
|
3871
5124
|
${renderInterruptedDetailNotice(detail)}
|
|
3872
5125
|
${renderMoltbookReplyComposer(detail)}
|
|
3873
5126
|
${renderMoltbookDraftComposer(detail)}
|
|
@@ -3907,6 +5160,7 @@ function renderStandardDetailMobile(detail) {
|
|
|
3907
5160
|
<div class="mobile-detail-scroll mobile-detail-scroll--detail">
|
|
3908
5161
|
${renderDetailMetaRow(detail, kindInfo, { mobile: true })}
|
|
3909
5162
|
${renderPreviousContextCard(detail, { mobile: true })}
|
|
5163
|
+
${renderAutoPilotManualReview(detail, { mobile: true })}
|
|
3910
5164
|
${renderInterruptedDetailNotice(detail, { mobile: true })}
|
|
3911
5165
|
${renderMoltbookReplyComposer(detail, { mobile: true })}
|
|
3912
5166
|
${renderMoltbookDraftComposer(detail, { mobile: true })}
|
|
@@ -4087,6 +5341,25 @@ function renderPreviousContextCard(detail, options = {}) {
|
|
|
4087
5341
|
`;
|
|
4088
5342
|
}
|
|
4089
5343
|
|
|
5344
|
+
function renderAutoPilotManualReview(detail, options = {}) {
|
|
5345
|
+
const review = detail?.autoPilotReview;
|
|
5346
|
+
if (!review?.title || !review?.body) {
|
|
5347
|
+
return "";
|
|
5348
|
+
}
|
|
5349
|
+
return `
|
|
5350
|
+
<section class="detail-card detail-card--autopilot ${options.mobile ? "detail-card--mobile" : ""}">
|
|
5351
|
+
<div class="detail-context-card__header">
|
|
5352
|
+
<div class="detail-context-card__eyebrow">
|
|
5353
|
+
<span class="detail-context-card__icon" aria-hidden="true">${renderIcon("settings")}</span>
|
|
5354
|
+
<span>${escapeHtml(L("detail.autoPilotManualEyebrow"))}</span>
|
|
5355
|
+
</div>
|
|
5356
|
+
</div>
|
|
5357
|
+
<p class="detail-context-card__kind">${escapeHtml(review.title)}</p>
|
|
5358
|
+
<p class="detail-autopilot-copy">${escapeHtml(review.body)}</p>
|
|
5359
|
+
</section>
|
|
5360
|
+
`;
|
|
5361
|
+
}
|
|
5362
|
+
|
|
4090
5363
|
function renderDetailImageGallery(detail, options = {}) {
|
|
4091
5364
|
const imageUrls = Array.isArray(detail?.imageUrls) ? detail.imageUrls.filter(Boolean) : [];
|
|
4092
5365
|
if (imageUrls.length === 0) {
|
|
@@ -5100,6 +6373,20 @@ function renderTopBanner() {
|
|
|
5100
6373
|
return "";
|
|
5101
6374
|
}
|
|
5102
6375
|
|
|
6376
|
+
function renderGlobalErrorBanner() {
|
|
6377
|
+
if (!state.pushError) {
|
|
6378
|
+
return "";
|
|
6379
|
+
}
|
|
6380
|
+
return `
|
|
6381
|
+
<section class="install-banner install-banner--push">
|
|
6382
|
+
<div class="install-banner__copy">
|
|
6383
|
+
<strong>${escapeHtml(state.locale === "ja" ? "エラー" : "Error")}</strong>
|
|
6384
|
+
<p class="muted">${escapeHtml(state.pushError)}</p>
|
|
6385
|
+
</div>
|
|
6386
|
+
</section>
|
|
6387
|
+
`;
|
|
6388
|
+
}
|
|
6389
|
+
|
|
5103
6390
|
function renderPushBanner() {
|
|
5104
6391
|
if (!shouldShowPushBanner()) {
|
|
5105
6392
|
return "";
|
|
@@ -5168,6 +6455,35 @@ function renderImageViewerModal() {
|
|
|
5168
6455
|
`;
|
|
5169
6456
|
}
|
|
5170
6457
|
|
|
6458
|
+
function renderHazbaseLogoutConfirmModal() {
|
|
6459
|
+
if (!state.hazbaseLogoutConfirmOpen) {
|
|
6460
|
+
return "";
|
|
6461
|
+
}
|
|
6462
|
+
const email = state.hazbaseStatus?.email || "";
|
|
6463
|
+
// Mirror the session-logout dialog's shape but drop the split-choice
|
|
6464
|
+
// options — wallet sign-out is a single binary (log out or don't), no
|
|
6465
|
+
// "keep this device trusted" toggle applies.
|
|
6466
|
+
// Note: the backdrop uses a dedicated `data-close-hazbase-logout-confirm`
|
|
6467
|
+
// marker (bound in bindSharedUi) rather than a `data-hazbase-action`.
|
|
6468
|
+
// If we reused the action dispatcher here, an inside-card click would
|
|
6469
|
+
// bubble up to the backdrop's handler and run a second async dispatch
|
|
6470
|
+
// in parallel with the button's own handler — which races against the
|
|
6471
|
+
// API call. The mirror approach matches the session-logout modal.
|
|
6472
|
+
return `
|
|
6473
|
+
<div class="modal-backdrop" data-close-hazbase-logout-confirm>
|
|
6474
|
+
<section class="modal-card modal-card--confirm" role="dialog" aria-modal="true" aria-labelledby="hazbase-logout-confirm-title">
|
|
6475
|
+
<div class="helper-copy">
|
|
6476
|
+
<strong id="hazbase-logout-confirm-title">${escapeHtml(L("settings.hazbase.logout.confirm.title"))}</strong>
|
|
6477
|
+
<p class="muted">${escapeHtml(L("settings.hazbase.logout.confirm.copy"))}</p>
|
|
6478
|
+
${email ? `<p class="muted"><code>${escapeHtml(email)}</code></p>` : ""}
|
|
6479
|
+
</div>
|
|
6480
|
+
<button class="secondary secondary--wide" type="button" data-hazbase-action="logout-confirm">${escapeHtml(L("settings.hazbase.logout.confirm.confirmLabel"))}</button>
|
|
6481
|
+
<button class="ghost ghost--wide" type="button" data-close-hazbase-logout-confirm>${escapeHtml(L("common.cancel"))}</button>
|
|
6482
|
+
</section>
|
|
6483
|
+
</div>
|
|
6484
|
+
`;
|
|
6485
|
+
}
|
|
6486
|
+
|
|
5171
6487
|
function renderLogoutConfirmModal() {
|
|
5172
6488
|
if (!state.logoutConfirmOpen || !state.session?.authenticated) {
|
|
5173
6489
|
return "";
|
|
@@ -5254,6 +6570,7 @@ function bindShellInteractions() {
|
|
|
5254
6570
|
openSettingsSubpage(nextPage);
|
|
5255
6571
|
}
|
|
5256
6572
|
await renderShell();
|
|
6573
|
+
refreshVersionStatusForTechnicalPage();
|
|
5257
6574
|
});
|
|
5258
6575
|
}
|
|
5259
6576
|
|
|
@@ -5266,6 +6583,7 @@ function bindShellInteractions() {
|
|
|
5266
6583
|
syncCurrentItemUrl(null);
|
|
5267
6584
|
openSettingsSubpage("advanced");
|
|
5268
6585
|
await renderShell();
|
|
6586
|
+
refreshVersionStatusForTechnicalPage();
|
|
5269
6587
|
});
|
|
5270
6588
|
}
|
|
5271
6589
|
|
|
@@ -5273,6 +6591,7 @@ function bindShellInteractions() {
|
|
|
5273
6591
|
button.addEventListener("click", async () => {
|
|
5274
6592
|
openSettingsSubpage(button.dataset.settingsSubpage || "");
|
|
5275
6593
|
await renderShell();
|
|
6594
|
+
refreshVersionStatusForTechnicalPage();
|
|
5276
6595
|
});
|
|
5277
6596
|
}
|
|
5278
6597
|
|
|
@@ -5283,6 +6602,8 @@ function bindShellInteractions() {
|
|
|
5283
6602
|
});
|
|
5284
6603
|
}
|
|
5285
6604
|
|
|
6605
|
+
|
|
6606
|
+
|
|
5286
6607
|
for (const select of document.querySelectorAll("[data-timeline-thread-select]")) {
|
|
5287
6608
|
const handleInteractionStart = () => {
|
|
5288
6609
|
markThreadFilterInteraction();
|
|
@@ -5316,6 +6637,7 @@ function bindShellInteractions() {
|
|
|
5316
6637
|
state.timelineKindFilter = "all";
|
|
5317
6638
|
state.timelineKindFilterOpen = false;
|
|
5318
6639
|
state.completedThreadFilter = "all";
|
|
6640
|
+
state.diffThreadFilter = "all";
|
|
5319
6641
|
alignCurrentItemToVisibleEntries();
|
|
5320
6642
|
await renderShell();
|
|
5321
6643
|
});
|
|
@@ -5341,6 +6663,25 @@ function bindShellInteractions() {
|
|
|
5341
6663
|
});
|
|
5342
6664
|
}
|
|
5343
6665
|
|
|
6666
|
+
for (const select of document.querySelectorAll("[data-diff-thread-select]")) {
|
|
6667
|
+
const handleInteractionStart = () => {
|
|
6668
|
+
markThreadFilterInteraction();
|
|
6669
|
+
};
|
|
6670
|
+
const handleInteractionEnd = () => {
|
|
6671
|
+
clearThreadFilterInteraction();
|
|
6672
|
+
};
|
|
6673
|
+
select.addEventListener("pointerdown", handleInteractionStart);
|
|
6674
|
+
select.addEventListener("click", handleInteractionStart);
|
|
6675
|
+
select.addEventListener("focus", handleInteractionStart);
|
|
6676
|
+
select.addEventListener("blur", handleInteractionEnd);
|
|
6677
|
+
select.addEventListener("change", async () => {
|
|
6678
|
+
clearThreadFilterInteraction();
|
|
6679
|
+
state.diffThreadFilter = select.value || "all";
|
|
6680
|
+
alignCurrentItemToVisibleEntries();
|
|
6681
|
+
await renderShell();
|
|
6682
|
+
});
|
|
6683
|
+
}
|
|
6684
|
+
|
|
5344
6685
|
for (const select of document.querySelectorAll("[data-completed-thread-select]")) {
|
|
5345
6686
|
const handleInteractionStart = () => {
|
|
5346
6687
|
markThreadFilterInteraction();
|
|
@@ -5493,7 +6834,23 @@ function bindShellInteractions() {
|
|
|
5493
6834
|
button.innerHTML = `<span class="action-spinner" aria-hidden="true"></span><span>${escapeHtml(L("reply.sendSending"))}</span>`;
|
|
5494
6835
|
|
|
5495
6836
|
try {
|
|
5496
|
-
|
|
6837
|
+
let postBody = body;
|
|
6838
|
+
if (body?.hazbaseReauth === true) {
|
|
6839
|
+
if (!hazbasePasskeyHostSupport().eligible) {
|
|
6840
|
+
throw new Error(L("error.hazbasePasskeyLocalHostRequired"));
|
|
6841
|
+
}
|
|
6842
|
+
const { createPasskeyAssertionCredential } = await loadHazbasePasskeyModule();
|
|
6843
|
+
const challenge = await apiPost("/api/hazbase/passkey/assert/challenge", { purpose: "reauth" });
|
|
6844
|
+
const credential = await createPasskeyAssertionCredential(challenge);
|
|
6845
|
+
await apiPost("/api/hazbase/passkey/assert/complete", {
|
|
6846
|
+
challengeId: challenge.challengeId,
|
|
6847
|
+
credential,
|
|
6848
|
+
purpose: "reauth",
|
|
6849
|
+
});
|
|
6850
|
+
postBody = { ...body };
|
|
6851
|
+
delete postBody.hazbaseReauth;
|
|
6852
|
+
}
|
|
6853
|
+
await apiPost(actionUrl, postBody);
|
|
5497
6854
|
if (keepDetailOpen && activeItem?.kind === "approval") {
|
|
5498
6855
|
pinActionOutcomeDetail(
|
|
5499
6856
|
activeItem,
|
|
@@ -5513,6 +6870,9 @@ function bindShellInteractions() {
|
|
|
5513
6870
|
state.pendingActionUrls.delete(actionUrl);
|
|
5514
6871
|
} catch (error) {
|
|
5515
6872
|
state.pendingActionUrls.delete(actionUrl);
|
|
6873
|
+
if (error?.errorKey === "hazbase-session-expired") {
|
|
6874
|
+
await fetchHazbaseStatus();
|
|
6875
|
+
}
|
|
5516
6876
|
// Restore buttons on failure so the user can retry
|
|
5517
6877
|
for (const sibling of siblingButtons) {
|
|
5518
6878
|
if (originalLabels.has(sibling)) {
|
|
@@ -5522,7 +6882,8 @@ function bindShellInteractions() {
|
|
|
5522
6882
|
sibling.removeAttribute("aria-busy");
|
|
5523
6883
|
}
|
|
5524
6884
|
button.classList.remove("is-loading");
|
|
5525
|
-
|
|
6885
|
+
state.pushError = error.message || String(error);
|
|
6886
|
+
await renderShell();
|
|
5526
6887
|
}
|
|
5527
6888
|
});
|
|
5528
6889
|
}
|
|
@@ -5657,6 +7018,16 @@ function bindShellInteractions() {
|
|
|
5657
7018
|
const action = button.dataset.pushAction;
|
|
5658
7019
|
state.pushError = "";
|
|
5659
7020
|
state.pushNotice = "";
|
|
7021
|
+
// Immediate visual feedback — enable/disable inherently wait on
|
|
7022
|
+
// Web Push APIs (permission prompt → pushManager.subscribe() →
|
|
7023
|
+
// server POST → refreshPushStatus), which can take 1–3 seconds.
|
|
7024
|
+
// Without this the button just sat there looking inert the whole
|
|
7025
|
+
// time. The .is-loading + aria-busy pair matches the pattern used
|
|
7026
|
+
// by approval action buttons elsewhere in this file.
|
|
7027
|
+
const wasDisabled = button.disabled;
|
|
7028
|
+
button.classList.add("is-loading");
|
|
7029
|
+
button.disabled = true;
|
|
7030
|
+
button.setAttribute("aria-busy", "true");
|
|
5660
7031
|
try {
|
|
5661
7032
|
if (action === "enable") {
|
|
5662
7033
|
await enableNotifications();
|
|
@@ -5675,6 +7046,12 @@ function bindShellInteractions() {
|
|
|
5675
7046
|
await refreshPushStatus();
|
|
5676
7047
|
} catch (error) {
|
|
5677
7048
|
state.pushError = error.message || String(error);
|
|
7049
|
+
// Restore this specific button on failure; renderShell() below
|
|
7050
|
+
// would rebuild it anyway but leaving it disabled between
|
|
7051
|
+
// exception and render flashes a dead button.
|
|
7052
|
+
button.classList.remove("is-loading");
|
|
7053
|
+
button.disabled = wasDisabled;
|
|
7054
|
+
button.removeAttribute("aria-busy");
|
|
5678
7055
|
}
|
|
5679
7056
|
await renderShell();
|
|
5680
7057
|
});
|
|
@@ -5683,11 +7060,96 @@ function bindShellInteractions() {
|
|
|
5683
7060
|
for (const checkbox of document.querySelectorAll("[data-claude-away-checkbox]")) {
|
|
5684
7061
|
checkbox.addEventListener("change", async () => {
|
|
5685
7062
|
const next = checkbox.checked === true;
|
|
7063
|
+
const previous = state.session?.claudeAwayMode === true;
|
|
7064
|
+
// Optimistic flip — the server round-trip and the old post-toggle
|
|
7065
|
+
// refreshAuthenticatedState() (7 endpoints) used to gate the UI
|
|
7066
|
+
// update. Flip state now, POST in background, roll back on error.
|
|
7067
|
+
if (state.session) {
|
|
7068
|
+
state.session.claudeAwayMode = next;
|
|
7069
|
+
}
|
|
7070
|
+
await renderShell();
|
|
5686
7071
|
try {
|
|
5687
7072
|
const result = await apiPost("/api/settings/claude-away-mode", { enabled: next });
|
|
7073
|
+
if (state.session && result && Object.prototype.hasOwnProperty.call(result, "enabled")) {
|
|
7074
|
+
const reconciled = result.enabled === true;
|
|
7075
|
+
if (reconciled !== next) {
|
|
7076
|
+
state.session.claudeAwayMode = reconciled;
|
|
7077
|
+
await renderShell();
|
|
7078
|
+
}
|
|
7079
|
+
}
|
|
7080
|
+
} catch (error) {
|
|
5688
7081
|
if (state.session) {
|
|
5689
|
-
state.session.claudeAwayMode =
|
|
7082
|
+
state.session.claudeAwayMode = previous;
|
|
5690
7083
|
}
|
|
7084
|
+
state.pushError = error.message || String(error);
|
|
7085
|
+
await renderShell();
|
|
7086
|
+
}
|
|
7087
|
+
});
|
|
7088
|
+
}
|
|
7089
|
+
|
|
7090
|
+
function applyAutoPilotSettingsResult(result) {
|
|
7091
|
+
if (!state.session) {
|
|
7092
|
+
return;
|
|
7093
|
+
}
|
|
7094
|
+
state.session.autoPilotTrustedReads = result?.trustedReadsEnabled === true;
|
|
7095
|
+
state.session.autoPilotTrustedWrites = result?.trustedWritesEnabled === true;
|
|
7096
|
+
state.session.autoPilotWriteLaneContent = result?.writeLaneContentEnabled === true;
|
|
7097
|
+
state.session.autoPilotWriteLaneUiTests = result?.writeLaneUiTestsEnabled === true;
|
|
7098
|
+
state.session.autoPilotWriteLaneSource = result?.writeLaneSourceEnabled === true;
|
|
7099
|
+
}
|
|
7100
|
+
|
|
7101
|
+
for (const checkbox of document.querySelectorAll("[data-auto-pilot-checkbox]")) {
|
|
7102
|
+
checkbox.addEventListener("change", async () => {
|
|
7103
|
+
const next = checkbox.checked === true;
|
|
7104
|
+
try {
|
|
7105
|
+
const result = await apiPost("/api/settings/auto-pilot", { trustedReadsEnabled: next });
|
|
7106
|
+
applyAutoPilotSettingsResult(result);
|
|
7107
|
+
await refreshAuthenticatedState();
|
|
7108
|
+
} catch (error) {
|
|
7109
|
+
state.pushError = error.message || String(error);
|
|
7110
|
+
}
|
|
7111
|
+
await renderShell();
|
|
7112
|
+
});
|
|
7113
|
+
}
|
|
7114
|
+
|
|
7115
|
+
for (const checkbox of document.querySelectorAll("[data-auto-pilot-write-lane-checkbox]")) {
|
|
7116
|
+
checkbox.addEventListener("change", async () => {
|
|
7117
|
+
const next = checkbox.checked === true;
|
|
7118
|
+
const lane = normalizeClientText(checkbox.getAttribute("data-auto-pilot-write-lane-checkbox") || "");
|
|
7119
|
+
const payload =
|
|
7120
|
+
lane === "content"
|
|
7121
|
+
? { writeLaneContentEnabled: next }
|
|
7122
|
+
: lane === "ui-tests"
|
|
7123
|
+
? { writeLaneUiTestsEnabled: next }
|
|
7124
|
+
: { writeLaneSourceEnabled: next };
|
|
7125
|
+
try {
|
|
7126
|
+
const result = await apiPost("/api/settings/auto-pilot", payload);
|
|
7127
|
+
applyAutoPilotSettingsResult(result);
|
|
7128
|
+
await refreshAuthenticatedState();
|
|
7129
|
+
} catch (error) {
|
|
7130
|
+
state.pushError = error.message || String(error);
|
|
7131
|
+
}
|
|
7132
|
+
await renderShell();
|
|
7133
|
+
});
|
|
7134
|
+
}
|
|
7135
|
+
|
|
7136
|
+
for (const button of document.querySelectorAll("[data-auto-pilot-suggest-lane]")) {
|
|
7137
|
+
button.addEventListener("click", async () => {
|
|
7138
|
+
const lane = normalizeClientText(button.getAttribute("data-auto-pilot-suggest-lane") || "");
|
|
7139
|
+
const payload =
|
|
7140
|
+
lane === "content"
|
|
7141
|
+
? { writeLaneContentEnabled: true }
|
|
7142
|
+
: lane === "ui_tests"
|
|
7143
|
+
? { writeLaneUiTestsEnabled: true }
|
|
7144
|
+
: lane === "source"
|
|
7145
|
+
? { writeLaneSourceEnabled: true }
|
|
7146
|
+
: null;
|
|
7147
|
+
if (!payload) {
|
|
7148
|
+
return;
|
|
7149
|
+
}
|
|
7150
|
+
try {
|
|
7151
|
+
const result = await apiPost("/api/settings/auto-pilot", payload);
|
|
7152
|
+
applyAutoPilotSettingsResult(result);
|
|
5691
7153
|
await refreshAuthenticatedState();
|
|
5692
7154
|
} catch (error) {
|
|
5693
7155
|
state.pushError = error.message || String(error);
|
|
@@ -5699,13 +7161,29 @@ function bindShellInteractions() {
|
|
|
5699
7161
|
for (const checkbox of document.querySelectorAll("[data-a2a-public-checkbox]")) {
|
|
5700
7162
|
checkbox.addEventListener("change", async () => {
|
|
5701
7163
|
const next = checkbox.checked === true;
|
|
7164
|
+
const previous = state.a2aRelayStatus?.acceptPublicTasks === true;
|
|
7165
|
+
// Optimistic flip — POST + a follow-up GET of the (remote-worker)
|
|
7166
|
+
// relay-status endpoint used to gate the visual update. Flip the
|
|
7167
|
+
// local flag, render, then reconcile in background.
|
|
7168
|
+
if (state.a2aRelayStatus) {
|
|
7169
|
+
state.a2aRelayStatus = { ...state.a2aRelayStatus, acceptPublicTasks: next };
|
|
7170
|
+
}
|
|
7171
|
+
await renderShell();
|
|
5702
7172
|
try {
|
|
5703
7173
|
await apiPost("/api/a2a/public-tasks", { accept: next });
|
|
5704
|
-
|
|
7174
|
+
apiGet("/api/a2a/relay-status")
|
|
7175
|
+
.then((fresh) => {
|
|
7176
|
+
state.a2aRelayStatus = fresh;
|
|
7177
|
+
renderShell();
|
|
7178
|
+
})
|
|
7179
|
+
.catch(() => {});
|
|
5705
7180
|
} catch (error) {
|
|
7181
|
+
if (state.a2aRelayStatus) {
|
|
7182
|
+
state.a2aRelayStatus = { ...state.a2aRelayStatus, acceptPublicTasks: previous };
|
|
7183
|
+
}
|
|
5706
7184
|
state.pushError = error.message || String(error);
|
|
7185
|
+
await renderShell();
|
|
5707
7186
|
}
|
|
5708
|
-
await renderShell();
|
|
5709
7187
|
});
|
|
5710
7188
|
}
|
|
5711
7189
|
|
|
@@ -5713,13 +7191,24 @@ function bindShellInteractions() {
|
|
|
5713
7191
|
radio.addEventListener("change", async () => {
|
|
5714
7192
|
if (!radio.checked) return;
|
|
5715
7193
|
const preference = radio.value || "auto";
|
|
7194
|
+
const previous = state.session?.a2aExecutorPreference || "ask";
|
|
7195
|
+
// Optimistic flip — the POST + refreshSession() GET used to gate
|
|
7196
|
+
// any re-render that depends on the preference (e.g. downstream
|
|
7197
|
+
// picker defaults). Flip locally and reconcile in background.
|
|
7198
|
+
if (state.session) {
|
|
7199
|
+
state.session.a2aExecutorPreference = preference;
|
|
7200
|
+
}
|
|
7201
|
+
await renderShell();
|
|
5716
7202
|
try {
|
|
5717
7203
|
await apiPost("/api/settings/a2a-executor", { preference });
|
|
5718
|
-
|
|
7204
|
+
refreshSession().catch(() => {});
|
|
5719
7205
|
} catch (error) {
|
|
7206
|
+
if (state.session) {
|
|
7207
|
+
state.session.a2aExecutorPreference = previous;
|
|
7208
|
+
}
|
|
5720
7209
|
state.pushError = error.message || String(error);
|
|
7210
|
+
await renderShell();
|
|
5721
7211
|
}
|
|
5722
|
-
await renderShell();
|
|
5723
7212
|
});
|
|
5724
7213
|
}
|
|
5725
7214
|
|
|
@@ -5749,18 +7238,199 @@ function bindShellInteractions() {
|
|
|
5749
7238
|
});
|
|
5750
7239
|
}
|
|
5751
7240
|
|
|
7241
|
+
|
|
7242
|
+
for (const button of document.querySelectorAll("[data-hazbase-action]")) {
|
|
7243
|
+
button.addEventListener("click", async () => {
|
|
7244
|
+
state.hazbaseNotice = "";
|
|
7245
|
+
state.hazbaseError = "";
|
|
7246
|
+
const action = button.dataset.hazbaseAction || "";
|
|
7247
|
+
try {
|
|
7248
|
+
if (action === "request-otp") {
|
|
7249
|
+
// Read from the DOM input, not just state. The state mirror is
|
|
7250
|
+
// populated on every keystroke, but a pre-filled value (returning
|
|
7251
|
+
// user whose email came back from hazbase status) never fires
|
|
7252
|
+
// `input`, so state stays empty while the DOM shows the address.
|
|
7253
|
+
// Treat DOM as authoritative and fall back to state.
|
|
7254
|
+
const emailInput = document.querySelector('[data-hazbase-input="otp-email"]');
|
|
7255
|
+
const email = (emailInput?.value || state.hazbaseOtpEmail || "").trim();
|
|
7256
|
+
if (!email) throw new Error(L("error.hazbaseEmailRequired"));
|
|
7257
|
+
const result = await apiPost("/api/hazbase/request-otp", { email });
|
|
7258
|
+
state.hazbaseOtpEmail = email;
|
|
7259
|
+
state.hazbaseOtpRequested = true;
|
|
7260
|
+
state.hazbaseOtpCode = "";
|
|
7261
|
+
state.hazbaseNotice = result?.debugCode
|
|
7262
|
+
? `${L("settings.hazbase.notice.otpRequested")} (${result.debugCode})`
|
|
7263
|
+
: L("settings.hazbase.notice.otpRequested");
|
|
7264
|
+
} else if (action === "verify-otp") {
|
|
7265
|
+
// Same pattern — DOM wins, state fallback covers the rare case
|
|
7266
|
+
// where the field was removed/re-added between type and click.
|
|
7267
|
+
const emailInput = document.querySelector('[data-hazbase-input="otp-email"]');
|
|
7268
|
+
const codeInput = document.querySelector('[data-hazbase-input="otp-code"]');
|
|
7269
|
+
const email = (emailInput?.value || state.hazbaseOtpEmail || "").trim();
|
|
7270
|
+
const code = (codeInput?.value || state.hazbaseOtpCode || "").trim();
|
|
7271
|
+
if (!email) throw new Error(L("error.hazbaseEmailRequired"));
|
|
7272
|
+
if (!code) throw new Error(L("error.hazbaseOtpRequired"));
|
|
7273
|
+
await apiPost("/api/hazbase/verify-otp", { email, code });
|
|
7274
|
+
state.hazbaseOtpRequested = false;
|
|
7275
|
+
state.hazbaseOtpEmail = "";
|
|
7276
|
+
state.hazbaseOtpCode = "";
|
|
7277
|
+
state.hazbaseNotice = L("settings.hazbase.notice.otpVerified");
|
|
7278
|
+
} else if (action === "register-passkey") {
|
|
7279
|
+
if (!hazbasePasskeyHostSupport().eligible) {
|
|
7280
|
+
throw new Error(L("error.hazbasePasskeyLocalHostRequired"));
|
|
7281
|
+
}
|
|
7282
|
+
const { createPasskeyRegistrationCredential } = await loadHazbasePasskeyModule();
|
|
7283
|
+
const challenge = await apiPost("/api/hazbase/passkey/register/challenge", {});
|
|
7284
|
+
const credential = await createPasskeyRegistrationCredential(challenge);
|
|
7285
|
+
await apiPost("/api/hazbase/passkey/register/complete", {
|
|
7286
|
+
challengeId: challenge.challengeId,
|
|
7287
|
+
credential,
|
|
7288
|
+
});
|
|
7289
|
+
state.hazbaseNotice = L("settings.hazbase.notice.passkeyRegistered");
|
|
7290
|
+
} else if (action === "bootstrap-base-sepolia" || action === "bootstrap-base") {
|
|
7291
|
+
if (!hazbasePasskeyHostSupport().eligible) {
|
|
7292
|
+
throw new Error(L("error.hazbasePasskeyLocalHostRequired"));
|
|
7293
|
+
}
|
|
7294
|
+
const { createPasskeyAssertionCredential } = await loadHazbasePasskeyModule();
|
|
7295
|
+
const chainId = action === "bootstrap-base" ? 8453 : 84532;
|
|
7296
|
+
const challenge = await apiPost("/api/hazbase/passkey/assert/challenge", { purpose: "bootstrap" });
|
|
7297
|
+
const credential = await createPasskeyAssertionCredential(challenge);
|
|
7298
|
+
await apiPost("/api/hazbase/passkey/assert/complete", {
|
|
7299
|
+
challengeId: challenge.challengeId,
|
|
7300
|
+
credential,
|
|
7301
|
+
purpose: "bootstrap",
|
|
7302
|
+
});
|
|
7303
|
+
await apiPost("/api/hazbase/account/bootstrap", { chainId });
|
|
7304
|
+
state.hazbaseNotice = L("settings.hazbase.notice.walletBootstrapped", { chainId });
|
|
7305
|
+
} else if (action === "logout") {
|
|
7306
|
+
// Gate wallet logout behind an explicit confirm — the modal's
|
|
7307
|
+
// "confirm" button dispatches `logout-confirm`, which actually
|
|
7308
|
+
// hits the API. Short-circuit here so the initial click only
|
|
7309
|
+
// opens the dialog.
|
|
7310
|
+
state.hazbaseLogoutConfirmOpen = true;
|
|
7311
|
+
await renderShell();
|
|
7312
|
+
return;
|
|
7313
|
+
} else if (action === "logout-confirm") {
|
|
7314
|
+
state.hazbaseLogoutConfirmOpen = false;
|
|
7315
|
+
await apiPost("/api/hazbase/logout", {});
|
|
7316
|
+
state.hazbaseNotice = L("settings.hazbase.notice.signedOut");
|
|
7317
|
+
} else if (action === "refresh-session") {
|
|
7318
|
+
await apiPost("/api/hazbase/session/refresh", {});
|
|
7319
|
+
state.hazbaseOtpRequested = false;
|
|
7320
|
+
state.hazbaseOtpCode = "";
|
|
7321
|
+
state.hazbaseNotice = L("settings.hazbase.notice.sessionRefreshStarted");
|
|
7322
|
+
} else if (action === "change-email") {
|
|
7323
|
+
// Flip the form back to pre-send mode. We keep the email so typo
|
|
7324
|
+
// recovery ("hoshin" → "hoshino") stays one edit away, but drop
|
|
7325
|
+
// the now-stale OTP code. No server call — hazbase invalidates
|
|
7326
|
+
// the previous OTP automatically when a fresh one is requested.
|
|
7327
|
+
state.hazbaseOtpRequested = false;
|
|
7328
|
+
state.hazbaseOtpCode = "";
|
|
7329
|
+
state.hazbaseNotice = "";
|
|
7330
|
+
state.hazbaseError = "";
|
|
7331
|
+
await renderShell();
|
|
7332
|
+
// Move focus back to the (now re-enabled) email input so the user
|
|
7333
|
+
// can start editing immediately.
|
|
7334
|
+
document.querySelector('[data-hazbase-input="otp-email"]')?.focus();
|
|
7335
|
+
return;
|
|
7336
|
+
} else if (action === "mainnet-opt-in") {
|
|
7337
|
+
// Pure client-side reveal — the mainnet step is always in the flow
|
|
7338
|
+
// data, we just hide it behind an opt-in link to keep the default
|
|
7339
|
+
// path (testnet only) focused. No network call; no status refetch.
|
|
7340
|
+
state.hazbaseMainnetOptIn = true;
|
|
7341
|
+
await renderShell();
|
|
7342
|
+
return;
|
|
7343
|
+
}
|
|
7344
|
+
await fetchHazbaseStatus();
|
|
7345
|
+
} catch (error) {
|
|
7346
|
+
if (error?.errorKey === "hazbase-session-expired") {
|
|
7347
|
+
state.hazbaseOtpRequested = false;
|
|
7348
|
+
state.hazbaseOtpCode = "";
|
|
7349
|
+
await fetchHazbaseStatus();
|
|
7350
|
+
}
|
|
7351
|
+
state.hazbaseError = error.message || String(error);
|
|
7352
|
+
}
|
|
7353
|
+
await renderShell();
|
|
7354
|
+
});
|
|
7355
|
+
}
|
|
7356
|
+
|
|
7357
|
+
// Mirror every keystroke into state so a background re-render (poll tick,
|
|
7358
|
+
// notice clear, etc.) can repopulate `value="..."` without wiping what
|
|
7359
|
+
// the user was typing. Reads happen at button-click time against state,
|
|
7360
|
+
// which is why we don't need to also query the DOM in the handler.
|
|
7361
|
+
for (const input of document.querySelectorAll("[data-hazbase-input]")) {
|
|
7362
|
+
const name = input.dataset.hazbaseInput || "";
|
|
7363
|
+
input.addEventListener("input", () => {
|
|
7364
|
+
if (name === "otp-email") state.hazbaseOtpEmail = input.value;
|
|
7365
|
+
else if (name === "otp-code") state.hazbaseOtpCode = input.value;
|
|
7366
|
+
});
|
|
7367
|
+
// Enter key submits the step. In the email field it triggers "send"
|
|
7368
|
+
// (or "verify" once a code was already issued — the email field stays
|
|
7369
|
+
// editable post-send to support typo recovery via resend). In the OTP
|
|
7370
|
+
// field it always submits verify.
|
|
7371
|
+
input.addEventListener("keydown", (event) => {
|
|
7372
|
+
if (event.key !== "Enter" || event.isComposing) return;
|
|
7373
|
+
event.preventDefault();
|
|
7374
|
+
const target = name === "otp-code"
|
|
7375
|
+
? "verify-otp"
|
|
7376
|
+
: state.hazbaseOtpRequested ? "verify-otp" : "request-otp";
|
|
7377
|
+
const btn = document.querySelector(`[data-hazbase-action="${target}"]`);
|
|
7378
|
+
btn?.click();
|
|
7379
|
+
});
|
|
7380
|
+
}
|
|
7381
|
+
|
|
7382
|
+
// Tap-to-copy the wallet payout address. We swap the clipboard icon to a
|
|
7383
|
+
// check mark for ~1.5s via CSS (toggling `.is-copied`) rather than a full
|
|
7384
|
+
// re-render, so focus stays on the button and the rest of the settings
|
|
7385
|
+
// page doesn't flicker. The address sits inside a banner that doesn't
|
|
7386
|
+
// otherwise re-render on every tick, so an ephemeral class flip is the
|
|
7387
|
+
// cleanest way to acknowledge the copy.
|
|
7388
|
+
for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
7389
|
+
button.addEventListener("click", async () => {
|
|
7390
|
+
const text = button.dataset.walletAddressCopy || "";
|
|
7391
|
+
if (!text) return;
|
|
7392
|
+
try {
|
|
7393
|
+
await copyTextToClipboard(text);
|
|
7394
|
+
button.classList.add("is-copied");
|
|
7395
|
+
if (button._copyResetTimer) clearTimeout(button._copyResetTimer);
|
|
7396
|
+
button._copyResetTimer = setTimeout(() => {
|
|
7397
|
+
button.classList.remove("is-copied");
|
|
7398
|
+
button._copyResetTimer = null;
|
|
7399
|
+
}, 1500);
|
|
7400
|
+
} catch {
|
|
7401
|
+
// Copy failed (permissions denied, execCommand unsupported, etc).
|
|
7402
|
+
// We intentionally stay silent — the address is still visible and
|
|
7403
|
+
// `user-select: all` on the text span lets the user long-press to
|
|
7404
|
+
// select manually on iOS.
|
|
7405
|
+
}
|
|
7406
|
+
});
|
|
7407
|
+
}
|
|
7408
|
+
|
|
5752
7409
|
for (const button of document.querySelectorAll("[data-locale-option]")) {
|
|
5753
7410
|
button.addEventListener("click", async () => {
|
|
5754
7411
|
state.pushError = "";
|
|
5755
7412
|
state.pushNotice = "";
|
|
7413
|
+
const nextLocale = button.dataset.localeOption || "";
|
|
7414
|
+
// Flip language synchronously and render before the POST — the
|
|
7415
|
+
// UI switches in one frame instead of waiting on the round-trip
|
|
7416
|
+
// plus the 7-endpoint refreshAuthenticatedState() that followed.
|
|
7417
|
+
const previousSession = applyLocaleOverrideOptimistically(nextLocale);
|
|
7418
|
+
await renderShell();
|
|
5756
7419
|
try {
|
|
5757
|
-
await
|
|
5758
|
-
|
|
5759
|
-
|
|
7420
|
+
await persistLocaleOverride(nextLocale, previousSession);
|
|
7421
|
+
// Server-rendered inbox/timeline strings (kind labels, summaries)
|
|
7422
|
+
// are localised; refresh them in the background so the user
|
|
7423
|
+
// doesn't wait. Polling would eventually pick this up anyway,
|
|
7424
|
+
// but this makes the switch feel instant.
|
|
7425
|
+
Promise.all([
|
|
7426
|
+
refreshInbox().catch(() => {}),
|
|
7427
|
+
refreshInboxDiff().catch(() => {}),
|
|
7428
|
+
refreshTimeline().catch(() => {}),
|
|
7429
|
+
]).then(() => renderShell());
|
|
5760
7430
|
} catch (error) {
|
|
5761
7431
|
state.pushError = error.message || String(error);
|
|
7432
|
+
await renderShell();
|
|
5762
7433
|
}
|
|
5763
|
-
await renderShell();
|
|
5764
7434
|
});
|
|
5765
7435
|
}
|
|
5766
7436
|
|
|
@@ -6261,6 +7931,21 @@ function bindSharedUi(renderFn) {
|
|
|
6261
7931
|
});
|
|
6262
7932
|
}
|
|
6263
7933
|
|
|
7934
|
+
for (const button of document.querySelectorAll("[data-close-hazbase-logout-confirm]")) {
|
|
7935
|
+
// Mirror the session-logout modal: the backdrop swallows outside-clicks
|
|
7936
|
+
// to dismiss, but inside-card clicks bubble up through here too — skip
|
|
7937
|
+
// those so the Confirm button's own handler can run alone.
|
|
7938
|
+
button.addEventListener("click", async (event) => {
|
|
7939
|
+
if (button.classList.contains("modal-backdrop")) {
|
|
7940
|
+
if (event.target.closest(".modal-card")) {
|
|
7941
|
+
return;
|
|
7942
|
+
}
|
|
7943
|
+
}
|
|
7944
|
+
state.hazbaseLogoutConfirmOpen = false;
|
|
7945
|
+
await renderFn();
|
|
7946
|
+
});
|
|
7947
|
+
}
|
|
7948
|
+
|
|
6264
7949
|
for (const button of document.querySelectorAll("[data-dismiss-install]")) {
|
|
6265
7950
|
button.addEventListener("click", async () => {
|
|
6266
7951
|
state.installBannerDismissed = true;
|
|
@@ -6319,6 +8004,15 @@ async function switchTab(tab) {
|
|
|
6319
8004
|
syncCurrentItemUrl(state.currentItem);
|
|
6320
8005
|
}
|
|
6321
8006
|
await renderShell();
|
|
8007
|
+
if (tab === "settings") {
|
|
8008
|
+
void fetchHazbaseStatus()
|
|
8009
|
+
.then(() => {
|
|
8010
|
+
if (state.currentTab === "settings") {
|
|
8011
|
+
renderCurrentSurface();
|
|
8012
|
+
}
|
|
8013
|
+
})
|
|
8014
|
+
.catch(() => {});
|
|
8015
|
+
}
|
|
6322
8016
|
}
|
|
6323
8017
|
|
|
6324
8018
|
function openItem({ kind, token, sourceTab, sourceSubtab }) {
|
|
@@ -6455,6 +8149,9 @@ function pendingInboxCount() {
|
|
|
6455
8149
|
}
|
|
6456
8150
|
|
|
6457
8151
|
function tabForItemKind(kind, fallback) {
|
|
8152
|
+
if (kind === "completion" || kind === "plan_ready" || kind === "moltbook_draft" || kind === "moltbook_reply" || kind === "thread_share" || kind === "a2a_task" || kind === "a2a_task_result") {
|
|
8153
|
+
return "inbox";
|
|
8154
|
+
}
|
|
6458
8155
|
if (kind === "diff_thread") {
|
|
6459
8156
|
return "diff";
|
|
6460
8157
|
}
|
|
@@ -6464,22 +8161,20 @@ function tabForItemKind(kind, fallback) {
|
|
|
6464
8161
|
if (TIMELINE_MESSAGE_KINDS.has(kind)) {
|
|
6465
8162
|
return "timeline";
|
|
6466
8163
|
}
|
|
6467
|
-
if (kind === "completion") {
|
|
6468
|
-
return "inbox";
|
|
6469
|
-
}
|
|
6470
8164
|
if (fallback === "timeline") {
|
|
6471
8165
|
return "timeline";
|
|
6472
8166
|
}
|
|
6473
8167
|
return kind === "approval" || kind === "plan" || kind === "choice"
|
|
6474
8168
|
? "inbox"
|
|
6475
|
-
:
|
|
8169
|
+
: "inbox";
|
|
6476
8170
|
}
|
|
6477
8171
|
|
|
6478
8172
|
function inboxSubtabForItemKind(kind, sourceSubtab = "") {
|
|
6479
8173
|
if (normalizeClientText(sourceSubtab || "") === "completed") {
|
|
6480
8174
|
return "completed";
|
|
6481
8175
|
}
|
|
6482
|
-
|
|
8176
|
+
const completedKinds = new Set(["completion", "assistant_final", "plan_ready", "moltbook_reply", "thread_share", "a2a_task_result"]);
|
|
8177
|
+
return completedKinds.has(kind) ? "completed" : "pending";
|
|
6483
8178
|
}
|
|
6484
8179
|
|
|
6485
8180
|
function kindMeta(kind, item) {
|
|
@@ -6893,6 +8588,8 @@ function renderIcon(name) {
|
|
|
6893
8588
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M5 7h14"/><path d="M8 12h8"/><path d="M10.5 17h3"/></svg>`;
|
|
6894
8589
|
case "check":
|
|
6895
8590
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="m6.8 12.5 3.2 3.2 7.2-7.4"/></svg>`;
|
|
8591
|
+
case "copy":
|
|
8592
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="3.5" width="11" height="14" rx="2"/><path d="M6.5 7.5H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2v-.5"/></svg>`;
|
|
6896
8593
|
case "lock":
|
|
6897
8594
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="5.5" y="10.5" width="13" height="9" rx="2"/><path d="M8 10.5V7.5a4 4 0 0 1 8 0v3"/></svg>`;
|
|
6898
8595
|
case "coin":
|
|
@@ -7114,6 +8811,11 @@ function localizeApiError(value) {
|
|
|
7114
8811
|
"choice-input-read-only": "error.choiceInputReadOnly",
|
|
7115
8812
|
"choice-input-already-handled": "error.choiceInputAlreadyHandled",
|
|
7116
8813
|
"mkcert-root-ca-not-found": "error.mkcertRootCaNotFound",
|
|
8814
|
+
"hazbase-auth-required": "error.hazbaseAuthRequired",
|
|
8815
|
+
"hazbase-session-expired": "error.hazbaseSessionExpired",
|
|
8816
|
+
"hazbase-passkey-local-host-required": "error.hazbasePasskeyLocalHostRequired",
|
|
8817
|
+
"hazbase-wallet-account-missing": "error.hazbaseWalletAccountMissing",
|
|
8818
|
+
"unsupported-chain": "error.unsupportedChain",
|
|
7117
8819
|
};
|
|
7118
8820
|
const key = map[raw];
|
|
7119
8821
|
return key ? L(key) : raw;
|
|
@@ -7209,6 +8911,16 @@ function parseItemRef(value) {
|
|
|
7209
8911
|
return kind && token ? { kind, token } : null;
|
|
7210
8912
|
}
|
|
7211
8913
|
|
|
8914
|
+
function sanitizeExternalTargetTab(value) {
|
|
8915
|
+
const normalized = normalizeClientText(value || "");
|
|
8916
|
+
return EXTERNAL_TARGET_TABS.has(normalized) ? normalized : "";
|
|
8917
|
+
}
|
|
8918
|
+
|
|
8919
|
+
function sanitizeExternalTargetInboxSubtab(value) {
|
|
8920
|
+
const normalized = normalizeClientText(value || "");
|
|
8921
|
+
return EXTERNAL_TARGET_INBOX_SUBTABS.has(normalized) ? normalized : "";
|
|
8922
|
+
}
|
|
8923
|
+
|
|
7212
8924
|
async function applyExternalTargetUrl(urlString, { allowRefresh = true } = {}) {
|
|
7213
8925
|
if (!state.session?.authenticated) {
|
|
7214
8926
|
return;
|
|
@@ -7225,10 +8937,16 @@ async function applyExternalTargetUrl(urlString, { allowRefresh = true } = {}) {
|
|
|
7225
8937
|
if (!itemRef) {
|
|
7226
8938
|
return;
|
|
7227
8939
|
}
|
|
8940
|
+
const explicitTargetTab = sanitizeExternalTargetTab(nextUrl.searchParams.get("tab"));
|
|
8941
|
+
const explicitTargetSubtab = sanitizeExternalTargetInboxSubtab(nextUrl.searchParams.get("subtab"));
|
|
8942
|
+
const targetTab = explicitTargetTab || tabForItemKind(itemRef.kind, state.currentTab);
|
|
8943
|
+
const targetSubtab = targetTab === "inbox" ? inboxSubtabForItemKind(itemRef.kind, explicitTargetSubtab) : "";
|
|
7228
8944
|
|
|
7229
8945
|
const sameItem =
|
|
7230
8946
|
Boolean(state.currentItem) &&
|
|
7231
8947
|
isSameItemRef(state.currentItem, itemRef) &&
|
|
8948
|
+
state.currentTab === targetTab &&
|
|
8949
|
+
(targetTab !== "inbox" || state.inboxSubtab === targetSubtab) &&
|
|
7232
8950
|
(isDesktopLayout() || state.detailOpen);
|
|
7233
8951
|
if (sameItem) {
|
|
7234
8952
|
if (allowRefresh) {
|
|
@@ -7242,7 +8960,8 @@ async function applyExternalTargetUrl(urlString, { allowRefresh = true } = {}) {
|
|
|
7242
8960
|
openItem({
|
|
7243
8961
|
kind: itemRef.kind,
|
|
7244
8962
|
token: itemRef.token,
|
|
7245
|
-
sourceTab:
|
|
8963
|
+
sourceTab: targetTab,
|
|
8964
|
+
sourceSubtab: targetSubtab,
|
|
7246
8965
|
});
|
|
7247
8966
|
if (isFastPathItemRef(itemRef)) {
|
|
7248
8967
|
state.launchItemIntent = {
|