@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,73 @@
1
+ export function normalizeStreamSeq(value) {
2
+ if (value === null || value === undefined || value === '') return null;
3
+ const numeric = Number(value);
4
+ if (!Number.isFinite(numeric)) return null;
5
+ return Math.max(0, Math.trunc(numeric));
6
+ }
7
+
8
+ export function deriveLatestInFlightStreamEvent(inFlight) {
9
+ const rows = Array.isArray(inFlight?.streamEvents) ? inFlight.streamEvents : [];
10
+ if (!rows.length) return null;
11
+ let latest = null;
12
+ for (const row of rows) {
13
+ const seq = normalizeStreamSeq(row?.seq);
14
+ if (seq === null) continue;
15
+ if (!latest || seq > latest.seq) {
16
+ latest = {
17
+ seq,
18
+ text: String(row?.text || ''),
19
+ done: !!row?.done,
20
+ };
21
+ }
22
+ }
23
+ return latest;
24
+ }
25
+
26
+ /**
27
+ * Latest persisted stream row per thread, so a reload can repaint the main
28
+ * reply preview and every subagent bubble. `deriveLatestInFlightStreamEvent`
29
+ * collapses all threads into one row, which is right for the shared seq/done
30
+ * state machine but loses the per-thread text.
31
+ *
32
+ * Returns `{ main, bySubagentRunId }` — `main` is null when the turn has only
33
+ * produced subagent output so far.
34
+ */
35
+ export function deriveInFlightStreamTextByThread(inFlight) {
36
+ const rows = Array.isArray(inFlight?.streamEvents) ? inFlight.streamEvents : [];
37
+ let main = null;
38
+ const bySubagentRunId = new Map();
39
+ for (const row of rows) {
40
+ const seq = normalizeStreamSeq(row?.seq);
41
+ if (seq === null) continue;
42
+ const entry = { seq, text: String(row?.text || ''), done: !!row?.done };
43
+ const subagentRunId = String(row?.subagentRunId || '').trim();
44
+ if (!subagentRunId) {
45
+ if (!main || seq > main.seq) main = entry;
46
+ continue;
47
+ }
48
+ const previous = bySubagentRunId.get(subagentRunId);
49
+ if (!previous || seq > previous.seq) bySubagentRunId.set(subagentRunId, entry);
50
+ }
51
+ return { main, bySubagentRunId };
52
+ }
53
+
54
+ export function computeNextRelayStreamState(previousState = null, incoming = null) {
55
+ const previous = previousState && typeof previousState === 'object'
56
+ ? previousState
57
+ : { seq: 0, done: false };
58
+ const nextSeq = normalizeStreamSeq(incoming?.seq);
59
+ const incomingDone = !!incoming?.done;
60
+ if (nextSeq !== null && nextSeq <= Number(previous.seq || 0)) {
61
+ return { accept: false, state: previous };
62
+ }
63
+ if (previous.done && !incomingDone) {
64
+ return { accept: false, state: previous };
65
+ }
66
+ return {
67
+ accept: true,
68
+ state: {
69
+ seq: nextSeq === null ? Number(previous.seq || 0) : nextSeq,
70
+ done: previous.done || incomingDone,
71
+ },
72
+ };
73
+ }
@@ -0,0 +1,23 @@
1
+ // Pure planning helpers for incremental streaming renders: streamed markdown
2
+ // and activity logs grow at the tail, so a render only needs to touch nodes
3
+ // from the first divergence onward instead of rebuilding the whole container.
4
+
5
+ export function computeStablePrefixLength(previous = [], next = []) {
6
+ const limit = Math.min(previous.length, next.length);
7
+ let index = 0;
8
+ while (index < limit && previous[index] === next[index]) index += 1;
9
+ return index;
10
+ }
11
+
12
+ /**
13
+ * Plan how to bring a rendered list of rows up to date. Returns `reset: true`
14
+ * (rebuild everything) when existing rows diverge from the expected list;
15
+ * otherwise only the missing tail is appended.
16
+ */
17
+ export function planListPatch(currentTexts = [], expectedTexts = []) {
18
+ const stable = computeStablePrefixLength(currentTexts, expectedTexts);
19
+ if (stable < currentTexts.length) {
20
+ return { reset: true, appends: [...expectedTexts] };
21
+ }
22
+ return { reset: false, appends: expectedTexts.slice(currentTexts.length) };
23
+ }
@@ -0,0 +1,208 @@
1
+ import { BASE, CLIENT_ID, authHeaders, conversations } from './store.js';
2
+
3
+ // Durable outbox for Background Sync: message sends, ask_user answers, and
4
+ // draft flushes that failed (or might die mid-flight on pagehide) are queued
5
+ // in IndexedDB and replayed by the service worker's `sync` handler. The store
6
+ // layout here must match the reader in sw.js, which cannot import modules.
7
+
8
+ const DB_NAME = 'copilot-remote-sync';
9
+ const DB_VERSION = 1;
10
+ const STORE_NAME = 'outbox';
11
+ export const OUTBOX_SYNC_TAG = 'copilot-outbox';
12
+ const MAX_REPLAY_ATTEMPTS = 10;
13
+
14
+ function openOutboxDb() {
15
+ return new Promise((resolve, reject) => {
16
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
17
+ request.onupgradeneeded = () => {
18
+ const db = request.result;
19
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
20
+ db.createObjectStore(STORE_NAME, { autoIncrement: true });
21
+ }
22
+ };
23
+ request.onsuccess = () => resolve(request.result);
24
+ request.onerror = () => reject(request.error || new Error('IndexedDB unavailable'));
25
+ });
26
+ }
27
+
28
+ function requestAsPromise(request) {
29
+ return new Promise((resolve, reject) => {
30
+ request.onsuccess = () => resolve(request.result);
31
+ request.onerror = () => reject(request.error || new Error('IndexedDB request failed'));
32
+ });
33
+ }
34
+
35
+ /**
36
+ * @param {{ kind: string, path: string, method?: string, body: string }} entry
37
+ * `path` is BASE-relative (e.g. "/api/message"); `body` is a JSON string.
38
+ */
39
+ export async function enqueueOutboxRequest(entry) {
40
+ if (!('indexedDB' in window)) return false;
41
+ try {
42
+ const db = await openOutboxDb();
43
+ const tx = db.transaction(STORE_NAME, 'readwrite');
44
+ tx.objectStore(STORE_NAME).add({
45
+ kind: String(entry?.kind || 'request'),
46
+ path: String(entry?.path || ''),
47
+ method: String(entry?.method || 'POST'),
48
+ body: String(entry?.body || ''),
49
+ createdAt: Date.now(),
50
+ attempts: 0,
51
+ });
52
+ await new Promise((resolve, reject) => {
53
+ tx.oncomplete = resolve;
54
+ tx.onerror = () => reject(tx.error || new Error('IndexedDB write failed'));
55
+ tx.onabort = () => reject(tx.error || new Error('IndexedDB write aborted'));
56
+ });
57
+ db.close();
58
+ return true;
59
+ } catch {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Ask the service worker to replay the outbox when connectivity returns.
66
+ * Returns false when Background Sync is unavailable (non-Chromium); the caller
67
+ * falls back to page-side replay via initOutboxFallbackReplay().
68
+ */
69
+ export async function registerOutboxSync() {
70
+ try {
71
+ const registration = await navigator.serviceWorker?.getRegistration?.();
72
+ if (registration && 'sync' in registration) {
73
+ await registration.sync.register(OUTBOX_SYNC_TAG);
74
+ return true;
75
+ }
76
+ } catch {}
77
+ return false;
78
+ }
79
+
80
+ async function readOutboxEntries(db) {
81
+ const tx = db.transaction(STORE_NAME, 'readonly');
82
+ const store = tx.objectStore(STORE_NAME);
83
+ const [keys, values] = await Promise.all([
84
+ requestAsPromise(store.getAllKeys()),
85
+ requestAsPromise(store.getAll()),
86
+ ]);
87
+ return keys.map((key, index) => ({ key, value: values[index] }));
88
+ }
89
+
90
+ async function deleteOutboxEntry(db, key) {
91
+ const tx = db.transaction(STORE_NAME, 'readwrite');
92
+ tx.objectStore(STORE_NAME).delete(key);
93
+ return new Promise((resolve) => {
94
+ tx.oncomplete = resolve;
95
+ tx.onerror = resolve;
96
+ tx.onabort = resolve;
97
+ });
98
+ }
99
+
100
+ async function updateOutboxEntry(db, key, value) {
101
+ const tx = db.transaction(STORE_NAME, 'readwrite');
102
+ tx.objectStore(STORE_NAME).put(value, key);
103
+ return new Promise((resolve) => {
104
+ tx.oncomplete = resolve;
105
+ tx.onerror = resolve;
106
+ tx.onabort = resolve;
107
+ });
108
+ }
109
+
110
+ // A 4xx (other than timeout/rate-limit) means the server made a decision about
111
+ // this exact request — including 409 DUPLICATE_MESSAGE_ID for a send that
112
+ // actually landed on a previous ambiguous attempt — so the entry is finished.
113
+ function isFinalResponse(response) {
114
+ if (!response) return false;
115
+ if (response.ok) return true;
116
+ return response.status >= 400 && response.status < 500
117
+ && response.status !== 408 && response.status !== 429;
118
+ }
119
+
120
+ /**
121
+ * Page-side replay for browsers without Background Sync. Replays in insertion
122
+ * order; stops at the first retryable failure so ordering is preserved.
123
+ */
124
+ export async function replayOutboxFromPage() {
125
+ if (!('indexedDB' in window)) return { replayed: 0 };
126
+ let db = null;
127
+ let replayed = 0;
128
+ try {
129
+ db = await openOutboxDb();
130
+ const entries = await readOutboxEntries(db);
131
+ for (const { key, value } of entries) {
132
+ let response = null;
133
+ try {
134
+ response = await fetch(`${BASE}${value.path}`, {
135
+ method: value.method || 'POST',
136
+ headers: { 'Content-Type': 'application/json', ...authHeaders() },
137
+ body: value.body,
138
+ });
139
+ } catch {
140
+ response = null;
141
+ }
142
+ if (isFinalResponse(response)) {
143
+ await deleteOutboxEntry(db, key);
144
+ replayed += 1;
145
+ continue;
146
+ }
147
+ const attempts = Number(value.attempts || 0) + 1;
148
+ if (attempts >= MAX_REPLAY_ATTEMPTS) {
149
+ await deleteOutboxEntry(db, key);
150
+ } else {
151
+ await updateOutboxEntry(db, key, { ...value, attempts });
152
+ }
153
+ break;
154
+ }
155
+ } catch {} finally {
156
+ try { db?.close(); } catch {}
157
+ }
158
+ return { replayed };
159
+ }
160
+
161
+ let fallbackReplayBound = false;
162
+
163
+ /**
164
+ * When Background Sync is unsupported, replay the queue from the page on load
165
+ * and whenever connectivity returns.
166
+ */
167
+ export async function initOutboxFallbackReplay() {
168
+ try {
169
+ const registration = await navigator.serviceWorker?.getRegistration?.();
170
+ if (registration && 'sync' in registration) return;
171
+ } catch {}
172
+ if (fallbackReplayBound) return;
173
+ fallbackReplayBound = true;
174
+ void replayOutboxFromPage();
175
+ window.addEventListener('online', () => {
176
+ void replayOutboxFromPage();
177
+ });
178
+ }
179
+
180
+ /**
181
+ * Best-effort durable draft flush for pagehide: the direct PATCH the page
182
+ * fires can be killed mid-flight, so the same update is queued for the
183
+ * service worker. If the direct write wins, the replay's baseDraftUpdatedAt
184
+ * is stale and the server rejects it with a version conflict, which the
185
+ * replay treats as final. Only queues when the draft differs from the last
186
+ * persisted state.
187
+ */
188
+ export async function enqueueDraftFlushForBackgroundSync(conversationId) {
189
+ const id = String(conversationId || '').trim();
190
+ if (!id) return false;
191
+ const conversation = conversations[id];
192
+ if (!conversation) return false;
193
+ const input = document.getElementById('msg-input');
194
+ const draftText = String(input ? input.value : (conversation.draftText || ''));
195
+ if (draftText === String(conversation.draftText || '')) return false;
196
+ const queued = await enqueueOutboxRequest({
197
+ kind: 'draft-flush',
198
+ path: `/api/conversation/${encodeURIComponent(id)}/draft`,
199
+ method: 'PATCH',
200
+ body: JSON.stringify({
201
+ draftText,
202
+ clientId: CLIENT_ID,
203
+ baseDraftUpdatedAt: conversation.draftUpdatedAt || null,
204
+ }),
205
+ });
206
+ if (queued) void registerOutboxSync();
207
+ return queued;
208
+ }
@@ -0,0 +1,62 @@
1
+ // Shared touch/click activation helpers for the header menus (chat actions menu,
2
+ // sidebar toggle, theme button, tmux inspector menu).
3
+ //
4
+ // These activate on `pointerup` and then swallow the follow-up synthetic click on
5
+ // the *same* element. That is only safe when the surrounding menu is an overlay
6
+ // that does not reflow siblings AND the caller arms a full-screen shield (see
7
+ // lockChatActionsMenuShield in bootstrap.js) so the delayed compatibility click
8
+ // cannot land on whatever is underneath.
9
+ //
10
+ // New code should prefer plain `click` activation instead — see cwd-picker.js,
11
+ // which does all of its state changes in click/keydown handlers so no synthetic
12
+ // click can ever leak to another element.
13
+
14
+ export function bindTapAction(element, handler) {
15
+ if (!element || element.dataset.tapBound === '1') return;
16
+ element.dataset.tapBound = '1';
17
+ let suppressClickUntil = 0;
18
+ const markSuppressed = (ms = 450) => {
19
+ suppressClickUntil = Date.now() + Math.max(200, Number(ms) || 450);
20
+ };
21
+ element.addEventListener('pointerup', (event) => {
22
+ if (event.pointerType === 'mouse' && event.button !== 0) return;
23
+ event.preventDefault();
24
+ event.stopPropagation();
25
+ markSuppressed();
26
+ handler(event);
27
+ });
28
+ element.addEventListener('click', (event) => {
29
+ if (Date.now() < suppressClickUntil) {
30
+ event.preventDefault();
31
+ event.stopPropagation();
32
+ return;
33
+ }
34
+ handler(event);
35
+ });
36
+ }
37
+
38
+ export function bindMenuAction(element, handler) {
39
+ if (!element || element.dataset.menuTapBound === '1') return;
40
+ element.dataset.menuTapBound = '1';
41
+ let suppressClickUntil = 0;
42
+ const markSuppressed = (ms = 450) => {
43
+ suppressClickUntil = Date.now() + Math.max(200, Number(ms) || 450);
44
+ };
45
+ element.addEventListener('pointerup', (event) => {
46
+ if (event.pointerType === 'mouse' && event.button !== 0) return;
47
+ event.preventDefault();
48
+ event.stopPropagation();
49
+ event.stopImmediatePropagation();
50
+ markSuppressed();
51
+ handler(event);
52
+ }, true);
53
+ element.addEventListener('click', (event) => {
54
+ if (Date.now() < suppressClickUntil) {
55
+ event.preventDefault();
56
+ event.stopPropagation();
57
+ event.stopImmediatePropagation();
58
+ return;
59
+ }
60
+ handler(event);
61
+ }, true);
62
+ }
@@ -0,0 +1,54 @@
1
+ function normalizeId(value) {
2
+ const text = String(value || '').trim();
3
+ return text || '';
4
+ }
5
+
6
+ export function getMessageThreadAnchor(message, messageById = null) {
7
+ const sourceId = normalizeId(message?.sourceMessageId || message?.parentMessageId || message?.threadId);
8
+ if (sourceId && messageById instanceof Map && messageById.has(sourceId)) return sourceId;
9
+ return normalizeId(message?.id);
10
+ }
11
+
12
+ export function sortConversationMessages(messages) {
13
+ const list = Array.isArray(messages) ? messages.filter((message) => !!message) : [];
14
+ const messageById = new Map(
15
+ list
16
+ .map((message) => [normalizeId(message?.id), message])
17
+ .filter(([id]) => !!id),
18
+ );
19
+
20
+ const anchorTimestampCache = new Map();
21
+ function anchorTimestamp(message) {
22
+ const anchorId = getMessageThreadAnchor(message, messageById);
23
+ if (anchorTimestampCache.has(anchorId)) return anchorTimestampCache.get(anchorId);
24
+ const anchorMessage = messageById.get(anchorId) || message;
25
+ const ts = Date.parse(anchorMessage?.timestamp || message?.timestamp || 0);
26
+ const value = Number.isFinite(ts) ? ts : 0;
27
+ anchorTimestampCache.set(anchorId, value);
28
+ return value;
29
+ }
30
+
31
+ function roleRank(message) {
32
+ return String(message?.role || '').trim().toLowerCase() === 'user' ? 0 : 1;
33
+ }
34
+
35
+ return list.slice().sort((a, b) => {
36
+ const aAnchorTs = anchorTimestamp(a);
37
+ const bAnchorTs = anchorTimestamp(b);
38
+ if (aAnchorTs !== bAnchorTs) return aAnchorTs - bAnchorTs;
39
+
40
+ const aAnchorId = getMessageThreadAnchor(a, messageById);
41
+ const bAnchorId = getMessageThreadAnchor(b, messageById);
42
+ if (aAnchorId !== bAnchorId) return aAnchorId.localeCompare(bAnchorId);
43
+
44
+ const aRoleRank = roleRank(a);
45
+ const bRoleRank = roleRank(b);
46
+ if (aRoleRank !== bRoleRank) return aRoleRank - bRoleRank;
47
+
48
+ const aTs = Date.parse(a?.timestamp || 0);
49
+ const bTs = Date.parse(b?.timestamp || 0);
50
+ if (Number.isFinite(aTs) && Number.isFinite(bTs) && aTs !== bTs) return aTs - bTs;
51
+
52
+ return normalizeId(a?.id).localeCompare(normalizeId(b?.id));
53
+ });
54
+ }