@oar-sh/oar 0.9.0

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 (293) hide show
  1. package/.github/extensions/web-relay/README.md +190 -0
  2. package/.github/extensions/web-relay/extension.mjs +972 -0
  3. package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
  4. package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
  5. package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
  6. package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
  7. package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
  8. package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
  9. package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
  10. package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
  11. package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
  12. package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
  13. package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
  14. package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
  15. package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
  16. package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
  17. package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
  18. package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
  19. package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
  20. package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
  21. package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
  22. package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
  23. package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
  24. package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
  25. package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
  26. package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
  27. package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
  28. package/CHANGELOG.md +25 -0
  29. package/LICENSE +21 -0
  30. package/README.md +717 -0
  31. package/bin/bat/oar.bat +46 -0
  32. package/bin/oar.js +665 -0
  33. package/package.json +68 -0
  34. package/server/README.md +1379 -0
  35. package/server/claude-worker/claude-attachments.mjs +134 -0
  36. package/server/claude-worker/claude-preview-tool.mjs +57 -0
  37. package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
  38. package/server/claude-worker/claude-session-process.mjs +2391 -0
  39. package/server/claude-worker/claude-session-test-harness.mjs +236 -0
  40. package/server/claude-worker/claude-session-worker.mjs +186 -0
  41. package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
  42. package/server/claude-worker/claude-turn-publisher.mjs +291 -0
  43. package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
  44. package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
  45. package/server/claude-worker/workflow-progress-digest.mjs +269 -0
  46. package/server/copilot-sdk-runtime.mjs +126 -0
  47. package/server/copilot-worker/copilot-attachments.mjs +97 -0
  48. package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
  49. package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
  50. package/server/copilot-worker/copilot-plan-board.mjs +110 -0
  51. package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
  52. package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
  53. package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
  54. package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
  55. package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
  56. package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
  57. package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
  58. package/server/copilot-worker/fixtures/abort-turn.json +588 -0
  59. package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
  60. package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
  61. package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
  62. package/server/copilot-worker/fixtures/happy-turn.json +566 -0
  63. package/server/copilot-worker/fixtures/quota-turn.json +647 -0
  64. package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
  65. package/server/copilot-worker/fixtures/resume-turn.json +700 -0
  66. package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
  67. package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
  68. package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
  69. package/server/cursor-worker/cursor-attachments.mjs +88 -0
  70. package/server/cursor-worker/cursor-context-usage.mjs +85 -0
  71. package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
  72. package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
  73. package/server/cursor-worker/cursor-session-worker.mjs +145 -0
  74. package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
  75. package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
  76. package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
  77. package/server/db-schema.mjs +932 -0
  78. package/server/features.mjs +106 -0
  79. package/server/grok-worker/acp-client.mjs +434 -0
  80. package/server/grok-worker/acp-host-services.mjs +277 -0
  81. package/server/grok-worker/grok-context-usage.mjs +77 -0
  82. package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
  83. package/server/grok-worker/grok-session-worker.mjs +145 -0
  84. package/server/grok-worker/grok-turn-runner.mjs +523 -0
  85. package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
  86. package/server/migrations/0001-add-structured-answer.mjs +80 -0
  87. package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
  88. package/server/migrations/0003-push-subscriptions.mjs +87 -0
  89. package/server/public/app/action-confirmations.js +267 -0
  90. package/server/public/app/activity-replay-state.mjs +149 -0
  91. package/server/public/app/api-client.js +1082 -0
  92. package/server/public/app/ask-user-view.js +545 -0
  93. package/server/public/app/attachments-view.js +2112 -0
  94. package/server/public/app/auto-compact-window-options.mjs +59 -0
  95. package/server/public/app/background-tasks-view.mjs +494 -0
  96. package/server/public/app/bootstrap.js +4347 -0
  97. package/server/public/app/claude-auth-ui.js +394 -0
  98. package/server/public/app/claude-thinking-options.mjs +38 -0
  99. package/server/public/app/cli-install-ui.js +455 -0
  100. package/server/public/app/code-copy.mjs +85 -0
  101. package/server/public/app/composer-attachment-cache.mjs +151 -0
  102. package/server/public/app/composer-control-state.mjs +107 -0
  103. package/server/public/app/composer-paste.mjs +132 -0
  104. package/server/public/app/context-tier-options.mjs +68 -0
  105. package/server/public/app/context-usage-view.mjs +338 -0
  106. package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
  107. package/server/public/app/conversation-preferences.mjs +91 -0
  108. package/server/public/app/conversation-provider-indicator.mjs +88 -0
  109. package/server/public/app/conversation-view.js +2898 -0
  110. package/server/public/app/cwd-menu-placement.mjs +85 -0
  111. package/server/public/app/cwd-picker.js +605 -0
  112. package/server/public/app/emoji-view.js +101 -0
  113. package/server/public/app/external-link-policy.mjs +80 -0
  114. package/server/public/app/file-preview-navigation.mjs +51 -0
  115. package/server/public/app/font-scaling.js +262 -0
  116. package/server/public/app/git-changes-view.js +339 -0
  117. package/server/public/app/git-diff-model.mjs +109 -0
  118. package/server/public/app/grok-auth-ui.js +387 -0
  119. package/server/public/app/image-downscale.mjs +174 -0
  120. package/server/public/app/in-flight-snapshot.mjs +40 -0
  121. package/server/public/app/infinite-loader.js +164 -0
  122. package/server/public/app/journal-view.js +1141 -0
  123. package/server/public/app/known-cwd-options.mjs +41 -0
  124. package/server/public/app/launch-cwd-path.mjs +23 -0
  125. package/server/public/app/live-message-dedupe.mjs +57 -0
  126. package/server/public/app/message-search-view.js +242 -0
  127. package/server/public/app/model-selector-options.mjs +31 -0
  128. package/server/public/app/model-vision-support.mjs +56 -0
  129. package/server/public/app/new-conversation-model-choice.mjs +67 -0
  130. package/server/public/app/plan-usage-view.mjs +307 -0
  131. package/server/public/app/preview-cards.mjs +273 -0
  132. package/server/public/app/preview-command.mjs +103 -0
  133. package/server/public/app/push-settings.js +370 -0
  134. package/server/public/app/pwa-install.js +456 -0
  135. package/server/public/app/question-schema-view.mjs +71 -0
  136. package/server/public/app/reasoning-effort-labels.mjs +42 -0
  137. package/server/public/app/relay-board-view.js +117 -0
  138. package/server/public/app/relay-dot-state.mjs +69 -0
  139. package/server/public/app/relay-error-ctas.mjs +52 -0
  140. package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
  141. package/server/public/app/relay-thoughts.mjs +98 -0
  142. package/server/public/app/relay-watchdog-policy.mjs +29 -0
  143. package/server/public/app/repo-browser-preferences.mjs +79 -0
  144. package/server/public/app/repo-browser-tree-state.mjs +97 -0
  145. package/server/public/app/router.js +657 -0
  146. package/server/public/app/selection-guard.mjs +93 -0
  147. package/server/public/app/settings-modal.js +1484 -0
  148. package/server/public/app/settings-tabs.js +129 -0
  149. package/server/public/app/slash-autocomplete.mjs +188 -0
  150. package/server/public/app/slash-commands.mjs +183 -0
  151. package/server/public/app/socket-handlers.js +703 -0
  152. package/server/public/app/status-store.mjs +242 -0
  153. package/server/public/app/status-view.mjs +315 -0
  154. package/server/public/app/store.js +1454 -0
  155. package/server/public/app/stream-state.mjs +73 -0
  156. package/server/public/app/streaming-dom-patch.mjs +23 -0
  157. package/server/public/app/sync-outbox.mjs +208 -0
  158. package/server/public/app/tap-actions.js +62 -0
  159. package/server/public/app/thread-order.mjs +54 -0
  160. package/server/public/app/tmux-inspector-view.js +422 -0
  161. package/server/public/app/transcript-separators.mjs +282 -0
  162. package/server/public/app-icon-192.png +0 -0
  163. package/server/public/app-icon-512.png +0 -0
  164. package/server/public/app-icon.svg +4 -0
  165. package/server/public/favicon.ico +0 -0
  166. package/server/public/index.html +5743 -0
  167. package/server/public/manifest.webmanifest +32 -0
  168. package/server/public/sw.js +307 -0
  169. package/server/relay-exit-codes.mjs +4 -0
  170. package/server/relay-self-restart.mjs +170 -0
  171. package/server/relay-tools.md +27 -0
  172. package/server/relay.mjs +1026 -0
  173. package/server/repositories/image-conversation-repository.mjs +317 -0
  174. package/server/repositories/message-repository.mjs +434 -0
  175. package/server/repositories/question-repository.mjs +161 -0
  176. package/server/repositories/session-repository.mjs +320 -0
  177. package/server/repositories/share-visibility-statements.mjs +24 -0
  178. package/server/routes/ask-user-routes.mjs +292 -0
  179. package/server/routes/cache-routes.mjs +44 -0
  180. package/server/routes/git-routes.mjs +92 -0
  181. package/server/routes/messages-routes-test-harness.mjs +111 -0
  182. package/server/routes/messages-routes.mjs +7107 -0
  183. package/server/routes/preview-routes.mjs +108 -0
  184. package/server/routes/push-routes.mjs +59 -0
  185. package/server/routes/relay-board-routes.mjs +268 -0
  186. package/server/routes/sessions-routes.mjs +6150 -0
  187. package/server/server-runtime.mjs +7463 -0
  188. package/server/server.js +24 -0
  189. package/server/services/ask-user-routing-service.mjs +117 -0
  190. package/server/services/cache-rebuild-service.mjs +236 -0
  191. package/server/services/claude-auth-service.mjs +578 -0
  192. package/server/services/claude-context-usage.mjs +240 -0
  193. package/server/services/claude-session-root-service.mjs +228 -0
  194. package/server/services/cli-install-service.mjs +1152 -0
  195. package/server/services/cli-process-runner.mjs +181 -0
  196. package/server/services/cloudflared-tunnel-service.mjs +358 -0
  197. package/server/services/context-snapshot-service.mjs +499 -0
  198. package/server/services/context-usage-view.mjs +160 -0
  199. package/server/services/conversation-preferences-service.mjs +208 -0
  200. package/server/services/cursor-dashboard-usage.mjs +217 -0
  201. package/server/services/cursor-session-root-service.mjs +62 -0
  202. package/server/services/delete-archive-service.mjs +154 -0
  203. package/server/services/drives-path-helpers.mjs +66 -0
  204. package/server/services/fixtures/claude-auth-stub.sh +62 -0
  205. package/server/services/fixtures/cli-install-stub.sh +75 -0
  206. package/server/services/fixtures/grok-stub.sh +86 -0
  207. package/server/services/generated-image-cleanup-service.mjs +114 -0
  208. package/server/services/git-changes-service.mjs +195 -0
  209. package/server/services/github-billing-usage.mjs +133 -0
  210. package/server/services/grok-auth-service.mjs +593 -0
  211. package/server/services/grok-billing-usage.mjs +120 -0
  212. package/server/services/image-context-service.mjs +106 -0
  213. package/server/services/image-operation-service.mjs +135 -0
  214. package/server/services/keyed-mutex-service.mjs +82 -0
  215. package/server/services/mime-sniffer.mjs +118 -0
  216. package/server/services/oar-cli-helpers.mjs +109 -0
  217. package/server/services/oar-state-migration-service.mjs +243 -0
  218. package/server/services/plan-usage-claude.mjs +387 -0
  219. package/server/services/plan-usage-contract.mjs +271 -0
  220. package/server/services/plan-usage-copilot.mjs +421 -0
  221. package/server/services/plan-usage-cursor.mjs +566 -0
  222. package/server/services/plan-usage-grok.mjs +436 -0
  223. package/server/services/plan-usage-service.mjs +487 -0
  224. package/server/services/preview-health-probe.mjs +82 -0
  225. package/server/services/preview-proxy-server.mjs +498 -0
  226. package/server/services/preview-registry-service.mjs +457 -0
  227. package/server/services/preview-static-handler.mjs +237 -0
  228. package/server/services/provider-model-selection.mjs +91 -0
  229. package/server/services/provider-reasoning-effort.mjs +94 -0
  230. package/server/services/push-active-device-service.mjs +57 -0
  231. package/server/services/push-dispatch-service.mjs +459 -0
  232. package/server/services/rate-limit-service.mjs +63 -0
  233. package/server/services/relay-bridge-owner-service.mjs +84 -0
  234. package/server/services/relay-cli-launcher-service.mjs +209 -0
  235. package/server/services/relay-prompt-sanitizer.mjs +137 -0
  236. package/server/services/relay-queue-gate-service.mjs +38 -0
  237. package/server/services/relay-restart-orchestrator-service.mjs +935 -0
  238. package/server/services/relay-singleton-guard.mjs +124 -0
  239. package/server/services/safe-served-content.mjs +57 -0
  240. package/server/services/sdk-session-import-service.mjs +271 -0
  241. package/server/services/sdk-session-sync-service.mjs +207 -0
  242. package/server/services/session-history-refresh-service.mjs +497 -0
  243. package/server/services/session-transcript-service.mjs +419 -0
  244. package/server/services/session-worker-launch-service.mjs +786 -0
  245. package/server/services/session-worker-process-service.mjs +319 -0
  246. package/server/services/session-worker-registry-service.mjs +194 -0
  247. package/server/services/session-worker-stop-service.mjs +141 -0
  248. package/server/services/session-worker-supervisor-service.mjs +898 -0
  249. package/server/services/session-worker-websocket-service.mjs +483 -0
  250. package/server/services/ssh-tunnel-manager-service.mjs +423 -0
  251. package/server/services/status-event-service.mjs +158 -0
  252. package/server/services/tmux-inspector-access-policy.mjs +77 -0
  253. package/server/services/tmux-inspector-socket-service.mjs +191 -0
  254. package/server/services/tmux-inspector-stream-service.mjs +552 -0
  255. package/server/services/tunnel-worker-path-guard.mjs +107 -0
  256. package/server/services/upload-sweep.mjs +60 -0
  257. package/server/services/usage-snapshot-helpers.mjs +209 -0
  258. package/server/services/windows-autostart-service.mjs +115 -0
  259. package/server/services/workspace-root-defaults-service.mjs +34 -0
  260. package/server/services/workspace-root-path-policy.mjs +196 -0
  261. package/server/services/workspace-root-relaunch-service.mjs +132 -0
  262. package/server/services/workspace-symlink-guard.mjs +54 -0
  263. package/server/tools/capture-proxy.mjs +173 -0
  264. package/server/tty-console-bootstrap.mjs +75 -0
  265. package/server/uploads/README.md +36 -0
  266. package/server/uploads/image_input.md +540 -0
  267. package/server/windows-terminal-launcher.mjs +29 -0
  268. package/server/workspace-root.mjs +87 -0
  269. package/shared/ask-user-bridge.mjs +131 -0
  270. package/shared/auto-compact-window.mjs +116 -0
  271. package/shared/background-task-timeout.mjs +52 -0
  272. package/shared/claude-thinking.mjs +101 -0
  273. package/shared/context-window-fallbacks.mjs +148 -0
  274. package/shared/control-poller.mjs +115 -0
  275. package/shared/empty-turn-completion.mjs +20 -0
  276. package/shared/model-catalog-freshness.mjs +11 -0
  277. package/shared/model-descriptors.mjs +118 -0
  278. package/shared/model-id.mjs +147 -0
  279. package/shared/model-refresh.mjs +24 -0
  280. package/shared/openai-reasoning.mjs +13 -0
  281. package/shared/plan-lines.mjs +13 -0
  282. package/shared/preview-instructions.mjs +77 -0
  283. package/shared/preview-tool-core.mjs +191 -0
  284. package/shared/provider-routing.mjs +29 -0
  285. package/shared/question-schema.mjs +315 -0
  286. package/shared/question-timeout.mjs +18 -0
  287. package/shared/stream-emit-gating.mjs +28 -0
  288. package/shared/subagent-run-id.mjs +17 -0
  289. package/shared/thought-cap.mjs +13 -0
  290. package/shared/tool-activity.mjs +98 -0
  291. package/shared/turn-ceiling.mjs +53 -0
  292. package/shared/worker-bootstrap.mjs +44 -0
  293. package/shared/worker-crash-guard.mjs +60 -0
package/README.md ADDED
@@ -0,0 +1,717 @@
1
+ # OAR — Open Agent Relay
2
+
3
+ Drive your local coding agents from any browser (phone, tablet, or second computer) through a self-hosted web relay.
4
+
5
+ ```bash
6
+ curl -fsSL oar.sh/install | sh
7
+ ```
8
+
9
+ Landing page and installers: [oar.sh](https://oar.sh). From a git checkout, the Quick start below still applies unchanged.
10
+
11
+ GitHub Copilot CLI is the default runtime, and two more can be enabled per conversation: **OpenAI (BYOK)** and **Claude (Agent SDK)**. All three share the same chat UI, queue, history, file browser, and question cards — you pick the runtime when you start a conversation.
12
+
13
+ ```text
14
+ ┌── Copilot CLI session (default)
15
+ [Browser] <--WebSocket--> [server.js :3333] ┼── OpenAI BYOK worker (your API key)
16
+ └── Claude worker (host's Claude login)
17
+ ```
18
+
19
+ ## In action
20
+
21
+ OAR is built to feel at home on both desktop and mobile. Your conversations can follow you from a browser tab to a PWA install, with file browsing and previews built in.
22
+
23
+ <div align="center">
24
+ <table width="100%" cellspacing="0" cellpadding="0">
25
+ <tr>
26
+ <td rowspan="2" width="40%" align="center" valign="middle">
27
+ <a href="docs/screenshots/mobile_session.jpg" target="_blank" rel="noopener noreferrer">
28
+ <img src="docs/screenshots/mobile_session.jpg" alt="Mobile session view">
29
+ </a>
30
+ <div align="center"><small>Mobile session view with the composer and a PWA-style fullscreen layout.</small></div>
31
+ </td>
32
+ <td width="60%" align="right" valign="top">
33
+ <a href="docs/screenshots/desktop_pwa_background_agents.png" target="_blank" rel="noopener noreferrer">
34
+ <img src="docs/screenshots/desktop_pwa_background_agents.png" alt="Desktop PWA chat view with background agents">
35
+ </a>
36
+ <div align="center"><small>Desktop PWA App (Chrome), chat view with background agents running in parallel.</small></div>
37
+ </td>
38
+ </tr>
39
+ <tr>
40
+ <td width="60%" align="right" valign="bottom">
41
+ <a href="docs/screenshots/desktop_pwa_file_viewer.png" target="_blank" rel="noopener noreferrer">
42
+ <img src="docs/screenshots/desktop_pwa_file_viewer.png" alt="Desktop PWA file viewer">
43
+ </a>
44
+ <div align="center"><small>Integrated file viewer for previews and inline file browsing.</small></div>
45
+ </td>
46
+ </tr>
47
+ </table>
48
+ </div>
49
+
50
+ ## What this repository provides
51
+
52
+ OAR is split into three pieces:
53
+
54
+ 1. **Web relay server** (`server/`): queueing, persistence, auth, browser UI, file browser, uploads, and the OpenAI BYOK image path.
55
+ 2. **Copilot CLI extension** (`.github/extensions/web-relay/`): polls the relay, executes turns, streams activity, bridges `ask_user` questions into web question cards.
56
+ 3. **Claude worker** (`server/claude-worker/`): a per-conversation Node process that runs turns through the Claude Agent SDK and speaks the same relay contracts as the Copilot workers.
57
+
58
+
59
+
60
+ ## Project Status
61
+
62
+ OAR is still under active development, so expect occasional rough edges and some provider SDK features to be missing or incomplete for now.
63
+
64
+
65
+ ## Highlights
66
+
67
+ - Remote chat UI for local coding agents — Copilot CLI, OpenAI (BYOK), Claude (Agent SDK), or Cursor (Agent SDK), chosen per conversation
68
+ - Per-message **mode** picker: `plan`, `ask`, `agent`, `autopilot`
69
+ - Per-message **model** and **reasoning effort** pickers (live model discovery + fallback catalog)
70
+ - Streaming tool/activity updates *and* live assistant reply text while a turn runs
71
+ - Nested **subagent bubbles**: each subagent gets its own live bubble with its own thoughts, activity, and streamed text, kept as collapsible sections after the turn finishes
72
+ - **Background task panel** with live per-task state, model, and token use; Claude workflow tasks fold out into a progress tree of phases and agents, and leave a *Finished background task* card in the transcript when they complete
73
+ - Mathematical and scientific notation rendering for TeX/LaTeX equations and chemical formulas
74
+ - Web question cards for `ask_user` clarification flows (single-field text and multi-field structured forms)
75
+ - Structured answer support: multi-field elicitation with JSON schema validation and UI-rendered forms
76
+ - **Context usage** modal with a per-category token breakdown of the model's context window, plus a per-conversation **auto-compact window** slider for Claude sessions
77
+ - **Transcript breaks**: day separators, a marker where a Claude session auto-compacted its context, and matching dots beside the scrollbar
78
+ - **Plan usage** modal with subscription credits, rate-limit windows and reset countdowns for Copilot, Claude, Cursor, and Grok
79
+ - **Image conversations** (OpenAI BYOK): generate images in chat and iterate on a generated image with *Edit this image*
80
+ - **Share** a conversation by link, with per-message *Hide from shares* control
81
+ - Conversation history stored in local SQLite
82
+ - Conversation delete requests are relayed to Copilot CLI SDK `deleteSession()` so web deletes can remove resumable CLI sessions
83
+ - Conversation **compact** workflow (`/compact`) to continue with summary carry-over
84
+ - Workspace + drives browser with file preview, raw file access, and sticky hidden/heavy filters
85
+ - **Git changes** modal: branch + ahead/behind info, pull, and a per-file diff viewer with *Changes only* / *Full file* modes
86
+ - `@file:` and `@folder:` reference tokens with copy-to-clipboard helpers
87
+ - Uploads and image attachment relay support
88
+ - Optional SSH reverse tunnel support for internet access
89
+ - PWA install support with installed-app fullscreen preference and browser-mode fallbacks
90
+
91
+ ## Prerequisites
92
+
93
+ Only Node.js and the runtime you actually intend to use are required.
94
+
95
+
96
+ | Requirement | Needed for | Notes |
97
+ | ---------------------------- | ----------------------------- | ----------------------------------------- |
98
+ | Node.js 24+ | always | Runs the relay server. Verified on 24; the unit suite requires 24+ (20 and 22 fail — see DEVELOPING.md) |
99
+ | GitHub CLI (`gh`) | Copilot provider | Must be available in PATH |
100
+ | GitHub Copilot CLI extension | Copilot provider | `gh extension install github/gh-copilot` |
101
+ | Copilot subscription | Copilot provider | Individual, Business, or Enterprise |
102
+ | OpenAI API key | OpenAI + OpenAI Image chats | Entered in **⚙️ Settings**, stored locally |
103
+ | Claude Code CLI, logged in | Claude chats | Log in from **⚙️ Settings → Providers → Claude → Relogin**, or run `claude` once on the relay host; no API key is stored by the relay |
104
+
105
+
106
+ ## Quick start
107
+
108
+ ```bash
109
+ git clone https://github.com/oar-sh/oar
110
+ cd oar
111
+ npm install
112
+ ```
113
+
114
+ Create `server/config.json`:
115
+
116
+ ```json
117
+ {
118
+ "authToken": "change-me",
119
+ "port": 3333,
120
+ "localhostOnly": true,
121
+ "pollIntervalMs": 3000,
122
+ "processingTimeoutMs": 600000,
123
+ "conversationSessionMode": "isolated"
124
+ }
125
+ ```
126
+
127
+ Start Copilot with the relay extension:
128
+
129
+ ```bash
130
+ npm run copilot:relay
131
+ ```
132
+
133
+ If you installed the extension globally in `~/.copilot/extensions/web-relay/`, you can also start plain Copilot from any repository:
134
+
135
+ ```bash
136
+ gh copilot
137
+ ```
138
+
139
+ In that setup, the extension auto-starts and supervises `server.js` for the active CLI session; `npm run copilot:relay` is just a convenience launcher for this repository.
140
+
141
+ On Windows, the relay's visible launcher path now targets a stable per-workspace Windows Terminal window name so later foreground launches reuse the same window instead of opening new desktop windows. Use the hidden/stdio fallback only when you explicitly need it.
142
+
143
+ Open:
144
+
145
+ ```text
146
+ http://<your-pc-ip>:3333/
147
+ ```
148
+
149
+ When `localhostOnly` is `true`, use `http://localhost:3333/` from the same machine.
150
+
151
+ Sign in once with your token. The relay then uses an HttpOnly auth cookie.
152
+
153
+ For day-to-day development workflows, relay restart steps, and worker debugging notes, see [`DEVELOPING.md`](./DEVELOPING.md).
154
+
155
+ ## Runtime modes and startup commands
156
+
157
+ | Command | Purpose |
158
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
159
+ | `oar` | Global npm command (`npm i -g @oar-sh/oar`, or `npm link` from a checkout) that starts the web relay if needed, then runs `gh copilot` in the same shell |
160
+ | `oar --install-extension` | Installs/updates a user-global `web-relay` wrapper entrypoint and exits |
161
+ | `npm run copilot:relay` | Starts Copilot CLI with an initial prompt so the extension loads and the relay worker link comes online |
162
+ | `node server/server.js` | The one way to start the relay server. `npm start` is an alias for exactly this command |
163
+
164
+ ### The single server entry point
165
+
166
+ `server/server.js` is the only entry point, in every mode — manual runs, the CLI
167
+ extension, `oar`, Windows autostart, and the e2e runner all start it.
168
+ Its role is chosen by argv, never by the environment:
169
+
170
+ - `node server/server.js` — the process stays attached to your terminal as a
171
+ supervisor and runs the real server (`server-runtime.mjs`) in a worker child it
172
+ marks with `--relay-runtime`. Exit code 75 relaunches the worker; any other
173
+ non-zero exit is retried up to 3 times before the supervisor gives up.
174
+ - `node server/server.js --supervised` — runs the server in this process and
175
+ exits 75 on restart, leaving restarts to whoever spawned it. The CLI extension
176
+ passes this so its own bounded-backoff supervision is the only one in play.
177
+
178
+ Role flags travel on argv because the server's environment is inherited by tmux
179
+ worker sessions and by the Copilot CLI it launches — an env-based flag would be
180
+ read by unrelated servers started further down that tree.
181
+
182
+ ### Single runtime owner rule
183
+
184
+ Run only one relay owner at a time:
185
+
186
+ 1. **Extension-managed mode** (the normal one): the Copilot CLI extension owns the relay worker WebSocket and fallback dequeue loop. `github`/`openai` turns run in Copilot CLI sessions the server launches per conversation.
187
+ 2. **Standalone mode** (manual escape hatch): start the server, then run `node server/relay.mjs` by hand. It spawns its own Copilot CLI process and polls `GET /api/pending` over HTTP. Use it only when the extension transport is unavailable — there is no npm script for it.
188
+
189
+ Do not run extension-managed relay transport together with standalone relay runtime transport.
190
+ Do not restart the web relay unless the user has explicitly given permission.
191
+ If a manual restart is requested, use `POST /api/relay/shutdown` only.
192
+ Do not run tests that spawn Copilot CLI clients unless the user explicitly permits it.
193
+ Unit tests (`npm test`) and e2e tests (`npm run test:e2e`) are isolated from a live relay and may run beside it; see `DEVELOPING.md` → Tests.
194
+
195
+ In extension-managed mode, the worker WebSocket begins after the CLI session becomes active (typically after the first prompt), with HTTP dequeue kept only as fallback when the socket is unavailable.
196
+ The extension now supervises managed `server.js` restarts (bounded backoff) while the CLI session is alive, and stops restart attempts on session shutdown.
197
+ When the CLI extension connects, it also prints the relay info window (local/network/remote/auth URLs) directly in the Copilot CLI client.
198
+
199
+ On Windows, **Settings → Autostart (Windows)** can add a per-user Startup entry. It opens a visible terminal at sign-in and runs the installed `node server\server.js` path. This starts only the web relay server; a Copilot CLI session using the extension must attach separately before queued turns can be processed. Turning the setting off removes the OAR Startup entry.
200
+
201
+ Do not restart the relay by killing processes; use `POST /api/relay/shutdown` instead.
202
+
203
+ Manual relay control details:
204
+
205
+ - `POST /api/relay/shutdown` without `restart` queues a normal relay shutdown.
206
+ - `POST /api/relay/shutdown` with `restart: true` queues an intentional self-restart.
207
+ - Requests are localhost-only and still require relay auth.
208
+ - The relay waits until the queue is idle before acting; this endpoint does not interrupt an in-flight turn.
209
+ - `/api/status` exposes the queued relay exit state as `relayShutdown` so the UI/logs can distinguish idle, queued, and shutting-down restart/shutdown flows.
210
+ - Restart ownership follows the argv role, so exactly one supervisor acts on exit code 75:
211
+ - `node server/server.js` — the attached supervisor respawns its worker child in the same terminal session
212
+ - `node server/server.js --supervised` — the server exits 75 and the CLI extension relaunches it (bounded backoff, and it stops trying once the CLI session shuts down)
213
+
214
+ ### Global npm command (Windows first)
215
+
216
+ You can install the repo locally and get a global `oar` command without publishing:
217
+
218
+ ```powershell
219
+ npm link
220
+ # or
221
+ npm install -g .
222
+ ```
223
+
224
+ Run it from any folder to start the web relay server for that folder's workspace root, then immediately hand the shell to `gh copilot` without a bootstrap prompt. If a relay is already active, the command reuses it and still opens Copilot in the same shell.
225
+
226
+ Relay server output is written to a logfile under `%LOCALAPPDATA%\copilot-remote\logs` (git checkouts) or `%APPDATA%\oar\logs` (global installs) by default (or `COPILOT_WEB_RELAY_LOG_DIR` if you set it), so it stays out of the CLI terminal.
227
+
228
+ If you want custom token/tunnel settings from a specific `server/config.json`, point `COPILOT_WEB_RELAY_CONFIG` at that file before launching. A plain `npm install -g .` does not bundle the repo-local gitignored config file.
229
+
230
+ Manual relay shutdowns are queued via `POST /api/relay/shutdown` and only take effect after the current turn goes idle, so they are not a way to interrupt a turn in progress.
231
+
232
+ Roadmap for later launcher modes:
233
+
234
+ 1. **Option 2**: launch/attach a Copilot CLI session directly.
235
+ 2. **Option 3**: support `oar -- [gh copilot args]` pass-through.
236
+ 3. **Session resume**: add `--session-id=<...>` handoff once the session orchestration contract is defined.
237
+
238
+ ## Using the web UI
239
+
240
+ On startup, the relay imports locally persisted Copilot sessions through the installed Copilot SDK into its database. Session lists, details, sharing, and history refreshes use that database; an unavailable SDK runtime is reported as an import failure and is not replaced with filesystem discovery.
241
+
242
+ - Start a chat with **New Chat**, which asks for the **working directory**, **provider**, **model**, and **reasoning effort** (or **quality** and **size**, for image chats) before the conversation exists. The working directory picker lists the known CWDs (current session, relay workspace, browser folder, recent roots) plus a **Custom path…** entry, defaults to the last directory you picked, and the chosen directory is applied before the session worker first launches. With Copilot as the only provider the provider row is hidden.
243
+ - Choose **mode** and **model** per message in the composer.
244
+ - Use **Compact** to branch to a fresh conversation seeded with summary context.
245
+ - Type **`/preview`** to publish a local dev server or directory on the public preview host without involving the agent: `/preview 5173 [label]`, `/preview ./dist [label]`, `/preview list`, `/preview close`. Agents can do the same via the `preview` tool (Claude/Cursor) or the documented API (see `docs/preview-servers.md`).
246
+ - Use **Browse files** to inspect workspace/drives and open previews. The **Hidden** and **Heavy** toolbar filters are remembered per browser, and a refresh re-opens the folders you had expanded.
247
+ - Click file/folder copy controls to insert `@file:...` / `@folder:...` tokens.
248
+ - Use **🌿 Git changes** in the conversation `⋯` menu to review the workspace repository: the header shows the branch with ahead/behind counts and a **Pull** button, and the list shows every staged, unstaged, and untracked file (deleted files struck through). Clicking a file opens a diff viewer with **Changes only** and **Full file** modes; closing it returns to the still-open list.
249
+ - Answer clarification prompts in relay question cards (from `ask_user`, or Claude's `AskUserQuestion`).
250
+ - Watch the reply arrive: assistant text streams into the pending bubble as it is generated, and any subagent the turn spawns gets its own nested bubble with its own thoughts, activity, and text.
251
+ - Use **Check Usage** (`📊`) in the conversation menu for plan usage across every configured provider: remaining credits, rate-limit windows, reset countdowns, and collapsible cost/token detail. Each provider gets its own tab, opening on the conversation's own provider, and the card names the signed-in account (email and plan) beneath its title. Sources differ per provider:
252
+ - **Copilot** — live quota (AI credits or premium requests, chat, plan), plus per-model/product billed cost when your GitHub token can read personal billing. Conversations running on the experimental SDK engine add a **Last SDK worker turn** section (AI credits actually spent, tokens, model calls, overage) with the model and how long ago it was captured; it is one turn's numbers rather than a running total, and it stops being shown once it is more than seven days old.
253
+ - **Claude** — subscription limit windows (5-hour, weekly, per-model), extra-usage credits, session cost, and local usage attribution. Read from the live session at the end of a turn; the relay never starts a hidden turn to refresh it, so the newest reading is from your last Claude turn.
254
+ - **Cursor** — spend from the Cursor SDK measured against the monthly allowances you enter in Settings, split into the Cursor Models and Other Models pools. Cursor exposes no account API for included pools, so these figures are estimates and the Spending dashboard remains authoritative.
255
+ - **Grok** — per-turn tokens and estimated cost from the agent prompt result (no live plan-quota API over ACP). Optional monthly USD allowance in Settings for an estimated remaining meter; card is hidden when Grok is disabled. Billing: [console.x.ai](https://console.x.ai).
256
+ - Per-reply usage lines are recorded only for Copilot turns — OpenAI, Claude, Cursor, and Grok turns do not consume Copilot premium requests, and no usage line is attached to them.
257
+ - Use the **Context** button for a per-category breakdown of the conversation's context window: a usage bar, a token/percentage table, and free space. Claude sessions report exact SDK categories; Copilot sessions show the coarser system/tools + messages + buffer split, labelled as a lower-bound estimate when the runtime no longer emits full buckets.
258
+ - Claude conversations additionally get an **auto-compact window** slider in that modal. Claude Code compacts a session once it approaches a model-tuned window (around 967k tokens on Opus 5), which is why long conversations rarely compact at all; setting a smaller window makes it happen sooner and keeps turns cheaper. *Auto* hands the choice back to the CLI. The smallest window is 100k, because the CLI silently ignores anything below that and falls back to its own default. The line beneath the slider reports the window actually in force and where it came from — your setting, the model default, or the `CLAUDE_CODE_AUTO_COMPACT_WINDOW` environment override — and fills in once the conversation's first turn completes. The change reaches a running session on its next message.
259
+ - The transcript marks day boundaries, and marks the point where a Claude session compacted its context with the tokens before and after. Both appear as dots beside the scrollbar for the messages currently loaded.
260
+ - Use **Share** in the conversation menu to publish a read-only link. Hover any message and choose **Hide from shares** to keep it out of the shared view without deleting it; hidden messages stay fully visible to you and are marked as hidden.
261
+ - External links in chat open in a new tab with `noopener`/`noreferrer`; workspace file mentions stay in the in-app preview.
262
+ - Workspace browsing follows the selected CLI session's effective CWD. Running sessions keep their learned runtime CWD, menu changes update the next-launch CWD, and chat `cd ...` commands do not retarget the browser.
263
+
264
+ ## Providers
265
+
266
+ The provider is chosen in **New Chat** and then fixed for that conversation: once a conversation has sent its first message it keeps that provider, and its composer is locked to that provider's models. The composer states this above the input — `🔒 Session locked to GitHub Copilot / OpenAI / OpenAI Image / Claude SDK / Cursor SDK models.`, colour-coded per provider. OpenAI sessions also pin one exact model, which the note names in brackets and the model dropdown shows as a disabled `🔒` entry.
267
+
268
+ Turning a provider **off** (or removing the OpenAI key) rebinds conversations that have not yet sent a message back to Copilot, so you are never left with a conversation pointing at a runtime that can no longer start. Conversations already in flight, and conversations belonging to a different provider, are left alone.
269
+
270
+
271
+ | Provider | Enable via | Auth | Notes |
272
+ | --------------------- | --------------------------------------- | ------------------------------------- | --------------------------------------------------------------------- |
273
+ | **Copilot** (default) | always available | your `gh` / Copilot CLI login | Full relay feature set; the only provider that reports Copilot usage. Two engines: the CLI extension (default) or an experimental headless SDK worker |
274
+ | **OpenAI (BYOK)** | ⚙️ Settings → Providers → OpenAI | your API key, stored in the relay DB | Runs the Copilot CLI in BYOK mode against an OpenAI-compatible endpoint |
275
+ | **OpenAI Image (BYOK)** | ⚙️ Settings → Providers → OpenAI | same key | Calls the OpenAI Images API directly; a chat whose replies are images |
276
+ | **Claude (Agent SDK)** | ⚙️ Settings → Providers → Claude | the relay host's logged-in Claude CLI — **switchable from the panel** | No API key is stored; runs a dedicated Node worker per conversation. The CLI itself can be installed/updated from the panel |
277
+ | **Cursor (Agent SDK)** | ⚙️ Settings → Providers → Cursor | your Cursor API key, stored in the relay DB | Runs a dedicated Node worker per conversation through the Cursor Agent SDK |
278
+ | **Grok (CLI ACP)** | ⚙️ Settings → Providers → Grok | the relay host's Grok CLI login — **sign in / out from the panel** | No API key is stored; drives `grok agent stdio` over ACP. The CLI itself can be installed/updated from the panel |
279
+
280
+ ### Installing a provider CLI from the relay
281
+
282
+ Grok and Claude both run as CLIs on the relay host, and the failure mode used to be a dead end: a
283
+ turn fails with *"Grok CLI was not found on PATH"* and the only fix is a shell on the host — the one
284
+ thing the relay exists to avoid. Each provider sub-tab now carries a CLI row at the top:
285
+
286
+ ```
287
+ Grok CLI not installed [ Install ]
288
+ Grok CLI 1.0.13 · ~/.grok/bin/grok · native · up to date [ Update ]
289
+ ```
290
+
291
+ - **Install** asks for confirmation first, naming the exact command and the directory it writes into.
292
+ The commands are the vendors' own one-liners (`curl -fsSL https://x.ai/cli/install.sh | bash`,
293
+ `curl -fsSL https://claude.ai/install.sh | bash`, or their PowerShell equivalents on Windows) and
294
+ they are hardcoded in the relay — nothing you type reaches a shell. They run as the relay user,
295
+ into your home directory, never under sudo.
296
+ - The output streams into the panel live, and the log survives closing the modal or watching from
297
+ another device. One install at a time, relay-wide.
298
+ - When it finishes, the relay resolves the binary, wires it into the environment it launches workers
299
+ with, and remembers it across restarts — **no relay restart is needed**. Sessions already running
300
+ keep the binary they started with.
301
+ - **Update** runs the CLI's own updater (`grok update`, `claude update`), not the install script again.
302
+ - If your Claude was installed with npm into a folder the relay user cannot write, `claude doctor`
303
+ says so and the button becomes **Switch to native installer** — Anthropic's own recommended fix.
304
+ The npm copy stays where it is; the native build takes precedence on PATH.
305
+ - The **Copilot** row is read-only: that CLI is managed with npm on this host, so there is nothing
306
+ the relay could usefully run.
307
+
308
+ When a turn fails because a CLI is missing or signed out, the failed reply itself carries the fix as
309
+ a button — **Install Grok CLI**, **Sign in to Grok**, **Claude settings** — which opens the right
310
+ panel and, for an install, the same confirmation sheet.
311
+
312
+ ### GitHub Copilot — engine choice
313
+
314
+ Copilot conversations can run on either of two engines, chosen in **⚙️ Settings → Providers → Copilot → Copilot engine**. The setting is per relay and applies to newly started conversations; sessions already running keep the engine they started on until their worker restarts.
315
+
316
+ | Engine | What runs | Trade-offs |
317
+ | ------ | --------- | ---------- |
318
+ | **Extension** (default) | The Copilot CLI in a terminal session with the web-relay extension loaded | The engine everything has shipped on. Attach to a live session with the tmux inspector (`tmux attach -t <sdk-session-id>`) |
319
+ | **SDK** (experimental) | A headless Node worker per conversation, driving the CLI's bundled SDK runtime over JSON-RPC | No CLI extension to install or keep in sync — first run needs only a Copilot CLI that is installed and logged in. **No tmux inspector** for those sessions: there is no TUI to attach to |
320
+
321
+ Switching to **SDK** can be refused, and the panel says why in place of the engine description:
322
+
323
+ - *"The Copilot SDK was not found when the relay started (COPILOT_SDK_PATH did not resolve)…"* — no Copilot CLI bundle with an SDK was found. Install or upgrade the GitHub Copilot CLI **and restart the relay**: the launch environment is snapshotted at startup, so a CLI installed since then is not visible yet.
324
+ - *"The SDK engine requires session worker routing, which is disabled on this relay (SESSION_WORKER_ROUTING_ENABLED)…"* — with routing off no SDK worker is ever spawned, so the setting would have no effect.
325
+
326
+ A refusal never changes the stored engine; the select snaps back to the engine the relay actually runs.
327
+
328
+ SDK-engine turns report their own per-turn billing to the relay, which appears as the **Last SDK worker turn** section on the Copilot card in **Check Usage** (see above). The card's meters come from the account-level quota API and are correct for both engines.
329
+
330
+ ### Claude (Agent SDK)
331
+
332
+ Turn on **⚙️ Settings → Providers → Claude → Enable Claude for New Chat model selection**. The relay authenticates through the Claude credentials present on the host machine (`~/.claude`), so there is no key to enter.
333
+
334
+ The same panel manages the account itself. The row at the top names the signed-in account and plan, and:
335
+
336
+ - **Relogin** runs the Claude CLI's login on the relay host and brings the flow to the browser: the authorize link appears inline with a **Copy link** button — open it on any device, authorize on claude.ai, then paste the returned code back into the field. The relay holds no Claude secret; the CLI rewrites the host credentials, and a fresh model discovery runs straight after, so switching accounts needs no relay restart. The flow is pushed over the socket, so you can start it on one device and finish it on another, and closing the modal does not lose it.
337
+ - **Logout** asks for confirmation first and tells you how many Claude workers are running. Running Claude sessions keep the previous account's token until their worker exits; new sessions use the new account.
338
+
339
+ If Claude has never been logged in on the host, running `claude` there once still works as before.
340
+
341
+ Enabling it also runs model discovery against the Agent SDK and adds the discovered `claude-*` model IDs to the pickers. Use **Select Models → Claude SDK** to choose which of them appear in the composer; the configured default model always stays enabled.
342
+
343
+ What Claude conversations support:
344
+
345
+ - Per-message model and reasoning effort (`none`, `low`, `medium`, `high`, `xhigh`, `max`), changeable between turns, plus **Ultracode** on models that support `xhigh` — `xhigh` effort *and* multi-agent workflow orchestration, at a matching jump in token use
346
+ - All four relay modes — `plan` maps to the SDK's plan permission mode and produces a **Plan ready** board, `ask` and `autopilot` adjust the system prompt
347
+ - Image and file attachments (images up to ~5 MB are inlined; larger files are passed as paths for Claude to read)
348
+ - Question cards, thinking/thought streams, live reply streaming, and nested subagent bubbles
349
+ - **Stop** to abort the running turn
350
+ - Background tasks that outlive the reply that started them: the composer's task panel lists them live with their own **Stop**, and an **Ultracode** workflow folds out into a tree of its phases and agents (state, model, tokens). When the workflow finishes, the summarizing reply keeps a collapsed *Finished background task* card holding the final tree, which survives reloads
351
+ - Session continuity across worker restarts — the native Agent SDK session id is stored and resumed
352
+ - Real context-window metrics, reported after each turn
353
+
354
+ Differences from Copilot conversations:
355
+
356
+ - Cancelling one individual subagent works only for backgrounded ones; a subagent running inside the current turn can be stopped only by stopping the whole turn
357
+ - Claude turns are not included in the Copilot usage line, and no usage line is attached to their replies (Claude's own plan limits appear in **Check Usage**)
358
+ - The browsable **Session** root points at the Agent SDK's project directory rather than a Copilot session-state folder
359
+
360
+ ### Cursor (Agent SDK)
361
+
362
+ Turn on **⚙️ Settings → Providers → Cursor**, paste your Cursor API key, and enable it for New Chat model selection. Saving the key runs model discovery and also discovers each model's supported reasoning-effort tiers; use **Select Models → Cursor SDK** to choose which models appear in the composer (the configured default model always stays enabled).
363
+
364
+ What Cursor conversations support:
365
+
366
+ - Per-message model and reasoning effort — effort tiers come from per-model discovery, and `none` means the model's default behavior
367
+ - All four relay modes — `plan` uses the SDK's native plan mode and produces a **Plan ready** board; `ask` and `autopilot` ride as instructions on the message text, injected only when the mode changes
368
+ - Live reply streaming, question cards, and **Stop** to abort the running turn
369
+ - The browsable **Session** root points at the worker's per-session agent store, created on the session's first turn
370
+ - Expired cached agent handles are recreated and retried automatically once — a second auth failure means the API key itself is invalid
371
+
372
+ Like Claude, Cursor turns are not included in the Copilot usage line and no usage line is attached to their replies. Cursor spend is tracked separately in **Check Usage**; set your monthly pool allowances and billing reset day under Settings → Providers → Cursor → Cursor monthly plan allowance.
373
+
374
+ ### Grok (CLI ACP)
375
+
376
+ Turn on **⚙️ Settings → Providers → Grok**. There is no key to enter: the relay drives the Grok CLI
377
+ on the host and uses whatever account that CLI is signed in to (or `XAI_API_KEY` in the host
378
+ environment).
379
+
380
+ The same panel manages both the CLI and the account:
381
+
382
+ - The **Grok CLI** row installs or updates the CLI itself — see
383
+ [Installing a provider CLI from the relay](#installing-a-provider-cli-from-the-relay).
384
+ - **Sign in** starts the CLI's device-code login on the relay host and brings it to the browser: the
385
+ x.ai authorization link appears inline with a **Copy link** button, and the `XXXX-XXXX` code is
386
+ shown next to it so you can check it against what the browser displays. Open the link on any
387
+ device, confirm, and the panel flips to signed-in **by itself** — nothing is pasted back through
388
+ the relay, because the CLI polls x.ai and finishes on its own. Model discovery re-runs straight
389
+ after, so switching accounts needs no relay restart. The flow is pushed over the socket: start it
390
+ on one device, finish it on another, and closing the modal does not lose it.
391
+ - **Sign out** asks for confirmation first and tells you how many Grok workers are running. Running
392
+ sessions keep the previous token until their worker exits.
393
+
394
+ The relay holds no Grok secret: the device code is public by design, and the token is written by the
395
+ CLI straight into `~/.grok/auth.json`. Running `grok login` on the host still works as before.
396
+
397
+ Grok conversations support live reply streaming, thoughts, plan boards, subagent lifecycle chips,
398
+ **Stop**, session resume across worker restarts, per-turn context metrics, and the live weekly quota
399
+ bar in **Check Usage**. The model is fixed per conversation (ACP has no mid-session switch), and
400
+ question cards are not available — the protocol has no ask-user surface.
401
+
402
+ ## Relay modes
403
+
404
+
405
+ | Mode | Behavior |
406
+ | ----------- | ------------------------------------------------------------ |
407
+ | `ask` | Clarification-first behavior before implementation |
408
+ | `plan` | Planning response style (no implementation unless requested) |
409
+ | `agent` | Interactive coding agent behavior |
410
+ | `autopilot` | Action-first behavior; asks only when truly blocking |
411
+
412
+
413
+ ## Models
414
+
415
+ The composer's model picker is the union of every enabled provider's catalog, filtered to the models the active conversation's provider can actually serve:
416
+
417
+ - **Copilot** models come from live snapshot updates published by the active CLI runtime, falling back to a curated set (`claude-sonnet-4.6`, `claude-haiku-4.5`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.3-codex`).
418
+ - **OpenAI (BYOK)** models are discovered from `/v1/models` when the key is saved or re-enabled.
419
+ - **Claude** models are discovered from the Agent SDK when the provider is enabled. Bracketed `[1m]` long-context variants (such as `claude-opus-5[1m]`) do not appear as separate entries; they surface as a 1M option in the composer's context-size dropdown for the base model.
420
+ - **Cursor** models (and their per-model reasoning-effort tiers) are discovered when the API key is saved or the provider is re-enabled.
421
+
422
+ Use **Select Models** to choose which variants show up in the composer. The modal has one tab per runtime — **Copilot**, **OpenAI**, **Claude SDK**, **Cursor SDK** — and each tab lists only the models that runtime serves; there is no cross-runtime switching inside a conversation.
423
+
424
+ Selection is persisted in browser storage and attached per message.
425
+
426
+ ## Settings (⚙️ in the web UI)
427
+
428
+ The modal is organised into four tabs — **General**, **Providers** (with an OpenAI / Claude / Grok / Cursor sub-tab each), **Previews**, and **Notifications** — and reopens on the tab you used last. Most of these settings live in the relay database rather than `server/config.json`, and apply to every browser that connects:
429
+
430
+ | Setting | Default | What it does |
431
+ | -------------------------- | ---------- | ------------------------------------------------------------------------------- |
432
+ | OpenAI API key / model / base URL | — | Enables the OpenAI and OpenAI Image providers |
433
+ | Claude (Agent SDK) | disabled | Enables Claude as a New Chat provider and runs model discovery |
434
+ | Claude account (Relogin / Logout) | host login | Switches the Claude account the relay host's CLI uses, from the browser (see [Claude (Agent SDK)](#claude-agent-sdk)) |
435
+ | Max turn duration | `60 min` | Hard cap on how long one turn may run before the relay requeues it (see below) |
436
+ | Default session workspace root | — | CWD used by sessions that have no configured root |
437
+ | Install app name | — | Label used for future PWA installs (per browser) |
438
+ | Show Suspend host action | on | UI visibility of the **💤 Suspend host** menu action |
439
+ | Autostart (Windows) | off | Adds a per-user Startup entry that launches the relay server at sign-in |
440
+
441
+ ### How a stuck turn is detected
442
+
443
+ Two independent guards, both of which exempt a turn that is waiting on an unanswered question card:
444
+
445
+ 1. **Inactivity** — a turn is only considered stale after `processingTimeoutMs` with no sign of life from its worker. Worker heartbeats name the message they are working on, so a turn that legitimately runs for an hour of tool calls keeps resetting this window.
446
+ 2. **Max turn duration** — an absolute ceiling on elapsed time, set with the Settings slider (0 = no limit, up to 10 hours). This exists purely to catch a worker that has hung while still heartbeating.
447
+
448
+ When either trips, the turn is returned to the queue rather than lost.
449
+
450
+ ## Configuration reference (`server/config.json`)
451
+
452
+
453
+ | Key | Default | Description |
454
+ | -------------------------- | -------------------- | ------------------------------------------------------------------------- |
455
+ | `authToken` | generated if missing | Required for API/UI auth; set explicitly for stable access |
456
+ | `port` | `3333` | HTTP + WebSocket port |
457
+ | `localhostOnly` | `true` | Bind only to loopback (`127.0.0.1`) and disable LAN/WAN access |
458
+ | `pollIntervalMs` | `3000` | CLI heartbeat/poll cadence |
459
+ | `processingTimeoutMs` | `600000` | Inactivity window before a turn is treated as stale (not a cap on turn length) |
460
+ | `ask_user` timeout | `900000` | `ask_user` question wait; edit `shared/question-timeout.mjs` to change it |
461
+ | `conversationSessionMode` | `isolated` | Configured strategy (`isolated` / `shared`) exposed in status |
462
+ | `restartGracefulTimeoutMs` | `8000` | Graceful restart wait before force fallback |
463
+ | `restartShutdownTimeoutMs` | `45000` | Drain timeout while waiting for active queue job completion |
464
+ | `restartSpawnTimeoutMs` | `18000` | Max wait for resume/restart phase per attempt |
465
+ | `restartRebindTimeoutMs` | `20000` | Max wait for rebind/session-sync completion per attempt |
466
+ | `restartMaxAttempts` | `3` | Bounded restart attempts before terminal exhaustion |
467
+ | `restartRetryBackoffMs` | `[1000,3000,7000]` | Deterministic retry backoff schedule in milliseconds |
468
+ | `maxRequeueRetries` | `5` | Queue retry limit for failed processing |
469
+ | `remotePath` | `""` | URL base path when reverse-proxied under a subpath; also drives PWA URLs and socket.io path |
470
+ | `sshTunnel.mode` | `disabled` | Tunnel mode (`disabled` or `managed`) |
471
+ | `sshTunnel.enabled` | `false` | Legacy alias (`true` => `managed`) |
472
+ | `sshTunnel.required` | `false` | Pause dequeue while managed tunnel is disconnected |
473
+ | `sshTunnel.remoteBind` | `loopback` | Remote bind mode for SSH `-R` (`loopback` or `public`) |
474
+ | `sshTunnel.command` | `ssh` | SSH executable path/command |
475
+ | `sshTunnel.user` | — | SSH user |
476
+ | `sshTunnel.host` | — | SSH host |
477
+ | `sshTunnel.remotePort` | — | Remote forwarded port |
478
+ | `sshTunnel.identityFile` | optional | SSH key path (falls back to default agent/key) |
479
+ | `cloudflaredTunnel.mode` | `disabled` | Cloudflare tunnel mode (`disabled` or `managed`) |
480
+ | `cloudflaredTunnel.required` | `false` | Pause dequeue while the managed Cloudflare tunnel is disconnected |
481
+ | `cloudflaredTunnel.token` | — | Tunnel token issued by the router panel |
482
+ | `cloudflaredTunnel.binary` | *(auto)* | `cloudflared` path; defaults to the npm package, then `PATH` |
483
+ | `cloudflaredTunnel.extraArgs` | `[]` | Extra arguments appended to `cloudflared tunnel run` |
484
+ | `tunnelMarkerHeaders` | `[]` | Extra edge-injected headers that mark tunnel traffic (see worker-path guard) |
485
+ | `publicHostnames` | `[]` | Hostnames the relay itself answers on; the preview lane refuses to share one |
486
+ | `previews.enabled` | `false` | Publish local dev servers on a separate listener (see `docs/preview-servers.md`) |
487
+ | `previews.port` | `port + 1` | Loopback port for the preview listener; `0` picks an ephemeral port |
488
+ | `previews.bindHost` | `127.0.0.1` | Bind address; non-loopback needs `previews.allowPublicBind` |
489
+ | `previews.publicBaseUrl` | — | Public base URL on a hostname **different** from the relay's |
490
+ | `previews.allowedTargetHosts` | `[]` | Upstreams allowed beyond loopback (container/VM IPs) |
491
+ | `previews.maxLive` | `8` | Maximum simultaneously published previews |
492
+
493
+
494
+ > Session mismatch recovery is restart-driven: the relay restart orchestrator parks queue work, restarts/rebinds the CLI runtime, and resumes dequeueing after rebind confirmation. The extension no longer attempts in-process session switch APIs from the dequeue/send path.
495
+
496
+ ## Optional remote internet access (SSH tunnel)
497
+
498
+ Configure:
499
+
500
+ ```json
501
+ "sshTunnel": {
502
+ "mode": "managed",
503
+ "required": false,
504
+ "remoteBind": "loopback",
505
+ "user": "ubuntu",
506
+ "host": "relay.example.com",
507
+ "remotePort": 4444,
508
+ "identityFile": "~/.ssh/id_rsa"
509
+ }
510
+ ```
511
+
512
+ `localhostOnly` controls only the local relay listener (`127.0.0.1` vs `0.0.0.0`).
513
+ SSH tunnel exposure is controlled independently by `sshTunnel.remoteBind`.
514
+
515
+ Then reverse proxy on the VPS (example Caddy):
516
+
517
+ ```text
518
+ relay.example.com {
519
+ reverse_proxy localhost:4444
520
+ }
521
+ ```
522
+
523
+ The relay auto-reconnects tunnel drops with exponential backoff.
524
+
525
+ ## Optional remote internet access (Cloudflare Tunnel)
526
+
527
+ An alternative to the SSH tunnel that needs no VPS and no inbound port: the relay
528
+ supervises Cloudflare's `cloudflared` binary, and Cloudflare carries your hostname down
529
+ to `127.0.0.1:3333`. Both modes are independent and may run at the same time.
530
+
531
+ ```json
532
+ "cloudflaredTunnel": {
533
+ "mode": "managed",
534
+ "required": false,
535
+ "token": "<tunnel token>",
536
+ "binary": "",
537
+ "extraArgs": []
538
+ }
539
+ ```
540
+
541
+ Hostname-to-machine bindings are managed remotely in the router panel (see the
542
+ `cpr-router` project), never configured on this machine — paste the token it gives you and
543
+ start the relay. Environment overrides: `COPILOT_CLOUDFLARED_MODE`,
544
+ `COPILOT_CLOUDFLARED_TOKEN`, `COPILOT_CLOUDFLARED_BINARY`.
545
+
546
+ `localhostOnly` stays `true`: `cloudflared` connects outbound and nothing binds publicly.
547
+ The binary resolves from `cloudflaredTunnel.binary`, then the optional `cloudflared` npm
548
+ package, then `PATH`; a managed config with no resolvable binary is reported as a config
549
+ error instead of crashing. Connection drops reconnect with jittered exponential backoff,
550
+ and repeated fast exits (deleted tunnel or bad token) are reported as `auth-or-config`
551
+ instead of hammering Cloudflare.
552
+
553
+ The relay status dot turns **amber** while the Cloudflare tunnel is connected, so it is
554
+ obvious at a glance that the relay is reachable from the internet rather than only from
555
+ this machine. It stays green when no tunnel is configured and grey when the relay itself
556
+ is unreachable; a managed tunnel that has dropped keeps the dot green — the relay still
557
+ answers locally — and reports the drop in the tooltip.
558
+
559
+ ### Session-worker path guard
560
+
561
+ Any public tunnel forwards *every* path on the bound hostname to port `3333`, including
562
+ the internal session-worker WebSocket endpoints. Requests to those paths that carry an
563
+ edge marker header (`cf-ray`, plus anything listed in `tunnelMarkerHeaders`) are rejected
564
+ with `403` on both the request and upgrade paths. Local workers connect over `127.0.0.1`
565
+ without such a header and are unaffected, and shared conversation links keep working
566
+ anonymously over the tunnel.
567
+
568
+ Shared links work unchanged through Cloudflare. Do not add a Cloudflare Cache Rule
569
+ covering `/api/shared/*` — shared views poll for liveness and an edge-cached response
570
+ would pin viewers to a stale snapshot. Likewise, a Cloudflare Access application over the
571
+ bound hostname breaks share links unless it bypasses `/shared/*` and `/api/shared/*`.
572
+
573
+ ## Global extension install (optional)
574
+
575
+ Install a user-global extension entrypoint for use across repositories:
576
+
577
+ ```text
578
+ %USERPROFILE%\.copilot\extensions\web-relay\ (Windows)
579
+ ~/.copilot/extensions/web-relay/ (Linux/macOS)
580
+ ```
581
+
582
+ Recommended command:
583
+
584
+ ```bash
585
+ oar --install-extension
586
+ ```
587
+
588
+ This writes/updates `extension.mjs` in the user-global extension directory as a wrapper that imports the repository extension entrypoint directly.
589
+ The wrapper also avoids double-loading when you start Copilot from this repository itself, so the
590
+ project-local extension remains the single runtime owner in repo-root sessions.
591
+
592
+ Useful environment variables:
593
+
594
+ - `COPILOT_WEB_RELAY_SERVER_DIR` (recommended)
595
+ - `COPILOT_WEB_RELAY_ROOT`
596
+ - `COPILOT_WEB_RELAY_CONFIG`
597
+ - `COPILOT_WEB_RELAY_TOOLS`
598
+ - `COPILOT_WEB_RELAY_LOG_DIR`
599
+ - `COPILOT_WEB_RELAY_NODE`
600
+
601
+ Project-local extension files still take precedence when both exist.
602
+
603
+ If the same extension is available both project-local (`.github/extensions/web-relay/`) and user-global (`~/.copilot/extensions/web-relay/`), Copilot may show duplicates in extension management. Keep only one active copy to avoid double-loading.
604
+
605
+ ## API overview
606
+
607
+ Common routes:
608
+
609
+ - Browser/API: `/api/message`, `/api/conversations`, `/api/conversation/:id`, `/api/status`, `/api/models`, `/api/usage`, `/api/context/:conversationId`
610
+ - Settings: `/api/settings/openai`, `/api/settings/claude`, `/api/settings/grok`, `/api/settings/cursor`, `/api/settings/copilot`, `/api/settings/turn-ceiling`, `/api/settings/windows-autostart`
611
+ - Relay control: `/api/relay/shutdown`, `/api/relay/pause`, `/api/relay/resume`
612
+ - Worker bridge: `/api/pending`, `/api/response`, `/api/activity`, `/api/stream`, `/api/thought`, `/api/heartbeat`
613
+ - Claude worker: `/api/claude-native-session`, `/api/claude-context-usage`, `/api/claude-plan-usage`
614
+ - Claude account auth: `/api/claude/auth/status`, `/api/claude/auth/login/start`, `/api/claude/auth/login/code`, `/api/claude/auth/login/cancel`, `/api/claude/auth/logout`
615
+ - Grok account auth: `/api/grok/auth/status`, `/api/grok/auth/login/start`, `/api/grok/auth/login/cancel`, `/api/grok/auth/logout`
616
+ - Provider CLI install: `/api/cli/status`, `/api/cli/install`, `/api/cli/install/cancel`
617
+ - Cursor worker: `/api/cursor-agent-id`, `/api/cursor-context-usage`, `/api/cursor-plan-usage`
618
+ - Questions: `/api/relay-question`, `/api/relay-question/:id`, `/api/relay-question/:id/answer`
619
+ - Sharing: `/api/conversation/:id/share`, `/api/conversation/:id/message/:messageId/share-visibility`, `/api/shared/:token`
620
+ - Images: `/api/openai/images/generate`, `/api/image-operations/:operationId/execute`, `/api/generated-image/:conversationId/:messageId/:imageId/content`
621
+ - File access: `/api/files/*`, `/api/files-preview/*`, `/api/repo/tree`, `/api/drives/*`
622
+ - Git: `/api/git/status`, `/api/git/diff`, `/api/git/pull`
623
+ - Previews: `/api/previews`, `/api/previews/:token` (publish a local dev server; see `docs/preview-servers.md`)
624
+ - Uploads: `/api/upload`, `/api/upload/:sha256/content`
625
+
626
+ All authenticated routes accept either:
627
+
628
+ - `Authorization: Bearer <token>`
629
+ - auth cookie from prior login
630
+
631
+ For deeper implementation/API details, see `[server/README.md](server/README.md)`.
632
+
633
+ ## Troubleshooting
634
+
635
+
636
+ | Symptom | What to check |
637
+ | ---------------------------------- | -------------------------------------------------------------------------------- |
638
+ | UI says CLI offline | Send one CLI prompt to trigger extension session start, then check `/api/status` |
639
+ | Messages stuck pending | Ensure only one relay owner is running and only one process owns port `3333` |
640
+ | Wrong/old model shown | Check `/api/models` and extension logs for model snapshot updates |
641
+ | Clarification card not progressing | Answer via the web card; relay resumes after question status becomes `answered` |
642
+ | File links fail | Verify auth token/cookie and that paths are inside allowed workspace/drive roots |
643
+ | Claude missing from New Chat | Enable it in **⚙️ Settings → Providers → Claude**; the toggle is off by default |
644
+ | Claude reply says it cannot authenticate | Press **Claude settings** on the failed reply, then **Relogin** (or run `claude` on the relay host), and retry the turn |
645
+ | Grok reply says the CLI was not found | Press **Install Grok CLI** on the failed reply, or install it from **⚙️ Settings → Providers → Grok**; no relay restart is needed afterwards |
646
+ | Grok reply says authentication failed | Press **Sign in to Grok** on the failed reply and confirm the device code in a browser |
647
+ | Claude model list empty or stale | Re-save the Claude settings, or use **Select Models → Refresh** to rerun discovery |
648
+ | Long turn requeued unexpectedly | Raise or clear **Max turn duration** in Settings (0 = no limit) |
649
+ | No usage line under a reply | Expected for OpenAI, Claude, and Cursor turns; only Copilot turns record plan usage |
650
+
651
+
652
+ ## Security notes
653
+
654
+ - Auth is token-based and enforced on API + Socket.IO.
655
+ - Successful auth sets an HttpOnly cookie for browser sessions.
656
+ - Keep `server/config.json` private and rotate `authToken` if exposed.
657
+ - Set `localhostOnly` to `true` to force local-only access (no LAN/WAN listener).
658
+ - If exposed beyond LAN, use HTTPS and a strong token.
659
+
660
+ ## Repository layout
661
+
662
+ ```text
663
+ oar/
664
+ ├── .github/extensions/web-relay/ # Copilot CLI extension (worker WebSocket, ask_user bridge, model snapshotting)
665
+ ├── server/
666
+ │ ├── claude-worker/ # Claude Agent SDK session worker (turn runner, ask-user bridge, attachments)
667
+ │ ├── cursor-worker/ # Cursor Agent SDK session worker (turn runner, mode nudges, auth retry)
668
+ │ ├── public/app/ # Browser app modules
669
+ │ ├── routes/ # Express route registration
670
+ │ ├── services/ # Relay services (workers, context usage, images, tunnels)
671
+ │ └── server.js # Express + Socket.IO relay server
672
+ ├── shared/ # Code shared by server, extension, and workers
673
+ ├── docs/ # Project planning notes
674
+ └── README.md
675
+ ```
676
+
677
+ ## Extra screenshots
678
+
679
+ More views from the same app experience:
680
+
681
+ <div align="center">
682
+ <table width="100%" cellspacing="0" cellpadding="0">
683
+ <tr>
684
+ <td rowspan="2" width="50%" align="center" valign="middle">
685
+ <a href="docs/screenshots/mobile_session_input.jpg" target="_blank" rel="noopener noreferrer">
686
+ <img src="docs/screenshots/mobile_session_input.jpg" alt="Mobile session composer">
687
+ </a>
688
+ <div align="center"><small>Mobile chat composer in portrait, with the keyboard open and the input ready to send.</small></div>
689
+ </td>
690
+ <td width="50%" align="right" valign="top">
691
+ <a href="docs/screenshots/desktop_pwa_chrome_portrait.png" target="_blank" rel="noopener noreferrer">
692
+ <img src="docs/screenshots/desktop_pwa_chrome_portrait.png" alt="Desktop PWA portrait view">
693
+ </a>
694
+ <div align="center"><small>Desktop PWA running in portrait mode, sized for a narrow browser window.</small></div>
695
+ </td>
696
+ </tr>
697
+ <tr>
698
+ <td width="50%" align="right" valign="bottom">
699
+ <a href="docs/screenshots/desktop_pwa_workspace_file_explorer.png" target="_blank" rel="noopener noreferrer">
700
+ <img src="docs/screenshots/desktop_pwa_workspace_file_explorer.png" alt="Workspace file explorer">
701
+ </a>
702
+ <div align="center"><small>Workspace file explorer with folder browsing and file previews.</small></div>
703
+ </td>
704
+ </tr>
705
+ </table>
706
+
707
+ <table width="50%" cellspacing="0" cellpadding="0">
708
+ <tr>
709
+ <td align="center">
710
+ <a href="docs/screenshots/mobile_file_viewer.jpg" target="_blank" rel="noopener noreferrer">
711
+ <img src="docs/screenshots/mobile_file_viewer.jpg" alt="Mobile file viewer">
712
+ </a>
713
+ <div align="center"><small>Mobile (PWA/Browser) file viewer showing an image preview and download actions.</small></div>
714
+ </td>
715
+ </tr>
716
+ </table>
717
+ </div>