@oar-sh/oar 0.9.2 → 0.9.4

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 (138) hide show
  1. package/.github/extensions/web-relay/extension.mjs +6 -1
  2. package/.github/extensions/web-relay/skills/tool-activity.mjs +7 -0
  3. package/CHANGELOG.md +336 -0
  4. package/README.md +455 -423
  5. package/bin/oar.js +25 -10
  6. package/package.json +11 -5
  7. package/server/README.md +44 -4
  8. package/server/claude-worker/claude-remote-relay-tool.mjs +99 -0
  9. package/server/claude-worker/claude-sdk-adapter.mjs +26 -4
  10. package/server/claude-worker/claude-session-process.mjs +1073 -69
  11. package/server/claude-worker/claude-session-worker.mjs +45 -4
  12. package/server/claude-worker/claude-turn-publisher.mjs +81 -20
  13. package/server/claude-worker/claude-worker-link-wiring.mjs +9 -0
  14. package/server/claude-worker/sdk-message-normalizer.mjs +7 -0
  15. package/server/copilot-worker/copilot-ask-user-tool.mjs +89 -0
  16. package/server/copilot-worker/copilot-continuation-signals.mjs +55 -0
  17. package/server/copilot-worker/copilot-question-bridge.mjs +34 -12
  18. package/server/copilot-worker/copilot-remote-relay-tool.mjs +59 -0
  19. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +113 -14
  20. package/server/copilot-worker/copilot-sdk-session-process.mjs +2349 -498
  21. package/server/copilot-worker/copilot-sdk-session-worker.mjs +82 -20
  22. package/server/cursor-worker/cursor-remote-relay-tool.mjs +65 -0
  23. package/server/cursor-worker/cursor-turn-runner.mjs +45 -2
  24. package/server/cursor-worker/sdk-message-normalizer.mjs +25 -0
  25. package/server/db-schema.mjs +28 -0
  26. package/server/grok-worker/acp-client.mjs +92 -6
  27. package/server/grok-worker/grok-turn-runner.mjs +59 -8
  28. package/server/grok-worker/sdk-message-normalizer.mjs +13 -0
  29. package/server/mcp/oar-mcp-server.mjs +473 -0
  30. package/server/migrations/0004-steer-settle-markers.mjs +119 -0
  31. package/server/migrations/0005-remote-relays.mjs +90 -0
  32. package/server/public/app/action-confirmations.js +141 -13
  33. package/server/public/app/api-client.js +125 -2
  34. package/server/public/app/ask-user-view.js +102 -13
  35. package/server/public/app/attachments-view.js +37 -2
  36. package/server/public/app/background-task-timeout-options.mjs +9 -0
  37. package/server/public/app/background-tasks-view.mjs +33 -18
  38. package/server/public/app/bootstrap.js +55 -14
  39. package/server/public/app/composer-control-state.mjs +52 -34
  40. package/server/public/app/conversation-deep-link.mjs +42 -0
  41. package/server/public/app/conversation-draft-sync.mjs +187 -0
  42. package/server/public/app/conversation-list-filter.mjs +86 -0
  43. package/server/public/app/conversation-provider-indicator.mjs +8 -0
  44. package/server/public/app/conversation-view.js +1016 -200
  45. package/server/public/app/emoji-view.js +2 -0
  46. package/server/public/app/font-scaling.js +4 -37
  47. package/server/public/app/host-suspend-ui.js +272 -0
  48. package/server/public/app/journal-view.js +149 -6
  49. package/server/public/app/mention-autocomplete.mjs +298 -0
  50. package/server/public/app/model-selector-options.mjs +1 -1
  51. package/server/public/app/push-settings.js +1 -0
  52. package/server/public/app/pwa-install.js +8 -3
  53. package/server/public/app/question-multi-select.mjs +52 -0
  54. package/server/public/app/relay-prompt-sanitizer.mjs +61 -3
  55. package/server/public/app/remote-relay-origin-view.mjs +76 -0
  56. package/server/public/app/remote-relay-shared.mjs +85 -0
  57. package/server/public/app/remote-relays-settings.js +618 -0
  58. package/server/public/app/remote-relays-store.mjs +126 -0
  59. package/server/public/app/settings-modal.js +12 -8
  60. package/server/public/app/settings-tabs.js +1 -1
  61. package/server/public/app/socket-handlers.js +37 -1
  62. package/server/public/app/steered-turn-merge.mjs +100 -0
  63. package/server/public/app/store.js +126 -15
  64. package/server/public/app/sync-outbox.mjs +16 -8
  65. package/server/public/app/thinking-follow.js +260 -0
  66. package/server/public/app/tool-activity-emoji.mjs +40 -0
  67. package/server/public/app/transcript-viewport-anchor.mjs +210 -0
  68. package/server/public/index.html +463 -17
  69. package/server/public/sw.js +7 -0
  70. package/server/relay-tools.md +7 -1
  71. package/server/repositories/message-repository.mjs +68 -18
  72. package/server/repositories/question-repository.mjs +18 -0
  73. package/server/repositories/remote-relay-repository.mjs +75 -0
  74. package/server/repositories/session-repository.mjs +13 -32
  75. package/server/routes/ask-user-routes.mjs +42 -2
  76. package/server/routes/messages-routes.mjs +572 -56
  77. package/server/routes/remote-relay-routes.mjs +133 -0
  78. package/server/routes/sessions-routes.mjs +378 -84
  79. package/server/server-runtime.mjs +303 -25
  80. package/server/services/github-ci-activity-service.mjs +199 -0
  81. package/server/services/host-suspend-command.mjs +38 -0
  82. package/server/services/host-suspend-service.mjs +254 -0
  83. package/server/services/live-turn-picker.mjs +47 -0
  84. package/server/services/push-dispatch-service.mjs +37 -4
  85. package/server/services/relay-prompt-sanitizer.mjs +17 -2
  86. package/server/services/relay-question-context.mjs +46 -0
  87. package/server/services/remote-relay-caller-context.mjs +341 -0
  88. package/server/services/remote-relay-client.mjs +332 -0
  89. package/server/services/remote-relay-dispatcher.mjs +1500 -0
  90. package/server/services/remote-relay-inbound.mjs +187 -0
  91. package/server/services/remote-relay-pairing.mjs +189 -0
  92. package/server/services/remote-relay-registry-service.mjs +581 -0
  93. package/server/services/sdk-session-import-service.mjs +49 -7
  94. package/server/services/session-worker-launch-service.mjs +131 -12
  95. package/server/services/session-worker-registry-service.mjs +75 -0
  96. package/server/services/session-worker-supervisor-service.mjs +7 -1
  97. package/server/services/session-worker-websocket-service.mjs +23 -1
  98. package/server/services/steer-resend.mjs +45 -0
  99. package/shared/background-task-timeout.mjs +12 -4
  100. package/shared/control-poller.mjs +4 -1
  101. package/shared/question-multi-select.mjs +16 -0
  102. package/shared/relay-stop-failure.mjs +15 -0
  103. package/shared/remote-relay-contract.mjs +539 -0
  104. package/shared/remote-relay-mentions.mjs +129 -0
  105. package/shared/remote-relay-tool-core.mjs +314 -0
  106. package/shared/slash-command-guard.mjs +76 -0
  107. package/shared/steer-note.mjs +66 -0
  108. package/shared/steer-settle-failure.mjs +89 -0
  109. package/shared/steer-settle-markers.mjs +18 -0
  110. package/shared/worker-crash-guard.mjs +40 -25
  111. package/shared/worker-runtime/api-client.mjs +112 -22
  112. package/shared/worker-runtime/heartbeat.mjs +55 -1
  113. package/shared/worker-runtime/runner-link-wiring.mjs +44 -0
  114. package/shared/worker-runtime/worker-websocket-link.mjs +119 -8
  115. package/server/claude-worker/claude-session-test-harness.mjs +0 -250
  116. package/server/claude-worker/fixtures/workflow-run-record.json +0 -1
  117. package/server/copilot-worker/copilot-sdk-test-harness.mjs +0 -382
  118. package/server/copilot-worker/fixtures/abort-turn.json +0 -588
  119. package/server/copilot-worker/fixtures/ask-user-turn.json +0 -1154
  120. package/server/copilot-worker/fixtures/background-timer-continuation.json +0 -445
  121. package/server/copilot-worker/fixtures/background-timer-turn.json +0 -497
  122. package/server/copilot-worker/fixtures/happy-turn.json +0 -566
  123. package/server/copilot-worker/fixtures/quota-turn.json +0 -647
  124. package/server/copilot-worker/fixtures/reasoning-turn.json +0 -625
  125. package/server/copilot-worker/fixtures/resume-turn.json +0 -700
  126. package/server/copilot-worker/fixtures/subagent-turn.json +0 -1378
  127. package/server/copilot-worker/fixtures/tool-permission-turn.json +0 -1293
  128. package/server/routes/messages-routes-test-harness.mjs +0 -111
  129. package/server/services/fixtures/claude-auth-stub.cmd +0 -1
  130. package/server/services/fixtures/claude-auth-stub.mjs +0 -76
  131. package/server/services/fixtures/claude-auth-stub.sh +0 -4
  132. package/server/services/fixtures/cli-install-stub.cmd +0 -1
  133. package/server/services/fixtures/cli-install-stub.mjs +0 -98
  134. package/server/services/fixtures/cli-install-stub.sh +0 -4
  135. package/server/services/fixtures/grok-stub.cmd +0 -1
  136. package/server/services/fixtures/grok-stub.mjs +0 -89
  137. package/server/services/fixtures/grok-stub.sh +0 -4
  138. package/shared/fixtures/copilot-catalog-raw-2026-09-07.json +0 -2363
@@ -210,6 +210,13 @@
210
210
  document.documentElement.style.setProperty('--font-scale', String(value));
211
211
  document.documentElement.style.setProperty('--font-scale-percent', `${Math.round(value * 100)}%`);
212
212
  })();
213
+ // Mirrored by syncScreenShortSide in app/store.js.
214
+ (function applyScreenShortSideEarly() {
215
+ const shortSide = Math.min(Number(screen.width) || 0, Number(screen.height) || 0);
216
+ if (!shortSide) return;
217
+ document.documentElement.style.setProperty('--screen-short-side', `${shortSide}px`);
218
+ document.documentElement.toggleAttribute('data-phone-screen', shortSide <= 500);
219
+ })();
213
220
  </script>
214
221
  <style>
215
222
  * { box-sizing: border-box; margin: 0; padding: 0; }
@@ -318,6 +325,20 @@
318
325
 
319
326
  html {
320
327
  font-size: var(--font-scale-percent, 100%);
328
+ /* Mobile browsers otherwise inflate text on rotation. */
329
+ -webkit-text-size-adjust: 100%;
330
+ text-size-adjust: 100%;
331
+ }
332
+ /* A rotated phone must keep its portrait text size. Portrait scales with
333
+ the width (3.4vw, phone rule below), which is the screen's short side;
334
+ landscape uses that short side directly. No viewport unit or height
335
+ works here: with interactive-widget=resizes-content the on-screen
336
+ keyboard shrinks the viewport, and would shrink the text with it (and
337
+ make a portrait phone match this landscape query mid-typing). :root
338
+ outranks the later width-based html rule, which a narrow phone still
339
+ matches in landscape. */
340
+ @media (pointer: coarse) and (orientation: landscape) and (max-height: 500px) {
341
+ :root[data-phone-screen] { font-size: calc(clamp(12px, calc(var(--screen-short-side) * 0.034), 14px) * var(--font-scale, 1)); }
321
342
  }
322
343
  html, body {
323
344
  height: 100%;
@@ -508,6 +529,65 @@
508
529
  box-shadow: 0 0 0 1px rgba(210, 153, 34, 0.42), 0 0 8px rgba(210, 153, 34, 0.35);
509
530
  }
510
531
 
532
+ #conv-filter-row {
533
+ position: relative;
534
+ padding: 10px 16px;
535
+ }
536
+ #conv-filter-input {
537
+ width: 100%;
538
+ box-sizing: border-box;
539
+ height: 36px;
540
+ border: 1px solid var(--border);
541
+ border-radius: 8px;
542
+ background: var(--bg2);
543
+ color: var(--text);
544
+ padding: 0 38px 0 10px;
545
+ font-size: var(--fs-meta-md);
546
+ }
547
+ /* iOS Safari zooms the page on focus for inputs under 16px, and the mobile
548
+ root font is 12-14px, so 1rem alone is not enough. */
549
+ @media (pointer: coarse), (max-width: 680px) {
550
+ #conv-filter-input { font-size: max(16px, 1rem); }
551
+ }
552
+ #conv-filter-input:focus {
553
+ outline: none;
554
+ border-color: var(--accent);
555
+ }
556
+ #conv-filter-clear {
557
+ position: absolute;
558
+ top: 50%;
559
+ right: 20px;
560
+ transform: translateY(-50%);
561
+ width: 28px;
562
+ height: 28px;
563
+ display: inline-flex;
564
+ align-items: center;
565
+ justify-content: center;
566
+ border: none;
567
+ border-radius: 6px;
568
+ background: transparent;
569
+ color: var(--muted);
570
+ cursor: pointer;
571
+ font-size: 1rem;
572
+ line-height: 1;
573
+ padding: 0;
574
+ }
575
+ #conv-filter-clear:hover { color: var(--text); background: var(--bg3); }
576
+ /* The display rule above outranks the UA [hidden] rule, same trap as the
577
+ other [hidden] overrides in this file. */
578
+ #conv-filter-clear[hidden] { display: none; }
579
+ #conv-filter-status {
580
+ position: absolute;
581
+ width: 1px;
582
+ height: 1px;
583
+ margin: -1px;
584
+ padding: 0;
585
+ overflow: hidden;
586
+ clip: rect(0 0 0 0);
587
+ white-space: nowrap;
588
+ border: 0;
589
+ }
590
+
511
591
  #new-conv-btn {
512
592
  margin: 0; padding: 10px 14px; background: var(--accent); color: #000;
513
593
  border: none; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: var(--fs-meta-md);
@@ -1408,14 +1488,58 @@
1408
1488
  width: 100%; max-width: 100%; box-sizing: border-box;
1409
1489
  min-width: 0;
1410
1490
  }
1491
+ /* Sticky inside the scroller: Stop on the live bubble is the only way to
1492
+ stop a turn, and a long turn must stay stoppable without scrolling back
1493
+ up. It sticks within its own bubble only, so it never rides over the
1494
+ messages steered in below it. The bubble's background keeps streamed
1495
+ text from showing through; `top` cancels #messages' padding-top (sticky
1496
+ insets start at the padding edge), so nothing scrolls visibly above it. */
1411
1497
  .thinking-bubble-header {
1412
1498
  display: flex;
1413
1499
  align-items: center;
1414
1500
  justify-content: space-between;
1415
1501
  gap: 12px;
1416
1502
  margin-bottom: 4px;
1503
+ position: sticky;
1504
+ top: -20px;
1505
+ z-index: 2;
1506
+ background: var(--bot-bg);
1507
+ padding: 4px 0;
1417
1508
  }
1418
1509
  .thinking-bubble-header:empty { display: none; margin-bottom: 0; }
1510
+ /* Follow buttons (thinking-follow.js): 💭 thoughts, 📃 answer, 🛠️ tools.
1511
+ Chips on the right of the header, Stop stays left; the armed one gets
1512
+ the accent ring. */
1513
+ .thinking-follow-group {
1514
+ display: inline-flex;
1515
+ align-items: center;
1516
+ gap: 4px;
1517
+ flex-shrink: 0;
1518
+ margin-left: auto; /* right edge even when there is no Stop button */
1519
+ }
1520
+ .thinking-follow-btn {
1521
+ display: inline-flex;
1522
+ align-items: center;
1523
+ justify-content: center;
1524
+ min-width: 30px;
1525
+ height: 26px;
1526
+ padding: 0 6px;
1527
+ font-size: var(--fs-meta-md);
1528
+ line-height: 1;
1529
+ color: var(--muted);
1530
+ background: transparent;
1531
+ border: 1px solid transparent;
1532
+ border-radius: 6px;
1533
+ cursor: pointer;
1534
+ transition: background 0.15s, border-color 0.15s, opacity 0.15s;
1535
+ }
1536
+ .thinking-follow-btn:hover { background: rgba(127, 127, 127, 0.12); }
1537
+ .thinking-follow-btn.active,
1538
+ .thinking-follow-btn[aria-pressed="true"] {
1539
+ color: var(--accent);
1540
+ border-color: var(--accent);
1541
+ background: color-mix(in srgb, var(--accent) 16%, transparent);
1542
+ }
1419
1543
  .bubble-action-btn {
1420
1544
  display: inline-flex;
1421
1545
  align-items: center;
@@ -2239,6 +2363,61 @@
2239
2363
  pointer-events: none;
2240
2364
  }
2241
2365
  #relay-toast.visible { display: block; }
2366
+ #relay-toast.has-action { pointer-events: auto; }
2367
+ .relay-toast-action {
2368
+ margin-left: 10px;
2369
+ border: 1px solid var(--warn);
2370
+ background: transparent;
2371
+ color: var(--warn);
2372
+ border-radius: 999px;
2373
+ padding: 4px 12px;
2374
+ font-size: var(--fs-meta-sm);
2375
+ font-weight: 600;
2376
+ cursor: pointer;
2377
+ }
2378
+ .relay-toast-action:hover { background: rgba(210, 153, 34, 0.2); }
2379
+ /* Persistent app-wide banner for a queued host suspend / relay restart
2380
+ (host-suspend-ui.js). Same look as the toast, one row per pending action,
2381
+ sits just below the toast so both stay readable; takes pointer input for
2382
+ its Cancel buttons. */
2383
+ #pending-action-banner {
2384
+ display: none;
2385
+ position: fixed;
2386
+ top: calc(var(--header-safe-top, 0px) + 10px);
2387
+ left: 50%;
2388
+ transform: translateX(-50%);
2389
+ z-index: 12590;
2390
+ max-width: min(640px, calc(100vw - 24px));
2391
+ background: var(--bg2);
2392
+ background: color-mix(in srgb, var(--warn) 14%, var(--bg2));
2393
+ border: 1px solid var(--warn);
2394
+ color: var(--warn);
2395
+ border-radius: 10px;
2396
+ padding: 6px 12px;
2397
+ font-size: var(--fs-meta-sm);
2398
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
2399
+ pointer-events: auto;
2400
+ }
2401
+ #pending-action-banner.visible { display: flex; flex-direction: column; gap: 4px; }
2402
+ #relay-toast.visible ~ #app #pending-action-banner,
2403
+ #relay-toast.visible + #pending-action-banner { top: calc(var(--header-safe-top, 0px) + 56px); }
2404
+ .pending-action-row {
2405
+ display: flex;
2406
+ align-items: center;
2407
+ justify-content: center;
2408
+ gap: 8px;
2409
+ flex-wrap: wrap;
2410
+ text-align: center;
2411
+ }
2412
+ .pending-action-text { overflow-wrap: anywhere; }
2413
+ .pending-action-cancel { margin-left: 0; }
2414
+ .suspend-host-blockers {
2415
+ margin: 6px 0 10px;
2416
+ padding-left: 20px;
2417
+ color: var(--text);
2418
+ font-size: var(--fs-meta-md);
2419
+ }
2420
+ .suspend-host-blockers li { margin: 2px 0; overflow-wrap: anywhere; }
2242
2421
  #model-metadata-blocker {
2243
2422
  display: none;
2244
2423
  align-items: center;
@@ -2337,8 +2516,11 @@
2337
2516
  gap: 6px;
2338
2517
  }
2339
2518
  /* Slash-command autocompletion: same composer-anchored overlay as the emoji
2340
- popup, but list-shaped. Row highlight follows the arrow keys only. */
2341
- #slash-autocomplete-popup {
2519
+ popup, but list-shaped. Row highlight follows the arrow keys only. The
2520
+ @relay mention popup (mention-autocomplete.mjs) is its twin; the two are
2521
+ never open at once. */
2522
+ #slash-autocomplete-popup,
2523
+ #mention-autocomplete-popup {
2342
2524
  display: none;
2343
2525
  position: absolute;
2344
2526
  left: 16px;
@@ -2353,7 +2535,9 @@
2353
2535
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.45);
2354
2536
  z-index: 30;
2355
2537
  }
2356
- #slash-autocomplete-popup.visible { display: block; }
2538
+ #slash-autocomplete-popup.visible,
2539
+ #mention-autocomplete-popup.visible { display: block; }
2540
+ .mention-item { align-items: center; }
2357
2541
  .slash-item {
2358
2542
  display: flex; align-items: baseline; gap: 10px;
2359
2543
  padding: 6px 8px; border-radius: 8px; cursor: pointer;
@@ -2365,7 +2549,8 @@
2365
2549
  .slash-item-hint { cursor: default; }
2366
2550
  .slash-item-hint:hover { background: transparent; }
2367
2551
  .slash-item-hint .slash-item-name { color: var(--muted); }
2368
- body.shared-reader-mode #slash-autocomplete-popup { display: none; }
2552
+ body.shared-reader-mode #slash-autocomplete-popup,
2553
+ body.shared-reader-mode #mention-autocomplete-popup { display: none; }
2369
2554
  #emoji-popup.visible {
2370
2555
  display: grid;
2371
2556
  }
@@ -2439,6 +2624,16 @@
2439
2624
  (badge above tokens/elapsed/Stop) so the badge cannot float mid-row when
2440
2625
  the wrapped description spans two lines. */
2441
2626
  .bg-task-side { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; flex-shrink: 0; }
2627
+ .bg-task-side-head { display: flex; align-items: center; gap: 8px; }
2628
+ .bg-task-model { color: var(--text); font-size: var(--fs-meta-sm); white-space: nowrap; }
2629
+ /* Many tasks or previews must not push the composer off-screen: both lists
2630
+ scroll together inside the panel while the summary line stays put. The dvh term keeps the whole
2631
+ composer on screen in phone landscape, where 45vh alone left the input
2632
+ row clipped below the fold (header + the rest of the composer take ~296px). */
2633
+ #background-panel-body {
2634
+ max-height: clamp(64px, calc(100dvh - 296px), min(45vh, 420px));
2635
+ overflow-y: auto; overscroll-behavior: contain;
2636
+ }
2442
2637
  .bg-task-side-status { display: flex; align-items: center; gap: 10px; }
2443
2638
  /* Mobile readability: bounded wrapping instead of single-line ellipsis.
2444
2639
  Long text clamps to two lines so the tail (the part that distinguishes
@@ -2454,6 +2649,19 @@
2454
2649
  background: var(--bg3); white-space: nowrap;
2455
2650
  }
2456
2651
  .bg-task-detail { color: var(--muted); font-size: var(--fs-meta-sm); }
2652
+ /* A live tool call is one line cropped to the row: the command's head is
2653
+ what identifies it, and wrapping would make rows jump on every call. */
2654
+ .bg-task-detail.bg-task-toolcall {
2655
+ display: block; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;
2656
+ }
2657
+ /* A phone row is too narrow for one line to say anything; two clamped lines
2658
+ fit beside the side column's height anyway. */
2659
+ @media (max-width: 680px) {
2660
+ .bg-task-detail.bg-task-toolcall {
2661
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
2662
+ white-space: normal; overflow-wrap: anywhere;
2663
+ }
2664
+ }
2457
2665
  .bg-task-tokens { color: var(--muted); flex-shrink: 0; font-variant-numeric: tabular-nums; }
2458
2666
  .bg-task-elapsed { color: var(--muted); flex-shrink: 0; font-variant-numeric: tabular-nums; }
2459
2667
  .bg-task-stop {
@@ -2492,6 +2700,68 @@
2492
2700
  .preview-close:hover:not(:disabled) { border-color: #f85149; color: #f85149; }
2493
2701
  .preview-close:disabled { opacity: 0.5; cursor: default; }
2494
2702
  #settings-previews-empty { color: var(--muted); font-size: var(--fs-meta-sm); }
2703
+ /* Remote relays (Settings → Relays, the @mention popup): a health dot —
2704
+ online green, unreachable/refused red, never checked grey. */
2705
+ .remote-relay-dot {
2706
+ width: 9px; height: 9px; border-radius: 50%; display: inline-block; flex-shrink: 0;
2707
+ background: var(--relay-tone-offline);
2708
+ }
2709
+ .remote-relay-dot[data-status="online"] { background: var(--relay-tone-online); }
2710
+ .remote-relay-dot[data-status="offline"],
2711
+ .remote-relay-dot[data-status="unauthorized"],
2712
+ .remote-relay-dot[data-status="error"] { background: #f85149; }
2713
+ #remote-relays-list { width: 100%; }
2714
+ .remote-relay-tag {
2715
+ border: 1px solid var(--border); border-radius: 999px; padding: 0 7px;
2716
+ font-size: var(--fs-meta-sm); color: var(--muted); background: var(--bg3); white-space: nowrap;
2717
+ }
2718
+ .remote-relay-error { color: #f85149; overflow-wrap: anywhere; }
2719
+ .remote-relay-http-warning { color: var(--warn); opacity: 1; }
2720
+ label.remote-relay-permission { display: flex; align-items: center; gap: 6px; font-size: var(--fs-meta-sm); color: var(--muted); font-weight: normal; }
2721
+ .remote-relay-permission select {
2722
+ background: var(--bg); color: var(--text); border: 1px solid var(--border);
2723
+ border-radius: 6px; padding: 2px 6px; font-size: var(--fs-meta-sm);
2724
+ }
2725
+ .remote-relays-self-name-row { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
2726
+ .remote-relays-self-name { font-family: var(--mono, monospace); color: var(--text); }
2727
+ #remote-relays-public-url-input,
2728
+ #remote-relays-add-url-input,
2729
+ #remote-relays-add-token-input {
2730
+ background: var(--bg); color: var(--text); border: 1px solid var(--border);
2731
+ border-radius: 6px; padding: 6px 12px; outline: none; font-size: var(--fs-meta-md);
2732
+ width: min(420px, 100%); box-sizing: border-box;
2733
+ }
2734
+ .remote-relays-add-token-row { display: flex; flex-direction: column; gap: 6px; width: 100%; }
2735
+ .remote-relays-add-token-row[hidden] { display: none; }
2736
+ .remote-relays-add-token-row label { font-weight: normal; color: var(--muted); font-size: var(--fs-meta-sm); }
2737
+ .remote-relays-link-btn {
2738
+ background: none; border: none; padding: 0; cursor: pointer;
2739
+ color: var(--accent); font-size: var(--fs-meta-sm); text-decoration: underline;
2740
+ }
2741
+ .remote-relays-link-btn[hidden] { display: none; }
2742
+ .settings-row label.remote-relays-pair-back { display: flex; align-items: center; gap: 8px; font-weight: normal; }
2743
+ #remote-relays-add-status[data-state="working"] { color: var(--muted); }
2744
+ #remote-relays-add-status[data-state="added"],
2745
+ #remote-relays-add-status[data-state="paired"] { color: #3fb950; }
2746
+ #remote-relays-add-status[data-state="pair-failed"],
2747
+ #remote-relays-add-status[data-state="needs-token"] { color: var(--warn); }
2748
+ #remote-relays-add-status[data-state="self"],
2749
+ #remote-relays-add-status[data-state="error"] { color: #f85149; }
2750
+ /* Provenance of a prompt another relay's agent sent (remote-relay-origin-view.mjs). */
2751
+ .msg-origin { margin-bottom: 6px; max-width: 100%; }
2752
+ .msg-origin-badge {
2753
+ display: inline-block; max-width: 100%; box-sizing: border-box;
2754
+ padding: 1px 8px; border-radius: 999px; font-size: var(--fs-meta-sm); line-height: 1.5;
2755
+ border: 1px solid color-mix(in srgb, currentColor 40%, transparent);
2756
+ background: color-mix(in srgb, currentColor 12%, transparent);
2757
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
2758
+ }
2759
+ .msg.user .msg-bubble a.msg-origin-badge { text-decoration: none; }
2760
+ .msg.user .msg-bubble a.msg-origin-badge:hover { text-decoration: underline; }
2761
+ .conv-origin-marker {
2762
+ flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
2763
+ color: var(--muted); font-size: var(--fs-ui-chip);
2764
+ }
2495
2765
  /* Transcript card: a preview published during this turn, pinned into the
2496
2766
  response bubble. Compact single row; closed previews keep the label but
2497
2767
  lose the dead link. */
@@ -2535,6 +2805,30 @@
2535
2805
  background: rgba(63, 185, 80, 0.12); color: var(--relay-tone-online);
2536
2806
  border-radius: 6px; padding: 0 6px; font-size: var(--fs-meta-sm);
2537
2807
  }
2808
+ /* Mid-turn steering. The CLI usually FOLDS a steered message into the
2809
+ running turn and answers everything with one reply; the steered row then
2810
+ settles as a compact marker under its own message (kind='folded'). More
2811
+ rarely the turn is handed off: the interrupted reply settles as
2812
+ kind='absorbed' and the steered message carries the continuing reply. A
2813
+ steer cut off by Stop settles as kind='stopped' with a Resend button.
2814
+ Markers are notes, not answers, so they render muted and compact.
2815
+ steered-turn-merge.mjs adds the adjacency classes. */
2816
+ .msg.assistant.msg-absorbed .msg-bubble,
2817
+ .msg.assistant.msg-folded .msg-bubble,
2818
+ .msg.assistant.msg-steer-stopped .msg-bubble {
2819
+ font-size: var(--fs-meta-md); color: var(--muted); font-style: italic;
2820
+ padding: 6px 12px; opacity: 0.85;
2821
+ }
2822
+ .msg.assistant.msg-absorbed { margin-top: -4px; }
2823
+ /* Attached to the steered message above it (the transcript gap is 12px). */
2824
+ .msg.assistant.msg-folded,
2825
+ .msg.assistant.msg-steer-stopped { margin-top: -8px; }
2826
+ .msg.assistant.msg-folded .msg-label,
2827
+ .msg.assistant.msg-steer-stopped .msg-label { display: none; }
2828
+ .msg.assistant.msg-steer-stopped .msg-bubble-actions { font-style: normal; }
2829
+ .msg.user.msg-steered .msg-bubble { border-style: dashed; }
2830
+ .msg.user.msg-steered .msg-label::after { content: ' · steered mid-turn'; }
2831
+ .msg.assistant.msg-steered-continuation { margin-top: -2px; }
2538
2832
  body.shared-reader-mode #background-tasks-panel { display: none; }
2539
2833
  #model-pricing-details {
2540
2834
  display: none; margin-top: 6px; color: var(--muted); font-size: var(--fs-meta-sm);
@@ -4197,6 +4491,43 @@
4197
4491
  color: #000;
4198
4492
  font-weight: 700;
4199
4493
  }
4494
+ /* Multi-select cards: the same pills, with a checkmark instead of one-shot
4495
+ buttons, so several answers can be ticked before one Reply. */
4496
+ .relay-question-choice-check {
4497
+ display: inline-flex;
4498
+ align-items: center;
4499
+ gap: 8px;
4500
+ user-select: none;
4501
+ }
4502
+ .relay-question-choice-check input[type="checkbox"] {
4503
+ margin: 0;
4504
+ width: 16px;
4505
+ height: 16px;
4506
+ accent-color: var(--accent);
4507
+ cursor: pointer;
4508
+ }
4509
+ .relay-question-choice-check:has(input:checked) {
4510
+ border-color: var(--accent);
4511
+ background: rgba(88, 166, 255, 0.12);
4512
+ }
4513
+ /* The "Select several" switch on Copilot choice cards. */
4514
+ .relay-question-multi-toggle {
4515
+ display: inline-flex;
4516
+ align-items: center;
4517
+ gap: 6px;
4518
+ margin-top: 8px;
4519
+ font-size: var(--fs-meta-sm);
4520
+ color: var(--muted, var(--text));
4521
+ cursor: pointer;
4522
+ user-select: none;
4523
+ }
4524
+ .relay-question-multi-toggle input[type="checkbox"] {
4525
+ margin: 0;
4526
+ width: 16px;
4527
+ height: 16px;
4528
+ accent-color: var(--accent);
4529
+ cursor: pointer;
4530
+ }
4200
4531
  .relay-question-reply {
4201
4532
  display: flex;
4202
4533
  gap: 8px;
@@ -4397,10 +4728,12 @@
4397
4728
  font-size: var(--fs-meta-md);
4398
4729
  min-height: 36px;
4399
4730
  }
4400
- .thinking-bubble-header { margin-bottom: 6px; }
4731
+ .thinking-bubble-header { margin-bottom: 6px; top: -12px; }
4401
4732
  .sidebar-header { padding: var(--sidebar-header-top-pad-mobile) 12px 12px; gap: 8px; }
4402
4733
  #new-conv-btn { padding: 9px 12px; border-radius: 8px; font-size: var(--fs-meta-md); }
4403
4734
  #status-btn { min-width: 72px; padding: 9px 10px; font-size: var(--fs-meta-md); }
4735
+ #conv-filter-row { padding: 8px 12px; }
4736
+ #conv-filter-clear { right: 16px; }
4404
4737
  #conv-list { padding: 0 5px 8px; }
4405
4738
  .conv-item { padding: 6px 11px; border-radius: 8px; }
4406
4739
  .conv-title {
@@ -4916,6 +5249,7 @@
4916
5249
  <div id="chat-actions-menu-backdrop" onclick="window.closeChatActionsMenu && window.closeChatActionsMenu()"></div>
4917
5250
  <div id="font-scale-indicator" aria-live="polite" aria-atomic="true" hidden>100%</div>
4918
5251
  <div id="relay-toast" role="status" aria-live="polite"></div>
5252
+ <div id="pending-action-banner" role="status" aria-live="polite" hidden></div>
4919
5253
 
4920
5254
  <!-- Main App -->
4921
5255
  <div id="app">
@@ -4928,6 +5262,11 @@
4928
5262
  <button id="install-btn" class="header-icon-btn" onclick="promptInstallApp()" title="Install app to home screen" aria-label="Install app">⬇</button>
4929
5263
  </div>
4930
5264
  </div>
5265
+ <div id="conv-filter-row">
5266
+ <input id="conv-filter-input" type="text" placeholder="Filter conversations…" autocomplete="off" spellcheck="false" aria-label="Filter conversations by title">
5267
+ <button id="conv-filter-clear" type="button" title="Clear filter" aria-label="Clear filter" hidden>×</button>
5268
+ <div id="conv-filter-status" role="status" aria-live="polite"></div>
5269
+ </div>
4931
5270
  <div id="conv-list"></div>
4932
5271
  </div>
4933
5272
  <div id="sidebar-resizer" role="separator" aria-orientation="vertical" aria-label="Resize conversations sidebar"></div>
@@ -4991,15 +5330,18 @@
4991
5330
  <div id="image-edit-target" aria-live="polite"></div>
4992
5331
  <div id="emoji-popup" aria-hidden="true"></div>
4993
5332
  <div id="slash-autocomplete-popup" aria-hidden="true" role="listbox"></div>
5333
+ <div id="mention-autocomplete-popup" aria-hidden="true" role="listbox" aria-label="Remote relays"></div>
4994
5334
  <details id="background-tasks-panel" hidden>
4995
5335
  <summary id="background-tasks-summary"></summary>
4996
- <div id="background-previews-list"></div>
4997
- <div id="background-tasks-list"></div>
5336
+ <div id="background-panel-body">
5337
+ <div id="background-previews-list"></div>
5338
+ <div id="background-tasks-list"></div>
5339
+ </div>
4998
5340
  </details>
4999
5341
  <div class="input-row">
5000
5342
  <input id="image-input" type="file" multiple hidden onchange="handleAttachmentInput(this.files); this.value='';">
5001
5343
  <button id="emoji-btn" onclick="toggleEmojiPicker()" title="Emoji picker (Tip: Win + .)">🙂</button>
5002
- <textarea id="msg-input" placeholder="Message Copilot…" rows="1" onkeydown="handleKey(event)" oninput="autoResize(this); updateComposerSlashMenu(this)"></textarea>
5344
+ <textarea id="msg-input" placeholder="Message Copilot…" rows="1" aria-autocomplete="list" onkeydown="handleKey(event)" oninput="autoResize(this); updateComposerSlashMenu(this)"></textarea>
5003
5345
  <select id="mode-select" title="Mode">
5004
5346
  <option value="agent" selected>Agent</option>
5005
5347
  <option value="ask">Ask</option>
@@ -5136,6 +5478,16 @@
5136
5478
  tabindex="-1"
5137
5479
  data-settings-tab="providers"
5138
5480
  >Providers</button>
5481
+ <button
5482
+ id="settings-tab-relays"
5483
+ class="settings-tab"
5484
+ type="button"
5485
+ role="tab"
5486
+ aria-selected="false"
5487
+ aria-controls="settings-panel-relays"
5488
+ tabindex="-1"
5489
+ data-settings-tab="relays"
5490
+ >Relays</button>
5139
5491
  <button
5140
5492
  id="settings-tab-previews"
5141
5493
  class="settings-tab"
@@ -5214,20 +5566,20 @@
5214
5566
  </div>
5215
5567
  <div class="settings-help" style="margin-top: 6px;">A hard cap on how long one turn may run before the relay requeues it. Turns are already kept alive by worker heartbeats, and a turn waiting on your answer to a question is never cut off — so this only catches a worker that has hung. Set to 0 for no limit.</div>
5216
5568
  <div class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
5217
- <label for="background-task-timeout-slider">Background task timeout <span id="background-task-timeout-value">No limit</span></label>
5569
+ <label for="background-task-timeout-slider">Background task timeout <span id="background-task-timeout-value">4 h</span></label>
5218
5570
  <input
5219
5571
  id="background-task-timeout-slider"
5220
5572
  type="range"
5221
5573
  min="0"
5222
5574
  max="600"
5223
5575
  step="5"
5224
- value="0"
5576
+ value="240"
5225
5577
  aria-label="Background task timeout in minutes"
5226
5578
  oninput="previewBackgroundTaskTimeoutSetting(this.value)"
5227
5579
  onchange="updateBackgroundTaskTimeoutSetting(this.value)"
5228
5580
  >
5229
5581
  </div>
5230
- <div class="settings-help" style="margin-top: 6px;">How long background tasks (backgrounded commands, subagents, monitors) may keep a Claude session's CLI process alive after its turn ends. When the limit is reached the tasks are stopped and the agent reports what they left behind. Set to 0 for no limit — tasks then run until they finish or you stop them from the composer panel.</div>
5582
+ <div class="settings-help" style="margin-top: 6px;">How long background tasks may keep a session alive after its turn ends: backgrounded commands, subagents and monitors on Claude; background agents and detached shells on Copilot's SDK engine. When the limit is reached the tasks are stopped. Set to 0 for no limit — tasks then run until they finish or you stop them from the composer panel.</div>
5231
5583
  <div id="windows-autostart-setting" hidden>
5232
5584
  <div class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
5233
5585
  <label>Autostart (Windows)</label>
@@ -5251,16 +5603,16 @@
5251
5603
  </div>
5252
5604
  </div>
5253
5605
  <div class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
5254
- <label for="pwa-app-name-input">Install app name</label>
5606
+ <label for="pwa-app-name-input">Relay name</label>
5255
5607
  <input
5256
5608
  id="pwa-app-name-input"
5257
5609
  type="text"
5258
5610
  maxlength="60"
5259
5611
  placeholder="OAR"
5260
- aria-label="Install app name"
5612
+ aria-label="Relay name"
5261
5613
  onchange="updatePwaAppName(this.value)"
5262
5614
  >
5263
- <div class="settings-help">Used for the installed PWA app label on every device connecting to this relay. On Android, a rename reaches an already-installed app after a short delay, with one confirmation prompt showing the new name.</div>
5615
+ <div class="settings-help">Shown to other relays and used as the installed app's name. On Android, a rename reaches an already-installed app after a short delay, with one confirmation prompt showing the new name.</div>
5264
5616
  </div>
5265
5617
  <div class="settings-row" style="margin-top: 12px; margin-bottom: 0;">
5266
5618
  <label for="update-auto-check-toggle">Check for updates automatically</label>
@@ -5402,14 +5754,14 @@
5402
5754
  <div class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
5403
5755
  <label for="copilot-engine-select">Copilot engine</label>
5404
5756
  <select id="copilot-engine-select" aria-label="Copilot engine">
5405
- <option value="extension">Extension — current</option>
5406
- <option value="sdk">SDK — experimental</option>
5757
+ <option value="extension">Extension</option>
5758
+ <option value="sdk">SDK — default</option>
5407
5759
  </select>
5408
5760
  <div id="copilot-settings-status" class="settings-help settings-provider-status">Loading Copilot settings…</div>
5409
5761
  <div style="display: flex; gap: 8px; margin-top: 8px;">
5410
5762
  <button id="copilot-save-btn" type="button" class="summary-btn" onclick="saveCopilotSettings()">Save Copilot settings</button>
5411
5763
  </div>
5412
- <div class="settings-help">Chooses how GitHub Copilot conversations run. <strong>Extension</strong> is the current default: the Copilot CLI in a terminal session, which you can attach to with the tmux inspector. <strong>SDK</strong> is experimental and headless — no CLI extension to install, but no tmux inspector for those sessions. Applies to newly started conversations; sessions already running keep their engine until their worker restarts.</div>
5764
+ <div class="settings-help">Chooses how GitHub Copilot conversations run. <strong>SDK</strong> is the default: a headless worker per conversation with mid-turn steering and background task cards — no CLI extension to install, but no tmux inspector for those sessions. <strong>Extension</strong> runs the Copilot CLI in a terminal session, which you can attach to with the tmux inspector. A relay that cannot run the SDK engine uses Extension. Applies to newly started conversations; sessions already running keep their engine until their worker restarts.</div>
5413
5765
  </div>
5414
5766
  </div>
5415
5767
  <div
@@ -5752,6 +6104,96 @@
5752
6104
  </div>
5753
6105
  </div>
5754
6106
  </div>
6107
+ <div
6108
+ id="settings-panel-relays"
6109
+ class="settings-panel"
6110
+ role="tabpanel"
6111
+ tabindex="0"
6112
+ aria-labelledby="settings-tab-relays"
6113
+ data-settings-panel="relays"
6114
+ hidden
6115
+ >
6116
+ <div id="remote-relays-self-section" class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
6117
+ <label>This relay</label>
6118
+ <div class="remote-relays-self-name-row">
6119
+ <span id="remote-relays-self-name" class="remote-relays-self-name">—</span>
6120
+ <span class="settings-help">Change it under General → Relay name.</span>
6121
+ </div>
6122
+ </div>
6123
+ <div class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
6124
+ <label for="remote-relays-public-url-input">Public address</label>
6125
+ <input
6126
+ id="remote-relays-public-url-input"
6127
+ type="url"
6128
+ inputmode="url"
6129
+ autocomplete="off"
6130
+ autocapitalize="off"
6131
+ spellcheck="false"
6132
+ placeholder="https://…"
6133
+ aria-label="Public address of this relay"
6134
+ onchange="saveRemoteRelayPublicUrl(this.value)"
6135
+ >
6136
+ <div class="settings-help">How other relays reach this one — the address you open this relay at from another device. Needed for "Also add this relay there".</div>
6137
+ <div id="remote-relays-self-status" class="settings-help settings-provider-status" role="status" aria-live="polite" hidden></div>
6138
+ </div>
6139
+ <div class="settings-row" style="margin-top: 12px; margin-bottom: 0;">
6140
+ <label for="remote-relays-inbound-toggle">Accept prompts from other relays' agents</label>
6141
+ <input
6142
+ id="remote-relays-inbound-toggle"
6143
+ type="checkbox"
6144
+ onchange="toggleRemoteRelayInbound(this.checked)"
6145
+ aria-label="Accept prompts from other relays' agents"
6146
+ checked
6147
+ >
6148
+ </div>
6149
+ <div class="settings-help" style="margin-top: 6px;">When off, agents on paired relays can still read sessions here, but cannot send prompts, start sessions, answer questions or stop turns. The relay token stays the real boundary: anyone holding it has full access.</div>
6150
+ <div id="remote-relays-list-section" class="settings-row settings-row-column" style="margin-top: 16px; margin-bottom: 0;">
6151
+ <label>Remote relays</label>
6152
+ <div id="remote-relays-list"></div>
6153
+ <div id="remote-relays-empty" class="settings-help">Loading remote relays…</div>
6154
+ <div id="remote-relays-status" class="settings-help settings-provider-status" role="status" aria-live="polite" hidden></div>
6155
+ <div class="settings-help">Agents in any session here can work on these relays once you mention one in the conversation (for example <code>@name</code>). "Agents may" limits what they can do there.</div>
6156
+ </div>
6157
+ <div id="remote-relays-add-section" class="settings-row settings-row-column" style="margin-top: 16px; margin-bottom: 0;">
6158
+ <label for="remote-relays-add-url-input">Add a relay</label>
6159
+ <input
6160
+ id="remote-relays-add-url-input"
6161
+ type="url"
6162
+ inputmode="url"
6163
+ autocomplete="off"
6164
+ autocapitalize="off"
6165
+ spellcheck="false"
6166
+ placeholder="Paste the other relay's web address"
6167
+ aria-label="Paste the other relay's web address"
6168
+ onkeydown="if (event.key === 'Enter') { event.preventDefault(); addRemoteRelayFromForm(); }"
6169
+ >
6170
+ <div id="remote-relays-add-token-row" class="remote-relays-add-token-row" hidden>
6171
+ <label for="remote-relays-add-token-input">Token of the other relay</label>
6172
+ <input
6173
+ id="remote-relays-add-token-input"
6174
+ type="password"
6175
+ autocomplete="off"
6176
+ autocapitalize="off"
6177
+ spellcheck="false"
6178
+ placeholder="Only needed when it differs from this relay's token"
6179
+ aria-label="Token of the other relay"
6180
+ onkeydown="if (event.key === 'Enter') { event.preventDefault(); addRemoteRelayFromForm(); }"
6181
+ >
6182
+ </div>
6183
+ <button id="remote-relays-use-token-btn" class="remote-relays-link-btn" type="button" onclick="showRemoteRelayTokenField()">Use a different token</button>
6184
+ <label class="remote-relays-pair-back">
6185
+ <input id="remote-relays-pair-back-toggle" type="checkbox" checked>
6186
+ Also add this relay there
6187
+ </label>
6188
+ <div id="remote-relays-pair-back-hint" class="settings-help">
6189
+ The other relay gets this relay's public address and, if it uses a different token, this
6190
+ relay's token too: its agents can then work here. Removing a relay here does not revoke
6191
+ that; rotate this relay's token to lock it out.
6192
+ </div>
6193
+ <button id="remote-relays-add-btn" class="chat-title-action-btn" type="button" onclick="addRemoteRelayFromForm()">Add</button>
6194
+ <div id="remote-relays-add-status" class="settings-help settings-provider-status" role="status" aria-live="polite" hidden></div>
6195
+ </div>
6196
+ </div>
5755
6197
  <div
5756
6198
  id="settings-panel-previews"
5757
6199
  class="settings-panel"
@@ -5821,6 +6263,10 @@
5821
6263
  <label for="push-event-cli-offline-toggle">CLI went offline</label>
5822
6264
  <input id="push-event-cli-offline-toggle" type="checkbox" onchange="updatePushPreferencesFromControls()" disabled>
5823
6265
  </div>
6266
+ <div class="settings-row" style="width: 100%; margin-top: 6px; margin-bottom: 0;">
6267
+ <label for="push-event-host-suspend-toggle">Host suspend fired or was dropped</label>
6268
+ <input id="push-event-host-suspend-toggle" type="checkbox" onchange="updatePushPreferencesFromControls()" checked disabled>
6269
+ </div>
5824
6270
  <label style="margin-top: 10px;">Devices</label>
5825
6271
  <div id="push-device-list" style="width: 100%;"></div>
5826
6272
  <div class="settings-help">Notifications are only sent while no device has the app in the foreground. Content defaults to generic text; message previews are opt-in per device.</div>