oysterun 1.4.6 → 1.4.8

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 (46) hide show
  1. package/dev/client/web/index.html +761 -116
  2. package/dev/client/web-chat/dist/assets/{ReactPrism-lmxWDpVG.js → ReactPrism-C6f8462z.js} +1 -1
  3. package/dev/client/web-chat/dist/assets/{ReactPrism-lmxWDpVG.js.map → ReactPrism-C6f8462z.js.map} +1 -1
  4. package/dev/client/web-chat/dist/assets/{index-6GA21osP.js → index-BXXBmMM8.js} +93 -93
  5. package/dev/client/web-chat/dist/assets/index-BXXBmMM8.js.map +1 -0
  6. package/dev/client/web-chat/dist/assets/{index-DgT92KX_.js → index-Bhqm7xK7.js} +2 -2
  7. package/dev/client/web-chat/dist/assets/{index-DgT92KX_.js.map → index-Bhqm7xK7.js.map} +1 -1
  8. package/dev/client/web-chat/dist/assets/{index-BrEPk04R.js → index-tlM2bptc.js} +1 -1
  9. package/dev/client/web-chat/dist/assets/{index-BrEPk04R.js.map → index-tlM2bptc.js.map} +1 -1
  10. package/dev/client/web-chat/dist/assets/{pdf-BgTAydf1.js → pdf-D0Ks17S3.js} +3 -3
  11. package/dev/client/web-chat/dist/assets/{pdf-BgTAydf1.js.map → pdf-D0Ks17S3.js.map} +1 -1
  12. package/dev/client/web-chat/dist/assets/{web-BPMNryMv.js → web-Bnl6J5CG.js} +1 -1
  13. package/dev/client/web-chat/dist/assets/{web-BPMNryMv.js.map → web-Bnl6J5CG.js.map} +1 -1
  14. package/dev/client/web-chat/dist/assets/{web-B6RLoRzA.js → web-lzzU_sai.js} +1 -1
  15. package/dev/client/web-chat/dist/assets/{web-B6RLoRzA.js.map → web-lzzU_sai.js.map} +1 -1
  16. package/dev/client/web-chat/dist/index.html +1 -1
  17. package/docs/routec/dev/scheduler_mail_skill.md +96 -0
  18. package/host-service/assets/product-skills/Oysterun/modules/oysterun-mail/SKILL.md +28 -4
  19. package/host-service/assets/product-skills/Oysterun/modules/oysterun-mail/scripts/send_mail.mjs +25 -23
  20. package/host-service/assets/product-skills/Oysterun/modules/oysterun-scheduler/SKILL.md +29 -0
  21. package/host-service/assets/product-skills/oysterun-mail/SKILL.md +28 -4
  22. package/host-service/assets/product-skills/oysterun-mail/scripts/send_mail.mjs +25 -23
  23. package/host-service/assets/product-skills/oysterun-scheduler/SKILL.md +29 -0
  24. package/host-service/cli/product-cli.mjs +60 -7
  25. package/host-service/folder-browser.mjs +129 -2
  26. package/host-service/mail-notification-dispatcher.mjs +2 -2
  27. package/host-service/mail-store.mjs +17 -26
  28. package/host-service/package.json +1 -1
  29. package/host-service/scheduler-direct-provider-runner.mjs +8 -1
  30. package/host-service/scheduler-service.mjs +49 -0
  31. package/host-service/server.mjs +71 -7
  32. package/host-service/session-loop-definitions.mjs +36 -1
  33. package/host-service/session-manager.mjs +35 -1
  34. package/host-service/templates/demo-agents/oysterun-github-tracker/.codex/prompts/scheduler_task.md +71 -0
  35. package/host-service/templates/demo-agents/oysterun-github-tracker/.oysterun/config.json +5 -1
  36. package/host-service/templates/demo-agents/oysterun-github-tracker/.oysterun/schedulers.json +60 -0
  37. package/host-service/templates/demo-agents/oysterun-github-tracker/README.md +40 -5
  38. package/host-service/templates/demo-agents/oysterun-github-tracker/config.json +18 -0
  39. package/host-service/templates/demo-agents/oysterun-github-tracker/lib/github_tracker.py +247 -0
  40. package/host-service/templates/demo-agents/oysterun-github-tracker/tasks/repo_crawler_rich.py +825 -0
  41. package/package.json +1 -1
  42. package/static/site/index.html +13 -0
  43. package/tool_scripts/install_oysterun_launch_agents.sh +4 -0
  44. package/dev/client/web-chat/dist/assets/index-6GA21osP.js.map +0 -1
  45. package/host-service/templates/demo-agents/oysterun-github-tracker/agent_brief.md +0 -11
  46. package/host-service/templates/demo-agents/oysterun-github-tracker/issues.md +0 -11
@@ -1160,7 +1160,8 @@
1160
1160
  .topbar.sessions-mode,
1161
1161
  .topbar.scheduler-mode,
1162
1162
  .topbar.explorer-mode,
1163
- .topbar.session-loop-mode {
1163
+ .topbar.session-loop-mode,
1164
+ .topbar.mail-detail-mode {
1164
1165
  display: grid;
1165
1166
  grid-template-columns: 58px minmax(0, 1fr) 58px;
1166
1167
  gap: 0;
@@ -1175,7 +1176,8 @@
1175
1176
  .topbar.sessions-mode .topbar-left,
1176
1177
  .topbar.scheduler-mode .topbar-left,
1177
1178
  .topbar.explorer-mode .topbar-left,
1178
- .topbar.session-loop-mode .topbar-left {
1179
+ .topbar.session-loop-mode .topbar-left,
1180
+ .topbar.mail-detail-mode .topbar-left {
1179
1181
  display: contents;
1180
1182
  }
1181
1183
 
@@ -1192,7 +1194,8 @@
1192
1194
 
1193
1195
  .topbar.scheduler-setup-mode #topbar-back-btn,
1194
1196
  .topbar.explorer-mode #topbar-back-btn,
1195
- .topbar.session-loop-mode #topbar-back-btn {
1197
+ .topbar.session-loop-mode #topbar-back-btn,
1198
+ .topbar.mail-detail-mode #topbar-back-btn {
1196
1199
  grid-column: 1;
1197
1200
  justify-self: start;
1198
1201
  color: var(--accent);
@@ -1201,7 +1204,8 @@
1201
1204
  .topbar.sessions-mode .topbar-title,
1202
1205
  .topbar.scheduler-mode .topbar-title,
1203
1206
  .topbar.explorer-mode .topbar-title,
1204
- .topbar.session-loop-mode .topbar-title {
1207
+ .topbar.session-loop-mode .topbar-title,
1208
+ .topbar.mail-detail-mode .topbar-title {
1205
1209
  grid-column: 2;
1206
1210
  justify-self: center;
1207
1211
  text-align: center;
@@ -1223,7 +1227,8 @@
1223
1227
  .topbar.sessions-mode .topbar-title strong,
1224
1228
  .topbar.scheduler-mode .topbar-title strong,
1225
1229
  .topbar.explorer-mode .page-title strong,
1226
- .topbar.session-loop-mode .page-title strong {
1230
+ .topbar.session-loop-mode .page-title strong,
1231
+ .topbar.mail-detail-mode .page-title strong {
1227
1232
  font-family: var(--font-ui);
1228
1233
  font-size: 20px;
1229
1234
  font-weight: 700;
@@ -1244,14 +1249,16 @@
1244
1249
  }
1245
1250
 
1246
1251
  .topbar.explorer-mode .page-title span,
1247
- .topbar.session-loop-mode .page-title span {
1252
+ .topbar.session-loop-mode .page-title span,
1253
+ .topbar.mail-detail-mode .page-title span {
1248
1254
  display: none;
1249
1255
  }
1250
1256
 
1251
1257
  .topbar.sessions-mode .topbar-right,
1252
1258
  .topbar.scheduler-mode .topbar-right,
1253
1259
  .topbar.explorer-mode .topbar-right,
1254
- .topbar.session-loop-mode .topbar-right {
1260
+ .topbar.session-loop-mode .topbar-right,
1261
+ .topbar.mail-detail-mode .topbar-right {
1255
1262
  grid-column: 3;
1256
1263
  justify-self: end;
1257
1264
  }
@@ -4430,6 +4437,181 @@
4430
4437
  outline: none;
4431
4438
  }
4432
4439
 
4440
+ .mail-row-actions {
4441
+ display: grid;
4442
+ place-items: start center;
4443
+ padding: 10px 10px 0 0;
4444
+ }
4445
+
4446
+ .mail-row-menu-button {
4447
+ width: 36px;
4448
+ height: 36px;
4449
+ display: grid;
4450
+ place-items: center;
4451
+ border-radius: 50%;
4452
+ background: transparent;
4453
+ color: #7f715f;
4454
+ cursor: pointer;
4455
+ }
4456
+
4457
+ .mail-row-menu-button:hover,
4458
+ .mail-row-menu-button:focus-visible {
4459
+ transform: none;
4460
+ background: rgba(39, 28, 18, 0.08);
4461
+ outline: none;
4462
+ }
4463
+
4464
+ .mail-row-menu-button span::before {
4465
+ content: "\2022\2022\2022";
4466
+ display: block;
4467
+ font-size: 13px;
4468
+ letter-spacing: 2px;
4469
+ line-height: 1;
4470
+ }
4471
+
4472
+ .topbar-mail-actions {
4473
+ align-self: center;
4474
+ justify-self: end;
4475
+ }
4476
+
4477
+ .mail-detail {
4478
+ display: grid;
4479
+ gap: 14px;
4480
+ width: 100%;
4481
+ min-width: 0;
4482
+ }
4483
+
4484
+ .mail-detail-header {
4485
+ display: grid;
4486
+ gap: 6px;
4487
+ min-width: 0;
4488
+ padding: 0 2px;
4489
+ }
4490
+
4491
+ .mail-detail-title {
4492
+ margin: 0;
4493
+ min-width: 0;
4494
+ color: var(--ink);
4495
+ font-family: var(--font-ui);
4496
+ font-size: clamp(22px, 5vw, 30px);
4497
+ font-weight: 750;
4498
+ line-height: 1.15;
4499
+ letter-spacing: 0;
4500
+ overflow-wrap: anywhere;
4501
+ word-break: break-word;
4502
+ }
4503
+
4504
+ .mail-detail-meta {
4505
+ display: flex;
4506
+ flex-wrap: wrap;
4507
+ gap: 4px 8px;
4508
+ min-width: 0;
4509
+ color: var(--ink-soft);
4510
+ font-size: 12px;
4511
+ line-height: 1.45;
4512
+ }
4513
+
4514
+ .mail-detail-meta span {
4515
+ min-width: 0;
4516
+ overflow-wrap: anywhere;
4517
+ word-break: break-word;
4518
+ }
4519
+
4520
+ .mail-detail-meta span + span::before {
4521
+ content: "\00b7";
4522
+ margin-right: 8px;
4523
+ color: rgba(101, 76, 51, 0.45);
4524
+ }
4525
+
4526
+ .mail-detail-body {
4527
+ width: 100%;
4528
+ min-width: 0;
4529
+ }
4530
+
4531
+ .mail-html-frame-shell {
4532
+ width: 100%;
4533
+ min-width: 0;
4534
+ background: transparent;
4535
+ }
4536
+
4537
+ .mail-html-frame {
4538
+ display: block;
4539
+ width: 100%;
4540
+ min-width: 0;
4541
+ min-height: min(640px, calc(100vh - var(--topbar-height) - 148px));
4542
+ border: 0;
4543
+ background: transparent;
4544
+ }
4545
+
4546
+ .mail-row-menu-layer {
4547
+ position: fixed;
4548
+ inset: 0;
4549
+ z-index: 10000;
4550
+ background: transparent;
4551
+ pointer-events: auto;
4552
+ }
4553
+
4554
+ .mail-row-menu-panel {
4555
+ position: fixed;
4556
+ width: 190px;
4557
+ padding: 6px;
4558
+ border: 1px solid var(--line);
4559
+ border-radius: 14px;
4560
+ background: rgba(255, 253, 250, 0.98);
4561
+ box-shadow: var(--shadow);
4562
+ pointer-events: auto;
4563
+ }
4564
+
4565
+ .mail-row-menu-action {
4566
+ width: 100%;
4567
+ min-height: 36px;
4568
+ display: grid;
4569
+ grid-template-columns: 22px minmax(0, 1fr);
4570
+ gap: 8px;
4571
+ align-items: center;
4572
+ padding: 8px 10px;
4573
+ border-radius: 10px;
4574
+ background: transparent;
4575
+ color: var(--ink);
4576
+ font-size: 13px;
4577
+ font-weight: 700;
4578
+ text-align: left;
4579
+ }
4580
+
4581
+ .mail-row-menu-action-icon {
4582
+ width: 22px;
4583
+ height: 22px;
4584
+ display: grid;
4585
+ place-items: center;
4586
+ color: #7f715f;
4587
+ }
4588
+
4589
+ .mail-row-menu-action-icon svg {
4590
+ width: 18px;
4591
+ height: 18px;
4592
+ display: block;
4593
+ fill: none;
4594
+ stroke: currentColor;
4595
+ stroke-width: 1.8;
4596
+ stroke-linecap: round;
4597
+ stroke-linejoin: round;
4598
+ }
4599
+
4600
+ .mail-row-menu-action:hover,
4601
+ .mail-row-menu-action:focus-visible {
4602
+ transform: none;
4603
+ background: rgba(39, 28, 18, 0.08);
4604
+ outline: none;
4605
+ }
4606
+
4607
+ .mail-row-menu-action.danger {
4608
+ color: var(--red);
4609
+ }
4610
+
4611
+ .mail-row-menu-action.danger .mail-row-menu-action-icon {
4612
+ color: var(--red);
4613
+ }
4614
+
4433
4615
  .session-row-menu {
4434
4616
  position: relative;
4435
4617
  align-self: start;
@@ -7197,11 +7379,46 @@
7197
7379
  white-space: nowrap;
7198
7380
  }
7199
7381
 
7382
+ .bottom-nav-item .bottom-nav-icon {
7383
+ position: relative;
7384
+ width: 28px;
7385
+ height: 22px;
7386
+ display: grid;
7387
+ place-items: center;
7388
+ overflow: visible;
7389
+ text-overflow: clip;
7390
+ }
7391
+
7200
7392
  .bottom-nav-item svg {
7201
7393
  width: 18px;
7202
7394
  height: 18px;
7203
7395
  }
7204
7396
 
7397
+ .bottom-nav-item .bottom-nav-mail-badge {
7398
+ position: absolute;
7399
+ top: -7px;
7400
+ right: -9px;
7401
+ min-width: 17px;
7402
+ height: 17px;
7403
+ display: inline-flex;
7404
+ align-items: center;
7405
+ justify-content: center;
7406
+ padding: 0 4px;
7407
+ border-radius: 999px;
7408
+ border: 1.5px solid rgba(248, 242, 233, 0.96);
7409
+ background: #ff3b30;
7410
+ color: #fff;
7411
+ box-shadow: 0 2px 7px rgba(165, 34, 27, 0.28);
7412
+ font-size: 10px;
7413
+ font-weight: 500;
7414
+ line-height: 1;
7415
+ letter-spacing: 0;
7416
+ pointer-events: none;
7417
+ overflow: visible;
7418
+ text-overflow: clip;
7419
+ white-space: nowrap;
7420
+ }
7421
+
7205
7422
  .bottom-nav-item.active {
7206
7423
  background: rgba(185, 99, 53, 0.12);
7207
7424
  color: var(--ink);
@@ -7593,6 +7810,18 @@
7593
7810
  >
7594
7811
  Save
7595
7812
  </button>
7813
+ <button
7814
+ class="mail-row-menu-button topbar-mail-actions hidden"
7815
+ id="topbar-mail-actions-btn"
7816
+ type="button"
7817
+ title="Mail actions"
7818
+ aria-label="Mail actions"
7819
+ data-mail-menu-action="open"
7820
+ data-mail-id=""
7821
+ data-oysterun-mail-detail-actions="topbar_overflow_menu"
7822
+ >
7823
+ <span aria-hidden="true"></span>
7824
+ </button>
7596
7825
  <button
7597
7826
  class="topbar-circle-btn hidden"
7598
7827
  id="btn-agent-site"
@@ -9863,6 +10092,11 @@ Terminal output will appear here.</pre
9863
10092
  class="hidden"
9864
10093
  data-oysterun-mail-detail="host_owned_mail_item"
9865
10094
  ></div>
10095
+ <div
10096
+ class="mail-row-menu-layer hidden"
10097
+ id="mail-row-menu-layer"
10098
+ data-oysterun-mail-row-menu-layer="true"
10099
+ ></div>
9866
10100
  </section>
9867
10101
  </div>
9868
10102
  </div>
@@ -10785,35 +11019,39 @@ Waiting for Claude login output…</pre
10785
11019
  data-oysterun-mail-tab="true"
10786
11020
  onClick=${call("switchTab", "mail")}
10787
11021
  >
10788
- <svg
10789
- viewBox="0 0 24 24"
11022
+ <span
11023
+ class="bottom-nav-icon bottom-nav-mail-icon"
10790
11024
  aria-hidden="true"
10791
- focusable="false"
10792
- data-oysterun-bottom-tab-icon="mail"
10793
11025
  >
10794
- <path
10795
- d="M4.8 6h14.4c1 0 1.8.8 1.8 1.8v8.4c0 1-.8 1.8-1.8 1.8H4.8c-1 0-1.8-.8-1.8-1.8V7.8C3 6.8 3.8 6 4.8 6Z"
10796
- fill="none"
10797
- stroke="currentColor"
10798
- stroke-width="1.7"
10799
- stroke-linejoin="round"
10800
- ></path>
10801
- <path
10802
- d="m4.5 7.5 7.5 5.25 7.5-5.25"
10803
- fill="none"
10804
- stroke="currentColor"
10805
- stroke-width="1.7"
10806
- stroke-linecap="round"
10807
- stroke-linejoin="round"
10808
- ></path>
10809
- </svg>
11026
+ <svg
11027
+ viewBox="0 0 24 24"
11028
+ focusable="false"
11029
+ data-oysterun-bottom-tab-icon="mail"
11030
+ >
11031
+ <path
11032
+ d="M4.8 6h14.4c1 0 1.8.8 1.8 1.8v8.4c0 1-.8 1.8-1.8 1.8H4.8c-1 0-1.8-.8-1.8-1.8V7.8C3 6.8 3.8 6 4.8 6Z"
11033
+ fill="none"
11034
+ stroke="currentColor"
11035
+ stroke-width="1.7"
11036
+ stroke-linejoin="round"
11037
+ ></path>
11038
+ <path
11039
+ d="m4.5 7.5 7.5 5.25 7.5-5.25"
11040
+ fill="none"
11041
+ stroke="currentColor"
11042
+ stroke-width="1.7"
11043
+ stroke-linecap="round"
11044
+ stroke-linejoin="round"
11045
+ ></path>
11046
+ </svg>
11047
+ <span
11048
+ class="bottom-nav-mail-badge hidden"
11049
+ id="mail-unread-badge"
11050
+ data-oysterun-mail-unread-badge="true"
11051
+ aria-label="Unread mail count"
11052
+ ></span>
11053
+ </span>
10810
11054
  <span>Mail</span>
10811
- <span
10812
- class="pill hidden"
10813
- id="mail-unread-badge"
10814
- data-oysterun-mail-unread-badge="true"
10815
- aria-label="Unread mail count"
10816
- ></span>
10817
11055
  </button>
10818
11056
  </nav>
10819
11057
  </div>
@@ -11347,6 +11585,7 @@ Waiting for Claude login output…</pre
11347
11585
  loaded: false,
11348
11586
  loading: false,
11349
11587
  saving: false,
11588
+ pendingTargets: {},
11350
11589
  editingId: null,
11351
11590
  error: "",
11352
11591
  };
@@ -12546,6 +12785,7 @@ Waiting for Claude login output…</pre
12546
12785
  let sessionLoopState = createSessionLoopState();
12547
12786
  let hostSchedulerState = createHostSchedulerState();
12548
12787
  let mailState = createMailState();
12788
+ let mailRowActionMenuState = null;
12549
12789
  let schedulerSetupCopyState = createSchedulerSetupCopyState();
12550
12790
  let hostSchedulerActionMenuState = null;
12551
12791
  let sessionLoopActionMenuState = null;
@@ -27749,8 +27989,8 @@ Waiting for Claude login output…</pre
27749
27989
  if (screen === "mail") {
27750
27990
  if (normalized.mailId) {
27751
27991
  return {
27752
- title: "Mail",
27753
- subtitle: "Host-owned Mail item detail.",
27992
+ title: "Email",
27993
+ subtitle: "",
27754
27994
  };
27755
27995
  }
27756
27996
  return {
@@ -27839,6 +28079,7 @@ Waiting for Claude login output…</pre
27839
28079
  const isAgentProfile = screen === "agent-profile";
27840
28080
  const isBrowser = screen === "browser";
27841
28081
  const currentRoute = normalizeClientRoute(getCurrentRoute());
28082
+ const isMailDetail = screen === "mail" && Boolean(currentRoute.mailId);
27842
28083
  const isExplorer = screen === "explorer";
27843
28084
  const isSessionSetupFolderPicker =
27844
28085
  screen === "folder-picker" &&
@@ -27873,6 +28114,30 @@ Waiting for Claude login output…</pre
27873
28114
  setVisible("topbar-scheduler-new-btn", isSchedulerList);
27874
28115
  setVisible("topbar-session-loop-new-btn", isSessionLoop);
27875
28116
  setVisible("topbar-explorer-add-folder-btn", showTopbarAddFolder);
28117
+ setVisible("topbar-mail-actions-btn", isMailDetail);
28118
+ const topbarMailActionsButton = $("topbar-mail-actions-btn");
28119
+ if (topbarMailActionsButton) {
28120
+ if (isMailDetail) {
28121
+ topbarMailActionsButton.setAttribute(
28122
+ "data-mail-id",
28123
+ currentRoute.mailId
28124
+ );
28125
+ const item =
28126
+ mailState.selectedItem ||
28127
+ mailState.itemById.get(currentRoute.mailId) ||
28128
+ null;
28129
+ topbarMailActionsButton.setAttribute(
28130
+ "aria-label",
28131
+ `Mail actions${item?.title ? ` for ${item.title}` : ""}`
28132
+ );
28133
+ topbarMailActionsButton.disabled =
28134
+ mailState.actionBusyId === currentRoute.mailId;
28135
+ } else {
28136
+ topbarMailActionsButton.removeAttribute("data-mail-id");
28137
+ topbarMailActionsButton.disabled = false;
28138
+ topbarMailActionsButton.setAttribute("aria-label", "Mail actions");
28139
+ }
28140
+ }
27876
28141
  const topbarExplorerAddFolderButton = $(
27877
28142
  "topbar-explorer-add-folder-btn"
27878
28143
  );
@@ -28060,6 +28325,7 @@ Waiting for Claude login output…</pre
28060
28325
  $("topbar")?.classList.toggle("scheduler-mode", isSchedulerList);
28061
28326
  $("topbar")?.classList.toggle("explorer-mode", isExplorer);
28062
28327
  $("topbar")?.classList.toggle("session-loop-mode", isSessionLoop);
28328
+ $("topbar")?.classList.toggle("mail-detail-mode", isMailDetail);
28063
28329
  $("topbar")?.classList.toggle("browser-mode", isBrowser);
28064
28330
  $("topbar")?.classList.toggle(
28065
28331
  "scheduler-setup-mode",
@@ -30521,6 +30787,10 @@ Waiting for Claude login output…</pre
30521
30787
  badge.textContent = count > 99 ? "99+" : String(count);
30522
30788
  badge.classList.toggle("hidden", count <= 0);
30523
30789
  badge.setAttribute("data-oysterun-mail-unread-count", String(count));
30790
+ badge.setAttribute(
30791
+ "aria-label",
30792
+ count === 1 ? "1 unread mail" : `${count} unread mail`
30793
+ );
30524
30794
  }
30525
30795
 
30526
30796
  function renderMailFilterBar() {
@@ -30588,6 +30858,122 @@ Waiting for Claude login output…</pre
30588
30858
  `;
30589
30859
  }
30590
30860
 
30861
+ function renderMailActionIcon(name) {
30862
+ if (name === "read") {
30863
+ return `
30864
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
30865
+ <path d="M4.5 7h15v10h-15z" />
30866
+ <path d="m5 8 7 5 7-5" />
30867
+ <path d="m8 12 2.4 2.4L16 9" />
30868
+ </svg>
30869
+ `;
30870
+ }
30871
+ if (name === "unread") {
30872
+ return `
30873
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
30874
+ <path d="M4.5 7h15v10h-15z" />
30875
+ <path d="m5 8 7 5 7-5" />
30876
+ <circle cx="18" cy="6" r="2.2" />
30877
+ </svg>
30878
+ `;
30879
+ }
30880
+ if (name === "archive") {
30881
+ return `
30882
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
30883
+ <path d="M4.5 6.5h15v3h-15z" />
30884
+ <path d="M6 9.5v8h12v-8" />
30885
+ <path d="M9.5 13h5" />
30886
+ </svg>
30887
+ `;
30888
+ }
30889
+ if (name === "unarchive") {
30890
+ return `
30891
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
30892
+ <path d="M4.5 6.5h15v3h-15z" />
30893
+ <path d="M6 9.5v8h12v-8" />
30894
+ <path d="M12 16v-4" />
30895
+ <path d="m9.5 14.5 2.5-2.5 2.5 2.5" />
30896
+ </svg>
30897
+ `;
30898
+ }
30899
+ if (name === "copy") {
30900
+ return `
30901
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
30902
+ <path d="M8 7.5V5.8c0-1 .8-1.8 1.8-1.8h6.4c1 0 1.8.8 1.8 1.8v8.4c0 1-.8 1.8-1.8 1.8h-1.7" />
30903
+ <path d="M5.8 8h6.4c1 0 1.8.8 1.8 1.8v8.4c0 1-.8 1.8-1.8 1.8H5.8c-1 0-1.8-.8-1.8-1.8V9.8C4 8.8 4.8 8 5.8 8Z" />
30904
+ </svg>
30905
+ `;
30906
+ }
30907
+ if (name === "trash") {
30908
+ return `
30909
+ <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
30910
+ <path d="M5 7h14" />
30911
+ <path d="M9 7V5.8c0-1 .8-1.8 1.8-1.8h2.4c1 0 1.8.8 1.8 1.8V7" />
30912
+ <path d="m7 7 1 12c.1 1 1 1.8 2 1.8h4c1 0 1.9-.8 2-1.8l1-12" />
30913
+ <path d="M10.5 11v6M13.5 11v6" />
30914
+ </svg>
30915
+ `;
30916
+ }
30917
+ return "";
30918
+ }
30919
+
30920
+ function getMailRowActionMenuItems(item) {
30921
+ if (!item?.id) return [];
30922
+ const unread = !item.read_at;
30923
+ const archived = Boolean(item.archived_at);
30924
+ const busy = mailState.actionBusyId === item.id;
30925
+ return [
30926
+ {
30927
+ action: unread ? "read" : "unread",
30928
+ label: unread ? "Mark read" : "Mark unread",
30929
+ icon: unread ? "read" : "unread",
30930
+ disabled: busy,
30931
+ },
30932
+ {
30933
+ action: archived ? "unarchive" : "archive",
30934
+ label: archived ? "Unarchive" : "Archive",
30935
+ icon: archived ? "unarchive" : "archive",
30936
+ disabled: busy,
30937
+ },
30938
+ {
30939
+ action: "copy",
30940
+ label: "Copy mail link",
30941
+ icon: "copy",
30942
+ },
30943
+ {
30944
+ action: "delete",
30945
+ label: "Delete",
30946
+ icon: "trash",
30947
+ danger: true,
30948
+ disabled: busy,
30949
+ },
30950
+ ];
30951
+ }
30952
+
30953
+ function renderMailRowMenuActionButton({
30954
+ action,
30955
+ label,
30956
+ icon,
30957
+ danger = false,
30958
+ disabled = false,
30959
+ }) {
30960
+ return `
30961
+ <button
30962
+ class="mail-row-menu-action${danger ? " danger" : ""}"
30963
+ type="button"
30964
+ data-mail-menu-item-action="${esc(action)}"
30965
+ role="menuitem"
30966
+ aria-disabled="${String(disabled)}"
30967
+ ${disabled ? "disabled" : ""}
30968
+ >
30969
+ <span class="mail-row-menu-action-icon">
30970
+ ${renderMailActionIcon(icon)}
30971
+ </span>
30972
+ <span>${esc(label)}</span>
30973
+ </button>
30974
+ `;
30975
+ }
30976
+
30591
30977
  function renderMailRow(item) {
30592
30978
  const mailId = item.id;
30593
30979
  const unread = !item.read_at;
@@ -30629,55 +31015,148 @@ Waiting for Claude login output…</pre
30629
31015
  </span>
30630
31016
  </span>
30631
31017
  </button>
30632
- <div class="web-site-row-actions" data-oysterun-mail-row-actions="true">
31018
+ <div class="mail-row-actions" data-oysterun-mail-row-actions="true">
30633
31019
  <button
30634
- class="web-site-row-action"
31020
+ class="mail-row-menu-button"
30635
31021
  type="button"
30636
- title="${unread ? "Mark read" : "Mark unread"}"
30637
- aria-label="${unread ? "Mark read" : "Mark unread"}"
30638
- data-mail-action="${unread ? "read" : "unread"}"
31022
+ title="Mail actions"
31023
+ aria-label="Mail actions for ${esc(item.title)}"
31024
+ data-mail-menu-action="open"
30639
31025
  data-mail-id="${esc(mailId)}"
30640
31026
  ${getMailActionBusyAttr(mailId)}
30641
31027
  >
30642
- ${unread ? "Read" : "Unread"}
30643
- </button>
30644
- <button
30645
- class="web-site-row-action"
30646
- type="button"
30647
- title="${archived ? "Unarchive" : "Archive"}"
30648
- aria-label="${archived ? "Unarchive" : "Archive"}"
30649
- data-mail-action="${archived ? "unarchive" : "archive"}"
30650
- data-mail-id="${esc(mailId)}"
30651
- ${getMailActionBusyAttr(mailId)}
30652
- >
30653
- ${archived ? "Inbox" : "Archive"}
30654
- </button>
30655
- <button
30656
- class="web-site-row-action"
30657
- type="button"
30658
- title="Copy mail link"
30659
- aria-label="Copy mail link"
30660
- data-mail-action="copy"
30661
- data-mail-id="${esc(mailId)}"
30662
- >
30663
- Copy
30664
- </button>
30665
- <button
30666
- class="web-site-row-action"
30667
- type="button"
30668
- title="Delete"
30669
- aria-label="Delete"
30670
- data-mail-action="delete"
30671
- data-mail-id="${esc(mailId)}"
30672
- ${getMailActionBusyAttr(mailId)}
30673
- >
30674
- Delete
31028
+ <span aria-hidden="true"></span>
30675
31029
  </button>
30676
31030
  </div>
30677
31031
  </article>
30678
31032
  `;
30679
31033
  }
30680
31034
 
31035
+ function getMailRowItem(mailId) {
31036
+ const normalizedId = sanitizeRouteText(mailId);
31037
+ if (!normalizedId) return null;
31038
+ return (
31039
+ mailState.itemById.get(normalizedId) ||
31040
+ mailState.items.find((item) => item.id === normalizedId) ||
31041
+ (mailState.selectedItem?.id === normalizedId
31042
+ ? mailState.selectedItem
31043
+ : null)
31044
+ );
31045
+ }
31046
+
31047
+ function closeMailRowActionMenu() {
31048
+ const layer = $("mail-row-menu-layer");
31049
+ if (layer) {
31050
+ layer.classList.add("hidden");
31051
+ layer.innerHTML = "";
31052
+ }
31053
+ mailRowActionMenuState = null;
31054
+ document.removeEventListener(
31055
+ "click",
31056
+ handleMailRowActionMenuDocumentClick,
31057
+ true
31058
+ );
31059
+ document.removeEventListener("keydown", handleMailRowActionMenuKeydown);
31060
+ }
31061
+
31062
+ function handleMailRowActionMenuDocumentClick(event) {
31063
+ const target = event.target;
31064
+ if (!(target instanceof Element)) {
31065
+ closeMailRowActionMenu();
31066
+ event.preventDefault();
31067
+ event.stopPropagation();
31068
+ return;
31069
+ }
31070
+ if (
31071
+ target.closest(".mail-row-menu-panel") ||
31072
+ target.closest("[data-mail-menu-action='open']")
31073
+ ) {
31074
+ return;
31075
+ }
31076
+ event.preventDefault();
31077
+ event.stopPropagation();
31078
+ closeMailRowActionMenu();
31079
+ }
31080
+
31081
+ function handleMailRowActionMenuKeydown(event) {
31082
+ if (event.key === "Escape") {
31083
+ closeMailRowActionMenu();
31084
+ }
31085
+ }
31086
+
31087
+ function positionMailRowActionMenu(anchorButton) {
31088
+ const layer = $("mail-row-menu-layer");
31089
+ const panel = layer?.querySelector(".mail-row-menu-panel");
31090
+ if (!layer || !panel || !anchorButton) return;
31091
+ const rect = anchorButton.getBoundingClientRect();
31092
+ const panelWidth = panel.offsetWidth || 190;
31093
+ const panelHeight = panel.offsetHeight || 160;
31094
+ const edgeInset = 12;
31095
+ let left = rect.right - panelWidth;
31096
+ left = Math.max(
31097
+ edgeInset,
31098
+ Math.min(left, window.innerWidth - panelWidth - edgeInset)
31099
+ );
31100
+ let top = rect.bottom + 6;
31101
+ if (top + panelHeight > window.innerHeight - edgeInset) {
31102
+ top = rect.top - panelHeight - 6;
31103
+ }
31104
+ top = Math.max(
31105
+ edgeInset,
31106
+ Math.min(top, window.innerHeight - panelHeight - edgeInset)
31107
+ );
31108
+ panel.style.left = `${Math.round(left)}px`;
31109
+ panel.style.top = `${Math.round(top)}px`;
31110
+ }
31111
+
31112
+ function openMailRowActionMenu(anchorButton, mailId) {
31113
+ const normalizedId = sanitizeRouteText(mailId);
31114
+ if (!normalizedId) return;
31115
+ const item = getMailRowItem(normalizedId);
31116
+ if (!item) return;
31117
+ const layer = $("mail-row-menu-layer");
31118
+ if (!layer) return;
31119
+ const alreadyOpen =
31120
+ mailRowActionMenuState?.mailId === normalizedId &&
31121
+ !layer.classList.contains("hidden");
31122
+ closeMailRowActionMenu();
31123
+ if (alreadyOpen) return;
31124
+ mailRowActionMenuState = { mailId: normalizedId };
31125
+ const menuItems = getMailRowActionMenuItems(item);
31126
+ layer.innerHTML = `
31127
+ <div
31128
+ class="mail-row-menu-panel"
31129
+ role="menu"
31130
+ data-oysterun-mail-row-menu="${esc(normalizedId)}"
31131
+ >
31132
+ ${menuItems.map(renderMailRowMenuActionButton).join("")}
31133
+ </div>
31134
+ `;
31135
+ layer.classList.remove("hidden");
31136
+ positionMailRowActionMenu(anchorButton);
31137
+ for (const actionButton of layer.querySelectorAll(
31138
+ "[data-mail-menu-item-action]"
31139
+ )) {
31140
+ actionButton.addEventListener("click", (event) => {
31141
+ event.preventDefault();
31142
+ event.stopPropagation();
31143
+ const action = actionButton.getAttribute(
31144
+ "data-mail-menu-item-action"
31145
+ );
31146
+ closeMailRowActionMenu();
31147
+ void performMailAction(normalizedId, action || "");
31148
+ });
31149
+ }
31150
+ window.setTimeout(() => {
31151
+ document.addEventListener(
31152
+ "click",
31153
+ handleMailRowActionMenuDocumentClick,
31154
+ true
31155
+ );
31156
+ document.addEventListener("keydown", handleMailRowActionMenuKeydown);
31157
+ }, 0);
31158
+ }
31159
+
30681
31160
  function normalizeTrustedMailHref(rawHref) {
30682
31161
  const href = String(rawHref || "").trim();
30683
31162
  if (!href || href.startsWith("#")) return href;
@@ -30890,15 +31369,57 @@ Waiting for Claude login output…</pre
30890
31369
  return doc.body.innerHTML;
30891
31370
  }
30892
31371
 
31372
+ function buildMailHtmlSrcdoc(value) {
31373
+ const html = String(value || "");
31374
+ if (/<(?:!doctype|html|head|body)\b/i.test(html)) return html;
31375
+ return `<!doctype html>
31376
+ <html>
31377
+ <head>
31378
+ <meta charset="utf-8" />
31379
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
31380
+ <style>
31381
+ html,
31382
+ body {
31383
+ margin: 0;
31384
+ padding: 0;
31385
+ min-width: 0;
31386
+ overflow-wrap: anywhere;
31387
+ word-break: normal;
31388
+ font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", sans-serif;
31389
+ color: #271c12;
31390
+ background: transparent;
31391
+ }
31392
+ img,
31393
+ video,
31394
+ canvas,
31395
+ svg {
31396
+ max-width: 100%;
31397
+ height: auto;
31398
+ }
31399
+ table {
31400
+ max-width: 100%;
31401
+ }
31402
+ </style>
31403
+ </head>
31404
+ <body>${html}</body>
31405
+ </html>`;
31406
+ }
31407
+
30893
31408
  function renderMailBody(item) {
30894
31409
  if (item.body_format === "html") {
30895
31410
  return `
30896
31411
  <div
30897
- class="markdown-doc rich-text"
30898
- data-oysterun-mail-body-format="trusted_html"
30899
- data-oysterun-mail-html-sanitized="true"
31412
+ class="mail-html-frame-shell"
31413
+ data-oysterun-mail-body-format="html"
31414
+ data-oysterun-mail-html-sandboxed="true"
30900
31415
  >
30901
- ${sanitizeTrustedMailHtml(item.body_html)}
31416
+ <iframe
31417
+ class="mail-html-frame"
31418
+ title="Mail HTML body"
31419
+ sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"
31420
+ data-oysterun-mail-html-frame="true"
31421
+ srcdoc="${esc(buildMailHtmlSrcdoc(item.body_html))}"
31422
+ ></iframe>
30902
31423
  </div>
30903
31424
  `;
30904
31425
  }
@@ -30936,51 +31457,28 @@ Waiting for Claude login output…</pre
30936
31457
  const unread = !item.read_at;
30937
31458
  const createdLabel = formatMailTimestamp(item.created_at);
30938
31459
  const tags = item.tags.length ? item.tags.join(", ") : "";
31460
+ const metaParts = [
31461
+ getMailSourceLabel(item),
31462
+ createdLabel,
31463
+ item.severity,
31464
+ unread ? "Unread" : "Read",
31465
+ archived ? "Archived" : "",
31466
+ tags,
31467
+ ].filter(Boolean);
30939
31468
  return `
30940
31469
  <article
30941
31470
  class="mail-detail"
30942
31471
  data-oysterun-mail-detail-id="${esc(item.id)}"
30943
31472
  data-oysterun-mail-detail-read="${unread ? "false" : "true"}"
30944
31473
  data-oysterun-mail-detail-archived="${archived ? "true" : "false"}"
31474
+ data-oysterun-mail-detail-layout="title_meta_body"
30945
31475
  >
30946
- <div class="page-header">
30947
- <div>
30948
- <span class="eyebrow">Mail</span>
30949
- <h2>${esc(item.title)}</h2>
30950
- <div class="meta-text">
30951
- ${esc(getMailSourceLabel(item))}
30952
- ${createdLabel ? ` · ${esc(createdLabel)}` : ""}
30953
- ${tags ? ` · ${esc(tags)}` : ""}
30954
- </div>
31476
+ <header class="mail-detail-header">
31477
+ <h1 class="mail-detail-title">${esc(item.title)}</h1>
31478
+ <div class="mail-detail-meta">
31479
+ ${metaParts.map((part) => `<span>${esc(part)}</span>`).join("")}
30955
31480
  </div>
30956
- <div class="page-header-actions">
30957
- <span class="pill">${unread ? "Unread" : "Read"}</span>
30958
- ${archived ? '<span class="pill">Archived</span>' : ""}
30959
- </div>
30960
- </div>
30961
- <div class="toolbar" data-oysterun-mail-detail-actions="true">
30962
- <button class="btn-soft" type="button" data-mail-action="${
30963
- unread ? "read" : "unread"
30964
- }" data-mail-id="${esc(item.id)}">${
30965
- unread ? "Mark read" : "Mark unread"
30966
- }</button>
30967
- <button class="btn-soft" type="button" data-mail-action="${
30968
- archived ? "unarchive" : "archive"
30969
- }" data-mail-id="${esc(item.id)}">${
30970
- archived ? "Unarchive" : "Archive"
30971
- }</button>
30972
- <button class="btn-soft" type="button" data-mail-action="copy" data-mail-id="${esc(
30973
- item.id
30974
- )}">Copy mail link</button>
30975
- <button class="btn-ghost" type="button" data-mail-action="delete" data-mail-id="${esc(
30976
- item.id
30977
- )}">Delete</button>
30978
- </div>
30979
- ${
30980
- item.summary
30981
- ? `<p class="field-hint">${esc(item.summary)}</p>`
30982
- : ""
30983
- }
31481
+ </header>
30984
31482
  <div
30985
31483
  class="mail-detail-body"
30986
31484
  data-oysterun-mail-detail-renderer="${esc(item.body_format)}"
@@ -31023,6 +31521,7 @@ Waiting for Claude login output…</pre
31023
31521
  null;
31024
31522
  detailRoot.innerHTML = renderMailDetail(item);
31025
31523
  hydrateRichContent(detailRoot);
31524
+ hydrateMailHtmlFrames(detailRoot);
31026
31525
  }
31027
31526
  return;
31028
31527
  }
@@ -31292,11 +31791,25 @@ Waiting for Claude login output…</pre
31292
31791
 
31293
31792
  function handleMailClick(event) {
31294
31793
  const target = event.target instanceof Element ? event.target : null;
31295
- if (!target || !$("mail-view")?.contains(target)) return false;
31794
+ if (!target) return false;
31795
+ const topbarMenuButton = target.closest(
31796
+ "#topbar-mail-actions-btn[data-mail-menu-action='open']"
31797
+ );
31798
+ if (topbarMenuButton) {
31799
+ event.preventDefault();
31800
+ event.stopPropagation();
31801
+ openMailRowActionMenu(
31802
+ topbarMenuButton,
31803
+ topbarMenuButton.getAttribute("data-mail-id") || ""
31804
+ );
31805
+ return true;
31806
+ }
31807
+ if (!$("mail-view")?.contains(target)) return false;
31296
31808
  const filterButton = target.closest("[data-mail-filter]");
31297
31809
  if (filterButton) {
31298
31810
  event.preventDefault();
31299
31811
  event.stopPropagation();
31812
+ closeMailRowActionMenu();
31300
31813
  void navigateToRoute(
31301
31814
  buildMailRoute({
31302
31815
  filter: filterButton.getAttribute("data-mail-filter") || "inbox",
@@ -31304,10 +31817,21 @@ Waiting for Claude login output…</pre
31304
31817
  );
31305
31818
  return true;
31306
31819
  }
31820
+ const menuButton = target.closest("[data-mail-menu-action='open']");
31821
+ if (menuButton) {
31822
+ event.preventDefault();
31823
+ event.stopPropagation();
31824
+ openMailRowActionMenu(
31825
+ menuButton,
31826
+ menuButton.getAttribute("data-mail-id") || ""
31827
+ );
31828
+ return true;
31829
+ }
31307
31830
  const actionButton = target.closest("[data-mail-action][data-mail-id]");
31308
31831
  if (!actionButton) return false;
31309
31832
  event.preventDefault();
31310
31833
  event.stopPropagation();
31834
+ closeMailRowActionMenu();
31311
31835
  void performMailAction(
31312
31836
  actionButton.getAttribute("data-mail-id") || "",
31313
31837
  actionButton.getAttribute("data-mail-action") || ""
@@ -36412,6 +36936,7 @@ Waiting for Claude login output…</pre
36412
36936
 
36413
36937
  function renderSessionLoopRow(row) {
36414
36938
  const enabled = row.enabled === true;
36939
+ const pending = sessionLoopState.pendingTargets?.[row.id] === true;
36415
36940
  const expanded = sessionLoopState.editingId === row.id;
36416
36941
  const intervalLabel = row.interval_token
36417
36942
  ? formatSessionLoopIntervalToken(row.interval_token)
@@ -36422,6 +36947,7 @@ Waiting for Claude login output…</pre
36422
36947
  data-loop-row-id="${esc(row.id)}"
36423
36948
  data-oysterun-session-loop-row="true"
36424
36949
  data-oysterun-session-loop-enabled="${String(enabled)}"
36950
+ data-oysterun-session-loop-pending="${String(pending)}"
36425
36951
  data-oysterun-session-loop-storage-owner="${esc(
36426
36952
  row.storage_owner || "agent_folder_oysterun_loops_json"
36427
36953
  )}"
@@ -36465,10 +36991,18 @@ Waiting for Claude login output…</pre
36465
36991
  type="button"
36466
36992
  role="switch"
36467
36993
  aria-checked="${enabled ? "true" : "false"}"
36468
- title="${enabled ? "Enabled" : "Disabled"}"
36994
+ aria-disabled="${pending ? "true" : "false"}"
36995
+ title="${
36996
+ pending ? "Updating..." : enabled ? "Enabled" : "Disabled"
36997
+ }"
36998
+ ${pending ? "disabled" : ""}
36469
36999
  data-loop-action="toggle-row"
36470
37000
  data-loop-id="${esc(row.id)}"
36471
37001
  data-oysterun-session-loop-row-toggle="${esc(row.id)}"
37002
+ data-oysterun-session-loop-row-toggle-pending="${String(pending)}"
37003
+ data-oysterun-session-loop-row-toggle-desired="${
37004
+ enabled ? "false" : "true"
37005
+ }"
36472
37006
  >
36473
37007
  <span aria-hidden="true"></span>
36474
37008
  </button>
@@ -36635,12 +37169,23 @@ Waiting for Claude login output…</pre
36635
37169
  const liveSession = getSelectedLiveSession();
36636
37170
  const row = findSessionLoopRow(scheduleId);
36637
37171
  if (!liveSession || !row) return;
37172
+ if (sessionLoopState.pendingTargets?.[scheduleId] === true) return;
37173
+ sessionLoopState = {
37174
+ ...sessionLoopState,
37175
+ pendingTargets: {
37176
+ ...(sessionLoopState.pendingTargets || {}),
37177
+ [scheduleId]: true,
37178
+ },
37179
+ error: "",
37180
+ };
37181
+ renderSessionLoopSection(liveSession);
36638
37182
  try {
36639
37183
  await api(
36640
37184
  "PATCH",
36641
37185
  `/session/loops/${encodeURIComponent(scheduleId)}`,
36642
37186
  {
36643
37187
  session_id: liveSession.sessionId,
37188
+ desired_enabled: enabled,
36644
37189
  enabled,
36645
37190
  runtime_only: true,
36646
37191
  }
@@ -36654,6 +37199,16 @@ Waiting for Claude login output…</pre
36654
37199
  error: err.message || String(err),
36655
37200
  };
36656
37201
  renderSessionLoopSection(liveSession);
37202
+ } finally {
37203
+ const nextPendingTargets = {
37204
+ ...(sessionLoopState.pendingTargets || {}),
37205
+ };
37206
+ delete nextPendingTargets[scheduleId];
37207
+ sessionLoopState = {
37208
+ ...sessionLoopState,
37209
+ pendingTargets: nextPendingTargets,
37210
+ };
37211
+ renderSessionLoopSection(getSelectedLiveSession());
36657
37212
  }
36658
37213
  }
36659
37214
 
@@ -36730,6 +37285,8 @@ Waiting for Claude login output…</pre
36730
37285
  return true;
36731
37286
  }
36732
37287
  if (action === "toggle-row") {
37288
+ if (sessionLoopState.pendingTargets?.[scheduleId] === true)
37289
+ return true;
36733
37290
  const row = findSessionLoopRow(scheduleId);
36734
37291
  if (row) {
36735
37292
  void toggleSessionLoop(scheduleId, row.enabled !== true);
@@ -45386,6 +45943,94 @@ Waiting for Claude login output…</pre
45386
45943
  return false;
45387
45944
  }
45388
45945
 
45946
+ function resizeMailHtmlFrame(frame) {
45947
+ if (!frame) return;
45948
+ let doc;
45949
+ try {
45950
+ doc = frame.contentDocument;
45951
+ } catch {
45952
+ return;
45953
+ }
45954
+ if (!doc) return;
45955
+ const bodyHeight = doc.body?.scrollHeight || 0;
45956
+ const documentHeight = doc.documentElement?.scrollHeight || 0;
45957
+ const height = Math.max(360, bodyHeight, documentHeight);
45958
+ frame.style.height = `${Math.ceil(height)}px`;
45959
+ }
45960
+
45961
+ function normalizeMailHtmlFrameLinks(frame) {
45962
+ let doc;
45963
+ try {
45964
+ doc = frame.contentDocument;
45965
+ } catch {
45966
+ return;
45967
+ }
45968
+ if (!doc?.body) return;
45969
+ doc.querySelectorAll("a[href]").forEach((anchor) => {
45970
+ const rawHref = anchor.getAttribute("href") || "";
45971
+ const unsupportedText =
45972
+ getUnsupportedLocalPathFailClosedText(rawHref);
45973
+ if (unsupportedText) {
45974
+ anchor.replaceWith(doc.createTextNode(unsupportedText));
45975
+ return;
45976
+ }
45977
+ const safeHref = normalizeTrustedMailHref(rawHref);
45978
+ if (!safeHref) {
45979
+ anchor.removeAttribute("href");
45980
+ anchor.setAttribute("aria-disabled", "true");
45981
+ return;
45982
+ }
45983
+ anchor.setAttribute("href", safeHref);
45984
+ applyTrustedMailAnchorPolicy(anchor);
45985
+ if (anchor.dataset.boundMailHtmlLink === "true") return;
45986
+ anchor.dataset.boundMailHtmlLink = "true";
45987
+ anchor.addEventListener("click", (event) => {
45988
+ const parsed = parseInteractiveRichHref(
45989
+ anchor.getAttribute("href") || anchor.href || ""
45990
+ );
45991
+ if (!parsed) return;
45992
+ event.preventDefault();
45993
+ event.stopPropagation();
45994
+ void handleInteractiveRichLinkClick(anchor).catch((err) => {
45995
+ showBanner(err.message || "Could not open link.", true);
45996
+ });
45997
+ });
45998
+ });
45999
+ }
46000
+
46001
+ function hydrateMailHtmlFrame(frame) {
46002
+ if (!frame || frame.dataset.boundMailHtmlFrame === "true") return;
46003
+ frame.dataset.boundMailHtmlFrame = "true";
46004
+ const applyFramePolicy = () => {
46005
+ normalizeMailHtmlFrameLinks(frame);
46006
+ resizeMailHtmlFrame(frame);
46007
+ let doc;
46008
+ try {
46009
+ doc = frame.contentDocument;
46010
+ } catch {
46011
+ return;
46012
+ }
46013
+ doc?.querySelectorAll("img,video").forEach((media) => {
46014
+ if (media.dataset.boundMailHtmlResize === "true") return;
46015
+ media.dataset.boundMailHtmlResize = "true";
46016
+ media.addEventListener("load", () => resizeMailHtmlFrame(frame));
46017
+ media.addEventListener("error", () => resizeMailHtmlFrame(frame));
46018
+ });
46019
+ window.setTimeout(() => resizeMailHtmlFrame(frame), 100);
46020
+ window.setTimeout(() => resizeMailHtmlFrame(frame), 600);
46021
+ window.setTimeout(() => resizeMailHtmlFrame(frame), 1600);
46022
+ };
46023
+ frame.addEventListener("load", applyFramePolicy);
46024
+ window.setTimeout(applyFramePolicy, 0);
46025
+ }
46026
+
46027
+ function hydrateMailHtmlFrames(root) {
46028
+ if (!root) return;
46029
+ root
46030
+ .querySelectorAll("iframe[data-oysterun-mail-html-frame='true']")
46031
+ .forEach((frame) => hydrateMailHtmlFrame(frame));
46032
+ }
46033
+
45389
46034
  function hydrateRichContent(root) {
45390
46035
  if (!root) return;
45391
46036
  root