oysterun 1.4.5 → 1.4.7

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 (34) hide show
  1. package/dev/client/web/index.html +792 -116
  2. package/dev/client/web-chat/dist/assets/{ReactPrism-Pt9CJmFr.js → ReactPrism-DQpy9C6y.js} +1 -1
  3. package/dev/client/web-chat/dist/assets/{ReactPrism-Pt9CJmFr.js.map → ReactPrism-DQpy9C6y.js.map} +1 -1
  4. package/dev/client/web-chat/dist/assets/{index-BUELz8_v.js → index-5-_pCTeV.js} +2 -2
  5. package/dev/client/web-chat/dist/assets/{index-BUELz8_v.js.map → index-5-_pCTeV.js.map} +1 -1
  6. package/dev/client/web-chat/dist/assets/{index-BFyCCLfm.js → index-DKEDScEA.js} +90 -90
  7. package/dev/client/web-chat/dist/assets/index-DKEDScEA.js.map +1 -0
  8. package/dev/client/web-chat/dist/assets/{index-DjA8TMy2.js → index-DogixscE.js} +1 -1
  9. package/dev/client/web-chat/dist/assets/{index-DjA8TMy2.js.map → index-DogixscE.js.map} +1 -1
  10. package/dev/client/web-chat/dist/assets/{pdf-B5HuyJzk.js → pdf-AYwu3-03.js} +3 -3
  11. package/dev/client/web-chat/dist/assets/{pdf-B5HuyJzk.js.map → pdf-AYwu3-03.js.map} +1 -1
  12. package/dev/client/web-chat/dist/assets/{web-BuBqjNYh.js → web-BGIphkU6.js} +1 -1
  13. package/dev/client/web-chat/dist/assets/{web-BuBqjNYh.js.map → web-BGIphkU6.js.map} +1 -1
  14. package/dev/client/web-chat/dist/assets/{web-DU_hq0TE.js → web-CXqN5ZBB.js} +1 -1
  15. package/dev/client/web-chat/dist/assets/{web-DU_hq0TE.js.map → web-CXqN5ZBB.js.map} +1 -1
  16. package/dev/client/web-chat/dist/index.html +1 -1
  17. package/host-service/adapters/claude-code-adapter.mjs +21 -2
  18. package/host-service/adapters/codex-app-server-adapter.mjs +27 -3
  19. package/host-service/assets/product-skills/Oysterun/modules/oysterun-mail/SKILL.md +20 -1
  20. package/host-service/assets/product-skills/Oysterun/modules/oysterun-scheduler/SKILL.md +52 -0
  21. package/host-service/assets/product-skills/oysterun-mail/SKILL.md +20 -1
  22. package/host-service/assets/product-skills/oysterun-scheduler/SKILL.md +52 -0
  23. package/host-service/cli/product-cli.mjs +46 -3
  24. package/host-service/mail-store.mjs +13 -2
  25. package/host-service/package.json +1 -1
  26. package/host-service/scheduler-runner.mjs +2 -0
  27. package/host-service/scheduler-service.mjs +163 -17
  28. package/host-service/server.mjs +304 -72
  29. package/host-service/session-loop-definitions.mjs +36 -1
  30. package/host-service/session-manager.mjs +98 -3
  31. package/package.json +1 -1
  32. package/tool_scripts/oysterun_service_common.sh +75 -0
  33. package/tool_scripts/start_oysterun.sh +17 -42
  34. package/dev/client/web-chat/dist/assets/index-BFyCCLfm.js.map +0 -1
@@ -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
  };
@@ -12526,6 +12765,7 @@ Waiting for Claude login output…</pre
12526
12765
  const SESSION_UPLOADS_DISABLED_MESSAGE =
12527
12766
  "File uploads are unavailable in this chat.";
12528
12767
  let pendingAttachments = [];
12768
+ let routeCInterruptRequestInFlight = false;
12529
12769
  const uploadPreviewObjectUrlCache = new Map();
12530
12770
  const uploadPreviewObjectUrlRequests = new Map();
12531
12771
  let currentStreamAgentId = null;
@@ -12545,6 +12785,7 @@ Waiting for Claude login output…</pre
12545
12785
  let sessionLoopState = createSessionLoopState();
12546
12786
  let hostSchedulerState = createHostSchedulerState();
12547
12787
  let mailState = createMailState();
12788
+ let mailRowActionMenuState = null;
12548
12789
  let schedulerSetupCopyState = createSchedulerSetupCopyState();
12549
12790
  let hostSchedulerActionMenuState = null;
12550
12791
  let sessionLoopActionMenuState = null;
@@ -27748,8 +27989,8 @@ Waiting for Claude login output…</pre
27748
27989
  if (screen === "mail") {
27749
27990
  if (normalized.mailId) {
27750
27991
  return {
27751
- title: "Mail",
27752
- subtitle: "Host-owned Mail item detail.",
27992
+ title: "Email",
27993
+ subtitle: "",
27753
27994
  };
27754
27995
  }
27755
27996
  return {
@@ -27838,6 +28079,7 @@ Waiting for Claude login output…</pre
27838
28079
  const isAgentProfile = screen === "agent-profile";
27839
28080
  const isBrowser = screen === "browser";
27840
28081
  const currentRoute = normalizeClientRoute(getCurrentRoute());
28082
+ const isMailDetail = screen === "mail" && Boolean(currentRoute.mailId);
27841
28083
  const isExplorer = screen === "explorer";
27842
28084
  const isSessionSetupFolderPicker =
27843
28085
  screen === "folder-picker" &&
@@ -27872,6 +28114,30 @@ Waiting for Claude login output…</pre
27872
28114
  setVisible("topbar-scheduler-new-btn", isSchedulerList);
27873
28115
  setVisible("topbar-session-loop-new-btn", isSessionLoop);
27874
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
+ }
27875
28141
  const topbarExplorerAddFolderButton = $(
27876
28142
  "topbar-explorer-add-folder-btn"
27877
28143
  );
@@ -28059,6 +28325,7 @@ Waiting for Claude login output…</pre
28059
28325
  $("topbar")?.classList.toggle("scheduler-mode", isSchedulerList);
28060
28326
  $("topbar")?.classList.toggle("explorer-mode", isExplorer);
28061
28327
  $("topbar")?.classList.toggle("session-loop-mode", isSessionLoop);
28328
+ $("topbar")?.classList.toggle("mail-detail-mode", isMailDetail);
28062
28329
  $("topbar")?.classList.toggle("browser-mode", isBrowser);
28063
28330
  $("topbar")?.classList.toggle(
28064
28331
  "scheduler-setup-mode",
@@ -30520,6 +30787,10 @@ Waiting for Claude login output…</pre
30520
30787
  badge.textContent = count > 99 ? "99+" : String(count);
30521
30788
  badge.classList.toggle("hidden", count <= 0);
30522
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
+ );
30523
30794
  }
30524
30795
 
30525
30796
  function renderMailFilterBar() {
@@ -30587,6 +30858,122 @@ Waiting for Claude login output…</pre
30587
30858
  `;
30588
30859
  }
30589
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
+
30590
30977
  function renderMailRow(item) {
30591
30978
  const mailId = item.id;
30592
30979
  const unread = !item.read_at;
@@ -30628,55 +31015,148 @@ Waiting for Claude login output…</pre
30628
31015
  </span>
30629
31016
  </span>
30630
31017
  </button>
30631
- <div class="web-site-row-actions" data-oysterun-mail-row-actions="true">
30632
- <button
30633
- class="web-site-row-action"
30634
- type="button"
30635
- title="${unread ? "Mark read" : "Mark unread"}"
30636
- aria-label="${unread ? "Mark read" : "Mark unread"}"
30637
- data-mail-action="${unread ? "read" : "unread"}"
30638
- data-mail-id="${esc(mailId)}"
30639
- ${getMailActionBusyAttr(mailId)}
30640
- >
30641
- ${unread ? "Read" : "Unread"}
30642
- </button>
30643
- <button
30644
- class="web-site-row-action"
30645
- type="button"
30646
- title="${archived ? "Unarchive" : "Archive"}"
30647
- aria-label="${archived ? "Unarchive" : "Archive"}"
30648
- data-mail-action="${archived ? "unarchive" : "archive"}"
30649
- data-mail-id="${esc(mailId)}"
30650
- ${getMailActionBusyAttr(mailId)}
30651
- >
30652
- ${archived ? "Inbox" : "Archive"}
30653
- </button>
30654
- <button
30655
- class="web-site-row-action"
30656
- type="button"
30657
- title="Copy mail link"
30658
- aria-label="Copy mail link"
30659
- data-mail-action="copy"
30660
- data-mail-id="${esc(mailId)}"
30661
- >
30662
- Copy
30663
- </button>
31018
+ <div class="mail-row-actions" data-oysterun-mail-row-actions="true">
30664
31019
  <button
30665
- class="web-site-row-action"
31020
+ class="mail-row-menu-button"
30666
31021
  type="button"
30667
- title="Delete"
30668
- aria-label="Delete"
30669
- data-mail-action="delete"
31022
+ title="Mail actions"
31023
+ aria-label="Mail actions for ${esc(item.title)}"
31024
+ data-mail-menu-action="open"
30670
31025
  data-mail-id="${esc(mailId)}"
30671
31026
  ${getMailActionBusyAttr(mailId)}
30672
31027
  >
30673
- Delete
31028
+ <span aria-hidden="true"></span>
30674
31029
  </button>
30675
31030
  </div>
30676
31031
  </article>
30677
31032
  `;
30678
31033
  }
30679
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
+
30680
31160
  function normalizeTrustedMailHref(rawHref) {
30681
31161
  const href = String(rawHref || "").trim();
30682
31162
  if (!href || href.startsWith("#")) return href;
@@ -30889,15 +31369,57 @@ Waiting for Claude login output…</pre
30889
31369
  return doc.body.innerHTML;
30890
31370
  }
30891
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
+
30892
31408
  function renderMailBody(item) {
30893
31409
  if (item.body_format === "html") {
30894
31410
  return `
30895
31411
  <div
30896
- class="markdown-doc rich-text"
30897
- data-oysterun-mail-body-format="trusted_html"
30898
- 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"
30899
31415
  >
30900
- ${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>
30901
31423
  </div>
30902
31424
  `;
30903
31425
  }
@@ -30935,51 +31457,28 @@ Waiting for Claude login output…</pre
30935
31457
  const unread = !item.read_at;
30936
31458
  const createdLabel = formatMailTimestamp(item.created_at);
30937
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);
30938
31468
  return `
30939
31469
  <article
30940
31470
  class="mail-detail"
30941
31471
  data-oysterun-mail-detail-id="${esc(item.id)}"
30942
31472
  data-oysterun-mail-detail-read="${unread ? "false" : "true"}"
30943
31473
  data-oysterun-mail-detail-archived="${archived ? "true" : "false"}"
31474
+ data-oysterun-mail-detail-layout="title_meta_body"
30944
31475
  >
30945
- <div class="page-header">
30946
- <div>
30947
- <span class="eyebrow">Mail</span>
30948
- <h2>${esc(item.title)}</h2>
30949
- <div class="meta-text">
30950
- ${esc(getMailSourceLabel(item))}
30951
- ${createdLabel ? ` · ${esc(createdLabel)}` : ""}
30952
- ${tags ? ` · ${esc(tags)}` : ""}
30953
- </div>
30954
- </div>
30955
- <div class="page-header-actions">
30956
- <span class="pill">${unread ? "Unread" : "Read"}</span>
30957
- ${archived ? '<span class="pill">Archived</span>' : ""}
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("")}
30958
31480
  </div>
30959
- </div>
30960
- <div class="toolbar" data-oysterun-mail-detail-actions="true">
30961
- <button class="btn-soft" type="button" data-mail-action="${
30962
- unread ? "read" : "unread"
30963
- }" data-mail-id="${esc(item.id)}">${
30964
- unread ? "Mark read" : "Mark unread"
30965
- }</button>
30966
- <button class="btn-soft" type="button" data-mail-action="${
30967
- archived ? "unarchive" : "archive"
30968
- }" data-mail-id="${esc(item.id)}">${
30969
- archived ? "Unarchive" : "Archive"
30970
- }</button>
30971
- <button class="btn-soft" type="button" data-mail-action="copy" data-mail-id="${esc(
30972
- item.id
30973
- )}">Copy mail link</button>
30974
- <button class="btn-ghost" type="button" data-mail-action="delete" data-mail-id="${esc(
30975
- item.id
30976
- )}">Delete</button>
30977
- </div>
30978
- ${
30979
- item.summary
30980
- ? `<p class="field-hint">${esc(item.summary)}</p>`
30981
- : ""
30982
- }
31481
+ </header>
30983
31482
  <div
30984
31483
  class="mail-detail-body"
30985
31484
  data-oysterun-mail-detail-renderer="${esc(item.body_format)}"
@@ -31022,6 +31521,7 @@ Waiting for Claude login output…</pre
31022
31521
  null;
31023
31522
  detailRoot.innerHTML = renderMailDetail(item);
31024
31523
  hydrateRichContent(detailRoot);
31524
+ hydrateMailHtmlFrames(detailRoot);
31025
31525
  }
31026
31526
  return;
31027
31527
  }
@@ -31291,11 +31791,25 @@ Waiting for Claude login output…</pre
31291
31791
 
31292
31792
  function handleMailClick(event) {
31293
31793
  const target = event.target instanceof Element ? event.target : null;
31294
- 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;
31295
31808
  const filterButton = target.closest("[data-mail-filter]");
31296
31809
  if (filterButton) {
31297
31810
  event.preventDefault();
31298
31811
  event.stopPropagation();
31812
+ closeMailRowActionMenu();
31299
31813
  void navigateToRoute(
31300
31814
  buildMailRoute({
31301
31815
  filter: filterButton.getAttribute("data-mail-filter") || "inbox",
@@ -31303,10 +31817,21 @@ Waiting for Claude login output…</pre
31303
31817
  );
31304
31818
  return true;
31305
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
+ }
31306
31830
  const actionButton = target.closest("[data-mail-action][data-mail-id]");
31307
31831
  if (!actionButton) return false;
31308
31832
  event.preventDefault();
31309
31833
  event.stopPropagation();
31834
+ closeMailRowActionMenu();
31310
31835
  void performMailAction(
31311
31836
  actionButton.getAttribute("data-mail-id") || "",
31312
31837
  actionButton.getAttribute("data-mail-action") || ""
@@ -36411,6 +36936,7 @@ Waiting for Claude login output…</pre
36411
36936
 
36412
36937
  function renderSessionLoopRow(row) {
36413
36938
  const enabled = row.enabled === true;
36939
+ const pending = sessionLoopState.pendingTargets?.[row.id] === true;
36414
36940
  const expanded = sessionLoopState.editingId === row.id;
36415
36941
  const intervalLabel = row.interval_token
36416
36942
  ? formatSessionLoopIntervalToken(row.interval_token)
@@ -36421,6 +36947,7 @@ Waiting for Claude login output…</pre
36421
36947
  data-loop-row-id="${esc(row.id)}"
36422
36948
  data-oysterun-session-loop-row="true"
36423
36949
  data-oysterun-session-loop-enabled="${String(enabled)}"
36950
+ data-oysterun-session-loop-pending="${String(pending)}"
36424
36951
  data-oysterun-session-loop-storage-owner="${esc(
36425
36952
  row.storage_owner || "agent_folder_oysterun_loops_json"
36426
36953
  )}"
@@ -36464,10 +36991,18 @@ Waiting for Claude login output…</pre
36464
36991
  type="button"
36465
36992
  role="switch"
36466
36993
  aria-checked="${enabled ? "true" : "false"}"
36467
- title="${enabled ? "Enabled" : "Disabled"}"
36994
+ aria-disabled="${pending ? "true" : "false"}"
36995
+ title="${
36996
+ pending ? "Updating..." : enabled ? "Enabled" : "Disabled"
36997
+ }"
36998
+ ${pending ? "disabled" : ""}
36468
36999
  data-loop-action="toggle-row"
36469
37000
  data-loop-id="${esc(row.id)}"
36470
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
+ }"
36471
37006
  >
36472
37007
  <span aria-hidden="true"></span>
36473
37008
  </button>
@@ -36634,12 +37169,23 @@ Waiting for Claude login output…</pre
36634
37169
  const liveSession = getSelectedLiveSession();
36635
37170
  const row = findSessionLoopRow(scheduleId);
36636
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);
36637
37182
  try {
36638
37183
  await api(
36639
37184
  "PATCH",
36640
37185
  `/session/loops/${encodeURIComponent(scheduleId)}`,
36641
37186
  {
36642
37187
  session_id: liveSession.sessionId,
37188
+ desired_enabled: enabled,
36643
37189
  enabled,
36644
37190
  runtime_only: true,
36645
37191
  }
@@ -36653,6 +37199,16 @@ Waiting for Claude login output…</pre
36653
37199
  error: err.message || String(err),
36654
37200
  };
36655
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());
36656
37212
  }
36657
37213
  }
36658
37214
 
@@ -36729,6 +37285,8 @@ Waiting for Claude login output…</pre
36729
37285
  return true;
36730
37286
  }
36731
37287
  if (action === "toggle-row") {
37288
+ if (sessionLoopState.pendingTargets?.[scheduleId] === true)
37289
+ return true;
36732
37290
  const row = findSessionLoopRow(scheduleId);
36733
37291
  if (row) {
36734
37292
  void toggleSessionLoop(scheduleId, row.enabled !== true);
@@ -45385,6 +45943,94 @@ Waiting for Claude login output…</pre
45385
45943
  return false;
45386
45944
  }
45387
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
+
45388
46034
  function hydrateRichContent(root) {
45389
46035
  if (!root) return;
45390
46036
  root
@@ -45847,6 +46493,9 @@ Waiting for Claude login output…</pre
45847
46493
  async function doInterruptSession() {
45848
46494
  const liveSession = getSelectedLiveSession();
45849
46495
  if (!liveSession) return;
46496
+ if (routeCInterruptRequestInFlight) return;
46497
+ routeCInterruptRequestInFlight = true;
46498
+ updateComposerAvailability();
45850
46499
  try {
45851
46500
  await api("POST", "/session/interrupt", {
45852
46501
  session_id: liveSession.sessionId,
@@ -45854,6 +46503,9 @@ Waiting for Claude login output…</pre
45854
46503
  appendSystem(`Interrupted ${liveSession.sessionId}.`);
45855
46504
  } catch (err) {
45856
46505
  showBanner(err.message, true);
46506
+ } finally {
46507
+ routeCInterruptRequestInFlight = false;
46508
+ updateComposerAvailability();
45857
46509
  }
45858
46510
  }
45859
46511
 
@@ -47005,6 +47657,30 @@ Waiting for Claude login output…</pre
47005
47657
  return;
47006
47658
  }
47007
47659
 
47660
+ if (text.startsWith("!!")) {
47661
+ sendBtn.disabled =
47662
+ routeCInterruptRequestInFlight ||
47663
+ !hasActiveSession() ||
47664
+ !getSelectedLiveSession();
47665
+ sendBtn.dataset.oysterunRoutecInterruptInFlight = String(
47666
+ routeCInterruptRequestInFlight
47667
+ );
47668
+ sendBtn.dataset.oysterunRoutecInterruptOneShotDisabled = String(
47669
+ routeCInterruptRequestInFlight
47670
+ );
47671
+ setSendButtonState(
47672
+ routeCInterruptRequestInFlight
47673
+ ? "Interrupting..."
47674
+ : "Stop response",
47675
+ true
47676
+ );
47677
+ updateComposerDebug();
47678
+ return;
47679
+ }
47680
+
47681
+ delete sendBtn.dataset.oysterunRoutecInterruptInFlight;
47682
+ delete sendBtn.dataset.oysterunRoutecInterruptOneShotDisabled;
47683
+
47008
47684
  if (hasAttachments && matchedCommand) {
47009
47685
  sendBtn.disabled = true;
47010
47686
  setSendButtonState("Attachments only work with normal chat");