viveworker 0.8.0 → 0.8.2

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