viveworker 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/README.md +141 -0
- package/package.json +7 -1
- package/plugins/viveworker-control-plane/.codex-plugin/plugin.json +49 -0
- package/plugins/viveworker-control-plane/.mcp.json +11 -0
- package/plugins/viveworker-control-plane/DISTRIBUTION.md +96 -0
- package/plugins/viveworker-control-plane/assets/viveworker-logo-v2.png +0 -0
- package/plugins/viveworker-control-plane/assets/viveworker-logo-v2.svg +39 -0
- package/plugins/viveworker-control-plane/skills/viveworker-control-plane/SKILL.md +83 -0
- package/scripts/a2a-executor.mjs +261 -7
- package/scripts/a2a-handler.mjs +88 -0
- package/scripts/a2a-relay-client.mjs +6 -0
- package/scripts/lib/markdown-render.mjs +128 -1
- package/scripts/mcp-server.mjs +891 -0
- package/scripts/stats-cli.mjs +683 -0
- package/scripts/viveworker-bridge.mjs +1504 -128
- package/scripts/viveworker.mjs +262 -1
- package/skills/viveworker-control-plane/SKILL.md +104 -0
- package/skills/viveworker-control-plane/agents/openai.yaml +12 -0
- package/templates/CLAUDE.viveworker.md +67 -0
- package/web/app.css +162 -0
- package/web/app.js +1164 -101
- package/web/build-id.js +1 -0
- package/web/i18n.js +123 -15
- package/web/icons/apple-touch-icon.png +0 -0
- package/web/icons/viveworker-icon-192.png +0 -0
- package/web/icons/viveworker-icon-512.png +0 -0
- package/web/icons/viveworker-v-pulse.svg +16 -1
- package/web/index.html +2 -2
- package/web/remote-pairing/api-router.js +84 -0
- package/web/remote-pairing/transport.js +67 -2
- package/web/sw.js +16 -6
- package/web/icons/viveworker-beacon-v.svg +0 -19
- package/web/icons/viveworker-icon-1024.png +0 -0
- package/web/icons/viveworker-v-check.svg +0 -19
package/web/app.js
CHANGED
|
@@ -5,10 +5,10 @@ import {
|
|
|
5
5
|
savePairingState as saveRemotePairingState,
|
|
6
6
|
clearPairingState as clearRemotePairingState,
|
|
7
7
|
} from "./remote-pairing/pairing-state.js";
|
|
8
|
-
|
|
8
|
+
const APP_BUILD_ID = "__VIVEWORKER_APP_BUILD_ID__";
|
|
9
|
+
const { getRoutingTelemetry, routedFetch } = await import(`./remote-pairing/api-router.js?v=${encodeURIComponent(APP_BUILD_ID)}`);
|
|
9
10
|
|
|
10
11
|
const DESKTOP_BREAKPOINT = 980;
|
|
11
|
-
const APP_BUILD_ID = "20260428-remote-token-refresh";
|
|
12
12
|
const INSTALL_BANNER_DISMISS_KEY = "viveworker-install-banner-dismissed-v2";
|
|
13
13
|
const PUSH_BANNER_DISMISS_KEY = "viveworker-push-banner-dismissed-v1";
|
|
14
14
|
const INITIAL_DETECTED_LOCALE = detectBrowserLocale();
|
|
@@ -17,6 +17,7 @@ const TIMELINE_OPERATIONAL_KINDS = new Set(["approval", "plan", "plan_ready", "c
|
|
|
17
17
|
const EXTERNAL_TARGET_TABS = new Set(["inbox", "timeline", "diff"]);
|
|
18
18
|
const EXTERNAL_TARGET_INBOX_SUBTABS = new Set(["pending", "completed"]);
|
|
19
19
|
const THREAD_FILTER_INTERACTION_DEFER_MS = 8000;
|
|
20
|
+
const SCROLLABLE_CONTENT_INTERACTION_DEFER_MS = 8000;
|
|
20
21
|
const MAX_COMPLETION_REPLY_IMAGE_COUNT = 4;
|
|
21
22
|
const NOTIFICATION_INTENT_CACHE = "viveworker-notification-intent-v1";
|
|
22
23
|
const NOTIFICATION_INTENT_PATH = "/__viveworker_notification_intent__";
|
|
@@ -37,8 +38,41 @@ const BOOT_SPLASH_STAGE = Object.freeze({
|
|
|
37
38
|
establishing: 2,
|
|
38
39
|
loading: 3,
|
|
39
40
|
});
|
|
41
|
+
const DETAIL_FETCH_TIMEOUT_MS = 12_000;
|
|
42
|
+
const DETAIL_REFRESH_FALLBACK_TIMEOUT_MS = 2_500;
|
|
43
|
+
const DETAIL_STICKY_LAN_PROBE_TIMEOUT_MS = 350;
|
|
44
|
+
const COMPLETION_REPLY_SEND_TIMEOUT_MS = 22_000;
|
|
45
|
+
const COMPLETION_REPLY_OPTIMISTIC_SENT_MS = 1_600;
|
|
46
|
+
const TIMELINE_REFRESH_TIMEOUT_MS = 8_000;
|
|
47
|
+
const TIMELINE_POLL_TIMEOUT_MS = 4_500;
|
|
48
|
+
const FAST_POLL_STEP_TIMEOUT_MS = 4_500;
|
|
49
|
+
const TIMELINE_STICKY_LAN_PROBE_TIMEOUT_MS = 350;
|
|
50
|
+
const CLIENT_EVENT_REPORT_TIMEOUT_MS = 1_800;
|
|
51
|
+
const TIMELINE_LIVE_REFRESH_TIMEOUT_MS = 2_000;
|
|
52
|
+
const TIMELINE_LIVE_RETRY_MS = 5_000;
|
|
40
53
|
const timelineImageObjectUrlCache = new Map();
|
|
41
54
|
|
|
55
|
+
function wait(ms) {
|
|
56
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function runFastPollStep(label, fn, timeoutMs = FAST_POLL_STEP_TIMEOUT_MS) {
|
|
60
|
+
try {
|
|
61
|
+
await Promise.race([
|
|
62
|
+
Promise.resolve().then(fn),
|
|
63
|
+
wait(timeoutMs).then(() => {
|
|
64
|
+
const error = new Error(`${label}-poll-timeout`);
|
|
65
|
+
error.code = "poll-timeout";
|
|
66
|
+
throw error;
|
|
67
|
+
}),
|
|
68
|
+
]);
|
|
69
|
+
return { label, ok: true };
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.warn(`[poll:${label}]`, error?.message || error);
|
|
72
|
+
return { label, ok: false };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
42
76
|
const state = {
|
|
43
77
|
session: null,
|
|
44
78
|
inbox: null,
|
|
@@ -73,6 +107,7 @@ const state = {
|
|
|
73
107
|
listScrollState: null,
|
|
74
108
|
pendingListScrollRestore: false,
|
|
75
109
|
threadFilterInteractionUntilMs: 0,
|
|
110
|
+
scrollableContentInteractionUntilMs: 0,
|
|
76
111
|
diffThreadExpandedFiles: {},
|
|
77
112
|
detailDiffExpanded: {},
|
|
78
113
|
choiceLocalDrafts: {},
|
|
@@ -156,7 +191,16 @@ const state = {
|
|
|
156
191
|
};
|
|
157
192
|
|
|
158
193
|
let detailLoadSequence = 0;
|
|
194
|
+
let timelineHydrationSequence = 0;
|
|
195
|
+
let authenticatedPollInFlight = false;
|
|
159
196
|
let hazbasePasskeyModulePromise = null;
|
|
197
|
+
let lastTimelineRenderReportKey = "";
|
|
198
|
+
let timelineLiveStream = null;
|
|
199
|
+
let timelineLiveStreamRetryTimer = 0;
|
|
200
|
+
let timelineLiveRefreshInFlight = false;
|
|
201
|
+
let timelineLiveRefreshPending = false;
|
|
202
|
+
let lastTimelineLiveRevision = 0;
|
|
203
|
+
const reportedTimelineRenderTokens = new Set();
|
|
160
204
|
|
|
161
205
|
async function loadHazbasePasskeyModule() {
|
|
162
206
|
if (!hazbasePasskeyModulePromise) {
|
|
@@ -675,6 +719,8 @@ async function boot() {
|
|
|
675
719
|
.then(async () => {
|
|
676
720
|
if (!shouldDeferRenderForActiveInteraction()) {
|
|
677
721
|
await renderShell();
|
|
722
|
+
} else {
|
|
723
|
+
renderDeferredInteractionShellUpdates();
|
|
678
724
|
}
|
|
679
725
|
})
|
|
680
726
|
.catch(() => {});
|
|
@@ -686,55 +732,78 @@ async function boot() {
|
|
|
686
732
|
.then(async () => {
|
|
687
733
|
if (!shouldDeferRenderForActiveInteraction()) {
|
|
688
734
|
await renderShell();
|
|
735
|
+
} else {
|
|
736
|
+
renderDeferredInteractionShellUpdates();
|
|
689
737
|
}
|
|
690
738
|
})
|
|
691
739
|
.catch(() => {});
|
|
692
740
|
syncDetectedLocalePreference().catch(() => {});
|
|
693
741
|
|
|
694
742
|
setInterval(async () => {
|
|
695
|
-
if (!state.session?.authenticated) {
|
|
696
|
-
return;
|
|
697
|
-
}
|
|
698
|
-
const consumedNotificationIntent = await consumePendingNotificationIntent();
|
|
699
|
-
if (consumedNotificationIntent) {
|
|
743
|
+
if (!state.session?.authenticated || authenticatedPollInFlight) {
|
|
700
744
|
return;
|
|
701
745
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
746
|
+
authenticatedPollInFlight = true;
|
|
747
|
+
try {
|
|
748
|
+
const consumedNotificationIntent = await consumePendingNotificationIntent();
|
|
749
|
+
if (consumedNotificationIntent) {
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
// Keep timeline freshness independent from secondary status probes.
|
|
753
|
+
// Remote relay reconnects can make one small API call wait for a full
|
|
754
|
+
// transport timeout; the timeline should still render on the next tick.
|
|
755
|
+
await Promise.all([
|
|
756
|
+
runFastPollStep(
|
|
757
|
+
"timeline",
|
|
758
|
+
() => refreshTimeline({ timeoutMs: TIMELINE_POLL_TIMEOUT_MS }),
|
|
759
|
+
TIMELINE_POLL_TIMEOUT_MS + 500,
|
|
760
|
+
),
|
|
761
|
+
runFastPollStep(
|
|
762
|
+
"inbox",
|
|
763
|
+
() => refreshInbox({ timeoutMs: FAST_POLL_STEP_TIMEOUT_MS }),
|
|
764
|
+
FAST_POLL_STEP_TIMEOUT_MS + 500,
|
|
765
|
+
),
|
|
766
|
+
runFastPollStep(
|
|
767
|
+
"devices",
|
|
768
|
+
() => refreshDevices({ timeoutMs: FAST_POLL_STEP_TIMEOUT_MS }),
|
|
769
|
+
FAST_POLL_STEP_TIMEOUT_MS + 500,
|
|
770
|
+
),
|
|
771
|
+
runFastPollStep(
|
|
772
|
+
"push",
|
|
773
|
+
() => refreshPushStatus({ timeoutMs: FAST_POLL_STEP_TIMEOUT_MS }),
|
|
774
|
+
FAST_POLL_STEP_TIMEOUT_MS + 500,
|
|
775
|
+
),
|
|
776
|
+
runFastPollStep(
|
|
777
|
+
"a2a-relay",
|
|
778
|
+
() => fetchA2aRelayStatus({ timeoutMs: FAST_POLL_STEP_TIMEOUT_MS }),
|
|
779
|
+
FAST_POLL_STEP_TIMEOUT_MS + 500,
|
|
780
|
+
),
|
|
781
|
+
]);
|
|
782
|
+
ensureCurrentSelection();
|
|
783
|
+
maybeAutoFocusClaudePending();
|
|
784
|
+
if (!shouldDeferRenderForActiveInteraction()) {
|
|
785
|
+
await renderShell();
|
|
786
|
+
} else {
|
|
787
|
+
renderDeferredInteractionShellUpdates();
|
|
788
|
+
}
|
|
725
789
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
790
|
+
Promise.allSettled([
|
|
791
|
+
refreshInboxDiff(),
|
|
792
|
+
fetchMoltbookScoutStatus(),
|
|
793
|
+
fetchA2aShareStatus(),
|
|
794
|
+
fetchRemotePairingStatus(),
|
|
795
|
+
])
|
|
796
|
+
.then(async () => {
|
|
797
|
+
if (!shouldDeferRenderForActiveInteraction()) {
|
|
798
|
+
await renderShell();
|
|
799
|
+
} else {
|
|
800
|
+
renderDeferredInteractionShellUpdates();
|
|
801
|
+
}
|
|
802
|
+
})
|
|
803
|
+
.catch(() => {});
|
|
804
|
+
} finally {
|
|
805
|
+
authenticatedPollInFlight = false;
|
|
806
|
+
}
|
|
738
807
|
}, 3000);
|
|
739
808
|
}
|
|
740
809
|
|
|
@@ -883,7 +952,11 @@ async function refreshAuthenticatedState() {
|
|
|
883
952
|
// moltbook/a2a worker calls — the a2a share worker has a 10s timeout) and
|
|
884
953
|
// runs in the background after the shell renders.
|
|
885
954
|
async function refreshAuthenticatedStateLocal() {
|
|
886
|
-
await Promise.all([
|
|
955
|
+
await Promise.all([
|
|
956
|
+
refreshInbox({ timeoutMs: FAST_POLL_STEP_TIMEOUT_MS }),
|
|
957
|
+
refreshTimeline({ timeoutMs: TIMELINE_POLL_TIMEOUT_MS }),
|
|
958
|
+
refreshDevices({ timeoutMs: FAST_POLL_STEP_TIMEOUT_MS }),
|
|
959
|
+
]);
|
|
887
960
|
ensureCurrentSelection();
|
|
888
961
|
}
|
|
889
962
|
|
|
@@ -939,9 +1012,7 @@ async function refreshBootstrap() {
|
|
|
939
1012
|
syncCompletedThreadFilter();
|
|
940
1013
|
syncInboxSubtab();
|
|
941
1014
|
|
|
942
|
-
|
|
943
|
-
syncTimelineThreadFilter();
|
|
944
|
-
syncTimelineKindFilter();
|
|
1015
|
+
setTimelinePayload(bootstrap?.timeline || null, { hydrateImages: true, renderOnHydrate: false });
|
|
945
1016
|
|
|
946
1017
|
const devicesPayload = bootstrap?.devices;
|
|
947
1018
|
state.devices = Array.isArray(devicesPayload?.devices) ? devicesPayload.devices : [];
|
|
@@ -1048,7 +1119,7 @@ function detectBrowserLocale() {
|
|
|
1048
1119
|
return normalizeLocale(navigator.language || "") || DEFAULT_LOCALE;
|
|
1049
1120
|
}
|
|
1050
1121
|
|
|
1051
|
-
async function refreshPushStatus() {
|
|
1122
|
+
async function refreshPushStatus(opts = {}) {
|
|
1052
1123
|
const client = await getClientPushState();
|
|
1053
1124
|
const { clientSubscription, ...clientStatus } = client;
|
|
1054
1125
|
if (!state.session?.authenticated) {
|
|
@@ -1064,7 +1135,7 @@ async function refreshPushStatus() {
|
|
|
1064
1135
|
}
|
|
1065
1136
|
|
|
1066
1137
|
try {
|
|
1067
|
-
let server = await apiGet("/api/push/status");
|
|
1138
|
+
let server = await apiGet("/api/push/status", opts);
|
|
1068
1139
|
if (
|
|
1069
1140
|
server?.enabled === true &&
|
|
1070
1141
|
server?.subscribed !== true &&
|
|
@@ -1076,8 +1147,8 @@ async function refreshPushStatus() {
|
|
|
1076
1147
|
subscription: clientSubscription,
|
|
1077
1148
|
userAgent: navigator.userAgent,
|
|
1078
1149
|
standalone: isStandaloneMode(),
|
|
1079
|
-
});
|
|
1080
|
-
server = await apiGet("/api/push/status");
|
|
1150
|
+
}, opts);
|
|
1151
|
+
server = await apiGet("/api/push/status", opts);
|
|
1081
1152
|
} catch {
|
|
1082
1153
|
// Best effort: if the browser still has a local subscription, the
|
|
1083
1154
|
// status row can reflect that while the enable action repairs it.
|
|
@@ -1114,13 +1185,13 @@ async function fetchMoltbookScoutStatus() {
|
|
|
1114
1185
|
}
|
|
1115
1186
|
}
|
|
1116
1187
|
|
|
1117
|
-
async function fetchA2aRelayStatus() {
|
|
1188
|
+
async function fetchA2aRelayStatus(opts = {}) {
|
|
1118
1189
|
if (!state.session?.a2aRelayEnabled) {
|
|
1119
1190
|
state.a2aRelayStatus = null;
|
|
1120
1191
|
return;
|
|
1121
1192
|
}
|
|
1122
1193
|
try {
|
|
1123
|
-
state.a2aRelayStatus = await apiGet("/api/a2a/relay-status");
|
|
1194
|
+
state.a2aRelayStatus = await apiGet("/api/a2a/relay-status", opts);
|
|
1124
1195
|
} catch {
|
|
1125
1196
|
state.a2aRelayStatus = null;
|
|
1126
1197
|
}
|
|
@@ -1204,8 +1275,8 @@ async function getClientPushState() {
|
|
|
1204
1275
|
};
|
|
1205
1276
|
}
|
|
1206
1277
|
|
|
1207
|
-
async function refreshInbox() {
|
|
1208
|
-
const fast = await apiGet("/api/inbox");
|
|
1278
|
+
async function refreshInbox(opts = {}) {
|
|
1279
|
+
const fast = await apiGet("/api/inbox", opts);
|
|
1209
1280
|
// `/api/inbox` now returns only `{ pending, completed }`. The `diff`
|
|
1210
1281
|
// half lives at `/api/inbox/diff` because it spawns `git` subprocesses
|
|
1211
1282
|
// server-side and was blocking first paint of the completed/pending
|
|
@@ -1240,13 +1311,309 @@ async function refreshInboxDiff() {
|
|
|
1240
1311
|
}
|
|
1241
1312
|
}
|
|
1242
1313
|
|
|
1243
|
-
async function refreshTimeline() {
|
|
1244
|
-
|
|
1314
|
+
async function refreshTimeline(opts = {}) {
|
|
1315
|
+
const requestOpts = {
|
|
1316
|
+
timeoutMs: TIMELINE_REFRESH_TIMEOUT_MS,
|
|
1317
|
+
probeLanWhileSticky: true,
|
|
1318
|
+
stickyLanProbeTimeoutMs: TIMELINE_STICKY_LAN_PROBE_TIMEOUT_MS,
|
|
1319
|
+
...opts,
|
|
1320
|
+
};
|
|
1321
|
+
setTimelinePayload(await apiGet("/api/timeline", requestOpts), { hydrateImages: true });
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
async function refreshTimelineDirectLan(opts = {}) {
|
|
1325
|
+
setTimelinePayload(await apiGetDirectLan("/api/timeline", opts), { hydrateImages: true });
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function closeTimelineLiveStream() {
|
|
1329
|
+
if (timelineLiveStream) {
|
|
1330
|
+
try {
|
|
1331
|
+
timelineLiveStream.close();
|
|
1332
|
+
} catch {}
|
|
1333
|
+
}
|
|
1334
|
+
timelineLiveStream = null;
|
|
1335
|
+
if (timelineLiveStreamRetryTimer) {
|
|
1336
|
+
clearTimeout(timelineLiveStreamRetryTimer);
|
|
1337
|
+
timelineLiveStreamRetryTimer = 0;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function shouldUseTimelineLiveStream() {
|
|
1342
|
+
if (!state.session?.authenticated || typeof EventSource !== "function") {
|
|
1343
|
+
return false;
|
|
1344
|
+
}
|
|
1345
|
+
const telemetry = getRoutingTelemetry() || {};
|
|
1346
|
+
return telemetry.lastRoute === "lan";
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function scheduleTimelineLiveStreamRetry() {
|
|
1350
|
+
if (timelineLiveStreamRetryTimer || !state.session?.authenticated) {
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
timelineLiveStreamRetryTimer = setTimeout(() => {
|
|
1354
|
+
timelineLiveStreamRetryTimer = 0;
|
|
1355
|
+
syncTimelineLiveStream();
|
|
1356
|
+
}, TIMELINE_LIVE_RETRY_MS);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function syncTimelineLiveStream() {
|
|
1360
|
+
if (!state.session?.authenticated) {
|
|
1361
|
+
closeTimelineLiveStream();
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
if (timelineLiveStream) {
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
if (!shouldUseTimelineLiveStream()) {
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
try {
|
|
1371
|
+
const stream = new EventSource("/api/timeline/stream");
|
|
1372
|
+
timelineLiveStream = stream;
|
|
1373
|
+
stream.addEventListener("hello", (event) => {
|
|
1374
|
+
const data = parseTimelineLiveEvent(event);
|
|
1375
|
+
lastTimelineLiveRevision = Math.max(lastTimelineLiveRevision, Number(data?.revision) || 0);
|
|
1376
|
+
});
|
|
1377
|
+
stream.addEventListener("timeline:update", (event) => {
|
|
1378
|
+
const data = parseTimelineLiveEvent(event);
|
|
1379
|
+
handleTimelineLiveUpdate(data).catch((err) => {
|
|
1380
|
+
console.warn("[timeline-live]", err?.message || err);
|
|
1381
|
+
});
|
|
1382
|
+
});
|
|
1383
|
+
stream.addEventListener("heartbeat", (event) => {
|
|
1384
|
+
const data = parseTimelineLiveEvent(event);
|
|
1385
|
+
lastTimelineLiveRevision = Math.max(lastTimelineLiveRevision, Number(data?.revision) || 0);
|
|
1386
|
+
});
|
|
1387
|
+
stream.onerror = () => {
|
|
1388
|
+
if (timelineLiveStream === stream) {
|
|
1389
|
+
closeTimelineLiveStream();
|
|
1390
|
+
scheduleTimelineLiveStreamRetry();
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
console.warn("[timeline-live]", err?.message || err);
|
|
1395
|
+
closeTimelineLiveStream();
|
|
1396
|
+
scheduleTimelineLiveStreamRetry();
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
function parseTimelineLiveEvent(event) {
|
|
1401
|
+
try {
|
|
1402
|
+
return JSON.parse(event?.data || "{}");
|
|
1403
|
+
} catch {
|
|
1404
|
+
return null;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
async function handleTimelineLiveUpdate(data) {
|
|
1409
|
+
const revision = Number(data?.revision) || 0;
|
|
1410
|
+
if (revision && revision <= lastTimelineLiveRevision) {
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
lastTimelineLiveRevision = Math.max(lastTimelineLiveRevision, revision);
|
|
1414
|
+
if (timelineLiveRefreshInFlight) {
|
|
1415
|
+
timelineLiveRefreshPending = true;
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
timelineLiveRefreshInFlight = true;
|
|
1419
|
+
try {
|
|
1420
|
+
do {
|
|
1421
|
+
timelineLiveRefreshPending = false;
|
|
1422
|
+
try {
|
|
1423
|
+
await refreshTimelineDirectLan({ timeoutMs: TIMELINE_LIVE_REFRESH_TIMEOUT_MS });
|
|
1424
|
+
} catch {
|
|
1425
|
+
await refreshTimeline({
|
|
1426
|
+
timeoutMs: TIMELINE_LIVE_REFRESH_TIMEOUT_MS,
|
|
1427
|
+
probeLanWhileSticky: true,
|
|
1428
|
+
stickyLanProbeTimeoutMs: TIMELINE_STICKY_LAN_PROBE_TIMEOUT_MS,
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
renderAfterBackgroundDataRefresh();
|
|
1432
|
+
} while (timelineLiveRefreshPending);
|
|
1433
|
+
} finally {
|
|
1434
|
+
timelineLiveRefreshInFlight = false;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function setTimelinePayload(payload, options = {}) {
|
|
1439
|
+
const requestId = ++timelineHydrationSequence;
|
|
1440
|
+
const normalizedPayload = payload && typeof payload === "object" ? payload : null;
|
|
1441
|
+
state.timeline = normalizedPayload;
|
|
1245
1442
|
syncTimelineThreadFilter();
|
|
1246
1443
|
syncTimelineKindFilter();
|
|
1444
|
+
syncTimelineLiveStream();
|
|
1445
|
+
|
|
1446
|
+
if (options.hydrateImages !== true || !timelinePayloadHasImages(normalizedPayload)) {
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
hydrateTimelinePayloadImages(normalizedPayload)
|
|
1451
|
+
.then((hydrated) => {
|
|
1452
|
+
if (requestId !== timelineHydrationSequence || !hydrated) {
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
state.timeline = hydrated;
|
|
1456
|
+
syncTimelineThreadFilter();
|
|
1457
|
+
syncTimelineKindFilter();
|
|
1458
|
+
if (options.renderOnHydrate === false || !state.session?.authenticated) {
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
renderAfterBackgroundDataRefresh();
|
|
1462
|
+
})
|
|
1463
|
+
.catch(() => {});
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
function latestTimelineEntryForClientEvent() {
|
|
1467
|
+
const entries = Array.isArray(state.timeline?.entries) ? state.timeline.entries : [];
|
|
1468
|
+
return entries.length > 0 ? entries[0] : null;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function newlyRenderedTimelineTokensForClientEvent(visible) {
|
|
1472
|
+
if (!visible) {
|
|
1473
|
+
return [];
|
|
1474
|
+
}
|
|
1475
|
+
const entries = Array.isArray(state.timeline?.entries) ? state.timeline.entries : [];
|
|
1476
|
+
const rendered = [];
|
|
1477
|
+
for (const entry of entries.slice(0, 20)) {
|
|
1478
|
+
const token = normalizeClientText(entry?.token || entry?.createdAtMs || "");
|
|
1479
|
+
const kind = normalizeClientText(entry?.kind || "");
|
|
1480
|
+
if (!token || !kind || reportedTimelineRenderTokens.has(token)) {
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
reportedTimelineRenderTokens.add(token);
|
|
1484
|
+
rendered.push({
|
|
1485
|
+
token,
|
|
1486
|
+
kind,
|
|
1487
|
+
createdAtMs: Number(entry?.createdAtMs) || 0,
|
|
1488
|
+
});
|
|
1489
|
+
if (rendered.length >= 5) {
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (reportedTimelineRenderTokens.size > 500) {
|
|
1494
|
+
const keep = new Set(entries.slice(0, 250).map((entry) => normalizeClientText(entry?.token || entry?.createdAtMs || "")).filter(Boolean));
|
|
1495
|
+
for (const token of reportedTimelineRenderTokens) {
|
|
1496
|
+
if (!keep.has(token)) {
|
|
1497
|
+
reportedTimelineRenderTokens.delete(token);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return rendered;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
function reportTimelineRendered(reason = "render") {
|
|
1505
|
+
if (!state.session?.authenticated) {
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
const latest = latestTimelineEntryForClientEvent();
|
|
1509
|
+
if (!latest) {
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
const latestToken = normalizeClientText(latest.token || latest.createdAtMs || "");
|
|
1513
|
+
const latestKind = normalizeClientText(latest.kind || "");
|
|
1514
|
+
if (!latestToken || !latestKind) {
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
const desktop = isDesktopLayout();
|
|
1518
|
+
const visible = state.currentTab === "timeline" && (desktop || !state.detailOpen);
|
|
1519
|
+
const telemetry = getRoutingTelemetry() || {};
|
|
1520
|
+
const route = normalizeClientText(telemetry.lastRoute || "unknown");
|
|
1521
|
+
const renderedTokens = newlyRenderedTimelineTokensForClientEvent(visible);
|
|
1522
|
+
const reportKey = [
|
|
1523
|
+
latestKind,
|
|
1524
|
+
latestToken,
|
|
1525
|
+
state.currentTab,
|
|
1526
|
+
visible ? "visible" : "hidden",
|
|
1527
|
+
route,
|
|
1528
|
+
renderedTokens.map((item) => item.token).join(","),
|
|
1529
|
+
].join(":");
|
|
1530
|
+
if (lastTimelineRenderReportKey === reportKey) {
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
lastTimelineRenderReportKey = reportKey;
|
|
1534
|
+
|
|
1535
|
+
const send = () => {
|
|
1536
|
+
const latestNow = latestTimelineEntryForClientEvent();
|
|
1537
|
+
if (!latestNow) {
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
const telemetryNow = getRoutingTelemetry() || telemetry;
|
|
1541
|
+
apiPost(
|
|
1542
|
+
"/api/client-events",
|
|
1543
|
+
{
|
|
1544
|
+
type: "timeline-render",
|
|
1545
|
+
reason,
|
|
1546
|
+
latestToken: normalizeClientText(latestNow.token || latestNow.createdAtMs || ""),
|
|
1547
|
+
latestKind: normalizeClientText(latestNow.kind || ""),
|
|
1548
|
+
latestCreatedAtMs: Number(latestNow.createdAtMs) || 0,
|
|
1549
|
+
renderedTokens,
|
|
1550
|
+
entryCount: Array.isArray(state.timeline?.entries) ? state.timeline.entries.length : 0,
|
|
1551
|
+
currentTab: state.currentTab,
|
|
1552
|
+
visible,
|
|
1553
|
+
route: normalizeClientText(telemetryNow.lastRoute || route || "unknown"),
|
|
1554
|
+
clientAtMs: Date.now(),
|
|
1555
|
+
appBuildId: APP_BUILD_ID,
|
|
1556
|
+
},
|
|
1557
|
+
{
|
|
1558
|
+
timeoutMs: CLIENT_EVENT_REPORT_TIMEOUT_MS,
|
|
1559
|
+
probeLanWhileSticky: false,
|
|
1560
|
+
suppressRoutingStatus: true,
|
|
1561
|
+
},
|
|
1562
|
+
).catch(() => {});
|
|
1563
|
+
};
|
|
1564
|
+
|
|
1565
|
+
if (typeof requestAnimationFrame === "function") {
|
|
1566
|
+
requestAnimationFrame(send);
|
|
1567
|
+
} else {
|
|
1568
|
+
queueMicrotask(send);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
function timelinePayloadHasImages(payload) {
|
|
1573
|
+
const entries = Array.isArray(payload?.entries) ? payload.entries : [];
|
|
1574
|
+
return entries.some((entry) => Array.isArray(entry?.imageUrls) && entry.imageUrls.length > 0);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function renderAfterBackgroundDataRefresh() {
|
|
1578
|
+
ensureCurrentSelection();
|
|
1579
|
+
if (shouldDeferRenderForActiveInteraction()) {
|
|
1580
|
+
if (renderDeferredInteractionShellUpdates()) {
|
|
1581
|
+
reportTimelineRendered("deferred-refresh");
|
|
1582
|
+
}
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
renderCurrentSurface();
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
async function refreshPrimaryTabData(tab = state.currentTab) {
|
|
1589
|
+
if (tab === "inbox") {
|
|
1590
|
+
await refreshInbox();
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
if (tab === "timeline") {
|
|
1594
|
+
await refreshTimeline();
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
if (tab === "diff") {
|
|
1598
|
+
await refreshInboxDiff();
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function refreshPrimaryTabAfterNavigation(tab = state.currentTab) {
|
|
1603
|
+
if (!state.session?.authenticated || tab === "settings") {
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
refreshPrimaryTabData(tab)
|
|
1607
|
+
.then(() => {
|
|
1608
|
+
if (state.currentTab !== tab || state.currentTab === "settings") {
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
renderAfterBackgroundDataRefresh();
|
|
1612
|
+
})
|
|
1613
|
+
.catch(() => {});
|
|
1247
1614
|
}
|
|
1248
1615
|
|
|
1249
|
-
async function refreshDevices() {
|
|
1616
|
+
async function refreshDevices(opts = {}) {
|
|
1250
1617
|
if (!state.session?.authenticated) {
|
|
1251
1618
|
state.devices = [];
|
|
1252
1619
|
state.deviceError = "";
|
|
@@ -1254,7 +1621,7 @@ async function refreshDevices() {
|
|
|
1254
1621
|
}
|
|
1255
1622
|
|
|
1256
1623
|
try {
|
|
1257
|
-
const payload = await apiGet("/api/devices");
|
|
1624
|
+
const payload = await apiGet("/api/devices", opts);
|
|
1258
1625
|
state.devices = Array.isArray(payload?.devices) ? payload.devices : [];
|
|
1259
1626
|
state.deviceError = "";
|
|
1260
1627
|
} catch (error) {
|
|
@@ -1425,6 +1792,7 @@ function normalizeProviderClient(value) {
|
|
|
1425
1792
|
if (normalized === "moltbook") return "moltbook";
|
|
1426
1793
|
if (normalized === "a2a") return "a2a";
|
|
1427
1794
|
if (normalized === "viveworker") return "viveworker";
|
|
1795
|
+
if (normalized === "mcp") return "mcp";
|
|
1428
1796
|
return "codex";
|
|
1429
1797
|
}
|
|
1430
1798
|
|
|
@@ -1434,6 +1802,7 @@ function providerDisplayName(provider) {
|
|
|
1434
1802
|
if (p === "moltbook") return "Moltbook";
|
|
1435
1803
|
if (p === "a2a") return "A2A";
|
|
1436
1804
|
if (p === "viveworker") return L("common.appName");
|
|
1805
|
+
if (p === "mcp") return "MCP";
|
|
1437
1806
|
return L("common.codex");
|
|
1438
1807
|
}
|
|
1439
1808
|
|
|
@@ -1530,6 +1899,7 @@ function timelineKindFilterOptions() {
|
|
|
1530
1899
|
{ id: "messages", label: L("timeline.kindFilter.messages"), icon: "timeline" },
|
|
1531
1900
|
{ id: "suggestions", label: L("timeline.kindFilter.suggestions"), icon: "suggestions" },
|
|
1532
1901
|
{ id: "files", label: L("timeline.kindFilter.files"), icon: "file-event" },
|
|
1902
|
+
{ id: "commands", label: L("timeline.kindFilter.commands"), icon: "command" },
|
|
1533
1903
|
{ id: "approvals", label: L("timeline.kindFilter.approvals"), icon: "approval" },
|
|
1534
1904
|
{ id: "plans", label: L("timeline.kindFilter.plans"), icon: "plan" },
|
|
1535
1905
|
{ id: "choices", label: L("timeline.kindFilter.choices"), icon: "choice" },
|
|
@@ -1567,6 +1937,8 @@ function timelineEntryMatchesKindFilter(entry, filterId) {
|
|
|
1567
1937
|
return kind === "ambient_suggestions";
|
|
1568
1938
|
case "files":
|
|
1569
1939
|
return kind === "file_event";
|
|
1940
|
+
case "commands":
|
|
1941
|
+
return kind === "command_event";
|
|
1570
1942
|
case "approvals":
|
|
1571
1943
|
return kind === "approval";
|
|
1572
1944
|
case "plans":
|
|
@@ -1845,6 +2217,7 @@ async function logout({ revokeCurrentDeviceTrust = false } = {}) {
|
|
|
1845
2217
|
}
|
|
1846
2218
|
|
|
1847
2219
|
function resetAuthenticatedState() {
|
|
2220
|
+
closeTimelineLiveStream();
|
|
1848
2221
|
state.session = null;
|
|
1849
2222
|
state.inbox = null;
|
|
1850
2223
|
// Reset the diff-loaded flag so the next sign-in shows the skeleton
|
|
@@ -1894,6 +2267,57 @@ async function revokeTrustedDevice(deviceId) {
|
|
|
1894
2267
|
await renderShell();
|
|
1895
2268
|
}
|
|
1896
2269
|
|
|
2270
|
+
/**
|
|
2271
|
+
* `renderShell()` rebuilds the entire `#app` subtree by reassigning
|
|
2272
|
+
* `innerHTML`, which destroys every <pre> element in the DOM — including
|
|
2273
|
+
* any horizontal scroll position the user dragged into a wide code block.
|
|
2274
|
+
* On a polling interval, that means a long line of code keeps snapping
|
|
2275
|
+
* back to the start while the reader is mid-line.
|
|
2276
|
+
*
|
|
2277
|
+
* `snapshotScrollableContentScrolls()` records each scrollable code/diff
|
|
2278
|
+
* block's scrollLeft
|
|
2279
|
+
* keyed by its trimmed textContent (so the key survives a fresh render
|
|
2280
|
+
* regardless of position in the DOM tree). `restoreScrollableContentScrolls()`
|
|
2281
|
+
* walks the new DOM and restores any scrollLeft we still have a key for.
|
|
2282
|
+
*
|
|
2283
|
+
* Content-keyed matching is intentional: if the underlying code text
|
|
2284
|
+
* changes mid-scroll, the new <pre> is logically different and we let it
|
|
2285
|
+
* start at scrollLeft=0 rather than landing the reader somewhere unrelated.
|
|
2286
|
+
*/
|
|
2287
|
+
// Blocks the user can scroll horizontally — we preserve their position
|
|
2288
|
+
// across innerHTML rebuilds and defer background renders while the user is
|
|
2289
|
+
// actively scrolling/selecting them. <pre> for fenced code; <table> for
|
|
2290
|
+
// pipe-style markdown tables; `.detail-diff-viewer` for file diffs.
|
|
2291
|
+
const SCROLLABLE_CONTENT_SELECTORS = ".markdown pre, .markdown table, .detail-diff-viewer";
|
|
2292
|
+
|
|
2293
|
+
function snapshotScrollableContentScrolls() {
|
|
2294
|
+
if (typeof document === "undefined") return null;
|
|
2295
|
+
const blocks = document.querySelectorAll(SCROLLABLE_CONTENT_SELECTORS);
|
|
2296
|
+
if (blocks.length === 0) return null;
|
|
2297
|
+
const map = new Map();
|
|
2298
|
+
for (const el of blocks) {
|
|
2299
|
+
const key = el.textContent ? el.textContent.trim() : "";
|
|
2300
|
+
if (!key) continue;
|
|
2301
|
+
if (el.scrollLeft === 0 && el.scrollTop === 0) continue;
|
|
2302
|
+
map.set(`${el.tagName}:${key}`, {
|
|
2303
|
+
scrollLeft: el.scrollLeft,
|
|
2304
|
+
scrollTop: el.scrollTop,
|
|
2305
|
+
});
|
|
2306
|
+
}
|
|
2307
|
+
return map.size > 0 ? map : null;
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
function restoreScrollableContentScrolls(snapshot) {
|
|
2311
|
+
if (!snapshot || typeof document === "undefined") return;
|
|
2312
|
+
for (const el of document.querySelectorAll(SCROLLABLE_CONTENT_SELECTORS)) {
|
|
2313
|
+
const key = el.textContent ? el.textContent.trim() : "";
|
|
2314
|
+
const saved = key ? snapshot.get(`${el.tagName}:${key}`) : null;
|
|
2315
|
+
if (!saved) continue;
|
|
2316
|
+
if (saved.scrollLeft) el.scrollLeft = saved.scrollLeft;
|
|
2317
|
+
if (saved.scrollTop) el.scrollTop = saved.scrollTop;
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
|
|
1897
2321
|
async function renderShell() {
|
|
1898
2322
|
syncVisualViewportMetrics();
|
|
1899
2323
|
const desktop = isDesktopLayout();
|
|
@@ -1914,6 +2338,8 @@ async function renderShell() {
|
|
|
1914
2338
|
.filter(Boolean)
|
|
1915
2339
|
.join(" ");
|
|
1916
2340
|
|
|
2341
|
+
const scrollableContentSnapshot = snapshotScrollableContentScrolls();
|
|
2342
|
+
|
|
1917
2343
|
app.innerHTML = `
|
|
1918
2344
|
<div class="${shellClassName}">
|
|
1919
2345
|
${desktop ? renderDesktopHeader(detail) : renderMobileTopBar(detail)}
|
|
@@ -1939,7 +2365,12 @@ async function renderShell() {
|
|
|
1939
2365
|
applyPendingListScrollRestore();
|
|
1940
2366
|
applyPendingSettingsSubpageScrollReset();
|
|
1941
2367
|
applyPendingSettingsScrollRestore();
|
|
2368
|
+
// Reapply any horizontal scroll the user dragged into a code block before
|
|
2369
|
+
// this re-render. Done after the imperative scroll resets above so they
|
|
2370
|
+
// can't fight each other.
|
|
2371
|
+
restoreScrollableContentScrolls(scrollableContentSnapshot);
|
|
1942
2372
|
requestAnimationFrame(dismissBootSplash);
|
|
2373
|
+
reportTimelineRendered("shell");
|
|
1943
2374
|
}
|
|
1944
2375
|
|
|
1945
2376
|
function applyPendingDetailScrollReset() {
|
|
@@ -1988,6 +2419,53 @@ function applyPendingSettingsScrollRestore() {
|
|
|
1988
2419
|
});
|
|
1989
2420
|
}
|
|
1990
2421
|
|
|
2422
|
+
function renderDeferredInteractionShellUpdates() {
|
|
2423
|
+
if (typeof document === "undefined") {
|
|
2424
|
+
return false;
|
|
2425
|
+
}
|
|
2426
|
+
if (state.currentTab === "settings") {
|
|
2427
|
+
return false;
|
|
2428
|
+
}
|
|
2429
|
+
const desktop = isDesktopLayout();
|
|
2430
|
+
// On mobile detail screens the list is intentionally not mounted, so the
|
|
2431
|
+
// latest state will appear as soon as the user backs out. On desktop the
|
|
2432
|
+
// list and detail are side-by-side, so updating only the list keeps new
|
|
2433
|
+
// timeline/inbox cards flowing without disturbing the detail pane.
|
|
2434
|
+
if (!desktop && state.detailOpen) {
|
|
2435
|
+
return false;
|
|
2436
|
+
}
|
|
2437
|
+
if (isListSurfaceInteractionActive()) {
|
|
2438
|
+
return false;
|
|
2439
|
+
}
|
|
2440
|
+
const listSurface = document.querySelector("[data-list-surface]");
|
|
2441
|
+
if (!listSurface) {
|
|
2442
|
+
return false;
|
|
2443
|
+
}
|
|
2444
|
+
const scrollLeft = listSurface.scrollLeft;
|
|
2445
|
+
const scrollTop = listSurface.scrollTop;
|
|
2446
|
+
listSurface.innerHTML = renderListPanel({
|
|
2447
|
+
tab: state.currentTab,
|
|
2448
|
+
entries: listEntriesForTab(state.currentTab),
|
|
2449
|
+
desktop,
|
|
2450
|
+
});
|
|
2451
|
+
listSurface.scrollLeft = scrollLeft;
|
|
2452
|
+
listSurface.scrollTop = scrollTop;
|
|
2453
|
+
bindPartialListSurfaceInteractions(listSurface);
|
|
2454
|
+
reportTimelineRendered("deferred-list");
|
|
2455
|
+
return true;
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
function isListSurfaceInteractionActive() {
|
|
2459
|
+
if (state.threadFilterInteractionUntilMs > Date.now() || state.timelineKindFilterOpen) {
|
|
2460
|
+
return true;
|
|
2461
|
+
}
|
|
2462
|
+
if (typeof document === "undefined" || typeof Element === "undefined") {
|
|
2463
|
+
return false;
|
|
2464
|
+
}
|
|
2465
|
+
const activeElement = document.activeElement;
|
|
2466
|
+
return activeElement instanceof Element && Boolean(activeElement.closest("[data-list-surface]"));
|
|
2467
|
+
}
|
|
2468
|
+
|
|
1991
2469
|
function currentViewportScrollY() {
|
|
1992
2470
|
return window.scrollY || window.pageYOffset || document.documentElement?.scrollTop || 0;
|
|
1993
2471
|
}
|
|
@@ -2000,11 +2478,40 @@ function clearThreadFilterInteraction() {
|
|
|
2000
2478
|
state.threadFilterInteractionUntilMs = 0;
|
|
2001
2479
|
}
|
|
2002
2480
|
|
|
2481
|
+
function markScrollableContentInteraction() {
|
|
2482
|
+
state.scrollableContentInteractionUntilMs = Date.now() + SCROLLABLE_CONTENT_INTERACTION_DEFER_MS;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
function selectionIntersectsScrollableContent() {
|
|
2486
|
+
if (typeof document === "undefined" || typeof Element === "undefined") {
|
|
2487
|
+
return false;
|
|
2488
|
+
}
|
|
2489
|
+
const selection = document.getSelection?.();
|
|
2490
|
+
if (!selection || selection.isCollapsed || selection.rangeCount === 0) {
|
|
2491
|
+
return false;
|
|
2492
|
+
}
|
|
2493
|
+
for (let index = 0; index < selection.rangeCount; index += 1) {
|
|
2494
|
+
const range = selection.getRangeAt(index);
|
|
2495
|
+
const node = range.commonAncestorContainer;
|
|
2496
|
+
const element = node instanceof Element ? node : node?.parentElement;
|
|
2497
|
+
if (element?.closest?.(SCROLLABLE_CONTENT_SELECTORS)) {
|
|
2498
|
+
return true;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
return false;
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2003
2504
|
function shouldDeferRenderForActiveInteraction() {
|
|
2004
2505
|
const activeElement = document.activeElement;
|
|
2005
2506
|
if (state.completionReplySheetToken) {
|
|
2006
2507
|
return true;
|
|
2007
2508
|
}
|
|
2509
|
+
if (state.scrollableContentInteractionUntilMs > Date.now()) {
|
|
2510
|
+
return true;
|
|
2511
|
+
}
|
|
2512
|
+
if (selectionIntersectsScrollableContent()) {
|
|
2513
|
+
return true;
|
|
2514
|
+
}
|
|
2008
2515
|
if (
|
|
2009
2516
|
activeElement instanceof HTMLTextAreaElement &&
|
|
2010
2517
|
activeElement.matches("[data-completion-reply-textarea]") &&
|
|
@@ -2220,6 +2727,25 @@ function normalizeCompletionReplyWarning(value) {
|
|
|
2220
2727
|
};
|
|
2221
2728
|
}
|
|
2222
2729
|
|
|
2730
|
+
function normalizeCompletionReplyCompareText(value) {
|
|
2731
|
+
return normalizeClientText(value)
|
|
2732
|
+
.replace(/\s+/gu, " ")
|
|
2733
|
+
.trim();
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
function completionReplyWarningMatchesSentText(error, text, attachmentCount = 0) {
|
|
2737
|
+
if (error?.errorKey !== "completion-reply-thread-advanced") {
|
|
2738
|
+
return false;
|
|
2739
|
+
}
|
|
2740
|
+
if (attachmentCount > 0) {
|
|
2741
|
+
return false;
|
|
2742
|
+
}
|
|
2743
|
+
const warning = normalizeCompletionReplyWarning(error?.payload?.warning);
|
|
2744
|
+
const warningText = normalizeCompletionReplyCompareText(warning?.summary || "");
|
|
2745
|
+
const sentText = normalizeCompletionReplyCompareText(text);
|
|
2746
|
+
return Boolean(warningText && sentText && warningText === sentText);
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2223
2749
|
function normalizeCompletionReplyAttachments(values) {
|
|
2224
2750
|
const rawValues = Array.isArray(values)
|
|
2225
2751
|
? values
|
|
@@ -2425,9 +2951,15 @@ async function fetchCurrentDetailForItem(itemRef = state.currentItem) {
|
|
|
2425
2951
|
if (hasDetailOverride(itemRef)) {
|
|
2426
2952
|
return state.detailOverride.detail;
|
|
2427
2953
|
}
|
|
2954
|
+
const detailUrl = `/api/items/${encodeURIComponent(itemRef.kind)}/${encodeURIComponent(itemRef.token)}`;
|
|
2428
2955
|
try {
|
|
2429
2956
|
const detail = await hydrateDetailImages(
|
|
2430
|
-
await apiGet(
|
|
2957
|
+
await apiGet(detailUrl, {
|
|
2958
|
+
timeoutMs: DETAIL_FETCH_TIMEOUT_MS,
|
|
2959
|
+
probeLanWhileSticky: true,
|
|
2960
|
+
stickyLanProbeTimeoutMs: DETAIL_STICKY_LAN_PROBE_TIMEOUT_MS,
|
|
2961
|
+
preferRelayError: true,
|
|
2962
|
+
})
|
|
2431
2963
|
);
|
|
2432
2964
|
if (hasLaunchItemIntent(itemRef)) {
|
|
2433
2965
|
state.launchItemIntent.status = "loaded";
|
|
@@ -2440,16 +2972,24 @@ async function fetchCurrentDetailForItem(itemRef = state.currentItem) {
|
|
|
2440
2972
|
renderPair();
|
|
2441
2973
|
return null;
|
|
2442
2974
|
}
|
|
2443
|
-
await
|
|
2975
|
+
await Promise.race([
|
|
2976
|
+
refreshInbox(),
|
|
2977
|
+
wait(DETAIL_REFRESH_FALLBACK_TIMEOUT_MS),
|
|
2978
|
+
]).catch(() => {});
|
|
2444
2979
|
try {
|
|
2445
2980
|
const detail = await hydrateDetailImages(
|
|
2446
|
-
await apiGet(
|
|
2981
|
+
await apiGet(detailUrl, {
|
|
2982
|
+
timeoutMs: DETAIL_FETCH_TIMEOUT_MS,
|
|
2983
|
+
probeLanWhileSticky: true,
|
|
2984
|
+
stickyLanProbeTimeoutMs: DETAIL_STICKY_LAN_PROBE_TIMEOUT_MS,
|
|
2985
|
+
preferRelayError: true,
|
|
2986
|
+
})
|
|
2447
2987
|
);
|
|
2448
2988
|
if (hasLaunchItemIntent(itemRef)) {
|
|
2449
2989
|
state.launchItemIntent.status = "loaded";
|
|
2450
2990
|
}
|
|
2451
2991
|
return detail;
|
|
2452
|
-
} catch {
|
|
2992
|
+
} catch (retryError) {
|
|
2453
2993
|
if (hasLaunchItemIntent(itemRef)) {
|
|
2454
2994
|
clearChoiceLocalDraftForItem(itemRef);
|
|
2455
2995
|
const fallbackDetail = buildLaunchItemFallbackDetail(itemRef);
|
|
@@ -2464,7 +3004,7 @@ async function fetchCurrentDetailForItem(itemRef = state.currentItem) {
|
|
|
2464
3004
|
if (!state.currentItem) {
|
|
2465
3005
|
return null;
|
|
2466
3006
|
}
|
|
2467
|
-
return
|
|
3007
|
+
return buildDetailLoadErrorDetail(itemRef, retryError || error);
|
|
2468
3008
|
}
|
|
2469
3009
|
}
|
|
2470
3010
|
}
|
|
@@ -2479,6 +3019,7 @@ function queueCurrentDetailLoad(itemRef = state.currentItem) {
|
|
|
2479
3019
|
|
|
2480
3020
|
const requestedItem = { ...itemRef };
|
|
2481
3021
|
const requestId = ++detailLoadSequence;
|
|
3022
|
+
const hadRenderableDetailAtStart = Boolean(renderableCurrentDetail(requestedItem));
|
|
2482
3023
|
state.currentDetailLoading = true;
|
|
2483
3024
|
state.detailLoadingItem = requestedItem;
|
|
2484
3025
|
|
|
@@ -2505,6 +3046,16 @@ function queueCurrentDetailLoad(itemRef = state.currentItem) {
|
|
|
2505
3046
|
}
|
|
2506
3047
|
state.currentDetailLoading = false;
|
|
2507
3048
|
state.detailLoadingItem = null;
|
|
3049
|
+
const completedInitialDetailLoad =
|
|
3050
|
+
!hadRenderableDetailAtStart &&
|
|
3051
|
+
Boolean(state.currentDetail) &&
|
|
3052
|
+
isSameItemRef(state.currentDetail, requestedItem) &&
|
|
3053
|
+
Boolean(state.currentItem) &&
|
|
3054
|
+
isSameItemRef(state.currentItem, requestedItem);
|
|
3055
|
+
if (shouldDeferRenderForActiveInteraction() && !completedInitialDetailLoad) {
|
|
3056
|
+
renderDeferredInteractionShellUpdates();
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
2508
3059
|
renderCurrentSurface();
|
|
2509
3060
|
});
|
|
2510
3061
|
}
|
|
@@ -2523,6 +3074,31 @@ function buildLaunchItemFallbackDetail(itemRef) {
|
|
|
2523
3074
|
};
|
|
2524
3075
|
}
|
|
2525
3076
|
|
|
3077
|
+
function buildDetailLoadErrorDetail(itemRef, error) {
|
|
3078
|
+
const snapshot = buildDetailLoadingSnapshot(itemRef) || {};
|
|
3079
|
+
const entry = selectedEntryForItem(itemRef);
|
|
3080
|
+
const item = entry?.item || {};
|
|
3081
|
+
const fallbackText = normalizeClientText(item.messageText || item.summary || item.title || "");
|
|
3082
|
+
const messageParts = [
|
|
3083
|
+
fallbackText ? `<p>${escapeHtml(fallbackText)}</p>` : "",
|
|
3084
|
+
].filter(Boolean);
|
|
3085
|
+
return {
|
|
3086
|
+
kind: itemRef.kind,
|
|
3087
|
+
token: itemRef.token,
|
|
3088
|
+
title: item.title || snapshot.title || kindMeta(itemRef.kind).label,
|
|
3089
|
+
threadId: item.threadId || "",
|
|
3090
|
+
threadLabel: item.threadLabel || snapshot.threadLabel || "",
|
|
3091
|
+
summary: item.summary || fallbackText || "",
|
|
3092
|
+
messageHtml: messageParts.join(""),
|
|
3093
|
+
provider: item.provider || normalizeProviderClient(item.provider) || "",
|
|
3094
|
+
createdAtMs: Number(item.createdAtMs || snapshot.createdAtMs) || Date.now(),
|
|
3095
|
+
readOnly: true,
|
|
3096
|
+
actions: [],
|
|
3097
|
+
loadError: true,
|
|
3098
|
+
loadErrorMessage: normalizeClientText(error?.message || String(error || "")),
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
|
|
2526
3102
|
function resolveLaunchFallbackMessage(kind, isHandled) {
|
|
2527
3103
|
if (kind === "approval") {
|
|
2528
3104
|
return isHandled ? L("error.approvalAlreadyHandled") : L("error.approvalNotFound");
|
|
@@ -2587,7 +3163,7 @@ function renderDesktopWorkspace(detail) {
|
|
|
2587
3163
|
(state.currentDetailLoading || !renderableCurrentDetail());
|
|
2588
3164
|
return `
|
|
2589
3165
|
<section class="desktop-workspace">
|
|
2590
|
-
<aside class="surface surface--list">
|
|
3166
|
+
<aside class="surface surface--list" data-list-surface>
|
|
2591
3167
|
${renderListPanel({
|
|
2592
3168
|
tab: state.currentTab,
|
|
2593
3169
|
entries,
|
|
@@ -2615,7 +3191,7 @@ function renderMobileWorkspace(detail) {
|
|
|
2615
3191
|
}
|
|
2616
3192
|
|
|
2617
3193
|
return `
|
|
2618
|
-
<section class="screen-block">
|
|
3194
|
+
<section class="screen-block" data-list-surface>
|
|
2619
3195
|
${renderListPanel({
|
|
2620
3196
|
tab: state.currentTab,
|
|
2621
3197
|
entries: listEntriesForTab(state.currentTab),
|
|
@@ -3196,15 +3772,17 @@ function renderTimelineEntry(entry, { desktop }) {
|
|
|
3196
3772
|
const isMoltbookOrA2A = item.kind === "moltbook_reply" || item.kind === "moltbook_draft" || item.kind === "a2a_task" || item.kind === "a2a_task_result" || item.kind === "thread_share";
|
|
3197
3773
|
const isMessageLike = TIMELINE_MESSAGE_KINDS.has(item.kind) || isMoltbookOrA2A;
|
|
3198
3774
|
const isFileEvent = item.kind === "file_event";
|
|
3775
|
+
const isCommandEvent = item.kind === "command_event";
|
|
3199
3776
|
const imageUrls = Array.isArray(item.imageUrls) ? item.imageUrls.filter(Boolean) : [];
|
|
3200
3777
|
const fileRefs = normalizeClientFileRefs(item.fileRefs);
|
|
3201
|
-
const primaryText = timelineEntryPrimaryText(item, entry.status, { isMessageLike, isFileEvent });
|
|
3202
|
-
const secondaryText = timelineEntrySecondaryText(item, entry.status, primaryText, { isMessageLike, isFileEvent });
|
|
3778
|
+
const primaryText = timelineEntryPrimaryText(item, entry.status, { isMessageLike, isFileEvent, isCommandEvent });
|
|
3779
|
+
const secondaryText = timelineEntrySecondaryText(item, entry.status, primaryText, { isMessageLike, isFileEvent, isCommandEvent });
|
|
3203
3780
|
const threadLabel = timelineEntryThreadLabel(item, isMessageLike);
|
|
3204
3781
|
const timestampLabel = formatTimelineTimestamp(item.createdAtMs);
|
|
3205
3782
|
const statusLabel = timelineEntryStatusLabel(item, isMessageLike);
|
|
3206
3783
|
const fileEventFileSummary = isFileEvent ? timelineFileEventFileSummary(item) : "";
|
|
3207
3784
|
const fileEventDiffStatsHtml = isFileEvent ? renderDiffEntryStatsHtml(item) : "";
|
|
3785
|
+
const toolEventCommand = timelineToolEventCommand(item);
|
|
3208
3786
|
|
|
3209
3787
|
return `
|
|
3210
3788
|
<button
|
|
@@ -3227,6 +3805,7 @@ function renderTimelineEntry(entry, { desktop }) {
|
|
|
3227
3805
|
${threadLabel ? `<p class="timeline-entry__thread">${escapeHtml(threadLabel)}</p>` : ""}
|
|
3228
3806
|
<div class="timeline-entry__body">
|
|
3229
3807
|
<p class="timeline-entry__title">${escapeHtml(primaryText)}</p>
|
|
3808
|
+
${toolEventCommand ? `<pre class="timeline-entry__command"><code>${escapeHtml(toolEventCommand)}</code></pre>` : ""}
|
|
3230
3809
|
${secondaryText ? `<p class="timeline-entry__summary">${escapeHtml(secondaryText)}</p>` : ""}
|
|
3231
3810
|
${
|
|
3232
3811
|
isFileEvent && fileEventFileSummary
|
|
@@ -3276,7 +3855,7 @@ function renderDiffEntry(entry) {
|
|
|
3276
3855
|
}
|
|
3277
3856
|
|
|
3278
3857
|
function timelineEntryStatusLabel(item, isMessageLike) {
|
|
3279
|
-
if (isMessageLike || item?.kind === "file_event") {
|
|
3858
|
+
if (isMessageLike || item?.kind === "file_event" || item?.kind === "command_event") {
|
|
3280
3859
|
return "";
|
|
3281
3860
|
}
|
|
3282
3861
|
|
|
@@ -3359,7 +3938,7 @@ function timelineEntryThreadLabel(item, isMessage) {
|
|
|
3359
3938
|
return threadLabel || "";
|
|
3360
3939
|
}
|
|
3361
3940
|
|
|
3362
|
-
function timelineEntryPrimaryText(item, status, { isMessageLike = false, isFileEvent = false } = {}) {
|
|
3941
|
+
function timelineEntryPrimaryText(item, status, { isMessageLike = false, isFileEvent = false, isCommandEvent = false } = {}) {
|
|
3363
3942
|
if (item?.kind === "ambient_suggestions") {
|
|
3364
3943
|
return item.summary || fallbackSummaryForKind(item.kind, status, item.provider);
|
|
3365
3944
|
}
|
|
@@ -3372,10 +3951,14 @@ function timelineEntryPrimaryText(item, status, { isMessageLike = false, isFileE
|
|
|
3372
3951
|
return fileEventTimelineCountLabel(item) || fallbackSummaryForKind(item.kind, status, item.provider);
|
|
3373
3952
|
}
|
|
3374
3953
|
|
|
3954
|
+
if (isCommandEvent) {
|
|
3955
|
+
return L("common.commandEvent");
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3375
3958
|
return timelineDisplayTitleWithoutThread(item, { allowFallbackSummary: true }) || L("common.untitledItem");
|
|
3376
3959
|
}
|
|
3377
3960
|
|
|
3378
|
-
function timelineEntrySecondaryText(item, status, primaryText, { isMessageLike = false, isFileEvent = false } = {}) {
|
|
3961
|
+
function timelineEntrySecondaryText(item, status, primaryText, { isMessageLike = false, isFileEvent = false, isCommandEvent = false } = {}) {
|
|
3379
3962
|
if (item?.kind === "ambient_suggestions") {
|
|
3380
3963
|
return "";
|
|
3381
3964
|
}
|
|
@@ -3393,6 +3976,10 @@ function timelineEntrySecondaryText(item, status, primaryText, { isMessageLike =
|
|
|
3393
3976
|
return "";
|
|
3394
3977
|
}
|
|
3395
3978
|
|
|
3979
|
+
if (isCommandEvent) {
|
|
3980
|
+
return "";
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3396
3983
|
const compactTitle = timelineDisplayTitleWithoutThread(item, { allowFallbackSummary: false });
|
|
3397
3984
|
return compactTitle ? summaryText : "";
|
|
3398
3985
|
}
|
|
@@ -3442,6 +4029,7 @@ function timelineGeneratedTitlePrefixes() {
|
|
|
3442
4029
|
kindMeta("assistant_commentary").label,
|
|
3443
4030
|
kindMeta("assistant_final").label,
|
|
3444
4031
|
L("common.fileEvent"),
|
|
4032
|
+
L("common.commandEvent"),
|
|
3445
4033
|
"Approval",
|
|
3446
4034
|
"Plan",
|
|
3447
4035
|
"Choice",
|
|
@@ -3450,6 +4038,7 @@ function timelineGeneratedTitlePrefixes() {
|
|
|
3450
4038
|
"Commentary",
|
|
3451
4039
|
"Final answer",
|
|
3452
4040
|
"Files",
|
|
4041
|
+
"Command",
|
|
3453
4042
|
"承認",
|
|
3454
4043
|
"プラン",
|
|
3455
4044
|
"選択",
|
|
@@ -3458,13 +4047,39 @@ function timelineGeneratedTitlePrefixes() {
|
|
|
3458
4047
|
"途中経過",
|
|
3459
4048
|
"最終回答",
|
|
3460
4049
|
"ファイル",
|
|
4050
|
+
"コマンド",
|
|
3461
4051
|
];
|
|
3462
4052
|
}
|
|
3463
4053
|
|
|
4054
|
+
function timelineCommandEventCommand(item) {
|
|
4055
|
+
return truncateUiText(firstMarkdownCodeFence(item?.messageText || "") || item?.summary || item?.title || "", 220);
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
function shouldRenderFileEventCommand(item) {
|
|
4059
|
+
if (item?.kind !== "file_event") {
|
|
4060
|
+
return false;
|
|
4061
|
+
}
|
|
4062
|
+
return ["read", "search"].includes(normalizeClientText(item?.fileEventType || ""));
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
function timelineToolEventCommand(item) {
|
|
4066
|
+
if (item?.kind === "command_event") {
|
|
4067
|
+
return timelineCommandEventCommand(item);
|
|
4068
|
+
}
|
|
4069
|
+
if (!shouldRenderFileEventCommand(item)) {
|
|
4070
|
+
return "";
|
|
4071
|
+
}
|
|
4072
|
+
return truncateUiText(firstMarkdownCodeFence(item?.messageText || "") || item?.commandText || "", 220);
|
|
4073
|
+
}
|
|
4074
|
+
|
|
3464
4075
|
function fileEventDisplayLabel(fileEventType) {
|
|
3465
4076
|
switch (normalizeClientText(fileEventType || "")) {
|
|
3466
4077
|
case "read":
|
|
3467
4078
|
return L("fileEvent.read");
|
|
4079
|
+
case "search":
|
|
4080
|
+
return L("fileEvent.search");
|
|
4081
|
+
case "command":
|
|
4082
|
+
return L("fileEvent.command");
|
|
3468
4083
|
case "write":
|
|
3469
4084
|
return L("fileEvent.write");
|
|
3470
4085
|
case "create":
|
|
@@ -3487,6 +4102,10 @@ function fileEventTimelineCountLabel(item) {
|
|
|
3487
4102
|
switch (fileEventType) {
|
|
3488
4103
|
case "read":
|
|
3489
4104
|
return L("fileEvent.timeline.read", { count });
|
|
4105
|
+
case "search":
|
|
4106
|
+
return L("fileEvent.timeline.search", { count });
|
|
4107
|
+
case "command":
|
|
4108
|
+
return L("fileEvent.timeline.command", { count });
|
|
3490
4109
|
case "write":
|
|
3491
4110
|
return L("fileEvent.timeline.write", { count });
|
|
3492
4111
|
case "create":
|
|
@@ -3535,7 +4154,7 @@ function diffThreadFilesSummary(item) {
|
|
|
3535
4154
|
.map((entry) => fileChangeEntryLabel(entry))
|
|
3536
4155
|
.filter(Boolean);
|
|
3537
4156
|
if (labels.length === 0) {
|
|
3538
|
-
return "";
|
|
4157
|
+
return truncateUiText(firstMarkdownCodeFence(item?.messageText || "") || item?.summary || "");
|
|
3539
4158
|
}
|
|
3540
4159
|
const visibleLabels = labels.slice(0, 3);
|
|
3541
4160
|
const hiddenCount = labels.length - visibleLabels.length;
|
|
@@ -5173,6 +5792,15 @@ function renderSettingsAutoPilotSuggestion({ lane, count }) {
|
|
|
5173
5792
|
`;
|
|
5174
5793
|
}
|
|
5175
5794
|
|
|
5795
|
+
function renderSettingsExternalLink({ href, label }) {
|
|
5796
|
+
return `
|
|
5797
|
+
<a class="settings-external-link" href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer" style="display:inline-flex;align-items:center;justify-content:flex-end;gap:.32rem;text-decoration:none;color:#8fd7ff;max-width:100%;">
|
|
5798
|
+
<span class="settings-external-link__label">${escapeHtml(label)}</span>
|
|
5799
|
+
<span class="settings-external-link__icon" aria-hidden="true" style="display:inline-flex;width:.86rem;height:.86rem;flex:0 0 auto;">${renderIcon("external-link")}</span>
|
|
5800
|
+
</a>
|
|
5801
|
+
`.trim();
|
|
5802
|
+
}
|
|
5803
|
+
|
|
5176
5804
|
function renderSettingsMoltbookPage(context) {
|
|
5177
5805
|
const scout = context.moltbookScout;
|
|
5178
5806
|
if (!scout?.enabled) {
|
|
@@ -5187,10 +5815,16 @@ function renderSettingsMoltbookPage(context) {
|
|
|
5187
5815
|
renderSettingsInfoRow(L("settings.row.moltbookBatchTopScore"), String(scout.batch.topScore)),
|
|
5188
5816
|
renderSettingsInfoRow(L("settings.row.moltbookBatchRemaining"), `${Math.floor(scout.batch.remainingSeconds / 60)}:${String(scout.batch.remainingSeconds % 60).padStart(2, "0")}`),
|
|
5189
5817
|
] : [];
|
|
5190
|
-
const
|
|
5818
|
+
const accountProfileLink = scout.account?.name && scout.account?.profileUrl
|
|
5819
|
+
? renderSettingsExternalLink({
|
|
5820
|
+
href: scout.account.profileUrl,
|
|
5821
|
+
label: scout.account.name,
|
|
5822
|
+
})
|
|
5823
|
+
: "";
|
|
5824
|
+
const accountRow = accountProfileLink
|
|
5191
5825
|
? renderSettingsInfoRow(
|
|
5192
5826
|
L("settings.row.moltbookAccount"),
|
|
5193
|
-
|
|
5827
|
+
accountProfileLink,
|
|
5194
5828
|
{ rawValue: true }
|
|
5195
5829
|
)
|
|
5196
5830
|
: null;
|
|
@@ -5261,7 +5895,10 @@ function renderSettingsA2aRelayPage(context) {
|
|
|
5261
5895
|
? L("settings.a2aRelay.status.polling")
|
|
5262
5896
|
: L("settings.a2aRelay.status.disconnected");
|
|
5263
5897
|
const profileUrl = `${relay.relayUrl}/u/${relay.userId}`;
|
|
5264
|
-
const userIdLink =
|
|
5898
|
+
const userIdLink = renderSettingsExternalLink({
|
|
5899
|
+
href: profileUrl,
|
|
5900
|
+
label: relay.userId,
|
|
5901
|
+
});
|
|
5265
5902
|
const relayHost = (() => { try { return new URL(relay.relayUrl).host; } catch { return relay.relayUrl; } })();
|
|
5266
5903
|
const publicChecked = relay.acceptPublicTasks === true;
|
|
5267
5904
|
|
|
@@ -6286,6 +6923,7 @@ function renderStandardDetailDesktop(detail) {
|
|
|
6286
6923
|
<div class="detail-shell">
|
|
6287
6924
|
${renderDetailMetaRow(detail, kindInfo)}
|
|
6288
6925
|
<h2 class="detail-title detail-title--desktop">${renderDetailTitle(detail)}</h2>
|
|
6926
|
+
${renderDetailLoadErrorNotice(detail)}
|
|
6289
6927
|
${detail.readOnly || detail.kind === "approval" || detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "thread_share" ? "" : renderDetailLead(detail, kindInfo)}
|
|
6290
6928
|
${renderPreviousContextCard(detail)}
|
|
6291
6929
|
${renderAutoPilotManualReview(detail)}
|
|
@@ -6306,6 +6944,7 @@ function renderStandardDetailDesktop(detail) {
|
|
|
6306
6944
|
</section>
|
|
6307
6945
|
`
|
|
6308
6946
|
}
|
|
6947
|
+
${renderCommandEventDetail(detail)}
|
|
6309
6948
|
${renderClaudePlanSection(detail)}
|
|
6310
6949
|
${renderClaudeQuestionSection(detail)}
|
|
6311
6950
|
${renderDetailImageGallery(detail)}
|
|
@@ -6328,6 +6967,7 @@ function renderStandardDetailMobile(detail) {
|
|
|
6328
6967
|
<div class="detail-shell detail-shell--mobile">
|
|
6329
6968
|
<div class="mobile-detail-scroll mobile-detail-scroll--detail">
|
|
6330
6969
|
${renderDetailMetaRow(detail, kindInfo, { mobile: true })}
|
|
6970
|
+
${renderDetailLoadErrorNotice(detail, { mobile: true })}
|
|
6331
6971
|
${renderPreviousContextCard(detail, { mobile: true })}
|
|
6332
6972
|
${renderAutoPilotManualReview(detail, { mobile: true })}
|
|
6333
6973
|
${renderInterruptedDetailNotice(detail, { mobile: true })}
|
|
@@ -6348,6 +6988,7 @@ function renderStandardDetailMobile(detail) {
|
|
|
6348
6988
|
</section>
|
|
6349
6989
|
`
|
|
6350
6990
|
}
|
|
6991
|
+
${renderCommandEventDetail(detail, { mobile: true })}
|
|
6351
6992
|
${renderClaudePlanSection(detail, { mobile: true })}
|
|
6352
6993
|
${renderClaudeQuestionSection(detail, { mobile: true })}
|
|
6353
6994
|
${renderDetailImageGallery(detail, { mobile: true })}
|
|
@@ -6390,6 +7031,46 @@ function renderAmbientSuggestionsSection(detail, options = {}) {
|
|
|
6390
7031
|
`;
|
|
6391
7032
|
}
|
|
6392
7033
|
|
|
7034
|
+
function renderCommandEventDetail(detail, options = {}) {
|
|
7035
|
+
if (detail?.kind !== "command_event" && !shouldRenderFileEventCommand(detail)) {
|
|
7036
|
+
return "";
|
|
7037
|
+
}
|
|
7038
|
+
const commandText = normalizeClientText(detail?.commandText || "") || firstMarkdownCodeFence(detail?.messageText || "");
|
|
7039
|
+
if (!commandText) {
|
|
7040
|
+
return "";
|
|
7041
|
+
}
|
|
7042
|
+
return `
|
|
7043
|
+
<section class="detail-card detail-card--command ${options.mobile ? "detail-card--mobile" : ""}">
|
|
7044
|
+
<div class="detail-files-card__header">
|
|
7045
|
+
<span class="detail-files-card__icon" aria-hidden="true">${renderIcon("command")}</span>
|
|
7046
|
+
<span>${escapeHtml(L("common.commandEvent"))}</span>
|
|
7047
|
+
</div>
|
|
7048
|
+
<pre class="detail-command-block"><code>${escapeHtml(commandText)}</code></pre>
|
|
7049
|
+
</section>
|
|
7050
|
+
`;
|
|
7051
|
+
}
|
|
7052
|
+
|
|
7053
|
+
function renderDetailLoadErrorNotice(detail, options = {}) {
|
|
7054
|
+
if (detail?.loadError !== true) {
|
|
7055
|
+
return "";
|
|
7056
|
+
}
|
|
7057
|
+
const errorText = normalizeClientText(detail.loadErrorMessage || "");
|
|
7058
|
+
return `
|
|
7059
|
+
<section class="detail-card detail-card--body ${options.mobile ? "detail-card--mobile" : ""}">
|
|
7060
|
+
<div class="detail-body markdown">
|
|
7061
|
+
<p><strong>${escapeHtml(L("detail.loadFailedTitle"))}</strong></p>
|
|
7062
|
+
<p>${escapeHtml(L("detail.loadFailedCopy"))}</p>
|
|
7063
|
+
${errorText ? `<p class="muted">${escapeHtml(errorText)}</p>` : ""}
|
|
7064
|
+
</div>
|
|
7065
|
+
<div class="actions actions--stack">
|
|
7066
|
+
<button class="secondary secondary--wide" type="button" data-detail-retry>
|
|
7067
|
+
${escapeHtml(L("detail.loadRetry"))}
|
|
7068
|
+
</button>
|
|
7069
|
+
</div>
|
|
7070
|
+
</section>
|
|
7071
|
+
`;
|
|
7072
|
+
}
|
|
7073
|
+
|
|
6393
7074
|
function renderAmbientSuggestionCard(detail, suggestion, index) {
|
|
6394
7075
|
const copyKey = ambientSuggestionCopyKey(detail?.token || "", suggestion?.id || "", index);
|
|
6395
7076
|
const copyStatus = state.ambientSuggestionCopyState?.key === copyKey
|
|
@@ -6422,7 +7103,7 @@ function renderAmbientSuggestionCard(detail, suggestion, index) {
|
|
|
6422
7103
|
}
|
|
6423
7104
|
|
|
6424
7105
|
function renderDetailPlainIntro(detail, options = {}) {
|
|
6425
|
-
if (!["approval", "diff_thread", "file_event"].includes(detail?.kind || "")) {
|
|
7106
|
+
if (!["approval", "diff_thread", "file_event", "command_event"].includes(detail?.kind || "")) {
|
|
6426
7107
|
return "";
|
|
6427
7108
|
}
|
|
6428
7109
|
const ak = normalizeClientText(detail?.approvalKind || "");
|
|
@@ -6994,6 +7675,29 @@ function renderA2ATaskDetail(detail, options = {}) {
|
|
|
6994
7675
|
const statusBadge = !enabled && detail.kind === "a2a_task_result"
|
|
6995
7676
|
? `<span class="eyebrow-pill eyebrow-pill--subtle">${escapeHtml(L(statusKey))}</span>`
|
|
6996
7677
|
: "";
|
|
7678
|
+
const viveworkerTask = detail.viveworker || {};
|
|
7679
|
+
const payment = viveworkerTask.payment || {};
|
|
7680
|
+
const priceLabel = payment.price
|
|
7681
|
+
? `${payment.price} USDC`
|
|
7682
|
+
: detail.paidDeliverable?.price
|
|
7683
|
+
? `${detail.paidDeliverable.price} USDC`
|
|
7684
|
+
: "";
|
|
7685
|
+
const paidDeliverableBlock = viveworkerTask.paidDeliverable || detail.paidDeliverable
|
|
7686
|
+
? `
|
|
7687
|
+
<div class="reply-composer__context">
|
|
7688
|
+
<span class="eyebrow-pill eyebrow-pill--quiet">${escapeHtml(L("a2a.task.paidDeliverable"))}</span>
|
|
7689
|
+
<div class="reply-composer__context-body">
|
|
7690
|
+
${viveworkerTask.requestedTier ? `<p><strong>${escapeHtml(L("a2a.task.requestedTier"))}</strong>: ${escapeHtml(viveworkerTask.requestedTier)}</p>` : ""}
|
|
7691
|
+
${viveworkerTask.requestedExecutor ? `<p><strong>${escapeHtml(L("a2a.task.requestedExecutor"))}</strong>: ${escapeHtml(viveworkerTask.requestedExecutor)}</p>` : ""}
|
|
7692
|
+
${viveworkerTask.requestedModel ? `<p><strong>${escapeHtml(L("a2a.task.requestedModel"))}</strong>: ${escapeHtml(viveworkerTask.requestedModel)}</p>` : ""}
|
|
7693
|
+
${viveworkerTask.deliverableType ? `<p><strong>${escapeHtml(L("a2a.task.deliverableType"))}</strong>: ${escapeHtml(viveworkerTask.deliverableType)}</p>` : ""}
|
|
7694
|
+
${priceLabel ? `<p><strong>${escapeHtml(L("a2a.task.price"))}</strong>: ${escapeHtml(priceLabel)}</p>` : ""}
|
|
7695
|
+
${payment.payTo ? `<p><strong>${escapeHtml(L("a2a.task.payTo"))}</strong>: <code>${escapeHtml(payment.payTo)}</code></p>` : ""}
|
|
7696
|
+
${detail.paidDeliverable?.url ? `<p><strong>${escapeHtml(L("a2a.task.unlockUrl"))}</strong>: <a href="${escapeHtml(detail.paidDeliverable.url)}" target="_blank" rel="noopener">${escapeHtml(detail.paidDeliverable.url)}</a></p>` : ""}
|
|
7697
|
+
</div>
|
|
7698
|
+
</div>
|
|
7699
|
+
`
|
|
7700
|
+
: "";
|
|
6997
7701
|
|
|
6998
7702
|
// Show executor selector when "ask" mode is active and both CLIs are available.
|
|
6999
7703
|
const executors = state.session?.a2aExecutors || { codex: false, claude: false };
|
|
@@ -7035,6 +7739,7 @@ function renderA2ATaskDetail(detail, options = {}) {
|
|
|
7035
7739
|
<span class="eyebrow-pill eyebrow-pill--quiet">${escapeHtml(L("a2a.task.eyebrow"))}</span>
|
|
7036
7740
|
${statusBadge}
|
|
7037
7741
|
${callerLine}
|
|
7742
|
+
${paidDeliverableBlock}
|
|
7038
7743
|
<p class="muted reply-composer__description">${enabled ? escapeHtml(L("a2a.task.editHint")) : ""}</p>
|
|
7039
7744
|
</div>
|
|
7040
7745
|
<div class="reply-composer__instruction">
|
|
@@ -7769,6 +8474,8 @@ function renderTabButtons({ buttonClass, withIcons }) {
|
|
|
7769
8474
|
}
|
|
7770
8475
|
|
|
7771
8476
|
function bindShellInteractions() {
|
|
8477
|
+
bindScrollableContentRenderDeferral();
|
|
8478
|
+
|
|
7772
8479
|
for (const button of document.querySelectorAll("[data-tab]")) {
|
|
7773
8480
|
button.addEventListener("click", async () => {
|
|
7774
8481
|
await switchTab(button.dataset.tab);
|
|
@@ -8014,14 +8721,31 @@ function bindShellInteractions() {
|
|
|
8014
8721
|
});
|
|
8015
8722
|
}
|
|
8016
8723
|
|
|
8724
|
+
for (const button of document.querySelectorAll("[data-detail-retry]")) {
|
|
8725
|
+
button.addEventListener("click", async (event) => {
|
|
8726
|
+
event.preventDefault();
|
|
8727
|
+
event.stopPropagation();
|
|
8728
|
+
if (!state.currentItem) {
|
|
8729
|
+
return;
|
|
8730
|
+
}
|
|
8731
|
+
state.currentDetail = null;
|
|
8732
|
+
state.currentDetailLoading = false;
|
|
8733
|
+
state.detailLoadingItem = null;
|
|
8734
|
+
queueCurrentDetailLoad(state.currentItem);
|
|
8735
|
+
await renderShell();
|
|
8736
|
+
});
|
|
8737
|
+
}
|
|
8738
|
+
|
|
8017
8739
|
for (const button of document.querySelectorAll("[data-back-to-list]")) {
|
|
8018
8740
|
button.addEventListener("click", async () => {
|
|
8741
|
+
const nextTab = state.currentTab;
|
|
8019
8742
|
clearChoiceLocalDraftForItem(state.currentItem);
|
|
8020
8743
|
state.detailOpen = false;
|
|
8021
8744
|
state.pendingListScrollRestore = !isDesktopLayout() && Boolean(state.listScrollState);
|
|
8022
8745
|
clearPinnedDetailState();
|
|
8023
8746
|
syncCurrentItemUrl(null);
|
|
8024
8747
|
await renderShell();
|
|
8748
|
+
refreshPrimaryTabAfterNavigation(nextTab);
|
|
8025
8749
|
});
|
|
8026
8750
|
}
|
|
8027
8751
|
|
|
@@ -8069,7 +8793,8 @@ function bindShellInteractions() {
|
|
|
8069
8793
|
delete postBody.hazbaseReauth;
|
|
8070
8794
|
}
|
|
8071
8795
|
await apiPost(actionUrl, postBody);
|
|
8072
|
-
if (
|
|
8796
|
+
if (activeItem?.kind === "approval") {
|
|
8797
|
+
state.pendingActionUrls.delete(actionUrl);
|
|
8073
8798
|
pinActionOutcomeDetail(
|
|
8074
8799
|
activeItem,
|
|
8075
8800
|
buildActionOutcomeDetail({
|
|
@@ -8078,6 +8803,13 @@ function bindShellInteractions() {
|
|
|
8078
8803
|
message: approvalOutcomeMessage(actionUrl, activeItem?.provider),
|
|
8079
8804
|
})
|
|
8080
8805
|
);
|
|
8806
|
+
await renderShell();
|
|
8807
|
+
void refreshAuthenticatedState()
|
|
8808
|
+
.then(() => renderShell())
|
|
8809
|
+
.catch((error) => {
|
|
8810
|
+
console.debug?.("[approval-action-refresh-failed]", error?.message || String(error));
|
|
8811
|
+
});
|
|
8812
|
+
return;
|
|
8081
8813
|
}
|
|
8082
8814
|
await refreshAuthenticatedState();
|
|
8083
8815
|
if (!keepDetailOpen && !isDesktopLayout()) {
|
|
@@ -8092,6 +8824,14 @@ function bindShellInteractions() {
|
|
|
8092
8824
|
if (error?.errorKey === "hazbase-session-expired") {
|
|
8093
8825
|
await fetchHazbaseStatus();
|
|
8094
8826
|
}
|
|
8827
|
+
if (activeItem?.kind === "approval" && isAlreadyHandledApprovalError(error)) {
|
|
8828
|
+
const recovered = await recoverHandledApprovalDetail(activeItem);
|
|
8829
|
+
if (recovered) {
|
|
8830
|
+
pinActionOutcomeDetail(activeItem, recovered);
|
|
8831
|
+
await renderShell();
|
|
8832
|
+
return;
|
|
8833
|
+
}
|
|
8834
|
+
}
|
|
8095
8835
|
if (approvalFinalized) {
|
|
8096
8836
|
await refreshAuthenticatedState();
|
|
8097
8837
|
if (keepDetailOpen && activeItem?.kind === "approval") {
|
|
@@ -9190,8 +9930,43 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
9190
9930
|
for (const attachment of attachments) {
|
|
9191
9931
|
requestBody.append("image", attachment.file, attachment.name || attachment.file.name);
|
|
9192
9932
|
}
|
|
9193
|
-
const
|
|
9194
|
-
|
|
9933
|
+
const sentNotice = L(draft.mode === "plan" ? "reply.notice.sentPlan" : "reply.notice.sentDefault", { provider: providerDisplayName(replyProvider) });
|
|
9934
|
+
const renderOptimisticSent = () => {
|
|
9935
|
+
const currentDraft = getCompletionReplyDraft(token);
|
|
9936
|
+
if (!currentDraft.sending || currentDraft.text !== text) {
|
|
9937
|
+
return;
|
|
9938
|
+
}
|
|
9939
|
+
setCompletionReplyDraft(token, {
|
|
9940
|
+
text: "",
|
|
9941
|
+
sentText: text,
|
|
9942
|
+
attachments: currentDraft.attachments,
|
|
9943
|
+
mode: draft.mode,
|
|
9944
|
+
sending: false,
|
|
9945
|
+
error: "",
|
|
9946
|
+
notice: sentNotice,
|
|
9947
|
+
warning: null,
|
|
9948
|
+
confirmOverride: false,
|
|
9949
|
+
collapsedAfterSend: true,
|
|
9950
|
+
});
|
|
9951
|
+
renderShell().catch((renderError) => {
|
|
9952
|
+
console.warn("[completion-reply-optimistic-render]", renderError?.message || renderError);
|
|
9953
|
+
});
|
|
9954
|
+
};
|
|
9955
|
+
const optimisticSentTimer = setTimeout(renderOptimisticSent, COMPLETION_REPLY_OPTIMISTIC_SENT_MS);
|
|
9956
|
+
let replyResult = null;
|
|
9957
|
+
try {
|
|
9958
|
+
const replyKind = replyForm.dataset.replyKind || "completion";
|
|
9959
|
+
replyResult = await apiPost(
|
|
9960
|
+
`/api/items/${encodeURIComponent(replyKind)}/${encodeURIComponent(token)}/reply`,
|
|
9961
|
+
requestBody,
|
|
9962
|
+
{
|
|
9963
|
+
timeoutMs: COMPLETION_REPLY_SEND_TIMEOUT_MS,
|
|
9964
|
+
preferRelayError: true,
|
|
9965
|
+
},
|
|
9966
|
+
);
|
|
9967
|
+
} finally {
|
|
9968
|
+
clearTimeout(optimisticSentTimer);
|
|
9969
|
+
}
|
|
9195
9970
|
setCompletionReplyDraft(token, {
|
|
9196
9971
|
text: "",
|
|
9197
9972
|
sentText: text,
|
|
@@ -9199,14 +9974,51 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
9199
9974
|
mode: draft.mode,
|
|
9200
9975
|
sending: false,
|
|
9201
9976
|
error: "",
|
|
9202
|
-
notice:
|
|
9977
|
+
notice: sentNotice,
|
|
9203
9978
|
warning: null,
|
|
9204
9979
|
confirmOverride: false,
|
|
9205
9980
|
collapsedAfterSend: true,
|
|
9206
9981
|
});
|
|
9207
|
-
|
|
9982
|
+
if (replyResult?.ackTimeout === true) {
|
|
9983
|
+
console.info("[completion-reply] accepted after slow Codex ACK");
|
|
9984
|
+
}
|
|
9985
|
+
await renderShell();
|
|
9986
|
+
refreshAuthenticatedState()
|
|
9987
|
+
.then(renderShell)
|
|
9988
|
+
.catch((refreshError) => console.warn("[completion-reply-refresh]", refreshError?.message || refreshError));
|
|
9989
|
+
return;
|
|
9208
9990
|
} catch (error) {
|
|
9991
|
+
const optimisticDraft = getCompletionReplyDraft(token);
|
|
9992
|
+
if (
|
|
9993
|
+
error.errorKey === "request-timeout" &&
|
|
9994
|
+
optimisticDraft.collapsedAfterSend &&
|
|
9995
|
+
optimisticDraft.sentText === text
|
|
9996
|
+
) {
|
|
9997
|
+
refreshAuthenticatedState()
|
|
9998
|
+
.then(renderShell)
|
|
9999
|
+
.catch((refreshError) => console.warn("[completion-reply-refresh]", refreshError?.message || refreshError));
|
|
10000
|
+
return;
|
|
10001
|
+
}
|
|
9209
10002
|
if (error.errorKey === "completion-reply-thread-advanced") {
|
|
10003
|
+
if (completionReplyWarningMatchesSentText(error, text, attachments.length)) {
|
|
10004
|
+
setCompletionReplyDraft(token, {
|
|
10005
|
+
text: "",
|
|
10006
|
+
sentText: text,
|
|
10007
|
+
attachments: [],
|
|
10008
|
+
mode: draft.mode,
|
|
10009
|
+
sending: false,
|
|
10010
|
+
error: "",
|
|
10011
|
+
notice: L(draft.mode === "plan" ? "reply.notice.sentPlan" : "reply.notice.sentDefault", { provider: providerDisplayName(replyProvider) }),
|
|
10012
|
+
warning: null,
|
|
10013
|
+
confirmOverride: false,
|
|
10014
|
+
collapsedAfterSend: true,
|
|
10015
|
+
});
|
|
10016
|
+
await renderShell();
|
|
10017
|
+
refreshAuthenticatedState()
|
|
10018
|
+
.then(renderShell)
|
|
10019
|
+
.catch((refreshError) => console.warn("[completion-reply-refresh]", refreshError?.message || refreshError));
|
|
10020
|
+
return;
|
|
10021
|
+
}
|
|
9210
10022
|
setCompletionReplyDraft(token, {
|
|
9211
10023
|
text,
|
|
9212
10024
|
sentText: "",
|
|
@@ -9410,6 +10222,143 @@ function bindSharedUi(renderFn) {
|
|
|
9410
10222
|
}
|
|
9411
10223
|
}
|
|
9412
10224
|
|
|
10225
|
+
function bindScrollableContentRenderDeferral() {
|
|
10226
|
+
const mark = () => {
|
|
10227
|
+
markScrollableContentInteraction();
|
|
10228
|
+
};
|
|
10229
|
+
for (const el of document.querySelectorAll(SCROLLABLE_CONTENT_SELECTORS)) {
|
|
10230
|
+
el.addEventListener("scroll", mark, { passive: true });
|
|
10231
|
+
el.addEventListener("wheel", mark, { passive: true });
|
|
10232
|
+
el.addEventListener("touchstart", mark, { passive: true });
|
|
10233
|
+
el.addEventListener("touchmove", mark, { passive: true });
|
|
10234
|
+
el.addEventListener("pointerdown", mark);
|
|
10235
|
+
el.addEventListener("copy", mark);
|
|
10236
|
+
}
|
|
10237
|
+
}
|
|
10238
|
+
|
|
10239
|
+
function bindPartialListSurfaceInteractions(listSurface) {
|
|
10240
|
+
if (!listSurface || listSurface.dataset.partialListInteractionsBound === "true") {
|
|
10241
|
+
return;
|
|
10242
|
+
}
|
|
10243
|
+
listSurface.dataset.partialListInteractionsBound = "true";
|
|
10244
|
+
|
|
10245
|
+
const targetElement = (event) => {
|
|
10246
|
+
const target = event.target;
|
|
10247
|
+
return target instanceof Element ? target : target?.parentElement || null;
|
|
10248
|
+
};
|
|
10249
|
+
const threadSelectSelector = "[data-timeline-thread-select], [data-diff-thread-select], [data-completed-thread-select]";
|
|
10250
|
+
|
|
10251
|
+
listSurface.addEventListener("pointerdown", (event) => {
|
|
10252
|
+
if (targetElement(event)?.closest(threadSelectSelector)) {
|
|
10253
|
+
markThreadFilterInteraction();
|
|
10254
|
+
}
|
|
10255
|
+
});
|
|
10256
|
+
listSurface.addEventListener("focusin", (event) => {
|
|
10257
|
+
if (targetElement(event)?.closest(threadSelectSelector)) {
|
|
10258
|
+
markThreadFilterInteraction();
|
|
10259
|
+
}
|
|
10260
|
+
});
|
|
10261
|
+
listSurface.addEventListener("focusout", (event) => {
|
|
10262
|
+
if (targetElement(event)?.closest(threadSelectSelector)) {
|
|
10263
|
+
clearThreadFilterInteraction();
|
|
10264
|
+
}
|
|
10265
|
+
});
|
|
10266
|
+
|
|
10267
|
+
listSurface.addEventListener("change", async (event) => {
|
|
10268
|
+
const target = targetElement(event);
|
|
10269
|
+
const timelineSelect = target?.closest("[data-timeline-thread-select]");
|
|
10270
|
+
const diffSelect = target?.closest("[data-diff-thread-select]");
|
|
10271
|
+
const completedSelect = target?.closest("[data-completed-thread-select]");
|
|
10272
|
+
if (!timelineSelect && !diffSelect && !completedSelect) {
|
|
10273
|
+
return;
|
|
10274
|
+
}
|
|
10275
|
+
clearThreadFilterInteraction();
|
|
10276
|
+
if (timelineSelect) {
|
|
10277
|
+
state.timelineThreadFilter = timelineSelect.value || "all";
|
|
10278
|
+
state.timelineKindFilterOpen = false;
|
|
10279
|
+
} else if (diffSelect) {
|
|
10280
|
+
state.diffThreadFilter = diffSelect.value || "all";
|
|
10281
|
+
} else if (completedSelect) {
|
|
10282
|
+
state.completedThreadFilter = completedSelect.value || "all";
|
|
10283
|
+
}
|
|
10284
|
+
alignCurrentItemToVisibleEntries();
|
|
10285
|
+
await renderShell();
|
|
10286
|
+
});
|
|
10287
|
+
|
|
10288
|
+
listSurface.addEventListener("click", async (event) => {
|
|
10289
|
+
const target = targetElement(event);
|
|
10290
|
+
const threadSelect = target?.closest(threadSelectSelector);
|
|
10291
|
+
if (threadSelect) {
|
|
10292
|
+
markThreadFilterInteraction();
|
|
10293
|
+
return;
|
|
10294
|
+
}
|
|
10295
|
+
|
|
10296
|
+
const providerButton = target?.closest("[data-provider-filter]");
|
|
10297
|
+
if (providerButton) {
|
|
10298
|
+
event.preventDefault();
|
|
10299
|
+
const next = providerButton.dataset.providerFilter || "all";
|
|
10300
|
+
if (state.providerFilter === next) {
|
|
10301
|
+
return;
|
|
10302
|
+
}
|
|
10303
|
+
state.providerFilter = next;
|
|
10304
|
+
state.timelineThreadFilter = "all";
|
|
10305
|
+
state.timelineKindFilter = "all";
|
|
10306
|
+
state.timelineKindFilterOpen = false;
|
|
10307
|
+
state.completedThreadFilter = "all";
|
|
10308
|
+
state.diffThreadFilter = "all";
|
|
10309
|
+
alignCurrentItemToVisibleEntries();
|
|
10310
|
+
await renderShell();
|
|
10311
|
+
return;
|
|
10312
|
+
}
|
|
10313
|
+
|
|
10314
|
+
const kindToggle = target?.closest("[data-timeline-kind-filter-toggle]");
|
|
10315
|
+
if (kindToggle) {
|
|
10316
|
+
event.preventDefault();
|
|
10317
|
+
markThreadFilterInteraction();
|
|
10318
|
+
state.timelineKindFilterOpen = !state.timelineKindFilterOpen;
|
|
10319
|
+
await renderShell();
|
|
10320
|
+
return;
|
|
10321
|
+
}
|
|
10322
|
+
|
|
10323
|
+
const kindOption = target?.closest("[data-timeline-kind-filter-option]");
|
|
10324
|
+
if (kindOption) {
|
|
10325
|
+
event.preventDefault();
|
|
10326
|
+
clearThreadFilterInteraction();
|
|
10327
|
+
state.timelineKindFilter = kindOption.dataset.timelineKindFilterOption || "all";
|
|
10328
|
+
state.timelineKindFilterOpen = false;
|
|
10329
|
+
alignCurrentItemToVisibleEntries();
|
|
10330
|
+
await renderShell();
|
|
10331
|
+
return;
|
|
10332
|
+
}
|
|
10333
|
+
|
|
10334
|
+
const subtabButton = target?.closest("[data-inbox-subtab]");
|
|
10335
|
+
if (subtabButton) {
|
|
10336
|
+
const nextSubtab = subtabButton.dataset.inboxSubtab === "completed" ? "completed" : "pending";
|
|
10337
|
+
if (nextSubtab === state.inboxSubtab) {
|
|
10338
|
+
return;
|
|
10339
|
+
}
|
|
10340
|
+
state.inboxSubtab = nextSubtab;
|
|
10341
|
+
if (isDesktopLayout()) {
|
|
10342
|
+
alignCurrentItemToVisibleEntries();
|
|
10343
|
+
syncCurrentItemUrl(state.currentItem);
|
|
10344
|
+
}
|
|
10345
|
+
await renderShell();
|
|
10346
|
+
return;
|
|
10347
|
+
}
|
|
10348
|
+
|
|
10349
|
+
const itemButton = target?.closest("[data-open-item-kind][data-open-item-token]");
|
|
10350
|
+
if (itemButton) {
|
|
10351
|
+
openItem({
|
|
10352
|
+
kind: itemButton.dataset.openItemKind,
|
|
10353
|
+
token: itemButton.dataset.openItemToken,
|
|
10354
|
+
sourceTab: itemButton.dataset.sourceTab,
|
|
10355
|
+
sourceSubtab: itemButton.dataset.sourceSubtab,
|
|
10356
|
+
});
|
|
10357
|
+
await renderShell();
|
|
10358
|
+
}
|
|
10359
|
+
});
|
|
10360
|
+
}
|
|
10361
|
+
|
|
9413
10362
|
function openSettingsSubpage(page) {
|
|
9414
10363
|
if (!page) {
|
|
9415
10364
|
return;
|
|
@@ -9451,6 +10400,7 @@ async function switchTab(tab) {
|
|
|
9451
10400
|
syncCurrentItemUrl(state.currentItem);
|
|
9452
10401
|
}
|
|
9453
10402
|
await renderShell();
|
|
10403
|
+
refreshPrimaryTabAfterNavigation(tab);
|
|
9454
10404
|
if (tab === "settings") {
|
|
9455
10405
|
void fetchHazbaseStatus()
|
|
9456
10406
|
.then(() => {
|
|
@@ -9647,6 +10597,8 @@ function kindMeta(kind, item) {
|
|
|
9647
10597
|
return { label: L("common.diff"), tone: "neutral", icon: "diff" };
|
|
9648
10598
|
case "file_event":
|
|
9649
10599
|
return { label: L("common.fileEvent"), tone: "neutral", icon: "file-event" };
|
|
10600
|
+
case "command_event":
|
|
10601
|
+
return { label: L("common.commandEvent"), tone: "neutral", icon: "command" };
|
|
9650
10602
|
case "moltbook_reply":
|
|
9651
10603
|
return { label: L("common.moltbookReply"), tone: "neutral", icon: "moltbook-comment" };
|
|
9652
10604
|
case "moltbook_draft":
|
|
@@ -9679,6 +10631,9 @@ function itemIntentText(kind, status = "pending", provider) {
|
|
|
9679
10631
|
if (kind === "file_event") {
|
|
9680
10632
|
return L("intent.fileEvent");
|
|
9681
10633
|
}
|
|
10634
|
+
if (kind === "command_event") {
|
|
10635
|
+
return L("intent.commandEvent");
|
|
10636
|
+
}
|
|
9682
10637
|
if (kind === "ambient_suggestions") {
|
|
9683
10638
|
return L("intent.ambientSuggestions");
|
|
9684
10639
|
}
|
|
@@ -9716,6 +10671,9 @@ function detailIntentText(detail) {
|
|
|
9716
10671
|
if (detail.kind === "file_event") {
|
|
9717
10672
|
return itemIntentText(detail.kind, "timeline", provider);
|
|
9718
10673
|
}
|
|
10674
|
+
if (detail.kind === "command_event") {
|
|
10675
|
+
return itemIntentText(detail.kind, "timeline", provider);
|
|
10676
|
+
}
|
|
9719
10677
|
if (detail.kind === "ambient_suggestions") {
|
|
9720
10678
|
return itemIntentText(detail.kind, "timeline", provider);
|
|
9721
10679
|
}
|
|
@@ -9789,6 +10747,8 @@ function fallbackSummaryForKind(kind, status, provider) {
|
|
|
9789
10747
|
return L("summary.diffThread");
|
|
9790
10748
|
case "file_event":
|
|
9791
10749
|
return L("summary.fileEvent", vars);
|
|
10750
|
+
case "command_event":
|
|
10751
|
+
return L("summary.commandEvent", vars);
|
|
9792
10752
|
case "ambient_suggestions":
|
|
9793
10753
|
return L("summary.ambientSuggestions", { count: 0, firstTitle: "", more: 0 });
|
|
9794
10754
|
case "user_message":
|
|
@@ -9960,6 +10920,22 @@ function hasDetailOverride(itemRef = state.currentItem) {
|
|
|
9960
10920
|
return Boolean(state.detailOverride && isSameItemRef(state.detailOverride, itemRef));
|
|
9961
10921
|
}
|
|
9962
10922
|
|
|
10923
|
+
function isAlreadyHandledApprovalError(error) {
|
|
10924
|
+
return error?.errorKey === "approval-not-found" || error?.errorKey === "approval-already-handled";
|
|
10925
|
+
}
|
|
10926
|
+
|
|
10927
|
+
async function recoverHandledApprovalDetail(itemRef) {
|
|
10928
|
+
try {
|
|
10929
|
+
await refreshAuthenticatedState();
|
|
10930
|
+
const detail = await hydrateDetailImages(
|
|
10931
|
+
await apiGet(`/api/items/${encodeURIComponent(itemRef.kind)}/${encodeURIComponent(itemRef.token)}`)
|
|
10932
|
+
);
|
|
10933
|
+
return detail?.kind === "approval" && detail.readOnly === true ? detail : null;
|
|
10934
|
+
} catch {
|
|
10935
|
+
return null;
|
|
10936
|
+
}
|
|
10937
|
+
}
|
|
10938
|
+
|
|
9963
10939
|
function shouldPreserveCurrentItem(itemRef = state.currentItem) {
|
|
9964
10940
|
return Boolean(itemRef && (hasLaunchItemIntent(itemRef) || hasDetailOverride(itemRef)));
|
|
9965
10941
|
}
|
|
@@ -9995,6 +10971,8 @@ function renderIcon(name) {
|
|
|
9995
10971
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="4.5" width="14" height="15" rx="2.5"/><path d="M8.5 9h7"/><path d="M8.5 12h7"/><path d="M8.5 15h4.5"/></svg>`;
|
|
9996
10972
|
case "file-event":
|
|
9997
10973
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3.8h5.9l4.3 4.3v10a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2v-12.3a2 2 0 0 1 2-2Z"/><path d="M13.9 3.8v4.3h4.3"/><path d="M9.2 13.1h5.6"/><path d="M9.2 16.2h4"/></svg>`;
|
|
10974
|
+
case "command":
|
|
10975
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="4.2" y="5" width="15.6" height="14" rx="2.6"/><path d="m7.8 10 2.4 2-2.4 2"/><path d="M12.2 14h4"/></svg>`;
|
|
9998
10976
|
case "diff":
|
|
9999
10977
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M7.5 5.5v13"/><path d="M4.8 8.2 7.5 5.5 10.2 8.2"/><path d="M16.5 18.5v-13"/><path d="m13.8 15.8 2.7 2.7 2.7-2.7"/><path d="M11.8 7.5h1.2"/><path d="M11 12h2.8"/><path d="M11.8 16.5h1.2"/></svg>`;
|
|
10000
10978
|
case "pending":
|
|
@@ -10027,6 +11005,8 @@ function renderIcon(name) {
|
|
|
10027
11005
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="12" width="14" height="7" rx="2"/><path d="M9 19h6"/><path d="M12 12v7"/><path d="M8.2 8.8a5.4 5.4 0 0 1 7.6 0"/><path d="M10.2 6.3a8.2 8.2 0 0 1 3.6 0"/><path d="M11.2 9.8a1.2 1.2 0 0 1 1.6 0"/></svg>`;
|
|
10028
11006
|
case "link":
|
|
10029
11007
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M10.4 13.6 8.3 15.7a3 3 0 0 1-4.2-4.2l2.8-2.8a3 3 0 0 1 4.2 0"/><path d="m13.6 10.4 2.1-2.1a3 3 0 1 1 4.2 4.2l-2.8 2.8a3 3 0 0 1-4.2 0"/><path d="m9.5 14.5 5-5"/></svg>`;
|
|
11008
|
+
case "external-link":
|
|
11009
|
+
return `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M10 6H6.8A2.8 2.8 0 0 0 4 8.8v8.4A2.8 2.8 0 0 0 6.8 20h8.4a2.8 2.8 0 0 0 2.8-2.8V14"/><path d="M14 4h6v6"/><path d="m12.5 11.5 7-7"/></svg>`;
|
|
10030
11010
|
case "clip":
|
|
10031
11011
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="m9.5 12.5 5.9-5.9a3 3 0 1 1 4.2 4.2l-7.7 7.7a5 5 0 1 1-7.1-7.1l8.1-8.1"/></svg>`;
|
|
10032
11012
|
case "moltbook-draft":
|
|
@@ -10297,32 +11277,107 @@ function pruneTimelineImageObjectUrlCache() {
|
|
|
10297
11277
|
}
|
|
10298
11278
|
}
|
|
10299
11279
|
|
|
11280
|
+
function withRequestTimeout(init, opts = {}) {
|
|
11281
|
+
const timeoutMs = Number(opts.timeoutMs) || 0;
|
|
11282
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || typeof AbortController === "undefined") {
|
|
11283
|
+
return { init, cleanup: () => {} };
|
|
11284
|
+
}
|
|
11285
|
+
if (init.signal?.aborted) {
|
|
11286
|
+
return { init, cleanup: () => {} };
|
|
11287
|
+
}
|
|
11288
|
+
const controller = new AbortController();
|
|
11289
|
+
let timer = null;
|
|
11290
|
+
const externalAbortHandler = init.signal
|
|
11291
|
+
? () => controller.abort(init.signal.reason)
|
|
11292
|
+
: null;
|
|
11293
|
+
if (init.signal && externalAbortHandler) {
|
|
11294
|
+
init.signal.addEventListener("abort", externalAbortHandler, { once: true });
|
|
11295
|
+
}
|
|
11296
|
+
timer = setTimeout(() => {
|
|
11297
|
+
const error = new Error("request-timeout");
|
|
11298
|
+
error.name = "AbortError";
|
|
11299
|
+
controller.abort(error);
|
|
11300
|
+
}, timeoutMs);
|
|
11301
|
+
return {
|
|
11302
|
+
init: { ...init, signal: controller.signal },
|
|
11303
|
+
cleanup: () => {
|
|
11304
|
+
if (timer) clearTimeout(timer);
|
|
11305
|
+
if (init.signal && externalAbortHandler) {
|
|
11306
|
+
init.signal.removeEventListener("abort", externalAbortHandler);
|
|
11307
|
+
}
|
|
11308
|
+
},
|
|
11309
|
+
};
|
|
11310
|
+
}
|
|
11311
|
+
|
|
11312
|
+
function normalizeRequestError(error, opts = {}) {
|
|
11313
|
+
if (error?.name === "AbortError" && Number(opts.timeoutMs) > 0) {
|
|
11314
|
+
const timeoutError = new Error(L("error.requestTimedOut"));
|
|
11315
|
+
timeoutError.code = 0;
|
|
11316
|
+
timeoutError.status = 0;
|
|
11317
|
+
timeoutError.errorKey = "request-timeout";
|
|
11318
|
+
return timeoutError;
|
|
11319
|
+
}
|
|
11320
|
+
return error;
|
|
11321
|
+
}
|
|
11322
|
+
|
|
10300
11323
|
async function apiGet(url, opts = {}) {
|
|
10301
11324
|
// routedFetch tries LAN first, then falls back to the relay tunnel when
|
|
10302
11325
|
// the phone is off-LAN. Returns a fetch-Response-compatible object so the
|
|
10303
11326
|
// rest of this function is identical to a plain `fetch()` call.
|
|
10304
|
-
const
|
|
11327
|
+
const timed = withRequestTimeout({
|
|
10305
11328
|
credentials: "same-origin",
|
|
10306
11329
|
headers: {
|
|
10307
11330
|
Accept: "application/json",
|
|
10308
11331
|
},
|
|
10309
11332
|
}, opts);
|
|
10310
|
-
|
|
10311
|
-
const
|
|
10312
|
-
|
|
10313
|
-
|
|
10314
|
-
|
|
10315
|
-
|
|
10316
|
-
|
|
11333
|
+
try {
|
|
11334
|
+
const response = await routedFetch(url, timed.init, opts);
|
|
11335
|
+
if (!response.ok) {
|
|
11336
|
+
const errorInfo = await readError(response);
|
|
11337
|
+
const error = new Error(errorInfo.message);
|
|
11338
|
+
error.code = response.status;
|
|
11339
|
+
error.status = response.status;
|
|
11340
|
+
error.errorKey = errorInfo.errorKey || "";
|
|
11341
|
+
throw error;
|
|
11342
|
+
}
|
|
11343
|
+
return await response.json();
|
|
11344
|
+
} catch (error) {
|
|
11345
|
+
throw normalizeRequestError(error, opts);
|
|
11346
|
+
} finally {
|
|
11347
|
+
timed.cleanup();
|
|
10317
11348
|
}
|
|
10318
|
-
return response.json();
|
|
10319
11349
|
}
|
|
10320
11350
|
|
|
10321
|
-
async function
|
|
11351
|
+
async function apiGetDirectLan(url, opts = {}) {
|
|
11352
|
+
const timed = withRequestTimeout({
|
|
11353
|
+
credentials: "same-origin",
|
|
11354
|
+
headers: {
|
|
11355
|
+
Accept: "application/json",
|
|
11356
|
+
},
|
|
11357
|
+
}, opts);
|
|
11358
|
+
try {
|
|
11359
|
+
const response = await fetch(url, timed.init);
|
|
11360
|
+
if (!response.ok) {
|
|
11361
|
+
const errorInfo = await readError(response);
|
|
11362
|
+
const error = new Error(errorInfo.message);
|
|
11363
|
+
error.code = response.status;
|
|
11364
|
+
error.status = response.status;
|
|
11365
|
+
error.errorKey = errorInfo.errorKey || "";
|
|
11366
|
+
throw error;
|
|
11367
|
+
}
|
|
11368
|
+
return await response.json();
|
|
11369
|
+
} catch (error) {
|
|
11370
|
+
throw normalizeRequestError(error, opts);
|
|
11371
|
+
} finally {
|
|
11372
|
+
timed.cleanup();
|
|
11373
|
+
}
|
|
11374
|
+
}
|
|
11375
|
+
|
|
11376
|
+
async function apiPost(url, body, opts = {}) {
|
|
10322
11377
|
const isFormDataBody = typeof FormData !== "undefined" && body instanceof FormData;
|
|
10323
11378
|
// Keep native FormData for LAN; routedFetch serializes it to multipart
|
|
10324
11379
|
// bytes only when the request has to travel through the remote relay.
|
|
10325
|
-
const
|
|
11380
|
+
const timed = withRequestTimeout({
|
|
10326
11381
|
method: "POST",
|
|
10327
11382
|
credentials: "same-origin",
|
|
10328
11383
|
headers: isFormDataBody
|
|
@@ -10334,17 +11389,24 @@ async function apiPost(url, body) {
|
|
|
10334
11389
|
Accept: "application/json",
|
|
10335
11390
|
},
|
|
10336
11391
|
body: isFormDataBody ? body : JSON.stringify(body || {}),
|
|
10337
|
-
});
|
|
10338
|
-
|
|
10339
|
-
const
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
11392
|
+
}, opts);
|
|
11393
|
+
try {
|
|
11394
|
+
const response = await routedFetch(url, timed.init, opts);
|
|
11395
|
+
if (!response.ok) {
|
|
11396
|
+
const errorInfo = await readError(response);
|
|
11397
|
+
const error = new Error(errorInfo.message);
|
|
11398
|
+
error.code = response.status;
|
|
11399
|
+
error.status = response.status;
|
|
11400
|
+
error.errorKey = errorInfo.errorKey || "";
|
|
11401
|
+
error.payload = errorInfo.payload ?? null;
|
|
11402
|
+
throw error;
|
|
11403
|
+
}
|
|
11404
|
+
return await response.json();
|
|
11405
|
+
} catch (error) {
|
|
11406
|
+
throw normalizeRequestError(error, opts);
|
|
11407
|
+
} finally {
|
|
11408
|
+
timed.cleanup();
|
|
10346
11409
|
}
|
|
10347
|
-
return response.json();
|
|
10348
11410
|
}
|
|
10349
11411
|
|
|
10350
11412
|
async function readError(response) {
|
|
@@ -10372,6 +11434,7 @@ function localizeApiError(value) {
|
|
|
10372
11434
|
"device-not-found": "error.deviceNotFound",
|
|
10373
11435
|
"web-push-disabled": "error.webPushDisabled",
|
|
10374
11436
|
"push-subscription-expired": "error.pushSubscriptionExpired",
|
|
11437
|
+
"request-timeout": "error.requestTimedOut",
|
|
10375
11438
|
"item-not-found": "error.itemNotFound",
|
|
10376
11439
|
"completion-reply-unavailable": "error.completionReplyUnavailable",
|
|
10377
11440
|
"completion-reply-thread-advanced": "error.completionReplyThreadAdvanced",
|