viveworker 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- 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/viveworker-bridge.mjs +1113 -65
- package/web/app.css +114 -1
- package/web/app.js +1129 -115
- package/web/build-id.js +1 -1
- package/web/i18n.js +123 -15
- package/web/index.html +1 -1
- package/web/remote-pairing/api-router.js +84 -0
- 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
|
|
|
@@ -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(() => {});
|
|
1247
1464
|
}
|
|
1248
1465
|
|
|
1249
|
-
|
|
1466
|
+
function latestTimelineEntryForClientEvent() {
|
|
1467
|
+
const entries = Array.isArray(state.timeline?.entries) ? state.timeline.entries : [];
|
|
1468
|
+
return entries.length > 0 ? entries[0] : null;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function newlyRenderedTimelineTokensForClientEvent(visible) {
|
|
1472
|
+
if (!visible) {
|
|
1473
|
+
return [];
|
|
1474
|
+
}
|
|
1475
|
+
const entries = Array.isArray(state.timeline?.entries) ? state.timeline.entries : [];
|
|
1476
|
+
const rendered = [];
|
|
1477
|
+
for (const entry of entries.slice(0, 20)) {
|
|
1478
|
+
const token = normalizeClientText(entry?.token || entry?.createdAtMs || "");
|
|
1479
|
+
const kind = normalizeClientText(entry?.kind || "");
|
|
1480
|
+
if (!token || !kind || reportedTimelineRenderTokens.has(token)) {
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
reportedTimelineRenderTokens.add(token);
|
|
1484
|
+
rendered.push({
|
|
1485
|
+
token,
|
|
1486
|
+
kind,
|
|
1487
|
+
createdAtMs: Number(entry?.createdAtMs) || 0,
|
|
1488
|
+
});
|
|
1489
|
+
if (rendered.length >= 5) {
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
if (reportedTimelineRenderTokens.size > 500) {
|
|
1494
|
+
const keep = new Set(entries.slice(0, 250).map((entry) => normalizeClientText(entry?.token || entry?.createdAtMs || "")).filter(Boolean));
|
|
1495
|
+
for (const token of reportedTimelineRenderTokens) {
|
|
1496
|
+
if (!keep.has(token)) {
|
|
1497
|
+
reportedTimelineRenderTokens.delete(token);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return rendered;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
function reportTimelineRendered(reason = "render") {
|
|
1505
|
+
if (!state.session?.authenticated) {
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
const latest = latestTimelineEntryForClientEvent();
|
|
1509
|
+
if (!latest) {
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
const latestToken = normalizeClientText(latest.token || latest.createdAtMs || "");
|
|
1513
|
+
const latestKind = normalizeClientText(latest.kind || "");
|
|
1514
|
+
if (!latestToken || !latestKind) {
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
const desktop = isDesktopLayout();
|
|
1518
|
+
const visible = state.currentTab === "timeline" && (desktop || !state.detailOpen);
|
|
1519
|
+
const telemetry = getRoutingTelemetry() || {};
|
|
1520
|
+
const route = normalizeClientText(telemetry.lastRoute || "unknown");
|
|
1521
|
+
const renderedTokens = newlyRenderedTimelineTokensForClientEvent(visible);
|
|
1522
|
+
const reportKey = [
|
|
1523
|
+
latestKind,
|
|
1524
|
+
latestToken,
|
|
1525
|
+
state.currentTab,
|
|
1526
|
+
visible ? "visible" : "hidden",
|
|
1527
|
+
route,
|
|
1528
|
+
renderedTokens.map((item) => item.token).join(","),
|
|
1529
|
+
].join(":");
|
|
1530
|
+
if (lastTimelineRenderReportKey === reportKey) {
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
lastTimelineRenderReportKey = reportKey;
|
|
1534
|
+
|
|
1535
|
+
const send = () => {
|
|
1536
|
+
const latestNow = latestTimelineEntryForClientEvent();
|
|
1537
|
+
if (!latestNow) {
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
const telemetryNow = getRoutingTelemetry() || telemetry;
|
|
1541
|
+
apiPost(
|
|
1542
|
+
"/api/client-events",
|
|
1543
|
+
{
|
|
1544
|
+
type: "timeline-render",
|
|
1545
|
+
reason,
|
|
1546
|
+
latestToken: normalizeClientText(latestNow.token || latestNow.createdAtMs || ""),
|
|
1547
|
+
latestKind: normalizeClientText(latestNow.kind || ""),
|
|
1548
|
+
latestCreatedAtMs: Number(latestNow.createdAtMs) || 0,
|
|
1549
|
+
renderedTokens,
|
|
1550
|
+
entryCount: Array.isArray(state.timeline?.entries) ? state.timeline.entries.length : 0,
|
|
1551
|
+
currentTab: state.currentTab,
|
|
1552
|
+
visible,
|
|
1553
|
+
route: normalizeClientText(telemetryNow.lastRoute || route || "unknown"),
|
|
1554
|
+
clientAtMs: Date.now(),
|
|
1555
|
+
appBuildId: APP_BUILD_ID,
|
|
1556
|
+
},
|
|
1557
|
+
{
|
|
1558
|
+
timeoutMs: CLIENT_EVENT_REPORT_TIMEOUT_MS,
|
|
1559
|
+
probeLanWhileSticky: false,
|
|
1560
|
+
suppressRoutingStatus: true,
|
|
1561
|
+
},
|
|
1562
|
+
).catch(() => {});
|
|
1563
|
+
};
|
|
1564
|
+
|
|
1565
|
+
if (typeof requestAnimationFrame === "function") {
|
|
1566
|
+
requestAnimationFrame(send);
|
|
1567
|
+
} else {
|
|
1568
|
+
queueMicrotask(send);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
function timelinePayloadHasImages(payload) {
|
|
1573
|
+
const entries = Array.isArray(payload?.entries) ? payload.entries : [];
|
|
1574
|
+
return entries.some((entry) => Array.isArray(entry?.imageUrls) && entry.imageUrls.length > 0);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function renderAfterBackgroundDataRefresh() {
|
|
1578
|
+
ensureCurrentSelection();
|
|
1579
|
+
if (shouldDeferRenderForActiveInteraction()) {
|
|
1580
|
+
if (renderDeferredInteractionShellUpdates()) {
|
|
1581
|
+
reportTimelineRendered("deferred-refresh");
|
|
1582
|
+
}
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
renderCurrentSurface();
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
async function refreshPrimaryTabData(tab = state.currentTab) {
|
|
1589
|
+
if (tab === "inbox") {
|
|
1590
|
+
await refreshInbox();
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
if (tab === "timeline") {
|
|
1594
|
+
await refreshTimeline();
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
if (tab === "diff") {
|
|
1598
|
+
await refreshInboxDiff();
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
function refreshPrimaryTabAfterNavigation(tab = state.currentTab) {
|
|
1603
|
+
if (!state.session?.authenticated || tab === "settings") {
|
|
1604
|
+
return;
|
|
1605
|
+
}
|
|
1606
|
+
refreshPrimaryTabData(tab)
|
|
1607
|
+
.then(() => {
|
|
1608
|
+
if (state.currentTab !== tab || state.currentTab === "settings") {
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
renderAfterBackgroundDataRefresh();
|
|
1612
|
+
})
|
|
1613
|
+
.catch(() => {});
|
|
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" },
|
|
@@ -1569,6 +1937,8 @@ function timelineEntryMatchesKindFilter(entry, filterId) {
|
|
|
1569
1937
|
return kind === "ambient_suggestions";
|
|
1570
1938
|
case "files":
|
|
1571
1939
|
return kind === "file_event";
|
|
1940
|
+
case "commands":
|
|
1941
|
+
return kind === "command_event";
|
|
1572
1942
|
case "approvals":
|
|
1573
1943
|
return kind === "approval";
|
|
1574
1944
|
case "plans":
|
|
@@ -1847,6 +2217,7 @@ async function logout({ revokeCurrentDeviceTrust = false } = {}) {
|
|
|
1847
2217
|
}
|
|
1848
2218
|
|
|
1849
2219
|
function resetAuthenticatedState() {
|
|
2220
|
+
closeTimelineLiveStream();
|
|
1850
2221
|
state.session = null;
|
|
1851
2222
|
state.inbox = null;
|
|
1852
2223
|
// Reset the diff-loaded flag so the next sign-in shows the skeleton
|
|
@@ -1903,37 +2274,47 @@ async function revokeTrustedDevice(deviceId) {
|
|
|
1903
2274
|
* On a polling interval, that means a long line of code keeps snapping
|
|
1904
2275
|
* back to the start while the reader is mid-line.
|
|
1905
2276
|
*
|
|
1906
|
-
* `
|
|
2277
|
+
* `snapshotScrollableContentScrolls()` records each scrollable code/diff
|
|
2278
|
+
* block's scrollLeft
|
|
1907
2279
|
* keyed by its trimmed textContent (so the key survives a fresh render
|
|
1908
|
-
* regardless of position in the DOM tree). `
|
|
2280
|
+
* regardless of position in the DOM tree). `restoreScrollableContentScrolls()`
|
|
1909
2281
|
* walks the new DOM and restores any scrollLeft we still have a key for.
|
|
1910
2282
|
*
|
|
1911
2283
|
* Content-keyed matching is intentional: if the underlying code text
|
|
1912
2284
|
* changes mid-scroll, the new <pre> is logically different and we let it
|
|
1913
2285
|
* start at scrollLeft=0 rather than landing the reader somewhere unrelated.
|
|
1914
2286
|
*/
|
|
1915
|
-
|
|
2287
|
+
// Blocks the user can scroll horizontally — we preserve their position
|
|
2288
|
+
// across innerHTML rebuilds and defer background renders while the user is
|
|
2289
|
+
// actively scrolling/selecting them. <pre> for fenced code; <table> for
|
|
2290
|
+
// pipe-style markdown tables; `.detail-diff-viewer` for file diffs.
|
|
2291
|
+
const SCROLLABLE_CONTENT_SELECTORS = ".markdown pre, .markdown table, .detail-diff-viewer";
|
|
2292
|
+
|
|
2293
|
+
function snapshotScrollableContentScrolls() {
|
|
1916
2294
|
if (typeof document === "undefined") return null;
|
|
1917
|
-
const blocks = document.querySelectorAll(
|
|
2295
|
+
const blocks = document.querySelectorAll(SCROLLABLE_CONTENT_SELECTORS);
|
|
1918
2296
|
if (blocks.length === 0) return null;
|
|
1919
2297
|
const map = new Map();
|
|
1920
|
-
for (const
|
|
1921
|
-
const key =
|
|
2298
|
+
for (const el of blocks) {
|
|
2299
|
+
const key = el.textContent ? el.textContent.trim() : "";
|
|
1922
2300
|
if (!key) continue;
|
|
1923
|
-
if (
|
|
1924
|
-
map.set(key
|
|
2301
|
+
if (el.scrollLeft === 0 && el.scrollTop === 0) continue;
|
|
2302
|
+
map.set(`${el.tagName}:${key}`, {
|
|
2303
|
+
scrollLeft: el.scrollLeft,
|
|
2304
|
+
scrollTop: el.scrollTop,
|
|
2305
|
+
});
|
|
1925
2306
|
}
|
|
1926
2307
|
return map.size > 0 ? map : null;
|
|
1927
2308
|
}
|
|
1928
2309
|
|
|
1929
|
-
function
|
|
2310
|
+
function restoreScrollableContentScrolls(snapshot) {
|
|
1930
2311
|
if (!snapshot || typeof document === "undefined") return;
|
|
1931
|
-
for (const
|
|
1932
|
-
const key =
|
|
1933
|
-
const saved = key ? snapshot.get(key) : null;
|
|
2312
|
+
for (const el of document.querySelectorAll(SCROLLABLE_CONTENT_SELECTORS)) {
|
|
2313
|
+
const key = el.textContent ? el.textContent.trim() : "";
|
|
2314
|
+
const saved = key ? snapshot.get(`${el.tagName}:${key}`) : null;
|
|
1934
2315
|
if (!saved) continue;
|
|
1935
|
-
if (saved.scrollLeft)
|
|
1936
|
-
if (saved.scrollTop)
|
|
2316
|
+
if (saved.scrollLeft) el.scrollLeft = saved.scrollLeft;
|
|
2317
|
+
if (saved.scrollTop) el.scrollTop = saved.scrollTop;
|
|
1937
2318
|
}
|
|
1938
2319
|
}
|
|
1939
2320
|
|
|
@@ -1957,7 +2338,7 @@ async function renderShell() {
|
|
|
1957
2338
|
.filter(Boolean)
|
|
1958
2339
|
.join(" ");
|
|
1959
2340
|
|
|
1960
|
-
const
|
|
2341
|
+
const scrollableContentSnapshot = snapshotScrollableContentScrolls();
|
|
1961
2342
|
|
|
1962
2343
|
app.innerHTML = `
|
|
1963
2344
|
<div class="${shellClassName}">
|
|
@@ -1987,8 +2368,9 @@ async function renderShell() {
|
|
|
1987
2368
|
// Reapply any horizontal scroll the user dragged into a code block before
|
|
1988
2369
|
// this re-render. Done after the imperative scroll resets above so they
|
|
1989
2370
|
// can't fight each other.
|
|
1990
|
-
|
|
2371
|
+
restoreScrollableContentScrolls(scrollableContentSnapshot);
|
|
1991
2372
|
requestAnimationFrame(dismissBootSplash);
|
|
2373
|
+
reportTimelineRendered("shell");
|
|
1992
2374
|
}
|
|
1993
2375
|
|
|
1994
2376
|
function applyPendingDetailScrollReset() {
|
|
@@ -2037,6 +2419,53 @@ function applyPendingSettingsScrollRestore() {
|
|
|
2037
2419
|
});
|
|
2038
2420
|
}
|
|
2039
2421
|
|
|
2422
|
+
function renderDeferredInteractionShellUpdates() {
|
|
2423
|
+
if (typeof document === "undefined") {
|
|
2424
|
+
return false;
|
|
2425
|
+
}
|
|
2426
|
+
if (state.currentTab === "settings") {
|
|
2427
|
+
return false;
|
|
2428
|
+
}
|
|
2429
|
+
const desktop = isDesktopLayout();
|
|
2430
|
+
// On mobile detail screens the list is intentionally not mounted, so the
|
|
2431
|
+
// latest state will appear as soon as the user backs out. On desktop the
|
|
2432
|
+
// list and detail are side-by-side, so updating only the list keeps new
|
|
2433
|
+
// timeline/inbox cards flowing without disturbing the detail pane.
|
|
2434
|
+
if (!desktop && state.detailOpen) {
|
|
2435
|
+
return false;
|
|
2436
|
+
}
|
|
2437
|
+
if (isListSurfaceInteractionActive()) {
|
|
2438
|
+
return false;
|
|
2439
|
+
}
|
|
2440
|
+
const listSurface = document.querySelector("[data-list-surface]");
|
|
2441
|
+
if (!listSurface) {
|
|
2442
|
+
return false;
|
|
2443
|
+
}
|
|
2444
|
+
const scrollLeft = listSurface.scrollLeft;
|
|
2445
|
+
const scrollTop = listSurface.scrollTop;
|
|
2446
|
+
listSurface.innerHTML = renderListPanel({
|
|
2447
|
+
tab: state.currentTab,
|
|
2448
|
+
entries: listEntriesForTab(state.currentTab),
|
|
2449
|
+
desktop,
|
|
2450
|
+
});
|
|
2451
|
+
listSurface.scrollLeft = scrollLeft;
|
|
2452
|
+
listSurface.scrollTop = scrollTop;
|
|
2453
|
+
bindPartialListSurfaceInteractions(listSurface);
|
|
2454
|
+
reportTimelineRendered("deferred-list");
|
|
2455
|
+
return true;
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
function isListSurfaceInteractionActive() {
|
|
2459
|
+
if (state.threadFilterInteractionUntilMs > Date.now() || state.timelineKindFilterOpen) {
|
|
2460
|
+
return true;
|
|
2461
|
+
}
|
|
2462
|
+
if (typeof document === "undefined" || typeof Element === "undefined") {
|
|
2463
|
+
return false;
|
|
2464
|
+
}
|
|
2465
|
+
const activeElement = document.activeElement;
|
|
2466
|
+
return activeElement instanceof Element && Boolean(activeElement.closest("[data-list-surface]"));
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2040
2469
|
function currentViewportScrollY() {
|
|
2041
2470
|
return window.scrollY || window.pageYOffset || document.documentElement?.scrollTop || 0;
|
|
2042
2471
|
}
|
|
@@ -2049,11 +2478,40 @@ function clearThreadFilterInteraction() {
|
|
|
2049
2478
|
state.threadFilterInteractionUntilMs = 0;
|
|
2050
2479
|
}
|
|
2051
2480
|
|
|
2481
|
+
function markScrollableContentInteraction() {
|
|
2482
|
+
state.scrollableContentInteractionUntilMs = Date.now() + SCROLLABLE_CONTENT_INTERACTION_DEFER_MS;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
function selectionIntersectsScrollableContent() {
|
|
2486
|
+
if (typeof document === "undefined" || typeof Element === "undefined") {
|
|
2487
|
+
return false;
|
|
2488
|
+
}
|
|
2489
|
+
const selection = document.getSelection?.();
|
|
2490
|
+
if (!selection || selection.isCollapsed || selection.rangeCount === 0) {
|
|
2491
|
+
return false;
|
|
2492
|
+
}
|
|
2493
|
+
for (let index = 0; index < selection.rangeCount; index += 1) {
|
|
2494
|
+
const range = selection.getRangeAt(index);
|
|
2495
|
+
const node = range.commonAncestorContainer;
|
|
2496
|
+
const element = node instanceof Element ? node : node?.parentElement;
|
|
2497
|
+
if (element?.closest?.(SCROLLABLE_CONTENT_SELECTORS)) {
|
|
2498
|
+
return true;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
return false;
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2052
2504
|
function shouldDeferRenderForActiveInteraction() {
|
|
2053
2505
|
const activeElement = document.activeElement;
|
|
2054
2506
|
if (state.completionReplySheetToken) {
|
|
2055
2507
|
return true;
|
|
2056
2508
|
}
|
|
2509
|
+
if (state.scrollableContentInteractionUntilMs > Date.now()) {
|
|
2510
|
+
return true;
|
|
2511
|
+
}
|
|
2512
|
+
if (selectionIntersectsScrollableContent()) {
|
|
2513
|
+
return true;
|
|
2514
|
+
}
|
|
2057
2515
|
if (
|
|
2058
2516
|
activeElement instanceof HTMLTextAreaElement &&
|
|
2059
2517
|
activeElement.matches("[data-completion-reply-textarea]") &&
|
|
@@ -2269,6 +2727,25 @@ function normalizeCompletionReplyWarning(value) {
|
|
|
2269
2727
|
};
|
|
2270
2728
|
}
|
|
2271
2729
|
|
|
2730
|
+
function normalizeCompletionReplyCompareText(value) {
|
|
2731
|
+
return normalizeClientText(value)
|
|
2732
|
+
.replace(/\s+/gu, " ")
|
|
2733
|
+
.trim();
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
function completionReplyWarningMatchesSentText(error, text, attachmentCount = 0) {
|
|
2737
|
+
if (error?.errorKey !== "completion-reply-thread-advanced") {
|
|
2738
|
+
return false;
|
|
2739
|
+
}
|
|
2740
|
+
if (attachmentCount > 0) {
|
|
2741
|
+
return false;
|
|
2742
|
+
}
|
|
2743
|
+
const warning = normalizeCompletionReplyWarning(error?.payload?.warning);
|
|
2744
|
+
const warningText = normalizeCompletionReplyCompareText(warning?.summary || "");
|
|
2745
|
+
const sentText = normalizeCompletionReplyCompareText(text);
|
|
2746
|
+
return Boolean(warningText && sentText && warningText === sentText);
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2272
2749
|
function normalizeCompletionReplyAttachments(values) {
|
|
2273
2750
|
const rawValues = Array.isArray(values)
|
|
2274
2751
|
? values
|
|
@@ -2474,9 +2951,15 @@ async function fetchCurrentDetailForItem(itemRef = state.currentItem) {
|
|
|
2474
2951
|
if (hasDetailOverride(itemRef)) {
|
|
2475
2952
|
return state.detailOverride.detail;
|
|
2476
2953
|
}
|
|
2954
|
+
const detailUrl = `/api/items/${encodeURIComponent(itemRef.kind)}/${encodeURIComponent(itemRef.token)}`;
|
|
2477
2955
|
try {
|
|
2478
2956
|
const detail = await hydrateDetailImages(
|
|
2479
|
-
await apiGet(
|
|
2957
|
+
await apiGet(detailUrl, {
|
|
2958
|
+
timeoutMs: DETAIL_FETCH_TIMEOUT_MS,
|
|
2959
|
+
probeLanWhileSticky: true,
|
|
2960
|
+
stickyLanProbeTimeoutMs: DETAIL_STICKY_LAN_PROBE_TIMEOUT_MS,
|
|
2961
|
+
preferRelayError: true,
|
|
2962
|
+
})
|
|
2480
2963
|
);
|
|
2481
2964
|
if (hasLaunchItemIntent(itemRef)) {
|
|
2482
2965
|
state.launchItemIntent.status = "loaded";
|
|
@@ -2489,16 +2972,24 @@ async function fetchCurrentDetailForItem(itemRef = state.currentItem) {
|
|
|
2489
2972
|
renderPair();
|
|
2490
2973
|
return null;
|
|
2491
2974
|
}
|
|
2492
|
-
await
|
|
2975
|
+
await Promise.race([
|
|
2976
|
+
refreshInbox(),
|
|
2977
|
+
wait(DETAIL_REFRESH_FALLBACK_TIMEOUT_MS),
|
|
2978
|
+
]).catch(() => {});
|
|
2493
2979
|
try {
|
|
2494
2980
|
const detail = await hydrateDetailImages(
|
|
2495
|
-
await apiGet(
|
|
2981
|
+
await apiGet(detailUrl, {
|
|
2982
|
+
timeoutMs: DETAIL_FETCH_TIMEOUT_MS,
|
|
2983
|
+
probeLanWhileSticky: true,
|
|
2984
|
+
stickyLanProbeTimeoutMs: DETAIL_STICKY_LAN_PROBE_TIMEOUT_MS,
|
|
2985
|
+
preferRelayError: true,
|
|
2986
|
+
})
|
|
2496
2987
|
);
|
|
2497
2988
|
if (hasLaunchItemIntent(itemRef)) {
|
|
2498
2989
|
state.launchItemIntent.status = "loaded";
|
|
2499
2990
|
}
|
|
2500
2991
|
return detail;
|
|
2501
|
-
} catch {
|
|
2992
|
+
} catch (retryError) {
|
|
2502
2993
|
if (hasLaunchItemIntent(itemRef)) {
|
|
2503
2994
|
clearChoiceLocalDraftForItem(itemRef);
|
|
2504
2995
|
const fallbackDetail = buildLaunchItemFallbackDetail(itemRef);
|
|
@@ -2513,7 +3004,7 @@ async function fetchCurrentDetailForItem(itemRef = state.currentItem) {
|
|
|
2513
3004
|
if (!state.currentItem) {
|
|
2514
3005
|
return null;
|
|
2515
3006
|
}
|
|
2516
|
-
return
|
|
3007
|
+
return buildDetailLoadErrorDetail(itemRef, retryError || error);
|
|
2517
3008
|
}
|
|
2518
3009
|
}
|
|
2519
3010
|
}
|
|
@@ -2528,6 +3019,7 @@ function queueCurrentDetailLoad(itemRef = state.currentItem) {
|
|
|
2528
3019
|
|
|
2529
3020
|
const requestedItem = { ...itemRef };
|
|
2530
3021
|
const requestId = ++detailLoadSequence;
|
|
3022
|
+
const hadRenderableDetailAtStart = Boolean(renderableCurrentDetail(requestedItem));
|
|
2531
3023
|
state.currentDetailLoading = true;
|
|
2532
3024
|
state.detailLoadingItem = requestedItem;
|
|
2533
3025
|
|
|
@@ -2554,6 +3046,16 @@ function queueCurrentDetailLoad(itemRef = state.currentItem) {
|
|
|
2554
3046
|
}
|
|
2555
3047
|
state.currentDetailLoading = false;
|
|
2556
3048
|
state.detailLoadingItem = null;
|
|
3049
|
+
const completedInitialDetailLoad =
|
|
3050
|
+
!hadRenderableDetailAtStart &&
|
|
3051
|
+
Boolean(state.currentDetail) &&
|
|
3052
|
+
isSameItemRef(state.currentDetail, requestedItem) &&
|
|
3053
|
+
Boolean(state.currentItem) &&
|
|
3054
|
+
isSameItemRef(state.currentItem, requestedItem);
|
|
3055
|
+
if (shouldDeferRenderForActiveInteraction() && !completedInitialDetailLoad) {
|
|
3056
|
+
renderDeferredInteractionShellUpdates();
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
2557
3059
|
renderCurrentSurface();
|
|
2558
3060
|
});
|
|
2559
3061
|
}
|
|
@@ -2572,6 +3074,31 @@ function buildLaunchItemFallbackDetail(itemRef) {
|
|
|
2572
3074
|
};
|
|
2573
3075
|
}
|
|
2574
3076
|
|
|
3077
|
+
function buildDetailLoadErrorDetail(itemRef, error) {
|
|
3078
|
+
const snapshot = buildDetailLoadingSnapshot(itemRef) || {};
|
|
3079
|
+
const entry = selectedEntryForItem(itemRef);
|
|
3080
|
+
const item = entry?.item || {};
|
|
3081
|
+
const fallbackText = normalizeClientText(item.messageText || item.summary || item.title || "");
|
|
3082
|
+
const messageParts = [
|
|
3083
|
+
fallbackText ? `<p>${escapeHtml(fallbackText)}</p>` : "",
|
|
3084
|
+
].filter(Boolean);
|
|
3085
|
+
return {
|
|
3086
|
+
kind: itemRef.kind,
|
|
3087
|
+
token: itemRef.token,
|
|
3088
|
+
title: item.title || snapshot.title || kindMeta(itemRef.kind).label,
|
|
3089
|
+
threadId: item.threadId || "",
|
|
3090
|
+
threadLabel: item.threadLabel || snapshot.threadLabel || "",
|
|
3091
|
+
summary: item.summary || fallbackText || "",
|
|
3092
|
+
messageHtml: messageParts.join(""),
|
|
3093
|
+
provider: item.provider || normalizeProviderClient(item.provider) || "",
|
|
3094
|
+
createdAtMs: Number(item.createdAtMs || snapshot.createdAtMs) || Date.now(),
|
|
3095
|
+
readOnly: true,
|
|
3096
|
+
actions: [],
|
|
3097
|
+
loadError: true,
|
|
3098
|
+
loadErrorMessage: normalizeClientText(error?.message || String(error || "")),
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
|
|
2575
3102
|
function resolveLaunchFallbackMessage(kind, isHandled) {
|
|
2576
3103
|
if (kind === "approval") {
|
|
2577
3104
|
return isHandled ? L("error.approvalAlreadyHandled") : L("error.approvalNotFound");
|
|
@@ -2636,7 +3163,7 @@ function renderDesktopWorkspace(detail) {
|
|
|
2636
3163
|
(state.currentDetailLoading || !renderableCurrentDetail());
|
|
2637
3164
|
return `
|
|
2638
3165
|
<section class="desktop-workspace">
|
|
2639
|
-
<aside class="surface surface--list">
|
|
3166
|
+
<aside class="surface surface--list" data-list-surface>
|
|
2640
3167
|
${renderListPanel({
|
|
2641
3168
|
tab: state.currentTab,
|
|
2642
3169
|
entries,
|
|
@@ -2664,7 +3191,7 @@ function renderMobileWorkspace(detail) {
|
|
|
2664
3191
|
}
|
|
2665
3192
|
|
|
2666
3193
|
return `
|
|
2667
|
-
<section class="screen-block">
|
|
3194
|
+
<section class="screen-block" data-list-surface>
|
|
2668
3195
|
${renderListPanel({
|
|
2669
3196
|
tab: state.currentTab,
|
|
2670
3197
|
entries: listEntriesForTab(state.currentTab),
|
|
@@ -3245,15 +3772,17 @@ function renderTimelineEntry(entry, { desktop }) {
|
|
|
3245
3772
|
const isMoltbookOrA2A = item.kind === "moltbook_reply" || item.kind === "moltbook_draft" || item.kind === "a2a_task" || item.kind === "a2a_task_result" || item.kind === "thread_share";
|
|
3246
3773
|
const isMessageLike = TIMELINE_MESSAGE_KINDS.has(item.kind) || isMoltbookOrA2A;
|
|
3247
3774
|
const isFileEvent = item.kind === "file_event";
|
|
3775
|
+
const isCommandEvent = item.kind === "command_event";
|
|
3248
3776
|
const imageUrls = Array.isArray(item.imageUrls) ? item.imageUrls.filter(Boolean) : [];
|
|
3249
3777
|
const fileRefs = normalizeClientFileRefs(item.fileRefs);
|
|
3250
|
-
const primaryText = timelineEntryPrimaryText(item, entry.status, { isMessageLike, isFileEvent });
|
|
3251
|
-
const secondaryText = timelineEntrySecondaryText(item, entry.status, primaryText, { isMessageLike, isFileEvent });
|
|
3778
|
+
const primaryText = timelineEntryPrimaryText(item, entry.status, { isMessageLike, isFileEvent, isCommandEvent });
|
|
3779
|
+
const secondaryText = timelineEntrySecondaryText(item, entry.status, primaryText, { isMessageLike, isFileEvent, isCommandEvent });
|
|
3252
3780
|
const threadLabel = timelineEntryThreadLabel(item, isMessageLike);
|
|
3253
3781
|
const timestampLabel = formatTimelineTimestamp(item.createdAtMs);
|
|
3254
3782
|
const statusLabel = timelineEntryStatusLabel(item, isMessageLike);
|
|
3255
3783
|
const fileEventFileSummary = isFileEvent ? timelineFileEventFileSummary(item) : "";
|
|
3256
3784
|
const fileEventDiffStatsHtml = isFileEvent ? renderDiffEntryStatsHtml(item) : "";
|
|
3785
|
+
const toolEventCommand = timelineToolEventCommand(item);
|
|
3257
3786
|
|
|
3258
3787
|
return `
|
|
3259
3788
|
<button
|
|
@@ -3276,6 +3805,7 @@ function renderTimelineEntry(entry, { desktop }) {
|
|
|
3276
3805
|
${threadLabel ? `<p class="timeline-entry__thread">${escapeHtml(threadLabel)}</p>` : ""}
|
|
3277
3806
|
<div class="timeline-entry__body">
|
|
3278
3807
|
<p class="timeline-entry__title">${escapeHtml(primaryText)}</p>
|
|
3808
|
+
${toolEventCommand ? `<pre class="timeline-entry__command"><code>${escapeHtml(toolEventCommand)}</code></pre>` : ""}
|
|
3279
3809
|
${secondaryText ? `<p class="timeline-entry__summary">${escapeHtml(secondaryText)}</p>` : ""}
|
|
3280
3810
|
${
|
|
3281
3811
|
isFileEvent && fileEventFileSummary
|
|
@@ -3325,7 +3855,7 @@ function renderDiffEntry(entry) {
|
|
|
3325
3855
|
}
|
|
3326
3856
|
|
|
3327
3857
|
function timelineEntryStatusLabel(item, isMessageLike) {
|
|
3328
|
-
if (isMessageLike || item?.kind === "file_event") {
|
|
3858
|
+
if (isMessageLike || item?.kind === "file_event" || item?.kind === "command_event") {
|
|
3329
3859
|
return "";
|
|
3330
3860
|
}
|
|
3331
3861
|
|
|
@@ -3408,7 +3938,7 @@ function timelineEntryThreadLabel(item, isMessage) {
|
|
|
3408
3938
|
return threadLabel || "";
|
|
3409
3939
|
}
|
|
3410
3940
|
|
|
3411
|
-
function timelineEntryPrimaryText(item, status, { isMessageLike = false, isFileEvent = false } = {}) {
|
|
3941
|
+
function timelineEntryPrimaryText(item, status, { isMessageLike = false, isFileEvent = false, isCommandEvent = false } = {}) {
|
|
3412
3942
|
if (item?.kind === "ambient_suggestions") {
|
|
3413
3943
|
return item.summary || fallbackSummaryForKind(item.kind, status, item.provider);
|
|
3414
3944
|
}
|
|
@@ -3421,10 +3951,14 @@ function timelineEntryPrimaryText(item, status, { isMessageLike = false, isFileE
|
|
|
3421
3951
|
return fileEventTimelineCountLabel(item) || fallbackSummaryForKind(item.kind, status, item.provider);
|
|
3422
3952
|
}
|
|
3423
3953
|
|
|
3954
|
+
if (isCommandEvent) {
|
|
3955
|
+
return L("common.commandEvent");
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3424
3958
|
return timelineDisplayTitleWithoutThread(item, { allowFallbackSummary: true }) || L("common.untitledItem");
|
|
3425
3959
|
}
|
|
3426
3960
|
|
|
3427
|
-
function timelineEntrySecondaryText(item, status, primaryText, { isMessageLike = false, isFileEvent = false } = {}) {
|
|
3961
|
+
function timelineEntrySecondaryText(item, status, primaryText, { isMessageLike = false, isFileEvent = false, isCommandEvent = false } = {}) {
|
|
3428
3962
|
if (item?.kind === "ambient_suggestions") {
|
|
3429
3963
|
return "";
|
|
3430
3964
|
}
|
|
@@ -3442,6 +3976,10 @@ function timelineEntrySecondaryText(item, status, primaryText, { isMessageLike =
|
|
|
3442
3976
|
return "";
|
|
3443
3977
|
}
|
|
3444
3978
|
|
|
3979
|
+
if (isCommandEvent) {
|
|
3980
|
+
return "";
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3445
3983
|
const compactTitle = timelineDisplayTitleWithoutThread(item, { allowFallbackSummary: false });
|
|
3446
3984
|
return compactTitle ? summaryText : "";
|
|
3447
3985
|
}
|
|
@@ -3491,6 +4029,7 @@ function timelineGeneratedTitlePrefixes() {
|
|
|
3491
4029
|
kindMeta("assistant_commentary").label,
|
|
3492
4030
|
kindMeta("assistant_final").label,
|
|
3493
4031
|
L("common.fileEvent"),
|
|
4032
|
+
L("common.commandEvent"),
|
|
3494
4033
|
"Approval",
|
|
3495
4034
|
"Plan",
|
|
3496
4035
|
"Choice",
|
|
@@ -3499,6 +4038,7 @@ function timelineGeneratedTitlePrefixes() {
|
|
|
3499
4038
|
"Commentary",
|
|
3500
4039
|
"Final answer",
|
|
3501
4040
|
"Files",
|
|
4041
|
+
"Command",
|
|
3502
4042
|
"承認",
|
|
3503
4043
|
"プラン",
|
|
3504
4044
|
"選択",
|
|
@@ -3507,13 +4047,39 @@ function timelineGeneratedTitlePrefixes() {
|
|
|
3507
4047
|
"途中経過",
|
|
3508
4048
|
"最終回答",
|
|
3509
4049
|
"ファイル",
|
|
4050
|
+
"コマンド",
|
|
3510
4051
|
];
|
|
3511
4052
|
}
|
|
3512
4053
|
|
|
4054
|
+
function timelineCommandEventCommand(item) {
|
|
4055
|
+
return truncateUiText(firstMarkdownCodeFence(item?.messageText || "") || item?.summary || item?.title || "", 220);
|
|
4056
|
+
}
|
|
4057
|
+
|
|
4058
|
+
function shouldRenderFileEventCommand(item) {
|
|
4059
|
+
if (item?.kind !== "file_event") {
|
|
4060
|
+
return false;
|
|
4061
|
+
}
|
|
4062
|
+
return ["read", "search"].includes(normalizeClientText(item?.fileEventType || ""));
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
function timelineToolEventCommand(item) {
|
|
4066
|
+
if (item?.kind === "command_event") {
|
|
4067
|
+
return timelineCommandEventCommand(item);
|
|
4068
|
+
}
|
|
4069
|
+
if (!shouldRenderFileEventCommand(item)) {
|
|
4070
|
+
return "";
|
|
4071
|
+
}
|
|
4072
|
+
return truncateUiText(firstMarkdownCodeFence(item?.messageText || "") || item?.commandText || "", 220);
|
|
4073
|
+
}
|
|
4074
|
+
|
|
3513
4075
|
function fileEventDisplayLabel(fileEventType) {
|
|
3514
4076
|
switch (normalizeClientText(fileEventType || "")) {
|
|
3515
4077
|
case "read":
|
|
3516
4078
|
return L("fileEvent.read");
|
|
4079
|
+
case "search":
|
|
4080
|
+
return L("fileEvent.search");
|
|
4081
|
+
case "command":
|
|
4082
|
+
return L("fileEvent.command");
|
|
3517
4083
|
case "write":
|
|
3518
4084
|
return L("fileEvent.write");
|
|
3519
4085
|
case "create":
|
|
@@ -3536,6 +4102,10 @@ function fileEventTimelineCountLabel(item) {
|
|
|
3536
4102
|
switch (fileEventType) {
|
|
3537
4103
|
case "read":
|
|
3538
4104
|
return L("fileEvent.timeline.read", { count });
|
|
4105
|
+
case "search":
|
|
4106
|
+
return L("fileEvent.timeline.search", { count });
|
|
4107
|
+
case "command":
|
|
4108
|
+
return L("fileEvent.timeline.command", { count });
|
|
3539
4109
|
case "write":
|
|
3540
4110
|
return L("fileEvent.timeline.write", { count });
|
|
3541
4111
|
case "create":
|
|
@@ -3584,7 +4154,7 @@ function diffThreadFilesSummary(item) {
|
|
|
3584
4154
|
.map((entry) => fileChangeEntryLabel(entry))
|
|
3585
4155
|
.filter(Boolean);
|
|
3586
4156
|
if (labels.length === 0) {
|
|
3587
|
-
return "";
|
|
4157
|
+
return truncateUiText(firstMarkdownCodeFence(item?.messageText || "") || item?.summary || "");
|
|
3588
4158
|
}
|
|
3589
4159
|
const visibleLabels = labels.slice(0, 3);
|
|
3590
4160
|
const hiddenCount = labels.length - visibleLabels.length;
|
|
@@ -5222,6 +5792,15 @@ function renderSettingsAutoPilotSuggestion({ lane, count }) {
|
|
|
5222
5792
|
`;
|
|
5223
5793
|
}
|
|
5224
5794
|
|
|
5795
|
+
function renderSettingsExternalLink({ href, label }) {
|
|
5796
|
+
return `
|
|
5797
|
+
<a class="settings-external-link" href="${escapeHtml(href)}" target="_blank" rel="noopener noreferrer" style="display:inline-flex;align-items:center;justify-content:flex-end;gap:.32rem;text-decoration:none;color:#8fd7ff;max-width:100%;">
|
|
5798
|
+
<span class="settings-external-link__label">${escapeHtml(label)}</span>
|
|
5799
|
+
<span class="settings-external-link__icon" aria-hidden="true" style="display:inline-flex;width:.86rem;height:.86rem;flex:0 0 auto;">${renderIcon("external-link")}</span>
|
|
5800
|
+
</a>
|
|
5801
|
+
`.trim();
|
|
5802
|
+
}
|
|
5803
|
+
|
|
5225
5804
|
function renderSettingsMoltbookPage(context) {
|
|
5226
5805
|
const scout = context.moltbookScout;
|
|
5227
5806
|
if (!scout?.enabled) {
|
|
@@ -5236,10 +5815,16 @@ function renderSettingsMoltbookPage(context) {
|
|
|
5236
5815
|
renderSettingsInfoRow(L("settings.row.moltbookBatchTopScore"), String(scout.batch.topScore)),
|
|
5237
5816
|
renderSettingsInfoRow(L("settings.row.moltbookBatchRemaining"), `${Math.floor(scout.batch.remainingSeconds / 60)}:${String(scout.batch.remainingSeconds % 60).padStart(2, "0")}`),
|
|
5238
5817
|
] : [];
|
|
5239
|
-
const
|
|
5818
|
+
const accountProfileLink = scout.account?.name && scout.account?.profileUrl
|
|
5819
|
+
? renderSettingsExternalLink({
|
|
5820
|
+
href: scout.account.profileUrl,
|
|
5821
|
+
label: scout.account.name,
|
|
5822
|
+
})
|
|
5823
|
+
: "";
|
|
5824
|
+
const accountRow = accountProfileLink
|
|
5240
5825
|
? renderSettingsInfoRow(
|
|
5241
5826
|
L("settings.row.moltbookAccount"),
|
|
5242
|
-
|
|
5827
|
+
accountProfileLink,
|
|
5243
5828
|
{ rawValue: true }
|
|
5244
5829
|
)
|
|
5245
5830
|
: null;
|
|
@@ -5310,7 +5895,10 @@ function renderSettingsA2aRelayPage(context) {
|
|
|
5310
5895
|
? L("settings.a2aRelay.status.polling")
|
|
5311
5896
|
: L("settings.a2aRelay.status.disconnected");
|
|
5312
5897
|
const profileUrl = `${relay.relayUrl}/u/${relay.userId}`;
|
|
5313
|
-
const userIdLink =
|
|
5898
|
+
const userIdLink = renderSettingsExternalLink({
|
|
5899
|
+
href: profileUrl,
|
|
5900
|
+
label: relay.userId,
|
|
5901
|
+
});
|
|
5314
5902
|
const relayHost = (() => { try { return new URL(relay.relayUrl).host; } catch { return relay.relayUrl; } })();
|
|
5315
5903
|
const publicChecked = relay.acceptPublicTasks === true;
|
|
5316
5904
|
|
|
@@ -6335,6 +6923,7 @@ function renderStandardDetailDesktop(detail) {
|
|
|
6335
6923
|
<div class="detail-shell">
|
|
6336
6924
|
${renderDetailMetaRow(detail, kindInfo)}
|
|
6337
6925
|
<h2 class="detail-title detail-title--desktop">${renderDetailTitle(detail)}</h2>
|
|
6926
|
+
${renderDetailLoadErrorNotice(detail)}
|
|
6338
6927
|
${detail.readOnly || detail.kind === "approval" || detail.kind === "moltbook_draft" || detail.kind === "moltbook_reply" || detail.kind === "thread_share" ? "" : renderDetailLead(detail, kindInfo)}
|
|
6339
6928
|
${renderPreviousContextCard(detail)}
|
|
6340
6929
|
${renderAutoPilotManualReview(detail)}
|
|
@@ -6355,6 +6944,7 @@ function renderStandardDetailDesktop(detail) {
|
|
|
6355
6944
|
</section>
|
|
6356
6945
|
`
|
|
6357
6946
|
}
|
|
6947
|
+
${renderCommandEventDetail(detail)}
|
|
6358
6948
|
${renderClaudePlanSection(detail)}
|
|
6359
6949
|
${renderClaudeQuestionSection(detail)}
|
|
6360
6950
|
${renderDetailImageGallery(detail)}
|
|
@@ -6377,6 +6967,7 @@ function renderStandardDetailMobile(detail) {
|
|
|
6377
6967
|
<div class="detail-shell detail-shell--mobile">
|
|
6378
6968
|
<div class="mobile-detail-scroll mobile-detail-scroll--detail">
|
|
6379
6969
|
${renderDetailMetaRow(detail, kindInfo, { mobile: true })}
|
|
6970
|
+
${renderDetailLoadErrorNotice(detail, { mobile: true })}
|
|
6380
6971
|
${renderPreviousContextCard(detail, { mobile: true })}
|
|
6381
6972
|
${renderAutoPilotManualReview(detail, { mobile: true })}
|
|
6382
6973
|
${renderInterruptedDetailNotice(detail, { mobile: true })}
|
|
@@ -6397,6 +6988,7 @@ function renderStandardDetailMobile(detail) {
|
|
|
6397
6988
|
</section>
|
|
6398
6989
|
`
|
|
6399
6990
|
}
|
|
6991
|
+
${renderCommandEventDetail(detail, { mobile: true })}
|
|
6400
6992
|
${renderClaudePlanSection(detail, { mobile: true })}
|
|
6401
6993
|
${renderClaudeQuestionSection(detail, { mobile: true })}
|
|
6402
6994
|
${renderDetailImageGallery(detail, { mobile: true })}
|
|
@@ -6439,6 +7031,46 @@ function renderAmbientSuggestionsSection(detail, options = {}) {
|
|
|
6439
7031
|
`;
|
|
6440
7032
|
}
|
|
6441
7033
|
|
|
7034
|
+
function renderCommandEventDetail(detail, options = {}) {
|
|
7035
|
+
if (detail?.kind !== "command_event" && !shouldRenderFileEventCommand(detail)) {
|
|
7036
|
+
return "";
|
|
7037
|
+
}
|
|
7038
|
+
const commandText = normalizeClientText(detail?.commandText || "") || firstMarkdownCodeFence(detail?.messageText || "");
|
|
7039
|
+
if (!commandText) {
|
|
7040
|
+
return "";
|
|
7041
|
+
}
|
|
7042
|
+
return `
|
|
7043
|
+
<section class="detail-card detail-card--command ${options.mobile ? "detail-card--mobile" : ""}">
|
|
7044
|
+
<div class="detail-files-card__header">
|
|
7045
|
+
<span class="detail-files-card__icon" aria-hidden="true">${renderIcon("command")}</span>
|
|
7046
|
+
<span>${escapeHtml(L("common.commandEvent"))}</span>
|
|
7047
|
+
</div>
|
|
7048
|
+
<pre class="detail-command-block"><code>${escapeHtml(commandText)}</code></pre>
|
|
7049
|
+
</section>
|
|
7050
|
+
`;
|
|
7051
|
+
}
|
|
7052
|
+
|
|
7053
|
+
function renderDetailLoadErrorNotice(detail, options = {}) {
|
|
7054
|
+
if (detail?.loadError !== true) {
|
|
7055
|
+
return "";
|
|
7056
|
+
}
|
|
7057
|
+
const errorText = normalizeClientText(detail.loadErrorMessage || "");
|
|
7058
|
+
return `
|
|
7059
|
+
<section class="detail-card detail-card--body ${options.mobile ? "detail-card--mobile" : ""}">
|
|
7060
|
+
<div class="detail-body markdown">
|
|
7061
|
+
<p><strong>${escapeHtml(L("detail.loadFailedTitle"))}</strong></p>
|
|
7062
|
+
<p>${escapeHtml(L("detail.loadFailedCopy"))}</p>
|
|
7063
|
+
${errorText ? `<p class="muted">${escapeHtml(errorText)}</p>` : ""}
|
|
7064
|
+
</div>
|
|
7065
|
+
<div class="actions actions--stack">
|
|
7066
|
+
<button class="secondary secondary--wide" type="button" data-detail-retry>
|
|
7067
|
+
${escapeHtml(L("detail.loadRetry"))}
|
|
7068
|
+
</button>
|
|
7069
|
+
</div>
|
|
7070
|
+
</section>
|
|
7071
|
+
`;
|
|
7072
|
+
}
|
|
7073
|
+
|
|
6442
7074
|
function renderAmbientSuggestionCard(detail, suggestion, index) {
|
|
6443
7075
|
const copyKey = ambientSuggestionCopyKey(detail?.token || "", suggestion?.id || "", index);
|
|
6444
7076
|
const copyStatus = state.ambientSuggestionCopyState?.key === copyKey
|
|
@@ -6471,7 +7103,7 @@ function renderAmbientSuggestionCard(detail, suggestion, index) {
|
|
|
6471
7103
|
}
|
|
6472
7104
|
|
|
6473
7105
|
function renderDetailPlainIntro(detail, options = {}) {
|
|
6474
|
-
if (!["approval", "diff_thread", "file_event"].includes(detail?.kind || "")) {
|
|
7106
|
+
if (!["approval", "diff_thread", "file_event", "command_event"].includes(detail?.kind || "")) {
|
|
6475
7107
|
return "";
|
|
6476
7108
|
}
|
|
6477
7109
|
const ak = normalizeClientText(detail?.approvalKind || "");
|
|
@@ -7043,6 +7675,29 @@ function renderA2ATaskDetail(detail, options = {}) {
|
|
|
7043
7675
|
const statusBadge = !enabled && detail.kind === "a2a_task_result"
|
|
7044
7676
|
? `<span class="eyebrow-pill eyebrow-pill--subtle">${escapeHtml(L(statusKey))}</span>`
|
|
7045
7677
|
: "";
|
|
7678
|
+
const viveworkerTask = detail.viveworker || {};
|
|
7679
|
+
const payment = viveworkerTask.payment || {};
|
|
7680
|
+
const priceLabel = payment.price
|
|
7681
|
+
? `${payment.price} USDC`
|
|
7682
|
+
: detail.paidDeliverable?.price
|
|
7683
|
+
? `${detail.paidDeliverable.price} USDC`
|
|
7684
|
+
: "";
|
|
7685
|
+
const paidDeliverableBlock = viveworkerTask.paidDeliverable || detail.paidDeliverable
|
|
7686
|
+
? `
|
|
7687
|
+
<div class="reply-composer__context">
|
|
7688
|
+
<span class="eyebrow-pill eyebrow-pill--quiet">${escapeHtml(L("a2a.task.paidDeliverable"))}</span>
|
|
7689
|
+
<div class="reply-composer__context-body">
|
|
7690
|
+
${viveworkerTask.requestedTier ? `<p><strong>${escapeHtml(L("a2a.task.requestedTier"))}</strong>: ${escapeHtml(viveworkerTask.requestedTier)}</p>` : ""}
|
|
7691
|
+
${viveworkerTask.requestedExecutor ? `<p><strong>${escapeHtml(L("a2a.task.requestedExecutor"))}</strong>: ${escapeHtml(viveworkerTask.requestedExecutor)}</p>` : ""}
|
|
7692
|
+
${viveworkerTask.requestedModel ? `<p><strong>${escapeHtml(L("a2a.task.requestedModel"))}</strong>: ${escapeHtml(viveworkerTask.requestedModel)}</p>` : ""}
|
|
7693
|
+
${viveworkerTask.deliverableType ? `<p><strong>${escapeHtml(L("a2a.task.deliverableType"))}</strong>: ${escapeHtml(viveworkerTask.deliverableType)}</p>` : ""}
|
|
7694
|
+
${priceLabel ? `<p><strong>${escapeHtml(L("a2a.task.price"))}</strong>: ${escapeHtml(priceLabel)}</p>` : ""}
|
|
7695
|
+
${payment.payTo ? `<p><strong>${escapeHtml(L("a2a.task.payTo"))}</strong>: <code>${escapeHtml(payment.payTo)}</code></p>` : ""}
|
|
7696
|
+
${detail.paidDeliverable?.url ? `<p><strong>${escapeHtml(L("a2a.task.unlockUrl"))}</strong>: <a href="${escapeHtml(detail.paidDeliverable.url)}" target="_blank" rel="noopener">${escapeHtml(detail.paidDeliverable.url)}</a></p>` : ""}
|
|
7697
|
+
</div>
|
|
7698
|
+
</div>
|
|
7699
|
+
`
|
|
7700
|
+
: "";
|
|
7046
7701
|
|
|
7047
7702
|
// Show executor selector when "ask" mode is active and both CLIs are available.
|
|
7048
7703
|
const executors = state.session?.a2aExecutors || { codex: false, claude: false };
|
|
@@ -7084,6 +7739,7 @@ function renderA2ATaskDetail(detail, options = {}) {
|
|
|
7084
7739
|
<span class="eyebrow-pill eyebrow-pill--quiet">${escapeHtml(L("a2a.task.eyebrow"))}</span>
|
|
7085
7740
|
${statusBadge}
|
|
7086
7741
|
${callerLine}
|
|
7742
|
+
${paidDeliverableBlock}
|
|
7087
7743
|
<p class="muted reply-composer__description">${enabled ? escapeHtml(L("a2a.task.editHint")) : ""}</p>
|
|
7088
7744
|
</div>
|
|
7089
7745
|
<div class="reply-composer__instruction">
|
|
@@ -7818,6 +8474,8 @@ function renderTabButtons({ buttonClass, withIcons }) {
|
|
|
7818
8474
|
}
|
|
7819
8475
|
|
|
7820
8476
|
function bindShellInteractions() {
|
|
8477
|
+
bindScrollableContentRenderDeferral();
|
|
8478
|
+
|
|
7821
8479
|
for (const button of document.querySelectorAll("[data-tab]")) {
|
|
7822
8480
|
button.addEventListener("click", async () => {
|
|
7823
8481
|
await switchTab(button.dataset.tab);
|
|
@@ -8063,14 +8721,31 @@ function bindShellInteractions() {
|
|
|
8063
8721
|
});
|
|
8064
8722
|
}
|
|
8065
8723
|
|
|
8724
|
+
for (const button of document.querySelectorAll("[data-detail-retry]")) {
|
|
8725
|
+
button.addEventListener("click", async (event) => {
|
|
8726
|
+
event.preventDefault();
|
|
8727
|
+
event.stopPropagation();
|
|
8728
|
+
if (!state.currentItem) {
|
|
8729
|
+
return;
|
|
8730
|
+
}
|
|
8731
|
+
state.currentDetail = null;
|
|
8732
|
+
state.currentDetailLoading = false;
|
|
8733
|
+
state.detailLoadingItem = null;
|
|
8734
|
+
queueCurrentDetailLoad(state.currentItem);
|
|
8735
|
+
await renderShell();
|
|
8736
|
+
});
|
|
8737
|
+
}
|
|
8738
|
+
|
|
8066
8739
|
for (const button of document.querySelectorAll("[data-back-to-list]")) {
|
|
8067
8740
|
button.addEventListener("click", async () => {
|
|
8741
|
+
const nextTab = state.currentTab;
|
|
8068
8742
|
clearChoiceLocalDraftForItem(state.currentItem);
|
|
8069
8743
|
state.detailOpen = false;
|
|
8070
8744
|
state.pendingListScrollRestore = !isDesktopLayout() && Boolean(state.listScrollState);
|
|
8071
8745
|
clearPinnedDetailState();
|
|
8072
8746
|
syncCurrentItemUrl(null);
|
|
8073
8747
|
await renderShell();
|
|
8748
|
+
refreshPrimaryTabAfterNavigation(nextTab);
|
|
8074
8749
|
});
|
|
8075
8750
|
}
|
|
8076
8751
|
|
|
@@ -8118,7 +8793,8 @@ function bindShellInteractions() {
|
|
|
8118
8793
|
delete postBody.hazbaseReauth;
|
|
8119
8794
|
}
|
|
8120
8795
|
await apiPost(actionUrl, postBody);
|
|
8121
|
-
if (
|
|
8796
|
+
if (activeItem?.kind === "approval") {
|
|
8797
|
+
state.pendingActionUrls.delete(actionUrl);
|
|
8122
8798
|
pinActionOutcomeDetail(
|
|
8123
8799
|
activeItem,
|
|
8124
8800
|
buildActionOutcomeDetail({
|
|
@@ -8127,6 +8803,13 @@ function bindShellInteractions() {
|
|
|
8127
8803
|
message: approvalOutcomeMessage(actionUrl, activeItem?.provider),
|
|
8128
8804
|
})
|
|
8129
8805
|
);
|
|
8806
|
+
await renderShell();
|
|
8807
|
+
void refreshAuthenticatedState()
|
|
8808
|
+
.then(() => renderShell())
|
|
8809
|
+
.catch((error) => {
|
|
8810
|
+
console.debug?.("[approval-action-refresh-failed]", error?.message || String(error));
|
|
8811
|
+
});
|
|
8812
|
+
return;
|
|
8130
8813
|
}
|
|
8131
8814
|
await refreshAuthenticatedState();
|
|
8132
8815
|
if (!keepDetailOpen && !isDesktopLayout()) {
|
|
@@ -8141,6 +8824,14 @@ function bindShellInteractions() {
|
|
|
8141
8824
|
if (error?.errorKey === "hazbase-session-expired") {
|
|
8142
8825
|
await fetchHazbaseStatus();
|
|
8143
8826
|
}
|
|
8827
|
+
if (activeItem?.kind === "approval" && isAlreadyHandledApprovalError(error)) {
|
|
8828
|
+
const recovered = await recoverHandledApprovalDetail(activeItem);
|
|
8829
|
+
if (recovered) {
|
|
8830
|
+
pinActionOutcomeDetail(activeItem, recovered);
|
|
8831
|
+
await renderShell();
|
|
8832
|
+
return;
|
|
8833
|
+
}
|
|
8834
|
+
}
|
|
8144
8835
|
if (approvalFinalized) {
|
|
8145
8836
|
await refreshAuthenticatedState();
|
|
8146
8837
|
if (keepDetailOpen && activeItem?.kind === "approval") {
|
|
@@ -9239,8 +9930,43 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
9239
9930
|
for (const attachment of attachments) {
|
|
9240
9931
|
requestBody.append("image", attachment.file, attachment.name || attachment.file.name);
|
|
9241
9932
|
}
|
|
9242
|
-
const
|
|
9243
|
-
|
|
9933
|
+
const sentNotice = L(draft.mode === "plan" ? "reply.notice.sentPlan" : "reply.notice.sentDefault", { provider: providerDisplayName(replyProvider) });
|
|
9934
|
+
const renderOptimisticSent = () => {
|
|
9935
|
+
const currentDraft = getCompletionReplyDraft(token);
|
|
9936
|
+
if (!currentDraft.sending || currentDraft.text !== text) {
|
|
9937
|
+
return;
|
|
9938
|
+
}
|
|
9939
|
+
setCompletionReplyDraft(token, {
|
|
9940
|
+
text: "",
|
|
9941
|
+
sentText: text,
|
|
9942
|
+
attachments: currentDraft.attachments,
|
|
9943
|
+
mode: draft.mode,
|
|
9944
|
+
sending: false,
|
|
9945
|
+
error: "",
|
|
9946
|
+
notice: sentNotice,
|
|
9947
|
+
warning: null,
|
|
9948
|
+
confirmOverride: false,
|
|
9949
|
+
collapsedAfterSend: true,
|
|
9950
|
+
});
|
|
9951
|
+
renderShell().catch((renderError) => {
|
|
9952
|
+
console.warn("[completion-reply-optimistic-render]", renderError?.message || renderError);
|
|
9953
|
+
});
|
|
9954
|
+
};
|
|
9955
|
+
const optimisticSentTimer = setTimeout(renderOptimisticSent, COMPLETION_REPLY_OPTIMISTIC_SENT_MS);
|
|
9956
|
+
let replyResult = null;
|
|
9957
|
+
try {
|
|
9958
|
+
const replyKind = replyForm.dataset.replyKind || "completion";
|
|
9959
|
+
replyResult = await apiPost(
|
|
9960
|
+
`/api/items/${encodeURIComponent(replyKind)}/${encodeURIComponent(token)}/reply`,
|
|
9961
|
+
requestBody,
|
|
9962
|
+
{
|
|
9963
|
+
timeoutMs: COMPLETION_REPLY_SEND_TIMEOUT_MS,
|
|
9964
|
+
preferRelayError: true,
|
|
9965
|
+
},
|
|
9966
|
+
);
|
|
9967
|
+
} finally {
|
|
9968
|
+
clearTimeout(optimisticSentTimer);
|
|
9969
|
+
}
|
|
9244
9970
|
setCompletionReplyDraft(token, {
|
|
9245
9971
|
text: "",
|
|
9246
9972
|
sentText: text,
|
|
@@ -9248,14 +9974,51 @@ for (const button of document.querySelectorAll("[data-wallet-address-copy]")) {
|
|
|
9248
9974
|
mode: draft.mode,
|
|
9249
9975
|
sending: false,
|
|
9250
9976
|
error: "",
|
|
9251
|
-
notice:
|
|
9977
|
+
notice: sentNotice,
|
|
9252
9978
|
warning: null,
|
|
9253
9979
|
confirmOverride: false,
|
|
9254
9980
|
collapsedAfterSend: true,
|
|
9255
9981
|
});
|
|
9256
|
-
|
|
9982
|
+
if (replyResult?.ackTimeout === true) {
|
|
9983
|
+
console.info("[completion-reply] accepted after slow Codex ACK");
|
|
9984
|
+
}
|
|
9985
|
+
await renderShell();
|
|
9986
|
+
refreshAuthenticatedState()
|
|
9987
|
+
.then(renderShell)
|
|
9988
|
+
.catch((refreshError) => console.warn("[completion-reply-refresh]", refreshError?.message || refreshError));
|
|
9989
|
+
return;
|
|
9257
9990
|
} catch (error) {
|
|
9991
|
+
const optimisticDraft = getCompletionReplyDraft(token);
|
|
9992
|
+
if (
|
|
9993
|
+
error.errorKey === "request-timeout" &&
|
|
9994
|
+
optimisticDraft.collapsedAfterSend &&
|
|
9995
|
+
optimisticDraft.sentText === text
|
|
9996
|
+
) {
|
|
9997
|
+
refreshAuthenticatedState()
|
|
9998
|
+
.then(renderShell)
|
|
9999
|
+
.catch((refreshError) => console.warn("[completion-reply-refresh]", refreshError?.message || refreshError));
|
|
10000
|
+
return;
|
|
10001
|
+
}
|
|
9258
10002
|
if (error.errorKey === "completion-reply-thread-advanced") {
|
|
10003
|
+
if (completionReplyWarningMatchesSentText(error, text, attachments.length)) {
|
|
10004
|
+
setCompletionReplyDraft(token, {
|
|
10005
|
+
text: "",
|
|
10006
|
+
sentText: text,
|
|
10007
|
+
attachments: [],
|
|
10008
|
+
mode: draft.mode,
|
|
10009
|
+
sending: false,
|
|
10010
|
+
error: "",
|
|
10011
|
+
notice: L(draft.mode === "plan" ? "reply.notice.sentPlan" : "reply.notice.sentDefault", { provider: providerDisplayName(replyProvider) }),
|
|
10012
|
+
warning: null,
|
|
10013
|
+
confirmOverride: false,
|
|
10014
|
+
collapsedAfterSend: true,
|
|
10015
|
+
});
|
|
10016
|
+
await renderShell();
|
|
10017
|
+
refreshAuthenticatedState()
|
|
10018
|
+
.then(renderShell)
|
|
10019
|
+
.catch((refreshError) => console.warn("[completion-reply-refresh]", refreshError?.message || refreshError));
|
|
10020
|
+
return;
|
|
10021
|
+
}
|
|
9259
10022
|
setCompletionReplyDraft(token, {
|
|
9260
10023
|
text,
|
|
9261
10024
|
sentText: "",
|
|
@@ -9459,6 +10222,143 @@ function bindSharedUi(renderFn) {
|
|
|
9459
10222
|
}
|
|
9460
10223
|
}
|
|
9461
10224
|
|
|
10225
|
+
function bindScrollableContentRenderDeferral() {
|
|
10226
|
+
const mark = () => {
|
|
10227
|
+
markScrollableContentInteraction();
|
|
10228
|
+
};
|
|
10229
|
+
for (const el of document.querySelectorAll(SCROLLABLE_CONTENT_SELECTORS)) {
|
|
10230
|
+
el.addEventListener("scroll", mark, { passive: true });
|
|
10231
|
+
el.addEventListener("wheel", mark, { passive: true });
|
|
10232
|
+
el.addEventListener("touchstart", mark, { passive: true });
|
|
10233
|
+
el.addEventListener("touchmove", mark, { passive: true });
|
|
10234
|
+
el.addEventListener("pointerdown", mark);
|
|
10235
|
+
el.addEventListener("copy", mark);
|
|
10236
|
+
}
|
|
10237
|
+
}
|
|
10238
|
+
|
|
10239
|
+
function bindPartialListSurfaceInteractions(listSurface) {
|
|
10240
|
+
if (!listSurface || listSurface.dataset.partialListInteractionsBound === "true") {
|
|
10241
|
+
return;
|
|
10242
|
+
}
|
|
10243
|
+
listSurface.dataset.partialListInteractionsBound = "true";
|
|
10244
|
+
|
|
10245
|
+
const targetElement = (event) => {
|
|
10246
|
+
const target = event.target;
|
|
10247
|
+
return target instanceof Element ? target : target?.parentElement || null;
|
|
10248
|
+
};
|
|
10249
|
+
const threadSelectSelector = "[data-timeline-thread-select], [data-diff-thread-select], [data-completed-thread-select]";
|
|
10250
|
+
|
|
10251
|
+
listSurface.addEventListener("pointerdown", (event) => {
|
|
10252
|
+
if (targetElement(event)?.closest(threadSelectSelector)) {
|
|
10253
|
+
markThreadFilterInteraction();
|
|
10254
|
+
}
|
|
10255
|
+
});
|
|
10256
|
+
listSurface.addEventListener("focusin", (event) => {
|
|
10257
|
+
if (targetElement(event)?.closest(threadSelectSelector)) {
|
|
10258
|
+
markThreadFilterInteraction();
|
|
10259
|
+
}
|
|
10260
|
+
});
|
|
10261
|
+
listSurface.addEventListener("focusout", (event) => {
|
|
10262
|
+
if (targetElement(event)?.closest(threadSelectSelector)) {
|
|
10263
|
+
clearThreadFilterInteraction();
|
|
10264
|
+
}
|
|
10265
|
+
});
|
|
10266
|
+
|
|
10267
|
+
listSurface.addEventListener("change", async (event) => {
|
|
10268
|
+
const target = targetElement(event);
|
|
10269
|
+
const timelineSelect = target?.closest("[data-timeline-thread-select]");
|
|
10270
|
+
const diffSelect = target?.closest("[data-diff-thread-select]");
|
|
10271
|
+
const completedSelect = target?.closest("[data-completed-thread-select]");
|
|
10272
|
+
if (!timelineSelect && !diffSelect && !completedSelect) {
|
|
10273
|
+
return;
|
|
10274
|
+
}
|
|
10275
|
+
clearThreadFilterInteraction();
|
|
10276
|
+
if (timelineSelect) {
|
|
10277
|
+
state.timelineThreadFilter = timelineSelect.value || "all";
|
|
10278
|
+
state.timelineKindFilterOpen = false;
|
|
10279
|
+
} else if (diffSelect) {
|
|
10280
|
+
state.diffThreadFilter = diffSelect.value || "all";
|
|
10281
|
+
} else if (completedSelect) {
|
|
10282
|
+
state.completedThreadFilter = completedSelect.value || "all";
|
|
10283
|
+
}
|
|
10284
|
+
alignCurrentItemToVisibleEntries();
|
|
10285
|
+
await renderShell();
|
|
10286
|
+
});
|
|
10287
|
+
|
|
10288
|
+
listSurface.addEventListener("click", async (event) => {
|
|
10289
|
+
const target = targetElement(event);
|
|
10290
|
+
const threadSelect = target?.closest(threadSelectSelector);
|
|
10291
|
+
if (threadSelect) {
|
|
10292
|
+
markThreadFilterInteraction();
|
|
10293
|
+
return;
|
|
10294
|
+
}
|
|
10295
|
+
|
|
10296
|
+
const providerButton = target?.closest("[data-provider-filter]");
|
|
10297
|
+
if (providerButton) {
|
|
10298
|
+
event.preventDefault();
|
|
10299
|
+
const next = providerButton.dataset.providerFilter || "all";
|
|
10300
|
+
if (state.providerFilter === next) {
|
|
10301
|
+
return;
|
|
10302
|
+
}
|
|
10303
|
+
state.providerFilter = next;
|
|
10304
|
+
state.timelineThreadFilter = "all";
|
|
10305
|
+
state.timelineKindFilter = "all";
|
|
10306
|
+
state.timelineKindFilterOpen = false;
|
|
10307
|
+
state.completedThreadFilter = "all";
|
|
10308
|
+
state.diffThreadFilter = "all";
|
|
10309
|
+
alignCurrentItemToVisibleEntries();
|
|
10310
|
+
await renderShell();
|
|
10311
|
+
return;
|
|
10312
|
+
}
|
|
10313
|
+
|
|
10314
|
+
const kindToggle = target?.closest("[data-timeline-kind-filter-toggle]");
|
|
10315
|
+
if (kindToggle) {
|
|
10316
|
+
event.preventDefault();
|
|
10317
|
+
markThreadFilterInteraction();
|
|
10318
|
+
state.timelineKindFilterOpen = !state.timelineKindFilterOpen;
|
|
10319
|
+
await renderShell();
|
|
10320
|
+
return;
|
|
10321
|
+
}
|
|
10322
|
+
|
|
10323
|
+
const kindOption = target?.closest("[data-timeline-kind-filter-option]");
|
|
10324
|
+
if (kindOption) {
|
|
10325
|
+
event.preventDefault();
|
|
10326
|
+
clearThreadFilterInteraction();
|
|
10327
|
+
state.timelineKindFilter = kindOption.dataset.timelineKindFilterOption || "all";
|
|
10328
|
+
state.timelineKindFilterOpen = false;
|
|
10329
|
+
alignCurrentItemToVisibleEntries();
|
|
10330
|
+
await renderShell();
|
|
10331
|
+
return;
|
|
10332
|
+
}
|
|
10333
|
+
|
|
10334
|
+
const subtabButton = target?.closest("[data-inbox-subtab]");
|
|
10335
|
+
if (subtabButton) {
|
|
10336
|
+
const nextSubtab = subtabButton.dataset.inboxSubtab === "completed" ? "completed" : "pending";
|
|
10337
|
+
if (nextSubtab === state.inboxSubtab) {
|
|
10338
|
+
return;
|
|
10339
|
+
}
|
|
10340
|
+
state.inboxSubtab = nextSubtab;
|
|
10341
|
+
if (isDesktopLayout()) {
|
|
10342
|
+
alignCurrentItemToVisibleEntries();
|
|
10343
|
+
syncCurrentItemUrl(state.currentItem);
|
|
10344
|
+
}
|
|
10345
|
+
await renderShell();
|
|
10346
|
+
return;
|
|
10347
|
+
}
|
|
10348
|
+
|
|
10349
|
+
const itemButton = target?.closest("[data-open-item-kind][data-open-item-token]");
|
|
10350
|
+
if (itemButton) {
|
|
10351
|
+
openItem({
|
|
10352
|
+
kind: itemButton.dataset.openItemKind,
|
|
10353
|
+
token: itemButton.dataset.openItemToken,
|
|
10354
|
+
sourceTab: itemButton.dataset.sourceTab,
|
|
10355
|
+
sourceSubtab: itemButton.dataset.sourceSubtab,
|
|
10356
|
+
});
|
|
10357
|
+
await renderShell();
|
|
10358
|
+
}
|
|
10359
|
+
});
|
|
10360
|
+
}
|
|
10361
|
+
|
|
9462
10362
|
function openSettingsSubpage(page) {
|
|
9463
10363
|
if (!page) {
|
|
9464
10364
|
return;
|
|
@@ -9500,6 +10400,7 @@ async function switchTab(tab) {
|
|
|
9500
10400
|
syncCurrentItemUrl(state.currentItem);
|
|
9501
10401
|
}
|
|
9502
10402
|
await renderShell();
|
|
10403
|
+
refreshPrimaryTabAfterNavigation(tab);
|
|
9503
10404
|
if (tab === "settings") {
|
|
9504
10405
|
void fetchHazbaseStatus()
|
|
9505
10406
|
.then(() => {
|
|
@@ -9696,6 +10597,8 @@ function kindMeta(kind, item) {
|
|
|
9696
10597
|
return { label: L("common.diff"), tone: "neutral", icon: "diff" };
|
|
9697
10598
|
case "file_event":
|
|
9698
10599
|
return { label: L("common.fileEvent"), tone: "neutral", icon: "file-event" };
|
|
10600
|
+
case "command_event":
|
|
10601
|
+
return { label: L("common.commandEvent"), tone: "neutral", icon: "command" };
|
|
9699
10602
|
case "moltbook_reply":
|
|
9700
10603
|
return { label: L("common.moltbookReply"), tone: "neutral", icon: "moltbook-comment" };
|
|
9701
10604
|
case "moltbook_draft":
|
|
@@ -9728,6 +10631,9 @@ function itemIntentText(kind, status = "pending", provider) {
|
|
|
9728
10631
|
if (kind === "file_event") {
|
|
9729
10632
|
return L("intent.fileEvent");
|
|
9730
10633
|
}
|
|
10634
|
+
if (kind === "command_event") {
|
|
10635
|
+
return L("intent.commandEvent");
|
|
10636
|
+
}
|
|
9731
10637
|
if (kind === "ambient_suggestions") {
|
|
9732
10638
|
return L("intent.ambientSuggestions");
|
|
9733
10639
|
}
|
|
@@ -9765,6 +10671,9 @@ function detailIntentText(detail) {
|
|
|
9765
10671
|
if (detail.kind === "file_event") {
|
|
9766
10672
|
return itemIntentText(detail.kind, "timeline", provider);
|
|
9767
10673
|
}
|
|
10674
|
+
if (detail.kind === "command_event") {
|
|
10675
|
+
return itemIntentText(detail.kind, "timeline", provider);
|
|
10676
|
+
}
|
|
9768
10677
|
if (detail.kind === "ambient_suggestions") {
|
|
9769
10678
|
return itemIntentText(detail.kind, "timeline", provider);
|
|
9770
10679
|
}
|
|
@@ -9838,6 +10747,8 @@ function fallbackSummaryForKind(kind, status, provider) {
|
|
|
9838
10747
|
return L("summary.diffThread");
|
|
9839
10748
|
case "file_event":
|
|
9840
10749
|
return L("summary.fileEvent", vars);
|
|
10750
|
+
case "command_event":
|
|
10751
|
+
return L("summary.commandEvent", vars);
|
|
9841
10752
|
case "ambient_suggestions":
|
|
9842
10753
|
return L("summary.ambientSuggestions", { count: 0, firstTitle: "", more: 0 });
|
|
9843
10754
|
case "user_message":
|
|
@@ -10009,6 +10920,22 @@ function hasDetailOverride(itemRef = state.currentItem) {
|
|
|
10009
10920
|
return Boolean(state.detailOverride && isSameItemRef(state.detailOverride, itemRef));
|
|
10010
10921
|
}
|
|
10011
10922
|
|
|
10923
|
+
function isAlreadyHandledApprovalError(error) {
|
|
10924
|
+
return error?.errorKey === "approval-not-found" || error?.errorKey === "approval-already-handled";
|
|
10925
|
+
}
|
|
10926
|
+
|
|
10927
|
+
async function recoverHandledApprovalDetail(itemRef) {
|
|
10928
|
+
try {
|
|
10929
|
+
await refreshAuthenticatedState();
|
|
10930
|
+
const detail = await hydrateDetailImages(
|
|
10931
|
+
await apiGet(`/api/items/${encodeURIComponent(itemRef.kind)}/${encodeURIComponent(itemRef.token)}`)
|
|
10932
|
+
);
|
|
10933
|
+
return detail?.kind === "approval" && detail.readOnly === true ? detail : null;
|
|
10934
|
+
} catch {
|
|
10935
|
+
return null;
|
|
10936
|
+
}
|
|
10937
|
+
}
|
|
10938
|
+
|
|
10012
10939
|
function shouldPreserveCurrentItem(itemRef = state.currentItem) {
|
|
10013
10940
|
return Boolean(itemRef && (hasLaunchItemIntent(itemRef) || hasDetailOverride(itemRef)));
|
|
10014
10941
|
}
|
|
@@ -10044,6 +10971,8 @@ function renderIcon(name) {
|
|
|
10044
10971
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="4.5" width="14" height="15" rx="2.5"/><path d="M8.5 9h7"/><path d="M8.5 12h7"/><path d="M8.5 15h4.5"/></svg>`;
|
|
10045
10972
|
case "file-event":
|
|
10046
10973
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3.8h5.9l4.3 4.3v10a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2v-12.3a2 2 0 0 1 2-2Z"/><path d="M13.9 3.8v4.3h4.3"/><path d="M9.2 13.1h5.6"/><path d="M9.2 16.2h4"/></svg>`;
|
|
10974
|
+
case "command":
|
|
10975
|
+
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="4.2" y="5" width="15.6" height="14" rx="2.6"/><path d="m7.8 10 2.4 2-2.4 2"/><path d="M12.2 14h4"/></svg>`;
|
|
10047
10976
|
case "diff":
|
|
10048
10977
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M7.5 5.5v13"/><path d="M4.8 8.2 7.5 5.5 10.2 8.2"/><path d="M16.5 18.5v-13"/><path d="m13.8 15.8 2.7 2.7 2.7-2.7"/><path d="M11.8 7.5h1.2"/><path d="M11 12h2.8"/><path d="M11.8 16.5h1.2"/></svg>`;
|
|
10049
10978
|
case "pending":
|
|
@@ -10076,6 +11005,8 @@ function renderIcon(name) {
|
|
|
10076
11005
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.85" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="12" width="14" height="7" rx="2"/><path d="M9 19h6"/><path d="M12 12v7"/><path d="M8.2 8.8a5.4 5.4 0 0 1 7.6 0"/><path d="M10.2 6.3a8.2 8.2 0 0 1 3.6 0"/><path d="M11.2 9.8a1.2 1.2 0 0 1 1.6 0"/></svg>`;
|
|
10077
11006
|
case "link":
|
|
10078
11007
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M10.4 13.6 8.3 15.7a3 3 0 0 1-4.2-4.2l2.8-2.8a3 3 0 0 1 4.2 0"/><path d="m13.6 10.4 2.1-2.1a3 3 0 1 1 4.2 4.2l-2.8 2.8a3 3 0 0 1-4.2 0"/><path d="m9.5 14.5 5-5"/></svg>`;
|
|
11008
|
+
case "external-link":
|
|
11009
|
+
return `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M10 6H6.8A2.8 2.8 0 0 0 4 8.8v8.4A2.8 2.8 0 0 0 6.8 20h8.4a2.8 2.8 0 0 0 2.8-2.8V14"/><path d="M14 4h6v6"/><path d="m12.5 11.5 7-7"/></svg>`;
|
|
10079
11010
|
case "clip":
|
|
10080
11011
|
return `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="m9.5 12.5 5.9-5.9a3 3 0 1 1 4.2 4.2l-7.7 7.7a5 5 0 1 1-7.1-7.1l8.1-8.1"/></svg>`;
|
|
10081
11012
|
case "moltbook-draft":
|
|
@@ -10346,32 +11277,107 @@ function pruneTimelineImageObjectUrlCache() {
|
|
|
10346
11277
|
}
|
|
10347
11278
|
}
|
|
10348
11279
|
|
|
11280
|
+
function withRequestTimeout(init, opts = {}) {
|
|
11281
|
+
const timeoutMs = Number(opts.timeoutMs) || 0;
|
|
11282
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || typeof AbortController === "undefined") {
|
|
11283
|
+
return { init, cleanup: () => {} };
|
|
11284
|
+
}
|
|
11285
|
+
if (init.signal?.aborted) {
|
|
11286
|
+
return { init, cleanup: () => {} };
|
|
11287
|
+
}
|
|
11288
|
+
const controller = new AbortController();
|
|
11289
|
+
let timer = null;
|
|
11290
|
+
const externalAbortHandler = init.signal
|
|
11291
|
+
? () => controller.abort(init.signal.reason)
|
|
11292
|
+
: null;
|
|
11293
|
+
if (init.signal && externalAbortHandler) {
|
|
11294
|
+
init.signal.addEventListener("abort", externalAbortHandler, { once: true });
|
|
11295
|
+
}
|
|
11296
|
+
timer = setTimeout(() => {
|
|
11297
|
+
const error = new Error("request-timeout");
|
|
11298
|
+
error.name = "AbortError";
|
|
11299
|
+
controller.abort(error);
|
|
11300
|
+
}, timeoutMs);
|
|
11301
|
+
return {
|
|
11302
|
+
init: { ...init, signal: controller.signal },
|
|
11303
|
+
cleanup: () => {
|
|
11304
|
+
if (timer) clearTimeout(timer);
|
|
11305
|
+
if (init.signal && externalAbortHandler) {
|
|
11306
|
+
init.signal.removeEventListener("abort", externalAbortHandler);
|
|
11307
|
+
}
|
|
11308
|
+
},
|
|
11309
|
+
};
|
|
11310
|
+
}
|
|
11311
|
+
|
|
11312
|
+
function normalizeRequestError(error, opts = {}) {
|
|
11313
|
+
if (error?.name === "AbortError" && Number(opts.timeoutMs) > 0) {
|
|
11314
|
+
const timeoutError = new Error(L("error.requestTimedOut"));
|
|
11315
|
+
timeoutError.code = 0;
|
|
11316
|
+
timeoutError.status = 0;
|
|
11317
|
+
timeoutError.errorKey = "request-timeout";
|
|
11318
|
+
return timeoutError;
|
|
11319
|
+
}
|
|
11320
|
+
return error;
|
|
11321
|
+
}
|
|
11322
|
+
|
|
10349
11323
|
async function apiGet(url, opts = {}) {
|
|
10350
11324
|
// routedFetch tries LAN first, then falls back to the relay tunnel when
|
|
10351
11325
|
// the phone is off-LAN. Returns a fetch-Response-compatible object so the
|
|
10352
11326
|
// rest of this function is identical to a plain `fetch()` call.
|
|
10353
|
-
const
|
|
11327
|
+
const timed = withRequestTimeout({
|
|
10354
11328
|
credentials: "same-origin",
|
|
10355
11329
|
headers: {
|
|
10356
11330
|
Accept: "application/json",
|
|
10357
11331
|
},
|
|
10358
11332
|
}, opts);
|
|
10359
|
-
|
|
10360
|
-
const
|
|
10361
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
|
|
10365
|
-
|
|
11333
|
+
try {
|
|
11334
|
+
const response = await routedFetch(url, timed.init, opts);
|
|
11335
|
+
if (!response.ok) {
|
|
11336
|
+
const errorInfo = await readError(response);
|
|
11337
|
+
const error = new Error(errorInfo.message);
|
|
11338
|
+
error.code = response.status;
|
|
11339
|
+
error.status = response.status;
|
|
11340
|
+
error.errorKey = errorInfo.errorKey || "";
|
|
11341
|
+
throw error;
|
|
11342
|
+
}
|
|
11343
|
+
return await response.json();
|
|
11344
|
+
} catch (error) {
|
|
11345
|
+
throw normalizeRequestError(error, opts);
|
|
11346
|
+
} finally {
|
|
11347
|
+
timed.cleanup();
|
|
10366
11348
|
}
|
|
10367
|
-
return response.json();
|
|
10368
11349
|
}
|
|
10369
11350
|
|
|
10370
|
-
async function
|
|
11351
|
+
async function apiGetDirectLan(url, opts = {}) {
|
|
11352
|
+
const timed = withRequestTimeout({
|
|
11353
|
+
credentials: "same-origin",
|
|
11354
|
+
headers: {
|
|
11355
|
+
Accept: "application/json",
|
|
11356
|
+
},
|
|
11357
|
+
}, opts);
|
|
11358
|
+
try {
|
|
11359
|
+
const response = await fetch(url, timed.init);
|
|
11360
|
+
if (!response.ok) {
|
|
11361
|
+
const errorInfo = await readError(response);
|
|
11362
|
+
const error = new Error(errorInfo.message);
|
|
11363
|
+
error.code = response.status;
|
|
11364
|
+
error.status = response.status;
|
|
11365
|
+
error.errorKey = errorInfo.errorKey || "";
|
|
11366
|
+
throw error;
|
|
11367
|
+
}
|
|
11368
|
+
return await response.json();
|
|
11369
|
+
} catch (error) {
|
|
11370
|
+
throw normalizeRequestError(error, opts);
|
|
11371
|
+
} finally {
|
|
11372
|
+
timed.cleanup();
|
|
11373
|
+
}
|
|
11374
|
+
}
|
|
11375
|
+
|
|
11376
|
+
async function apiPost(url, body, opts = {}) {
|
|
10371
11377
|
const isFormDataBody = typeof FormData !== "undefined" && body instanceof FormData;
|
|
10372
11378
|
// Keep native FormData for LAN; routedFetch serializes it to multipart
|
|
10373
11379
|
// bytes only when the request has to travel through the remote relay.
|
|
10374
|
-
const
|
|
11380
|
+
const timed = withRequestTimeout({
|
|
10375
11381
|
method: "POST",
|
|
10376
11382
|
credentials: "same-origin",
|
|
10377
11383
|
headers: isFormDataBody
|
|
@@ -10383,17 +11389,24 @@ async function apiPost(url, body) {
|
|
|
10383
11389
|
Accept: "application/json",
|
|
10384
11390
|
},
|
|
10385
11391
|
body: isFormDataBody ? body : JSON.stringify(body || {}),
|
|
10386
|
-
});
|
|
10387
|
-
|
|
10388
|
-
const
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
11392
|
+
}, opts);
|
|
11393
|
+
try {
|
|
11394
|
+
const response = await routedFetch(url, timed.init, opts);
|
|
11395
|
+
if (!response.ok) {
|
|
11396
|
+
const errorInfo = await readError(response);
|
|
11397
|
+
const error = new Error(errorInfo.message);
|
|
11398
|
+
error.code = response.status;
|
|
11399
|
+
error.status = response.status;
|
|
11400
|
+
error.errorKey = errorInfo.errorKey || "";
|
|
11401
|
+
error.payload = errorInfo.payload ?? null;
|
|
11402
|
+
throw error;
|
|
11403
|
+
}
|
|
11404
|
+
return await response.json();
|
|
11405
|
+
} catch (error) {
|
|
11406
|
+
throw normalizeRequestError(error, opts);
|
|
11407
|
+
} finally {
|
|
11408
|
+
timed.cleanup();
|
|
10395
11409
|
}
|
|
10396
|
-
return response.json();
|
|
10397
11410
|
}
|
|
10398
11411
|
|
|
10399
11412
|
async function readError(response) {
|
|
@@ -10421,6 +11434,7 @@ function localizeApiError(value) {
|
|
|
10421
11434
|
"device-not-found": "error.deviceNotFound",
|
|
10422
11435
|
"web-push-disabled": "error.webPushDisabled",
|
|
10423
11436
|
"push-subscription-expired": "error.pushSubscriptionExpired",
|
|
11437
|
+
"request-timeout": "error.requestTimedOut",
|
|
10424
11438
|
"item-not-found": "error.itemNotFound",
|
|
10425
11439
|
"completion-reply-unavailable": "error.completionReplyUnavailable",
|
|
10426
11440
|
"completion-reply-thread-advanced": "error.completionReplyThreadAdvanced",
|