@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,242 @@
1
+ const DB_NAME = 'copilot-remote-client-status';
2
+ const DB_VERSION = 1;
3
+ const STORE_NAME = 'events';
4
+ const MAX_EVENTS = 300;
5
+ export const STATUS_EVENT_PAGE_SIZE = 40;
6
+
7
+ let dbPromise = null;
8
+ let memoryEvents = [];
9
+ let nextMemoryId = 1;
10
+ const listeners = new Set();
11
+ let lastRelayOnline = null;
12
+ let lastCliOnline = null;
13
+ let diagnosticsInitialized = false;
14
+ let nativeConsole = null;
15
+
16
+ function emit(event) {
17
+ for (const listener of listeners) {
18
+ try {
19
+ listener(event);
20
+ } catch {
21
+ // Status listeners must not interfere with the application.
22
+ }
23
+ }
24
+ }
25
+
26
+ function sanitize(value, depth = 0) {
27
+ if (depth > 3) return '[Max depth reached]';
28
+ if (value instanceof Error) {
29
+ return { name: value.name, message: value.message, stack: value.stack || '' };
30
+ }
31
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
32
+ if (typeof value === 'number') return Number.isFinite(value) ? value : String(value);
33
+ if (typeof value === 'bigint') return `${value}n`;
34
+ if (typeof value === 'function') return `[Function ${value.name || 'anonymous'}]`;
35
+ if (Array.isArray(value)) return value.slice(0, 20).map((item) => sanitize(item, depth + 1));
36
+ if (typeof value === 'object') {
37
+ const output = {};
38
+ for (const [key, item] of Object.entries(value).slice(0, 30)) {
39
+ output[String(key).slice(0, 80)] = sanitize(item, depth + 1);
40
+ }
41
+ return output;
42
+ }
43
+ return String(value);
44
+ }
45
+
46
+ function openDatabase() {
47
+ if (dbPromise) return dbPromise;
48
+ if (!globalThis.indexedDB) return Promise.resolve(null);
49
+ dbPromise = new Promise((resolve, reject) => {
50
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
51
+ request.onerror = () => reject(request.error || new Error('Could not open client status storage'));
52
+ request.onupgradeneeded = () => {
53
+ const database = request.result;
54
+ if (!database.objectStoreNames.contains(STORE_NAME)) {
55
+ database.createObjectStore(STORE_NAME, { keyPath: 'id' });
56
+ }
57
+ };
58
+ request.onsuccess = () => resolve(request.result);
59
+ }).catch(() => null);
60
+ return dbPromise;
61
+ }
62
+
63
+ function memoryInsert(event) {
64
+ memoryEvents.push(event);
65
+ if (memoryEvents.length > MAX_EVENTS) memoryEvents = memoryEvents.slice(-MAX_EVENTS);
66
+ }
67
+
68
+ async function persistEvent(event) {
69
+ const database = await openDatabase();
70
+ if (!database) return;
71
+ await new Promise((resolve, reject) => {
72
+ const transaction = database.transaction(STORE_NAME, 'readwrite');
73
+ const store = transaction.objectStore(STORE_NAME);
74
+ store.put(event);
75
+ const cursorRequest = store.openCursor();
76
+ const ids = [];
77
+ cursorRequest.onsuccess = () => {
78
+ const cursor = cursorRequest.result;
79
+ if (!cursor) {
80
+ ids.slice(0, Math.max(0, ids.length - MAX_EVENTS)).forEach((id) => store.delete(id));
81
+ return;
82
+ }
83
+ ids.push(cursor.primaryKey);
84
+ cursor.continue();
85
+ };
86
+ transaction.oncomplete = resolve;
87
+ transaction.onerror = () => reject(transaction.error || new Error('Could not persist client status event'));
88
+ }).catch(() => {});
89
+ }
90
+
91
+ export async function loadStatusEvents() {
92
+ const database = await openDatabase();
93
+ if (!database) return memoryEvents.slice();
94
+ const events = await new Promise((resolve) => {
95
+ const request = database.transaction(STORE_NAME, 'readonly').objectStore(STORE_NAME).getAll();
96
+ request.onsuccess = () => resolve(Array.isArray(request.result) ? request.result : []);
97
+ request.onerror = () => resolve([]);
98
+ });
99
+ return events.sort((a, b) => Number(a.timestamp || 0) - Number(b.timestamp || 0)).slice(-MAX_EVENTS);
100
+ }
101
+
102
+ function compareStatusEvents(left, right) {
103
+ const timestampDelta = Number(left?.timestamp || 0) - Number(right?.timestamp || 0);
104
+ if (timestampDelta !== 0) return timestampDelta;
105
+ return String(left?.id || '').localeCompare(String(right?.id || ''));
106
+ }
107
+
108
+ export function mergeStatusEvents(existing = [], incoming = []) {
109
+ const merged = new Map();
110
+ for (const event of [...existing, ...incoming]) {
111
+ const id = String(event?.id || '').trim();
112
+ if (!id) continue;
113
+ merged.set(id, event);
114
+ }
115
+ return Array.from(merged.values()).sort(compareStatusEvents);
116
+ }
117
+
118
+ export async function loadStatusEventPage({ before = null, limit = STATUS_EVENT_PAGE_SIZE } = {}) {
119
+ const events = await loadStatusEvents();
120
+ const cursorTimestamp = Number(before?.timestamp || 0);
121
+ const cursorId = String(before?.id || '');
122
+ const eligible = before
123
+ ? events.filter((event) => (
124
+ Number(event.timestamp || 0) < cursorTimestamp
125
+ || (Number(event.timestamp || 0) === cursorTimestamp && String(event.id || '').localeCompare(cursorId) < 0)
126
+ ))
127
+ : events;
128
+ const pageSize = Math.max(1, Math.min(STATUS_EVENT_PAGE_SIZE, Number(limit) || STATUS_EVENT_PAGE_SIZE));
129
+ const items = eligible.slice(-pageSize).sort(compareStatusEvents);
130
+ const first = items[0] || null;
131
+ return {
132
+ items,
133
+ hasMore: eligible.length > items.length,
134
+ nextCursor: first ? { timestamp: first.timestamp, id: first.id } : null,
135
+ };
136
+ }
137
+
138
+ export async function clearStatusEvents() {
139
+ memoryEvents = [];
140
+ const database = await openDatabase();
141
+ if (database) {
142
+ await new Promise((resolve) => {
143
+ const request = database.transaction(STORE_NAME, 'readwrite').objectStore(STORE_NAME).clear();
144
+ request.onsuccess = resolve;
145
+ request.onerror = resolve;
146
+ });
147
+ }
148
+ emit({ type: 'status-cleared', timestamp: Date.now() });
149
+ }
150
+
151
+ export function subscribeStatusEvents(listener) {
152
+ if (typeof listener !== 'function') return () => {};
153
+ listeners.add(listener);
154
+ return () => listeners.delete(listener);
155
+ }
156
+
157
+ export function recordStatusEvent(type, details = {}) {
158
+ const event = {
159
+ id: `status-${Date.now()}-${nextMemoryId++}`,
160
+ timestamp: Date.now(),
161
+ type: String(type || 'event'),
162
+ details: sanitize(details),
163
+ };
164
+ memoryInsert(event);
165
+ void persistEvent(event);
166
+ emit(event);
167
+ return event;
168
+ }
169
+
170
+ export function publishStatusEvent(event) {
171
+ const id = String(event?.id || '').trim();
172
+ if (!id) return null;
173
+ const published = {
174
+ ...event,
175
+ id,
176
+ timestamp: Number(event.timestamp) || Date.now(),
177
+ type: String(event.type || 'event'),
178
+ source: String(event.source || 'server'),
179
+ details: sanitize(event.details || {}),
180
+ };
181
+ emit(published);
182
+ return published;
183
+ }
184
+
185
+ export function recordRelayLifecycleEvent(online) {
186
+ const nextOnline = !!online;
187
+ if (lastRelayOnline === nextOnline) return;
188
+ const previous = lastRelayOnline;
189
+ lastRelayOnline = nextOnline;
190
+ recordStatusEvent(nextOnline ? 'relay-connected' : 'relay-unreachable', {
191
+ previous,
192
+ online: nextOnline,
193
+ });
194
+ }
195
+
196
+ export function recordCliLifecycleEvent(online) {
197
+ const nextOnline = !!online;
198
+ if (lastCliOnline === nextOnline) return;
199
+ const previous = lastCliOnline;
200
+ lastCliOnline = nextOnline;
201
+ recordStatusEvent(nextOnline ? 'cli-connected' : 'cli-unreachable', {
202
+ previous,
203
+ online: nextOnline,
204
+ });
205
+ }
206
+
207
+ function formatConsoleArguments(args) {
208
+ return args.slice(0, 12).map((value) => sanitize(value));
209
+ }
210
+
211
+ export function initClientDiagnostics() {
212
+ if (diagnosticsInitialized) return;
213
+ diagnosticsInitialized = true;
214
+ nativeConsole = {
215
+ log: console.log.bind(console),
216
+ warn: console.warn.bind(console),
217
+ error: console.error.bind(console),
218
+ };
219
+ for (const level of ['log', 'warn', 'error']) {
220
+ const original = nativeConsole[level];
221
+ console[level] = (...args) => {
222
+ original(...args);
223
+ recordStatusEvent(`console-${level}`, { arguments: formatConsoleArguments(args) });
224
+ };
225
+ }
226
+ window.addEventListener('error', (event) => {
227
+ recordStatusEvent('client-error', {
228
+ message: event.message,
229
+ source: event.filename,
230
+ line: event.lineno,
231
+ column: event.colno,
232
+ error: event.error,
233
+ });
234
+ });
235
+ window.addEventListener('unhandledrejection', (event) => {
236
+ recordStatusEvent('unhandled-rejection', { reason: event.reason });
237
+ });
238
+ }
239
+
240
+ export function getNativeConsole() {
241
+ return nativeConsole || console;
242
+ }
@@ -0,0 +1,315 @@
1
+ import {
2
+ activeRuntimeSessionCount,
3
+ runtimeSessionBindingCount,
4
+ cliOnline,
5
+ currentConvId,
6
+ escHtml,
7
+ relayOnline,
8
+ sessionWorkerStates,
9
+ setCurrentConv,
10
+ } from './store.js';
11
+ import { createInfiniteLoader } from './infinite-loader.js';
12
+ import { loadServerStatusEventPage } from './api-client.js';
13
+ import {
14
+ clearStatusEvents,
15
+ loadStatusEventPage,
16
+ mergeStatusEvents,
17
+ subscribeStatusEvents,
18
+ } from './status-store.mjs';
19
+
20
+ export const STATUS_CONVERSATION_ID = '__client_status__';
21
+ const STATUS_SCROLL_STORAGE_KEY = 'copilot_status_history_scroll';
22
+ const STATUS_BOTTOM_THRESHOLD_PX = 24;
23
+
24
+ let statusViewActive = false;
25
+ let previousConversationId = '';
26
+ let events = [];
27
+ let unsubscribe = null;
28
+ let statusViewGeneration = 0;
29
+ let statusRenderFrame = 0;
30
+ let statusScrollBound = false;
31
+ let statusLinkBound = false;
32
+
33
+ function isNearStatusBottom(host) {
34
+ return !!host && (host.scrollHeight - host.clientHeight - host.scrollTop) <= STATUS_BOTTOM_THRESHOLD_PX;
35
+ }
36
+
37
+ function formatValue(value) {
38
+ if (value === null || value === undefined) return '';
39
+ if (typeof value === 'string') return value;
40
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
41
+ if (value?.message) return String(value.message);
42
+ try {
43
+ return JSON.stringify(value);
44
+ } catch {
45
+ return String(value);
46
+ }
47
+ }
48
+
49
+ function formatStatusEvent(event) {
50
+ const details = event?.details || {};
51
+ const type = String(event?.type || 'event');
52
+ if (type.startsWith('console-')) {
53
+ return (details.arguments || []).map(formatValue).filter(Boolean).join(' ');
54
+ }
55
+ if (type === 'relay-connected') return 'Web relay connected';
56
+ if (type === 'relay-unreachable') return 'Web relay unreachable';
57
+ if (type === 'cli-connected') return 'CLI connected';
58
+ if (type === 'cli-unreachable') return 'CLI offline';
59
+ if (type === 'relay-reconnect-forced') return 'Web relay reconnect forced by watchdog';
60
+ if (type === 'foreground-recovery-skipped') {
61
+ return `Foreground recovery skipped, one already running (${details.reason || 'unknown'})`;
62
+ }
63
+ if (type === 'foreground-recovery-timeout') {
64
+ return `Foreground recovery timed out after ${Math.round(Number(details.timeoutMs || 0) / 1000)}s (${details.reason || 'unknown'})`;
65
+ }
66
+ if (type === 'push-dispatched') {
67
+ const failures = Number(details.failed || 0);
68
+ return `Push sent for ${details.eventType || 'event'} to ${Number(details.sent || 0)} device(s)${failures ? `, ${failures} failed` : ''}`;
69
+ }
70
+ if (type === 'push-suppressed') {
71
+ return `Push suppressed for ${details.eventType || 'event'} (a device is active)`;
72
+ }
73
+ if (type === 'push-subscription-pruned') {
74
+ return `Push subscription removed (${details.label || details.deviceId || 'unknown device'}${details.statusCode ? `, HTTP ${details.statusCode}` : ''}${details.failureCount ? `, ${details.failureCount} consecutive failures` : ''})`;
75
+ }
76
+ if (type === 'client-error') return `Error: ${formatValue(details.message || details.error)}`;
77
+ if (type === 'unhandled-rejection') return `Unhandled rejection: ${formatValue(details.reason)}`;
78
+ if (type === 'shared-access-opened') {
79
+ return `Shared conversation opened (${details.shareId || 'unknown share'}) from ${details.viewerIp || 'unknown'}`;
80
+ }
81
+ return formatValue(details);
82
+ }
83
+
84
+ function formatStatusEventHtml(event) {
85
+ const details = event?.details || {};
86
+ const type = String(event?.type || 'event');
87
+ if (type === 'shared-access-opened') {
88
+ const shareId = String(details.shareId || 'unknown share').trim() || 'unknown share';
89
+ const viewerIp = String(details.viewerIp || 'unknown').trim() || 'unknown';
90
+ const conversationId = String(details.conversationId || '').trim();
91
+ const conversationAttr = conversationId
92
+ ? ` data-conversation-id="${escHtml(conversationId)}"`
93
+ : '';
94
+ return `Shared conversation opened (<a href="#" class="status-share-link"${conversationAttr}>${escHtml(shareId)}</a>) from ${escHtml(viewerIp)}`;
95
+ }
96
+ return escHtml(formatStatusEvent(event));
97
+ }
98
+
99
+ function renderStatusHeader() {
100
+ if (!statusViewActive) return;
101
+ const title = document.getElementById('chat-title');
102
+ const metadata = document.getElementById('chat-title-cwd');
103
+ const usage = document.getElementById('chat-title-session-usage');
104
+ if (title) {
105
+ title.innerHTML = `Web Relay Status: <span class="${relayOnline ? 'status-online' : 'status-offline'}">${relayOnline ? 'Reachable' : 'Unreachable'}</span> | CLI: <span class="${cliOnline ? 'status-online' : 'status-offline'}">${cliOnline ? 'Online' : 'Offline'}</span>`;
106
+ }
107
+ if (metadata) {
108
+ const allWorkers = Array.from(sessionWorkerStates.values());
109
+ const assignedOnline = Number(activeRuntimeSessionCount || 0);
110
+ const onlineTotal = allWorkers.reduce((count, worker) => {
111
+ const status = String(worker?.status || '').trim().toLowerCase();
112
+ return count + ((status === 'starting' || status === 'ready' || status === 'processing') ? 1 : 0);
113
+ }, 0);
114
+ const unassignedOnline = Math.max(0, onlineTotal - assignedOnline);
115
+ metadata.textContent = `Workers online: ${assignedOnline}${unassignedOnline ? ` (+${unassignedOnline} unassigned)` : ''} (total: ${sessionWorkerStates.size}) | Active runtime sessions: ${assignedOnline} | Total session bindings: ${Number(runtimeSessionBindingCount || 0)}`;
116
+ metadata.hidden = false;
117
+ }
118
+ if (usage) {
119
+ usage.textContent = '';
120
+ usage.hidden = true;
121
+ }
122
+ const primary = document.getElementById('chat-title-primary');
123
+ if (primary && !document.getElementById('clear-status-events')) {
124
+ const clearButton = document.createElement('button');
125
+ clearButton.id = 'clear-status-events';
126
+ clearButton.type = 'button';
127
+ clearButton.textContent = 'Clear local client logs';
128
+ clearButton.title = 'Server status events are retained';
129
+ clearButton.addEventListener('click', async () => {
130
+ await clearStatusEvents();
131
+ events = events.filter((event) => event?.source === 'server');
132
+ localStorage.removeItem(STATUS_SCROLL_STORAGE_KEY);
133
+ await refreshStatusEvents(statusViewGeneration);
134
+ });
135
+ primary.append(clearButton);
136
+ }
137
+ }
138
+
139
+ function renderStatusView({ prepend = false, restoreScrollTop = null } = {}) {
140
+ if (!statusViewActive) return;
141
+ const host = document.getElementById('messages');
142
+ if (!host) return;
143
+ const previousHeight = host.scrollHeight;
144
+ const previousTop = host.scrollTop;
145
+ const followBottom = isNearStatusBottom(host);
146
+ renderStatusHeader();
147
+ const rows = events.map((event) => `
148
+ <div class="status-event">
149
+ <time>${new Date(Number(event.timestamp || 0)).toLocaleString()}</time>
150
+ <span class="status-event-source">${escHtml(String(event.source || 'client'))}</span>
151
+ <code>${formatStatusEventHtml(event)}</code>
152
+ </div>
153
+ `).join('');
154
+ host.innerHTML = `
155
+ <section class="status-view" aria-live="polite">
156
+ <div class="status-events">${rows || '<p class="status-empty">No status events recorded.</p>'}</div>
157
+ </section>
158
+ `;
159
+ if (Number.isFinite(restoreScrollTop)) {
160
+ host.scrollTop = Math.max(0, restoreScrollTop);
161
+ } else if (prepend) {
162
+ host.scrollTop = Math.max(0, previousTop + host.scrollHeight - previousHeight);
163
+ } else if (followBottom) {
164
+ host.scrollTop = host.scrollHeight;
165
+ } else {
166
+ host.scrollTop = previousTop;
167
+ }
168
+ }
169
+
170
+ function scheduleStatusHistoryBoundaryCheck() {
171
+ if (statusRenderFrame) return;
172
+ statusRenderFrame = requestAnimationFrame(() => {
173
+ statusRenderFrame = 0;
174
+ if (!statusViewActive) return;
175
+ const host = document.getElementById('messages');
176
+ if (host) void statusHistoryLoader.handleBoundaryDistance(host.scrollTop);
177
+ });
178
+ }
179
+
180
+ function emptyStatusEventPage() {
181
+ return {
182
+ items: [],
183
+ hasMore: false,
184
+ nextCursor: null,
185
+ };
186
+ }
187
+
188
+ function statusTimelineCursor(clientPage, serverPage) {
189
+ return {
190
+ client: clientPage.nextCursor,
191
+ clientExhausted: !clientPage.hasMore,
192
+ server: serverPage.nextCursor,
193
+ serverExhausted: !serverPage.hasMore,
194
+ };
195
+ }
196
+
197
+ const statusHistoryLoader = createInfiniteLoader({
198
+ fetchPage: async (cursor = {}) => {
199
+ const [clientPage, serverPage] = await Promise.all([
200
+ cursor.clientExhausted ? emptyStatusEventPage() : loadStatusEventPage({ before: cursor.client }),
201
+ cursor.serverExhausted ? emptyStatusEventPage() : loadServerStatusEventPage({ before: cursor.server }),
202
+ ]);
203
+ return {
204
+ items: mergeStatusEvents(clientPage.items, serverPage.items),
205
+ hasMore: clientPage.hasMore || serverPage.hasMore,
206
+ nextCursor: statusTimelineCursor(clientPage, serverPage),
207
+ };
208
+ },
209
+ applyPage: async (page) => {
210
+ events = mergeStatusEvents(events, page.items);
211
+ renderStatusView({ prepend: true });
212
+ scheduleStatusHistoryBoundaryCheck();
213
+ },
214
+ });
215
+
216
+ async function refreshStatusEvents(generation) {
217
+ const [clientPage, serverPage] = await Promise.all([
218
+ loadStatusEventPage(),
219
+ loadServerStatusEventPage(),
220
+ ]);
221
+ if (!statusViewActive || generation !== statusViewGeneration) return false;
222
+ events = mergeStatusEvents(events, [...clientPage.items, ...serverPage.items]);
223
+ statusHistoryLoader.reset({
224
+ hasMore: clientPage.hasMore || serverPage.hasMore,
225
+ nextCursor: statusTimelineCursor(clientPage, serverPage),
226
+ });
227
+ const savedScrollValue = localStorage.getItem(STATUS_SCROLL_STORAGE_KEY);
228
+ const savedScrollTop = savedScrollValue === null ? null : Number(savedScrollValue);
229
+ renderStatusView({ restoreScrollTop: Number.isFinite(savedScrollTop) ? savedScrollTop : null });
230
+ scheduleStatusHistoryBoundaryCheck();
231
+ return true;
232
+ }
233
+
234
+ function initStatusScrollPersistence() {
235
+ if (statusScrollBound) return;
236
+ statusScrollBound = true;
237
+ document.getElementById('messages')?.addEventListener('scroll', () => {
238
+ if (!statusViewActive) return;
239
+ const host = document.getElementById('messages');
240
+ if (!host) return;
241
+ localStorage.setItem(STATUS_SCROLL_STORAGE_KEY, String(Math.max(0, host.scrollTop)));
242
+ scheduleStatusHistoryBoundaryCheck();
243
+ }, { passive: true });
244
+ }
245
+
246
+ function initStatusEventLinkActions() {
247
+ if (statusLinkBound) return;
248
+ statusLinkBound = true;
249
+ document.getElementById('messages')?.addEventListener('click', async (event) => {
250
+ const target = event.target instanceof Element ? event.target.closest('.status-share-link') : null;
251
+ if (!target) return;
252
+ event.preventDefault();
253
+ const conversationId = String(target.getAttribute('data-conversation-id') || '').trim();
254
+ if (!conversationId) return;
255
+ leaveStatusView();
256
+ await window.openConversation?.(conversationId, { restoreScroll: true });
257
+ });
258
+ }
259
+
260
+ export function isStatusViewActive() {
261
+ return statusViewActive;
262
+ }
263
+
264
+ export function leaveStatusView() {
265
+ const wasActive = statusViewActive;
266
+ statusViewGeneration += 1;
267
+ statusViewActive = false;
268
+ document.body.classList.remove('status-view-active');
269
+ document.getElementById('input-area')?.removeAttribute('hidden');
270
+ document.getElementById('pending-question-banner')?.removeAttribute('hidden');
271
+ document.getElementById('clear-status-events')?.remove();
272
+ const metadata = document.getElementById('chat-title-cwd');
273
+ if (metadata) {
274
+ metadata.textContent = '';
275
+ metadata.hidden = true;
276
+ }
277
+ const usage = document.getElementById('chat-title-session-usage');
278
+ if (usage) {
279
+ usage.textContent = '';
280
+ usage.hidden = true;
281
+ }
282
+ unsubscribe?.();
283
+ unsubscribe = null;
284
+ window.renderConvList?.();
285
+ return wasActive;
286
+ }
287
+
288
+ export async function toggleStatusView() {
289
+ if (statusViewActive) {
290
+ leaveStatusView();
291
+ const restoreId = previousConversationId;
292
+ previousConversationId = '';
293
+ if (restoreId) await window.openConversation?.(restoreId, { restoreScroll: true });
294
+ return;
295
+ }
296
+ previousConversationId = String(currentConvId || '').trim();
297
+ const generation = ++statusViewGeneration;
298
+ statusViewActive = true;
299
+ setCurrentConv(null);
300
+ window.renderConvList?.();
301
+ if (previousConversationId) localStorage.setItem('copilot_last_conv', previousConversationId);
302
+ document.body.classList.add('status-view-active');
303
+ renderStatusHeader();
304
+ document.getElementById('pending-question-banner')?.setAttribute('hidden', '');
305
+ document.getElementById('input-area')?.setAttribute('hidden', '');
306
+ initStatusScrollPersistence();
307
+ initStatusEventLinkActions();
308
+ unsubscribe = subscribeStatusEvents((event) => {
309
+ events = mergeStatusEvents(events, [event]);
310
+ renderStatusView();
311
+ });
312
+ if (!await refreshStatusEvents(generation)) return;
313
+ document.getElementById('sidebar')?.classList.remove('open');
314
+ document.getElementById('sidebar-overlay')?.classList.remove('visible');
315
+ }