@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
@@ -0,0 +1,394 @@
1
+ // Claude account management inside Settings → Providers → Claude.
2
+ //
3
+ // The relay drives the CLI's OAuth flow: Relogin starts a login session on the
4
+ // host, the authorize URL shows up here (openable on a phone — that is the
5
+ // point of the relay), and the code the user pastes back completes it.
6
+ //
7
+ // Every visible state comes from the `claude_auth_state` socket broadcast (same
8
+ // payload as GET /api/claude/auth/status), so the flow survives closing the
9
+ // modal, switching tabs, or being finished from a different device. Rendering
10
+ // is therefore idempotent: it reads the latest payload and never assumes a
11
+ // local step sequence.
12
+ //
13
+ // Secret hygiene: the pasted code is read from the input, sent once, and
14
+ // immediately cleared. It is never stored, logged, or put in a broadcast.
15
+
16
+ import {
17
+ escHtml,
18
+ openSummaryModal,
19
+ closeSummaryModal,
20
+ setSummaryModalLoading,
21
+ showTransientRelayNotice,
22
+ } from './store.js';
23
+ import { copyTextToClipboard } from './router.js';
24
+ import {
25
+ getClaudeAuthStatus,
26
+ startClaudeLogin,
27
+ submitClaudeLoginCode,
28
+ cancelClaudeLogin,
29
+ claudeLogout,
30
+ } from './api-client.js';
31
+
32
+ const LOGIN_STATES = ['idle', 'starting', 'awaiting_code', 'exchanging', 'success', 'error'];
33
+ // States where a login session owns the CLI: Relogin/Logout stay disabled.
34
+ const BUSY_LOGIN_STATES = ['starting', 'awaiting_code', 'exchanging'];
35
+ const SUCCESS_COLLAPSE_MS = 2500;
36
+ const COPY_RESET_MS = 1500;
37
+
38
+ const PLAN_LABELS = {
39
+ max: 'Max plan',
40
+ pro: 'Pro plan',
41
+ team: 'Team plan',
42
+ enterprise: 'Enterprise plan',
43
+ free: 'Free plan',
44
+ };
45
+
46
+ let authState = null;
47
+ let lastLoginState = 'idle';
48
+ let successCollapsed = false;
49
+ let successCollapseTimer = null;
50
+ // Client-side failures (request rejected before any broadcast) shown in the
51
+ // login area; cleared by the next real state transition.
52
+ let localError = '';
53
+ let reloginInFlight = false;
54
+ let submitInFlight = false;
55
+ let cancelInFlight = false;
56
+ let logoutInFlight = false;
57
+
58
+ function normalizeLoginState(raw) {
59
+ const value = String(raw || '').trim().toLowerCase();
60
+ return LOGIN_STATES.includes(value) ? value : 'idle';
61
+ }
62
+
63
+ function currentLoginState() {
64
+ return normalizeLoginState(authState?.login?.state);
65
+ }
66
+
67
+ function isLoginBusy() {
68
+ return BUSY_LOGIN_STATES.includes(currentLoginState());
69
+ }
70
+
71
+ function planLabel(status) {
72
+ const subscription = String(status?.subscriptionType || '').trim().toLowerCase();
73
+ if (subscription) {
74
+ return PLAN_LABELS[subscription]
75
+ || `${subscription.charAt(0).toUpperCase()}${subscription.slice(1)} plan`;
76
+ }
77
+ return String(status?.authMethod || '').trim();
78
+ }
79
+
80
+ function accountLineText(status) {
81
+ if (!status) return 'Claude account status unavailable.';
82
+ if (status.loggedIn !== true) {
83
+ const error = String(status.error || '').trim();
84
+ return error ? `Not logged in — ${error}` : 'Not logged in';
85
+ }
86
+ const email = String(status.email || '').trim() || 'unknown account';
87
+ const plan = planLabel(status);
88
+ return plan ? `Account: ${email} · ${plan}` : `Account: ${email}`;
89
+ }
90
+
91
+ function accountLineState(status) {
92
+ if (!status) return 'error';
93
+ if (status.loggedIn === true) return 'active';
94
+ return String(status.error || '').trim() ? 'error' : 'unconfigured';
95
+ }
96
+
97
+ function loginStatusText(login, status) {
98
+ const state = normalizeLoginState(login?.state);
99
+ if (localError && (state === 'idle' || state === 'error')) return localError;
100
+ switch (state) {
101
+ case 'starting':
102
+ return 'Starting login on the relay host…';
103
+ case 'awaiting_code':
104
+ return 'Open the login link, authorize on claude.ai, then paste the code below.';
105
+ case 'exchanging':
106
+ return 'Completing login… this takes a few seconds.';
107
+ case 'success': {
108
+ const email = String(status?.email || '').trim();
109
+ return email ? `Logged in as ${email}.` : 'Logged in.';
110
+ }
111
+ case 'error':
112
+ return String(login?.error || '').trim() || 'Login failed. Press Relogin to try again.';
113
+ default:
114
+ return '';
115
+ }
116
+ }
117
+
118
+ function loginStatusState(login) {
119
+ const state = normalizeLoginState(login?.state);
120
+ if (state === 'success') return 'active';
121
+ if (state === 'error' || (localError && state === 'idle')) return 'error';
122
+ return 'pending';
123
+ }
124
+
125
+ function setHidden(element, hidden) {
126
+ if (element) element.hidden = !!hidden;
127
+ }
128
+
129
+ function setDisabled(element, disabled) {
130
+ if (element) element.disabled = !!disabled;
131
+ }
132
+
133
+ function clearSuccessCollapseTimer() {
134
+ if (successCollapseTimer === null) return;
135
+ clearTimeout(successCollapseTimer);
136
+ successCollapseTimer = null;
137
+ }
138
+
139
+ function clearCodeInput() {
140
+ const input = document.getElementById('claude-auth-code-input');
141
+ if (input) input.value = '';
142
+ }
143
+
144
+ export function renderClaudeAuthSection() {
145
+ const section = document.getElementById('claude-auth-section');
146
+ if (!section) return;
147
+ const status = authState?.status || null;
148
+ const login = authState?.login || {};
149
+ const state = normalizeLoginState(login.state);
150
+
151
+ const account = document.getElementById('claude-auth-account');
152
+ if (account) {
153
+ account.textContent = accountLineText(status);
154
+ account.dataset.state = accountLineState(status);
155
+ }
156
+
157
+ const busy = BUSY_LOGIN_STATES.includes(state);
158
+ setDisabled(document.getElementById('claude-auth-relogin-btn'), busy || reloginInFlight);
159
+ setDisabled(
160
+ document.getElementById('claude-auth-logout-btn'),
161
+ busy || logoutInFlight || status?.loggedIn !== true,
162
+ );
163
+
164
+ // The success line lingers for a beat and then the whole area folds away;
165
+ // `successCollapsed` keeps a later re-render (same sticky payload) from
166
+ // popping it back open.
167
+ const areaVisible = (state !== 'idle' && !(state === 'success' && successCollapsed))
168
+ || (!!localError && state === 'idle');
169
+ setHidden(document.getElementById('claude-auth-login-area'), !areaVisible);
170
+
171
+ const loginStatus = document.getElementById('claude-auth-login-status');
172
+ if (loginStatus) {
173
+ loginStatus.textContent = loginStatusText(login, status);
174
+ loginStatus.dataset.state = loginStatusState(login);
175
+ }
176
+
177
+ const authUrl = String(login.authUrl || '').trim();
178
+ const showUrl = state === 'awaiting_code' && !!authUrl;
179
+ setHidden(document.getElementById('claude-auth-url-row'), !showUrl);
180
+ const link = document.getElementById('claude-auth-url-link');
181
+ if (link) {
182
+ link.href = showUrl ? authUrl : '#';
183
+ link.textContent = showUrl ? authUrl : 'Open the Claude login page';
184
+ }
185
+
186
+ const showCode = state === 'awaiting_code' || state === 'exchanging';
187
+ setHidden(document.getElementById('claude-auth-code-row'), !showCode);
188
+ const codeBusy = state === 'exchanging' || submitInFlight;
189
+ setDisabled(document.getElementById('claude-auth-code-input'), codeBusy);
190
+ setDisabled(document.getElementById('claude-auth-submit-btn'), codeBusy);
191
+ setDisabled(document.getElementById('claude-auth-cancel-btn'), cancelInFlight);
192
+
193
+ if (state === 'success' && !successCollapsed && successCollapseTimer === null) {
194
+ successCollapseTimer = setTimeout(() => {
195
+ successCollapseTimer = null;
196
+ successCollapsed = true;
197
+ renderClaudeAuthSection();
198
+ }, SUCCESS_COLLAPSE_MS);
199
+ }
200
+ }
201
+
202
+ // Single entry point for every payload (GET response, POST response, socket
203
+ // broadcast). Transitions — not local expectations — reset the transient bits.
204
+ // Within one login session (same startedAt) states only ever move forward.
205
+ // Terminal outcomes share the top rank so neither can clobber the other.
206
+ const LOGIN_STATE_RANK = { starting: 1, awaiting_code: 2, exchanging: 3, success: 4, error: 4 };
207
+
208
+ function isStaleLoginPayload(currentLogin, incomingLogin, incomingState) {
209
+ const currentRank = LOGIN_STATE_RANK[normalizeLoginState(currentLogin?.state)] || 0;
210
+ const incomingRank = LOGIN_STATE_RANK[incomingState] || 0;
211
+ if (!currentRank || !incomingRank) return false;
212
+ if (!currentLogin?.startedAt || currentLogin.startedAt !== incomingLogin?.startedAt) return false;
213
+ return incomingRank < currentRank;
214
+ }
215
+
216
+ export function applyClaudeAuthState(payload) {
217
+ if (!payload || typeof payload !== 'object') return authState;
218
+ const login = payload.login && typeof payload.login === 'object' ? payload.login : {};
219
+ const nextState = normalizeLoginState(login.state);
220
+ // A socket broadcast can outrun the HTTP response that triggered it (e.g. the
221
+ // awaiting_code push with the auth URL lands before POST login/start returns
222
+ // its "starting" snapshot). Never let the older snapshot regress the UI.
223
+ if (isStaleLoginPayload(authState?.login, login, nextState)) return authState;
224
+ authState = {
225
+ status: payload.status && typeof payload.status === 'object' ? payload.status : null,
226
+ login: { ...login, state: nextState },
227
+ runningClaudeWorkers: Number(payload.runningClaudeWorkers || 0) || 0,
228
+ };
229
+ if (nextState !== lastLoginState) {
230
+ lastLoginState = nextState;
231
+ localError = '';
232
+ clearSuccessCollapseTimer();
233
+ successCollapsed = false;
234
+ if (nextState !== 'awaiting_code' && nextState !== 'exchanging') clearCodeInput();
235
+ // A fresh login rewrote the host credentials: pull the new account line.
236
+ if (nextState === 'success') void refreshClaudeAuthSection();
237
+ }
238
+ renderClaudeAuthSection();
239
+ return authState;
240
+ }
241
+
242
+ export async function refreshClaudeAuthSection() {
243
+ const payload = await getClaudeAuthStatus();
244
+ if (!payload) {
245
+ const account = document.getElementById('claude-auth-account');
246
+ if (account) {
247
+ account.textContent = 'Unable to load the Claude account status.';
248
+ account.dataset.state = 'error';
249
+ }
250
+ return null;
251
+ }
252
+ return applyClaudeAuthState(payload);
253
+ }
254
+
255
+ export async function startClaudeRelogin() {
256
+ if (reloginInFlight || isLoginBusy()) return;
257
+ reloginInFlight = true;
258
+ localError = '';
259
+ renderClaudeAuthSection();
260
+ try {
261
+ applyClaudeAuthState(await startClaudeLogin());
262
+ } catch (error) {
263
+ localError = String(error?.message || 'Failed to start the Claude login.');
264
+ } finally {
265
+ reloginInFlight = false;
266
+ renderClaudeAuthSection();
267
+ }
268
+ }
269
+
270
+ export async function submitClaudeLoginCodeFromInput() {
271
+ if (submitInFlight) return;
272
+ const input = document.getElementById('claude-auth-code-input');
273
+ const code = String(input?.value || '').trim();
274
+ if (!code) {
275
+ showTransientRelayNotice('Paste the code from claude.ai first.');
276
+ return;
277
+ }
278
+ // Clear before the request so the code never lingers in the DOM.
279
+ if (input) input.value = '';
280
+ submitInFlight = true;
281
+ renderClaudeAuthSection();
282
+ try {
283
+ applyClaudeAuthState(await submitClaudeLoginCode(code));
284
+ } catch (error) {
285
+ localError = String(error?.message || 'The login code was rejected.');
286
+ await refreshClaudeAuthSection().catch(() => {});
287
+ } finally {
288
+ submitInFlight = false;
289
+ renderClaudeAuthSection();
290
+ }
291
+ }
292
+
293
+ export function handleClaudeLoginCodeKey(event) {
294
+ if (event?.key !== 'Enter') return;
295
+ event.preventDefault();
296
+ void submitClaudeLoginCodeFromInput();
297
+ }
298
+
299
+ export async function cancelClaudeRelogin() {
300
+ if (cancelInFlight) return;
301
+ cancelInFlight = true;
302
+ renderClaudeAuthSection();
303
+ try {
304
+ clearCodeInput();
305
+ applyClaudeAuthState(await cancelClaudeLogin());
306
+ } catch (error) {
307
+ localError = String(error?.message || 'Failed to cancel the Claude login.');
308
+ await refreshClaudeAuthSection().catch(() => {});
309
+ } finally {
310
+ cancelInFlight = false;
311
+ renderClaudeAuthSection();
312
+ }
313
+ }
314
+
315
+ export async function copyClaudeLoginUrl() {
316
+ const url = String(authState?.login?.authUrl || '').trim();
317
+ if (!url) return;
318
+ let copied = false;
319
+ try {
320
+ copied = await copyTextToClipboard(url);
321
+ } catch {
322
+ copied = false;
323
+ }
324
+ const button = document.getElementById('claude-auth-copy-btn');
325
+ if (button) {
326
+ button.textContent = copied ? '✓ Copied' : 'Copy failed';
327
+ clearTimeout(button.__claudeAuthCopyTimer);
328
+ button.__claudeAuthCopyTimer = setTimeout(() => {
329
+ button.textContent = 'Copy link';
330
+ }, COPY_RESET_MS);
331
+ }
332
+ // Without a clipboard (plain http on some browsers) at least surface the URL.
333
+ if (!copied) showTransientRelayNotice(url, 10000);
334
+ }
335
+
336
+ export async function openClaudeLogoutConfirmation() {
337
+ if (logoutInFlight) return;
338
+ // Fresh read: the worker count in the warning must not be a stale number.
339
+ let payload = null;
340
+ try {
341
+ payload = await getClaudeAuthStatus();
342
+ } catch {
343
+ payload = null;
344
+ }
345
+ if (payload) applyClaudeAuthState(payload);
346
+ const status = authState?.status || null;
347
+ if (isLoginBusy()) {
348
+ showTransientRelayNotice('Finish or cancel the Claude login in progress first.');
349
+ return;
350
+ }
351
+ if (status?.loggedIn !== true) {
352
+ showTransientRelayNotice('No Claude account is logged in on the relay host.');
353
+ return;
354
+ }
355
+ const email = String(status.email || '').trim() || 'this Claude account';
356
+ const workers = Number(authState?.runningClaudeWorkers || 0) || 0;
357
+ const workerNote = workers > 0
358
+ ? `${workers} Claude worker${workers === 1 ? '' : 's'} ${workers === 1 ? 'is' : 'are'} currently running; `
359
+ + 'they keep their sessions, but new Claude turns will fail until you log in again.'
360
+ : 'No Claude workers are running right now; new Claude turns will fail until you log in again.';
361
+ openSummaryModal({
362
+ title: 'Log out of Claude',
363
+ subtitle: email,
364
+ kind: 'claude-logout',
365
+ bodyHtml: `
366
+ <p>Log out of <strong>${escHtml(email)}</strong>?</p>
367
+ <p>${escHtml(workerNote)}</p>
368
+ <div class="summary-modal-actions">
369
+ <button class="chat-title-action-btn danger-btn" type="button" onclick="confirmClaudeLogout()">Log out</button>
370
+ <button class="chat-title-action-btn" type="button" onclick="closeSummaryModal()">Cancel</button>
371
+ </div>
372
+ `,
373
+ });
374
+ }
375
+
376
+ export async function confirmClaudeLogout() {
377
+ if (logoutInFlight) return;
378
+ logoutInFlight = true;
379
+ setSummaryModalLoading(true);
380
+ try {
381
+ const payload = await claudeLogout();
382
+ closeSummaryModal();
383
+ applyClaudeAuthState(payload);
384
+ showTransientRelayNotice('Logged out of Claude on the relay host.');
385
+ } catch (error) {
386
+ closeSummaryModal();
387
+ alert(error?.message || 'Failed to log out of Claude.');
388
+ await refreshClaudeAuthSection().catch(() => {});
389
+ } finally {
390
+ logoutInFlight = false;
391
+ setSummaryModalLoading(false);
392
+ renderClaudeAuthSection();
393
+ }
394
+ }
@@ -0,0 +1,38 @@
1
+ // Mirrors shared/claude-thinking.mjs, which the browser cannot import (only
2
+ // server/public is served). shared/claude-thinking.test.mjs asserts the two
3
+ // stay identical.
4
+ //
5
+ // Relay defaults: thinking ON, thoughts VISIBLE (summarized). Two states per
6
+ // axis — there is no "host default", because it could only be stored as SQL
7
+ // NULL, which is indistinguishable from "never set" once NULL means the relay
8
+ // default. Turning thinking off applies at the next CLI spawn (the CLI ignores
9
+ // a mid-session disable); everything else applies on the next message.
10
+
11
+ export const DEFAULT_THINKING_ENABLED = true;
12
+
13
+ export const DEFAULT_THINKING_DISPLAY = 'summarized';
14
+
15
+ export const THINKING_DISPLAY_MODES = Object.freeze(['summarized', 'omitted']);
16
+
17
+ export function parseThinkingEnabled(value) {
18
+ if (value === false || value === 0) return false;
19
+ if (typeof value === 'string') {
20
+ const text = value.trim().toLowerCase();
21
+ if (text === 'false' || text === '0' || text === 'off') return false;
22
+ if (text === 'true' || text === '1' || text === 'on') return true;
23
+ return DEFAULT_THINKING_ENABLED;
24
+ }
25
+ if (value === true || value === 1) return true;
26
+ return DEFAULT_THINKING_ENABLED;
27
+ }
28
+
29
+ export function parseThinkingDisplay(value) {
30
+ const text = String(value ?? '').trim().toLowerCase();
31
+ return text === 'omitted' ? 'omitted' : DEFAULT_THINKING_DISPLAY;
32
+ }
33
+
34
+ export function formatThinkingLabel({ enabled = DEFAULT_THINKING_ENABLED, display = DEFAULT_THINKING_DISPLAY } = {}) {
35
+ const enabledLabel = parseThinkingEnabled(enabled) ? 'On' : 'Off';
36
+ const displayLabel = parseThinkingDisplay(display) === 'omitted' ? 'hidden' : 'visible';
37
+ return `${enabledLabel} · ${displayLabel}`;
38
+ }