@oar-sh/oar 0.9.1 → 0.9.3

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 (146) hide show
  1. package/.github/extensions/web-relay/extension.mjs +0 -3
  2. package/.github/extensions/web-relay/polling/heartbeat.mjs +2 -55
  3. package/.github/extensions/web-relay/polling/polling-loop.mjs +0 -1
  4. package/.github/extensions/web-relay/runtime/api-client.mjs +2 -34
  5. package/.github/extensions/web-relay/runtime/config-loader.mjs +2 -104
  6. package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +2 -170
  7. package/.github/extensions/web-relay/runtime/session-io.mjs +1 -71
  8. package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +2 -325
  9. package/.github/extensions/web-relay/skills/prompt-context.mjs +2 -160
  10. package/CHANGELOG.md +269 -0
  11. package/README.md +407 -424
  12. package/bin/oar.js +142 -11
  13. package/package.json +8 -4
  14. package/server/README.md +59 -22
  15. package/server/claude-worker/claude-sdk-adapter.mjs +7 -2
  16. package/server/claude-worker/claude-session-process.mjs +1013 -45
  17. package/server/claude-worker/claude-session-worker.mjs +54 -9
  18. package/server/claude-worker/claude-turn-publisher.mjs +110 -19
  19. package/server/claude-worker/claude-worker-link-wiring.mjs +9 -0
  20. package/server/copilot-sdk-runtime.mjs +12 -3
  21. package/server/copilot-worker/copilot-ask-user-tool.mjs +89 -0
  22. package/server/copilot-worker/copilot-attachments.mjs +35 -2
  23. package/server/copilot-worker/copilot-continuation-signals.mjs +55 -0
  24. package/server/copilot-worker/copilot-model-switch.mjs +425 -0
  25. package/server/copilot-worker/copilot-prompt-context.mjs +20 -8
  26. package/server/copilot-worker/copilot-question-bridge.mjs +129 -16
  27. package/server/copilot-worker/copilot-sdk-adapter.mjs +59 -11
  28. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +136 -12
  29. package/server/copilot-worker/copilot-sdk-session-process.mjs +2753 -527
  30. package/server/copilot-worker/copilot-sdk-session-worker.mjs +94 -25
  31. package/server/cursor-worker/cursor-session-worker.mjs +5 -5
  32. package/server/cursor-worker/cursor-turn-runner.mjs +50 -4
  33. package/server/db-schema.mjs +61 -1
  34. package/server/features.mjs +155 -41
  35. package/server/grok-worker/grok-session-worker.mjs +5 -5
  36. package/server/grok-worker/grok-turn-runner.mjs +45 -4
  37. package/server/migrations/0004-steer-settle-markers.mjs +119 -0
  38. package/server/public/app/annotate-core.mjs +181 -0
  39. package/server/public/app/annotate-editor.js +617 -0
  40. package/server/public/app/api-client.js +86 -5
  41. package/server/public/app/ask-user-view.js +102 -13
  42. package/server/public/app/attachments-view.js +172 -3
  43. package/server/public/app/background-task-timeout-options.mjs +9 -0
  44. package/server/public/app/background-tasks-view.mjs +34 -19
  45. package/server/public/app/bootstrap.js +153 -69
  46. package/server/public/app/composer-control-state.mjs +52 -34
  47. package/server/public/app/context-tier-options.mjs +24 -4
  48. package/server/public/app/conversation-draft-sync.mjs +187 -0
  49. package/server/public/app/conversation-list-filter.mjs +86 -0
  50. package/server/public/app/conversation-preferences.mjs +47 -10
  51. package/server/public/app/conversation-provider-indicator.mjs +8 -0
  52. package/server/public/app/conversation-view.js +1032 -218
  53. package/server/public/app/emoji-view.js +2 -0
  54. package/server/public/app/font-scaling.js +4 -37
  55. package/server/public/app/journal-view.js +190 -24
  56. package/server/public/app/model-selector-options.mjs +185 -1
  57. package/server/public/app/new-conversation-model-choice.mjs +29 -9
  58. package/server/public/app/pwa-install.js +64 -136
  59. package/server/public/app/question-multi-select.mjs +52 -0
  60. package/server/public/app/reasoning-effort-labels.mjs +13 -3
  61. package/server/public/app/relay-prompt-sanitizer.mjs +26 -3
  62. package/server/public/app/router.js +120 -11
  63. package/server/public/app/settings-modal.js +225 -20
  64. package/server/public/app/settings-tabs.js +1 -1
  65. package/server/public/app/socket-handlers.js +47 -4
  66. package/server/public/app/steered-turn-merge.mjs +100 -0
  67. package/server/public/app/store.js +159 -16
  68. package/server/public/app/sync-outbox.mjs +16 -8
  69. package/server/public/app/tool-activity-emoji.mjs +36 -0
  70. package/server/public/app/transcript-viewport-anchor.mjs +210 -0
  71. package/server/public/app/update-ui.js +194 -0
  72. package/server/public/index.html +483 -24
  73. package/server/public/sw.js +7 -0
  74. package/server/relay-tools.md +2 -0
  75. package/server/relay.mjs +23 -21
  76. package/server/repositories/message-repository.mjs +78 -21
  77. package/server/repositories/question-repository.mjs +19 -1
  78. package/server/repositories/session-repository.mjs +7 -31
  79. package/server/routes/ask-user-routes.mjs +10 -0
  80. package/server/routes/messages-routes.mjs +689 -159
  81. package/server/routes/sessions-routes.mjs +574 -80
  82. package/server/server-runtime.mjs +393 -925
  83. package/server/services/claude-auth-service.mjs +7 -1
  84. package/server/services/cli-install-service.mjs +8 -27
  85. package/server/services/cli-process-runner.mjs +30 -0
  86. package/server/services/copilot-model-discovery-service.mjs +202 -0
  87. package/server/services/data-dir-resolution.mjs +47 -0
  88. package/server/services/grok-auth-service.mjs +9 -1
  89. package/server/services/live-turn-picker.mjs +47 -0
  90. package/server/services/model-variant-catalog-service.mjs +1183 -0
  91. package/server/services/oar-cli-helpers.mjs +6 -2
  92. package/server/services/relay-prompt-sanitizer.mjs +30 -3
  93. package/server/services/relay-question-context.mjs +46 -0
  94. package/server/services/sdk-session-import-service.mjs +144 -16
  95. package/server/services/sdk-session-sync-service.mjs +84 -27
  96. package/server/services/session-worker-registry-service.mjs +116 -3
  97. package/server/services/session-worker-supervisor-service.mjs +7 -1
  98. package/server/services/session-worker-websocket-service.mjs +119 -24
  99. package/server/services/status-event-service.mjs +8 -2
  100. package/server/services/steer-resend.mjs +45 -0
  101. package/server/services/update-check-service.mjs +220 -0
  102. package/server/services/update-install-service.mjs +279 -0
  103. package/server/services/windows-autostart-service.mjs +42 -6
  104. package/server/services/windows-boot-autostart-service.mjs +487 -0
  105. package/shared/ask-user-bridge.mjs +37 -5
  106. package/shared/background-task-timeout.mjs +12 -4
  107. package/shared/claude-model-catalog.mjs +49 -0
  108. package/shared/context-window-fallbacks.mjs +1 -0
  109. package/shared/control-poller.mjs +4 -1
  110. package/shared/copilot-model-order.mjs +182 -0
  111. package/shared/media-embed-instructions.mjs +35 -0
  112. package/shared/model-descriptors.mjs +181 -30
  113. package/shared/model-id.mjs +6 -0
  114. package/shared/pwa-app-name.mjs +44 -0
  115. package/shared/question-multi-select.mjs +16 -0
  116. package/shared/relay-stop-failure.mjs +15 -0
  117. package/shared/slash-command-guard.mjs +76 -0
  118. package/shared/steer-note.mjs +66 -0
  119. package/shared/steer-settle-failure.mjs +89 -0
  120. package/shared/steer-settle-markers.mjs +18 -0
  121. package/shared/update-semver.mjs +50 -0
  122. package/shared/worker-crash-guard.mjs +40 -14
  123. package/shared/worker-runtime/api-client.mjs +34 -0
  124. package/shared/worker-runtime/config-loader.mjs +104 -0
  125. package/shared/worker-runtime/heartbeat.mjs +109 -0
  126. package/shared/worker-runtime/prompt-context.mjs +160 -0
  127. package/shared/worker-runtime/runner-link-wiring.mjs +44 -0
  128. package/shared/worker-runtime/send-and-wait-errors.mjs +170 -0
  129. package/shared/worker-runtime/worker-websocket-link.mjs +436 -0
  130. package/server/claude-worker/claude-session-test-harness.mjs +0 -236
  131. package/server/claude-worker/fixtures/workflow-run-record.json +0 -1
  132. package/server/copilot-worker/copilot-sdk-test-harness.mjs +0 -329
  133. package/server/copilot-worker/fixtures/abort-turn.json +0 -588
  134. package/server/copilot-worker/fixtures/ask-user-turn.json +0 -1154
  135. package/server/copilot-worker/fixtures/background-timer-continuation.json +0 -445
  136. package/server/copilot-worker/fixtures/background-timer-turn.json +0 -497
  137. package/server/copilot-worker/fixtures/happy-turn.json +0 -566
  138. package/server/copilot-worker/fixtures/quota-turn.json +0 -647
  139. package/server/copilot-worker/fixtures/reasoning-turn.json +0 -625
  140. package/server/copilot-worker/fixtures/resume-turn.json +0 -700
  141. package/server/copilot-worker/fixtures/subagent-turn.json +0 -1378
  142. package/server/copilot-worker/fixtures/tool-permission-turn.json +0 -1293
  143. package/server/routes/messages-routes-test-harness.mjs +0 -111
  144. package/server/services/fixtures/claude-auth-stub.sh +0 -62
  145. package/server/services/fixtures/cli-install-stub.sh +0 -75
  146. package/server/services/fixtures/grok-stub.sh +0 -86
@@ -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);
@@ -1249,6 +1329,12 @@
1249
1329
  max-width: 100%; min-width: 0; box-sizing: border-box;
1250
1330
  overflow-wrap: anywhere; word-break: break-word;
1251
1331
  }
1332
+ /* Inline-embedded media (markdown images by absolute path) stays inside the
1333
+ bubble and advertises click-to-open; attachment thumbnails override below. */
1334
+ .msg-bubble img { max-width: 100%; height: auto; cursor: zoom-in; border-radius: 8px; }
1335
+ .msg-bubble video { max-width: 100%; height: auto; border-radius: 8px; }
1336
+ .msg-bubble audio { max-width: 100%; }
1337
+ .msg-bubble .msg-attachment img { cursor: pointer; border-radius: initial; }
1252
1338
  .msg-bubble.msg-bubble-media-only {
1253
1339
  display: inline-flex;
1254
1340
  flex-direction: column;
@@ -1402,12 +1488,23 @@
1402
1488
  width: 100%; max-width: 100%; box-sizing: border-box;
1403
1489
  min-width: 0;
1404
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. */
1405
1497
  .thinking-bubble-header {
1406
1498
  display: flex;
1407
1499
  align-items: center;
1408
1500
  justify-content: space-between;
1409
1501
  gap: 12px;
1410
1502
  margin-bottom: 4px;
1503
+ position: sticky;
1504
+ top: -20px;
1505
+ z-index: 2;
1506
+ background: var(--bot-bg);
1507
+ padding: 4px 0;
1411
1508
  }
1412
1509
  .thinking-bubble-header:empty { display: none; margin-bottom: 0; }
1413
1510
  .bubble-action-btn {
@@ -2138,6 +2235,12 @@
2138
2235
  border: none; border-radius: 999px; background: rgba(0,0,0,0.65); color: var(--text);
2139
2236
  cursor: pointer; line-height: 22px; font-size: var(--fs-meta-md);
2140
2237
  }
2238
+ .attachment-preview-annotate {
2239
+ position: absolute; top: 4px; left: 4px; width: 22px; height: 22px;
2240
+ border-radius: 999px; background: rgba(0,0,0,0.65);
2241
+ display: flex; align-items: center; justify-content: center;
2242
+ font-size: 12px; pointer-events: none;
2243
+ }
2141
2244
  .attachment-preview-status {
2142
2245
  position: absolute; inset: 0 0 26px 0;
2143
2246
  display: flex; align-items: center; justify-content: center;
@@ -2184,9 +2287,16 @@
2184
2287
  border-radius: 8px; padding: 8px 14px; margin-bottom: 10px; font-size: var(--fs-meta-sm); text-align: center;
2185
2288
  }
2186
2289
  #offline-banner.visible { display: block; }
2290
+ /* Persistent composer-context banner (model catalog warnings, session
2291
+ binding notes). Stays in normal flow with no z-index hoist: transient
2292
+ notices that must outlive overlays render in #relay-toast instead, so
2293
+ this banner never bleeds through modals or the sidebar. The background
2294
+ is opaque (not alpha over whatever happens to be behind it) for the
2295
+ same reason. */
2187
2296
  #model-banner {
2188
2297
  display: none;
2189
- background: rgba(210, 153, 34, 0.15);
2298
+ background: var(--bg2);
2299
+ background: color-mix(in srgb, var(--warn) 14%, var(--bg2));
2190
2300
  border: 1px solid var(--warn);
2191
2301
  color: var(--warn);
2192
2302
  border-radius: 8px;
@@ -2196,6 +2306,43 @@
2196
2306
  text-align: center;
2197
2307
  }
2198
2308
  #model-banner.visible { display: block; }
2309
+ /* Transient relay notices (saves, errors, action confirmations) float as a
2310
+ fixed toast above every overlay — the annotation editor's error paths
2311
+ rely on being readable over its full-screen modal. pointer-events: none
2312
+ keeps the modal beneath clickable while the toast lingers. */
2313
+ #relay-toast {
2314
+ display: none;
2315
+ position: fixed;
2316
+ top: calc(var(--header-safe-top, 0px) + 10px);
2317
+ left: 50%;
2318
+ transform: translateX(-50%);
2319
+ z-index: 12600;
2320
+ max-width: min(560px, calc(100vw - 24px));
2321
+ background: var(--bg2);
2322
+ background: color-mix(in srgb, var(--warn) 14%, var(--bg2));
2323
+ border: 1px solid var(--warn);
2324
+ color: var(--warn);
2325
+ border-radius: 10px;
2326
+ padding: 8px 12px;
2327
+ font-size: var(--fs-meta-sm);
2328
+ text-align: center;
2329
+ box-shadow: 0 6px 24px rgba(0, 0, 0, 0.35);
2330
+ pointer-events: none;
2331
+ }
2332
+ #relay-toast.visible { display: block; }
2333
+ #relay-toast.has-action { pointer-events: auto; }
2334
+ .relay-toast-action {
2335
+ margin-left: 10px;
2336
+ border: 1px solid var(--warn);
2337
+ background: transparent;
2338
+ color: var(--warn);
2339
+ border-radius: 999px;
2340
+ padding: 4px 12px;
2341
+ font-size: var(--fs-meta-sm);
2342
+ font-weight: 600;
2343
+ cursor: pointer;
2344
+ }
2345
+ .relay-toast-action:hover { background: rgba(210, 153, 34, 0.2); }
2199
2346
  #model-metadata-blocker {
2200
2347
  display: none;
2201
2348
  align-items: center;
@@ -2362,7 +2509,7 @@
2362
2509
  border-radius: 12px; height: 46px; padding: 0 12px; font-size: var(--fs-core-control);
2363
2510
  min-width: 140px; outline: none;
2364
2511
  }
2365
- #context-tier-select { min-width: 70px; width: 70px; }
2512
+ #context-tier-select { min-width: 84px; width: 84px; } /* aligned with #mode-select above it */
2366
2513
  #reasoning-effort-select { min-width: 0; width: auto; }
2367
2514
  #model-select:focus,
2368
2515
  #reasoning-effort-select:focus,
@@ -2396,6 +2543,16 @@
2396
2543
  (badge above tokens/elapsed/Stop) so the badge cannot float mid-row when
2397
2544
  the wrapped description spans two lines. */
2398
2545
  .bg-task-side { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; flex-shrink: 0; }
2546
+ .bg-task-side-head { display: flex; align-items: center; gap: 8px; }
2547
+ .bg-task-model { color: var(--text); font-size: var(--fs-meta-sm); white-space: nowrap; }
2548
+ /* Many tasks or previews must not push the composer off-screen: both lists
2549
+ scroll together inside the panel while the summary line stays put. The dvh term keeps the whole
2550
+ composer on screen in phone landscape, where 45vh alone left the input
2551
+ row clipped below the fold (header + the rest of the composer take ~296px). */
2552
+ #background-panel-body {
2553
+ max-height: clamp(64px, calc(100dvh - 296px), min(45vh, 420px));
2554
+ overflow-y: auto; overscroll-behavior: contain;
2555
+ }
2399
2556
  .bg-task-side-status { display: flex; align-items: center; gap: 10px; }
2400
2557
  /* Mobile readability: bounded wrapping instead of single-line ellipsis.
2401
2558
  Long text clamps to two lines so the tail (the part that distinguishes
@@ -2411,6 +2568,19 @@
2411
2568
  background: var(--bg3); white-space: nowrap;
2412
2569
  }
2413
2570
  .bg-task-detail { color: var(--muted); font-size: var(--fs-meta-sm); }
2571
+ /* A live tool call is one line cropped to the row: the command's head is
2572
+ what identifies it, and wrapping would make rows jump on every call. */
2573
+ .bg-task-detail.bg-task-toolcall {
2574
+ display: block; white-space: nowrap; text-overflow: ellipsis; overflow: hidden;
2575
+ }
2576
+ /* A phone row is too narrow for one line to say anything; two clamped lines
2577
+ fit beside the side column's height anyway. */
2578
+ @media (max-width: 680px) {
2579
+ .bg-task-detail.bg-task-toolcall {
2580
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
2581
+ white-space: normal; overflow-wrap: anywhere;
2582
+ }
2583
+ }
2414
2584
  .bg-task-tokens { color: var(--muted); flex-shrink: 0; font-variant-numeric: tabular-nums; }
2415
2585
  .bg-task-elapsed { color: var(--muted); flex-shrink: 0; font-variant-numeric: tabular-nums; }
2416
2586
  .bg-task-stop {
@@ -2492,6 +2662,30 @@
2492
2662
  background: rgba(63, 185, 80, 0.12); color: var(--relay-tone-online);
2493
2663
  border-radius: 6px; padding: 0 6px; font-size: var(--fs-meta-sm);
2494
2664
  }
2665
+ /* Mid-turn steering. The CLI usually FOLDS a steered message into the
2666
+ running turn and answers everything with one reply; the steered row then
2667
+ settles as a compact marker under its own message (kind='folded'). More
2668
+ rarely the turn is handed off: the interrupted reply settles as
2669
+ kind='absorbed' and the steered message carries the continuing reply. A
2670
+ steer cut off by Stop settles as kind='stopped' with a Resend button.
2671
+ Markers are notes, not answers, so they render muted and compact.
2672
+ steered-turn-merge.mjs adds the adjacency classes. */
2673
+ .msg.assistant.msg-absorbed .msg-bubble,
2674
+ .msg.assistant.msg-folded .msg-bubble,
2675
+ .msg.assistant.msg-steer-stopped .msg-bubble {
2676
+ font-size: var(--fs-meta-md); color: var(--muted); font-style: italic;
2677
+ padding: 6px 12px; opacity: 0.85;
2678
+ }
2679
+ .msg.assistant.msg-absorbed { margin-top: -4px; }
2680
+ /* Attached to the steered message above it (the transcript gap is 12px). */
2681
+ .msg.assistant.msg-folded,
2682
+ .msg.assistant.msg-steer-stopped { margin-top: -8px; }
2683
+ .msg.assistant.msg-folded .msg-label,
2684
+ .msg.assistant.msg-steer-stopped .msg-label { display: none; }
2685
+ .msg.assistant.msg-steer-stopped .msg-bubble-actions { font-style: normal; }
2686
+ .msg.user.msg-steered .msg-bubble { border-style: dashed; }
2687
+ .msg.user.msg-steered .msg-label::after { content: ' · steered mid-turn'; }
2688
+ .msg.assistant.msg-steered-continuation { margin-top: -2px; }
2495
2689
  body.shared-reader-mode #background-tasks-panel { display: none; }
2496
2690
  #model-pricing-details {
2497
2691
  display: none; margin-top: 6px; color: var(--muted); font-size: var(--fs-meta-sm);
@@ -2504,8 +2698,10 @@
2504
2698
  .model-pricing-grid b { color: var(--text); font-weight: 600; }
2505
2699
  #mode-select {
2506
2700
  background: var(--bg3); border: 1px solid var(--border); color: var(--text);
2507
- border-radius: 12px; height: 46px; padding: 0 12px; font-size: var(--fs-core-control);
2508
- min-width: 70px; width: 70px; outline: none;
2701
+ border-radius: 12px; height: 46px; padding: 0 8px; font-size: var(--fs-core-control);
2702
+ /* Wide enough for "Autopilot", the longest mode; the model select is the
2703
+ flexible column and its labels are kept short (humanizeModelLabel). */
2704
+ min-width: 84px; width: 84px; outline: none;
2509
2705
  }
2510
2706
  #mode-select:focus { border-color: var(--accent); }
2511
2707
  #send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
@@ -2758,6 +2954,112 @@
2758
2954
  margin-top: 10px;
2759
2955
  }
2760
2956
 
2957
+ /* Annotation editor. Clones the file-preview shell, but sits above it
2958
+ (z-index 11500) so annotating a sent image can layer over the lightbox. */
2959
+ #image-annotate-modal {
2960
+ position: fixed; inset: 0; z-index: 11500;
2961
+ display: none; align-items: center; justify-content: center;
2962
+ background: rgba(0, 0, 0, 0.78);
2963
+ padding: 12px;
2964
+ }
2965
+ #image-annotate-modal.visible { display: flex; }
2966
+ .annotate-dialog {
2967
+ width: min(1100px, 100%);
2968
+ height: min(92vh, 900px);
2969
+ display: flex;
2970
+ flex-direction: column;
2971
+ background: var(--bg2);
2972
+ border: 1px solid var(--border);
2973
+ border-radius: 14px;
2974
+ overflow: hidden;
2975
+ }
2976
+ .annotate-header {
2977
+ display: flex; align-items: center; justify-content: space-between; gap: 10px;
2978
+ padding: 10px 14px;
2979
+ border-bottom: 1px solid var(--border);
2980
+ background: var(--bg3);
2981
+ }
2982
+ .annotate-title {
2983
+ min-width: 0;
2984
+ font-size: var(--fs-meta-md);
2985
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
2986
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
2987
+ }
2988
+ .annotate-close {
2989
+ flex: none;
2990
+ border: 1px solid var(--border);
2991
+ background: var(--bg2);
2992
+ color: var(--text);
2993
+ border-radius: 10px;
2994
+ width: 40px;
2995
+ height: 40px;
2996
+ cursor: pointer;
2997
+ }
2998
+ #annotate-stage {
2999
+ flex: 1;
3000
+ min-height: 0; /* lets the stage shrink inside the dialog's height */
3001
+ display: flex; align-items: center; justify-content: center;
3002
+ overflow: hidden;
3003
+ background: #0b0f14;
3004
+ /* The editor owns every gesture: no browser panning, and a pinch at the
3005
+ edge must not scroll the page behind the modal. */
3006
+ touch-action: none;
3007
+ overscroll-behavior: contain;
3008
+ user-select: none;
3009
+ -webkit-user-select: none;
3010
+ cursor: crosshair;
3011
+ }
3012
+ #annotate-surface {
3013
+ position: relative;
3014
+ transform-origin: center center;
3015
+ line-height: 0;
3016
+ }
3017
+ #annotate-image {
3018
+ display: block;
3019
+ pointer-events: none;
3020
+ user-select: none;
3021
+ -webkit-user-drag: none;
3022
+ }
3023
+ #annotate-canvas {
3024
+ position: absolute;
3025
+ inset: 0;
3026
+ pointer-events: none; /* the stage is the single gesture surface */
3027
+ }
3028
+ #annotate-toolbar {
3029
+ display: flex; flex-wrap: wrap; gap: 8px; align-items: center;
3030
+ padding: 10px 14px;
3031
+ border-top: 1px solid var(--border);
3032
+ background: var(--bg3);
3033
+ }
3034
+ .annotate-color, .annotate-width, .annotate-action {
3035
+ display: inline-flex; align-items: center; justify-content: center;
3036
+ min-width: 40px; height: 40px; /* thumb-sized targets */
3037
+ padding: 0 8px;
3038
+ border: 1px solid var(--border);
3039
+ border-radius: 10px;
3040
+ background: var(--bg2);
3041
+ color: var(--text);
3042
+ font-size: var(--fs-meta-md);
3043
+ cursor: pointer;
3044
+ }
3045
+ .annotate-color { padding: 0; }
3046
+ .annotate-color.active,
3047
+ .annotate-width.active {
3048
+ border-color: var(--accent);
3049
+ box-shadow: 0 0 0 3px var(--accent);
3050
+ }
3051
+ .annotate-width-dot {
3052
+ display: block;
3053
+ border-radius: 50%;
3054
+ background: var(--text);
3055
+ }
3056
+ .annotate-width[data-width="thin"] .annotate-width-dot { width: 5px; height: 5px; }
3057
+ .annotate-width[data-width="medium"] .annotate-width-dot { width: 9px; height: 9px; }
3058
+ .annotate-width[data-width="thick"] .annotate-width-dot { width: 15px; height: 15px; }
3059
+ .annotate-action { font-size: 1.1rem; }
3060
+ .annotate-toolbar-gap { flex: 1 1 auto; min-width: 8px; }
3061
+ #annotate-toolbar button:disabled { opacity: 0.45; cursor: default; }
3062
+
2761
3063
  #settings-modal, #summary-modal, #tmux-inspector-modal, #new-conversation-model-modal {
2762
3064
  position: fixed;
2763
3065
  inset: 0;
@@ -2775,6 +3077,8 @@
2775
3077
  #new-conversation-provider-select,
2776
3078
  #new-conversation-model-select,
2777
3079
  #new-conversation-reasoning-select,
3080
+ #new-conversation-context-select,
3081
+ #new-conversation-size-select,
2778
3082
  #new-conversation-cwd-select,
2779
3083
  #new-conversation-cwd-manual {
2780
3084
  width: 100%;
@@ -3657,7 +3961,7 @@
3657
3961
  }
3658
3962
  #repo-browser-modal.visible { display: flex; }
3659
3963
  /* CWD-pick mode is launched from the summary / new-chat modals (z 12050),
3660
- so the browser must stack above them; toasts stay on top at 12500. */
3964
+ so the browser must stack above them; #relay-toast stays on top at 12600. */
3661
3965
  #repo-browser-modal.cwd-pick-mode { z-index: 12400; }
3662
3966
  .repo-cwd-pick-bar {
3663
3967
  display: flex;
@@ -4044,6 +4348,43 @@
4044
4348
  color: #000;
4045
4349
  font-weight: 700;
4046
4350
  }
4351
+ /* Multi-select cards: the same pills, with a checkmark instead of one-shot
4352
+ buttons, so several answers can be ticked before one Reply. */
4353
+ .relay-question-choice-check {
4354
+ display: inline-flex;
4355
+ align-items: center;
4356
+ gap: 8px;
4357
+ user-select: none;
4358
+ }
4359
+ .relay-question-choice-check input[type="checkbox"] {
4360
+ margin: 0;
4361
+ width: 16px;
4362
+ height: 16px;
4363
+ accent-color: var(--accent);
4364
+ cursor: pointer;
4365
+ }
4366
+ .relay-question-choice-check:has(input:checked) {
4367
+ border-color: var(--accent);
4368
+ background: rgba(88, 166, 255, 0.12);
4369
+ }
4370
+ /* The "Select several" switch on Copilot choice cards. */
4371
+ .relay-question-multi-toggle {
4372
+ display: inline-flex;
4373
+ align-items: center;
4374
+ gap: 6px;
4375
+ margin-top: 8px;
4376
+ font-size: var(--fs-meta-sm);
4377
+ color: var(--muted, var(--text));
4378
+ cursor: pointer;
4379
+ user-select: none;
4380
+ }
4381
+ .relay-question-multi-toggle input[type="checkbox"] {
4382
+ margin: 0;
4383
+ width: 16px;
4384
+ height: 16px;
4385
+ accent-color: var(--accent);
4386
+ cursor: pointer;
4387
+ }
4047
4388
  .relay-question-reply {
4048
4389
  display: flex;
4049
4390
  gap: 8px;
@@ -4244,10 +4585,12 @@
4244
4585
  font-size: var(--fs-meta-md);
4245
4586
  min-height: 36px;
4246
4587
  }
4247
- .thinking-bubble-header { margin-bottom: 6px; }
4588
+ .thinking-bubble-header { margin-bottom: 6px; top: -12px; }
4248
4589
  .sidebar-header { padding: var(--sidebar-header-top-pad-mobile) 12px 12px; gap: 8px; }
4249
4590
  #new-conv-btn { padding: 9px 12px; border-radius: 8px; font-size: var(--fs-meta-md); }
4250
4591
  #status-btn { min-width: 72px; padding: 9px 10px; font-size: var(--fs-meta-md); }
4592
+ #conv-filter-row { padding: 8px 12px; }
4593
+ #conv-filter-clear { right: 16px; }
4251
4594
  #conv-list { padding: 0 5px 8px; }
4252
4595
  .conv-item { padding: 6px 11px; border-radius: 8px; }
4253
4596
  .conv-title {
@@ -4762,6 +5105,7 @@
4762
5105
  <div id="sidebar-overlay" onclick="closeSidebar()"></div>
4763
5106
  <div id="chat-actions-menu-backdrop" onclick="window.closeChatActionsMenu && window.closeChatActionsMenu()"></div>
4764
5107
  <div id="font-scale-indicator" aria-live="polite" aria-atomic="true" hidden>100%</div>
5108
+ <div id="relay-toast" role="status" aria-live="polite"></div>
4765
5109
 
4766
5110
  <!-- Main App -->
4767
5111
  <div id="app">
@@ -4774,6 +5118,11 @@
4774
5118
  <button id="install-btn" class="header-icon-btn" onclick="promptInstallApp()" title="Install app to home screen" aria-label="Install app">⬇</button>
4775
5119
  </div>
4776
5120
  </div>
5121
+ <div id="conv-filter-row">
5122
+ <input id="conv-filter-input" type="text" placeholder="Filter conversations…" autocomplete="off" spellcheck="false" aria-label="Filter conversations by title">
5123
+ <button id="conv-filter-clear" type="button" title="Clear filter" aria-label="Clear filter" hidden>×</button>
5124
+ <div id="conv-filter-status" role="status" aria-live="polite"></div>
5125
+ </div>
4777
5126
  <div id="conv-list"></div>
4778
5127
  </div>
4779
5128
  <div id="sidebar-resizer" role="separator" aria-orientation="vertical" aria-label="Resize conversations sidebar"></div>
@@ -4839,8 +5188,10 @@
4839
5188
  <div id="slash-autocomplete-popup" aria-hidden="true" role="listbox"></div>
4840
5189
  <details id="background-tasks-panel" hidden>
4841
5190
  <summary id="background-tasks-summary"></summary>
4842
- <div id="background-previews-list"></div>
4843
- <div id="background-tasks-list"></div>
5191
+ <div id="background-panel-body">
5192
+ <div id="background-previews-list"></div>
5193
+ <div id="background-tasks-list"></div>
5194
+ </div>
4844
5195
  </details>
4845
5196
  <div class="input-row">
4846
5197
  <input id="image-input" type="file" multiple hidden onchange="handleAttachmentInput(this.files); this.value='';">
@@ -5002,6 +5353,16 @@
5002
5353
  tabindex="-1"
5003
5354
  data-settings-tab="notifications"
5004
5355
  >Notifications</button>
5356
+ <button
5357
+ id="settings-tab-features"
5358
+ class="settings-tab"
5359
+ type="button"
5360
+ role="tab"
5361
+ aria-selected="false"
5362
+ aria-controls="settings-panel-features"
5363
+ tabindex="-1"
5364
+ data-settings-tab="features"
5365
+ >Features</button>
5005
5366
  </div>
5006
5367
  <div class="summary-body" style="padding: 16px;">
5007
5368
  <div
@@ -5050,31 +5411,41 @@
5050
5411
  </div>
5051
5412
  <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>
5052
5413
  <div class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
5053
- <label for="background-task-timeout-slider">Background task timeout <span id="background-task-timeout-value">No limit</span></label>
5414
+ <label for="background-task-timeout-slider">Background task timeout <span id="background-task-timeout-value">4 h</span></label>
5054
5415
  <input
5055
5416
  id="background-task-timeout-slider"
5056
5417
  type="range"
5057
5418
  min="0"
5058
5419
  max="600"
5059
5420
  step="5"
5060
- value="0"
5421
+ value="240"
5061
5422
  aria-label="Background task timeout in minutes"
5062
5423
  oninput="previewBackgroundTaskTimeoutSetting(this.value)"
5063
5424
  onchange="updateBackgroundTaskTimeoutSetting(this.value)"
5064
5425
  >
5065
5426
  </div>
5066
- <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>
5427
+ <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>
5067
5428
  <div id="windows-autostart-setting" hidden>
5068
- <div class="settings-row" style="margin-top: 12px; margin-bottom: 0;">
5069
- <label for="windows-autostart-toggle">Autostart (Windows)</label>
5070
- <input
5071
- id="windows-autostart-toggle"
5072
- type="checkbox"
5073
- onchange="updateWindowsAutostartSettingFromToggle(this.checked)"
5074
- aria-label="Autostart on Windows sign-in"
5075
- >
5429
+ <div class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
5430
+ <label>Autostart (Windows)</label>
5431
+ <div role="radiogroup" aria-label="Windows autostart mode" style="display: flex; flex-direction: column; gap: 4px;">
5432
+ <label style="font-weight: normal;">
5433
+ <input type="radio" name="windows-autostart-mode" value="off" onchange="updateWindowsAutostartMode('off')"> Off
5434
+ </label>
5435
+ <label style="font-weight: normal;">
5436
+ <input type="radio" name="windows-autostart-mode" value="signin" onchange="updateWindowsAutostartMode('signin')"> At sign-in — visible terminal after you log on
5437
+ </label>
5438
+ <label style="font-weight: normal;">
5439
+ <input type="radio" name="windows-autostart-mode" value="boot" onchange="updateWindowsAutostartMode('boot')"> At system startup — headless, no logon needed
5440
+ </label>
5441
+ </div>
5442
+ </div>
5443
+ <div class="settings-help" style="margin-top: 6px;">System startup runs the relay as your user via a scheduled task (no password stored), reachable ~60&nbsp;s after boot before anyone signs in. Switching modes asks for one admin confirmation on the PC itself. The task's identity has no network-share credentials — local files and the tunnel are unaffected.</div>
5444
+ <div id="windows-autostart-status" class="settings-help" style="margin-top: 6px;" hidden></div>
5445
+ <div id="windows-autostart-manual" class="settings-help" style="margin-top: 6px;" hidden>
5446
+ Run this from an elevated PowerShell instead:
5447
+ <code id="windows-autostart-manual-command" style="display: block; margin-top: 4px; user-select: all;"></code>
5076
5448
  </div>
5077
- <div class="settings-help" style="margin-top: 6px;">Open a visible terminal and start only the web relay server when you sign in. A Copilot CLI host must attach separately to process turns.</div>
5078
5449
  </div>
5079
5450
  <div class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
5080
5451
  <label for="pwa-app-name-input">Install app name</label>
@@ -5086,7 +5457,42 @@
5086
5457
  aria-label="Install app name"
5087
5458
  onchange="updatePwaAppName(this.value)"
5088
5459
  >
5089
- <div class="settings-help">Used for the installed PWA app label. Some platforms may require reinstalling the app after a rename.</div>
5460
+ <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>
5461
+ </div>
5462
+ <div class="settings-row" style="margin-top: 12px; margin-bottom: 0;">
5463
+ <label for="update-auto-check-toggle">Check for updates automatically</label>
5464
+ <input
5465
+ id="update-auto-check-toggle"
5466
+ type="checkbox"
5467
+ onchange="updateAutoCheckSetting(this.checked)"
5468
+ aria-label="Check for updates automatically"
5469
+ >
5470
+ </div>
5471
+ <div class="settings-help" style="margin-top: 6px;">Off by default — OAR sends no traffic anywhere unless you opt in. When enabled, the relay asks oar.sh for the latest version about twice a day. Manual checks below work regardless.</div>
5472
+ <div id="update-check-killed-help" class="settings-help" style="margin-top: 6px;" hidden>Update checks are disabled by OAR_NO_UPDATE_CHECK in this relay's environment.</div>
5473
+ <div class="settings-row" style="margin-top: 6px; margin-bottom: 0;">
5474
+ <button id="update-check-now-btn" class="chat-title-action-btn" type="button" onclick="checkForUpdatesNow()">Check for updates</button>
5475
+ <span id="update-last-checked" class="settings-help"></span>
5476
+ </div>
5477
+ <div id="update-available-card" class="settings-help" style="margin-top: 6px;" hidden>
5478
+ <span id="update-available-text"></span>
5479
+ <a id="update-notes-link" target="_blank" rel="noopener" hidden>release notes</a>
5480
+ <div class="settings-row" style="margin-top: 6px; margin-bottom: 0;">
5481
+ <button id="update-apply-btn" class="chat-title-action-btn" type="button" onclick="applyAvailableUpdate()">Update</button>
5482
+ <button id="update-dismiss-btn" class="chat-title-action-btn" type="button" onclick="dismissAvailableUpdate()">Dismiss</button>
5483
+ </div>
5484
+ <div id="update-git-hint" style="margin-top: 4px;" hidden>This relay runs from a git checkout — pull and restart to update.</div>
5485
+ </div>
5486
+ <div id="update-install-status" class="settings-help" style="margin-top: 6px;" hidden></div>
5487
+ <button id="update-cancel-btn" class="chat-title-action-btn" type="button" style="margin-top: 4px;" onclick="cancelRunningUpdate()" hidden>Cancel update</button>
5488
+ <details id="update-install-log" class="cli-install-log" hidden>
5489
+ <summary>Update log</summary>
5490
+ <pre id="update-install-log-pre"></pre>
5491
+ </details>
5492
+ <div id="update-outcome" class="settings-help" style="margin-top: 6px;" hidden>
5493
+ <span id="update-outcome-text"></span>
5494
+ <pre id="update-outcome-log" hidden></pre>
5495
+ <button id="update-outcome-dismiss-btn" class="chat-title-action-btn" type="button" onclick="dismissUpdateOutcome()">Dismiss</button>
5090
5496
  </div>
5091
5497
  <div class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
5092
5498
  <label for="default-session-workspace-root-input">Default CWD for new sessions</label>
@@ -5193,14 +5599,14 @@
5193
5599
  <div class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
5194
5600
  <label for="copilot-engine-select">Copilot engine</label>
5195
5601
  <select id="copilot-engine-select" aria-label="Copilot engine">
5196
- <option value="extension">Extension — current</option>
5197
- <option value="sdk">SDK — experimental</option>
5602
+ <option value="extension">Extension</option>
5603
+ <option value="sdk">SDK — default</option>
5198
5604
  </select>
5199
5605
  <div id="copilot-settings-status" class="settings-help settings-provider-status">Loading Copilot settings…</div>
5200
5606
  <div style="display: flex; gap: 8px; margin-top: 8px;">
5201
5607
  <button id="copilot-save-btn" type="button" class="summary-btn" onclick="saveCopilotSettings()">Save Copilot settings</button>
5202
5608
  </div>
5203
- <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>
5609
+ <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>
5204
5610
  </div>
5205
5611
  </div>
5206
5612
  <div
@@ -5617,6 +6023,23 @@
5617
6023
  <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>
5618
6024
  </div>
5619
6025
  </div>
6026
+ <div
6027
+ id="settings-panel-features"
6028
+ class="settings-panel"
6029
+ role="tabpanel"
6030
+ tabindex="0"
6031
+ aria-labelledby="settings-tab-features"
6032
+ data-settings-panel="features"
6033
+ hidden
6034
+ >
6035
+ <div class="settings-help" style="margin-top: 12px;">Feature flags are applied when the relay starts. Changes here are saved immediately, but only take effect after the relay restarts.</div>
6036
+ <div id="features-flag-list"></div>
6037
+ <div id="features-restart-notice" class="settings-help" style="margin-top: 12px;" hidden>
6038
+ Saved changes differ from the running relay.
6039
+ <button id="features-restart-btn" type="button" style="margin-left: 8px;">Restart relay</button>
6040
+ </div>
6041
+ <div id="features-settings-status" class="settings-help" style="margin-top: 12px;" hidden></div>
6042
+ </div>
5620
6043
  </div>
5621
6044
  </div>
5622
6045
  </div>
@@ -5654,6 +6077,11 @@
5654
6077
  <label for="new-conversation-reasoning-select" style="display:block;margin-top:12px;">Reasoning effort</label>
5655
6078
  <select id="new-conversation-reasoning-select"></select>
5656
6079
  <div id="new-conversation-reasoning-status" style="margin-top:8px;font-size:0.78rem;color:var(--muted);">Choose the effort used when this conversation starts.</div>
6080
+ <div id="new-conversation-context-row" style="display:none;">
6081
+ <label for="new-conversation-context-select" style="display:block;margin-top:12px;">Context window</label>
6082
+ <select id="new-conversation-context-select" disabled aria-readonly="true"></select>
6083
+ <div id="new-conversation-context-status" style="margin-top:8px;font-size:0.78rem;color:var(--muted);">Context window of the selected model; the tier can be changed from the composer once the chat is open.</div>
6084
+ </div>
5657
6085
  <div id="new-conversation-size-row" style="display:none;">
5658
6086
  <label for="new-conversation-size-select" style="display:block;margin-top:12px;">Size</label>
5659
6087
  <select id="new-conversation-size-select"></select>
@@ -5724,6 +6152,8 @@
5724
6152
  <button id="file-preview-mode-raw" class="file-preview-btn" onclick="setFilePreviewMode('raw')">Raw</button>
5725
6153
  <button id="file-preview-html-toggle" class="file-preview-btn" onclick="toggleFilePreviewHtml()">Show literal HTML</button>
5726
6154
  <a id="file-preview-open-raw" class="file-preview-btn" download>Download</a>
6155
+ <button id="file-preview-copy-btn" class="file-preview-btn" type="button" onclick="copyFilePreviewImage()" hidden>Copy</button>
6156
+ <button id="file-preview-annotate-btn" class="file-preview-btn" type="button" onclick="openAnnotateEditorForPreview()" hidden>🖍️ Annotate</button>
5727
6157
  <span id="file-preview-warning" class="file-preview-warning">⚠ Safe layout HTML enabled; active content is blocked</span>
5728
6158
  </div>
5729
6159
  <div id="file-preview-meta" class="file-preview-meta"></div>
@@ -5731,6 +6161,35 @@
5731
6161
  </div>
5732
6162
  </div>
5733
6163
 
6164
+ <!-- Image annotation editor (all listeners are attached by annotate-editor.js) -->
6165
+ <div id="image-annotate-modal" aria-hidden="true">
6166
+ <div class="annotate-dialog" role="dialog" aria-modal="true" aria-label="Annotate image">
6167
+ <div class="annotate-header">
6168
+ <div id="annotate-title" class="annotate-title">Annotate image</div>
6169
+ <button id="annotate-close-btn" class="annotate-close" type="button" aria-label="Close without saving">✖</button>
6170
+ </div>
6171
+ <div id="annotate-stage">
6172
+ <div id="annotate-surface">
6173
+ <img id="annotate-image" alt="Image being annotated">
6174
+ <canvas id="annotate-canvas"></canvas>
6175
+ </div>
6176
+ </div>
6177
+ <div id="annotate-toolbar">
6178
+ <button class="annotate-color active" type="button" data-color="yellow" style="background: #ffdd00;" aria-label="Yellow marker"></button>
6179
+ <button class="annotate-color" type="button" data-color="red" style="background: #ff3b30;" aria-label="Red marker"></button>
6180
+ <button class="annotate-color" type="button" data-color="green" style="background: #34c759;" aria-label="Green marker"></button>
6181
+ <button class="annotate-color" type="button" data-color="blue" style="background: #0a84ff;" aria-label="Blue marker"></button>
6182
+ <button class="annotate-width" type="button" data-width="thin" aria-label="Thin stroke"><span class="annotate-width-dot"></span></button>
6183
+ <button class="annotate-width active" type="button" data-width="medium" aria-label="Medium stroke"><span class="annotate-width-dot"></span></button>
6184
+ <button class="annotate-width" type="button" data-width="thick" aria-label="Thick stroke"><span class="annotate-width-dot"></span></button>
6185
+ <span class="annotate-toolbar-gap"></span>
6186
+ <button id="annotate-undo-btn" class="annotate-action" type="button" aria-label="Undo" disabled>↩️</button>
6187
+ <button id="annotate-reset-btn" class="annotate-action" type="button" aria-label="Clear all" disabled>🗑️</button>
6188
+ <button id="annotate-accept-btn" class="annotate-action" type="button" aria-label="Accept" disabled>✅</button>
6189
+ </div>
6190
+ </div>
6191
+ </div>
6192
+
5734
6193
  <script>
5735
6194
  window.__loadCopilotApp().catch((error) => {
5736
6195
  document.getElementById('startup-loading')?.remove();