@oar-sh/oar 0.9.21 → 0.9.23

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 (82) hide show
  1. package/CHANGELOG.md +206 -0
  2. package/README.md +9 -9
  3. package/bin/oar.js +28 -0
  4. package/package.json +13 -14
  5. package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +9 -1
  6. package/server/claude-worker/claude-cli-pinned-env.mjs +4 -1
  7. package/server/claude-worker/claude-sdk-adapter.mjs +149 -4
  8. package/server/claude-worker/claude-session-process.mjs +9 -4
  9. package/server/claude-worker/claude-session-worker-main.mjs +10 -1
  10. package/server/copilot-worker/copilot-sdk-session-process.mjs +1 -0
  11. package/server/copilot-worker/copilot-sdk-session-worker.mjs +10 -1
  12. package/server/cursor-worker/cursor-session-worker.mjs +9 -1
  13. package/server/db-schema.mjs +19 -0
  14. package/server/grok-worker/grok-background-tasks.mjs +1 -0
  15. package/server/grok-worker/grok-session-worker.mjs +10 -1
  16. package/server/grok-worker/grok-turn-runner.mjs +1 -0
  17. package/server/migrations/0037-relay-asks-queue-id.mjs +66 -0
  18. package/server/migrations/0038-turn-seq.mjs +92 -0
  19. package/server/public/app/activity-replay-state.mjs +160 -42
  20. package/server/public/app/api-client.js +7 -0
  21. package/server/public/app/ask-user-view.js +4 -3
  22. package/server/public/app/bootstrap.js +62 -4
  23. package/server/public/app/chat-actions-menu-state.mjs +2 -0
  24. package/server/public/app/context-usage-view.mjs +68 -0
  25. package/server/public/app/conversation-view.js +306 -49
  26. package/server/public/app/header-priority-actions.mjs +5 -1
  27. package/server/public/app/held-details.mjs +202 -0
  28. package/server/public/app/inbox-ui.js +70 -79
  29. package/server/public/app/inbox-view.mjs +35 -19
  30. package/server/public/app/journal-view.js +17 -0
  31. package/server/public/app/live-activity-cap.mjs +103 -0
  32. package/server/public/app/questions-panel-ui.js +634 -0
  33. package/server/public/app/questions-stub-view.mjs +40 -0
  34. package/server/public/app/questions-view.mjs +89 -0
  35. package/server/public/app/relay-board-view.js +12 -2
  36. package/server/public/app/relay-thoughts.mjs +15 -1
  37. package/server/public/app/session-history-ui.js +1 -0
  38. package/server/public/app/settings-modal.js +6 -0
  39. package/server/public/app/socket-handlers.js +42 -11
  40. package/server/public/app/stream-view-preference.mjs +60 -0
  41. package/server/public/app/subagent-fold.mjs +6 -3
  42. package/server/public/app/subagent-timeline-view.mjs +12 -5
  43. package/server/public/app/subagent-viewer.mjs +8 -6
  44. package/server/public/app/thoughts-preference.mjs +79 -0
  45. package/server/public/app/turn-stream-view.mjs +124 -0
  46. package/server/public/index.html +391 -214
  47. package/server/repositories/message-repository.mjs +10 -0
  48. package/server/repositories/question-repository.mjs +51 -7
  49. package/server/repositories/relay-turn-rows.mjs +56 -0
  50. package/server/routes/ask-user-routes.mjs +34 -5
  51. package/server/routes/messages-routes.mjs +46 -34
  52. package/server/routes/relay-ask-routes.mjs +15 -5
  53. package/server/routes/sessions-routes.mjs +77 -6
  54. package/server/server-runtime.mjs +199 -171
  55. package/server/services/cloudflared-tunnel-service.mjs +7 -0
  56. package/server/services/earlier-database-service.mjs +1 -1
  57. package/server/services/file-serving.mjs +13 -0
  58. package/server/services/idle-timers.mjs +184 -0
  59. package/server/services/native-addon-build-service.mjs +224 -0
  60. package/server/services/oar-cli-helpers.mjs +7 -4
  61. package/server/services/queue-wake-hook.mjs +145 -0
  62. package/server/services/queue-waker.mjs +127 -0
  63. package/server/services/relay-ask-service.mjs +64 -5
  64. package/server/services/relay-console-log.mjs +2 -3
  65. package/server/services/relay-event-service.mjs +4 -1
  66. package/server/services/remote-relay-dispatcher.mjs +23 -4
  67. package/server/services/remote-relay-loopback.mjs +3 -3
  68. package/server/services/sdk-session-sync-service.mjs +1 -1
  69. package/server/services/session-history-refresh-service.mjs +4 -0
  70. package/server/services/session-worker-supervisor-service.mjs +36 -0
  71. package/server/services/session-worker-websocket-service.mjs +143 -15
  72. package/server/services/ssh-tunnel-manager-service.mjs +7 -0
  73. package/server/services/update-install-service.mjs +32 -1
  74. package/shared/ask-user-bridge.mjs +9 -6
  75. package/shared/relay-asks.mjs +10 -0
  76. package/shared/relay-events.mjs +1 -0
  77. package/shared/worker-runtime/heartbeat.mjs +26 -0
  78. package/shared/worker-runtime/worker-keepalive.mjs +108 -0
  79. package/shared/worker-runtime/worker-websocket-link.mjs +21 -10
  80. package/server/services/package-data-rescue-preinstall.mjs +0 -22
  81. package/server/services/package-data-rescue.mjs +0 -139
  82. package/server/tty-console-bootstrap.mjs +0 -75
@@ -671,8 +671,11 @@
671
671
  .header-icon-count:empty { display: none; }
672
672
  /* The display rule above outranks the UA [hidden] rule, same trap as the
673
673
  other [hidden] overrides in this file. */
674
+ #questions-btn[hidden],
674
675
  #pinned-messages-btn[hidden],
675
676
  #hidden-messages-btn[hidden] { display: none; }
677
+ /* 📓 with an urgent question waiting: the urgent count, in red. */
678
+ .header-icon-count.is-urgent { color: var(--danger); font-weight: 600; }
676
679
  /* Same trap: without this the ︙ trigger stays drawn as a dead button. */
677
680
  #chat-actions-menu-btn[hidden] { display: none; }
678
681
  #cli-dot {
@@ -885,6 +888,7 @@
885
888
  text-align: center;
886
889
  }
887
890
  .inbox-count:empty { display: none; }
891
+ .inbox-count.is-urgent { background: var(--danger); color: #fff; }
888
892
  body.inbox-view-active #input-area { display: none; }
889
893
  body.inbox-view-active #pending-question-banner { display: none; }
890
894
  .inbox-view { display: grid; gap: calc(7px * var(--ui-scale)); padding: calc(4px * var(--ui-scale)) 0 calc(12px * var(--ui-scale)); }
@@ -941,7 +945,11 @@
941
945
  .inbox-item-header { font-weight: 600; font-size: var(--fs-meta-sm); }
942
946
  .inbox-item-question, .inbox-item-recommendation { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
943
947
  .inbox-item-recommendation { color: var(--muted); font-size: var(--fs-meta-sm); }
944
- .inbox-item-detail summary { cursor: pointer; color: var(--muted); font-size: var(--fs-meta-sm); }
948
+ /* A 28px tap row however small its text (the Inbox, 📓). */
949
+ .inbox-item-detail summary {
950
+ cursor: pointer; color: var(--muted); font-size: var(--fs-meta-sm);
951
+ min-height: calc(28px * var(--ui-scale)); line-height: calc(28px * var(--ui-scale)); width: fit-content;
952
+ }
945
953
  .inbox-item-detail-text {
946
954
  margin: calc(3px * var(--ui-scale)) 0 0;
947
955
  padding: calc(4px * var(--ui-scale)) calc(6px * var(--ui-scale));
@@ -989,8 +997,21 @@
989
997
  .inbox-recommended-tag { color: var(--accent); font-size: var(--fs-meta-xs); }
990
998
  .inbox-answer { border-color: var(--accent); color: var(--accent); }
991
999
  .inbox-dismiss { color: var(--muted); }
992
- .inbox-view button:disabled, .relay-ask-container button:disabled { opacity: 0.5; cursor: wait; }
993
- .relay-ask-container { max-width: calc(760px * var(--ui-scale)); }
1000
+ .inbox-view button:disabled { opacity: 0.5; cursor: wait; }
1001
+ /* 📓 An ask's one-line stub in the bubble of the turn that asked it (or
1002
+ standing alone at the transcript's end); a tap opens 📓 at it. */
1003
+ .msg-ask-stubs { display: grid; justify-items: start; gap: calc(2px * var(--ui-scale)); margin-top: calc(4px * var(--ui-scale)); }
1004
+ .msg-ask-stub {
1005
+ min-height: calc(28px * var(--ui-scale)); padding: calc(2px * var(--ui-scale)) calc(8px * var(--ui-scale));
1006
+ border: 1px solid var(--border); border-radius: calc(8px * var(--ui-scale)); background: var(--bg2);
1007
+ color: var(--text); font: inherit; font-size: var(--fs-meta-sm); text-align: left; cursor: pointer;
1008
+ max-width: 100%; overflow-wrap: anywhere;
1009
+ }
1010
+ .msg-ask-stub:hover { border-color: var(--accent); }
1011
+ .msg-ask-stub-urgent { color: var(--danger); font-weight: 600; }
1012
+ .msg-ask-stub.is-closed { color: var(--muted); }
1013
+ .relay-ask-stub-container { align-self: flex-start; max-width: 82%; }
1014
+ .relay-ask-stub-container > .msg-ask-stubs { margin-top: 0; }
994
1015
  /* The phone: one column, buttons the full width. */
995
1016
  @media (max-width: 680px) {
996
1017
  #inbox-btn { min-width: calc(64px * var(--ui-scale)); padding: calc(5px * var(--ui-scale)) calc(6px * var(--ui-scale)); }
@@ -1906,6 +1927,21 @@
1906
1927
  .settings-gap-md { margin-top: calc(5px * var(--ui-scale)); margin-bottom: 0; }
1907
1928
  .settings-gap-sm { margin-top: calc(4px * var(--ui-scale)); margin-bottom: 0; }
1908
1929
  .settings-gap-xs { margin-top: calc(3px * var(--ui-scale)); margin-bottom: 0; }
1930
+ /* Settings → General in headed groups: a title row per section and a thin
1931
+ rule above every section after the first. The first row under a title
1932
+ drops its own gap; the title's margin already spaces it. */
1933
+ .settings-section + .settings-section {
1934
+ border-top: 1px solid var(--border);
1935
+ margin-top: calc(10px * var(--ui-scale));
1936
+ }
1937
+ .settings-section-title {
1938
+ font-size: var(--fs-meta-md);
1939
+ font-weight: 600;
1940
+ color: var(--muted);
1941
+ margin: calc(14px * var(--ui-scale)) 0 calc(6px * var(--ui-scale));
1942
+ }
1943
+ .settings-section:first-child .settings-section-title { margin-top: 0; }
1944
+ .settings-section-title + .settings-row { margin-top: 0; }
1909
1945
  /* A row inside a column block: the block's full width; the lead row (the
1910
1946
  provider's on/off toggle) keeps a small gap to the fields below. */
1911
1947
  .settings-row-full { width: 100%; }
@@ -2845,15 +2881,25 @@
2845
2881
  opacity: 0.92;
2846
2882
  font-size: var(--fs-meta-md);
2847
2883
  }
2884
+ /* The browser's own disclosure marker, the glyph of the Thoughts panel
2885
+ above the rows. */
2848
2886
  .thinking-thought > summary {
2849
2887
  cursor: pointer;
2850
- list-style: none;
2888
+ display: list-item;
2889
+ list-style: disclosure-closed inside;
2851
2890
  white-space: normal;
2852
2891
  overflow-wrap: anywhere;
2853
2892
  word-break: break-word;
2854
2893
  }
2855
- .thinking-thought > summary::-webkit-details-marker { display: none; }
2856
- .thinking-thought[open] > summary { display: none; }
2894
+ /* Open, the summary stays as a one-line fold header so the reader can fold
2895
+ the thought by hand ("Expand thoughts" opens them). */
2896
+ .thinking-thought[open] > summary {
2897
+ white-space: nowrap;
2898
+ overflow: hidden;
2899
+ text-overflow: ellipsis;
2900
+ opacity: 0.75;
2901
+ list-style-type: disclosure-open;
2902
+ }
2857
2903
  .thinking-thought-body {
2858
2904
  margin: calc(4px * var(--ui-scale)) 0 calc(3px * var(--ui-scale)) calc(11px * var(--ui-scale));
2859
2905
  font-style: normal;
@@ -2887,6 +2933,36 @@
2887
2933
  gap: calc(3px * var(--ui-scale));
2888
2934
  min-width: 0;
2889
2935
  }
2936
+ /* Stream view (turn-stream-view.mjs): a finished turn's thoughts, tool
2937
+ lines and reply as one list in the order they happened; in the live
2938
+ bubble, #thinking-stream-list holds the thoughts and tool lines. */
2939
+ .msg-turn-stream,
2940
+ .thinking-stream-list {
2941
+ display: flex;
2942
+ flex-direction: column;
2943
+ gap: calc(4px * var(--ui-scale));
2944
+ min-width: 0;
2945
+ }
2946
+ .thinking-stream-list { margin-bottom: calc(4px * var(--ui-scale)); font-size: var(--fs-meta-md); color: var(--muted); }
2947
+ .thinking-stream-list:empty { display: none; }
2948
+ .msg-stream-thought { min-width: 0; color: var(--muted); font-size: var(--fs-meta-sm); }
2949
+ .msg-stream-thought > summary {
2950
+ cursor: pointer;
2951
+ font-style: italic;
2952
+ white-space: nowrap;
2953
+ overflow: hidden;
2954
+ text-overflow: ellipsis;
2955
+ }
2956
+ .msg-stream-thought-body {
2957
+ margin: calc(3px * var(--ui-scale)) 0 calc(2px * var(--ui-scale)) calc(11px * var(--ui-scale));
2958
+ overflow-wrap: anywhere;
2959
+ word-break: break-word;
2960
+ }
2961
+ .msg-stream-thought-body > :first-child { margin-top: 0; }
2962
+ .msg-stream-thought-body > :last-child { margin-bottom: 0; }
2963
+ .msg-stream-omitted { color: var(--muted); font-size: var(--fs-meta-sm); font-style: italic; }
2964
+ .msg-stream-reply { min-width: 0; margin-top: calc(2px * var(--ui-scale)); }
2965
+ .msg-stream-reply > :first-child { margin-top: 0; }
2890
2966
  .msg-thought-item {
2891
2967
  color: var(--muted);
2892
2968
  font-size: var(--fs-meta-sm);
@@ -3154,7 +3230,9 @@
3154
3230
  font-size: 0.85rem;
3155
3231
  }
3156
3232
  .ctx-thinking-group { display: inline-flex; gap: 0; }
3157
- .ctx-thinking-btn {
3233
+ /* The 💭 Thoughts row (a setting of this browser) has the same look. */
3234
+ .ctx-thinking-btn,
3235
+ .ctx-thoughts-btn {
3158
3236
  font-size: 0.78rem;
3159
3237
  padding: calc(4px * var(--ui-scale)) calc(6px * var(--ui-scale));
3160
3238
  border: 1px solid var(--border);
@@ -3162,10 +3240,14 @@
3162
3240
  color: var(--muted);
3163
3241
  cursor: pointer;
3164
3242
  }
3165
- .ctx-thinking-btn:first-child { border-radius: calc(6px * var(--ui-scale)) 0 0 calc(6px * var(--ui-scale)); }
3166
- .ctx-thinking-btn:last-child { border-radius: 0 calc(6px * var(--ui-scale)) calc(6px * var(--ui-scale)) 0; }
3167
- .ctx-thinking-btn + .ctx-thinking-btn { border-left: none; }
3168
- .ctx-thinking-btn.is-active {
3243
+ .ctx-thinking-btn:first-child,
3244
+ .ctx-thoughts-btn:first-child { border-radius: calc(6px * var(--ui-scale)) 0 0 calc(6px * var(--ui-scale)); }
3245
+ .ctx-thinking-btn:last-child,
3246
+ .ctx-thoughts-btn:last-child { border-radius: 0 calc(6px * var(--ui-scale)) calc(6px * var(--ui-scale)) 0; }
3247
+ .ctx-thinking-btn + .ctx-thinking-btn,
3248
+ .ctx-thoughts-btn + .ctx-thoughts-btn { border-left: none; }
3249
+ .ctx-thinking-btn.is-active,
3250
+ .ctx-thoughts-btn.is-active {
3169
3251
  background: var(--accent);
3170
3252
  color: #fff;
3171
3253
  border-color: var(--accent);
@@ -4811,6 +4893,47 @@
4811
4893
  }
4812
4894
  #chat-title-relay-schedules[hidden] { display: none; }
4813
4895
  .relay-watch-panel[hidden] { display: none; }
4896
+ /* 📓 Questions: the conversation's asks, dropped down under 📓 (or ︙),
4897
+ placed by questions-panel-ui.js; on a phone a sheet under the header. */
4898
+ .questions-panel {
4899
+ position: fixed; top: 0; left: 0; z-index: 60; overflow-y: auto; overscroll-behavior: contain; box-sizing: border-box;
4900
+ width: min(calc(420px * var(--ui-scale)), calc(100vw - calc(16px * var(--ui-scale))));
4901
+ background: var(--bg2); border: 1px solid var(--border); border-radius: calc(8px * var(--ui-scale));
4902
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.25); padding: calc(4px * var(--ui-scale)) calc(6px * var(--ui-scale)) calc(6px * var(--ui-scale));
4903
+ color: var(--text);
4904
+ }
4905
+ .questions-panel[hidden] { display: none; }
4906
+ .questions-panel-head {
4907
+ display: flex; align-items: center; justify-content: space-between; gap: calc(6px * var(--ui-scale));
4908
+ font-size: var(--fs-meta-md); font-weight: 600; padding: 0 0 calc(4px * var(--ui-scale));
4909
+ }
4910
+ .questions-panel-close {
4911
+ min-width: calc(28px * var(--ui-scale)); min-height: calc(28px * var(--ui-scale));
4912
+ font: inherit; color: var(--muted); background: transparent; border: 1px solid var(--border);
4913
+ border-radius: calc(6px * var(--ui-scale)); cursor: pointer;
4914
+ }
4915
+ .questions-panel-body { display: grid; gap: calc(6px * var(--ui-scale)); }
4916
+ .questions-panel-empty { margin: 0; color: var(--muted); font-size: var(--fs-meta-sm); }
4917
+ .questions-panel .inbox-item { background: var(--bg); }
4918
+ .questions-panel .inbox-item.questions-card-focus { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
4919
+ .questions-panel button:disabled { opacity: 0.5; cursor: wait; }
4920
+ #questions-panel-backdrop { display: none; }
4921
+ /* A phone, upright or sideways: full width under the header over a dimmed
4922
+ page; every button of a card half the width, one per row, left aligned,
4923
+ so the right half stays free to scroll. */
4924
+ .questions-panel.is-sheet {
4925
+ left: calc(8px * var(--ui-scale)); right: calc(8px * var(--ui-scale)); width: auto; z-index: 1200;
4926
+ }
4927
+ #questions-panel-backdrop:not([hidden]) {
4928
+ display: block; position: fixed; inset: 0; z-index: 1190; background: rgba(0, 0, 0, 0.42);
4929
+ }
4930
+ .questions-panel.is-sheet .inbox-item-options,
4931
+ .questions-panel.is-sheet .inbox-item-actions { flex-direction: column; align-items: flex-start; }
4932
+ .questions-panel.is-sheet .inbox-choice,
4933
+ .questions-panel.is-sheet .inbox-answer,
4934
+ .questions-panel.is-sheet .inbox-dismiss {
4935
+ width: 50%; min-width: calc(140px * var(--ui-scale)); justify-content: flex-start;
4936
+ }
4814
4937
  .relay-watch-chip {
4815
4938
  font: inherit; color: var(--accent); background: transparent; cursor: pointer;
4816
4939
  border: 1px solid var(--border); border-radius: calc(10px * var(--ui-scale)); padding: calc(2px * var(--ui-scale)) calc(5px * var(--ui-scale));
@@ -8070,6 +8193,7 @@
8070
8193
  </div>
8071
8194
  <button id="pending-question-banner" onclick="openPendingQuestionFromBanner()" title="Open pending question"></button>
8072
8195
  <div id="chat-header-actions">
8196
+ <button id="questions-btn" class="header-icon-btn has-count" type="button" title="Questions" aria-label="Questions" aria-haspopup="dialog" aria-expanded="false" aria-controls="questions-panel" hidden>📓<span class="header-icon-count"></span></button>
8073
8197
  <button id="pinned-messages-btn" class="header-icon-btn has-count" type="button" title="Pinned messages" aria-label="Pinned messages" hidden>📍<span class="header-icon-count"></span></button>
8074
8198
  <button id="hidden-messages-btn" class="header-icon-btn has-count" type="button" title="Hidden from shared viewers" aria-label="Hidden from shared viewers" hidden>🙈<span class="header-icon-count"></span></button>
8075
8199
  <button id="message-search-btn" class="header-icon-btn" onclick="openMessageSearchModal()" title="Search messages" aria-label="Search messages">🔍</button>
@@ -8086,6 +8210,7 @@
8086
8210
  <button id="chat-menu-schedules" class="chat-actions-menu-item chat-menu-meta-chip" type="button" role="menuitem" hidden>⏰ Schedules…</button>
8087
8211
  <button id="chat-menu-origin" class="chat-actions-menu-item chat-menu-meta-chip" type="button" role="menuitem" hidden>↗ Started by an agent</button>
8088
8212
  <button id="chat-menu-edit-title" class="chat-actions-menu-item" type="button" role="menuitem">✍️ Edit conversation title</button>
8213
+ <button id="chat-menu-questions" class="chat-actions-menu-item chat-menu-twin" type="button" role="menuitem" hidden>📓 Questions</button>
8089
8214
  <button id="chat-menu-pins" class="chat-actions-menu-item chat-menu-twin" type="button" role="menuitem" hidden>📍 Pinned messages</button>
8090
8215
  <button id="chat-menu-hidden" class="chat-actions-menu-item chat-menu-twin" type="button" role="menuitem" hidden>🙈 Hidden from shared viewers</button>
8091
8216
  <button id="chat-menu-search" class="chat-actions-menu-item chat-menu-twin" type="button" role="menuitem" hidden>🔍 Search messages</button>
@@ -8387,215 +8512,267 @@
8387
8512
  aria-labelledby="settings-tab-general"
8388
8513
  data-settings-panel="general"
8389
8514
  >
8390
- <div class="settings-row">
8391
- <label for="theme-select">Theme</label>
8392
- <select id="theme-select" onchange="updateTheme(this.value)">
8393
- <option value="dark">Dark</option>
8394
- <option value="light">Light</option>
8395
- </select>
8396
- </div>
8397
- <div class="settings-row" style="margin-bottom: 0;">
8398
- <label for="font-scale-select">Text scaling</label>
8399
- <select id="font-scale-select" onchange="updateFontScaleFromSelect(this.value)" aria-label="Text scaling"></select>
8400
- </div>
8401
- <div class="settings-row settings-gap">
8402
- <label for="show-suspend-host-toggle">Show Suspend host action</label>
8403
- <input
8404
- id="show-suspend-host-toggle"
8405
- type="checkbox"
8406
- onchange="updateShowSuspendHostSetting(this.checked)"
8407
- aria-label="Show Suspend host action"
8408
- checked
8409
- >
8410
- </div>
8411
- <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>
8412
- <div class="settings-row settings-gap">
8413
- <label for="scroll-on-send-toggle">Jump to the end when I send a message</label>
8414
- <input
8415
- id="scroll-on-send-toggle"
8416
- type="checkbox"
8417
- onchange="updateScrollOnSendSetting(this.checked)"
8418
- aria-label="Jump to the end when I send a message"
8419
- checked
8420
- >
8421
- </div>
8422
- <div class="settings-help settings-gap-xs">A conversation only scrolls by itself while you are at its end. Anywhere else it stays where you are reading, and the round button above the composer shows that something new or a question is waiting below. Your own message is the exception: sending one takes you to the end. Switch this off to stay put then too. Saved in this browser.</div>
8423
- <div class="settings-row settings-row-column settings-gap">
8424
- <label for="conversation-window-retention-slider">Messages kept per conversation for switching back <span id="conversation-window-retention-value">200 messages</span></label>
8425
- <input
8426
- id="conversation-window-retention-slider"
8427
- type="range"
8428
- min="100"
8429
- max="1100"
8430
- step="100"
8431
- value="200"
8432
- aria-label="Messages kept per conversation for switching back"
8433
- aria-valuetext="200 messages"
8434
- oninput="previewConversationWindowRetention(this.value)"
8435
- onchange="updateConversationWindowRetention(this.value)"
8436
- >
8437
- </div>
8438
- <div class="settings-help settings-gap-xs">A conversation you switch away from and back to opens where you were reading, with the older messages you had loaded. This many are kept around that place for each of the last eight conversations; No limit keeps them all, at the cost of memory. Saved in this browser.</div>
8439
- <div class="settings-row settings-row-column settings-gap">
8440
- <label for="turn-ceiling-slider">Max turn duration <span id="turn-ceiling-value">No limit</span></label>
8441
- <input
8442
- id="turn-ceiling-slider"
8443
- type="range"
8444
- min="0"
8445
- max="600"
8446
- step="5"
8447
- value="60"
8448
- aria-label="Maximum turn duration in minutes"
8449
- oninput="previewTurnCeilingSetting(this.value)"
8450
- onchange="updateTurnCeilingSetting(this.value)"
8451
- >
8452
- </div>
8453
- <div class="settings-help settings-gap-xs">A hard cap on how long one turn may run before the relay requeues it, counted from the start of the turn or from your last answer to one of its questions. Turns are already kept alive by worker heartbeats, and the relay never cuts off a turn waiting on your answer to a question — so this only catches a worker that has hung. Grok also enforces the limit itself, counted from the start of the prompt and without pausing for questions (remote relay approvals). Set to 0 for no limit.</div>
8454
- <div class="settings-row settings-row-column settings-gap">
8455
- <label for="background-task-timeout-slider">Background task timeout <span id="background-task-timeout-value">4 h</span></label>
8456
- <input
8457
- id="background-task-timeout-slider"
8458
- type="range"
8459
- min="0"
8460
- max="600"
8461
- step="5"
8462
- value="240"
8463
- aria-label="Background task timeout in minutes"
8464
- oninput="previewBackgroundTaskTimeoutSetting(this.value)"
8465
- onchange="updateBackgroundTaskTimeoutSetting(this.value)"
8466
- >
8467
- </div>
8468
- <div class="settings-help settings-gap-xs">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>
8469
- <div class="settings-row settings-row-column settings-gap">
8470
- <label for="task-stall-minutes-slider">Mark a background task as quiet after <span id="task-stall-minutes-value">10 min</span></label>
8471
- <input
8472
- id="task-stall-minutes-slider"
8473
- type="range"
8474
- min="1"
8475
- max="120"
8476
- step="1"
8477
- value="10"
8478
- aria-label="Minutes without output before a background task is marked as quiet"
8479
- oninput="previewTaskStallMinutesSetting(this.value)"
8480
- onchange="updateTaskStallMinutesSetting(this.value)"
8481
- >
8482
- </div>
8483
- <div class="settings-help settings-gap-xs">A shell or Monitor task that has written no output for this long gets an amber "no output for N min" note in the composer panel and its console.</div>
8484
- <div class="settings-row settings-gap">
8485
- <label for="restart-resume-toggle">After a relay restart, tell interrupted sessions to carry on</label>
8486
- <input
8487
- id="restart-resume-toggle"
8488
- type="checkbox"
8489
- onchange="updateRestartResumeSettingFromToggle(this.checked)"
8490
- aria-label="After a relay restart, tell interrupted sessions to carry on"
8491
- >
8492
- </div>
8493
- <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>
8494
- <div class="settings-row settings-gap">
8495
- <label for="relay-restart-max-wait-select">Ask after a queued relay restart has waited</label>
8496
- <select id="relay-restart-max-wait-select" onchange="updateRelayRestartMaxWaitSetting(this.value)" aria-label="Ask after a queued relay restart has waited">
8497
- <option value="5">5 min</option>
8498
- <option value="10">10 min</option>
8499
- <option value="15">15 min</option>
8500
- <option value="30" selected>30 min</option>
8501
- <option value="60">1 h</option>
8502
- <option value="120">2 h</option>
8503
- <option value="240">4 h</option>
8504
- <option value="0">No limit</option>
8505
- </select>
8506
- </div>
8507
- <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.</div>
8508
- <div class="settings-row settings-gap">
8509
- <label for="subagent-record-retention-select">Keep sub-agent tool output</label>
8510
- <select id="subagent-record-retention-select" onchange="updateSubagentRecordRetentionSetting(this.value)" aria-label="Keep sub-agent tool output">
8511
- <option value="7">7 days</option>
8512
- <option value="14">14 days</option>
8513
- <option value="30">30 days</option>
8514
- <option value="90" selected>90 days</option>
8515
- <option value="0">Always</option>
8516
- </select>
8517
- </div>
8518
- <div class="settings-help settings-gap-xs">A sub-agent's record keeps every tool call's full input and the first 4 KB of each result. After this many days the relay removes them from finished agents, once a day; the thoughts, the reply, notes and the one-line summary of each call stay. Always keeps everything.</div>
8519
- <div id="windows-autostart-setting" hidden>
8515
+ <section class="settings-section" id="settings-section-appearance" aria-labelledby="settings-section-appearance-title">
8516
+ <h3 class="settings-section-title" id="settings-section-appearance-title">Appearance</h3>
8517
+ <div class="settings-row">
8518
+ <label for="theme-select">Theme</label>
8519
+ <select id="theme-select" onchange="updateTheme(this.value)">
8520
+ <option value="dark">Dark</option>
8521
+ <option value="light">Light</option>
8522
+ </select>
8523
+ </div>
8524
+ <div class="settings-row" style="margin-bottom: 0;">
8525
+ <label for="font-scale-select">Text scaling</label>
8526
+ <select id="font-scale-select" onchange="updateFontScaleFromSelect(this.value)" aria-label="Text scaling"></select>
8527
+ </div>
8528
+ <div class="settings-row settings-gap">
8529
+ <label for="thoughts-expanded-toggle">Expand thoughts while an agent works</label>
8530
+ <input
8531
+ id="thoughts-expanded-toggle"
8532
+ type="checkbox"
8533
+ onchange="updateThoughtsExpandedSetting(this.checked)"
8534
+ aria-label="Expand thoughts while an agent works"
8535
+ checked
8536
+ >
8537
+ </div>
8538
+ <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>
8520
8539
  <div class="settings-row settings-row-column settings-gap">
8521
- <label>Autostart (Windows)</label>
8522
- <div role="radiogroup" aria-label="Windows autostart mode" class="settings-radio-stack">
8523
- <label style="font-weight: normal;">
8524
- <input type="radio" name="windows-autostart-mode" value="off" onchange="updateWindowsAutostartMode('off')"> Off
8525
- </label>
8526
- <label style="font-weight: normal;">
8527
- <input type="radio" name="windows-autostart-mode" value="signin" onchange="updateWindowsAutostartMode('signin')"> At sign-in — visible terminal after you log on
8528
- </label>
8529
- <label style="font-weight: normal;">
8530
- <input type="radio" name="windows-autostart-mode" value="boot" onchange="updateWindowsAutostartMode('boot')"> At system startup — headless, no logon needed
8531
- </label>
8540
+ <label for="live-activity-cap-slider">Tool lines shown while an agent works <span id="live-activity-cap-value">50 lines</span></label>
8541
+ <input
8542
+ id="live-activity-cap-slider"
8543
+ type="range"
8544
+ min="10"
8545
+ max="510"
8546
+ step="10"
8547
+ value="50"
8548
+ aria-label="Tool lines shown while an agent works"
8549
+ aria-valuetext="50 lines"
8550
+ oninput="previewLiveActivityCap(this.value)"
8551
+ onchange="updateLiveActivityCap(this.value)"
8552
+ >
8553
+ </div>
8554
+ <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>
8555
+ <div class="settings-row settings-gap">
8556
+ <label for="stream-view-toggle">Stream view: thoughts, tool calls and the reply in order</label>
8557
+ <input
8558
+ id="stream-view-toggle"
8559
+ type="checkbox"
8560
+ onchange="updateStreamViewSetting(this.checked)"
8561
+ aria-label="Stream view: thoughts, tool calls and the reply in order"
8562
+ >
8563
+ </div>
8564
+ <div class="settings-help settings-gap-xs">Turns recorded before this version keep the grouped layout. Saved in this browser.</div>
8565
+ <div class="settings-row settings-gap">
8566
+ <label for="show-suspend-host-toggle">Show Suspend host action</label>
8567
+ <input
8568
+ id="show-suspend-host-toggle"
8569
+ type="checkbox"
8570
+ onchange="updateShowSuspendHostSetting(this.checked)"
8571
+ aria-label="Show Suspend host action"
8572
+ checked
8573
+ >
8574
+ </div>
8575
+ <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>
8576
+ </section>
8577
+ <section class="settings-section" id="settings-section-reading" aria-labelledby="settings-section-reading-title">
8578
+ <h3 class="settings-section-title" id="settings-section-reading-title">Reading</h3>
8579
+ <div class="settings-row settings-gap">
8580
+ <label for="scroll-on-send-toggle">Jump to the end when I send a message</label>
8581
+ <input
8582
+ id="scroll-on-send-toggle"
8583
+ type="checkbox"
8584
+ onchange="updateScrollOnSendSetting(this.checked)"
8585
+ aria-label="Jump to the end when I send a message"
8586
+ checked
8587
+ >
8588
+ </div>
8589
+ <div class="settings-help settings-gap-xs">A conversation only scrolls by itself while you are at its end. Anywhere else it stays where you are reading, and the round button above the composer shows that something new or a question is waiting below. Your own message is the exception: sending one takes you to the end. Switch this off to stay put then too. Saved in this browser.</div>
8590
+ <div class="settings-row settings-row-column settings-gap">
8591
+ <label for="conversation-window-retention-slider">Messages kept per conversation for switching back <span id="conversation-window-retention-value">200 messages</span></label>
8592
+ <input
8593
+ id="conversation-window-retention-slider"
8594
+ type="range"
8595
+ min="100"
8596
+ max="1100"
8597
+ step="100"
8598
+ value="200"
8599
+ aria-label="Messages kept per conversation for switching back"
8600
+ aria-valuetext="200 messages"
8601
+ oninput="previewConversationWindowRetention(this.value)"
8602
+ onchange="updateConversationWindowRetention(this.value)"
8603
+ >
8604
+ </div>
8605
+ <div class="settings-help settings-gap-xs">A conversation you switch away from and back to opens where you were reading, with the older messages you had loaded. This many are kept around that place for each of the last eight conversations; No limit keeps them all, at the cost of memory. Saved in this browser.</div>
8606
+ </section>
8607
+ <section class="settings-section" id="settings-section-turns" aria-labelledby="settings-section-turns-title">
8608
+ <h3 class="settings-section-title" id="settings-section-turns-title">Turns and tasks</h3>
8609
+ <div class="settings-row settings-row-column settings-gap">
8610
+ <label for="turn-ceiling-slider">Max turn duration <span id="turn-ceiling-value">No limit</span></label>
8611
+ <input
8612
+ id="turn-ceiling-slider"
8613
+ type="range"
8614
+ min="0"
8615
+ max="600"
8616
+ step="5"
8617
+ value="60"
8618
+ aria-label="Maximum turn duration in minutes"
8619
+ oninput="previewTurnCeilingSetting(this.value)"
8620
+ onchange="updateTurnCeilingSetting(this.value)"
8621
+ >
8622
+ </div>
8623
+ <div class="settings-help settings-gap-xs">A hard cap on how long one turn may run before the relay requeues it, counted from the start of the turn or from your last answer to one of its questions. Turns are already kept alive by worker heartbeats, and the relay never cuts off a turn waiting on your answer to a question — so this only catches a worker that has hung. Grok also enforces the limit itself, counted from the start of the prompt and without pausing for questions (remote relay approvals). Set to 0 for no limit. A setting of this relay.</div>
8624
+ <div class="settings-row settings-row-column settings-gap">
8625
+ <label for="background-task-timeout-slider">Background task timeout <span id="background-task-timeout-value">4 h</span></label>
8626
+ <input
8627
+ id="background-task-timeout-slider"
8628
+ type="range"
8629
+ min="0"
8630
+ max="600"
8631
+ step="5"
8632
+ value="240"
8633
+ aria-label="Background task timeout in minutes"
8634
+ oninput="previewBackgroundTaskTimeoutSetting(this.value)"
8635
+ onchange="updateBackgroundTaskTimeoutSetting(this.value)"
8636
+ >
8637
+ </div>
8638
+ <div class="settings-help settings-gap-xs">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. A setting of this relay.</div>
8639
+ <div class="settings-row settings-row-column settings-gap">
8640
+ <label for="task-stall-minutes-slider">Mark a background task as quiet after <span id="task-stall-minutes-value">10 min</span></label>
8641
+ <input
8642
+ id="task-stall-minutes-slider"
8643
+ type="range"
8644
+ min="1"
8645
+ max="120"
8646
+ step="1"
8647
+ value="10"
8648
+ aria-label="Minutes without output before a background task is marked as quiet"
8649
+ oninput="previewTaskStallMinutesSetting(this.value)"
8650
+ onchange="updateTaskStallMinutesSetting(this.value)"
8651
+ >
8652
+ </div>
8653
+ <div class="settings-help settings-gap-xs">A shell or Monitor task that has written no output for this long gets an amber "no output for N min" note in the composer panel and its console. A setting of this relay.</div>
8654
+ <div class="settings-row settings-gap">
8655
+ <label for="subagent-record-retention-select">Keep sub-agent tool output</label>
8656
+ <select id="subagent-record-retention-select" onchange="updateSubagentRecordRetentionSetting(this.value)" aria-label="Keep sub-agent tool output">
8657
+ <option value="7">7 days</option>
8658
+ <option value="14">14 days</option>
8659
+ <option value="30">30 days</option>
8660
+ <option value="90" selected>90 days</option>
8661
+ <option value="0">Always</option>
8662
+ </select>
8663
+ </div>
8664
+ <div class="settings-help settings-gap-xs">A sub-agent's record keeps every tool call's full input and the first 4 KB of each result. After this many days the relay removes them from finished agents, once a day; the thoughts, the reply, notes and the one-line summary of each call stay. Always keeps everything. A setting of this relay.</div>
8665
+ </section>
8666
+ <section class="settings-section" id="settings-section-relay" aria-labelledby="settings-section-relay-title">
8667
+ <h3 class="settings-section-title" id="settings-section-relay-title">This relay</h3>
8668
+ <div class="settings-row settings-row-column settings-gap">
8669
+ <label for="pwa-app-name-input">Relay name</label>
8670
+ <input
8671
+ id="pwa-app-name-input"
8672
+ type="text"
8673
+ maxlength="60"
8674
+ placeholder="OAR"
8675
+ aria-label="Relay name"
8676
+ onchange="updatePwaAppName(this.value)"
8677
+ >
8678
+ <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>
8679
+ </div>
8680
+ <div class="settings-row settings-gap">
8681
+ <label for="restart-resume-toggle">After a relay restart, tell interrupted sessions to carry on</label>
8682
+ <input
8683
+ id="restart-resume-toggle"
8684
+ type="checkbox"
8685
+ onchange="updateRestartResumeSettingFromToggle(this.checked)"
8686
+ aria-label="After a relay restart, tell interrupted sessions to carry on"
8687
+ >
8688
+ </div>
8689
+ <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>
8690
+ <div class="settings-row settings-gap">
8691
+ <label for="relay-restart-max-wait-select">Ask after a queued relay restart has waited</label>
8692
+ <select id="relay-restart-max-wait-select" onchange="updateRelayRestartMaxWaitSetting(this.value)" aria-label="Ask after a queued relay restart has waited">
8693
+ <option value="5">5 min</option>
8694
+ <option value="10">10 min</option>
8695
+ <option value="15">15 min</option>
8696
+ <option value="30" selected>30 min</option>
8697
+ <option value="60">1 h</option>
8698
+ <option value="120">2 h</option>
8699
+ <option value="240">4 h</option>
8700
+ <option value="0">No limit</option>
8701
+ </select>
8702
+ </div>
8703
+ <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>
8704
+ <div class="settings-row settings-row-column settings-gap">
8705
+ <label for="default-session-workspace-root-input">Default CWD for new sessions</label>
8706
+ <input
8707
+ id="default-session-workspace-root-input"
8708
+ type="text"
8709
+ placeholder="Leave empty to use relay workspace root"
8710
+ aria-label="Default CWD for new sessions"
8711
+ onchange="updateDefaultSessionWorkspaceRootSetting(this.value)"
8712
+ >
8713
+ <div class="settings-help">Applies to newly created sessions. Leave blank to fallback to the relay workspace root. A setting of this relay.</div>
8714
+ </div>
8715
+ <div id="windows-autostart-setting" hidden>
8716
+ <div class="settings-row settings-row-column settings-gap">
8717
+ <label>Autostart (Windows)</label>
8718
+ <div role="radiogroup" aria-label="Windows autostart mode" class="settings-radio-stack">
8719
+ <label style="font-weight: normal;">
8720
+ <input type="radio" name="windows-autostart-mode" value="off" onchange="updateWindowsAutostartMode('off')"> Off
8721
+ </label>
8722
+ <label style="font-weight: normal;">
8723
+ <input type="radio" name="windows-autostart-mode" value="signin" onchange="updateWindowsAutostartMode('signin')"> At sign-in — visible terminal after you log on
8724
+ </label>
8725
+ <label style="font-weight: normal;">
8726
+ <input type="radio" name="windows-autostart-mode" value="boot" onchange="updateWindowsAutostartMode('boot')"> At system startup — headless, no logon needed
8727
+ </label>
8728
+ </div>
8729
+ </div>
8730
+ <div class="settings-help settings-gap-xs">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>
8731
+ <div id="windows-autostart-status" class="settings-help settings-gap-xs" hidden></div>
8732
+ <div id="windows-autostart-manual" class="settings-help settings-gap-xs" hidden>
8733
+ Run this from an elevated PowerShell instead:
8734
+ <code id="windows-autostart-manual-command" style="display: block; margin-top: calc(4px * var(--ui-scale)); user-select: all;"></code>
8532
8735
  </div>
8533
8736
  </div>
8534
- <div class="settings-help settings-gap-xs">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>
8535
- <div id="windows-autostart-status" class="settings-help settings-gap-xs" hidden></div>
8536
- <div id="windows-autostart-manual" class="settings-help settings-gap-xs" hidden>
8537
- Run this from an elevated PowerShell instead:
8538
- <code id="windows-autostart-manual-command" style="display: block; margin-top: calc(4px * var(--ui-scale)); user-select: all;"></code>
8737
+ </section>
8738
+ <section class="settings-section" id="settings-section-updates" aria-labelledby="settings-section-updates-title">
8739
+ <h3 class="settings-section-title" id="settings-section-updates-title">Updates</h3>
8740
+ <div class="settings-row settings-gap">
8741
+ <label for="update-auto-check-toggle">Check for updates automatically</label>
8742
+ <input
8743
+ id="update-auto-check-toggle"
8744
+ type="checkbox"
8745
+ onchange="updateAutoCheckSetting(this.checked)"
8746
+ aria-label="Check for updates automatically"
8747
+ >
8539
8748
  </div>
8540
- </div>
8541
- <div class="settings-row settings-row-column settings-gap">
8542
- <label for="pwa-app-name-input">Relay name</label>
8543
- <input
8544
- id="pwa-app-name-input"
8545
- type="text"
8546
- maxlength="60"
8547
- placeholder="OAR"
8548
- aria-label="Relay name"
8549
- onchange="updatePwaAppName(this.value)"
8550
- >
8551
- <div class="settings-help">Shown to other relays and used as the installed app's name. On Android, a rename reaches an already-installed app after a short delay, with one confirmation prompt showing the new name.</div>
8552
- </div>
8553
- <div class="settings-row settings-gap">
8554
- <label for="update-auto-check-toggle">Check for updates automatically</label>
8555
- <input
8556
- id="update-auto-check-toggle"
8557
- type="checkbox"
8558
- onchange="updateAutoCheckSetting(this.checked)"
8559
- aria-label="Check for updates automatically"
8560
- >
8561
- </div>
8562
- <div class="settings-help settings-gap-xs">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>
8563
- <div id="update-check-killed-help" class="settings-help settings-gap-xs" hidden>Update checks are disabled by OAR_NO_UPDATE_CHECK in this relay's environment.</div>
8564
- <div class="settings-row settings-gap-xs">
8565
- <button id="update-check-now-btn" class="chat-title-action-btn" type="button" onclick="checkForUpdatesNow()">Check for updates</button>
8566
- <span id="update-last-checked" class="settings-help"></span>
8567
- </div>
8568
- <div id="update-available-card" class="settings-help settings-gap-xs" hidden>
8569
- <span id="update-available-text"></span>
8570
- <a id="update-notes-link" target="_blank" rel="noopener" hidden>release notes</a>
8749
+ <div class="settings-help settings-gap-xs">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. A setting of this relay.</div>
8750
+ <div id="update-check-killed-help" class="settings-help settings-gap-xs" hidden>Update checks are disabled by OAR_NO_UPDATE_CHECK in this relay's environment.</div>
8571
8751
  <div class="settings-row settings-gap-xs">
8572
- <button id="update-apply-btn" class="chat-title-action-btn" type="button" onclick="applyAvailableUpdate()">Update</button>
8573
- <button id="update-dismiss-btn" class="chat-title-action-btn" type="button" onclick="dismissAvailableUpdate()">Dismiss</button>
8752
+ <button id="update-check-now-btn" class="chat-title-action-btn" type="button" onclick="checkForUpdatesNow()">Check for updates</button>
8753
+ <span id="update-last-checked" class="settings-help"></span>
8574
8754
  </div>
8575
- <div id="update-git-hint" class="settings-gap-sm" hidden>This relay runs from a git checkout — pull and restart to update.</div>
8576
- </div>
8577
- <div id="update-install-status" class="settings-help settings-gap-xs" hidden></div>
8578
- <button id="update-cancel-btn" class="chat-title-action-btn settings-gap-sm" type="button" onclick="cancelRunningUpdate()" hidden>Cancel update</button>
8579
- <details id="update-install-log" class="cli-install-log" hidden>
8580
- <summary>Update log</summary>
8581
- <pre id="update-install-log-pre"></pre>
8582
- </details>
8583
- <div id="update-outcome" class="settings-help settings-gap-xs" hidden>
8584
- <span id="update-outcome-text"></span>
8585
- <pre id="update-outcome-log" hidden></pre>
8586
- <button id="update-outcome-dismiss-btn" class="chat-title-action-btn" type="button" onclick="dismissUpdateOutcome()">Dismiss</button>
8587
- </div>
8588
- <div class="settings-row settings-row-column settings-gap">
8589
- <label for="default-session-workspace-root-input">Default CWD for new sessions</label>
8590
- <input
8591
- id="default-session-workspace-root-input"
8592
- type="text"
8593
- placeholder="Leave empty to use relay workspace root"
8594
- aria-label="Default CWD for new sessions"
8595
- onchange="updateDefaultSessionWorkspaceRootSetting(this.value)"
8596
- >
8597
- <div class="settings-help">Applies to newly created sessions. Leave blank to fallback to the relay workspace root.</div>
8598
- </div>
8755
+ <div id="update-available-card" class="settings-help settings-gap-xs" hidden>
8756
+ <span id="update-available-text"></span>
8757
+ <a id="update-notes-link" target="_blank" rel="noopener" hidden>release notes</a>
8758
+ <div class="settings-row settings-gap-xs">
8759
+ <button id="update-apply-btn" class="chat-title-action-btn" type="button" onclick="applyAvailableUpdate()">Update</button>
8760
+ <button id="update-dismiss-btn" class="chat-title-action-btn" type="button" onclick="dismissAvailableUpdate()">Dismiss</button>
8761
+ </div>
8762
+ <div id="update-git-hint" class="settings-gap-sm" hidden>This relay runs from a git checkout — pull and restart to update.</div>
8763
+ </div>
8764
+ <div id="update-install-status" class="settings-help settings-gap-xs" hidden></div>
8765
+ <button id="update-cancel-btn" class="chat-title-action-btn settings-gap-sm" type="button" onclick="cancelRunningUpdate()" hidden>Cancel update</button>
8766
+ <details id="update-install-log" class="cli-install-log" hidden>
8767
+ <summary>Update log</summary>
8768
+ <pre id="update-install-log-pre"></pre>
8769
+ </details>
8770
+ <div id="update-outcome" class="settings-help settings-gap-xs" hidden>
8771
+ <span id="update-outcome-text"></span>
8772
+ <pre id="update-outcome-log" hidden></pre>
8773
+ <button id="update-outcome-dismiss-btn" class="chat-title-action-btn" type="button" onclick="dismissUpdateOutcome()">Dismiss</button>
8774
+ </div>
8775
+ </section>
8599
8776
  </div>
8600
8777
  <div
8601
8778
  id="settings-panel-providers"