@oar-sh/oar 0.9.9 → 0.9.11

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 (154) hide show
  1. package/CHANGELOG.md +561 -1
  2. package/README.md +162 -909
  3. package/bin/bat/oar.bat +46 -46
  4. package/package.json +14 -14
  5. package/server/README.md +369 -13
  6. package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
  7. package/server/claude-worker/claude-session-process.mjs +121 -9
  8. package/server/claude-worker/claude-session-worker.mjs +9 -1
  9. package/server/claude-worker/claude-turn-publisher.mjs +3 -0
  10. package/server/claude-worker/sdk-message-normalizer.mjs +31 -4
  11. package/server/claude-worker/subagent-timeline-recorder.mjs +572 -0
  12. package/server/copilot-worker/copilot-sdk-session-process.mjs +79 -12
  13. package/server/copilot-worker/copilot-sdk-session-worker.mjs +6 -0
  14. package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
  15. package/server/db-schema.mjs +95 -2
  16. package/server/grok-worker/acp-client.mjs +2 -1
  17. package/server/grok-worker/acp-host-services.mjs +4 -3
  18. package/server/migrations/0009-subagent-timeline.mjs +135 -0
  19. package/server/migrations/0010-subagent-timeline-pruning.mjs +89 -0
  20. package/server/migrations/0011-relay-event-subscriptions.mjs +125 -0
  21. package/server/migrations/0012-cross-relay-event-subscriptions.mjs +116 -0
  22. package/server/migrations/0013-previews.mjs +130 -0
  23. package/server/migrations/0014-conversation-groups.mjs +89 -0
  24. package/server/migrations/0015-conversation-group-position.mjs +76 -0
  25. package/server/migrations/0016-share-access-log.mjs +110 -0
  26. package/server/public/app/about-ui.js +73 -0
  27. package/server/public/app/about-view.mjs +234 -0
  28. package/server/public/app/action-confirmations.js +31 -14
  29. package/server/public/app/agent-exchange-fold-view.mjs +187 -0
  30. package/server/public/app/api-client.js +312 -16
  31. package/server/public/app/archive-confirm.mjs +28 -0
  32. package/server/public/app/ask-user-view.js +17 -12
  33. package/server/public/app/attachments-view.js +516 -6
  34. package/server/public/app/background-tasks-view.mjs +18 -26
  35. package/server/public/app/bootstrap.js +336 -59
  36. package/server/public/app/chat-actions-menu-state.mjs +126 -0
  37. package/server/public/app/claude-cloud-conversation-ui.js +10 -22
  38. package/server/public/app/composer-control-state.mjs +16 -0
  39. package/server/public/app/composer-keyboard.mjs +54 -0
  40. package/server/public/app/context-menu.mjs +90 -6
  41. package/server/public/app/conversation-group-drag.mjs +191 -0
  42. package/server/public/app/conversation-group-order.mjs +265 -0
  43. package/server/public/app/conversation-group-state.mjs +100 -0
  44. package/server/public/app/conversation-list-drag.mjs +449 -0
  45. package/server/public/app/conversation-turn-state.mjs +81 -0
  46. package/server/public/app/conversation-view.js +436 -117
  47. package/server/public/app/cwd-picker.js +1 -1
  48. package/server/public/app/explorer-edit-state.mjs +68 -0
  49. package/server/public/app/hidden-messages-view.mjs +312 -0
  50. package/server/public/app/host-suspend-ui.js +13 -4
  51. package/server/public/app/journal-view.js +930 -45
  52. package/server/public/app/no-conversation-state.mjs +58 -0
  53. package/server/public/app/preview-cards.mjs +117 -31
  54. package/server/public/app/preview-command.mjs +10 -1
  55. package/server/public/app/provider-readiness.mjs +120 -0
  56. package/server/public/app/relay-board-view.js +19 -3
  57. package/server/public/app/relay-event-subscriptions-ui.js +184 -0
  58. package/server/public/app/relay-event-view.mjs +234 -0
  59. package/server/public/app/relay-restart-error.mjs +16 -0
  60. package/server/public/app/settings-modal.js +93 -9
  61. package/server/public/app/settings-tabs.js +1 -1
  62. package/server/public/app/share-format.mjs +49 -0
  63. package/server/public/app/share-preview-marks.mjs +167 -0
  64. package/server/public/app/share-state.js +109 -0
  65. package/server/public/app/shared-conversations-settings.js +480 -0
  66. package/server/public/app/socket-handlers.js +137 -40
  67. package/server/public/app/store.js +115 -32
  68. package/server/public/app/subagent-row.mjs +138 -0
  69. package/server/public/app/subagent-timeline-view.mjs +186 -0
  70. package/server/public/app/subagent-viewer-watch.mjs +50 -0
  71. package/server/public/app/subagent-viewer.mjs +558 -0
  72. package/server/public/app/task-row-format.mjs +29 -0
  73. package/server/public/app/transcript-jump-bottom-view.mjs +464 -0
  74. package/server/public/app/transcript-jump-bottom.mjs +210 -0
  75. package/server/public/app/transcript-reload-freshness.mjs +25 -0
  76. package/server/public/app/transcript-viewport-anchor.mjs +140 -0
  77. package/server/public/app/update-ui.js +1 -1
  78. package/server/public/app/viewer-history.mjs +5 -3
  79. package/server/public/index.html +774 -12
  80. package/server/relay-tools.md +2 -1
  81. package/server/relay.mjs +2 -1
  82. package/server/repositories/conversation-group-repository.mjs +444 -0
  83. package/server/repositories/message-repository.mjs +30 -3
  84. package/server/repositories/question-repository.mjs +9 -3
  85. package/server/repositories/remote-relay-repository.mjs +6 -2
  86. package/server/repositories/session-repository.mjs +12 -37
  87. package/server/repositories/share-repository.mjs +322 -0
  88. package/server/repositories/share-visibility-statements.mjs +10 -0
  89. package/server/repositories/subagent-timeline-statements.mjs +170 -0
  90. package/server/routes/about-routes.mjs +15 -0
  91. package/server/routes/ask-user-routes.mjs +3 -0
  92. package/server/routes/cache-routes.mjs +4 -0
  93. package/server/routes/conversation-group-routes.mjs +171 -0
  94. package/server/routes/messages-routes.mjs +698 -114
  95. package/server/routes/preview-routes.mjs +9 -1
  96. package/server/routes/relay-event-routes.mjs +157 -0
  97. package/server/routes/remote-relay-routes.mjs +15 -0
  98. package/server/routes/sessions-routes.mjs +843 -193
  99. package/server/server-runtime.mjs +559 -160
  100. package/server/services/about-service.mjs +461 -0
  101. package/server/services/background-task-store.mjs +128 -0
  102. package/server/services/background-work-view.mjs +228 -0
  103. package/server/services/cache-rebuild-service.mjs +30 -0
  104. package/server/services/delete-archive-service.mjs +4 -1
  105. package/server/services/detached-worker-watch-service.mjs +150 -0
  106. package/server/services/explorer-entry-name.mjs +26 -0
  107. package/server/services/explorer-mutations.mjs +122 -0
  108. package/server/services/hidden-message-list.mjs +21 -0
  109. package/server/services/host-suspend-command.mjs +2 -1
  110. package/server/services/message-pin-list.mjs +1 -1
  111. package/server/services/preview-health-probe.mjs +8 -1
  112. package/server/services/preview-proxy-server.mjs +49 -27
  113. package/server/services/preview-registry-service.mjs +676 -26
  114. package/server/services/preview-static-handler.mjs +40 -7
  115. package/server/services/preview-store.mjs +274 -0
  116. package/server/services/push-dispatch-service.mjs +18 -0
  117. package/server/services/rate-limit-service.mjs +12 -1
  118. package/server/services/relay-auth.mjs +183 -0
  119. package/server/services/relay-cli-launcher-service.mjs +3 -1
  120. package/server/services/relay-event-link-service.mjs +289 -0
  121. package/server/services/relay-event-service.mjs +1952 -0
  122. package/server/services/relay-shutdown-gate.mjs +31 -0
  123. package/server/services/remote-relay-caller-context.mjs +52 -7
  124. package/server/services/remote-relay-dispatcher.mjs +551 -7
  125. package/server/services/remote-relay-registry-service.mjs +11 -1
  126. package/server/services/request-boundary.mjs +183 -0
  127. package/server/services/session-activity-tracker.mjs +695 -0
  128. package/server/services/session-history-refresh-service.mjs +7 -0
  129. package/server/services/session-worker-launch-service.mjs +190 -6
  130. package/server/services/session-worker-process-service.mjs +10 -6
  131. package/server/services/share-access-log-service.mjs +123 -0
  132. package/server/services/share-path-redaction.mjs +424 -0
  133. package/server/services/stale-attempt-log-limiter.mjs +96 -0
  134. package/server/services/stale-recovery-clock-guard.mjs +57 -0
  135. package/server/services/stale-subagent-run-sweep.mjs +109 -0
  136. package/server/services/status-event-service.mjs +7 -3
  137. package/server/services/subagent-record-retention.mjs +164 -0
  138. package/server/services/subagent-timeline-service.mjs +328 -0
  139. package/server/services/subagent-viewer-rooms.mjs +119 -0
  140. package/server/services/tunnel-worker-path-guard.mjs +24 -41
  141. package/server/services/upgrade-socket-guard.mjs +16 -0
  142. package/server/services/windows-autostart-service.mjs +6 -1
  143. package/server/services/windows-boot-autostart-service.mjs +207 -23
  144. package/shared/preview-tool-core.mjs +15 -5
  145. package/shared/relay-events.mjs +371 -0
  146. package/shared/remote-relay-contract.mjs +160 -6
  147. package/shared/share-path-privacy.mjs +30 -0
  148. package/shared/subagent-record-retention.mjs +31 -0
  149. package/shared/subagent-timeline.mjs +79 -0
  150. package/shared/windows-system-tools.mjs +106 -0
  151. package/shared/worker-runtime/background-task-digest.mjs +30 -0
  152. package/shared/worker-runtime/heartbeat.mjs +24 -0
  153. package/shared/worker-runtime/kept-posts.mjs +83 -0
  154. package/shared/worker-runtime/worker-websocket-link.mjs +47 -15
@@ -94,6 +94,135 @@ export function restoreTranscriptAnchor(el, anchor) {
94
94
  return Number(el.scrollTop || 0) !== before;
95
95
  }
96
96
 
97
+ /** The blocks of a rendered message a reader reads one at a time. */
98
+ export const READING_BLOCK_SELECTOR = 'p, li, pre, h1, h2, h3, h4, h5, h6, tr';
99
+
100
+ // The live bubble of a running turn: a row under its prompt, with the
101
+ // prompt's message id.
102
+ const LIVE_ROW_ID = 'thinking-indicator';
103
+ // How many rows after the prompt the reply is looked for in.
104
+ const REPLY_SEARCH_ROWS = 3;
105
+
106
+ // A block's text without its controls: a finished reply's code block holds
107
+ // its Copy button, the same block of the streamed reply does not.
108
+ function blockText(block) {
109
+ if (typeof block.querySelector !== 'function' || !block.querySelector('button')) {
110
+ return String(block.textContent || '');
111
+ }
112
+ const copy = block.cloneNode(true);
113
+ copy.querySelectorAll('button').forEach((node) => node.remove());
114
+ return String(copy.textContent || '');
115
+ }
116
+
117
+ function sameBlocks(scope, tag, text) {
118
+ return Array.from(scope.querySelectorAll(tag)).filter((node) => blockText(node) === text);
119
+ }
120
+
121
+ // A block the reader can see. Inside a closed fold (a reply's thoughts, an
122
+ // event row) it cannot be read, yet the browser still gives it a box there:
123
+ // the fold's content is skipped, not removed from the layout.
124
+ function isReadable(block, rect) {
125
+ if (!(rect.bottom - rect.top > 0)) return false;
126
+ if (typeof block.checkVisibility === 'function') return block.checkVisibility();
127
+ return typeof block.closest !== 'function' || !block.closest('details:not([open]) > :not(summary)');
128
+ }
129
+
130
+ /**
131
+ * The block of text the reader is at: the first one that starts inside the
132
+ * viewport, else the one that reaches into it from above (a code block
133
+ * taller than the screen). Finer than the row anchor above, for the times
134
+ * the rows themselves are rebuilt: a reload of the conversation, or the reply
135
+ * that takes the live bubble's place. There the same text comes back in a
136
+ * row whose other parts (folded thoughts, tool lines, the usage line) have
137
+ * another height, and a row or a pixel anchor moves the text by that much.
138
+ *
139
+ * `scope` limits the search to one element (the live bubble's stream);
140
+ * without it the rows that cross the viewport are searched. Returns null
141
+ * when no block is there (the reader looks at an image, or at a card).
142
+ */
143
+ export function captureReadingBlock(el, { scope = null } = {}) {
144
+ if (!el) return null;
145
+ const view = el.getBoundingClientRect();
146
+ const scopes = scope ? [scope] : transcriptRows(el).filter((row) => {
147
+ const rect = row.getBoundingClientRect();
148
+ return rect.bottom > view.top && rect.top < view.bottom;
149
+ });
150
+ let found = null;
151
+ let foundScope = null;
152
+ for (const candidate of scopes) {
153
+ for (const block of candidate.querySelectorAll(READING_BLOCK_SELECTOR)) {
154
+ const rect = block.getBoundingClientRect();
155
+ if (!isReadable(block, rect) || rect.bottom <= view.top) continue;
156
+ if (rect.top >= view.bottom) break;
157
+ found = { block, rect };
158
+ foundScope = candidate;
159
+ // The first block that starts in view ends the search; one that only
160
+ // reaches into it is kept until a later one does.
161
+ if (rect.top >= view.top) break;
162
+ }
163
+ if (found && found.rect.top >= view.top) break;
164
+ }
165
+ if (!found) return null;
166
+ const { block, rect } = found;
167
+ const tag = String(block.tagName || '').toLowerCase();
168
+ const text = blockText(block);
169
+ const height = Math.max(0, rect.bottom - rect.top);
170
+ const offset = rect.top - view.top;
171
+ return {
172
+ messageId: String(foundScope?.dataset?.messageId || '').trim() || null,
173
+ // Read in the live bubble: the reply that replaces it is another row.
174
+ live: foundScope?.id === LIVE_ROW_ID,
175
+ tag,
176
+ text,
177
+ // Which of the row's blocks with this very text (a repeated line).
178
+ occurrence: Math.max(0, sameBlocks(foundScope, tag, text).indexOf(block)),
179
+ offset,
180
+ // Scaled to the block's height when its top is scrolled past, so the
181
+ // same share of it stays hidden if it comes back with another height.
182
+ offsetFraction: offset < 0 && height > 0 ? offset / height : null,
183
+ };
184
+ }
185
+
186
+ // The rows the captured block can be in now. A block read in the live bubble
187
+ // is in that bubble while the turn runs, and in the reply once it has ended:
188
+ // one of the rows that follow the prompt.
189
+ function readingBlockHosts(el, anchor) {
190
+ const messageId = String(anchor.messageId || '').trim();
191
+ if (!messageId) return [];
192
+ const rows = transcriptRows(el);
193
+ const matches = (row) => String(row.dataset?.messageId || '').trim() === messageId;
194
+ if (!anchor.live) return rows.filter((row) => matches(row) && row.id !== LIVE_ROW_ID).slice(0, 1);
195
+ const live = rows.find((row) => matches(row) && row.id === LIVE_ROW_ID);
196
+ if (live) return [live];
197
+ const prompt = rows.findIndex(matches);
198
+ return prompt < 0 ? [] : rows.slice(prompt + 1, prompt + 1 + REPLY_SEARCH_ROWS);
199
+ }
200
+
201
+ /**
202
+ * Scrolls so the captured block sits where it was. It is looked for in
203
+ * `scope` when given (the reply that replaced the live bubble), else in the
204
+ * row it was captured in. Returns false when it is not there; the caller
205
+ * falls back to what it did before.
206
+ */
207
+ export function restoreReadingBlock(el, anchor, { scope = null } = {}) {
208
+ if (!el || !anchor?.tag) return false;
209
+ let block = null;
210
+ for (const host of (scope ? [scope] : readingBlockHosts(el, anchor))) {
211
+ const matches = sameBlocks(host, anchor.tag, anchor.text).filter((node) => isReadable(node, node.getBoundingClientRect()));
212
+ block = matches[anchor.occurrence] || matches[0] || null;
213
+ if (block) break;
214
+ }
215
+ if (!block) return false;
216
+ const view = el.getBoundingClientRect();
217
+ const rect = block.getBoundingClientRect();
218
+ const target = anchor.offsetFraction === null || anchor.offsetFraction === undefined
219
+ ? Number(anchor.offset || 0)
220
+ : Number(anchor.offsetFraction) * Math.max(0, rect.bottom - rect.top);
221
+ const delta = rect.top - view.top - target;
222
+ if (Math.abs(delta) > 0.5) el.scrollTop = Number(el.scrollTop || 0) + delta;
223
+ return true;
224
+ }
225
+
97
226
  const KEYBOARD_INPUT_TYPES = new Set(['', 'text', 'search', 'email', 'url', 'tel', 'number', 'password']);
98
227
 
99
228
  /**
@@ -201,8 +330,19 @@ export function createTranscriptResizeKeeper({
201
330
  return apply(el);
202
331
  }
203
332
 
333
+ // Content landed under a reader who followed the end and was not followed
334
+ // (no scroll event tells): the position is what it is now, not "the end".
335
+ function recordEndLeft() {
336
+ if (settleTimer !== null) return false;
337
+ const el = element();
338
+ const next = el ? capture(el) : null;
339
+ if (next) committed = next;
340
+ return Boolean(next);
341
+ }
342
+
204
343
  return {
205
344
  recordScroll,
345
+ recordEndLeft,
206
346
  handleResize,
207
347
  isSettling: () => settleTimer !== null,
208
348
  committedAnchor: () => committed,
@@ -100,7 +100,7 @@ export function renderUpdateSection() {
100
100
  status.textContent = install.state === 'running'
101
101
  ? `Updating to ${install.targetVersion}…`
102
102
  : install.state === 'success'
103
- ? 'npm finished — the relay restarts when the queue is idle.'
103
+ ? 'npm finished — the relay restarts once the running turns finish.'
104
104
  : `Update failed: ${install.error || 'unknown error'}`;
105
105
  }
106
106
  const logPre = byId('update-install-log-pre');
@@ -9,21 +9,23 @@
9
9
  export const VIEWER_HISTORY_MARK = 'oarFileViewer';
10
10
  const OWN_POP_WINDOW_MS = 1500;
11
11
 
12
- export function createViewerHistory({ history, onBack, now = () => Date.now() } = {}) {
12
+ // `mark` names the viewer's entry: two viewers (the file viewer and the
13
+ // sub-agent viewer) must never take each other's entry for their own.
14
+ export function createViewerHistory({ history, onBack, now = () => Date.now(), mark = VIEWER_HISTORY_MARK } = {}) {
13
15
  const state = { pushed: false, ownPopAt: 0 };
14
16
  return {
15
17
  get pushed() { return state.pushed; },
16
18
  /** The viewer came up: one entry, however many files it then shows. */
17
19
  opened() {
18
20
  if (state.pushed) return;
19
- history.pushState({ [VIEWER_HISTORY_MARK]: true }, '');
21
+ history.pushState({ [mark]: true }, '');
20
22
  state.pushed = true;
21
23
  },
22
24
  /** The viewer went away by itself: take its entry back out of history. */
23
25
  closed() {
24
26
  if (!state.pushed) return;
25
27
  state.pushed = false;
26
- if (history.state?.[VIEWER_HISTORY_MARK]) {
28
+ if (history.state?.[mark]) {
27
29
  state.ownPopAt = now();
28
30
  history.back();
29
31
  }