@oar-sh/oar 0.9.6 → 0.9.8

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 (105) hide show
  1. package/CHANGELOG.md +285 -0
  2. package/README.md +222 -40
  3. package/bin/oar.js +320 -18
  4. package/package.json +1 -1
  5. package/server/README.md +174 -7
  6. package/server/claude-cloud-worker/claude-cloud-event-normalizer.mjs +340 -0
  7. package/server/claude-cloud-worker/claude-cloud-session-process.mjs +1325 -0
  8. package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +155 -0
  9. package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
  10. package/server/claude-worker/claude-sdk-adapter.mjs +18 -13
  11. package/server/claude-worker/claude-session-process.mjs +225 -24
  12. package/server/claude-worker/claude-session-worker.mjs +6 -0
  13. package/server/claude-worker/sdk-message-normalizer.mjs +28 -0
  14. package/server/db-schema.mjs +17 -0
  15. package/server/migrations/0006-workspace-root-settings.mjs +67 -0
  16. package/server/migrations/0007-claude-cloud.mjs +82 -0
  17. package/server/public/app/activity-replay-state.mjs +68 -5
  18. package/server/public/app/agent-sessions-settings-ui.js +197 -0
  19. package/server/public/app/agent-sessions-settings.mjs +134 -0
  20. package/server/public/app/api-client.js +149 -6
  21. package/server/public/app/attachments-view.js +20 -1
  22. package/server/public/app/attribution-control.mjs +46 -0
  23. package/server/public/app/bootstrap.js +468 -88
  24. package/server/public/app/claude-cloud-conversation-ui.js +110 -0
  25. package/server/public/app/claude-cloud-settings-ui.js +374 -0
  26. package/server/public/app/claude-cloud-ui.mjs +611 -0
  27. package/server/public/app/composer-control-state.mjs +11 -0
  28. package/server/public/app/context-menu.mjs +140 -0
  29. package/server/public/app/context-tier-options.mjs +4 -0
  30. package/server/public/app/context-usage-view.mjs +127 -8
  31. package/server/public/app/conversation-origin-ui.js +27 -0
  32. package/server/public/app/conversation-provider-indicator.mjs +22 -4
  33. package/server/public/app/conversation-view.js +145 -11
  34. package/server/public/app/cwd-picker.js +12 -0
  35. package/server/public/app/journal-view.js +698 -28
  36. package/server/public/app/math-dollars.mjs +89 -0
  37. package/server/public/app/model-selector-options.mjs +1 -1
  38. package/server/public/app/plan-usage-view.mjs +36 -8
  39. package/server/public/app/pwa-install.js +5 -0
  40. package/server/public/app/relay-error-ctas.mjs +16 -2
  41. package/server/public/app/remote-relay-origin-view.mjs +120 -4
  42. package/server/public/app/router.js +49 -12
  43. package/server/public/app/scrollbar-hover.mjs +78 -0
  44. package/server/public/app/settings-modal.js +37 -3
  45. package/server/public/app/settings-tabs.js +1 -1
  46. package/server/public/app/socket-handlers.js +53 -1
  47. package/server/public/app/store.js +26 -0
  48. package/server/public/app/transcript-separators.mjs +48 -11
  49. package/server/public/app-icon-192.png +0 -0
  50. package/server/public/app-icon-512.png +0 -0
  51. package/server/public/app-icon-light-192.png +0 -0
  52. package/server/public/app-icon-light-512.png +0 -0
  53. package/server/public/app-icon-light-maskable-512.png +0 -0
  54. package/server/public/app-icon-light.svg +4 -0
  55. package/server/public/app-icon-maskable-512.png +0 -0
  56. package/server/public/app-icon.svg +3 -3
  57. package/server/public/favicon.ico +0 -0
  58. package/server/public/index.html +564 -20
  59. package/server/public/manifest.webmanifest +9 -3
  60. package/server/public/sw.js +6 -1
  61. package/server/relay-tools.md +13 -1
  62. package/server/repositories/remote-relay-repository.mjs +42 -1
  63. package/server/repositories/session-repository.mjs +47 -1
  64. package/server/routes/claude-cloud-routes.mjs +140 -0
  65. package/server/routes/git-routes.mjs +37 -0
  66. package/server/routes/messages-routes.mjs +123 -16
  67. package/server/routes/remote-relay-routes.mjs +15 -1
  68. package/server/routes/sessions-routes.mjs +517 -74
  69. package/server/server-runtime.mjs +268 -33
  70. package/server/services/claude-account-usage-service.mjs +176 -0
  71. package/server/services/claude-cloud-repo-service.mjs +408 -0
  72. package/server/services/claude-cloud-session-service.mjs +326 -0
  73. package/server/services/claude-cloud-settings-service.mjs +269 -0
  74. package/server/services/claude-context-usage.mjs +51 -3
  75. package/server/services/git-remote-service.mjs +115 -0
  76. package/server/services/oar-cli-helpers.mjs +67 -1
  77. package/server/services/plan-usage-claude-cloud.mjs +211 -0
  78. package/server/services/plan-usage-claude.mjs +202 -40
  79. package/server/services/plan-usage-contract.mjs +39 -3
  80. package/server/services/plan-usage-service.mjs +82 -2
  81. package/server/services/provider-reasoning-effort.mjs +3 -0
  82. package/server/services/pwa-manifest-theme.mjs +30 -0
  83. package/server/services/remote-relay-caller-context.mjs +49 -10
  84. package/server/services/remote-relay-client.mjs +9 -0
  85. package/server/services/remote-relay-dispatcher.mjs +383 -35
  86. package/server/services/remote-relay-inbound.mjs +49 -2
  87. package/server/services/remote-relay-loopback.mjs +245 -0
  88. package/server/services/remote-relay-registry-service.mjs +52 -0
  89. package/server/services/session-worker-launch-service.mjs +100 -15
  90. package/server/services/session-worker-process-service.mjs +348 -129
  91. package/server/services/session-worker-registry-service.mjs +18 -1
  92. package/server/services/session-worker-stop-service.mjs +24 -13
  93. package/server/services/session-worker-supervisor-service.mjs +56 -1
  94. package/server/services/update-install-service.mjs +9 -3
  95. package/server/services/usage-limit-pause-service.mjs +3 -1
  96. package/server/services/worker-launch-note-service.mjs +151 -0
  97. package/shared/claude-attribution.mjs +117 -0
  98. package/shared/claude-cloud/api-client.mjs +809 -0
  99. package/shared/claude-cloud/base-url.mjs +47 -0
  100. package/shared/claude-cloud/credentials.mjs +221 -0
  101. package/shared/claude-cloud/repo-url.mjs +111 -0
  102. package/shared/provider-routing.mjs +4 -2
  103. package/shared/remote-relay-contract.mjs +153 -22
  104. package/shared/remote-relay-tool-core.mjs +11 -8
  105. package/shared/steer-settle-failure.mjs +1 -0
@@ -3,12 +3,23 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, viewport-fit=cover, interactive-widget=resizes-content">
6
- <meta name="theme-color" content="#161b22">
6
+ <meta name="theme-color" content="#0d1117">
7
7
  <meta name="application-name" content="OAR">
8
8
  <meta name="apple-mobile-web-app-title" content="OAR">
9
9
  <meta name="mobile-web-app-capable" content="yes">
10
10
  <meta name="apple-mobile-web-app-capable" content="yes">
11
11
  <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
12
+ <script>
13
+ // The stored Theme setting, applied before the first paint so the start
14
+ // screen (and its logo) already match it.
15
+ const __STORED_THEME = (() => {
16
+ try { return window.localStorage.getItem('copilot_theme') === 'light' ? 'light' : 'dark'; } catch { return 'dark'; }
17
+ })();
18
+ if (__STORED_THEME === 'light') {
19
+ document.documentElement.setAttribute('data-theme', 'light');
20
+ document.querySelector('meta[name="theme-color"]')?.setAttribute('content', '#ffffff');
21
+ }
22
+ </script>
12
23
  <script>
13
24
  window.__COPILOT_APP_CONFIG = { basePath: '' };
14
25
  const __APP_CONFIG = window.__COPILOT_APP_CONFIG;
@@ -163,13 +174,35 @@
163
174
  if (errorEl) errorEl.textContent = 'Could not reach the relay';
164
175
  }
165
176
  };
166
- if (!__IS_SHARED_ROUTE) {
167
- document.write(`<link rel="manifest" href="${__APP_BASE}/manifest.webmanifest?${__PWA_BUST}">`);
168
- }
169
- document.write(`<link rel="icon" type="image/png" sizes="192x192" href="${__APP_BASE}/app-icon-192.png?${__PWA_BUST}">`);
170
- document.write(`<link rel="icon" type="image/png" sizes="512x512" href="${__APP_BASE}/app-icon-512.png?${__PWA_BUST}">`);
171
- document.write(`<link rel="icon" type="image/svg+xml" href="${__APP_BASE}/app-icon.svg?${__PWA_BUST}">`);
172
- document.write(`<link rel="apple-touch-icon" href="${__APP_BASE}/app-icon-192.png?${__PWA_BUST}">`);
177
+ // Manifest and tab icons for one theme: the dark icon set, or its
178
+ // `app-icon-light*` twin with a white launch background.
179
+ const __themeAssetLinks = (theme) => {
180
+ const light = theme === 'light';
181
+ const icon = light ? 'app-icon-light' : 'app-icon';
182
+ return [
183
+ ...(__IS_SHARED_ROUTE ? [] : [
184
+ { rel: 'manifest', href: `${__APP_BASE}/manifest.webmanifest?${__PWA_BUST}${light ? '&theme=light' : ''}` },
185
+ ]),
186
+ { rel: 'icon', type: 'image/png', sizes: '192x192', href: `${__APP_BASE}/${icon}-192.png?${__PWA_BUST}` },
187
+ { rel: 'icon', type: 'image/png', sizes: '512x512', href: `${__APP_BASE}/${icon}-512.png?${__PWA_BUST}` },
188
+ { rel: 'icon', type: 'image/svg+xml', href: `${__APP_BASE}/${icon}.svg?${__PWA_BUST}` },
189
+ { rel: 'apple-touch-icon', href: `${__APP_BASE}/${icon}-192.png?${__PWA_BUST}` },
190
+ ];
191
+ };
192
+ const __themeAssetLinkHtml = (theme) => __themeAssetLinks(theme)
193
+ .map((link) => `<link data-theme-asset="1" ${Object.entries(link).map(([key, value]) => `${key}="${value}"`).join(' ')}>`)
194
+ .join('');
195
+ document.write(__themeAssetLinkHtml(__STORED_THEME));
196
+ document.write(`<style>
197
+ .brand-logo { background-image: url("${__APP_BASE}/app-icon.svg?${__PWA_BUST}"); }
198
+ :root[data-theme="light"] .brand-logo { background-image: url("${__APP_BASE}/app-icon-light.svg?${__PWA_BUST}"); }
199
+ </style>`);
200
+ // A Theme change swaps them in place (settings-modal's updateTheme).
201
+ window.__applyThemeAssets = (theme) => {
202
+ const next = theme === 'light' ? 'light' : 'dark';
203
+ for (const node of Array.from(document.head.querySelectorAll('link[data-theme-asset]'))) node.remove();
204
+ document.head.insertAdjacentHTML('beforeend', __themeAssetLinkHtml(next));
205
+ };
173
206
  </script>
174
207
  <title>OAR — Open Agent Relay</title>
175
208
  <script>document.write('<scr'+'ipt src="'+__APP_BASE+'/socket.io/socket.io.js"><\/scr'+'ipt>');</script>
@@ -256,6 +289,7 @@
256
289
  --provider-claude: #e0855a;
257
290
  --provider-cursor: #a78bfa;
258
291
  --provider-grok: #e879f9;
292
+ --provider-claude-cloud: #7dd3fc;
259
293
  --viewport-top-offset: 0px;
260
294
  --header-safe-top: max(env(safe-area-inset-top, 0px), var(--viewport-top-offset, 0px));
261
295
  --chat-header-top-base-desktop: 14px;
@@ -317,6 +351,7 @@
317
351
  --provider-claude: #b4531f;
318
352
  --provider-cursor: #6b3fd4;
319
353
  --provider-grok: #a21caf;
354
+ --provider-claude-cloud: #0369a1;
320
355
  --inline-code-bg: #eff1f3;
321
356
  }
322
357
  :root {
@@ -382,12 +417,44 @@
382
417
  display: none; align-items: center; justify-content: center;
383
418
  height: var(--app-height); padding: 24px;
384
419
  }
420
+ /* The start screen lies over the page until the app (or the sign-in box)
421
+ is ready. In the installed app it takes over from the system's launch
422
+ screen: the logo starts where and as large as the launch screen showed
423
+ it, then settles into this layout while the label fades in. */
385
424
  #startup-loading {
386
- display: flex; align-items: center; justify-content: center;
387
- height: var(--app-height); padding: 24px;
425
+ position: fixed; inset: 0; z-index: 10000;
426
+ display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 20px;
427
+ padding: 24px 24px 124px;
428
+ background: var(--bg);
388
429
  color: var(--muted);
389
430
  font-size: var(--fs-core-subtitle);
431
+ transition: opacity 150ms ease;
432
+ }
433
+ #startup-loading.is-leaving { opacity: 0; }
434
+ #startup-loading .brand-logo { width: 148px; height: 148px; }
435
+ #startup-loading.is-animated .brand-logo {
436
+ animation: startup-logo-settle 500ms cubic-bezier(0.22, 1, 0.36, 1) 200ms both;
390
437
  }
438
+ #startup-loading.is-animated .startup-label {
439
+ animation: startup-label-in 400ms ease 450ms both;
440
+ }
441
+ #startup-loading .startup-label { transition: opacity 150ms ease; }
442
+ @keyframes startup-logo-settle {
443
+ from { transform: translate(var(--startup-dx, 0px), var(--startup-dy, 0px)) scale(var(--startup-scale, 1)); }
444
+ to { transform: none; }
445
+ }
446
+ @keyframes startup-label-in {
447
+ from { opacity: 0; }
448
+ to { opacity: 1; }
449
+ }
450
+ /* The logo for the active theme. A background, not an <img>: the browser
451
+ fetches only the one the matching rule names (the rules with the URLs are
452
+ written by the head script, which knows the relay's base path). */
453
+ .brand-logo {
454
+ width: 128px; height: 128px; flex: none;
455
+ background-position: center; background-size: contain; background-repeat: no-repeat;
456
+ }
457
+ .auth-box .brand-logo { width: 96px; height: 96px; margin: 0 auto 16px; }
391
458
  .auth-box {
392
459
  background: var(--bg2); border: 1px solid var(--border); border-radius: 12px;
393
460
  padding: 40px 32px; width: 100%; max-width: 400px; text-align: center;
@@ -541,7 +608,7 @@
541
608
  border-radius: 8px;
542
609
  background: var(--bg2);
543
610
  color: var(--text);
544
- padding: 0 38px 0 10px;
611
+ padding: 0 76px 0 10px;
545
612
  font-size: var(--fs-meta-md);
546
613
  }
547
614
  /* iOS Safari zooms the page on focus for inputs under 16px, and the mobile
@@ -724,6 +791,7 @@
724
791
  .conv-provider-indicator[data-provider="claude"] { color: var(--provider-claude); }
725
792
  .conv-provider-indicator[data-provider="cursor"] { color: var(--provider-cursor); }
726
793
  .conv-provider-indicator[data-provider="grok"] { color: var(--provider-grok); }
794
+ .conv-provider-indicator[data-provider="claude-cloud"] { color: var(--provider-claude-cloud); }
727
795
  .conv-item.worker-ui-offline .conv-title,
728
796
  #chat-title[data-ui-state="offline"] { color: var(--conversation-title-offline); }
729
797
  .conv-item.worker-ui-ready .conv-title,
@@ -744,6 +812,70 @@
744
812
  }
745
813
  .conv-item:hover .conv-delete { opacity: 1; }
746
814
  .conv-delete:hover { color: var(--danger); }
815
+ /* Archive / unarchive sits left of the bin, shown on hover like it. */
816
+ .conv-archive {
817
+ position: absolute; right: 34px; top: 50%; transform: translateY(-50%);
818
+ background: none; border: none; color: var(--muted); cursor: pointer; font-size: var(--fs-ui-icon);
819
+ opacity: 0; transition: opacity 0.15s; padding: 4px;
820
+ }
821
+ .conv-item:hover .conv-archive { opacity: 1; }
822
+ .conv-archive:hover { color: var(--text); }
823
+ .conv-item.archived .conv-title { color: var(--muted); }
824
+ /* The Archived view: a toggle in the filter row, a banner above the list. */
825
+ #conv-archived-toggle {
826
+ position: absolute;
827
+ top: 50%;
828
+ right: 46px;
829
+ transform: translateY(-50%);
830
+ height: 28px;
831
+ min-width: 28px;
832
+ padding: 0 6px;
833
+ border: 1px solid transparent;
834
+ border-radius: 6px;
835
+ background: transparent;
836
+ color: var(--muted);
837
+ cursor: pointer;
838
+ font-size: var(--fs-meta-md);
839
+ }
840
+ #conv-archived-toggle:hover { color: var(--text); border-color: var(--border); }
841
+ #conv-archived-toggle[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); }
842
+ #conv-archived-banner {
843
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
844
+ margin: 0 16px 6px; padding: 6px 10px;
845
+ border: 1px solid var(--border); border-radius: 8px; background: var(--bg2);
846
+ font-size: var(--fs-meta-sm); color: var(--muted);
847
+ }
848
+ #conv-archived-banner[hidden] { display: none; }
849
+ .conv-archived-back {
850
+ border: 1px solid var(--border); border-radius: 6px; background: var(--bg3); color: var(--text);
851
+ font-size: var(--fs-meta-xs); padding: 2px 8px; cursor: pointer;
852
+ }
853
+ /* The conversation context menu (context-menu.mjs): right-click / long press. */
854
+ .context-menu {
855
+ position: fixed;
856
+ z-index: 1200;
857
+ min-width: 200px;
858
+ max-width: min(320px, calc(100vw - 8px));
859
+ padding: 4px;
860
+ display: flex;
861
+ flex-direction: column;
862
+ gap: 2px;
863
+ background: var(--bg2);
864
+ border: 1px solid var(--border);
865
+ border-radius: 10px;
866
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
867
+ }
868
+ .context-menu-item {
869
+ display: flex; align-items: center; gap: 8px; width: 100%; min-height: 34px;
870
+ border: 1px solid transparent; border-radius: 8px; background: transparent; color: var(--text);
871
+ padding: 0 10px; font: inherit; font-size: var(--fs-meta-md); text-align: left; cursor: pointer;
872
+ touch-action: manipulation; -webkit-tap-highlight-color: transparent;
873
+ }
874
+ .context-menu-item:hover, .context-menu-item:focus-visible { background: var(--bg3); border-color: var(--border); outline: none; }
875
+ .context-menu-item:disabled { opacity: 0.5; cursor: not-allowed; }
876
+ .context-menu-item-danger { color: var(--danger); }
877
+ .context-menu-sep { height: 1px; margin: 3px 6px; background: var(--border); }
878
+ #conv-list .conv-item { -webkit-touch-callout: none; user-select: none; -webkit-user-select: none; }
747
879
  .conv-open-questions {
748
880
  display: inline-flex;
749
881
  margin-left: 6px;
@@ -1285,6 +1417,9 @@
1285
1417
  padding: 2px 10px;
1286
1418
  background: var(--bg2);
1287
1419
  }
1420
+ /* A compaction that is still running: the label trails the bouncing dots. */
1421
+ .transcript-separator.is-pending .dots { margin-left: 6px; }
1422
+ .transcript-separator.is-pending .dots span { width: 4px; height: 4px; }
1288
1423
 
1289
1424
  .empty-state {
1290
1425
  flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center;
@@ -1434,6 +1569,17 @@
1434
1569
  overflow-wrap: normal;
1435
1570
  word-break: normal;
1436
1571
  }
1572
+ /* An inline formula breaks only between its terms: one long term is a box
1573
+ wider than the bubble and made the whole bubble scroll sideways. It
1574
+ scrolls inside itself instead. */
1575
+ .msg-bubble :not(.katex-display) > .katex {
1576
+ display: inline-block;
1577
+ max-width: 100%;
1578
+ overflow-x: auto;
1579
+ overflow-y: hidden;
1580
+ vertical-align: bottom;
1581
+ -webkit-overflow-scrolling: touch;
1582
+ }
1437
1583
  .katex-error {
1438
1584
  color: var(--danger);
1439
1585
  white-space: pre-wrap;
@@ -1666,6 +1812,12 @@
1666
1812
  word-break: break-word;
1667
1813
  }
1668
1814
  .thinking-stream[hidden] { display: none; }
1815
+ .thinking-compacting-note {
1816
+ color: var(--muted);
1817
+ font-size: var(--fs-meta-sm);
1818
+ font-style: italic;
1819
+ }
1820
+ .thinking-compacting-note[hidden] { display: none; }
1669
1821
  .thinking-stream > :first-child { margin-top: 0; }
1670
1822
  .thinking-stream > :last-child { margin-bottom: 0; }
1671
1823
  .thinking-activity {
@@ -2019,8 +2171,19 @@
2019
2171
  font-size: 0.76rem;
2020
2172
  color: var(--muted);
2021
2173
  }
2174
+ .ctx-autocompact-notes { display: flex; flex-direction: column; gap: 4px; }
2175
+ .ctx-autocompact-notes:empty { display: none; }
2176
+ .ctx-autocompact-warning { color: var(--warn); }
2022
2177
  /* Claude-only thinking control (on/off + text visibility), under the
2023
2178
  auto-compact slider. */
2179
+ /* The folder's commit attribution (attribution-control.mjs), styled like
2180
+ the thinking rows above it. */
2181
+ .ctx-attribution { margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border); }
2182
+ .ctx-attribution-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
2183
+ .ctx-attribution-label { font-size: var(--fs-meta-sm); color: var(--muted); }
2184
+ .ctx-attribution-select { font-size: var(--fs-meta-sm); max-width: 100%; }
2185
+ .ctx-attribution-note { margin-top: 6px; font-size: var(--fs-meta-sm); color: var(--muted); line-height: 1.4; overflow-wrap: anywhere; }
2186
+ .ctx-attribution-note code { font-size: inherit; }
2024
2187
  .ctx-thinking {
2025
2188
  margin-top: 14px;
2026
2189
  padding-top: 12px;
@@ -2119,6 +2282,13 @@
2119
2282
  }
2120
2283
  .plan-usage-card[data-status="unavailable"],
2121
2284
  .plan-usage-card[data-status="not-configured"] { opacity: 0.72; }
2285
+ /* A card whose provider is being read live while the kept reading shows. */
2286
+ .plan-usage-card[data-updating] .plan-usage-card-title::after {
2287
+ content: " · updating…";
2288
+ font-weight: 400;
2289
+ font-size: 0.78rem;
2290
+ color: var(--muted);
2291
+ }
2122
2292
  .plan-usage-card-head {
2123
2293
  display: flex;
2124
2294
  align-items: center;
@@ -2516,6 +2686,40 @@
2516
2686
  #model-lock-note[data-provider="claude"] { color: var(--provider-claude); }
2517
2687
  #model-lock-note[data-provider="cursor"] { color: var(--provider-cursor); }
2518
2688
  #model-lock-note[data-provider="grok"] { color: var(--provider-grok); }
2689
+ #model-lock-note[data-provider="claude-cloud"] { color: var(--provider-claude-cloud); }
2690
+ /* Claude Cloud conversations (claude-cloud-conversation-ui.js): where the
2691
+ session runs and what it pushed, above the composer like the lock note.
2692
+ The items wrap instead of scrolling, so a long branch name can never push
2693
+ the composer wider than a phone. */
2694
+ #cloud-session-line {
2695
+ display: flex;
2696
+ flex-wrap: wrap;
2697
+ align-items: center;
2698
+ gap: 2px 10px;
2699
+ margin-bottom: 6px;
2700
+ min-width: 0;
2701
+ color: var(--muted);
2702
+ font-size: var(--fs-meta-sm);
2703
+ line-height: 1.4;
2704
+ }
2705
+ #cloud-session-line[hidden] { display: none; }
2706
+ .cloud-line-item {
2707
+ min-width: 0;
2708
+ max-width: 100%;
2709
+ overflow: hidden;
2710
+ text-overflow: ellipsis;
2711
+ white-space: nowrap;
2712
+ }
2713
+ a.cloud-line-item { color: var(--accent); text-decoration: none; }
2714
+ a.cloud-line-item:hover { text-decoration: underline; }
2715
+ .cloud-line-repo { color: var(--provider-claude-cloud); font-weight: 600; }
2716
+ a.cloud-line-repo { color: var(--provider-claude-cloud); }
2717
+ /* No relay modes, no effort choice and no context tier in a cloud chat. */
2718
+ body.claude-cloud-conversation #mode-select,
2719
+ body.claude-cloud-conversation #context-tier-select,
2720
+ body.claude-cloud-conversation #reasoning-effort-select {
2721
+ display: none;
2722
+ }
2519
2723
  #model-select:disabled,
2520
2724
  #reasoning-effort-select:disabled {
2521
2725
  opacity: 0.55;
@@ -2822,6 +3026,46 @@
2822
3026
  flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
2823
3027
  color: var(--muted); font-size: var(--fs-ui-chip);
2824
3028
  }
3029
+ /* "via agent · “title”": an agent on this relay started the conversation.
3030
+ A button while the conversation it runs in can be opened, a span when it
3031
+ is gone. The title is long, so in the sidebar the date keeps its share. */
3032
+ .conv-origin-marker.conv-origin-agent { max-width: 60%; }
3033
+ button.conv-origin-agent,
3034
+ button.msg-origin-badge {
3035
+ font-family: inherit; cursor: pointer; text-align: left;
3036
+ touch-action: manipulation; -webkit-tap-highlight-color: transparent;
3037
+ }
3038
+ button.conv-origin-agent {
3039
+ background: none; border: none; padding: 0; margin: 0; line-height: inherit;
3040
+ color: var(--accent);
3041
+ }
3042
+ /* A finger needs more than one line of chip text: the padding grows the
3043
+ button, the negative margin keeps the row's height. */
3044
+ button.conv-origin-marker.conv-origin-agent { padding: 6px 0; margin: -6px 0; }
3045
+ button.conv-origin-agent:hover,
3046
+ button.msg-origin-badge:hover { text-decoration: underline; }
3047
+ button.msg-origin-badge { color: inherit; }
3048
+ #chat-title-origin {
3049
+ display: block; margin-top: 2px; min-width: 0;
3050
+ font-size: var(--fs-meta-xs); line-height: 1.2; color: var(--muted);
3051
+ overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
3052
+ }
3053
+ #chat-title-origin[hidden] { display: none; }
3054
+ #chat-title-origin .conv-origin-agent {
3055
+ display: block; max-width: 100%; font-size: inherit;
3056
+ overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
3057
+ }
3058
+ #chat-title-origin button.conv-origin-agent { padding: 3px 0; }
3059
+ /* Agent sessions (Settings → Relays). The slider spans the row so a thumb
3060
+ has room to travel at phone width, with a taller touch area than the
3061
+ track itself. */
3062
+ #agent-sessions-section[hidden] { display: none; }
3063
+ #agent-sessions-max-wait-slider {
3064
+ width: min(420px, 100%); min-height: 32px; margin: 0; box-sizing: border-box;
3065
+ accent-color: var(--accent); touch-action: pan-y;
3066
+ }
3067
+ #agent-sessions-max-wait-slider:disabled { opacity: 0.5; }
3068
+ #agent-sessions-status { overflow-wrap: anywhere; }
2825
3069
  /* Transcript card: a preview published during this turn, pinned into the
2826
3070
  response bubble. Compact single row; closed previews keep the label but
2827
3071
  lose the dead link. */
@@ -2865,6 +3109,11 @@
2865
3109
  background: rgba(63, 185, 80, 0.12); color: var(--relay-tone-online);
2866
3110
  border-radius: 6px; padding: 0 6px; font-size: var(--fs-meta-sm);
2867
3111
  }
3112
+ /* The relay's note while a session's worker cannot be started
3113
+ (worker-launch-note-service.mjs): a notice of the relay's, not a reply.
3114
+ Amber while the relay keeps trying or has stopped, green once started. */
3115
+ .msg.assistant.msg-worker-launch .msg-bubble { border-left: 3px solid #d29922; }
3116
+ .msg.assistant.msg-worker-launch-started .msg-bubble { border-left-color: var(--relay-tone-online); }
2868
3117
  /* Mid-turn steering. The CLI usually FOLDS a steered message into the
2869
3118
  running turn and answers everything with one reply; the steered row then
2870
3119
  settles as a compact marker under its own message (kind='folded'). More
@@ -2926,6 +3175,7 @@
2926
3175
  body.shared-reader-mode #reasoning-effort-select,
2927
3176
  body.shared-reader-mode #send-btn,
2928
3177
  body.shared-reader-mode #model-lock-note,
3178
+ body.shared-reader-mode #cloud-session-line,
2929
3179
  body.shared-reader-mode #model-pricing-details {
2930
3180
  display: none !important;
2931
3181
  }
@@ -3298,12 +3548,16 @@
3298
3548
  #new-conversation-context-select,
3299
3549
  #new-conversation-size-select,
3300
3550
  #new-conversation-cwd-select,
3301
- #new-conversation-cwd-manual {
3551
+ #new-conversation-cwd-manual,
3552
+ #new-conversation-cloud-repo,
3553
+ #new-conversation-cloud-branch {
3302
3554
  width: 100%;
3303
3555
  min-height: 42px;
3304
3556
  margin-top: 8px;
3305
3557
  }
3306
- #new-conversation-cwd-manual {
3558
+ #new-conversation-cwd-manual,
3559
+ #new-conversation-cloud-repo,
3560
+ #new-conversation-cloud-branch {
3307
3561
  box-sizing: border-box;
3308
3562
  padding: 0 10px;
3309
3563
  border-radius: 8px;
@@ -3312,6 +3566,84 @@
3312
3566
  color: var(--text);
3313
3567
  }
3314
3568
  #new-conversation-cwd-manual[hidden] { display: none; }
3569
+ #new-conversation-cloud-repo[aria-invalid="true"],
3570
+ #new-conversation-cloud-branch[aria-invalid="true"] { border-color: var(--danger); }
3571
+ /* Warning lines under Repository/Branch: what the cloud clone will not see. */
3572
+ #new-conversation-cloud-warnings {
3573
+ margin-top: 8px;
3574
+ font-size: 0.78rem;
3575
+ line-height: 1.4;
3576
+ color: var(--warn);
3577
+ overflow-wrap: anywhere;
3578
+ }
3579
+ #new-conversation-cloud-warnings:empty { display: none; }
3580
+ .new-conversation-cloud-warning::before { content: "⚠ "; }
3581
+ #new-conversation-cloud-error {
3582
+ margin-top: 8px;
3583
+ font-size: 0.78rem;
3584
+ line-height: 1.4;
3585
+ color: var(--danger);
3586
+ overflow-wrap: anywhere;
3587
+ }
3588
+ #new-conversation-cloud-error[hidden] { display: none; }
3589
+ /* Suggestions under Repository/Branch: a list in the flow of the modal
3590
+ (not a floating one: the modal scrolls on a phone and a popover would
3591
+ be clipped), at most a few rows high, scrolling inside itself. */
3592
+ .new-conversation-cloud-suggest {
3593
+ margin-top: 4px;
3594
+ max-height: min(40vh, 220px);
3595
+ overflow-y: auto;
3596
+ border: 1px solid var(--border);
3597
+ border-radius: 8px;
3598
+ background: var(--bg3);
3599
+ }
3600
+ .new-conversation-cloud-suggest[hidden] { display: none; }
3601
+ .new-conversation-cloud-suggest-item {
3602
+ display: flex;
3603
+ align-items: baseline;
3604
+ justify-content: space-between;
3605
+ gap: 8px;
3606
+ width: 100%;
3607
+ box-sizing: border-box;
3608
+ min-height: 38px;
3609
+ padding: 8px 10px;
3610
+ border: 0;
3611
+ border-bottom: 1px solid var(--border);
3612
+ background: transparent;
3613
+ color: var(--text);
3614
+ font: inherit;
3615
+ text-align: left;
3616
+ cursor: pointer;
3617
+ overflow-wrap: anywhere;
3618
+ }
3619
+ .new-conversation-cloud-suggest-item:last-child { border-bottom: 0; }
3620
+ .new-conversation-cloud-suggest-item:hover,
3621
+ .new-conversation-cloud-suggest-item[aria-selected="true"] { background: var(--bg2); }
3622
+ .new-conversation-cloud-suggest-meta {
3623
+ flex: 0 0 auto;
3624
+ font-size: 0.72rem;
3625
+ color: var(--muted);
3626
+ white-space: nowrap;
3627
+ }
3628
+ .new-conversation-cloud-refresh {
3629
+ margin-left: 6px;
3630
+ padding: 0 6px;
3631
+ min-height: 22px;
3632
+ border: 1px solid transparent;
3633
+ border-radius: 6px;
3634
+ background: transparent;
3635
+ color: var(--muted);
3636
+ font: inherit;
3637
+ cursor: pointer;
3638
+ vertical-align: middle;
3639
+ }
3640
+ .new-conversation-cloud-refresh:hover { color: var(--text); border-color: var(--border); }
3641
+ .new-conversation-cloud-refresh[aria-busy="true"] { opacity: 0.5; cursor: progress; }
3642
+ .new-conversation-cloud-suggest-note {
3643
+ padding: 8px 10px;
3644
+ font-size: 0.78rem;
3645
+ color: var(--muted);
3646
+ }
3315
3647
  /* Shared row/button recipe for the file-explorer CWD browse buttons in the
3316
3648
  New chat and Change CWD modals — one place to restyle both. */
3317
3649
  .cwd-browse-row {
@@ -4826,6 +5158,7 @@
4826
5158
  #status-btn { min-width: 72px; padding: 9px 10px; font-size: var(--fs-meta-md); }
4827
5159
  #conv-filter-row { padding: 8px 12px; }
4828
5160
  #conv-filter-clear { right: 16px; }
5161
+ #conv-archived-toggle { right: 42px; }
4829
5162
  #conv-list { padding: 0 5px 8px; }
4830
5163
  .conv-item { padding: 6px 11px; border-radius: 8px; }
4831
5164
  .conv-title {
@@ -4890,6 +5223,10 @@
4890
5223
  overflow: hidden;
4891
5224
  text-overflow: ellipsis;
4892
5225
  }
5226
+ #chat-title-origin {
5227
+ margin-top: 1px;
5228
+ font-size: clamp(0.62rem, 2vw, 0.7rem);
5229
+ }
4893
5230
  #chat-header-actions {
4894
5231
  margin-left: auto;
4895
5232
  }
@@ -5040,6 +5377,12 @@
5040
5377
  font-size: var(--fs-meta-md);
5041
5378
  }
5042
5379
  #attach-btn { order: 4; flex: 0 0 40px; }
5380
+ /* A cloud chat hides mode, context tier and effort: the model select
5381
+ takes the room they leave. */
5382
+ body.claude-cloud-conversation #model-select {
5383
+ flex: 1 1 0;
5384
+ width: auto;
5385
+ }
5043
5386
  #context-tier-select {
5044
5387
  order: 5;
5045
5388
  flex: 0 0 70px;
@@ -5319,6 +5662,18 @@
5319
5662
  ::-webkit-scrollbar { width: 6px; }
5320
5663
  ::-webkit-scrollbar-track { background: transparent; }
5321
5664
  ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
5665
+ /* With a mouse the track is twice as wide and the thumb is drawn thin in
5666
+ its middle; the thumb fills the track while the pointer is on the
5667
+ scrollbar and for a moment after (scrollbar-hover.mjs). The track keeps
5668
+ its width, so the page does not move when the thumb grows. */
5669
+ @media (hover: hover) and (pointer: fine) {
5670
+ ::-webkit-scrollbar { width: 12px; }
5671
+ ::-webkit-scrollbar-thumb {
5672
+ border: 3px solid transparent; border-top-width: 0; border-bottom-width: 0;
5673
+ background-clip: padding-box; border-radius: 6px;
5674
+ }
5675
+ .scrollbar-wide::-webkit-scrollbar-thumb { border-width: 0; background: var(--muted); }
5676
+ }
5322
5677
  </style>
5323
5678
  </head>
5324
5679
  <body>
@@ -5326,7 +5681,8 @@
5326
5681
  <!-- Auth Gate -->
5327
5682
  <div id="auth-gate">
5328
5683
  <div class="auth-box">
5329
- <h1>🛶 OAR</h1>
5684
+ <div class="brand-logo" aria-hidden="true"></div>
5685
+ <h1>OAR</h1>
5330
5686
  <p>Enter your access token to connect</p>
5331
5687
  <input id="token-input" type="password" placeholder="Access token" autofocus onkeydown="if(event.key==='Enter')window.__copilotAuthConnect(event)">
5332
5688
  <button onclick="window.__copilotAuthConnect(event)">Connect</button>
@@ -5334,7 +5690,87 @@
5334
5690
  </div>
5335
5691
  </div>
5336
5692
 
5337
- <div id="startup-loading" role="status" aria-live="polite">Connecting to OAR…</div>
5693
+ <div id="startup-loading" role="status" aria-live="polite"><div class="brand-logo" aria-hidden="true"></div><div class="startup-label">Connecting to OAR…</div></div>
5694
+ <script>
5695
+ // The start screen's hand-over from the system launch screen, and its two
5696
+ // ways out: into the app (finish) or into the sign-in box (toAuth). Only the
5697
+ // installed app animates and makes its exits wait for the animation; a
5698
+ // browser tab has no launch screen to continue from and leaves at once.
5699
+ window.__startupSplash = (() => {
5700
+ const root = document.getElementById('startup-loading');
5701
+ const logo = root.querySelector('.brand-logo');
5702
+ const label = root.querySelector('.startup-label');
5703
+ const media = (query) => { try { return window.matchMedia(query).matches; } catch { return false; } };
5704
+ const installed = media('(display-mode: standalone)') || media('(display-mode: fullscreen)')
5705
+ || window.navigator.standalone === true;
5706
+ const animated = installed && !media('(prefers-reduced-motion: reduce)');
5707
+ const wait = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
5708
+ let done = Promise.resolve();
5709
+ if (animated) {
5710
+ // Android draws its launch icon around the centre of the whole screen,
5711
+ // 275 px across for a maskable icon, which makes the otter 155 px wide.
5712
+ // The tile used here carries the otter at 373/512 of its width.
5713
+ const LAUNCH_TILE_PX = 155.2 / (373 / 512);
5714
+ // The page only knows the sum of the system bars; a three-button
5715
+ // navigation bar (48 px) is assumed for the split.
5716
+ const NAV_BAR_PX = 48;
5717
+ const rect = logo.getBoundingClientRect();
5718
+ const bars = Math.max(0, (window.screen?.height || window.innerHeight) - window.innerHeight);
5719
+ const topBar = Math.max(0, bars - NAV_BAR_PX);
5720
+ let targetY = (window.screen?.height || window.innerHeight) / 2 - topBar;
5721
+ // An implausible reading (split screen, a desktop window) falls back to
5722
+ // the middle of the page.
5723
+ if (!(Math.abs(targetY - window.innerHeight / 2) <= 60)) targetY = window.innerHeight / 2;
5724
+ root.style.setProperty('--startup-dx', `${window.innerWidth / 2 - (rect.left + rect.width / 2)}px`);
5725
+ root.style.setProperty('--startup-dy', `${targetY - (rect.top + rect.height / 2)}px`);
5726
+ root.style.setProperty('--startup-scale', String(LAUNCH_TILE_PX / rect.width));
5727
+ root.classList.add('is-animated');
5728
+ // Rest 200 ms, settle 500 ms; the label's fade ends at 850 ms.
5729
+ done = wait(850);
5730
+ }
5731
+ let left = null;
5732
+ const leave = (run) => {
5733
+ // Removed whatever happens on the way out: a start screen left over
5734
+ // the page would cover the whole app.
5735
+ if (!left) {
5736
+ left = (animated && root.isConnected ? done.then(run) : Promise.resolve())
5737
+ .catch(() => {})
5738
+ .then(() => { root.remove(); });
5739
+ }
5740
+ return left;
5741
+ };
5742
+ return {
5743
+ animated,
5744
+ // Into the app: fade the start screen away over the page below it.
5745
+ finish: () => leave(async () => {
5746
+ root.classList.add('is-leaving');
5747
+ await wait(150);
5748
+ }),
5749
+ // Into the sign-in box: the logo travels on into the box's own logo
5750
+ // place, shrinking to its size, and the box fades in around it.
5751
+ toAuth: () => leave(async () => {
5752
+ const box = document.querySelector('#auth-gate .auth-box');
5753
+ const target = box?.querySelector('.brand-logo')?.getBoundingClientRect();
5754
+ if (!box || !target || !target.width) return;
5755
+ box.style.opacity = '0';
5756
+ root.style.background = 'transparent';
5757
+ label.style.animation = 'none';
5758
+ label.style.opacity = '0';
5759
+ const from = logo.getBoundingClientRect();
5760
+ logo.style.animation = 'none';
5761
+ logo.style.transition = 'transform 450ms cubic-bezier(0.22, 1, 0.36, 1)';
5762
+ logo.style.transform = `translate(${target.left + target.width / 2 - (from.left + from.width / 2)}px, `
5763
+ + `${target.top + target.height / 2 - (from.top + from.height / 2)}px) scale(${target.width / from.width})`;
5764
+ await wait(450);
5765
+ box.style.transition = 'opacity 300ms ease';
5766
+ box.style.opacity = '1';
5767
+ await wait(300);
5768
+ box.style.transition = '';
5769
+ box.style.opacity = '';
5770
+ }),
5771
+ };
5772
+ })();
5773
+ </script>
5338
5774
 
5339
5775
  <!-- Sidebar overlay (mobile) -->
5340
5776
  <div id="sidebar-overlay" onclick="closeSidebar()"></div>
@@ -5357,8 +5793,10 @@
5357
5793
  <div id="conv-filter-row">
5358
5794
  <input id="conv-filter-input" type="text" placeholder="Filter conversations…" autocomplete="off" spellcheck="false" aria-label="Filter conversations by title">
5359
5795
  <button id="conv-filter-clear" type="button" title="Clear filter" aria-label="Clear filter" hidden>×</button>
5796
+ <button id="conv-archived-toggle" type="button" title="Show archived conversations" aria-label="Show archived conversations" aria-pressed="false" onclick="toggleArchivedConversations()">🗄</button>
5360
5797
  <div id="conv-filter-status" role="status" aria-live="polite"></div>
5361
5798
  </div>
5799
+ <div id="conv-archived-banner" hidden>Archived conversations <button type="button" class="conv-archived-back" onclick="toggleArchivedConversations()">Back to the list</button></div>
5362
5800
  <div id="conv-list"></div>
5363
5801
  </div>
5364
5802
  <div id="sidebar-resizer" role="separator" aria-orientation="vertical" aria-label="Resize conversations sidebar"></div>
@@ -5367,7 +5805,7 @@
5367
5805
  <div id="chat-header">
5368
5806
  <button id="sidebar-toggle" class="header-icon-btn" type="button" title="Toggle conversations sidebar" aria-label="Toggle conversations sidebar">☰</button>
5369
5807
  <div id="chat-title-wrap">
5370
- <span id="chat-title-slot"><span id="chat-title-primary"><span id="chat-title-watch-indicator" hidden>👁️ </span><span id="chat-title">Select or start a conversation</span></span><span id="chat-title-cwd" hidden></span><span id="chat-title-session-usage" hidden></span></span>
5808
+ <span id="chat-title-slot"><span id="chat-title-primary"><span id="chat-title-watch-indicator" hidden>👁️ </span><span id="chat-title">Select or start a conversation</span></span><span id="chat-title-origin" hidden></span><span id="chat-title-cwd" hidden></span><span id="chat-title-session-usage" hidden></span></span>
5371
5809
  <form id="chat-title-editor" hidden>
5372
5810
  <input id="chat-title-input" type="text" maxlength="120" autocomplete="off" spellcheck="false">
5373
5811
  <button id="chat-title-save-btn" class="chat-title-action-btn" type="submit">Save</button>
@@ -5387,11 +5825,13 @@
5387
5825
  <button id="chat-menu-usage" class="chat-actions-menu-item" type="button" role="menuitem">📊 Check Usage</button>
5388
5826
  <button id="chat-menu-compact" class="chat-actions-menu-item" type="button" role="menuitem" hidden disabled aria-hidden="true" tabindex="-1" style="display:none">🗜️ Compact</button>
5389
5827
  <button id="chat-menu-share-conversation" class="chat-actions-menu-item" type="button" role="menuitem">➡️ Share conversation</button>
5828
+ <button id="chat-menu-archive" class="chat-actions-menu-item" type="button" role="menuitem">🗄 Archive conversation</button>
5390
5829
  <button id="chat-menu-refresh-history" class="chat-actions-menu-item" type="button" role="menuitem" hidden disabled aria-hidden="true" tabindex="-1" style="display:none">🔄️ Refresh history</button>
5391
5830
  <button id="chat-menu-inspect-tmux" class="chat-actions-menu-item" type="button" role="menuitem">🖥️ Inspect tmux console</button>
5392
5831
  <button id="chat-menu-select-models" class="chat-actions-menu-item" type="button" role="menuitem">🤗 Select Models</button>
5393
5832
  <button id="chat-menu-change-cwd" class="chat-actions-menu-item" type="button" role="menuitem">🗂️ Change CWD</button>
5394
5833
  <button id="chat-menu-git-changes" class="chat-actions-menu-item" type="button" role="menuitem">🌿 Git changes</button>
5834
+ <button id="chat-menu-open-cloud-session" class="chat-actions-menu-item" type="button" role="menuitem" hidden>☁️ Open on claude.ai</button>
5395
5835
  <button id="chat-menu-settings" class="chat-actions-menu-item" type="button" role="menuitem">⚙️ Settings</button>
5396
5836
  <button id="chat-menu-restart-relay" class="chat-actions-menu-item" type="button" role="menuitem">🌄 Restart web relay</button>
5397
5837
  <button id="chat-menu-suspend-host" class="chat-actions-menu-item" type="button" role="menuitem" onclick="openSuspendHostConfirmation()">💤 Suspend host</button>
@@ -5414,6 +5854,7 @@
5414
5854
  <div id="model-banner"></div>
5415
5855
  <div id="usage-limit-banner" role="status" aria-live="polite" hidden></div>
5416
5856
  <div id="model-lock-note" hidden></div>
5857
+ <div id="cloud-session-line" aria-label="Cloud session" hidden></div>
5417
5858
  <div id="model-metadata-blocker" aria-live="polite">
5418
5859
  <span id="model-metadata-blocker-text">Model metadata is unavailable. Refresh to choose a model and reasoning effort.</span>
5419
5860
  <button type="button" id="model-metadata-retry-btn">Refresh models</button>
@@ -5794,6 +6235,16 @@
5794
6235
  tabindex="-1"
5795
6236
  data-settings-provider-tab="claude"
5796
6237
  >Claude</button>
6238
+ <button
6239
+ id="settings-provider-tab-claude-cloud"
6240
+ class="settings-tab settings-subtab"
6241
+ type="button"
6242
+ role="tab"
6243
+ aria-selected="false"
6244
+ aria-controls="settings-provider-panel-claude-cloud"
6245
+ tabindex="-1"
6246
+ data-settings-provider-tab="claude-cloud"
6247
+ >Claude Cloud</button>
5797
6248
  <button
5798
6249
  id="settings-provider-tab-grok"
5799
6250
  class="settings-tab settings-subtab"
@@ -5982,6 +6433,13 @@
5982
6433
  placeholder="claude-sonnet-5"
5983
6434
  aria-label="Claude model ID"
5984
6435
  >
6436
+ <label for="claude-attribution-select" style="margin-top: 8px;">Commit attribution</label>
6437
+ <select id="claude-attribution-select" aria-label="Commit attribution" onchange="window.previewClaudeAttribution?.(this.value)">
6438
+ <option value="oar">OAR — Co-authored-by: Open Agent Relay (model)</option>
6439
+ <option value="vanilla">Vanilla — Claude Code's own attribution</option>
6440
+ <option value="off">Off — no attribution</option>
6441
+ </select>
6442
+ <div id="claude-attribution-example" class="settings-help"></div>
5985
6443
  <div id="claude-settings-status" class="settings-help settings-provider-status">Loading Claude settings…</div>
5986
6444
  <div style="display: flex; gap: 8px; margin-top: 8px;">
5987
6445
  <button id="claude-save-btn" type="button" class="summary-btn" onclick="saveClaudeSettings()">Save Claude settings</button>
@@ -5989,6 +6447,42 @@
5989
6447
  <div class="settings-help">Runs Claude conversations through the Claude Agent SDK using the relay host's logged-in Claude credentials (no API key stored). Disabling also rebinds conversations that have not sent a message yet.</div>
5990
6448
  </div>
5991
6449
  </div>
6450
+ <!-- Claude Cloud (claude-cloud-settings-ui.js). The login itself is managed on
6451
+ the Claude tab; this panel only says which account pays. -->
6452
+ <div
6453
+ id="settings-provider-panel-claude-cloud"
6454
+ class="settings-panel"
6455
+ role="tabpanel"
6456
+ tabindex="0"
6457
+ aria-labelledby="settings-provider-tab-claude-cloud"
6458
+ data-settings-provider-panel="claude-cloud"
6459
+ hidden
6460
+ >
6461
+ <div class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
6462
+ <label for="claude-cloud-enabled-toggle">Claude Cloud</label>
6463
+ <div class="settings-help">Cloud chats run Claude Code in a sandbox at Anthropic, on its own clone of a GitHub repository, instead of on the relay host. They are billed to the Claude account below. The relay reads that account's login from the Claude CLI on the relay host; it does not store it or refresh it.</div>
6464
+ <div class="settings-row" style="width: 100%; margin-top: 8px; margin-bottom: 8px;">
6465
+ <label for="claude-cloud-enabled-toggle">Enable Claude Cloud for New Chat</label>
6466
+ <input id="claude-cloud-enabled-toggle" type="checkbox" onchange="toggleClaudeCloudProvider(this.checked)" disabled>
6467
+ </div>
6468
+ <div id="claude-cloud-account-row" class="settings-row" style="width: 100%; margin-bottom: 0;" hidden>
6469
+ <div id="claude-cloud-account" class="settings-help settings-provider-status" style="min-width: 0; overflow-wrap: anywhere;"></div>
6470
+ <button id="claude-cloud-login-btn" type="button" class="summary-btn" style="flex: 0 0 auto;" onclick="openClaudeCloudLoginTab()">Claude login</button>
6471
+ </div>
6472
+ <div id="claude-cloud-token" class="settings-help settings-provider-status" style="margin-top: 6px;" hidden></div>
6473
+ <label for="claude-cloud-environment-select" style="margin-top: 12px;">Cloud environment</label>
6474
+ <select id="claude-cloud-environment-select" aria-label="Cloud environment" style="max-width: 100%;" disabled></select>
6475
+ <div id="claude-cloud-environment-note" class="settings-help" hidden></div>
6476
+ <label for="claude-cloud-model-select" style="margin-top: 8px;">Default model</label>
6477
+ <select id="claude-cloud-model-select" aria-label="Default model for cloud chats" style="max-width: 100%;" disabled></select>
6478
+ <div id="claude-cloud-settings-status" class="settings-help settings-provider-status">Loading Claude Cloud settings…</div>
6479
+ <div style="display: flex; gap: 8px 12px; margin-top: 8px; flex-wrap: wrap; align-items: center;">
6480
+ <button id="claude-cloud-save-btn" type="button" class="summary-btn" onclick="saveClaudeCloudSettings()" disabled>Save Claude Cloud settings</button>
6481
+ <a id="claude-cloud-open-link" class="plan-usage-link" href="https://claude.ai/code" target="_blank" rel="noopener noreferrer" style="font-size: var(--fs-meta-sm);">Open claude.ai/code</a>
6482
+ </div>
6483
+ <div class="settings-help">A cloud chat works on what is pushed to GitHub, and its results come back as pushed branches. It has no relay modes, no reasoning effort choice and no mid-turn steering, and it takes images as its only attachments.</div>
6484
+ </div>
6485
+ </div>
5992
6486
  <div
5993
6487
  id="settings-provider-panel-grok"
5994
6488
  class="settings-panel"
@@ -6240,6 +6734,42 @@
6240
6734
  >
6241
6735
  </div>
6242
6736
  <div class="settings-help" style="margin-top: 6px;">When off, agents on paired relays can still read sessions here, but cannot send prompts, start sessions, answer questions or stop turns. The relay token stays the real boundary: anyone holding it has full access.</div>
6737
+ <!-- Agent sessions (agent-sessions-settings-ui.js). Hidden until the relay
6738
+ answers with settings; a relay without the route never shows it. -->
6739
+ <div id="agent-sessions-section" hidden>
6740
+ <div class="settings-row settings-row-column" style="margin-top: 16px; margin-bottom: 0;">
6741
+ <label>Agent sessions</label>
6742
+ </div>
6743
+ <div class="settings-row" style="margin-top: 6px; margin-bottom: 0;">
6744
+ <label for="agent-sessions-enabled-toggle" style="font-weight: normal;">Agents may start and use sessions on this relay</label>
6745
+ <input
6746
+ id="agent-sessions-enabled-toggle"
6747
+ type="checkbox"
6748
+ onchange="toggleAgentSessions(this.checked)"
6749
+ aria-label="Agents may start and use sessions on this relay"
6750
+ disabled
6751
+ >
6752
+ </div>
6753
+ <div class="settings-help" style="margin-top: 6px;">Sessions an agent starts use this relay's accounts. The first one in a conversation asks for your approval. <span id="agent-sessions-cap" hidden></span></div>
6754
+ <div class="settings-row settings-row-column" style="margin-top: 12px; margin-bottom: 0;">
6755
+ <label for="agent-sessions-max-wait-slider">Longest wait per tool call <span id="agent-sessions-max-wait-value">10 min</span></label>
6756
+ <input
6757
+ id="agent-sessions-max-wait-slider"
6758
+ type="range"
6759
+ min="120"
6760
+ max="3600"
6761
+ step="60"
6762
+ value="600"
6763
+ aria-label="Longest wait per tool call"
6764
+ aria-valuetext="10 min"
6765
+ oninput="previewAgentSessionsMaxWait(this.value)"
6766
+ onchange="saveAgentSessionsMaxWait(this.value)"
6767
+ disabled
6768
+ >
6769
+ </div>
6770
+ <div class="settings-help" style="margin-top: 6px;">How long an agent may wait for a session in one call, on this relay or a paired one. It can call again to keep waiting.</div>
6771
+ <div id="agent-sessions-status" class="settings-help settings-provider-status" style="margin-top: 6px;" role="status" aria-live="polite" hidden></div>
6772
+ </div>
6243
6773
  <div id="remote-relays-list-section" class="settings-row settings-row-column" style="margin-top: 16px; margin-bottom: 0;">
6244
6774
  <label>Remote relays</label>
6245
6775
  <div id="remote-relays-list"></div>
@@ -6414,11 +6944,25 @@
6414
6944
  </select>
6415
6945
  <div id="new-conversation-provider-help" style="margin-top:8px;font-size:0.78rem;color:var(--muted);">Copilot models use your GitHub Copilot runtime.</div>
6416
6946
  </div>
6947
+ <!-- Claude Cloud only (journal-view.js): the GitHub repository and branch
6948
+ the cloud session clones, filled from the folder picked above. -->
6949
+ <div id="new-conversation-cloud-row" hidden>
6950
+ <label for="new-conversation-cloud-repo" style="display:block;margin-top:12px;">Repository <button id="new-conversation-cloud-repo-refresh" type="button" class="new-conversation-cloud-refresh" title="Read the repository list again" aria-label="Read the repository list again" onclick="refreshNewConversationCloudRepoList()">↻</button></label>
6951
+ <input id="new-conversation-cloud-repo" type="text" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="new-conversation-cloud-repo-list" inputmode="url" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" placeholder="owner/repo or GitHub URL" aria-describedby="new-conversation-cloud-warnings new-conversation-cloud-error">
6952
+ <div id="new-conversation-cloud-repo-list" class="new-conversation-cloud-suggest" role="listbox" aria-label="Repositories" hidden></div>
6953
+ <label for="new-conversation-cloud-branch" style="display:block;margin-top:12px;">Branch</label>
6954
+ <input id="new-conversation-cloud-branch" type="text" role="combobox" aria-autocomplete="list" aria-expanded="false" aria-controls="new-conversation-cloud-branch-list" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" placeholder="Default branch" aria-describedby="new-conversation-cloud-warnings new-conversation-cloud-error">
6955
+ <div id="new-conversation-cloud-branch-list" class="new-conversation-cloud-suggest" role="listbox" aria-label="Branches" hidden></div>
6956
+ <div id="new-conversation-cloud-warnings" role="status" aria-live="polite"></div>
6957
+ <div id="new-conversation-cloud-error" role="alert" hidden></div>
6958
+ </div>
6417
6959
  <label for="new-conversation-model-select" style="display:block;margin-top:12px;">Model</label>
6418
6960
  <select id="new-conversation-model-select"></select>
6419
- <label for="new-conversation-reasoning-select" style="display:block;margin-top:12px;">Reasoning effort</label>
6420
- <select id="new-conversation-reasoning-select"></select>
6421
- <div id="new-conversation-reasoning-status" style="margin-top:8px;font-size:0.78rem;color:var(--muted);">Choose the effort used when this conversation starts.</div>
6961
+ <div id="new-conversation-reasoning-row">
6962
+ <label for="new-conversation-reasoning-select" style="display:block;margin-top:12px;">Reasoning effort</label>
6963
+ <select id="new-conversation-reasoning-select"></select>
6964
+ <div id="new-conversation-reasoning-status" style="margin-top:8px;font-size:0.78rem;color:var(--muted);">Choose the effort used when this conversation starts.</div>
6965
+ </div>
6422
6966
  <div id="new-conversation-context-row" style="display:none;">
6423
6967
  <label for="new-conversation-context-select" style="display:block;margin-top:12px;">Context window</label>
6424
6968
  <select id="new-conversation-context-select" disabled aria-readonly="true"></select>
@@ -6534,8 +7078,8 @@
6534
7078
 
6535
7079
  <script>
6536
7080
  window.__loadCopilotApp().catch((error) => {
6537
- document.getElementById('startup-loading')?.remove();
6538
7081
  document.getElementById('auth-gate').style.display = 'flex';
7082
+ void window.__startupSplash.toAuth();
6539
7083
  const errorEl = document.getElementById('auth-error');
6540
7084
  if (errorEl) errorEl.textContent = `App failed to initialize: ${String(error?.message || error || 'unknown error')}`;
6541
7085
  });