@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,267 @@
1
+ import {
2
+ currentConvId,
3
+ conversations,
4
+ escHtml,
5
+ openSummaryModal,
6
+ closeSummaryModal,
7
+ setSummaryModalLoading,
8
+ showTransientRelayNotice,
9
+ } from './store.js';
10
+ import {
11
+ killSessionWorker,
12
+ requestRelayRestart,
13
+ requestHostSuspend,
14
+ requestQueueEmpty,
15
+ refreshWorkspaceRootHints,
16
+ } from './api-client.js';
17
+ import { isSuspendHostActionVisible } from './settings-modal.js';
18
+
19
+ let killSessionInFlight = false;
20
+ let restartRelayInFlight = false;
21
+ let suspendHostInFlight = false;
22
+ let emptyQueueInFlight = false;
23
+
24
+ let menuDeps = {
25
+ lockChatActionsMenuShield: () => {},
26
+ closeChatActionsMenu: () => {},
27
+ syncQueueStatusMenuEntry: () => {},
28
+ refreshSessionWorkerStatus: () => Promise.resolve(),
29
+ };
30
+
31
+ function getCurrentConversationSessionInfo() {
32
+ const convId = String(currentConvId || '').trim();
33
+ if (!convId) return null;
34
+ const conversation = conversations[convId] || {};
35
+ const sdkSessionId = String(conversation.sdkSessionId || '').trim();
36
+ if (!sdkSessionId) return null;
37
+ const title = String(conversation.title || document.getElementById('chat-title')?.textContent || convId).trim() || convId;
38
+ return {
39
+ conversationId: convId,
40
+ sdkSessionId,
41
+ title,
42
+ };
43
+ }
44
+
45
+ export function openKillSessionConfirmation() {
46
+ const info = getCurrentConversationSessionInfo();
47
+ if (!info) {
48
+ showTransientRelayNotice('No active session is bound to this conversation.');
49
+ return;
50
+ }
51
+ const escapedTitle = escHtml(info.title);
52
+ openSummaryModal({
53
+ title: 'Kill session',
54
+ subtitle: info.sdkSessionId,
55
+ kind: 'kill-session',
56
+ bodyHtml: `
57
+ <p>Kill the session for <strong>${escapedTitle}</strong>?</p>
58
+ <p>This stops the current worker and any active turn will need a manual retry or a new message.</p>
59
+ <div class="summary-modal-actions">
60
+ <button class="chat-title-action-btn danger-btn" type="button" onclick="confirmKillCurrentSession()">☠️ Kill session</button>
61
+ <button class="chat-title-action-btn" type="button" onclick="closeSummaryModal()">Cancel</button>
62
+ </div>
63
+ `,
64
+ });
65
+ }
66
+
67
+ export async function confirmKillCurrentSession() {
68
+ if (killSessionInFlight) return;
69
+ const info = getCurrentConversationSessionInfo();
70
+ if (!info) {
71
+ closeSummaryModal();
72
+ showTransientRelayNotice('No active session is bound to this conversation.');
73
+ return;
74
+ }
75
+ killSessionInFlight = true;
76
+ setSummaryModalLoading(true);
77
+ try {
78
+ const result = await killSessionWorker(info.sdkSessionId, {
79
+ conversationId: info.conversationId,
80
+ title: info.title,
81
+ });
82
+ closeSummaryModal();
83
+ if (!result?.ok) {
84
+ alert('Failed to kill session');
85
+ return;
86
+ }
87
+ const statusText = result.processStatus === 'killed'
88
+ ? 'Session killed.'
89
+ : 'Session state cleared; no live worker process was found.';
90
+ showTransientRelayNotice(statusText);
91
+ await menuDeps.refreshSessionWorkerStatus().catch(() => {});
92
+ } finally {
93
+ killSessionInFlight = false;
94
+ setSummaryModalLoading(false);
95
+ }
96
+ }
97
+
98
+ export function openRestartRelayConfirmation() {
99
+ openSummaryModal({
100
+ title: 'Restart web relay',
101
+ subtitle: 'Queues restart via /api/relay/shutdown',
102
+ kind: 'restart-relay',
103
+ bodyHtml: `
104
+ <p>Queue a manual relay restart now?</p>
105
+ <p>The restart waits until the current turn is idle, so it does not interrupt an in-flight turn immediately.</p>
106
+ <div class="summary-modal-actions">
107
+ <button class="chat-title-action-btn" type="button" onclick="confirmRestartWebRelay()">🌄 Restart web relay</button>
108
+ <button class="chat-title-action-btn" type="button" onclick="closeSummaryModal()">Cancel</button>
109
+ </div>
110
+ `,
111
+ });
112
+ }
113
+
114
+ export function openEmptyQueueConfirmation() {
115
+ menuDeps.lockChatActionsMenuShield(350);
116
+ menuDeps.closeChatActionsMenu();
117
+ openSummaryModal({
118
+ title: 'Empty queue',
119
+ subtitle: 'Calls localhost /api/queue/empty',
120
+ kind: 'empty-queue',
121
+ bodyHtml: `
122
+ <p>Drop all queue rows in pending, processing, and parked states?</p>
123
+ <p>This is a local maintenance action and cannot be undone.</p>
124
+ <div class="summary-modal-actions">
125
+ <button class="chat-title-action-btn danger-btn" type="button" onclick="confirmEmptyQueue()">🚮 Empty queue</button>
126
+ <button class="chat-title-action-btn" type="button" onclick="closeSummaryModal()">Cancel</button>
127
+ </div>
128
+ `,
129
+ });
130
+ }
131
+
132
+ export function openSuspendHostConfirmation() {
133
+ if (!isSuspendHostActionVisible()) return;
134
+ menuDeps.lockChatActionsMenuShield(350);
135
+ menuDeps.closeChatActionsMenu();
136
+ openSummaryModal({
137
+ title: 'Suspend host',
138
+ subtitle: 'Requests suspend-to-RAM',
139
+ kind: 'suspend-host',
140
+ bodyHtml: `
141
+ <p>Put this PC to sleep now?</p>
142
+ <p>This requests suspend-to-RAM immediately.</p>
143
+ <div class="summary-modal-actions">
144
+ <button class="chat-title-action-btn" type="button" onclick="confirmSuspendHost()">💤 Suspend host</button>
145
+ <button class="chat-title-action-btn" type="button" onclick="closeSummaryModal()">Cancel</button>
146
+ </div>
147
+ `,
148
+ });
149
+ window.setTimeout(() => {
150
+ const modal = document.getElementById('summary-modal');
151
+ const classVisible = !!modal?.classList?.contains('visible');
152
+ const ariaVisible = String(modal?.getAttribute('aria-hidden') || 'true') === 'false';
153
+ const displayVisible = modal ? window.getComputedStyle(modal).display !== 'none' : false;
154
+ if (classVisible && ariaVisible && displayVisible) return;
155
+ const confirmed = window.confirm('Put this PC to sleep now?\n\nThis requests suspend-to-RAM.');
156
+ if (!confirmed) return;
157
+ confirmSuspendHost().catch(() => {});
158
+ }, 90);
159
+ }
160
+
161
+ export async function confirmSuspendHost() {
162
+ if (!isSuspendHostActionVisible()) return;
163
+ if (suspendHostInFlight) return;
164
+ suspendHostInFlight = true;
165
+ setSummaryModalLoading(true);
166
+ try {
167
+ const result = await requestHostSuspend({
168
+ reason: 'manual-suspend',
169
+ requestedBy: 'localhost-api',
170
+ });
171
+ closeSummaryModal();
172
+ if (!result?.ok) {
173
+ alert('Failed to suspend host');
174
+ return;
175
+ }
176
+ } finally {
177
+ suspendHostInFlight = false;
178
+ setSummaryModalLoading(false);
179
+ }
180
+ }
181
+
182
+ export async function confirmEmptyQueue() {
183
+ if (emptyQueueInFlight) return;
184
+ emptyQueueInFlight = true;
185
+ setSummaryModalLoading(true);
186
+ try {
187
+ const result = await requestQueueEmpty({
188
+ reason: 'manual-empty-queue',
189
+ requestedBy: 'localhost-api',
190
+ });
191
+ closeSummaryModal();
192
+ if (!result?.ok) {
193
+ alert('Failed to empty queue');
194
+ return;
195
+ }
196
+ const droppedCount = Number(result.droppedCount || 0);
197
+ if (droppedCount <= 0) {
198
+ showTransientRelayNotice('Queue is already empty.');
199
+ } else {
200
+ showTransientRelayNotice(`Queue emptied: dropped ${droppedCount} row${droppedCount === 1 ? '' : 's'}.`, 6000);
201
+ }
202
+ const status = await refreshWorkspaceRootHints();
203
+ menuDeps.syncQueueStatusMenuEntry(status);
204
+ } finally {
205
+ emptyQueueInFlight = false;
206
+ setSummaryModalLoading(false);
207
+ }
208
+ }
209
+
210
+ export async function confirmRestartWebRelay() {
211
+ if (restartRelayInFlight) return;
212
+ restartRelayInFlight = true;
213
+ setSummaryModalLoading(true);
214
+ try {
215
+ const result = await requestRelayRestart({
216
+ reason: 'manual-restart',
217
+ requestedBy: 'localhost-api',
218
+ restart: true,
219
+ });
220
+ closeSummaryModal();
221
+ // Restart can close the connection before the browser receives JSON.
222
+ if (!result) {
223
+ showTransientRelayNotice('Relay restart requested. Connection may briefly drop while it restarts.', 7000);
224
+ return;
225
+ }
226
+ if (!result.ok) {
227
+ alert('Failed to queue relay restart');
228
+ return;
229
+ }
230
+ if (result.accepted === false) {
231
+ showTransientRelayNotice('Relay is already shutting down/restarting.', 7000);
232
+ return;
233
+ }
234
+ const queue = result.queue || {};
235
+ showTransientRelayNotice(
236
+ `Relay restart queued (pending=${Number(queue.pendingCount || 0)}, processing=${Number(queue.processingCount || 0)}).`,
237
+ 7000,
238
+ );
239
+ } finally {
240
+ restartRelayInFlight = false;
241
+ setSummaryModalLoading(false);
242
+ }
243
+ }
244
+
245
+ export function initActionConfirmations({
246
+ lockChatActionsMenuShield,
247
+ closeChatActionsMenu,
248
+ syncQueueStatusMenuEntry,
249
+ refreshSessionWorkerStatus,
250
+ exposeOnWindow = true,
251
+ } = {}) {
252
+ menuDeps = {
253
+ lockChatActionsMenuShield: typeof lockChatActionsMenuShield === 'function' ? lockChatActionsMenuShield : menuDeps.lockChatActionsMenuShield,
254
+ closeChatActionsMenu: typeof closeChatActionsMenu === 'function' ? closeChatActionsMenu : menuDeps.closeChatActionsMenu,
255
+ syncQueueStatusMenuEntry: typeof syncQueueStatusMenuEntry === 'function' ? syncQueueStatusMenuEntry : menuDeps.syncQueueStatusMenuEntry,
256
+ refreshSessionWorkerStatus: typeof refreshSessionWorkerStatus === 'function' ? refreshSessionWorkerStatus : menuDeps.refreshSessionWorkerStatus,
257
+ };
258
+ if (!exposeOnWindow) return;
259
+ window.openKillSessionConfirmation = openKillSessionConfirmation;
260
+ window.openRestartRelayConfirmation = openRestartRelayConfirmation;
261
+ window.openEmptyQueueConfirmation = openEmptyQueueConfirmation;
262
+ window.openSuspendHostConfirmation = openSuspendHostConfirmation;
263
+ window.confirmKillCurrentSession = confirmKillCurrentSession;
264
+ window.confirmRestartWebRelay = confirmRestartWebRelay;
265
+ window.confirmSuspendHost = confirmSuspendHost;
266
+ window.confirmEmptyQueue = confirmEmptyQueue;
267
+ }
@@ -0,0 +1,149 @@
1
+ const DEFAULT_ACTIVITY_LIMIT = 24;
2
+
3
+ export function normalizeRelayActivityEntry(item) {
4
+ if (item && typeof item === 'object' && !Array.isArray(item)) {
5
+ const text = String(item.text || '').trim();
6
+ const subagentRunId = item.subagentRunId ? String(item.subagentRunId).trim() : null;
7
+ if (!text) return null;
8
+ const metadata = (item.metadata && typeof item.metadata === 'object' && !Array.isArray(item.metadata))
9
+ ? item.metadata
10
+ : null;
11
+ return { text, subagentRunId, metadata };
12
+ }
13
+ const text = String(item || '').trim();
14
+ if (!text) return null;
15
+ return { text, subagentRunId: null, metadata: null };
16
+ }
17
+
18
+ // A compaction boundary is published as an ordinary activity row carrying
19
+ // structured metadata. The transcript promotes it to a full-width break row
20
+ // (and drops it from the bubble's tool-activity list), so both sides ask here.
21
+ export function isCompactBoundaryActivityEntry(item) {
22
+ return normalizeRelayActivityEntry(item)?.metadata?.kind === 'compact_boundary';
23
+ }
24
+
25
+ function toTokenCount(value) {
26
+ // `Number(null)` is 0, not NaN: without this an omitted post_tokens (every
27
+ // real auto-compaction) would read as a compaction down to zero tokens.
28
+ if (value === null || value === undefined || value === '') return null;
29
+ const parsed = Number(value);
30
+ return Number.isFinite(parsed) && parsed >= 0 ? Math.round(parsed) : null;
31
+ }
32
+
33
+ // One message carries at most one break row, so when a turn compacted more
34
+ // than once only the LAST boundary is promoted. This returns that entry by
35
+ // identity, so the render path can keep the others visible as prose instead
36
+ // of dropping them (they would otherwise vanish from the transcript).
37
+ export function promotedCompactBoundaryEntry(items) {
38
+ const list = Array.isArray(items) ? items : [];
39
+ for (let index = list.length - 1; index >= 0; index -= 1) {
40
+ if (isCompactBoundaryActivityEntry(list[index])) return list[index];
41
+ }
42
+ return null;
43
+ }
44
+
45
+ // The last compaction recorded against one message's activities, as
46
+ // { preTokens, postTokens } (either may be null when the SDK omitted it).
47
+ export function compactBoundaryFromActivities(items) {
48
+ const promoted = promotedCompactBoundaryEntry(items);
49
+ if (!promoted) return null;
50
+ const metadata = normalizeRelayActivityEntry(promoted).metadata;
51
+ return {
52
+ preTokens: toTokenCount(metadata.preTokens),
53
+ postTokens: toTokenCount(metadata.postTokens),
54
+ };
55
+ }
56
+
57
+ // Head-cap that never drops structured rows. Activity lists are capped from
58
+ // the front (the start of a turn is the interesting part), but a
59
+ // metadata-bearing row carries transcript structure rather than prose, and a
60
+ // long agentic turn can push its compaction boundary past the cap — losing
61
+ // the break row entirely, with no prose fallback since the bubble filters
62
+ // compact entries out. Keep every structured row, spend the rest of the
63
+ // budget on the leading prose rows, preserve the original order.
64
+ export function capRelayActivityEntries(items, limit = DEFAULT_ACTIVITY_LIMIT) {
65
+ const list = Array.isArray(items) ? items : [];
66
+ const max = Math.max(1, Math.trunc(Number(limit)) || DEFAULT_ACTIVITY_LIMIT);
67
+ if (list.length <= max) return list.slice();
68
+ const structuredIndexes = [];
69
+ for (let index = 0; index < list.length; index += 1) {
70
+ if (normalizeRelayActivityEntry(list[index])?.metadata) structuredIndexes.push(index);
71
+ }
72
+ if (!structuredIndexes.length) return list.slice(0, max);
73
+ // Safety valve for a pathological turn with more structured rows than the
74
+ // cap allows: the most recent ones win.
75
+ const keep = new Set(structuredIndexes.slice(-max));
76
+ let budget = max - keep.size;
77
+ const kept = [];
78
+ for (let index = 0; index < list.length; index += 1) {
79
+ if (keep.has(index)) {
80
+ kept.push(list[index]);
81
+ continue;
82
+ }
83
+ if (budget <= 0) continue;
84
+ budget -= 1;
85
+ kept.push(list[index]);
86
+ }
87
+ return kept;
88
+ }
89
+
90
+ export function relayActivityEntryText(item) {
91
+ return normalizeRelayActivityEntry(item)?.text || '';
92
+ }
93
+
94
+ function normalizeActivityItems(items, limit = DEFAULT_ACTIVITY_LIMIT) {
95
+ const normalized = Array.isArray(items)
96
+ ? items.map((item) => normalizeRelayActivityEntry(item)).filter(Boolean)
97
+ : [];
98
+ return normalized.slice(-Math.max(1, Number(limit) || DEFAULT_ACTIVITY_LIMIT));
99
+ }
100
+
101
+ function activityEntryKey(item) {
102
+ const entry = normalizeRelayActivityEntry(item);
103
+ if (!entry) return '';
104
+ return `${entry.subagentRunId || ''}::${entry.text}`;
105
+ }
106
+
107
+ function isSubsequence(subset, sequence) {
108
+ if (!subset.length) return true;
109
+ let cursor = 0;
110
+ for (const item of sequence) {
111
+ if (activityEntryKey(item) === activityEntryKey(subset[cursor])) cursor += 1;
112
+ if (cursor >= subset.length) return true;
113
+ }
114
+ return false;
115
+ }
116
+
117
+ export function mergeRelayActivityTexts(existingItems, incomingItems, limit = DEFAULT_ACTIVITY_LIMIT) {
118
+ const existing = normalizeActivityItems(existingItems, Number.POSITIVE_INFINITY);
119
+ const incoming = normalizeActivityItems(incomingItems, Number.POSITIVE_INFINITY);
120
+ if (!existing.length) return normalizeActivityItems(incoming, limit);
121
+ if (!incoming.length) return normalizeActivityItems(existing, limit);
122
+ if (isSubsequence(existing, incoming)) return normalizeActivityItems(incoming, limit);
123
+ if (isSubsequence(incoming, existing)) return normalizeActivityItems(existing, limit);
124
+
125
+ const primary = incoming.length > existing.length ? incoming : existing;
126
+ const secondary = primary === incoming ? existing : incoming;
127
+ const merged = primary.slice();
128
+ const seen = new Set(primary.map((item) => activityEntryKey(item)));
129
+ for (const item of secondary) {
130
+ const key = activityEntryKey(item);
131
+ if (!key || seen.has(key)) continue;
132
+ merged.push(item);
133
+ seen.add(key);
134
+ }
135
+ return normalizeActivityItems(merged, limit);
136
+ }
137
+
138
+ export function shouldApplyConversationLoad({
139
+ requestedConversationId,
140
+ activeConversationId,
141
+ capturedVersion,
142
+ currentVersion,
143
+ } = {}) {
144
+ const requestedId = String(requestedConversationId || '').trim();
145
+ const activeId = String(activeConversationId || '').trim();
146
+ return !!requestedId
147
+ && requestedId === activeId
148
+ && Number(capturedVersion) === Number(currentVersion);
149
+ }