@oar-sh/oar 0.9.24 → 0.9.26

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 (92) hide show
  1. package/.github/extensions/web-relay/skills/reasoning-stream.mjs +3 -1
  2. package/CHANGELOG.md +284 -0
  3. package/README.md +12 -11
  4. package/package.json +5 -1
  5. package/server/claude-worker/claude-remote-relay-tool.mjs +15 -0
  6. package/server/claude-worker/claude-sdk-adapter.mjs +8 -4
  7. package/server/claude-worker/claude-session-process.mjs +4 -0
  8. package/server/claude-worker/claude-turn-publisher.mjs +25 -11
  9. package/server/claude-worker/sdk-message-normalizer.mjs +29 -1
  10. package/server/copilot-worker/copilot-prompt-context.mjs +2 -1
  11. package/server/copilot-worker/copilot-sdk-session-process.mjs +11 -9
  12. package/server/cursor-worker/cursor-turn-runner.mjs +3 -1
  13. package/server/cursor-worker/sdk-message-normalizer.mjs +13 -0
  14. package/server/db-schema.mjs +19 -0
  15. package/server/grok-worker/grok-turn-runner.mjs +3 -1
  16. package/server/grok-worker/sdk-message-normalizer.mjs +3 -0
  17. package/server/migrations/0039-media-index.mjs +167 -0
  18. package/server/migrations/0040-image-session-defaults.mjs +93 -0
  19. package/server/public/app/api-client.js +58 -0
  20. package/server/public/app/ask-user-view.js +2 -1
  21. package/server/public/app/attachments-view.js +66 -9
  22. package/server/public/app/bootstrap.js +85 -25
  23. package/server/public/app/chat-actions-menu-state.mjs +1 -0
  24. package/server/public/app/context-media-tab.js +143 -0
  25. package/server/public/app/context-usage-view.mjs +2 -2
  26. package/server/public/app/conversation-view.js +90 -5
  27. package/server/public/app/image-composer-ui.js +518 -0
  28. package/server/public/app/image-turn-placeholder.mjs +233 -0
  29. package/server/public/app/journal-view.js +12 -2
  30. package/server/public/app/live-activity-cap.mjs +3 -3
  31. package/server/public/app/markdown-math.mjs +471 -0
  32. package/server/public/app/media-gallery-model.mjs +202 -0
  33. package/server/public/app/media-gallery-ui.js +524 -0
  34. package/server/public/app/media-view.js +79 -0
  35. package/server/public/app/openai-image-composer.mjs +492 -0
  36. package/server/public/app/openai-image-cost-ui.js +426 -0
  37. package/server/public/app/openai-image-spend.mjs +443 -0
  38. package/server/public/app/plan-usage-view.mjs +38 -6
  39. package/server/public/app/pwa-install.js +25 -4
  40. package/server/public/app/relay-error-ctas.mjs +5 -0
  41. package/server/public/app/relay-prompt-sanitizer.mjs +3 -0
  42. package/server/public/app/relay-restart-ui.js +2 -2
  43. package/server/public/app/remote-relays-settings.js +33 -6
  44. package/server/public/app/router.js +49 -237
  45. package/server/public/app/session-history-ui.js +2 -0
  46. package/server/public/app/settings-modal.js +4 -0
  47. package/server/public/app/settings-tabs.js +10 -1
  48. package/server/public/app/socket-handlers.js +26 -0
  49. package/server/public/app/store.js +2 -0
  50. package/server/public/app/stream-view-preference.mjs +1 -1
  51. package/server/public/app/tab-strip-fade.mjs +1 -1
  52. package/server/public/app/thoughts-preference.mjs +1 -1
  53. package/server/public/app/tmux-inspector-view.js +29 -16
  54. package/server/public/app/viewer-gallery.mjs +23 -1
  55. package/server/public/index.html +517 -123
  56. package/server/relay-tools.md +1 -0
  57. package/server/repositories/image-conversation-repository.mjs +69 -0
  58. package/server/repositories/media-repository.mjs +305 -0
  59. package/server/routes/media-routes.mjs +361 -0
  60. package/server/routes/messages-routes.mjs +1410 -360
  61. package/server/routes/relay-board-routes.mjs +46 -1
  62. package/server/routes/remote-relay-routes.mjs +1 -1
  63. package/server/routes/sessions-routes.mjs +361 -14
  64. package/server/server-runtime.mjs +21 -0
  65. package/server/services/database-file.mjs +6 -4
  66. package/server/services/generated-image-cleanup-service.mjs +2 -1
  67. package/server/services/generated-image-files.mjs +110 -0
  68. package/server/services/image-input-paths.mjs +389 -0
  69. package/server/services/image-operation-service.mjs +6 -2
  70. package/server/services/media-extract.mjs +653 -0
  71. package/server/services/media-index-service.mjs +221 -0
  72. package/server/services/openai-image-preview-turns.mjs +330 -0
  73. package/server/services/openai-image-spend-service.mjs +516 -0
  74. package/server/services/openai-image-stream.mjs +170 -0
  75. package/server/services/plan-usage-openai-image.mjs +148 -0
  76. package/server/services/relay-prompt-sanitizer.mjs +7 -0
  77. package/server/services/relay-restart-queue.mjs +1 -1
  78. package/server/services/remote-relay-caller-context.mjs +6 -0
  79. package/server/services/remote-relay-dispatcher.mjs +409 -23
  80. package/server/services/remote-relay-registry-service.mjs +10 -2
  81. package/server/services/session-worker-stop-service.mjs +50 -18
  82. package/server/services/token-usage-service.mjs +15 -0
  83. package/shared/image-header.mjs +90 -0
  84. package/shared/math-instructions.mjs +30 -0
  85. package/shared/openai-image-options.mjs +299 -0
  86. package/shared/openai-image-presets.mjs +175 -0
  87. package/shared/openai-image-previews.mjs +40 -0
  88. package/shared/openai-image-prices.mjs +291 -0
  89. package/shared/openai-image-spend.mjs +90 -0
  90. package/shared/remote-relay-contract.mjs +126 -4
  91. package/shared/worker-runtime/direct-openai-image.mjs +96 -50
  92. package/server/public/app/math-dollars.mjs +0 -89
@@ -211,18 +211,17 @@
211
211
  <title>OAR — Open Agent Relay</title>
212
212
  <script>document.write('<scr'+'ipt src="'+__APP_BASE+'/socket.io/socket.io.js"><\/scr'+'ipt>');</script>
213
213
  <script src="https://cdn.jsdelivr.net/npm/marked@15.0.12/marked.min.js"></script>
214
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css">
215
- <script src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js"></script>
216
- <script src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/contrib/mhchem.min.js"></script>
217
- <script src="https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/contrib/auto-render.min.js"></script>
214
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.19.1/dist/katex.min.css" integrity="sha384-WwGtaC6ywCHxA5cstpj/3+xIZvTez8dq2IcBQaXRkenHZ7FVVq1ZROtURwfbtTlG" crossorigin="anonymous">
215
+ <script src="https://cdn.jsdelivr.net/npm/katex@0.19.1/dist/katex.min.js" integrity="sha384-a1WQdjRj162i159OYq041X6I4K8NenkrI09LlPmiHg0YOb7CR+wJOraXwK9abpnE" crossorigin="anonymous"></script>
216
+ <script src="https://cdn.jsdelivr.net/npm/katex@0.19.1/dist/contrib/mhchem.min.js" integrity="sha384-fB8BH//9nBzROkMUsu/Dr35jWHIbnKesUo9rW0hfEgw8mZGnkAyBAjKX9F98OVuo" crossorigin="anonymous"></script>
218
217
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
219
218
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
220
219
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/nginx.min.js"></script>
221
220
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/powershell.min.js"></script>
222
221
  <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/x86asm.min.js"></script>
223
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.5.0/css/xterm.min.css">
224
- <script src="https://cdn.jsdelivr.net/npm/xterm@5.5.0/lib/xterm.min.js"></script>
225
- <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.10.0/lib/xterm-addon-fit.min.js"></script>
222
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css" integrity="sha384-8Xk9wy/gzEDUKrXtrmCFa2bBuK3BpjpDuL/p0SeKQX19Khl/M+lHOgD/CyYf7efP" crossorigin="anonymous">
223
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.js" integrity="sha384-M169f14mRZOXm3hD/v2Ti0ThIT/RnAQagXA9nlE15yHAtrW19gdePJh/HaTzUOe/" crossorigin="anonymous"></script>
224
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.js" integrity="sha384-iF+jqbuti4XlB64clWgFWYEscb+UnSRv3VgVikGYZu+otNFnSHr7y7NcKfBnGizn" crossorigin="anonymous"></script>
226
225
  <script>
227
226
  if (window.hljs && typeof window.hljs.registerAliases === 'function') {
228
227
  window.hljs.registerAliases(['pwsh', 'ps1'], { languageName: 'powershell' });
@@ -891,6 +890,8 @@
891
890
  .inbox-count.is-urgent { background: var(--danger); color: #fff; }
892
891
  body.inbox-view-active #input-area { display: none; }
893
892
  body.inbox-view-active #pending-question-banner { display: none; }
893
+ body.media-view-active #input-area { display: none; }
894
+ body.media-view-active #pending-question-banner { display: none; }
894
895
  .inbox-view { display: grid; gap: calc(7px * var(--ui-scale)); padding: calc(4px * var(--ui-scale)) 0 calc(12px * var(--ui-scale)); }
895
896
  .inbox-filter { display: flex; gap: calc(3px * var(--ui-scale)); flex-wrap: wrap; }
896
897
  .inbox-filter-option {
@@ -1837,6 +1838,31 @@
1837
1838
  /* Two classes: .settings-help, later in this sheet, would grey it out. */
1838
1839
  .settings-help.claude-api-field-error { color: var(--danger); }
1839
1840
  .settings-help.claude-api-field-error[hidden] { display: none; }
1841
+ /* OpenAI's image cap and presets (openai-image-cost-ui.js). */
1842
+ .settings-help.openai-image-field-error { color: var(--danger); }
1843
+ .settings-help.openai-image-field-error[hidden] { display: none; }
1844
+ #openai-image-cap-input {
1845
+ background: var(--bg);
1846
+ color: var(--text);
1847
+ border: 1px solid var(--border);
1848
+ border-radius: calc(6px * var(--ui-scale));
1849
+ padding: calc(3px * var(--ui-scale)) calc(7px * var(--ui-scale));
1850
+ outline: none;
1851
+ font-size: var(--fs-meta-md);
1852
+ width: min(calc(140px * var(--ui-scale)), 100%);
1853
+ }
1854
+ #openai-image-cap-input[aria-invalid="true"] { border-color: var(--danger); }
1855
+ .openai-image-cap-row { display: flex; flex-wrap: wrap; gap: calc(6px * var(--ui-scale)); align-items: center; }
1856
+ #openai-image-spend-line[data-state="warning"] { color: var(--warn); }
1857
+ #openai-image-spend-line[data-state="error"] { color: var(--danger); }
1858
+ #openai-image-allow-today-btn[hidden],
1859
+ #openai-image-stop-allowing-btn[hidden] { display: none; }
1860
+ .openai-image-presets-list { list-style: none; margin: 0; padding: 0; width: 100%; display: flex; flex-direction: column; gap: calc(4px * var(--ui-scale)); }
1861
+ .openai-image-preset-row { display: flex; align-items: center; gap: calc(6px * var(--ui-scale)); min-width: 0; }
1862
+ .openai-image-preset-text { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
1863
+ .openai-image-preset-text strong { font-weight: 600; overflow-wrap: anywhere; }
1864
+ .openai-image-preset-details { color: var(--muted); font-size: var(--fs-meta-sm); overflow-wrap: anywhere; }
1865
+ .openai-image-preset-tag { color: var(--muted); font-size: var(--fs-meta-xs); flex: 0 0 auto; }
1840
1866
  /* The native spin buttons of a number field follow the theme: light ones
1841
1867
  on a dark field read as a glitch. */
1842
1868
  :root:not([data-theme="light"]) input[type="number"] { color-scheme: dark; }
@@ -1951,6 +1977,8 @@
1951
1977
  }
1952
1978
  .settings-section:first-child .settings-section-title { margin-top: 0; }
1953
1979
  .settings-section-title + .settings-row { margin-top: 0; }
1980
+ /* Settings → Appearance: the one line saying where its settings live. */
1981
+ #appearance-browser-note { margin-bottom: calc(8px * var(--ui-scale)); }
1954
1982
  /* A row inside a column block: the block's full width; the lead row (the
1955
1983
  provider's on/off toggle) keeps a small gap to the fields below. */
1956
1984
  .settings-row-full { width: 100%; }
@@ -2402,6 +2430,9 @@
2402
2430
  The anchor class comes from JS rather than :has() so positioning works
2403
2431
  everywhere the transcript renders. */
2404
2432
  pre.has-code-copy { position: relative; }
2433
+ /* The button gets a strip of its own above the first line, so a long line
2434
+ never runs under it. */
2435
+ pre.has-code-copy > code { padding-top: calc(0.72rem + 12px * var(--ui-scale) + 4px); }
2405
2436
  .code-copy-btn {
2406
2437
  position: absolute;
2407
2438
  top: calc(4px * var(--ui-scale));
@@ -2443,8 +2474,8 @@
2443
2474
  vertical-align: bottom;
2444
2475
  -webkit-overflow-scrolling: touch;
2445
2476
  }
2446
- .katex-error {
2447
- color: var(--danger);
2477
+ /* A formula KaTeX could not set is shown as its source, line breaks kept. */
2478
+ .math-source {
2448
2479
  white-space: pre-wrap;
2449
2480
  }
2450
2481
  .msg-bubble code:not(pre code) {
@@ -2499,6 +2530,27 @@
2499
2530
  width: 100%; max-width: 100%; box-sizing: border-box;
2500
2531
  min-width: 0;
2501
2532
  }
2533
+ /* A running image turn in the live bubble (image-turn-placeholder.mjs): the
2534
+ image area keeps the size's aspect ratio from the first second, so a
2535
+ partial arriving does not move the transcript. */
2536
+ .image-turn-placeholder { display: flex; flex-direction: column; gap: calc(4px * var(--ui-scale)); margin: calc(2px * var(--ui-scale)) 0 calc(6px * var(--ui-scale)); }
2537
+ .thinking-bubble:has(> .image-turn-placeholder) > .dots { display: none; }
2538
+ .image-turn-placeholder-meta { display: flex; align-items: baseline; flex-wrap: wrap; gap: calc(6px * var(--ui-scale)); color: var(--text); font-size: var(--fs-meta-md); }
2539
+ .image-turn-placeholder-timer { font-variant-numeric: tabular-nums; color: var(--muted); }
2540
+ .image-turn-placeholder-retry { color: var(--muted); font-style: italic; }
2541
+ .image-turn-placeholder-retry[hidden] { display: none; }
2542
+ .image-turn-placeholder-details { color: var(--muted); font-size: var(--fs-meta-sm); overflow-wrap: anywhere; }
2543
+ .image-turn-placeholder-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: calc(6px * var(--ui-scale)); width: min(100%, calc(360px * var(--ui-scale))); }
2544
+ .image-turn-placeholder-grid:not([data-count="1"]) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2545
+ .image-turn-placeholder-tile {
2546
+ position: relative; overflow: hidden; border-radius: calc(10px * var(--ui-scale));
2547
+ border: 1px solid var(--border); background: var(--bg3);
2548
+ animation: image-turn-pulse 1.8s ease-in-out infinite;
2549
+ }
2550
+ .image-turn-placeholder-tile.has-preview { animation: none; }
2551
+ .image-turn-placeholder-tile img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
2552
+ @keyframes image-turn-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }
2553
+ @media (prefers-reduced-motion: reduce) { .image-turn-placeholder-tile { animation: none; } }
2502
2554
  /* Sticky inside the scroller: Stop on the live bubble is the only way to
2503
2555
  stop a turn, and a long turn must stay stoppable without scrolling back
2504
2556
  up. It sticks within its own bubble only, so it never rides over the
@@ -3315,6 +3367,190 @@
3315
3367
  border-color: var(--accent);
3316
3368
  color: var(--accent);
3317
3369
  }
3370
+ /* The media gallery (media-gallery-ui.js): the 🧠 Media tab and the global
3371
+ Media view. A messenger's shared media: square tiles under month
3372
+ headers, 3 columns on a phone; a Files tab as a list. */
3373
+ .media-gallery {
3374
+ display: flex;
3375
+ flex-direction: column;
3376
+ gap: calc(8px * var(--ui-scale));
3377
+ min-width: 0;
3378
+ }
3379
+ .media-gallery[data-scope="global"] {
3380
+ padding: calc(12px * var(--ui-scale));
3381
+ max-width: calc(1100px * var(--ui-scale));
3382
+ margin: 0 auto;
3383
+ width: 100%;
3384
+ box-sizing: border-box;
3385
+ }
3386
+ .media-gallery-tabs,
3387
+ .media-gallery-chips,
3388
+ .context-tab-strip {
3389
+ display: flex;
3390
+ align-items: center;
3391
+ gap: calc(4px * var(--ui-scale));
3392
+ flex-wrap: wrap;
3393
+ }
3394
+ .media-gallery-tab,
3395
+ .media-gallery-chip,
3396
+ .context-tab {
3397
+ flex: 0 0 auto;
3398
+ border: 1px solid var(--border);
3399
+ background: var(--bg2);
3400
+ color: var(--text);
3401
+ border-radius: calc(8px * var(--ui-scale));
3402
+ height: calc(28px * var(--ui-scale));
3403
+ padding: 0 calc(10px * var(--ui-scale));
3404
+ font-size: var(--fs-meta-md);
3405
+ white-space: nowrap;
3406
+ cursor: pointer;
3407
+ }
3408
+ .media-gallery-tab[aria-selected="true"],
3409
+ .media-gallery-chip[aria-pressed="true"],
3410
+ .context-tab[aria-selected="true"] {
3411
+ border-color: var(--accent);
3412
+ color: var(--accent);
3413
+ }
3414
+ .context-tab-count {
3415
+ margin-left: calc(4px * var(--ui-scale));
3416
+ color: var(--muted);
3417
+ }
3418
+ .context-tab-strip {
3419
+ margin-bottom: calc(10px * var(--ui-scale));
3420
+ }
3421
+ .media-gallery-filters {
3422
+ display: flex;
3423
+ flex-wrap: wrap;
3424
+ align-items: center;
3425
+ gap: calc(6px * var(--ui-scale));
3426
+ }
3427
+ .media-gallery-select,
3428
+ .media-gallery-search {
3429
+ height: calc(28px * var(--ui-scale));
3430
+ border: 1px solid var(--border);
3431
+ border-radius: calc(8px * var(--ui-scale));
3432
+ background: var(--bg2);
3433
+ color: var(--text);
3434
+ font-size: var(--fs-meta-md);
3435
+ padding: 0 calc(6px * var(--ui-scale));
3436
+ max-width: 100%;
3437
+ }
3438
+ .media-gallery-select[data-media-filter="session"] {
3439
+ max-width: calc(260px * var(--ui-scale));
3440
+ }
3441
+ .media-gallery-search {
3442
+ flex: 1 1 calc(180px * var(--ui-scale));
3443
+ min-width: 0;
3444
+ }
3445
+ .media-gallery-toggle {
3446
+ display: inline-flex;
3447
+ align-items: center;
3448
+ gap: calc(4px * var(--ui-scale));
3449
+ font-size: var(--fs-meta-md);
3450
+ color: var(--muted);
3451
+ }
3452
+ .media-gallery-status p {
3453
+ margin: calc(4px * var(--ui-scale)) 0;
3454
+ color: var(--muted);
3455
+ font-size: var(--fs-meta-md);
3456
+ }
3457
+ .media-month-label {
3458
+ position: sticky;
3459
+ top: 0;
3460
+ z-index: 1;
3461
+ margin: 0;
3462
+ padding: calc(6px * var(--ui-scale)) calc(2px * var(--ui-scale));
3463
+ background: var(--bg);
3464
+ color: var(--muted);
3465
+ font-size: var(--fs-meta-md);
3466
+ font-weight: 600;
3467
+ }
3468
+ .media-grid {
3469
+ display: grid;
3470
+ grid-template-columns: repeat(auto-fill, minmax(calc(140px * var(--ui-scale)), 1fr));
3471
+ gap: 2px;
3472
+ }
3473
+ .media-tile {
3474
+ position: relative;
3475
+ aspect-ratio: 1;
3476
+ padding: 0;
3477
+ border: 0;
3478
+ background: var(--bg3);
3479
+ overflow: hidden;
3480
+ cursor: pointer;
3481
+ content-visibility: auto;
3482
+ contain-intrinsic-size: calc(140px * var(--ui-scale));
3483
+ }
3484
+ .media-tile img,
3485
+ .media-tile video {
3486
+ width: 100%;
3487
+ height: 100%;
3488
+ object-fit: cover;
3489
+ display: block;
3490
+ pointer-events: none;
3491
+ }
3492
+ .media-tile:focus-visible {
3493
+ outline: 2px solid var(--accent);
3494
+ outline-offset: -2px;
3495
+ }
3496
+ .media-tile-archived img,
3497
+ .media-tile-archived video {
3498
+ opacity: 0.6;
3499
+ }
3500
+ .media-tile-badge,
3501
+ .media-tile-play {
3502
+ position: absolute;
3503
+ font-size: calc(12px * var(--ui-scale));
3504
+ line-height: 1;
3505
+ padding: calc(3px * var(--ui-scale)) calc(4px * var(--ui-scale));
3506
+ border-radius: calc(6px * var(--ui-scale));
3507
+ background: rgba(0, 0, 0, 0.55);
3508
+ color: #fff;
3509
+ }
3510
+ .media-tile-badge { right: calc(4px * var(--ui-scale)); bottom: calc(4px * var(--ui-scale)); }
3511
+ .media-tile-play { left: calc(4px * var(--ui-scale)); bottom: calc(4px * var(--ui-scale)); }
3512
+ .media-file-list {
3513
+ display: flex;
3514
+ flex-direction: column;
3515
+ }
3516
+ .media-file-row {
3517
+ display: flex;
3518
+ align-items: center;
3519
+ gap: calc(10px * var(--ui-scale));
3520
+ padding: calc(8px * var(--ui-scale)) calc(4px * var(--ui-scale));
3521
+ border: 0;
3522
+ border-bottom: 1px solid var(--border);
3523
+ background: transparent;
3524
+ color: var(--text);
3525
+ text-align: left;
3526
+ cursor: pointer;
3527
+ }
3528
+ .media-file-icon { font-size: calc(22px * var(--ui-scale)); }
3529
+ .media-file-text { display: flex; flex-direction: column; min-width: 0; }
3530
+ .media-file-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3531
+ .media-file-meta { color: var(--muted); font-size: var(--fs-meta-md); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3532
+ .media-gallery-more {
3533
+ align-self: center;
3534
+ border: 1px solid var(--border);
3535
+ background: var(--bg2);
3536
+ color: var(--text);
3537
+ border-radius: calc(8px * var(--ui-scale));
3538
+ padding: calc(4px * var(--ui-scale)) calc(12px * var(--ui-scale));
3539
+ cursor: pointer;
3540
+ }
3541
+ .media-gallery-sentinel { height: 1px; }
3542
+ @media (max-width: 680px) {
3543
+ .media-grid { grid-template-columns: repeat(3, 1fr); }
3544
+ .media-gallery[data-scope="global"] { padding: calc(8px * var(--ui-scale)) calc(4px * var(--ui-scale)); }
3545
+ .media-gallery-select[data-media-filter="session"] { max-width: 100%; flex: 1 1 100%; }
3546
+ .media-gallery-tabs,
3547
+ .media-gallery-chips,
3548
+ .context-tab-strip {
3549
+ flex-wrap: nowrap;
3550
+ overflow-x: auto;
3551
+ scrollbar-width: none;
3552
+ }
3553
+ }
3318
3554
  .plan-usage-card-titles {
3319
3555
  display: flex;
3320
3556
  flex-direction: column;
@@ -3411,6 +3647,17 @@
3411
3647
  background: var(--bg3);
3412
3648
  }
3413
3649
  .plan-usage-meter-secondary .plan-usage-bar { height: calc(6px * var(--ui-scale)); border-radius: calc(3px * var(--ui-scale)); }
3650
+ /* A bar with a mark on it (the OpenAI images card's 80 % warning line). */
3651
+ .plan-usage-bar-marked { position: relative; }
3652
+ .plan-usage-bar-mark {
3653
+ position: absolute;
3654
+ top: 0;
3655
+ bottom: 0;
3656
+ width: 2px;
3657
+ margin-left: -1px;
3658
+ background: var(--text);
3659
+ opacity: 0.55;
3660
+ }
3414
3661
  .plan-usage-bar-fill {
3415
3662
  display: block;
3416
3663
  height: 100%;
@@ -4294,7 +4541,12 @@
4294
4541
  #mode-select,
4295
4542
  #model-select,
4296
4543
  #reasoning-effort-select,
4297
- #context-tier-select {
4544
+ #context-tier-select,
4545
+ #image-size-custom-input,
4546
+ #image-preset-select,
4547
+ #image-count-select,
4548
+ #image-format-select,
4549
+ #image-background-select {
4298
4550
  background: var(--bg3); border: 1px solid var(--border); color: var(--text);
4299
4551
  border-radius: calc(8px * var(--ui-scale)); height: calc(30px * var(--ui-scale)); padding: 0 calc(8px * var(--ui-scale)); font-size: var(--fs-core-control);
4300
4552
  width: 100%; min-width: 0; outline: none;
@@ -4302,7 +4554,64 @@
4302
4554
  #mode-select:focus,
4303
4555
  #model-select:focus,
4304
4556
  #reasoning-effort-select:focus,
4305
- #context-tier-select:focus { border-color: var(--accent); }
4557
+ #context-tier-select:focus,
4558
+ #image-size-custom-input:focus,
4559
+ #image-preset-select:focus,
4560
+ #image-count-select:focus,
4561
+ #image-format-select:focus,
4562
+ #image-background-select:focus { border-color: var(--accent); }
4563
+ /* An image model's custom W×H under the Size select, and Count, Format and
4564
+ Background side by side: three narrow selects fit the 340px popover and
4565
+ the phone's bottom sheet alike. */
4566
+ #image-size-custom-input { box-sizing: border-box; font-family: var(--mono, monospace); }
4567
+ /* No iOS focus zoom (see #conv-filter-input). */
4568
+ @media (pointer: coarse), (max-width: 680px) {
4569
+ #image-size-custom-input { font-size: max(16px, 1rem); }
4570
+ }
4571
+ #image-size-custom-input[aria-invalid="true"] { border-color: var(--danger); }
4572
+ .image-size-custom-row { display: flex; flex-direction: column; gap: calc(3px * var(--ui-scale)); margin-top: calc(4px * var(--ui-scale)); }
4573
+ .image-size-custom-row[hidden],
4574
+ .image-size-custom-error[hidden],
4575
+ .composer-settings-image-row[hidden],
4576
+ .composer-settings-image-row .composer-settings-field[hidden] { display: none; }
4577
+ .image-size-custom-error { color: var(--danger); font-size: var(--fs-meta-sm); overflow-wrap: anywhere; }
4578
+ .composer-settings-image-row { display: flex; gap: calc(6px * var(--ui-scale)); min-width: 0; }
4579
+ /* An image model's Preset select with "Save as preset…" beside it, and
4580
+ what a pick skipped or a save answered under them. */
4581
+ #image-preset-field[hidden],
4582
+ .image-preset-note[hidden] { display: none; }
4583
+ .image-preset-row { display: flex; gap: calc(6px * var(--ui-scale)); min-width: 0; align-items: center; }
4584
+ .image-preset-row #image-preset-select { flex: 1 1 auto; }
4585
+ .image-preset-save-btn {
4586
+ flex: 0 0 auto;
4587
+ background: var(--bg3); border: 1px solid var(--border); color: var(--text);
4588
+ border-radius: calc(8px * var(--ui-scale)); height: calc(30px * var(--ui-scale)); padding: 0 calc(8px * var(--ui-scale));
4589
+ font-size: var(--fs-core-control); cursor: pointer; white-space: nowrap;
4590
+ }
4591
+ .image-preset-save-btn:hover { border-color: var(--accent); }
4592
+ .image-preset-save-btn:disabled { opacity: 0.5; cursor: not-allowed; }
4593
+ .image-preset-note { color: var(--muted); font-size: var(--fs-meta-sm); overflow-wrap: anywhere; }
4594
+ .image-preset-note[data-state="error"] { color: var(--danger); }
4595
+ /* Today's OpenAI image spend in an image chat: amber from the warning
4596
+ mark, red once the cap stops image turns. */
4597
+ #openai-image-spend-banner {
4598
+ background: var(--bg2);
4599
+ background: color-mix(in srgb, var(--warn) 14%, var(--bg2));
4600
+ border: 1px solid var(--warn);
4601
+ color: var(--warn);
4602
+ border-radius: calc(8px * var(--ui-scale));
4603
+ padding: calc(3px * var(--ui-scale)) calc(8px * var(--ui-scale));
4604
+ margin-bottom: calc(5px * var(--ui-scale));
4605
+ font-size: var(--fs-meta-sm);
4606
+ overflow-wrap: anywhere;
4607
+ }
4608
+ #openai-image-spend-banner[hidden] { display: none; }
4609
+ #openai-image-spend-banner[data-kind="reached"] {
4610
+ background: color-mix(in srgb, var(--danger) 14%, var(--bg2));
4611
+ border-color: var(--danger);
4612
+ color: var(--danger);
4613
+ }
4614
+ .composer-settings-image-row .composer-settings-field { flex: 1 1 0; }
4306
4615
  /* Live background-task fold above the composer. The summary reuses the
4307
4616
  relay tones so its running state matches the sidebar dot / burger bars. */
4308
4617
  #background-tasks-panel {
@@ -4761,8 +5070,7 @@
4761
5070
  background: var(--bg); color: var(--text); border: 1px solid var(--border);
4762
5071
  border-radius: calc(6px * var(--ui-scale)); padding: calc(2px * var(--ui-scale)) calc(4px * var(--ui-scale)); font-size: var(--fs-meta-sm);
4763
5072
  }
4764
- .remote-relays-self-name-row { display: flex; align-items: baseline; gap: calc(5px * var(--ui-scale)); flex-wrap: wrap; }
4765
- .remote-relays-self-name { font-family: var(--mono, monospace); color: var(--text); }
5073
+ #settings-panel-relays #pwa-app-name-input,
4766
5074
  #remote-relays-public-url-input,
4767
5075
  #remote-relays-add-url-input,
4768
5076
  #remote-relays-add-token-input {
@@ -6790,6 +7098,10 @@
6790
7098
  /* CWD-pick mode is launched from the summary / new-chat modals (z 12050),
6791
7099
  so the browser must stack above them; #relay-toast stays on top at 12600. */
6792
7100
  #repo-browser-modal.cwd-pick-mode { z-index: 12400; }
7101
+ /* A picture opened from the 🧠 Media tab shows above that modal, and its
7102
+ annotate editor above the picture. */
7103
+ #file-preview-modal.over-summary { z-index: 12200; }
7104
+ body:has(#file-preview-modal.over-summary) #image-annotate-modal { z-index: 12300; }
6793
7105
  .repo-cwd-pick-bar {
6794
7106
  display: flex;
6795
7107
  align-items: center;
@@ -7872,19 +8184,22 @@
7872
8184
  width is the scroll padding too, so a tab scrolled into view (the
7873
8185
  selected one, a focused one) clears it. */
7874
8186
  .settings-tab-strip,
7875
- .plan-usage-tab-strip {
8187
+ .plan-usage-tab-strip,
8188
+ .context-tab-strip,
8189
+ .media-gallery-tabs,
8190
+ .media-gallery-chips {
7876
8191
  --tab-strip-fade: calc(44px * var(--ui-scale));
7877
8192
  scroll-padding-inline: var(--tab-strip-fade);
7878
8193
  }
7879
- :is(.settings-tab-strip, .plan-usage-tab-strip)[data-fade="right"] {
8194
+ :is(.settings-tab-strip, .plan-usage-tab-strip, .context-tab-strip, .media-gallery-tabs, .media-gallery-chips)[data-fade="right"] {
7880
8195
  -webkit-mask-image: linear-gradient(to left, transparent, #000 var(--tab-strip-fade));
7881
8196
  mask-image: linear-gradient(to left, transparent, #000 var(--tab-strip-fade));
7882
8197
  }
7883
- :is(.settings-tab-strip, .plan-usage-tab-strip)[data-fade="both"] {
8198
+ :is(.settings-tab-strip, .plan-usage-tab-strip, .context-tab-strip, .media-gallery-tabs, .media-gallery-chips)[data-fade="both"] {
7884
8199
  -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--tab-strip-fade), #000 calc(100% - var(--tab-strip-fade)), transparent);
7885
8200
  mask-image: linear-gradient(to right, transparent, #000 var(--tab-strip-fade), #000 calc(100% - var(--tab-strip-fade)), transparent);
7886
8201
  }
7887
- :is(.settings-tab-strip, .plan-usage-tab-strip)[data-fade="left"] {
8202
+ :is(.settings-tab-strip, .plan-usage-tab-strip, .context-tab-strip, .media-gallery-tabs, .media-gallery-chips)[data-fade="left"] {
7888
8203
  -webkit-mask-image: linear-gradient(to right, transparent, #000 var(--tab-strip-fade));
7889
8204
  mask-image: linear-gradient(to right, transparent, #000 var(--tab-strip-fade));
7890
8205
  }
@@ -8266,6 +8581,7 @@
8266
8581
  <button id="chat-menu-watching" class="chat-actions-menu-item chat-menu-meta-chip" type="button" role="menuitem" hidden>👁 Watching sessions…</button>
8267
8582
  <button id="chat-menu-schedules" class="chat-actions-menu-item chat-menu-meta-chip" type="button" role="menuitem" hidden>⏰ Schedules…</button>
8268
8583
  <button id="chat-menu-origin" class="chat-actions-menu-item chat-menu-meta-chip" type="button" role="menuitem" hidden>↗ Started by an agent</button>
8584
+ <div id="chat-menu-image-cost" class="chat-actions-menu-meta" data-testid="chat-menu-image-cost" hidden><span id="chat-menu-image-cost-value" class="chat-actions-menu-meta-secondary">OpenAI images: …</span></div>
8269
8585
  <button id="chat-menu-edit-title" class="chat-actions-menu-item" type="button" role="menuitem">✍️ Edit conversation title</button>
8270
8586
  <button id="chat-menu-questions" class="chat-actions-menu-item chat-menu-twin" type="button" role="menuitem" hidden>📓 Questions</button>
8271
8587
  <button id="chat-menu-pins" class="chat-actions-menu-item chat-menu-twin" type="button" role="menuitem" hidden>📍 Pinned messages</button>
@@ -8287,7 +8603,8 @@
8287
8603
  <button id="chat-menu-change-cwd" class="chat-actions-menu-item" type="button" role="menuitem">🗂️ Change CWD</button>
8288
8604
  <button id="chat-menu-git-changes" class="chat-actions-menu-item" type="button" role="menuitem">🌿 Git changes</button>
8289
8605
  <button id="chat-menu-open-cloud-session" class="chat-actions-menu-item" type="button" role="menuitem" hidden>☁️ Open on claude.ai</button>
8290
- <button id="chat-menu-settings" class="chat-actions-menu-item" type="button" role="menuitem">⚙️ Settings</button>
8606
+ <button id="chat-menu-media" class="chat-actions-menu-item" type="button" role="menuitem">🖼️ Media</button>
8607
+ <button id="chat-menu-settings" class="chat-actions-menu-item" type="button" role="menuitem">⚙️ Settings</button>
8291
8608
  <button id="chat-menu-about" class="chat-actions-menu-item" type="button" role="menuitem">ℹ️ About OAR</button>
8292
8609
  <button id="chat-menu-restart-relay" class="chat-actions-menu-item" type="button" role="menuitem">🌄 Restart web relay</button>
8293
8610
  <button id="chat-menu-suspend-host" class="chat-actions-menu-item" type="button" role="menuitem" onclick="openSuspendHostConfirmation()">💤 Suspend host</button>
@@ -8310,6 +8627,8 @@
8310
8627
  <div id="share-preview-banner" role="note"></div>
8311
8628
  <div id="model-banner"></div>
8312
8629
  <div id="usage-limit-banner" role="status" aria-live="polite" hidden></div>
8630
+ <!-- Today's OpenAI image spend in an image chat (openai-image-cost-ui.js). -->
8631
+ <div id="openai-image-spend-banner" data-testid="openai-image-spend-banner" role="status" aria-live="polite" hidden></div>
8313
8632
  <div id="cloud-session-line" aria-label="Cloud session" hidden></div>
8314
8633
  <div id="model-metadata-blocker" aria-live="polite">
8315
8634
  <span id="model-metadata-blocker-text">Model metadata is unavailable. Refresh to choose a model and reasoning effort.</span>
@@ -8367,6 +8686,15 @@
8367
8686
  </select>
8368
8687
  <div id="model-lock-note" hidden></div>
8369
8688
  </div>
8689
+ <!-- An image model's presets (image-composer-ui.js). -->
8690
+ <div id="image-preset-field" class="composer-settings-field" data-field="image-preset" hidden>
8691
+ <label class="composer-settings-field-label" for="image-preset-select">Preset</label>
8692
+ <div class="image-preset-row">
8693
+ <select id="image-preset-select" data-testid="image-preset-select" title="Image preset"><option value="">None</option></select>
8694
+ <button id="image-preset-save-btn" data-testid="image-preset-save" type="button" class="image-preset-save-btn" title="Save the image options below as a preset">Save as preset…</button>
8695
+ </div>
8696
+ <div id="image-preset-note" data-testid="image-preset-note" class="image-preset-note" role="status" aria-live="polite" hidden></div>
8697
+ </div>
8370
8698
  <div class="composer-settings-field" data-field="effort">
8371
8699
  <label id="reasoning-effort-label" class="composer-settings-field-label" for="reasoning-effort-select">Effort</label>
8372
8700
  <select id="reasoning-effort-select" title="Reasoning effort" disabled>
@@ -8378,6 +8706,26 @@
8378
8706
  <select id="context-tier-select" title="Context window">
8379
8707
  <option value="default">—</option>
8380
8708
  </select>
8709
+ <!-- An image model's "Custom…" size (image-composer-ui.js). -->
8710
+ <div id="image-size-custom-row" class="image-size-custom-row" hidden>
8711
+ <input id="image-size-custom-input" type="text" inputmode="text" autocomplete="off" autocapitalize="off" spellcheck="false" placeholder="2048x1152" aria-label="Custom image size, width x height" aria-describedby="image-size-custom-error">
8712
+ <div id="image-size-custom-error" class="image-size-custom-error" role="alert" hidden></div>
8713
+ </div>
8714
+ </div>
8715
+ <!-- An image model's Count, Format and Background (image-composer-ui.js). -->
8716
+ <div id="image-options-row" class="composer-settings-image-row" hidden>
8717
+ <div class="composer-settings-field" data-field="image-count">
8718
+ <label class="composer-settings-field-label" for="image-count-select">Count</label>
8719
+ <select id="image-count-select" title="Images per turn"></select>
8720
+ </div>
8721
+ <div class="composer-settings-field" data-field="image-format">
8722
+ <label class="composer-settings-field-label" for="image-format-select">Format</label>
8723
+ <select id="image-format-select" title="Image format"></select>
8724
+ </div>
8725
+ <div class="composer-settings-field" data-field="image-background">
8726
+ <label class="composer-settings-field-label" for="image-background-select">Background</label>
8727
+ <select id="image-background-select" title="Image background"></select>
8728
+ </div>
8381
8729
  </div>
8382
8730
  <!-- Subscription | API key in a Claude chat (claude-billing-toggle-ui.js). -->
8383
8731
  <div id="claude-billing-toggle" class="claude-billing-toggle composer-settings-field" data-field="billing" hidden></div>
@@ -8499,6 +8847,16 @@
8499
8847
  tabindex="0"
8500
8848
  data-settings-tab="general"
8501
8849
  >General</button>
8850
+ <button
8851
+ id="settings-tab-appearance"
8852
+ class="settings-tab"
8853
+ type="button"
8854
+ role="tab"
8855
+ aria-selected="false"
8856
+ aria-controls="settings-panel-appearance"
8857
+ tabindex="-1"
8858
+ data-settings-tab="appearance"
8859
+ >Appearance</button>
8502
8860
  <button
8503
8861
  id="settings-tab-providers"
8504
8862
  class="settings-tab"
@@ -8569,68 +8927,6 @@
8569
8927
  aria-labelledby="settings-tab-general"
8570
8928
  data-settings-panel="general"
8571
8929
  >
8572
- <section class="settings-section" id="settings-section-appearance" aria-labelledby="settings-section-appearance-title">
8573
- <h3 class="settings-section-title" id="settings-section-appearance-title">Appearance</h3>
8574
- <div class="settings-row">
8575
- <label for="theme-select">Theme</label>
8576
- <select id="theme-select" onchange="updateTheme(this.value)">
8577
- <option value="dark">Dark</option>
8578
- <option value="light">Light</option>
8579
- </select>
8580
- </div>
8581
- <div class="settings-row" style="margin-bottom: 0;">
8582
- <label for="font-scale-select">Text scaling</label>
8583
- <select id="font-scale-select" onchange="updateFontScaleFromSelect(this.value)" aria-label="Text scaling"></select>
8584
- </div>
8585
- <div class="settings-row settings-gap">
8586
- <label for="thoughts-expanded-toggle">Expand thoughts while an agent works</label>
8587
- <input
8588
- id="thoughts-expanded-toggle"
8589
- type="checkbox"
8590
- onchange="updateThoughtsExpandedSetting(this.checked)"
8591
- aria-label="Expand thoughts while an agent works"
8592
- checked
8593
- >
8594
- </div>
8595
- <div class="settings-help settings-gap-xs">Open thoughts in the live turn, in sub-agent bubbles and in the sub-agent viewer. Saved in this browser.</div>
8596
- <div class="settings-row settings-row-column settings-gap">
8597
- <label for="live-activity-cap-slider">Tool lines shown while an agent works <span id="live-activity-cap-value">50 lines</span></label>
8598
- <input
8599
- id="live-activity-cap-slider"
8600
- type="range"
8601
- min="10"
8602
- max="510"
8603
- step="10"
8604
- value="50"
8605
- aria-label="Tool lines shown while an agent works"
8606
- aria-valuetext="50 lines"
8607
- oninput="previewLiveActivityCap(this.value)"
8608
- onchange="updateLiveActivityCap(this.value)"
8609
- >
8610
- </div>
8611
- <div class="settings-help settings-gap-xs">Per agent: the main agent and each sub-agent keep this many of their newest tool lines in the live bubble. The relay keeps up to 500 per agent. Saved in this browser.</div>
8612
- <div class="settings-row settings-gap">
8613
- <label for="stream-view-toggle">Stream view: thoughts, tool calls and the reply in order</label>
8614
- <input
8615
- id="stream-view-toggle"
8616
- type="checkbox"
8617
- onchange="updateStreamViewSetting(this.checked)"
8618
- aria-label="Stream view: thoughts, tool calls and the reply in order"
8619
- >
8620
- </div>
8621
- <div class="settings-help settings-gap-xs">Turns recorded before this version keep the grouped layout. Saved in this browser.</div>
8622
- <div class="settings-row settings-gap">
8623
- <label for="show-suspend-host-toggle">Show Suspend host action</label>
8624
- <input
8625
- id="show-suspend-host-toggle"
8626
- type="checkbox"
8627
- onchange="updateShowSuspendHostSetting(this.checked)"
8628
- aria-label="Show Suspend host action"
8629
- checked
8630
- >
8631
- </div>
8632
- <div class="settings-help settings-gap-xs">Shows or hides the menu action in this browser only. Suspend works only when the relay runs on Windows; a queued suspend still shows its banner here either way.</div>
8633
- </section>
8634
8930
  <section class="settings-section" id="settings-section-reading" aria-labelledby="settings-section-reading-title">
8635
8931
  <h3 class="settings-section-title" id="settings-section-reading-title">Reading</h3>
8636
8932
  <div class="settings-row settings-gap">
@@ -8745,44 +9041,8 @@
8745
9041
  <div id="model-refresh-providers" class="settings-gap" role="group" aria-label="Providers checked for new models"></div>
8746
9042
  <div class="settings-help settings-gap-xs">The relay asks each switched-on provider for its model list this often. A provider whose list was refreshed in the meantime (Refresh in Select Models, a settings save, a sign-in) waits a full interval again, and a failed check is tried again after an hour at the earliest. Whether a new model starts on is set per provider in Select Models. A setting of this relay.</div>
8747
9043
  </section>
8748
- <section class="settings-section" id="settings-section-relay" aria-labelledby="settings-section-relay-title">
8749
- <h3 class="settings-section-title" id="settings-section-relay-title">This relay</h3>
8750
- <div class="settings-row settings-row-column settings-gap">
8751
- <label for="pwa-app-name-input">Relay name</label>
8752
- <input
8753
- id="pwa-app-name-input"
8754
- type="text"
8755
- maxlength="60"
8756
- placeholder="OAR"
8757
- aria-label="Relay name"
8758
- onchange="updatePwaAppName(this.value)"
8759
- >
8760
- <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. A setting of this relay.</div>
8761
- </div>
8762
- <div class="settings-row settings-gap">
8763
- <label for="restart-resume-toggle">After a relay restart, tell interrupted sessions to carry on</label>
8764
- <input
8765
- id="restart-resume-toggle"
8766
- type="checkbox"
8767
- onchange="updateRestartResumeSettingFromToggle(this.checked)"
8768
- aria-label="After a relay restart, tell interrupted sessions to carry on"
8769
- >
8770
- </div>
8771
- <div class="settings-help settings-gap-xs">Most turns a restart interrupts run again by themselves. A turn that cannot (the agent had already taken the message, or had started on its own after a background task) just stops. With this on, such a session gets one relay note once the relay is back, saying when it restarted and asking the agent to check what it was doing and carry on. Your message is never sent again; idle, archived and usage-limit-paused sessions get nothing. Off by default; a setting of this relay.</div>
8772
- <div class="settings-row settings-gap">
8773
- <label for="relay-restart-max-wait-select">Ask after a queued relay restart has waited</label>
8774
- <select id="relay-restart-max-wait-select" onchange="updateRelayRestartMaxWaitSetting(this.value)" aria-label="Ask after a queued relay restart has waited">
8775
- <option value="5">5 min</option>
8776
- <option value="10">10 min</option>
8777
- <option value="15">15 min</option>
8778
- <option value="30" selected>30 min</option>
8779
- <option value="60">1 h</option>
8780
- <option value="120">2 h</option>
8781
- <option value="240">4 h</option>
8782
- <option value="0">No limit</option>
8783
- </select>
8784
- </div>
8785
- <div class="settings-help settings-gap-xs">A queued relay restart waits for the running turns, and new messages wait for the restart. After this long the banner asks whether to restart now, keep waiting or cancel; it never restarts while the turns run, and still restarts as soon as they end. No limit waits until the turns end. A setting of this relay.</div>
9044
+ <section class="settings-section" id="settings-section-computer" aria-labelledby="settings-section-computer-title">
9045
+ <h3 class="settings-section-title" id="settings-section-computer-title">This computer</h3>
8786
9046
  <div class="settings-row settings-row-column settings-gap">
8787
9047
  <label for="default-session-workspace-root-input">Default CWD for new sessions</label>
8788
9048
  <input
@@ -8859,6 +9119,80 @@
8859
9119
  </div>
8860
9120
  </section>
8861
9121
  </div>
9122
+ <div
9123
+ id="settings-panel-appearance"
9124
+ class="settings-panel"
9125
+ role="tabpanel"
9126
+ tabindex="0"
9127
+ aria-labelledby="settings-tab-appearance"
9128
+ data-settings-panel="appearance"
9129
+ hidden
9130
+ >
9131
+ <div id="appearance-browser-note" class="settings-help">These settings are kept in this browser; other devices keep their own.</div>
9132
+ <div class="settings-row">
9133
+ <label for="theme-select">Theme</label>
9134
+ <select id="theme-select" onchange="updateTheme(this.value)" aria-describedby="appearance-browser-note">
9135
+ <option value="dark">Dark</option>
9136
+ <option value="light">Light</option>
9137
+ </select>
9138
+ </div>
9139
+ <div class="settings-row" style="margin-bottom: 0;">
9140
+ <label for="font-scale-select">Text scaling</label>
9141
+ <select id="font-scale-select" onchange="updateFontScaleFromSelect(this.value)" aria-label="Text scaling" aria-describedby="appearance-browser-note"></select>
9142
+ </div>
9143
+ <div class="settings-row settings-gap">
9144
+ <label for="thoughts-expanded-toggle">Expand thoughts while an agent works</label>
9145
+ <input
9146
+ id="thoughts-expanded-toggle"
9147
+ aria-describedby="appearance-browser-note"
9148
+ type="checkbox"
9149
+ onchange="updateThoughtsExpandedSetting(this.checked)"
9150
+ aria-label="Expand thoughts while an agent works"
9151
+ checked
9152
+ >
9153
+ </div>
9154
+ <div class="settings-help settings-gap-xs">Open thoughts in the live turn, in sub-agent bubbles and in the sub-agent viewer.</div>
9155
+ <div class="settings-row settings-row-column settings-gap">
9156
+ <label for="live-activity-cap-slider">Tool lines shown while an agent works <span id="live-activity-cap-value">50 lines</span></label>
9157
+ <input
9158
+ id="live-activity-cap-slider"
9159
+ aria-describedby="appearance-browser-note"
9160
+ type="range"
9161
+ min="10"
9162
+ max="510"
9163
+ step="10"
9164
+ value="50"
9165
+ aria-label="Tool lines shown while an agent works"
9166
+ aria-valuetext="50 lines"
9167
+ oninput="previewLiveActivityCap(this.value)"
9168
+ onchange="updateLiveActivityCap(this.value)"
9169
+ >
9170
+ </div>
9171
+ <div class="settings-help settings-gap-xs">Per agent: the main agent and each sub-agent keep this many of their newest tool lines in the live bubble. The relay keeps up to 500 per agent.</div>
9172
+ <div class="settings-row settings-gap">
9173
+ <label for="stream-view-toggle">Stream view: thoughts, tool calls and the reply in order</label>
9174
+ <input
9175
+ id="stream-view-toggle"
9176
+ aria-describedby="appearance-browser-note"
9177
+ type="checkbox"
9178
+ onchange="updateStreamViewSetting(this.checked)"
9179
+ aria-label="Stream view: thoughts, tool calls and the reply in order"
9180
+ >
9181
+ </div>
9182
+ <div class="settings-help settings-gap-xs">Turns recorded before this version keep the grouped layout.</div>
9183
+ <div class="settings-row settings-gap">
9184
+ <label for="show-suspend-host-toggle">Show Suspend host action</label>
9185
+ <input
9186
+ id="show-suspend-host-toggle"
9187
+ aria-describedby="appearance-browser-note"
9188
+ type="checkbox"
9189
+ onchange="updateShowSuspendHostSetting(this.checked)"
9190
+ aria-label="Show Suspend host action"
9191
+ checked
9192
+ >
9193
+ </div>
9194
+ <div class="settings-help settings-gap-xs">Shows or hides the menu action in this browser only. Suspend works only when the relay runs on Windows; a queued suspend still shows its banner here either way.</div>
9195
+ </div>
8862
9196
  <div
8863
9197
  id="settings-panel-providers"
8864
9198
  class="settings-panel"
@@ -9036,6 +9370,37 @@
9036
9370
  </div>
9037
9371
  <div class="settings-help">Leave the key blank to keep the saved key, or enter a new one to replace it. Provider changes also update conversations that have not sent a message yet. The Base URL is useful for OpenAI-compatible endpoints or for pointing to the capture proxy (<code>node server/tools/capture-proxy.mjs</code>) to inspect requests.</div>
9038
9372
  </div>
9373
+ <!-- Image turns: the daily cap, today's spend and the presets (openai-image-cost-ui.js). -->
9374
+ <div id="openai-image-spend-section" class="settings-row settings-row-column settings-gap" data-testid="openai-image-spend-section">
9375
+ <label for="openai-image-cap-input">Daily image cap (USD)</label>
9376
+ <div class="openai-image-cap-row">
9377
+ <input
9378
+ id="openai-image-cap-input"
9379
+ data-testid="openai-image-cap-input"
9380
+ type="number"
9381
+ inputmode="decimal"
9382
+ min="0"
9383
+ max="10000"
9384
+ step="0.01"
9385
+ value="5"
9386
+ aria-label="Daily image cap in US dollars, 0 for no cap"
9387
+ aria-describedby="openai-image-spend-error openai-image-cap-help"
9388
+ >
9389
+ <button id="openai-image-cap-save-btn" data-testid="openai-image-cap-save" type="button" class="summary-btn">Save cap</button>
9390
+ </div>
9391
+ <div id="openai-image-spend-error" data-testid="openai-image-spend-error" class="settings-help openai-image-field-error" role="alert" hidden></div>
9392
+ <div id="openai-image-cap-help" class="settings-help">What image turns may cost this relay per day (relay time), estimates included. Once today's spend reaches it, new image turns are refused until tomorrow or until you allow more for today; a running turn finishes. 0 means no cap.</div>
9393
+ <div id="openai-image-spend-line" data-testid="openai-image-spend-line" class="settings-help settings-provider-status" role="status">Today's image spend is not known yet.</div>
9394
+ <div class="settings-actions">
9395
+ <button id="openai-image-allow-today-btn" data-testid="openai-image-allow-today" type="button" class="summary-btn" hidden>Allow past the cap for today</button>
9396
+ <button id="openai-image-stop-allowing-btn" data-testid="openai-image-stop-allowing" type="button" class="summary-btn" hidden>Stop allowing</button>
9397
+ </div>
9398
+ <div id="openai-image-spend-status" class="settings-help" role="status" aria-live="polite"></div>
9399
+ <label class="settings-gap-sm" id="openai-image-presets-label">Image presets</label>
9400
+ <ul id="openai-image-presets-list" data-testid="openai-image-presets-list" class="openai-image-presets-list" aria-labelledby="openai-image-presets-label"></ul>
9401
+ <div id="openai-image-presets-error" class="settings-help openai-image-field-error" role="alert" hidden></div>
9402
+ <div id="openai-image-presets-note" class="settings-help"></div>
9403
+ </div>
9039
9404
  </div>
9040
9405
  <div
9041
9406
  id="settings-provider-panel-claude"
@@ -9560,11 +9925,16 @@
9560
9925
  hidden
9561
9926
  >
9562
9927
  <div id="remote-relays-self-section" class="settings-row settings-row-column settings-gap">
9563
- <label>This relay</label>
9564
- <div class="remote-relays-self-name-row">
9565
- <span id="remote-relays-self-name" class="remote-relays-self-name">—</span>
9566
- <span class="settings-help">Change it under General → Relay name.</span>
9567
- </div>
9928
+ <label for="pwa-app-name-input">Relay name</label>
9929
+ <input
9930
+ id="pwa-app-name-input"
9931
+ type="text"
9932
+ maxlength="60"
9933
+ placeholder="OAR"
9934
+ aria-label="Relay name"
9935
+ onchange="updatePwaAppName(this.value)"
9936
+ >
9937
+ <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. A setting of this relay.</div>
9568
9938
  </div>
9569
9939
  <div class="settings-row settings-row-column settings-gap">
9570
9940
  <label for="remote-relays-public-url-input">Public address</label>
@@ -9582,6 +9952,30 @@
9582
9952
  <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>
9583
9953
  <div id="remote-relays-self-status" class="settings-help settings-provider-status" role="status" aria-live="polite" hidden></div>
9584
9954
  </div>
9955
+ <div class="settings-row settings-gap">
9956
+ <label for="restart-resume-toggle">After a relay restart, tell interrupted sessions to carry on</label>
9957
+ <input
9958
+ id="restart-resume-toggle"
9959
+ type="checkbox"
9960
+ onchange="updateRestartResumeSettingFromToggle(this.checked)"
9961
+ aria-label="After a relay restart, tell interrupted sessions to carry on"
9962
+ >
9963
+ </div>
9964
+ <div class="settings-help settings-gap-xs">Most turns a restart interrupts run again by themselves. A turn that cannot (the agent had already taken the message, or had started on its own after a background task) just stops. With this on, such a session gets one relay note once the relay is back, saying when it restarted and asking the agent to check what it was doing and carry on. Your message is never sent again; idle, archived and usage-limit-paused sessions get nothing. Off by default; a setting of this relay.</div>
9965
+ <div class="settings-row settings-gap">
9966
+ <label for="relay-restart-max-wait-select">Ask after a queued relay restart has waited</label>
9967
+ <select id="relay-restart-max-wait-select" onchange="updateRelayRestartMaxWaitSetting(this.value)" aria-label="Ask after a queued relay restart has waited">
9968
+ <option value="5">5 min</option>
9969
+ <option value="10">10 min</option>
9970
+ <option value="15">15 min</option>
9971
+ <option value="30" selected>30 min</option>
9972
+ <option value="60">1 h</option>
9973
+ <option value="120">2 h</option>
9974
+ <option value="240">4 h</option>
9975
+ <option value="0">No limit</option>
9976
+ </select>
9977
+ </div>
9978
+ <div class="settings-help settings-gap-xs">A queued relay restart waits for the running turns, and new messages wait for the restart. After this long the banner asks whether to restart now, keep waiting or cancel; it never restarts while the turns run, and still restarts as soon as they end. No limit waits until the turns end. A setting of this relay.</div>
9585
9979
  <div class="settings-row settings-gap">
9586
9980
  <label for="remote-relays-inbound-toggle">Accept prompts from other relays' agents</label>
9587
9981
  <input