viveworker 0.8.1 → 0.8.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 +64 -4
- package/package.json +1 -1
- package/plugins/viveworker-control-plane/.codex-plugin/plugin.json +1 -1
- package/plugins/viveworker-control-plane/DISTRIBUTION.md +96 -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/com.viveworker.moltbook-scout.plist.sample +2 -2
- package/scripts/moltbook-api.mjs +178 -2
- package/scripts/moltbook-cli.mjs +134 -8
- package/scripts/moltbook-scout-auto.sh +100 -10
- package/scripts/moltbook-watcher.mjs +10 -0
- package/scripts/viveworker-bridge.mjs +1909 -183
- package/web/app.css +147 -1
- package/web/app.js +1205 -131
- package/web/build-id.js +1 -1
- package/web/i18n.js +147 -17
- package/web/index.html +1 -1
- package/web/remote-pairing/api-router.js +250 -13
- package/web/remote-pairing/transport.js +67 -2
- package/web/sw.js +14 -4
package/web/app.js
CHANGED
|
@@ -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
|
|
|
@@ -828,7 +897,7 @@ async function forceAppRefreshFromLan() {
|
|
|
828
897
|
const keys = await caches.keys();
|
|
829
898
|
await Promise.all(
|
|
830
899
|
keys
|
|
831
|
-
.filter((key) => /^viveworker
|
|
900
|
+
.filter((key) => /^viveworker-/u.test(key))
|
|
832
901
|
.map((key) => caches.delete(key))
|
|
833
902
|
);
|
|
834
903
|
}
|
|
@@ -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();
|
|
1247
1586
|
}
|
|
1248
1587
|
|
|
1249
|
-
async function
|
|
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(() => {});
|
|
1614
|
+
}
|
|
1615
|
+
|
|
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) {
|
|
@@ -1532,6 +1899,7 @@ function timelineKindFilterOptions() {
|
|
|
1532
1899
|
{ id: "messages", label: L("timeline.kindFilter.messages"), icon: "timeline" },
|
|
1533
1900
|
{ id: "suggestions", label: L("timeline.kindFilter.suggestions"), icon: "suggestions" },
|
|
1534
1901
|
{ id: "files", label: L("timeline.kindFilter.files"), icon: "file-event" },
|
|
1902
|
+
{ id: "commands", label: L("timeline.kindFilter.commands"), icon: "command" },
|
|
1535
1903
|
{ id: "approvals", label: L("timeline.kindFilter.approvals"), icon: "approval" },
|
|
1536
1904
|
{ id: "plans", label: L("timeline.kindFilter.plans"), icon: "plan" },
|
|
1537
1905
|
{ id: "choices", label: L("timeline.kindFilter.choices"), icon: "choice" },
|
|
@@ -1562,15 +1930,26 @@ function currentTimelineKindFilterOption() {
|
|
|
1562
1930
|
|
|
1563
1931
|
function timelineEntryMatchesKindFilter(entry, filterId) {
|
|
1564
1932
|
const kind = normalizeClientText(entry?.kind || entry?.item?.kind || "");
|
|
1933
|
+
const fileEventType = normalizeClientText(entry?.fileEventType || entry?.item?.fileEventType || "");
|
|
1934
|
+
const activityPhase = normalizeClientText(entry?.activityPhase || entry?.item?.activityPhase || "");
|
|
1565
1935
|
switch (filterId) {
|
|
1566
1936
|
case "messages":
|
|
1567
|
-
return TIMELINE_MESSAGE_KINDS.has(kind);
|
|
1937
|
+
return TIMELINE_MESSAGE_KINDS.has(kind) || (kind === "activity_status" && activityPhase === "thinking");
|
|
1568
1938
|
case "suggestions":
|
|
1569
1939
|
return kind === "ambient_suggestions";
|
|
1570
1940
|
case "files":
|
|
1571
|
-
return
|
|
1941
|
+
return (
|
|
1942
|
+
(kind === "file_event" && fileEventType !== "git") ||
|
|
1943
|
+
(kind === "activity_status" && ["reading", "searching", "editing"].includes(activityPhase))
|
|
1944
|
+
);
|
|
1945
|
+
case "commands":
|
|
1946
|
+
return (
|
|
1947
|
+
kind === "command_event" ||
|
|
1948
|
+
(kind === "file_event" && fileEventType === "git") ||
|
|
1949
|
+
(kind === "activity_status" && activityPhase === "running_command")
|
|
1950
|
+
);
|
|
1572
1951
|
case "approvals":
|
|
1573
|
-
return kind === "approval";
|
|
1952
|
+
return kind === "approval" || (kind === "activity_status" && activityPhase === "awaiting_approval");
|
|
1574
1953
|
case "plans":
|
|
1575
1954
|
return kind === "plan" || kind === "plan_ready";
|
|
1576
1955
|
case "choices":
|
|
@@ -1847,6 +2226,7 @@ async function logout({ revokeCurrentDeviceTrust = false } = {}) {
|
|
|
1847
2226
|
}
|
|
1848
2227
|
|
|
1849
2228
|
function resetAuthenticatedState() {
|
|
2229
|
+
closeTimelineLiveStream();
|
|
1850
2230
|
state.session = null;
|
|
1851
2231
|
state.inbox = null;
|
|
1852
2232
|
// Reset the diff-loaded flag so the next sign-in shows the skeleton
|
|
@@ -1903,37 +2283,47 @@ async function revokeTrustedDevice(deviceId) {
|
|
|
1903
2283
|
* On a polling interval, that means a long line of code keeps snapping
|
|
1904
2284
|
* back to the start while the reader is mid-line.
|
|
1905
2285
|
*
|
|
1906
|
-
* `
|
|
2286
|
+
* `snapshotScrollableContentScrolls()` records each scrollable code/diff
|
|
2287
|
+
* block's scrollLeft
|
|
1907
2288
|
* keyed by its trimmed textContent (so the key survives a fresh render
|
|
1908
|
-
* regardless of position in the DOM tree). `
|
|
2289
|
+
* regardless of position in the DOM tree). `restoreScrollableContentScrolls()`
|
|
1909
2290
|
* walks the new DOM and restores any scrollLeft we still have a key for.
|
|
1910
2291
|
*
|
|
1911
2292
|
* Content-keyed matching is intentional: if the underlying code text
|
|
1912
2293
|
* changes mid-scroll, the new <pre> is logically different and we let it
|
|
1913
2294
|
* start at scrollLeft=0 rather than landing the reader somewhere unrelated.
|
|
1914
2295
|
*/
|
|
1915
|
-
|
|
2296
|
+
// Blocks the user can scroll horizontally — we preserve their position
|
|
2297
|
+
// across innerHTML rebuilds and defer background renders while the user is
|
|
2298
|
+
// actively scrolling/selecting them. <pre> for fenced code; <table> for
|
|
2299
|
+
// pipe-style markdown tables; `.detail-diff-viewer` for file diffs.
|
|
2300
|
+
const SCROLLABLE_CONTENT_SELECTORS = ".markdown pre, .markdown table, .detail-diff-viewer";
|
|
2301
|
+
|
|
2302
|
+
function snapshotScrollableContentScrolls() {
|
|
1916
2303
|
if (typeof document === "undefined") return null;
|
|
1917
|
-
const blocks = document.querySelectorAll(
|
|
2304
|
+
const blocks = document.querySelectorAll(SCROLLABLE_CONTENT_SELECTORS);
|
|
1918
2305
|
if (blocks.length === 0) return null;
|
|
1919
2306
|
const map = new Map();
|
|
1920
|
-
for (const
|
|
1921
|
-
const key =
|
|
2307
|
+
for (const el of blocks) {
|
|
2308
|
+
const key = el.textContent ? el.textContent.trim() : "";
|
|
1922
2309
|
if (!key) continue;
|
|
1923
|
-
if (
|
|
1924
|
-
map.set(key
|
|
2310
|
+
if (el.scrollLeft === 0 && el.scrollTop === 0) continue;
|
|
2311
|
+
map.set(`${el.tagName}:${key}`, {
|
|
2312
|
+
scrollLeft: el.scrollLeft,
|
|
2313
|
+
scrollTop: el.scrollTop,
|
|
2314
|
+
});
|
|
1925
2315
|
}
|
|
1926
2316
|
return map.size > 0 ? map : null;
|
|
1927
2317
|
}
|
|
1928
2318
|
|
|
1929
|
-
function
|
|
2319
|
+
function restoreScrollableContentScrolls(snapshot) {
|
|
1930
2320
|
if (!snapshot || typeof document === "undefined") return;
|
|
1931
|
-
for (const
|
|
1932
|
-
const key =
|
|
1933
|
-
const saved = key ? snapshot.get(key) : null;
|
|
2321
|
+
for (const el of document.querySelectorAll(SCROLLABLE_CONTENT_SELECTORS)) {
|
|
2322
|
+
const key = el.textContent ? el.textContent.trim() : "";
|
|
2323
|
+
const saved = key ? snapshot.get(`${el.tagName}:${key}`) : null;
|
|
1934
2324
|
if (!saved) continue;
|
|
1935
|
-
if (saved.scrollLeft)
|
|
1936
|
-
if (saved.scrollTop)
|
|
2325
|
+
if (saved.scrollLeft) el.scrollLeft = saved.scrollLeft;
|
|
2326
|
+
if (saved.scrollTop) el.scrollTop = saved.scrollTop;
|
|
1937
2327
|
}
|
|
1938
2328
|
}
|
|
1939
2329
|
|
|
@@ -1957,7 +2347,7 @@ async function renderShell() {
|
|
|
1957
2347
|
.filter(Boolean)
|
|
1958
2348
|
.join(" ");
|
|
1959
2349
|
|
|
1960
|
-
const
|
|
2350
|
+
const scrollableContentSnapshot = snapshotScrollableContentScrolls();
|
|
1961
2351
|
|
|
1962
2352
|
app.innerHTML = `
|
|
1963
2353
|
<div class="${shellClassName}">
|
|
@@ -1987,8 +2377,9 @@ async function renderShell() {
|
|
|
1987
2377
|
// Reapply any horizontal scroll the user dragged into a code block before
|
|
1988
2378
|
// this re-render. Done after the imperative scroll resets above so they
|
|
1989
2379
|
// can't fight each other.
|
|
1990
|
-
|
|
2380
|
+
restoreScrollableContentScrolls(scrollableContentSnapshot);
|
|
1991
2381
|
requestAnimationFrame(dismissBootSplash);
|
|
2382
|
+
reportTimelineRendered("shell");
|
|
1992
2383
|
}
|
|
1993
2384
|
|
|
1994
2385
|
function applyPendingDetailScrollReset() {
|
|
@@ -2037,6 +2428,53 @@ function applyPendingSettingsScrollRestore() {
|
|
|
2037
2428
|
});
|
|
2038
2429
|
}
|
|
2039
2430
|
|
|
2431
|
+
function renderDeferredInteractionShellUpdates() {
|
|
2432
|
+
if (typeof document === "undefined") {
|
|
2433
|
+
return false;
|
|
2434
|
+
}
|
|
2435
|
+
if (state.currentTab === "settings") {
|
|
2436
|
+
return false;
|
|
2437
|
+
}
|
|
2438
|
+
const desktop = isDesktopLayout();
|
|
2439
|
+
// On mobile detail screens the list is intentionally not mounted, so the
|
|
2440
|
+
// latest state will appear as soon as the user backs out. On desktop the
|
|
2441
|
+
// list and detail are side-by-side, so updating only the list keeps new
|
|
2442
|
+
// timeline/inbox cards flowing without disturbing the detail pane.
|
|
2443
|
+
if (!desktop && state.detailOpen) {
|
|
2444
|
+
return false;
|
|
2445
|
+
}
|
|
2446
|
+
if (isListSurfaceInteractionActive()) {
|
|
2447
|
+
return false;
|
|
2448
|
+
}
|
|
2449
|
+
const listSurface = document.querySelector("[data-list-surface]");
|
|
2450
|
+
if (!listSurface) {
|
|
2451
|
+
return false;
|
|
2452
|
+
}
|
|
2453
|
+
const scrollLeft = listSurface.scrollLeft;
|
|
2454
|
+
const scrollTop = listSurface.scrollTop;
|
|
2455
|
+
listSurface.innerHTML = renderListPanel({
|
|
2456
|
+
tab: state.currentTab,
|
|
2457
|
+
entries: listEntriesForTab(state.currentTab),
|
|
2458
|
+
desktop,
|
|
2459
|
+
});
|
|
2460
|
+
listSurface.scrollLeft = scrollLeft;
|
|
2461
|
+
listSurface.scrollTop = scrollTop;
|
|
2462
|
+
bindPartialListSurfaceInteractions(listSurface);
|
|
2463
|
+
reportTimelineRendered("deferred-list");
|
|
2464
|
+
return true;
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
function isListSurfaceInteractionActive() {
|
|
2468
|
+
if (state.threadFilterInteractionUntilMs > Date.now() || state.timelineKindFilterOpen) {
|
|
2469
|
+
return true;
|
|
2470
|
+
}
|
|
2471
|
+
if (typeof document === "undefined" || typeof Element === "undefined") {
|
|
2472
|
+
return false;
|
|
2473
|
+
}
|
|
2474
|
+
const activeElement = document.activeElement;
|
|
2475
|
+
return activeElement instanceof Element && Boolean(activeElement.closest("[data-list-surface]"));
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2040
2478
|
function currentViewportScrollY() {
|
|
2041
2479
|
return window.scrollY || window.pageYOffset || document.documentElement?.scrollTop || 0;
|
|
2042
2480
|
}
|
|
@@ -2049,11 +2487,40 @@ function clearThreadFilterInteraction() {
|
|
|
2049
2487
|
state.threadFilterInteractionUntilMs = 0;
|
|
2050
2488
|
}
|
|
2051
2489
|
|
|
2490
|
+
function markScrollableContentInteraction() {
|
|
2491
|
+
state.scrollableContentInteractionUntilMs = Date.now() + SCROLLABLE_CONTENT_INTERACTION_DEFER_MS;
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
function selectionIntersectsScrollableContent() {
|
|
2495
|
+
if (typeof document === "undefined" || typeof Element === "undefined") {
|
|
2496
|
+
return false;
|
|
2497
|
+
}
|
|
2498
|
+
const selection = document.getSelection?.();
|
|
2499
|
+
if (!selection || selection.isCollapsed || selection.rangeCount === 0) {
|
|
2500
|
+
return false;
|
|
2501
|
+
}
|
|
2502
|
+
for (let index = 0; index < selection.rangeCount; index += 1) {
|
|
2503
|
+
const range = selection.getRangeAt(index);
|
|
2504
|
+
const node = range.commonAncestorContainer;
|
|
2505
|
+
const element = node instanceof Element ? node : node?.parentElement;
|
|
2506
|
+
if (element?.closest?.(SCROLLABLE_CONTENT_SELECTORS)) {
|
|
2507
|
+
return true;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
return false;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2052
2513
|
function shouldDeferRenderForActiveInteraction() {
|
|
2053
2514
|
const activeElement = document.activeElement;
|
|
2054
2515
|
if (state.completionReplySheetToken) {
|
|
2055
2516
|
return true;
|
|
2056
2517
|
}
|
|
2518
|
+
if (state.scrollableContentInteractionUntilMs > Date.now()) {
|
|
2519
|
+
return true;
|
|
2520
|
+
}
|
|
2521
|
+
if (selectionIntersectsScrollableContent()) {
|
|
2522
|
+
return true;
|
|
2523
|
+
}
|
|
2057
2524
|
if (
|
|
2058
2525
|
activeElement instanceof HTMLTextAreaElement &&
|
|
2059
2526
|
activeElement.matches("[data-completion-reply-textarea]") &&
|
|
@@ -2269,6 +2736,25 @@ function normalizeCompletionReplyWarning(value) {
|
|
|
2269
2736
|
};
|
|
2270
2737
|
}
|
|
2271
2738
|
|
|
2739
|
+
function normalizeCompletionReplyCompareText(value) {
|
|
2740
|
+
return normalizeClientText(value)
|
|
2741
|
+
.replace(/\s+/gu, " ")
|
|
2742
|
+
.trim();
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
function completionReplyWarningMatchesSentText(error, text, attachmentCount = 0) {
|
|
2746
|
+
if (error?.errorKey !== "completion-reply-thread-advanced") {
|
|
2747
|
+
return false;
|
|
2748
|
+
}
|
|
2749
|
+
if (attachmentCount > 0) {
|
|
2750
|
+
return false;
|
|
2751
|
+
}
|
|
2752
|
+
const warning = normalizeCompletionReplyWarning(error?.payload?.warning);
|
|
2753
|
+
const warningText = normalizeCompletionReplyCompareText(warning?.summary || "");
|
|
2754
|
+
const sentText = normalizeCompletionReplyCompareText(text);
|
|
2755
|
+
return Boolean(warningText && sentText && warningText === sentText);
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2272
2758
|
function normalizeCompletionReplyAttachments(values) {
|
|
2273
2759
|
const rawValues = Array.isArray(values)
|
|
2274
2760
|
? values
|
|
@@ -2474,9 +2960,15 @@ async function fetchCurrentDetailForItem(itemRef = state.currentItem) {
|
|
|
2474
2960
|
if (hasDetailOverride(itemRef)) {
|
|
2475
2961
|
return state.detailOverride.detail;
|
|
2476
2962
|
}
|
|
2963
|
+
const detailUrl = `/api/items/${encodeURIComponent(itemRef.kind)}/${encodeURIComponent(itemRef.token)}`;
|
|
2477
2964
|
try {
|
|
2478
2965
|
const detail = await hydrateDetailImages(
|
|
2479
|
-
await apiGet(
|
|
2966
|
+
await apiGet(detailUrl, {
|
|
2967
|
+
timeoutMs: DETAIL_FETCH_TIMEOUT_MS,
|
|
2968
|
+
probeLanWhileSticky: true,
|
|
2969
|
+
stickyLanProbeTimeoutMs: DETAIL_STICKY_LAN_PROBE_TIMEOUT_MS,
|
|
2970
|
+
preferRelayError: true,
|
|
2971
|
+
})
|
|
2480
2972
|
);
|
|
2481
2973
|
if (hasLaunchItemIntent(itemRef)) {
|
|
2482
2974
|
state.launchItemIntent.status = "loaded";
|
|
@@ -2489,16 +2981,24 @@ async function fetchCurrentDetailForItem(itemRef = state.currentItem) {
|
|
|
2489
2981
|
renderPair();
|
|
2490
2982
|
return null;
|
|
2491
2983
|
}
|
|
2492
|
-
await
|
|
2984
|
+
await Promise.race([
|
|
2985
|
+
refreshInbox(),
|
|
2986
|
+
wait(DETAIL_REFRESH_FALLBACK_TIMEOUT_MS),
|
|
2987
|
+
]).catch(() => {});
|
|
2493
2988
|
try {
|
|
2494
2989
|
const detail = await hydrateDetailImages(
|
|
2495
|
-
await apiGet(
|
|
2990
|
+
await apiGet(detailUrl, {
|
|
2991
|
+
timeoutMs: DETAIL_FETCH_TIMEOUT_MS,
|
|
2992
|
+
probeLanWhileSticky: true,
|
|
2993
|
+
stickyLanProbeTimeoutMs: DETAIL_STICKY_LAN_PROBE_TIMEOUT_MS,
|
|
2994
|
+
preferRelayError: true,
|
|
2995
|
+
})
|
|
2496
2996
|
);
|
|
2497
2997
|
if (hasLaunchItemIntent(itemRef)) {
|
|
2498
2998
|
state.launchItemIntent.status = "loaded";
|
|
2499
2999
|
}
|
|
2500
3000
|
return detail;
|
|
2501
|
-
} catch {
|
|
3001
|
+
} catch (retryError) {
|
|
2502
3002
|
if (hasLaunchItemIntent(itemRef)) {
|
|
2503
3003
|
clearChoiceLocalDraftForItem(itemRef);
|
|
2504
3004
|
const fallbackDetail = buildLaunchItemFallbackDetail(itemRef);
|
|
@@ -2513,7 +3013,7 @@ async function fetchCurrentDetailForItem(itemRef = state.currentItem) {
|
|
|
2513
3013
|
if (!state.currentItem) {
|
|
2514
3014
|
return null;
|
|
2515
3015
|
}
|
|
2516
|
-
return
|
|
3016
|
+
return buildDetailLoadErrorDetail(itemRef, retryError || error);
|
|
2517
3017
|
}
|
|
2518
3018
|
}
|
|
2519
3019
|
}
|
|
@@ -2528,6 +3028,7 @@ function queueCurrentDetailLoad(itemRef = state.currentItem) {
|
|
|
2528
3028
|
|
|
2529
3029
|
const requestedItem = { ...itemRef };
|
|
2530
3030
|
const requestId = ++detailLoadSequence;
|
|
3031
|
+
const hadRenderableDetailAtStart = Boolean(renderableCurrentDetail(requestedItem));
|
|
2531
3032
|
state.currentDetailLoading = true;
|
|
2532
3033
|
state.detailLoadingItem = requestedItem;
|
|
2533
3034
|
|
|
@@ -2554,6 +3055,16 @@ function queueCurrentDetailLoad(itemRef = state.currentItem) {
|
|
|
2554
3055
|
}
|
|
2555
3056
|
state.currentDetailLoading = false;
|
|
2556
3057
|
state.detailLoadingItem = null;
|
|
3058
|
+
const completedInitialDetailLoad =
|
|
3059
|
+
!hadRenderableDetailAtStart &&
|
|
3060
|
+
Boolean(state.currentDetail) &&
|
|
3061
|
+
isSameItemRef(state.currentDetail, requestedItem) &&
|
|
3062
|
+
Boolean(state.currentItem) &&
|
|
3063
|
+
isSameItemRef(state.currentItem, requestedItem);
|
|
3064
|
+
if (shouldDeferRenderForActiveInteraction() && !completedInitialDetailLoad) {
|
|
3065
|
+
renderDeferredInteractionShellUpdates();
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
2557
3068
|
renderCurrentSurface();
|
|
2558
3069
|
});
|
|
2559
3070
|
}
|
|
@@ -2572,6 +3083,31 @@ function buildLaunchItemFallbackDetail(itemRef) {
|
|
|
2572
3083
|
};
|
|
2573
3084
|
}
|
|
2574
3085
|
|
|
3086
|
+
function buildDetailLoadErrorDetail(itemRef, error) {
|
|
3087
|
+
const snapshot = buildDetailLoadingSnapshot(itemRef) || {};
|
|
3088
|
+
const entry = selectedEntryForItem(itemRef);
|
|
3089
|
+
const item = entry?.item || {};
|
|
3090
|
+
const fallbackText = normalizeClientText(item.messageText || item.summary || item.title || "");
|
|
3091
|
+
const messageParts = [
|
|
3092
|
+
fallbackText ? `<p>${escapeHtml(fallbackText)}</p>` : "",
|
|
3093
|
+
].filter(Boolean);
|
|
3094
|
+
return {
|
|
3095
|
+
kind: itemRef.kind,
|
|
3096
|
+
token: itemRef.token,
|
|
3097
|
+
title: item.title || snapshot.title || kindMeta(itemRef.kind).label,
|
|
3098
|
+
threadId: item.threadId || "",
|
|
3099
|
+
threadLabel: item.threadLabel || snapshot.threadLabel || "",
|
|
3100
|
+
summary: item.summary || fallbackText || "",
|
|
3101
|
+
messageHtml: messageParts.join(""),
|
|
3102
|
+
provider: item.provider || normalizeProviderClient(item.provider) || "",
|
|
3103
|
+
createdAtMs: Number(item.createdAtMs || snapshot.createdAtMs) || Date.now(),
|
|
3104
|
+
readOnly: true,
|
|
3105
|
+
actions: [],
|
|
3106
|
+
loadError: true,
|
|
3107
|
+
loadErrorMessage: normalizeClientText(error?.message || String(error || "")),
|
|
3108
|
+
};
|
|
3109
|
+
}
|
|
3110
|
+
|
|
2575
3111
|
function resolveLaunchFallbackMessage(kind, isHandled) {
|
|
2576
3112
|
if (kind === "approval") {
|
|
2577
3113
|
return isHandled ? L("error.approvalAlreadyHandled") : L("error.approvalNotFound");
|
|
@@ -2636,7 +3172,7 @@ function renderDesktopWorkspace(detail) {
|
|
|
2636
3172
|
(state.currentDetailLoading || !renderableCurrentDetail());
|
|
2637
3173
|
return `
|
|
2638
3174
|
<section class="desktop-workspace">
|
|
2639
|
-
<aside class="surface surface--list">
|
|
3175
|
+
<aside class="surface surface--list" data-list-surface>
|
|
2640
3176
|
${renderListPanel({
|
|
2641
3177
|
tab: state.currentTab,
|
|
2642
3178
|
entries,
|
|
@@ -2664,7 +3200,7 @@ function renderMobileWorkspace(detail) {
|
|
|
2664
3200
|
}
|
|
2665
3201
|
|
|
2666
3202
|
return `
|
|
2667
|
-
<section class="screen-block">
|
|
3203
|
+
<section class="screen-block" data-list-surface>
|
|
2668
3204
|
${renderListPanel({
|
|
2669
3205
|
tab: state.currentTab,
|
|
2670
3206
|
entries: listEntriesForTab(state.currentTab),
|
|
@@ -3245,22 +3781,27 @@ function renderTimelineEntry(entry, { desktop }) {
|
|
|
3245
3781
|
const isMoltbookOrA2A = item.kind === "moltbook_reply" || item.kind === "moltbook_draft" || item.kind === "a2a_task" || item.kind === "a2a_task_result" || item.kind === "thread_share";
|
|
3246
3782
|
const isMessageLike = TIMELINE_MESSAGE_KINDS.has(item.kind) || isMoltbookOrA2A;
|
|
3247
3783
|
const isFileEvent = item.kind === "file_event";
|
|
3784
|
+
const isCommandEvent = item.kind === "command_event";
|
|
3785
|
+
const isActivityStatus = item.kind === "activity_status";
|
|
3248
3786
|
const imageUrls = Array.isArray(item.imageUrls) ? item.imageUrls.filter(Boolean) : [];
|
|
3249
3787
|
const fileRefs = normalizeClientFileRefs(item.fileRefs);
|
|
3250
|
-
const primaryText = timelineEntryPrimaryText(item, entry.status, { isMessageLike, isFileEvent });
|
|
3251
|
-
const secondaryText = timelineEntrySecondaryText(item, entry.status, primaryText, { isMessageLike, isFileEvent });
|
|
3788
|
+
const primaryText = timelineEntryPrimaryText(item, entry.status, { isMessageLike, isFileEvent, isCommandEvent });
|
|
3789
|
+
const secondaryText = timelineEntrySecondaryText(item, entry.status, primaryText, { isMessageLike, isFileEvent, isCommandEvent });
|
|
3252
3790
|
const threadLabel = timelineEntryThreadLabel(item, isMessageLike);
|
|
3253
3791
|
const timestampLabel = formatTimelineTimestamp(item.createdAtMs);
|
|
3254
3792
|
const statusLabel = timelineEntryStatusLabel(item, isMessageLike);
|
|
3255
3793
|
const fileEventFileSummary = isFileEvent ? timelineFileEventFileSummary(item) : "";
|
|
3256
3794
|
const fileEventDiffStatsHtml = isFileEvent ? renderDiffEntryStatsHtml(item) : "";
|
|
3795
|
+
const toolEventCommand = timelineToolEventCommand(item);
|
|
3796
|
+
const tagName = isActivityStatus ? "div" : "button";
|
|
3797
|
+
const openAttrs = isActivityStatus
|
|
3798
|
+
? `role="status" aria-live="polite"`
|
|
3799
|
+
: `data-open-item-kind="${escapeHtml(item.kind)}" data-open-item-token="${escapeHtml(item.token)}" data-source-tab="timeline"`;
|
|
3257
3800
|
|
|
3258
3801
|
return `
|
|
3259
|
-
|
|
3260
|
-
class="timeline-entry timeline-entry--${kindClassName} timeline-entry--kind-${kindNameClass} ${isMessageLike ? "timeline-entry--message" : "timeline-entry--operational"}"
|
|
3261
|
-
|
|
3262
|
-
data-open-item-token="${escapeHtml(item.token)}"
|
|
3263
|
-
data-source-tab="timeline"
|
|
3802
|
+
<${tagName}
|
|
3803
|
+
class="timeline-entry timeline-entry--${kindClassName} timeline-entry--kind-${kindNameClass} ${isMessageLike ? "timeline-entry--message" : "timeline-entry--operational"} ${isActivityStatus ? "timeline-entry--activity" : ""}"
|
|
3804
|
+
${openAttrs}
|
|
3264
3805
|
>
|
|
3265
3806
|
<div class="timeline-entry__meta">
|
|
3266
3807
|
<span class="timeline-entry__kind">
|
|
@@ -3270,12 +3811,13 @@ function renderTimelineEntry(entry, { desktop }) {
|
|
|
3270
3811
|
</span>
|
|
3271
3812
|
<span class="timeline-entry__meta-right">
|
|
3272
3813
|
<span class="timeline-entry__time">${escapeHtml(timestampLabel)}</span>
|
|
3273
|
-
|
|
3814
|
+
${isActivityStatus ? `<span class="timeline-entry__activity-pulse" aria-hidden="true"></span>` : `<span class="timeline-entry__chevron" aria-hidden="true">${renderIcon("chevron-right")}</span>`}
|
|
3274
3815
|
</span>
|
|
3275
3816
|
</div>
|
|
3276
3817
|
${threadLabel ? `<p class="timeline-entry__thread">${escapeHtml(threadLabel)}</p>` : ""}
|
|
3277
3818
|
<div class="timeline-entry__body">
|
|
3278
3819
|
<p class="timeline-entry__title">${escapeHtml(primaryText)}</p>
|
|
3820
|
+
${toolEventCommand ? `<pre class="timeline-entry__command"><code>${escapeHtml(toolEventCommand)}</code></pre>` : ""}
|
|
3279
3821
|
${secondaryText ? `<p class="timeline-entry__summary">${escapeHtml(secondaryText)}</p>` : ""}
|
|
3280
3822
|
${
|
|
3281
3823
|
isFileEvent && fileEventFileSummary
|
|
@@ -3291,7 +3833,7 @@ function renderTimelineEntry(entry, { desktop }) {
|
|
|
3291
3833
|
${isFileEvent ? "" : renderTimelineEntryFileStrip(fileRefs)}
|
|
3292
3834
|
</div>
|
|
3293
3835
|
${statusLabel ? `<div class="timeline-entry__footer"><span class="timeline-entry__status">${escapeHtml(statusLabel)}</span></div>` : ""}
|
|
3294
|
-
|
|
3836
|
+
</${tagName}>
|
|
3295
3837
|
`;
|
|
3296
3838
|
}
|
|
3297
3839
|
|
|
@@ -3325,7 +3867,7 @@ function renderDiffEntry(entry) {
|
|
|
3325
3867
|
}
|
|
3326
3868
|
|
|
3327
3869
|
function timelineEntryStatusLabel(item, isMessageLike) {
|
|
3328
|
-
if (isMessageLike || item?.kind === "file_event") {
|
|
3870
|
+
if (isMessageLike || item?.kind === "file_event" || item?.kind === "command_event") {
|
|
3329
3871
|
return "";
|
|
3330
3872
|
}
|
|
3331
3873
|
|
|
@@ -3408,7 +3950,7 @@ function timelineEntryThreadLabel(item, isMessage) {
|
|
|
3408
3950
|
return threadLabel || "";
|
|
3409
3951
|
}
|
|
3410
3952
|
|
|
3411
|
-
function timelineEntryPrimaryText(item, status, { isMessageLike = false, isFileEvent = false } = {}) {
|
|
3953
|
+
function timelineEntryPrimaryText(item, status, { isMessageLike = false, isFileEvent = false, isCommandEvent = false } = {}) {
|
|
3412
3954
|
if (item?.kind === "ambient_suggestions") {
|
|
3413
3955
|
return item.summary || fallbackSummaryForKind(item.kind, status, item.provider);
|
|
3414
3956
|
}
|
|
@@ -3421,10 +3963,14 @@ function timelineEntryPrimaryText(item, status, { isMessageLike = false, isFileE
|
|
|
3421
3963
|
return fileEventTimelineCountLabel(item) || fallbackSummaryForKind(item.kind, status, item.provider);
|
|
3422
3964
|
}
|
|
3423
3965
|
|
|
3966
|
+
if (isCommandEvent) {
|
|
3967
|
+
return L("common.commandEvent");
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3424
3970
|
return timelineDisplayTitleWithoutThread(item, { allowFallbackSummary: true }) || L("common.untitledItem");
|
|
3425
3971
|
}
|
|
3426
3972
|
|
|
3427
|
-
function timelineEntrySecondaryText(item, status, primaryText, { isMessageLike = false, isFileEvent = false } = {}) {
|
|
3973
|
+
function timelineEntrySecondaryText(item, status, primaryText, { isMessageLike = false, isFileEvent = false, isCommandEvent = false } = {}) {
|
|
3428
3974
|
if (item?.kind === "ambient_suggestions") {
|
|
3429
3975
|
return "";
|
|
3430
3976
|
}
|
|
@@ -3442,6 +3988,10 @@ function timelineEntrySecondaryText(item, status, primaryText, { isMessageLike =
|
|
|
3442
3988
|
return "";
|
|
3443
3989
|
}
|
|
3444
3990
|
|
|
3991
|
+
if (isCommandEvent) {
|
|
3992
|
+
return "";
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3445
3995
|
const compactTitle = timelineDisplayTitleWithoutThread(item, { allowFallbackSummary: false });
|
|
3446
3996
|
return compactTitle ? summaryText : "";
|
|
3447
3997
|
}
|
|
@@ -3491,6 +4041,7 @@ function timelineGeneratedTitlePrefixes() {
|
|
|
3491
4041
|
kindMeta("assistant_commentary").label,
|
|
3492
4042
|
kindMeta("assistant_final").label,
|
|
3493
4043
|
L("common.fileEvent"),
|
|
4044
|
+
L("common.commandEvent"),
|
|
3494
4045
|
"Approval",
|
|
3495
4046
|
"Plan",
|
|
3496
4047
|
"Choice",
|
|
@@ -3499,6 +4050,7 @@ function timelineGeneratedTitlePrefixes() {
|
|
|
3499
4050
|
"Commentary",
|
|
3500
4051
|
"Final answer",
|
|
3501
4052
|
"Files",
|
|
4053
|
+
"Command",
|
|
3502
4054
|
"承認",
|
|
3503
4055
|
"プラン",
|
|
3504
4056
|
"選択",
|
|
@@ -3507,13 +4059,44 @@ function timelineGeneratedTitlePrefixes() {
|
|
|
3507
4059
|
"途中経過",
|
|
3508
4060
|
"最終回答",
|
|
3509
4061
|
"ファイル",
|
|
4062
|
+
"コマンド",
|
|
3510
4063
|
];
|
|
3511
4064
|
}
|
|
3512
4065
|
|
|
4066
|
+
function timelineCommandEventCommand(item) {
|
|
4067
|
+
return truncateUiText(firstMarkdownCodeFence(item?.messageText || "") || item?.summary || item?.title || "", 220);
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
function shouldRenderFileEventCommand(item) {
|
|
4071
|
+
if (item?.kind !== "file_event") {
|
|
4072
|
+
return false;
|
|
4073
|
+
}
|
|
4074
|
+
return ["read", "search", "git"].includes(normalizeClientText(item?.fileEventType || ""));
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4077
|
+
function timelineToolEventCommand(item) {
|
|
4078
|
+
if (item?.kind === "activity_status") {
|
|
4079
|
+
return truncateUiText(item?.commandText || "", 220);
|
|
4080
|
+
}
|
|
4081
|
+
if (item?.kind === "command_event") {
|
|
4082
|
+
return timelineCommandEventCommand(item);
|
|
4083
|
+
}
|
|
4084
|
+
if (!shouldRenderFileEventCommand(item)) {
|
|
4085
|
+
return "";
|
|
4086
|
+
}
|
|
4087
|
+
return truncateUiText(firstMarkdownCodeFence(item?.messageText || "") || item?.commandText || "", 220);
|
|
4088
|
+
}
|
|
4089
|
+
|
|
3513
4090
|
function fileEventDisplayLabel(fileEventType) {
|
|
3514
4091
|
switch (normalizeClientText(fileEventType || "")) {
|
|
3515
4092
|
case "read":
|
|
3516
4093
|
return L("fileEvent.read");
|
|
4094
|
+
case "search":
|
|
4095
|
+
return L("fileEvent.search");
|
|
4096
|
+
case "git":
|
|
4097
|
+
return L("fileEvent.command");
|
|
4098
|
+
case "command":
|
|
4099
|
+
return L("fileEvent.command");
|
|
3517
4100
|
case "write":
|
|
3518
4101
|
return L("fileEvent.write");
|
|
3519
4102
|
case "create":
|
|
@@ -3529,13 +4112,26 @@ function fileEventDisplayLabel(fileEventType) {
|
|
|
3529
4112
|
|
|
3530
4113
|
function fileEventTimelineCountLabel(item) {
|
|
3531
4114
|
const fileEventType = normalizeClientText(item?.fileEventType || "");
|
|
3532
|
-
const
|
|
4115
|
+
const fileRefs = normalizeClientFileRefs(item?.fileRefs);
|
|
4116
|
+
const count = fileRefs.length;
|
|
3533
4117
|
if (count <= 0) {
|
|
3534
4118
|
return fileEventDisplayLabel(fileEventType) || L("common.fileEvent");
|
|
3535
4119
|
}
|
|
3536
4120
|
switch (fileEventType) {
|
|
3537
4121
|
case "read":
|
|
4122
|
+
if (count === 1) {
|
|
4123
|
+
return L("fileEvent.read");
|
|
4124
|
+
}
|
|
3538
4125
|
return L("fileEvent.timeline.read", { count });
|
|
4126
|
+
case "search":
|
|
4127
|
+
if (count === 1) {
|
|
4128
|
+
return L("fileEvent.search");
|
|
4129
|
+
}
|
|
4130
|
+
return L("fileEvent.timeline.search", { count });
|
|
4131
|
+
case "git":
|
|
4132
|
+
return L("fileEvent.timeline.command", { count });
|
|
4133
|
+
case "command":
|
|
4134
|
+
return L("fileEvent.timeline.command", { count });
|
|
3539
4135
|
case "write":
|
|
3540
4136
|
return L("fileEvent.timeline.write", { count });
|
|
3541
4137
|
case "create":
|
|
@@ -3584,7 +4180,7 @@ function diffThreadFilesSummary(item) {
|
|
|
3584
4180
|
.map((entry) => fileChangeEntryLabel(entry))
|
|
3585
4181
|
.filter(Boolean);
|
|
3586
4182
|
if (labels.length === 0) {
|
|
3587
|
-
return "";
|
|
4183
|
+
return truncateUiText(firstMarkdownCodeFence(item?.messageText || "") || item?.summary || "");
|
|
3588
4184
|
}
|
|
3589
4185
|
const visibleLabels = labels.slice(0, 3);
|
|
3590
4186
|
const hiddenCount = labels.length - visibleLabels.length;
|
|
@@ -4878,20 +5474,29 @@ function recentAutoPilotEntries(limit = 5) {
|
|
|
4878
5474
|
}
|
|
4879
5475
|
|
|
4880
5476
|
function isAutoPilotApprovalEntry(entry) {
|
|
5477
|
+
const mode = normalizeClientText(entry?.autoPilotMode || "");
|
|
4881
5478
|
const stableId = normalizeClientText(entry?.stableId || "");
|
|
4882
5479
|
return (
|
|
4883
5480
|
normalizeClientText(entry?.kind || "") === "approval" &&
|
|
4884
5481
|
normalizeClientText(entry?.outcome || "") === "approved" &&
|
|
4885
|
-
(stableId.endsWith(":autopilot") || stableId.includes(":autopilot-write"))
|
|
5482
|
+
(mode === "read" || mode === "write" || stableId.endsWith(":autopilot") || stableId.includes(":autopilot-write"))
|
|
4886
5483
|
);
|
|
4887
5484
|
}
|
|
4888
5485
|
|
|
4889
5486
|
function autoPilotEntryMode(item) {
|
|
5487
|
+
const mode = normalizeClientText(item?.autoPilotMode || "");
|
|
5488
|
+
if (mode === "read" || mode === "write") {
|
|
5489
|
+
return mode;
|
|
5490
|
+
}
|
|
4890
5491
|
const stableId = normalizeClientText(item?.stableId || "");
|
|
4891
5492
|
return stableId.includes(":autopilot-write") ? "write" : "read";
|
|
4892
5493
|
}
|
|
4893
5494
|
|
|
4894
5495
|
function autoPilotEntryWriteLane(item) {
|
|
5496
|
+
const projected = normalizeClientText(item?.autoPilotWriteLane || "");
|
|
5497
|
+
if (projected) {
|
|
5498
|
+
return projected;
|
|
5499
|
+
}
|
|
4895
5500
|
const stableId = normalizeClientText(item?.stableId || "");
|
|
4896
5501
|
const match = stableId.match(/:autopilot-write:([a-z_-]+)$/u);
|
|
4897
5502
|
return normalizeClientText(match?.[1] || "");
|
|
@@ -5222,6 +5827,15 @@ function renderSettingsAutoPilotSuggestion({ lane, count }) {
|
|
|
5222
5827
|
`;
|
|
5223
5828
|
}
|
|
5224
5829
|
|
|
5830
|
+
function renderSettingsExternalLink({ href, label }) {
|
|
5831
|
+
return `
|
|
5832
|
+
<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%;">
|
|
5833
|
+
<span class="settings-external-link__label">${escapeHtml(label)}</span>
|
|
5834
|
+
<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>
|
|
5835
|
+
</a>
|
|
5836
|
+
`.trim();
|
|
5837
|
+
}
|
|
5838
|
+
|
|
5225
5839
|
function renderSettingsMoltbookPage(context) {
|
|
5226
5840
|
const scout = context.moltbookScout;
|
|
5227
5841
|
if (!scout?.enabled) {
|
|
@@ -5236,10 +5850,16 @@ function renderSettingsMoltbookPage(context) {
|
|
|
5236
5850
|
renderSettingsInfoRow(L("settings.row.moltbookBatchTopScore"), String(scout.batch.topScore)),
|
|
5237
5851
|
renderSettingsInfoRow(L("settings.row.moltbookBatchRemaining"), `${Math.floor(scout.batch.remainingSeconds / 60)}:${String(scout.batch.remainingSeconds % 60).padStart(2, "0")}`),
|
|
5238
5852
|
] : [];
|
|
5239
|
-
const
|
|
5853
|
+
const accountProfileLink = scout.account?.name && scout.account?.profileUrl
|
|
5854
|
+
? renderSettingsExternalLink({
|
|
5855
|
+
href: scout.account.profileUrl,
|
|
5856
|
+
label: scout.account.name,
|
|
5857
|
+
})
|
|
5858
|
+
: "";
|
|
5859
|
+
const accountRow = accountProfileLink
|
|
5240
5860
|
? renderSettingsInfoRow(
|
|
5241
5861
|
L("settings.row.moltbookAccount"),
|
|
5242
|
-
|
|
5862
|
+
accountProfileLink,
|
|
5243
5863
|
{ rawValue: true }
|
|
5244
5864
|
)
|
|
5245
5865
|
: null;
|
|
@@ -5310,7 +5930,10 @@ function renderSettingsA2aRelayPage(context) {
|
|
|
5310
5930
|
? L("settings.a2aRelay.status.polling")
|
|
5311
5931
|
: L("settings.a2aRelay.status.disconnected");
|
|
5312
5932
|
const profileUrl = `${relay.relayUrl}/u/${relay.userId}`;
|
|
5313
|
-
const userIdLink =
|
|
5933
|
+
const userIdLink = renderSettingsExternalLink({
|
|
5934
|
+
href: profileUrl,
|
|
5935
|
+
label: relay.userId,
|
|
5936
|
+
});
|
|
5314
5937
|
const relayHost = (() => { try { return new URL(relay.relayUrl).host; } catch { return relay.relayUrl; } })();
|
|
5315
5938
|
const publicChecked = relay.acceptPublicTasks === true;
|
|
5316
5939
|
|
|
@@ -6335,6 +6958,7 @@ function renderStandardDetailDesktop(detail) {
|
|
|
6335
6958
|
<div class="detail-shell">
|
|
6336
6959
|
${renderDetailMetaRow(detail, kindInfo)}
|
|
6337
6960
|
<h2 class="detail-title detail-title--desktop">${renderDetailTitle(detail)}</h2>
|
|
6961
|
+
${renderDetailLoadErrorNotice(detail)}
|
|
6338
6962
|
${detail.readOnly || detail.kind === "approval" || detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "thread_share" ? "" : renderDetailLead(detail, kindInfo)}
|
|
6339
6963
|
${renderPreviousContextCard(detail)}
|
|
6340
6964
|
${renderAutoPilotManualReview(detail)}
|
|
@@ -6355,6 +6979,7 @@ function renderStandardDetailDesktop(detail) {
|
|
|
6355
6979
|
</section>
|
|
6356
6980
|
`
|
|
6357
6981
|
}
|
|
6982
|
+
${renderCommandEventDetail(detail)}
|
|
6358
6983
|
${renderClaudePlanSection(detail)}
|
|
6359
6984
|
${renderClaudeQuestionSection(detail)}
|
|
6360
6985
|
${renderDetailImageGallery(detail)}
|
|
@@ -6377,6 +7002,7 @@ function renderStandardDetailMobile(detail) {
|
|
|
6377
7002
|
<div class="detail-shell detail-shell--mobile">
|
|
6378
7003
|
<div class="mobile-detail-scroll mobile-detail-scroll--detail">
|
|
6379
7004
|
${renderDetailMetaRow(detail, kindInfo, { mobile: true })}
|
|
7005
|
+
${renderDetailLoadErrorNotice(detail, { mobile: true })}
|
|
6380
7006
|
${renderPreviousContextCard(detail, { mobile: true })}
|
|
6381
7007
|
${renderAutoPilotManualReview(detail, { mobile: true })}
|
|
6382
7008
|
${renderInterruptedDetailNotice(detail, { mobile: true })}
|
|
@@ -6397,6 +7023,7 @@ function renderStandardDetailMobile(detail) {
|
|
|
6397
7023
|
</section>
|
|
6398
7024
|
`
|
|
6399
7025
|
}
|
|
7026
|
+
${renderCommandEventDetail(detail, { mobile: true })}
|
|
6400
7027
|
${renderClaudePlanSection(detail, { mobile: true })}
|
|
6401
7028
|
${renderClaudeQuestionSection(detail, { mobile: true })}
|
|
6402
7029
|
${renderDetailImageGallery(detail, { mobile: true })}
|
|
@@ -6439,6 +7066,46 @@ function renderAmbientSuggestionsSection(detail, options = {}) {
|
|
|
6439
7066
|
`;
|
|
6440
7067
|
}
|
|
6441
7068
|
|
|
7069
|
+
function renderCommandEventDetail(detail, options = {}) {
|
|
7070
|
+
if (detail?.kind !== "command_event" && !shouldRenderFileEventCommand(detail)) {
|
|
7071
|
+
return "";
|
|
7072
|
+
}
|
|
7073
|
+
const commandText = normalizeClientText(detail?.commandText || "") || firstMarkdownCodeFence(detail?.messageText || "");
|
|
7074
|
+
if (!commandText) {
|
|
7075
|
+
return "";
|
|
7076
|
+
}
|
|
7077
|
+
return `
|
|
7078
|
+
<section class="detail-card detail-card--command ${options.mobile ? "detail-card--mobile" : ""}">
|
|
7079
|
+
<div class="detail-files-card__header">
|
|
7080
|
+
<span class="detail-files-card__icon" aria-hidden="true">${renderIcon("command")}</span>
|
|
7081
|
+
<span>${escapeHtml(L("common.commandEvent"))}</span>
|
|
7082
|
+
</div>
|
|
7083
|
+
<pre class="detail-command-block"><code>${escapeHtml(commandText)}</code></pre>
|
|
7084
|
+
</section>
|
|
7085
|
+
`;
|
|
7086
|
+
}
|
|
7087
|
+
|
|
7088
|
+
function renderDetailLoadErrorNotice(detail, options = {}) {
|
|
7089
|
+
if (detail?.loadError !== true) {
|
|
7090
|
+
return "";
|
|
7091
|
+
}
|
|
7092
|
+
const errorText = normalizeClientText(detail.loadErrorMessage || "");
|
|
7093
|
+
return `
|
|
7094
|
+
<section class="detail-card detail-card--body ${options.mobile ? "detail-card--mobile" : ""}">
|
|
7095
|
+
<div class="detail-body markdown">
|
|
7096
|
+
<p><strong>${escapeHtml(L("detail.loadFailedTitle"))}</strong></p>
|
|
7097
|
+
<p>${escapeHtml(L("detail.loadFailedCopy"))}</p>
|
|
7098
|
+
${errorText ? `<p class="muted">${escapeHtml(errorText)}</p>` : ""}
|
|
7099
|
+
</div>
|
|
7100
|
+
<div class="actions actions--stack">
|
|
7101
|
+
<button class="secondary secondary--wide" type="button" data-detail-retry>
|
|
7102
|
+
${escapeHtml(L("detail.loadRetry"))}
|
|
7103
|
+
</button>
|
|
7104
|
+
</div>
|
|
7105
|
+
</section>
|
|
7106
|
+
`;
|
|
7107
|
+
}
|
|
7108
|
+
|
|
6442
7109
|
function renderAmbientSuggestionCard(detail, suggestion, index) {
|
|
6443
7110
|
const copyKey = ambientSuggestionCopyKey(detail?.token || "", suggestion?.id || "", index);
|
|
6444
7111
|
const copyStatus = state.ambientSuggestionCopyState?.key === copyKey
|
|
@@ -6471,7 +7138,7 @@ function renderAmbientSuggestionCard(detail, suggestion, index) {
|
|
|
6471
7138
|
}
|
|
6472
7139
|
|
|
6473
7140
|
function renderDetailPlainIntro(detail, options = {}) {
|
|
6474
|
-
if (!["approval", "diff_thread", "file_event"].includes(detail?.kind || "")) {
|
|
7141
|
+
if (!["approval", "diff_thread", "file_event", "command_event"].includes(detail?.kind || "")) {
|
|
6475
7142
|
return "";
|
|
6476
7143
|
}
|
|
6477
7144
|
const ak = normalizeClientText(detail?.approvalKind || "");
|
|
@@ -6995,7 +7662,7 @@ function renderMoltbookDraftComposer(detail, options = {}) {
|
|
|
6995
7662
|
? `
|
|
6996
7663
|
<div class="actions actions--stack${options.mobile ? " actions--sticky" : ""}">
|
|
6997
7664
|
<button type="submit" data-action="approve" class="primary primary--wide">${escapeHtml(approveLabel)}</button>
|
|
6998
|
-
<button type="submit" data-action="deny" class="danger danger--wide"
|
|
7665
|
+
<button type="submit" data-action="deny" class="danger danger--wide">${escapeHtml(L("moltbook.draft.deny"))}</button>
|
|
6999
7666
|
</div>
|
|
7000
7667
|
`
|
|
7001
7668
|
: `<p class="muted reply-composer__description">${escapeHtml(L("moltbook.draft.resolved"))}</p>`;
|
|
@@ -7043,6 +7710,29 @@ function renderA2ATaskDetail(detail, options = {}) {
|
|
|
7043
7710
|
const statusBadge = !enabled && detail.kind === "a2a_task_result"
|
|
7044
7711
|
? `<span class="eyebrow-pill eyebrow-pill--subtle">${escapeHtml(L(statusKey))}</span>`
|
|
7045
7712
|
: "";
|
|
7713
|
+
const viveworkerTask = detail.viveworker || {};
|
|
7714
|
+
const payment = viveworkerTask.payment || {};
|
|
7715
|
+
const priceLabel = payment.price
|
|
7716
|
+
? `${payment.price} USDC`
|
|
7717
|
+
: detail.paidDeliverable?.price
|
|
7718
|
+
? `${detail.paidDeliverable.price} USDC`
|
|
7719
|
+
: "";
|
|
7720
|
+
const paidDeliverableBlock = viveworkerTask.paidDeliverable || detail.paidDeliverable
|
|
7721
|
+
? `
|
|
7722
|
+
<div class="reply-composer__context">
|
|
7723
|
+
<span class="eyebrow-pill eyebrow-pill--quiet">${escapeHtml(L("a2a.task.paidDeliverable"))}</span>
|
|
7724
|
+
<div class="reply-composer__context-body">
|
|
7725
|
+
${viveworkerTask.requestedTier ? `<p><strong>${escapeHtml(L("a2a.task.requestedTier"))}</strong>: ${escapeHtml(viveworkerTask.requestedTier)}</p>` : ""}
|
|
7726
|
+
${viveworkerTask.requestedExecutor ? `<p><strong>${escapeHtml(L("a2a.task.requestedExecutor"))}</strong>: ${escapeHtml(viveworkerTask.requestedExecutor)}</p>` : ""}
|
|
7727
|
+
${viveworkerTask.requestedModel ? `<p><strong>${escapeHtml(L("a2a.task.requestedModel"))}</strong>: ${escapeHtml(viveworkerTask.requestedModel)}</p>` : ""}
|
|
7728
|
+
${viveworkerTask.deliverableType ? `<p><strong>${escapeHtml(L("a2a.task.deliverableType"))}</strong>: ${escapeHtml(viveworkerTask.deliverableType)}</p>` : ""}
|
|
7729
|
+
${priceLabel ? `<p><strong>${escapeHtml(L("a2a.task.price"))}</strong>: ${escapeHtml(priceLabel)}</p>` : ""}
|
|
7730
|
+
${payment.payTo ? `<p><strong>${escapeHtml(L("a2a.task.payTo"))}</strong>: <code>${escapeHtml(payment.payTo)}</code></p>` : ""}
|
|
7731
|
+
${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>` : ""}
|
|
7732
|
+
</div>
|
|
7733
|
+
</div>
|
|
7734
|
+
`
|
|
7735
|
+
: "";
|
|
7046
7736
|
|
|
7047
7737
|
// Show executor selector when "ask" mode is active and both CLIs are available.
|
|
7048
7738
|
const executors = state.session?.a2aExecutors || { codex: false, claude: false };
|
|
@@ -7084,6 +7774,7 @@ function renderA2ATaskDetail(detail, options = {}) {
|
|
|
7084
7774
|
<span class="eyebrow-pill eyebrow-pill--quiet">${escapeHtml(L("a2a.task.eyebrow"))}</span>
|
|
7085
7775
|
${statusBadge}
|
|
7086
7776
|
${callerLine}
|
|
7777
|
+
${paidDeliverableBlock}
|
|
7087
7778
|
<p class="muted reply-composer__description">${enabled ? escapeHtml(L("a2a.task.editHint")) : ""}</p>
|
|
7088
7779
|
</div>
|
|
7089
7780
|
<div class="reply-composer__instruction">
|
|
@@ -7818,6 +8509,8 @@ function renderTabButtons({ buttonClass, withIcons }) {
|
|
|
7818
8509
|
}
|
|
7819
8510
|
|
|
7820
8511
|
function bindShellInteractions() {
|
|
8512
|
+
bindScrollableContentRenderDeferral();
|
|
8513
|
+
|
|
7821
8514
|
for (const button of document.querySelectorAll("[data-tab]")) {
|
|
7822
8515
|
button.addEventListener("click", async () => {
|
|
7823
8516
|
await switchTab(button.dataset.tab);
|
|
@@ -8063,14 +8756,31 @@ function bindShellInteractions() {
|
|
|
8063
8756
|
});
|
|
8064
8757
|
}
|
|
8065
8758
|
|
|
8759
|
+
for (const button of document.querySelectorAll("[data-detail-retry]")) {
|
|
8760
|
+
button.addEventListener("click", async (event) => {
|
|
8761
|
+
event.preventDefault();
|
|
8762
|
+
event.stopPropagation();
|
|
8763
|
+
if (!state.currentItem) {
|
|
8764
|
+
return;
|
|
8765
|
+
}
|
|
8766
|
+
state.currentDetail = null;
|
|
8767
|
+
state.currentDetailLoading = false;
|
|
8768
|
+
state.detailLoadingItem = null;
|
|
8769
|
+
queueCurrentDetailLoad(state.currentItem);
|
|
8770
|
+
await renderShell();
|
|
8771
|
+
});
|
|
8772
|
+
}
|
|
8773
|
+
|
|
8066
8774
|
for (const button of document.querySelectorAll("[data-back-to-list]")) {
|
|
8067
8775
|
button.addEventListener("click", async () => {
|
|
8776
|
+
const nextTab = state.currentTab;
|
|
8068
8777
|
clearChoiceLocalDraftForItem(state.currentItem);
|
|
8069
8778
|
state.detailOpen = false;
|
|
8070
8779
|
state.pendingListScrollRestore = !isDesktopLayout() && Boolean(state.listScrollState);
|
|
8071
8780
|
clearPinnedDetailState();
|
|
8072
8781
|
syncCurrentItemUrl(null);
|
|
8073
8782
|
await renderShell();
|
|
8783
|
+
refreshPrimaryTabAfterNavigation(nextTab);
|
|
8074
8784
|
});
|
|
8075
8785
|
}
|
|
8076
8786
|
|
|
@@ -8118,15 +8828,23 @@ function bindShellInteractions() {
|
|
|
8118
8828
|
delete postBody.hazbaseReauth;
|
|
8119
8829
|
}
|
|
8120
8830
|
await apiPost(actionUrl, postBody);
|
|
8121
|
-
if (
|
|
8831
|
+
if (activeItem?.kind === "approval") {
|
|
8832
|
+
state.pendingActionUrls.delete(actionUrl);
|
|
8122
8833
|
pinActionOutcomeDetail(
|
|
8123
8834
|
activeItem,
|
|
8124
8835
|
buildActionOutcomeDetail({
|
|
8125
8836
|
kind: "approval",
|
|
8126
8837
|
title: state.currentDetail?.title,
|
|
8127
|
-
message: approvalOutcomeMessage(actionUrl, activeItem?.provider),
|
|
8838
|
+
message: approvalOutcomeMessage(actionUrl, state.currentDetail?.provider || activeItem?.provider),
|
|
8128
8839
|
})
|
|
8129
8840
|
);
|
|
8841
|
+
await renderShell();
|
|
8842
|
+
void refreshAuthenticatedState()
|
|
8843
|
+
.then(() => renderShell())
|
|
8844
|
+
.catch((error) => {
|
|
8845
|
+
console.debug?.("[approval-action-refresh-failed]", error?.message || String(error));
|
|
8846
|
+
});
|
|
8847
|
+
return;
|
|
8130
8848
|
}
|
|
8131
8849
|
await refreshAuthenticatedState();
|
|
8132
8850
|
if (!keepDetailOpen && !isDesktopLayout()) {
|
|
@@ -8141,6 +8859,14 @@ function bindShellInteractions() {
|
|
|
8141
8859
|
if (error?.errorKey === "hazbase-session-expired") {
|
|
8142
8860
|
await fetchHazbaseStatus();
|
|
8143
8861
|
}
|
|
8862
|
+
if (activeItem?.kind === "approval" && isAlreadyHandledApprovalError(error)) {
|
|
8863
|
+
const recovered = await recoverHandledApprovalDetail(activeItem);
|
|
8864
|
+
if (recovered) {
|
|
8865
|
+
pinActionOutcomeDetail(activeItem, recovered);
|
|
8866
|
+
await renderShell();
|
|
8867
|
+
return;
|
|
8868
|
+
}
|
|
8869
|
+
}
|
|
8144
8870
|
if (approvalFinalized) {
|
|
8145
8871
|
await refreshAuthenticatedState();
|
|
8146
8872
|
if (keepDetailOpen && activeItem?.kind === "approval") {
|
|
@@ -9021,7 +9747,9 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
9021
9747
|
labelCache.set(btn, btn.innerHTML);
|
|
9022
9748
|
btn.disabled = true;
|
|
9023
9749
|
if (btn.dataset.action === submittedAction) {
|
|
9024
|
-
btn.innerHTML = submittedAction === "approve"
|
|
9750
|
+
btn.innerHTML = submittedAction === "approve"
|
|
9751
|
+
? escapeHtml(L("moltbook.draft.submittingApprove"))
|
|
9752
|
+
: escapeHtml(L("moltbook.draft.submittingDeny"));
|
|
9025
9753
|
btn.classList.add("is-loading");
|
|
9026
9754
|
} else {
|
|
9027
9755
|
btn.classList.add("is-dimmed");
|
|
@@ -9239,8 +9967,43 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
9239
9967
|
for (const attachment of attachments) {
|
|
9240
9968
|
requestBody.append("image", attachment.file, attachment.name || attachment.file.name);
|
|
9241
9969
|
}
|
|
9242
|
-
const
|
|
9243
|
-
|
|
9970
|
+
const sentNotice = L(draft.mode === "plan" ? "reply.notice.sentPlan" : "reply.notice.sentDefault", { provider: providerDisplayName(replyProvider) });
|
|
9971
|
+
const renderOptimisticSent = () => {
|
|
9972
|
+
const currentDraft = getCompletionReplyDraft(token);
|
|
9973
|
+
if (!currentDraft.sending || currentDraft.text !== text) {
|
|
9974
|
+
return;
|
|
9975
|
+
}
|
|
9976
|
+
setCompletionReplyDraft(token, {
|
|
9977
|
+
text: "",
|
|
9978
|
+
sentText: text,
|
|
9979
|
+
attachments: currentDraft.attachments,
|
|
9980
|
+
mode: draft.mode,
|
|
9981
|
+
sending: false,
|
|
9982
|
+
error: "",
|
|
9983
|
+
notice: sentNotice,
|
|
9984
|
+
warning: null,
|
|
9985
|
+
confirmOverride: false,
|
|
9986
|
+
collapsedAfterSend: true,
|
|
9987
|
+
});
|
|
9988
|
+
renderShell().catch((renderError) => {
|
|
9989
|
+
console.warn("[completion-reply-optimistic-render]", renderError?.message || renderError);
|
|
9990
|
+
});
|
|
9991
|
+
};
|
|
9992
|
+
const optimisticSentTimer = setTimeout(renderOptimisticSent, COMPLETION_REPLY_OPTIMISTIC_SENT_MS);
|
|
9993
|
+
let replyResult = null;
|
|
9994
|
+
try {
|
|
9995
|
+
const replyKind = replyForm.dataset.replyKind || "completion";
|
|
9996
|
+
replyResult = await apiPost(
|
|
9997
|
+
`/api/items/${encodeURIComponent(replyKind)}/${encodeURIComponent(token)}/reply`,
|
|
9998
|
+
requestBody,
|
|
9999
|
+
{
|
|
10000
|
+
timeoutMs: COMPLETION_REPLY_SEND_TIMEOUT_MS,
|
|
10001
|
+
preferRelayError: true,
|
|
10002
|
+
},
|
|
10003
|
+
);
|
|
10004
|
+
} finally {
|
|
10005
|
+
clearTimeout(optimisticSentTimer);
|
|
10006
|
+
}
|
|
9244
10007
|
setCompletionReplyDraft(token, {
|
|
9245
10008
|
text: "",
|
|
9246
10009
|
sentText: text,
|
|
@@ -9248,14 +10011,51 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
9248
10011
|
mode: draft.mode,
|
|
9249
10012
|
sending: false,
|
|
9250
10013
|
error: "",
|
|
9251
|
-
notice:
|
|
10014
|
+
notice: sentNotice,
|
|
9252
10015
|
warning: null,
|
|
9253
10016
|
confirmOverride: false,
|
|
9254
10017
|
collapsedAfterSend: true,
|
|
9255
10018
|
});
|
|
9256
|
-
|
|
10019
|
+
if (replyResult?.ackTimeout === true) {
|
|
10020
|
+
console.info("[completion-reply] accepted after slow Codex ACK");
|
|
10021
|
+
}
|
|
10022
|
+
await renderShell();
|
|
10023
|
+
refreshAuthenticatedState()
|
|
10024
|
+
.then(renderShell)
|
|
10025
|
+
.catch((refreshError) => console.warn("[completion-reply-refresh]", refreshError?.message || refreshError));
|
|
10026
|
+
return;
|
|
9257
10027
|
} catch (error) {
|
|
10028
|
+
const optimisticDraft = getCompletionReplyDraft(token);
|
|
10029
|
+
if (
|
|
10030
|
+
error.errorKey === "request-timeout" &&
|
|
10031
|
+
optimisticDraft.collapsedAfterSend &&
|
|
10032
|
+
optimisticDraft.sentText === text
|
|
10033
|
+
) {
|
|
10034
|
+
refreshAuthenticatedState()
|
|
10035
|
+
.then(renderShell)
|
|
10036
|
+
.catch((refreshError) => console.warn("[completion-reply-refresh]", refreshError?.message || refreshError));
|
|
10037
|
+
return;
|
|
10038
|
+
}
|
|
9258
10039
|
if (error.errorKey === "completion-reply-thread-advanced") {
|
|
10040
|
+
if (completionReplyWarningMatchesSentText(error, text, attachments.length)) {
|
|
10041
|
+
setCompletionReplyDraft(token, {
|
|
10042
|
+
text: "",
|
|
10043
|
+
sentText: text,
|
|
10044
|
+
attachments: [],
|
|
10045
|
+
mode: draft.mode,
|
|
10046
|
+
sending: false,
|
|
10047
|
+
error: "",
|
|
10048
|
+
notice: L(draft.mode === "plan" ? "reply.notice.sentPlan" : "reply.notice.sentDefault", { provider: providerDisplayName(replyProvider) }),
|
|
10049
|
+
warning: null,
|
|
10050
|
+
confirmOverride: false,
|
|
10051
|
+
collapsedAfterSend: true,
|
|
10052
|
+
});
|
|
10053
|
+
await renderShell();
|
|
10054
|
+
refreshAuthenticatedState()
|
|
10055
|
+
.then(renderShell)
|
|
10056
|
+
.catch((refreshError) => console.warn("[completion-reply-refresh]", refreshError?.message || refreshError));
|
|
10057
|
+
return;
|
|
10058
|
+
}
|
|
9259
10059
|
setCompletionReplyDraft(token, {
|
|
9260
10060
|
text,
|
|
9261
10061
|
sentText: "",
|
|
@@ -9459,6 +10259,143 @@ function bindSharedUi(renderFn) {
|
|
|
9459
10259
|
}
|
|
9460
10260
|
}
|
|
9461
10261
|
|
|
10262
|
+
function bindScrollableContentRenderDeferral() {
|
|
10263
|
+
const mark = () => {
|
|
10264
|
+
markScrollableContentInteraction();
|
|
10265
|
+
};
|
|
10266
|
+
for (const el of document.querySelectorAll(SCROLLABLE_CONTENT_SELECTORS)) {
|
|
10267
|
+
el.addEventListener("scroll", mark, { passive: true });
|
|
10268
|
+
el.addEventListener("wheel", mark, { passive: true });
|
|
10269
|
+
el.addEventListener("touchstart", mark, { passive: true });
|
|
10270
|
+
el.addEventListener("touchmove", mark, { passive: true });
|
|
10271
|
+
el.addEventListener("pointerdown", mark);
|
|
10272
|
+
el.addEventListener("copy", mark);
|
|
10273
|
+
}
|
|
10274
|
+
}
|
|
10275
|
+
|
|
10276
|
+
function bindPartialListSurfaceInteractions(listSurface) {
|
|
10277
|
+
if (!listSurface || listSurface.dataset.partialListInteractionsBound === "true") {
|
|
10278
|
+
return;
|
|
10279
|
+
}
|
|
10280
|
+
listSurface.dataset.partialListInteractionsBound = "true";
|
|
10281
|
+
|
|
10282
|
+
const targetElement = (event) => {
|
|
10283
|
+
const target = event.target;
|
|
10284
|
+
return target instanceof Element ? target : target?.parentElement || null;
|
|
10285
|
+
};
|
|
10286
|
+
const threadSelectSelector = "[data-timeline-thread-select], [data-diff-thread-select], [data-completed-thread-select]";
|
|
10287
|
+
|
|
10288
|
+
listSurface.addEventListener("pointerdown", (event) => {
|
|
10289
|
+
if (targetElement(event)?.closest(threadSelectSelector)) {
|
|
10290
|
+
markThreadFilterInteraction();
|
|
10291
|
+
}
|
|
10292
|
+
});
|
|
10293
|
+
listSurface.addEventListener("focusin", (event) => {
|
|
10294
|
+
if (targetElement(event)?.closest(threadSelectSelector)) {
|
|
10295
|
+
markThreadFilterInteraction();
|
|
10296
|
+
}
|
|
10297
|
+
});
|
|
10298
|
+
listSurface.addEventListener("focusout", (event) => {
|
|
10299
|
+
if (targetElement(event)?.closest(threadSelectSelector)) {
|
|
10300
|
+
clearThreadFilterInteraction();
|
|
10301
|
+
}
|
|
10302
|
+
});
|
|
10303
|
+
|
|
10304
|
+
listSurface.addEventListener("change", async (event) => {
|
|
10305
|
+
const target = targetElement(event);
|
|
10306
|
+
const timelineSelect = target?.closest("[data-timeline-thread-select]");
|
|
10307
|
+
const diffSelect = target?.closest("[data-diff-thread-select]");
|
|
10308
|
+
const completedSelect = target?.closest("[data-completed-thread-select]");
|
|
10309
|
+
if (!timelineSelect && !diffSelect && !completedSelect) {
|
|
10310
|
+
return;
|
|
10311
|
+
}
|
|
10312
|
+
clearThreadFilterInteraction();
|
|
10313
|
+
if (timelineSelect) {
|
|
10314
|
+
state.timelineThreadFilter = timelineSelect.value || "all";
|
|
10315
|
+
state.timelineKindFilterOpen = false;
|
|
10316
|
+
} else if (diffSelect) {
|
|
10317
|
+
state.diffThreadFilter = diffSelect.value || "all";
|
|
10318
|
+
} else if (completedSelect) {
|
|
10319
|
+
state.completedThreadFilter = completedSelect.value || "all";
|
|
10320
|
+
}
|
|
10321
|
+
alignCurrentItemToVisibleEntries();
|
|
10322
|
+
await renderShell();
|
|
10323
|
+
});
|
|
10324
|
+
|
|
10325
|
+
listSurface.addEventListener("click", async (event) => {
|
|
10326
|
+
const target = targetElement(event);
|
|
10327
|
+
const threadSelect = target?.closest(threadSelectSelector);
|
|
10328
|
+
if (threadSelect) {
|
|
10329
|
+
markThreadFilterInteraction();
|
|
10330
|
+
return;
|
|
10331
|
+
}
|
|
10332
|
+
|
|
10333
|
+
const providerButton = target?.closest("[data-provider-filter]");
|
|
10334
|
+
if (providerButton) {
|
|
10335
|
+
event.preventDefault();
|
|
10336
|
+
const next = providerButton.dataset.providerFilter || "all";
|
|
10337
|
+
if (state.providerFilter === next) {
|
|
10338
|
+
return;
|
|
10339
|
+
}
|
|
10340
|
+
state.providerFilter = next;
|
|
10341
|
+
state.timelineThreadFilter = "all";
|
|
10342
|
+
state.timelineKindFilter = "all";
|
|
10343
|
+
state.timelineKindFilterOpen = false;
|
|
10344
|
+
state.completedThreadFilter = "all";
|
|
10345
|
+
state.diffThreadFilter = "all";
|
|
10346
|
+
alignCurrentItemToVisibleEntries();
|
|
10347
|
+
await renderShell();
|
|
10348
|
+
return;
|
|
10349
|
+
}
|
|
10350
|
+
|
|
10351
|
+
const kindToggle = target?.closest("[data-timeline-kind-filter-toggle]");
|
|
10352
|
+
if (kindToggle) {
|
|
10353
|
+
event.preventDefault();
|
|
10354
|
+
markThreadFilterInteraction();
|
|
10355
|
+
state.timelineKindFilterOpen = !state.timelineKindFilterOpen;
|
|
10356
|
+
await renderShell();
|
|
10357
|
+
return;
|
|
10358
|
+
}
|
|
10359
|
+
|
|
10360
|
+
const kindOption = target?.closest("[data-timeline-kind-filter-option]");
|
|
10361
|
+
if (kindOption) {
|
|
10362
|
+
event.preventDefault();
|
|
10363
|
+
clearThreadFilterInteraction();
|
|
10364
|
+
state.timelineKindFilter = kindOption.dataset.timelineKindFilterOption || "all";
|
|
10365
|
+
state.timelineKindFilterOpen = false;
|
|
10366
|
+
alignCurrentItemToVisibleEntries();
|
|
10367
|
+
await renderShell();
|
|
10368
|
+
return;
|
|
10369
|
+
}
|
|
10370
|
+
|
|
10371
|
+
const subtabButton = target?.closest("[data-inbox-subtab]");
|
|
10372
|
+
if (subtabButton) {
|
|
10373
|
+
const nextSubtab = subtabButton.dataset.inboxSubtab === "completed" ? "completed" : "pending";
|
|
10374
|
+
if (nextSubtab === state.inboxSubtab) {
|
|
10375
|
+
return;
|
|
10376
|
+
}
|
|
10377
|
+
state.inboxSubtab = nextSubtab;
|
|
10378
|
+
if (isDesktopLayout()) {
|
|
10379
|
+
alignCurrentItemToVisibleEntries();
|
|
10380
|
+
syncCurrentItemUrl(state.currentItem);
|
|
10381
|
+
}
|
|
10382
|
+
await renderShell();
|
|
10383
|
+
return;
|
|
10384
|
+
}
|
|
10385
|
+
|
|
10386
|
+
const itemButton = target?.closest("[data-open-item-kind][data-open-item-token]");
|
|
10387
|
+
if (itemButton) {
|
|
10388
|
+
openItem({
|
|
10389
|
+
kind: itemButton.dataset.openItemKind,
|
|
10390
|
+
token: itemButton.dataset.openItemToken,
|
|
10391
|
+
sourceTab: itemButton.dataset.sourceTab,
|
|
10392
|
+
sourceSubtab: itemButton.dataset.sourceSubtab,
|
|
10393
|
+
});
|
|
10394
|
+
await renderShell();
|
|
10395
|
+
}
|
|
10396
|
+
});
|
|
10397
|
+
}
|
|
10398
|
+
|
|
9462
10399
|
function openSettingsSubpage(page) {
|
|
9463
10400
|
if (!page) {
|
|
9464
10401
|
return;
|
|
@@ -9500,6 +10437,7 @@ async function switchTab(tab) {
|
|
|
9500
10437
|
syncCurrentItemUrl(state.currentItem);
|
|
9501
10438
|
}
|
|
9502
10439
|
await renderShell();
|
|
10440
|
+
refreshPrimaryTabAfterNavigation(tab);
|
|
9503
10441
|
if (tab === "settings") {
|
|
9504
10442
|
void fetchHazbaseStatus()
|
|
9505
10443
|
.then(() => {
|
|
@@ -9681,6 +10619,8 @@ function kindMeta(kind, item) {
|
|
|
9681
10619
|
return { label: L("common.assistantCommentary"), tone: "plan", icon: "assistant-commentary" };
|
|
9682
10620
|
case "assistant_final":
|
|
9683
10621
|
return { label: L("common.assistantFinal"), tone: "completion", icon: "assistant-final" };
|
|
10622
|
+
case "activity_status":
|
|
10623
|
+
return { label: L("server.title.activityStatus"), tone: "plan", icon: activityStatusIcon(item?.activityPhase) };
|
|
9684
10624
|
case "ambient_suggestions":
|
|
9685
10625
|
return { label: L("common.ambientSuggestions"), tone: "neutral", icon: "suggestions" };
|
|
9686
10626
|
case "approval":
|
|
@@ -9695,7 +10635,12 @@ function kindMeta(kind, item) {
|
|
|
9695
10635
|
case "diff_thread":
|
|
9696
10636
|
return { label: L("common.diff"), tone: "neutral", icon: "diff" };
|
|
9697
10637
|
case "file_event":
|
|
10638
|
+
if (normalizeClientText(item?.fileEventType || "") === "git") {
|
|
10639
|
+
return { label: L("common.commandEvent"), tone: "neutral", icon: "command" };
|
|
10640
|
+
}
|
|
9698
10641
|
return { label: L("common.fileEvent"), tone: "neutral", icon: "file-event" };
|
|
10642
|
+
case "command_event":
|
|
10643
|
+
return { label: L("common.commandEvent"), tone: "neutral", icon: "command" };
|
|
9699
10644
|
case "moltbook_reply":
|
|
9700
10645
|
return { label: L("common.moltbookReply"), tone: "neutral", icon: "moltbook-comment" };
|
|
9701
10646
|
case "moltbook_draft":
|
|
@@ -9713,6 +10658,24 @@ function kindMeta(kind, item) {
|
|
|
9713
10658
|
}
|
|
9714
10659
|
}
|
|
9715
10660
|
|
|
10661
|
+
function activityStatusIcon(phase) {
|
|
10662
|
+
switch (normalizeClientText(phase || "")) {
|
|
10663
|
+
case "reading":
|
|
10664
|
+
return "file-event";
|
|
10665
|
+
case "searching":
|
|
10666
|
+
return "filter";
|
|
10667
|
+
case "editing":
|
|
10668
|
+
return "diff";
|
|
10669
|
+
case "running_command":
|
|
10670
|
+
return "command";
|
|
10671
|
+
case "awaiting_approval":
|
|
10672
|
+
return "approval";
|
|
10673
|
+
case "thinking":
|
|
10674
|
+
default:
|
|
10675
|
+
return "pending";
|
|
10676
|
+
}
|
|
10677
|
+
}
|
|
10678
|
+
|
|
9716
10679
|
function renderTypePillContent(kindInfo) {
|
|
9717
10680
|
return `
|
|
9718
10681
|
<span class="type-pill__icon" aria-hidden="true">${renderIcon(kindInfo.icon)}</span>
|
|
@@ -9728,6 +10691,9 @@ function itemIntentText(kind, status = "pending", provider) {
|
|
|
9728
10691
|
if (kind === "file_event") {
|
|
9729
10692
|
return L("intent.fileEvent");
|
|
9730
10693
|
}
|
|
10694
|
+
if (kind === "command_event") {
|
|
10695
|
+
return L("intent.commandEvent");
|
|
10696
|
+
}
|
|
9731
10697
|
if (kind === "ambient_suggestions") {
|
|
9732
10698
|
return L("intent.ambientSuggestions");
|
|
9733
10699
|
}
|
|
@@ -9765,6 +10731,9 @@ function detailIntentText(detail) {
|
|
|
9765
10731
|
if (detail.kind === "file_event") {
|
|
9766
10732
|
return itemIntentText(detail.kind, "timeline", provider);
|
|
9767
10733
|
}
|
|
10734
|
+
if (detail.kind === "command_event") {
|
|
10735
|
+
return itemIntentText(detail.kind, "timeline", provider);
|
|
10736
|
+
}
|
|
9768
10737
|
if (detail.kind === "ambient_suggestions") {
|
|
9769
10738
|
return itemIntentText(detail.kind, "timeline", provider);
|
|
9770
10739
|
}
|
|
@@ -9838,6 +10807,8 @@ function fallbackSummaryForKind(kind, status, provider) {
|
|
|
9838
10807
|
return L("summary.diffThread");
|
|
9839
10808
|
case "file_event":
|
|
9840
10809
|
return L("summary.fileEvent", vars);
|
|
10810
|
+
case "command_event":
|
|
10811
|
+
return L("summary.commandEvent", vars);
|
|
9841
10812
|
case "ambient_suggestions":
|
|
9842
10813
|
return L("summary.ambientSuggestions", { count: 0, firstTitle: "", more: 0 });
|
|
9843
10814
|
case "user_message":
|
|
@@ -10009,6 +10980,22 @@ function hasDetailOverride(itemRef = state.currentItem) {
|
|
|
10009
10980
|
return Boolean(state.detailOverride && isSameItemRef(state.detailOverride, itemRef));
|
|
10010
10981
|
}
|
|
10011
10982
|
|
|
10983
|
+
function isAlreadyHandledApprovalError(error) {
|
|
10984
|
+
return error?.errorKey === "approval-not-found" || error?.errorKey === "approval-already-handled";
|
|
10985
|
+
}
|
|
10986
|
+
|
|
10987
|
+
async function recoverHandledApprovalDetail(itemRef) {
|
|
10988
|
+
try {
|
|
10989
|
+
await refreshAuthenticatedState();
|
|
10990
|
+
const detail = await hydrateDetailImages(
|
|
10991
|
+
await apiGet(`/api/items/${encodeURIComponent(itemRef.kind)}/${encodeURIComponent(itemRef.token)}`)
|
|
10992
|
+
);
|
|
10993
|
+
return detail?.kind === "approval" && detail.readOnly === true ? detail : null;
|
|
10994
|
+
} catch {
|
|
10995
|
+
return null;
|
|
10996
|
+
}
|
|
10997
|
+
}
|
|
10998
|
+
|
|
10012
10999
|
function shouldPreserveCurrentItem(itemRef = state.currentItem) {
|
|
10013
11000
|
return Boolean(itemRef && (hasLaunchItemIntent(itemRef) || hasDetailOverride(itemRef)));
|
|
10014
11001
|
}
|
|
@@ -10044,6 +11031,8 @@ function renderIcon(name) {
|
|
|
10044
11031
|
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>`;
|
|
10045
11032
|
case "file-event":
|
|
10046
11033
|
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>`;
|
|
11034
|
+
case "command":
|
|
11035
|
+
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>`;
|
|
10047
11036
|
case "diff":
|
|
10048
11037
|
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>`;
|
|
10049
11038
|
case "pending":
|
|
@@ -10076,6 +11065,8 @@ function renderIcon(name) {
|
|
|
10076
11065
|
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>`;
|
|
10077
11066
|
case "link":
|
|
10078
11067
|
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>`;
|
|
11068
|
+
case "external-link":
|
|
11069
|
+
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>`;
|
|
10079
11070
|
case "clip":
|
|
10080
11071
|
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>`;
|
|
10081
11072
|
case "moltbook-draft":
|
|
@@ -10346,32 +11337,107 @@ function pruneTimelineImageObjectUrlCache() {
|
|
|
10346
11337
|
}
|
|
10347
11338
|
}
|
|
10348
11339
|
|
|
11340
|
+
function withRequestTimeout(init, opts = {}) {
|
|
11341
|
+
const timeoutMs = Number(opts.timeoutMs) || 0;
|
|
11342
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || typeof AbortController === "undefined") {
|
|
11343
|
+
return { init, cleanup: () => {} };
|
|
11344
|
+
}
|
|
11345
|
+
if (init.signal?.aborted) {
|
|
11346
|
+
return { init, cleanup: () => {} };
|
|
11347
|
+
}
|
|
11348
|
+
const controller = new AbortController();
|
|
11349
|
+
let timer = null;
|
|
11350
|
+
const externalAbortHandler = init.signal
|
|
11351
|
+
? () => controller.abort(init.signal.reason)
|
|
11352
|
+
: null;
|
|
11353
|
+
if (init.signal && externalAbortHandler) {
|
|
11354
|
+
init.signal.addEventListener("abort", externalAbortHandler, { once: true });
|
|
11355
|
+
}
|
|
11356
|
+
timer = setTimeout(() => {
|
|
11357
|
+
const error = new Error("request-timeout");
|
|
11358
|
+
error.name = "AbortError";
|
|
11359
|
+
controller.abort(error);
|
|
11360
|
+
}, timeoutMs);
|
|
11361
|
+
return {
|
|
11362
|
+
init: { ...init, signal: controller.signal },
|
|
11363
|
+
cleanup: () => {
|
|
11364
|
+
if (timer) clearTimeout(timer);
|
|
11365
|
+
if (init.signal && externalAbortHandler) {
|
|
11366
|
+
init.signal.removeEventListener("abort", externalAbortHandler);
|
|
11367
|
+
}
|
|
11368
|
+
},
|
|
11369
|
+
};
|
|
11370
|
+
}
|
|
11371
|
+
|
|
11372
|
+
function normalizeRequestError(error, opts = {}) {
|
|
11373
|
+
if (error?.name === "AbortError" && Number(opts.timeoutMs) > 0) {
|
|
11374
|
+
const timeoutError = new Error(L("error.requestTimedOut"));
|
|
11375
|
+
timeoutError.code = 0;
|
|
11376
|
+
timeoutError.status = 0;
|
|
11377
|
+
timeoutError.errorKey = "request-timeout";
|
|
11378
|
+
return timeoutError;
|
|
11379
|
+
}
|
|
11380
|
+
return error;
|
|
11381
|
+
}
|
|
11382
|
+
|
|
10349
11383
|
async function apiGet(url, opts = {}) {
|
|
10350
11384
|
// routedFetch tries LAN first, then falls back to the relay tunnel when
|
|
10351
11385
|
// the phone is off-LAN. Returns a fetch-Response-compatible object so the
|
|
10352
11386
|
// rest of this function is identical to a plain `fetch()` call.
|
|
10353
|
-
const
|
|
11387
|
+
const timed = withRequestTimeout({
|
|
10354
11388
|
credentials: "same-origin",
|
|
10355
11389
|
headers: {
|
|
10356
11390
|
Accept: "application/json",
|
|
10357
11391
|
},
|
|
10358
11392
|
}, opts);
|
|
10359
|
-
|
|
10360
|
-
const
|
|
10361
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
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
|
+
throw error;
|
|
11402
|
+
}
|
|
11403
|
+
return await response.json();
|
|
11404
|
+
} catch (error) {
|
|
11405
|
+
throw normalizeRequestError(error, opts);
|
|
11406
|
+
} finally {
|
|
11407
|
+
timed.cleanup();
|
|
11408
|
+
}
|
|
11409
|
+
}
|
|
11410
|
+
|
|
11411
|
+
async function apiGetDirectLan(url, opts = {}) {
|
|
11412
|
+
const timed = withRequestTimeout({
|
|
11413
|
+
credentials: "same-origin",
|
|
11414
|
+
headers: {
|
|
11415
|
+
Accept: "application/json",
|
|
11416
|
+
},
|
|
11417
|
+
}, opts);
|
|
11418
|
+
try {
|
|
11419
|
+
const response = await fetch(url, timed.init);
|
|
11420
|
+
if (!response.ok) {
|
|
11421
|
+
const errorInfo = await readError(response);
|
|
11422
|
+
const error = new Error(errorInfo.message);
|
|
11423
|
+
error.code = response.status;
|
|
11424
|
+
error.status = response.status;
|
|
11425
|
+
error.errorKey = errorInfo.errorKey || "";
|
|
11426
|
+
throw error;
|
|
11427
|
+
}
|
|
11428
|
+
return await response.json();
|
|
11429
|
+
} catch (error) {
|
|
11430
|
+
throw normalizeRequestError(error, opts);
|
|
11431
|
+
} finally {
|
|
11432
|
+
timed.cleanup();
|
|
10366
11433
|
}
|
|
10367
|
-
return response.json();
|
|
10368
11434
|
}
|
|
10369
11435
|
|
|
10370
|
-
async function apiPost(url, body) {
|
|
11436
|
+
async function apiPost(url, body, opts = {}) {
|
|
10371
11437
|
const isFormDataBody = typeof FormData !== "undefined" && body instanceof FormData;
|
|
10372
11438
|
// Keep native FormData for LAN; routedFetch serializes it to multipart
|
|
10373
11439
|
// bytes only when the request has to travel through the remote relay.
|
|
10374
|
-
const
|
|
11440
|
+
const timed = withRequestTimeout({
|
|
10375
11441
|
method: "POST",
|
|
10376
11442
|
credentials: "same-origin",
|
|
10377
11443
|
headers: isFormDataBody
|
|
@@ -10383,17 +11449,24 @@ async function apiPost(url, body) {
|
|
|
10383
11449
|
Accept: "application/json",
|
|
10384
11450
|
},
|
|
10385
11451
|
body: isFormDataBody ? body : JSON.stringify(body || {}),
|
|
10386
|
-
});
|
|
10387
|
-
|
|
10388
|
-
const
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
11452
|
+
}, opts);
|
|
11453
|
+
try {
|
|
11454
|
+
const response = await routedFetch(url, timed.init, opts);
|
|
11455
|
+
if (!response.ok) {
|
|
11456
|
+
const errorInfo = await readError(response);
|
|
11457
|
+
const error = new Error(errorInfo.message);
|
|
11458
|
+
error.code = response.status;
|
|
11459
|
+
error.status = response.status;
|
|
11460
|
+
error.errorKey = errorInfo.errorKey || "";
|
|
11461
|
+
error.payload = errorInfo.payload ?? null;
|
|
11462
|
+
throw error;
|
|
11463
|
+
}
|
|
11464
|
+
return await response.json();
|
|
11465
|
+
} catch (error) {
|
|
11466
|
+
throw normalizeRequestError(error, opts);
|
|
11467
|
+
} finally {
|
|
11468
|
+
timed.cleanup();
|
|
10395
11469
|
}
|
|
10396
|
-
return response.json();
|
|
10397
11470
|
}
|
|
10398
11471
|
|
|
10399
11472
|
async function readError(response) {
|
|
@@ -10421,6 +11494,7 @@ function localizeApiError(value) {
|
|
|
10421
11494
|
"device-not-found": "error.deviceNotFound",
|
|
10422
11495
|
"web-push-disabled": "error.webPushDisabled",
|
|
10423
11496
|
"push-subscription-expired": "error.pushSubscriptionExpired",
|
|
11497
|
+
"request-timeout": "error.requestTimedOut",
|
|
10424
11498
|
"item-not-found": "error.itemNotFound",
|
|
10425
11499
|
"completion-reply-unavailable": "error.completionReplyUnavailable",
|
|
10426
11500
|
"completion-reply-thread-advanced": "error.completionReplyThreadAdvanced",
|