@oar-sh/oar 0.9.21 โ†’ 0.9.22

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 (69) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/README.md +9 -9
  3. package/package.json +1 -1
  4. package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +9 -1
  5. package/server/claude-worker/claude-sdk-adapter.mjs +149 -4
  6. package/server/claude-worker/claude-session-process.mjs +9 -4
  7. package/server/claude-worker/claude-session-worker-main.mjs +10 -1
  8. package/server/copilot-worker/copilot-sdk-session-process.mjs +1 -0
  9. package/server/copilot-worker/copilot-sdk-session-worker.mjs +10 -1
  10. package/server/cursor-worker/cursor-session-worker.mjs +9 -1
  11. package/server/db-schema.mjs +19 -0
  12. package/server/grok-worker/grok-background-tasks.mjs +1 -0
  13. package/server/grok-worker/grok-session-worker.mjs +10 -1
  14. package/server/grok-worker/grok-turn-runner.mjs +1 -0
  15. package/server/migrations/0037-relay-asks-queue-id.mjs +66 -0
  16. package/server/migrations/0038-turn-seq.mjs +92 -0
  17. package/server/public/app/activity-replay-state.mjs +160 -42
  18. package/server/public/app/api-client.js +7 -0
  19. package/server/public/app/ask-user-view.js +4 -3
  20. package/server/public/app/bootstrap.js +62 -4
  21. package/server/public/app/chat-actions-menu-state.mjs +2 -0
  22. package/server/public/app/context-usage-view.mjs +68 -0
  23. package/server/public/app/conversation-view.js +306 -49
  24. package/server/public/app/header-priority-actions.mjs +5 -1
  25. package/server/public/app/held-details.mjs +202 -0
  26. package/server/public/app/inbox-ui.js +70 -79
  27. package/server/public/app/inbox-view.mjs +35 -19
  28. package/server/public/app/journal-view.js +17 -0
  29. package/server/public/app/live-activity-cap.mjs +103 -0
  30. package/server/public/app/questions-panel-ui.js +634 -0
  31. package/server/public/app/questions-stub-view.mjs +40 -0
  32. package/server/public/app/questions-view.mjs +89 -0
  33. package/server/public/app/relay-board-view.js +12 -2
  34. package/server/public/app/relay-thoughts.mjs +15 -1
  35. package/server/public/app/session-history-ui.js +1 -0
  36. package/server/public/app/settings-modal.js +6 -0
  37. package/server/public/app/socket-handlers.js +42 -11
  38. package/server/public/app/stream-view-preference.mjs +60 -0
  39. package/server/public/app/subagent-fold.mjs +6 -3
  40. package/server/public/app/subagent-timeline-view.mjs +12 -5
  41. package/server/public/app/subagent-viewer.mjs +8 -6
  42. package/server/public/app/thoughts-preference.mjs +79 -0
  43. package/server/public/app/turn-stream-view.mjs +124 -0
  44. package/server/public/index.html +391 -214
  45. package/server/repositories/message-repository.mjs +10 -0
  46. package/server/repositories/question-repository.mjs +51 -7
  47. package/server/repositories/relay-turn-rows.mjs +56 -0
  48. package/server/routes/ask-user-routes.mjs +34 -5
  49. package/server/routes/messages-routes.mjs +35 -14
  50. package/server/routes/relay-ask-routes.mjs +15 -5
  51. package/server/routes/sessions-routes.mjs +76 -5
  52. package/server/server-runtime.mjs +178 -131
  53. package/server/services/cloudflared-tunnel-service.mjs +7 -0
  54. package/server/services/idle-timers.mjs +184 -0
  55. package/server/services/queue-wake-hook.mjs +145 -0
  56. package/server/services/queue-waker.mjs +127 -0
  57. package/server/services/relay-ask-service.mjs +64 -5
  58. package/server/services/relay-event-service.mjs +4 -1
  59. package/server/services/remote-relay-dispatcher.mjs +23 -4
  60. package/server/services/session-history-refresh-service.mjs +4 -0
  61. package/server/services/session-worker-supervisor-service.mjs +36 -0
  62. package/server/services/session-worker-websocket-service.mjs +143 -15
  63. package/server/services/ssh-tunnel-manager-service.mjs +7 -0
  64. package/shared/ask-user-bridge.mjs +9 -6
  65. package/shared/relay-asks.mjs +10 -0
  66. package/shared/relay-events.mjs +1 -0
  67. package/shared/worker-runtime/heartbeat.mjs +26 -0
  68. package/shared/worker-runtime/worker-keepalive.mjs +108 -0
  69. package/shared/worker-runtime/worker-websocket-link.mjs +21 -10
@@ -63,6 +63,9 @@ import {
63
63
  } from './transcript-viewport-anchor.mjs';
64
64
  import { initTranscriptJumpToBottom, onTranscriptEndLeft } from './transcript-jump-bottom-view.mjs';
65
65
  import { syncScrollOnSendToggle, updateScrollOnSendSetting } from './transcript-jump-bottom.mjs';
66
+ import { isThoughtsExpanded, syncThoughtsExpandedToggle, updateThoughtsExpandedSetting } from './thoughts-preference.mjs';
67
+ import { previewLiveActivityCap, syncLiveActivityCapSlider, updateLiveActivityCap } from './live-activity-cap.mjs';
68
+ import { STREAM_VIEW_CHANGE_EVENT, isStreamViewEnabled, syncStreamViewToggle, updateStreamViewSetting } from './stream-view-preference.mjs';
66
69
  import { previewWindowRetention, syncWindowRetentionSlider, updateWindowRetention } from './conversation-window-retention.mjs';
67
70
  import { conversationPayloadCache, mergeReloadIntoWindow } from './conversation-payload-cache.mjs';
68
71
  import {
@@ -93,7 +96,7 @@ import {
93
96
  reportSharedViewerPresence,
94
97
  setNetworkRequestsEnabled,
95
98
  } from './api-client.js';
96
- import { loadConversations, refreshConversations, openConversation, renderConvList, applyLoadedConversationState, initConversationListLazyLoading, initConversationFilter } from './journal-view.js';
99
+ import { loadConversations, refreshConversations, openConversation, renderConvList, applyLoadedConversationState, heldDetailsStore, initConversationListLazyLoading, initConversationFilter } from './journal-view.js';
97
100
  import {
98
101
  newConversation,
99
102
  confirmNewConversationModel,
@@ -150,6 +153,7 @@ import {
150
153
  getHeldBackLiveRowIds,
151
154
  captureConversationReadingAnchor,
152
155
  canMergeIntoTranscript,
156
+ refreshStreamViewLayout,
153
157
  } from './conversation-view.js';
154
158
  import { loadKeepingLiveRows } from './transcript-reload-freshness.mjs';
155
159
  import { backgroundTasksSnapshotMark } from './background-tasks-view.mjs';
@@ -250,6 +254,8 @@ import { closeAnnotateEditor } from './annotate-editor.js';
250
254
  import { closeEmojiPicker } from './emoji-view.js';
251
255
  import { closeChangeCwdMenu } from './cwd-picker.js';
252
256
  import { closeRelayWatchPanel } from './relay-event-subscriptions-ui.js';
257
+ import { closeQuestionsPanel, initQuestionsPanel, openQuestionsPanel, setQuestionsConversation } from './questions-panel-ui.js';
258
+ import { questionsTwinLabel } from './questions-view.mjs';
253
259
  import { closeRelaySchedulePanel } from './relay-schedules-ui.js';
254
260
  import { isInboxViewActive } from './inbox-ui.js';
255
261
  import { initScrollbarHover } from './scrollbar-hover.mjs';
@@ -261,6 +267,8 @@ import {
261
267
  renderAutoCompactNotesHtml,
262
268
  renderContextUsageHtml,
263
269
  renderThinkingControlHtml,
270
+ renderThoughtsPreferenceHtml,
271
+ renderStreamViewPreferenceHtml,
264
272
  thinkingEnabledFromKey,
265
273
  thinkingEnabledToKey,
266
274
  } from './context-usage-view.mjs';
@@ -3702,6 +3710,18 @@ function initThinkingControl() {
3702
3710
  if (!body || body.dataset.thinkingBound === '1') return;
3703
3711
  body.dataset.thinkingBound = '1';
3704
3712
  body.addEventListener('click', (event) => {
3713
+ // ๐Ÿงต Stream view and ๐Ÿ’ญ Thoughts: settings of this browser, nothing goes
3714
+ // to the relay. (The stream view's buttons share the thoughts' look.)
3715
+ const streamViewBtn = event.target?.closest?.('.ctx-stream-view-btn');
3716
+ if (streamViewBtn && body.contains(streamViewBtn)) {
3717
+ updateStreamViewSetting(streamViewBtn.dataset.streamView === 'on');
3718
+ return;
3719
+ }
3720
+ const thoughtsBtn = event.target?.closest?.('.ctx-thoughts-btn[data-thoughts-expanded]');
3721
+ if (thoughtsBtn && body.contains(thoughtsBtn)) {
3722
+ updateThoughtsExpandedSetting(thoughtsBtn.dataset.thoughtsExpanded !== 'off');
3723
+ return;
3724
+ }
3705
3725
  const btn = event.target?.closest?.('.ctx-thinking-btn');
3706
3726
  if (!btn || !body.contains(btn)) return;
3707
3727
  void applyThinkingSelection(btn.dataset.thinkingAxis, btn.dataset.thinkingValue);
@@ -3861,6 +3881,10 @@ async function loadContextSummaryAndRender(convId) {
3861
3881
  thinkingDisplay: payload.thinkingDisplay ?? null,
3862
3882
  })
3863
3883
  : '';
3884
+ // Every provider streams thoughts somewhere; the row mirrors the Settings
3885
+ // toggle and is this browser's own.
3886
+ const thoughtsHtml = renderThoughtsPreferenceHtml({ expanded: isThoughtsExpanded() })
3887
+ + renderStreamViewPreferenceHtml({ on: isStreamViewEnabled() });
3864
3888
  // What this folder's commits say, with the folder override: Claude (on
3865
3889
  // either billing), and a Claude Cloud chat that was started from a folder
3866
3890
  // (no path, no row).
@@ -3885,10 +3909,10 @@ async function loadContextSummaryAndRender(convId) {
3885
3909
  // The structured breakdown is the answer; the runtime's own text dump stays
3886
3910
  // available underneath for the details it carries that categories don't.
3887
3911
  const bodyHtml = usageHtml
3888
- ? `${usageHtml}${autoCompactHtml}${thinkingHtml}${attributionHtml}${agentSessionsHtml}${detailText
3912
+ ? `${usageHtml}${autoCompactHtml}${thinkingHtml}${thoughtsHtml}${attributionHtml}${agentSessionsHtml}${detailText
3889
3913
  ? `<details class="ctx-usage-raw"><summary>Raw details</summary><pre>${escHtml(detailText)}</pre></details>`
3890
3914
  : ''}`
3891
- : `${autoCompactHtml}${thinkingHtml}${attributionHtml}${agentSessionsHtml}<pre>${escHtml(detailText || 'No context data available.')}</pre>`;
3915
+ : `${autoCompactHtml}${thinkingHtml}${thoughtsHtml}${attributionHtml}${agentSessionsHtml}<pre>${escHtml(detailText || 'No context data available.')}</pre>`;
3892
3916
 
3893
3917
  renderSummaryModalContent({
3894
3918
  title: 'Context usage',
@@ -4130,12 +4154,17 @@ async function pollAuthenticatedCurrentConversationLive() {
4130
4154
  const requestLimit = Math.max(20, getConversationLoadedMessageCount() || 0, savedLoadedCount || 0);
4131
4155
  const tasksMark = backgroundTasksSnapshotMark(currentId);
4132
4156
  const jumpEpoch = getTranscriptJumpEpoch();
4157
+ // The finished replies this page already holds go out without their
4158
+ // tool lines and thoughts (held-details.mjs); the answer gets them back
4159
+ // from the store, or is dropped if it names one the store lacks.
4160
+ const heldDetails = heldDetailsStore.param(currentId);
4133
4161
  const { response, stale } = await loadKeepingLiveRows(
4134
- () => loadConversation(currentId, { limit: requestLimit }),
4162
+ () => loadConversation(currentId, { limit: requestLimit, heldDetails }),
4135
4163
  getTranscriptLiveRowEpoch,
4136
4164
  );
4137
4165
  if (!response) return;
4138
4166
  if (String(currentConvId || '').trim() !== currentId) return;
4167
+ if (!heldDetailsStore.restore(currentId, response)) return;
4139
4168
  // The user jumped into the history while this request was on its way: the
4140
4169
  // answer is the end of the conversation as it was asked for before that,
4141
4170
  // and would overwrite the window they jumped to. The next round asks again.
@@ -4155,6 +4184,7 @@ async function pollAuthenticatedCurrentConversationLive() {
4155
4184
  followLiveUpdates: preserveBottom,
4156
4185
  mergeFrom: merge?.mergeFrom || null,
4157
4186
  backgroundTasksAsOf: tasksMark,
4187
+ fromPoll: true,
4158
4188
  });
4159
4189
  // A merge does not scroll as the full render does: a reader at the end
4160
4190
  // is taken to the new end here.
@@ -4600,6 +4630,7 @@ function headerPriorityActionAvailability() {
4600
4630
  const fullscreen = document.getElementById('fullscreen-btn');
4601
4631
  return resolveHeaderActionAvailability({
4602
4632
  sharedMode: isSharedReaderMode(),
4633
+ questionCount: Number(document.getElementById('questions-btn')?.dataset.count || 0),
4603
4634
  pinCount: Number(pins?.dataset.count || 0),
4604
4635
  hiddenCount: Number(hiddenBtn?.dataset.count || 0),
4605
4636
  fullscreenAvailable: fullscreen?.dataset.headerAvailable === '1',
@@ -4676,6 +4707,8 @@ function headerFitSignature(header, availability) {
4676
4707
  banner?.className, banner?.hidden, banner?.textContent,
4677
4708
  document.getElementById('sidebar-toggle')?.hidden,
4678
4709
  document.getElementById('chat-actions-menu-btn')?.hidden,
4710
+ document.getElementById('questions-btn')?.dataset.count,
4711
+ document.getElementById('questions-btn')?.dataset.urgent,
4679
4712
  document.getElementById('pinned-messages-btn')?.dataset.count,
4680
4713
  document.getElementById('hidden-messages-btn')?.dataset.count,
4681
4714
  document.getElementById('fullscreen-btn')?.textContent,
@@ -4782,6 +4815,14 @@ function syncHeaderPriorityTwinLabels() {
4782
4815
  const count = Number(document.getElementById(buttonId)?.dataset.count || 0);
4783
4816
  twin.textContent = count ? `${icon} ${text} (${count})` : `${icon} ${text}`;
4784
4817
  };
4818
+ const questions = document.getElementById('questions-btn');
4819
+ const questionsTwin = document.getElementById('chat-menu-questions');
4820
+ if (questionsTwin) {
4821
+ questionsTwin.textContent = questionsTwinLabel({
4822
+ pending: Number(questions?.dataset.count || 0),
4823
+ urgent: Number(questions?.dataset.urgent || 0),
4824
+ });
4825
+ }
4785
4826
  label('pinned-messages-btn', 'chat-menu-pins', 'Pinned messages', '๐Ÿ“');
4786
4827
  label('hidden-messages-btn', 'chat-menu-hidden', 'Hidden from shared viewers', '๐Ÿ™ˆ');
4787
4828
  }
@@ -4812,6 +4853,11 @@ function scheduleHeaderPrioritySync({ force = false } = {}) {
4812
4853
  // syncChatTitleControls.
4813
4854
  function initHeaderPriorityActions({ sharedMode = false } = {}) {
4814
4855
  if (!sharedMode) {
4856
+ // ๐Ÿ““'s own button toggles the list under it (questions-panel-ui.js); the
4857
+ // twin opens it under ๏ธ™.
4858
+ bindHeaderPriorityAction('questions', () => {
4859
+ openQuestionsPanel({ anchor: document.getElementById('chat-actions-menu-btn') });
4860
+ }, { inline: false });
4815
4861
  bindHeaderPriorityAction('pins', () => openPinnedMessagesModal(), { inline: false });
4816
4862
  bindHeaderPriorityAction('hidden', () => openHiddenMessagesModal(), { inline: false });
4817
4863
  bindHeaderPriorityAction('search', () => openMessageSearchModal(), { inline: false });
@@ -5052,6 +5098,7 @@ function syncChatTitleControls() {
5052
5098
  if (!convId) {
5053
5099
  setPinsConversation(null);
5054
5100
  setHiddenMessagesConversation(null);
5101
+ setQuestionsConversation(null);
5055
5102
  }
5056
5103
  const conversation = convId ? (conversations[convId] || null) : null;
5057
5104
  const sdkSessionId = String(conversation?.sdkSessionId || '').trim();
@@ -5333,6 +5380,9 @@ async function initApp() {
5333
5380
  if (!sharedMode) {
5334
5381
  syncSuspendHostVisibility();
5335
5382
  syncScrollOnSendToggle();
5383
+ syncThoughtsExpandedToggle();
5384
+ syncLiveActivityCapSlider();
5385
+ syncStreamViewToggle();
5336
5386
  syncWindowRetentionSlider();
5337
5387
  initHostSuspendUi();
5338
5388
  initUsageLimitUi();
@@ -5734,6 +5784,7 @@ async function initApp() {
5734
5784
  void loadRelaySchedules();
5735
5785
  // The Inbox's count, and the cards of questions that don't block.
5736
5786
  startInbox();
5787
+ initQuestionsPanel();
5737
5788
  if (pushConversationId) {
5738
5789
  await openConversation(pushConversationId, { history: 'replace' }).catch(() => {});
5739
5790
  }
@@ -5812,6 +5863,7 @@ async function bootstrap() {
5812
5863
  closeTmuxInspector: closeTmuxInspectorView,
5813
5864
  closeAnnotateEditor,
5814
5865
  closeRelayWatchPanel,
5866
+ closeQuestionsPanel,
5815
5867
  closeRelaySchedulePanel,
5816
5868
  closeEmojiPicker,
5817
5869
  });
@@ -5900,6 +5952,12 @@ window.resetGrokAllowanceAccounting = resetGrokAllowanceAccounting;
5900
5952
  window.toggleCursorProvider = toggleCursorProvider;
5901
5953
  window.updateShowSuspendHostSetting = updateShowSuspendHostSetting;
5902
5954
  window.updateScrollOnSendSetting = (enabled) => updateScrollOnSendSetting(enabled);
5955
+ window.updateThoughtsExpandedSetting = (expanded) => updateThoughtsExpandedSetting(expanded);
5956
+ window.updateStreamViewSetting = (on) => updateStreamViewSetting(on);
5957
+ // The transcript and the live bubble take the other layout at once.
5958
+ window.addEventListener(STREAM_VIEW_CHANGE_EVENT, () => refreshStreamViewLayout());
5959
+ window.previewLiveActivityCap = (position) => previewLiveActivityCap(position);
5960
+ window.updateLiveActivityCap = (position) => updateLiveActivityCap(position);
5903
5961
  window.previewConversationWindowRetention = (position) => previewWindowRetention(position);
5904
5962
  // A lower cap cuts the kept windows down now; the open conversation's is cut
5905
5963
  // when it is left.
@@ -47,6 +47,7 @@ export const CHAT_ACTIONS_MENU_ENTRY_IDS = Object.freeze([
47
47
  'chat-menu-schedules',
48
48
  'chat-menu-origin',
49
49
  'chat-menu-edit-title',
50
+ 'chat-menu-questions',
50
51
  'chat-menu-pins',
51
52
  'chat-menu-hidden',
52
53
  'chat-menu-search',
@@ -82,6 +83,7 @@ const ALWAYS_HIDDEN_IDS = new Set(['chat-menu-compact', 'chat-menu-refresh-histo
82
83
  * with HEADER_PRIORITY_MENU_IDS by the tests.
83
84
  */
84
85
  export const CHAT_ACTIONS_MENU_TWIN_IDS = Object.freeze([
86
+ 'chat-menu-questions',
85
87
  'chat-menu-pins',
86
88
  'chat-menu-hidden',
87
89
  'chat-menu-search',
@@ -350,6 +350,74 @@ ${renderThinkingButtons('display', THINKING_DISPLAY_CHOICES, displayKey)}
350
350
  `;
351
351
  }
352
352
 
353
+ const THOUGHTS_EXPANDED_CHOICES = Object.freeze([
354
+ { key: 'on', label: 'Expanded' },
355
+ { key: 'off', label: 'Collapsed' },
356
+ ]);
357
+
358
+ /**
359
+ * The mirror of Settings โ€บ General "Expand thoughts while an agent works", for
360
+ * every provider. A setting of this browser, so nothing goes to the relay:
361
+ * the buttons write thoughts-preference.mjs and both places follow it.
362
+ *
363
+ * @returns {string} HTML
364
+ */
365
+ export function renderThoughtsPreferenceHtml({ expanded = true } = {}) {
366
+ const activeKey = expanded === false ? 'off' : 'on';
367
+ const buttons = THOUGHTS_EXPANDED_CHOICES.map(({ key, label }) => `
368
+ <button
369
+ type="button"
370
+ class="ctx-thoughts-btn${key === activeKey ? ' is-active' : ''}"
371
+ data-thoughts-expanded="${key}"
372
+ aria-pressed="${key === activeKey ? 'true' : 'false'}"
373
+ >${escapeHtml(label)}</button>`).join('');
374
+ return `
375
+ <div class="ctx-thinking ctx-thoughts">
376
+ <div class="ctx-thinking-row">
377
+ <span class="ctx-thinking-label">๐Ÿ’ญ Thoughts</span>
378
+ <div class="ctx-thinking-group" role="group" aria-label="Thoughts while an agent works">
379
+ ${buttons}
380
+ </div>
381
+ </div>
382
+ <div class="ctx-thinking-note">Whether thoughts open while an agent works, in sub-agent bubbles and in the sub-agent viewer. A thought you fold or open by hand stays that way. Saved in this browser.</div>
383
+ </div>
384
+ `;
385
+ }
386
+
387
+ const STREAM_VIEW_CHOICES = Object.freeze([
388
+ { key: 'on', label: 'On' },
389
+ { key: 'off', label: 'Off' },
390
+ ]);
391
+
392
+ /**
393
+ * The mirror of Settings โ€บ General "Stream view", for every provider. A
394
+ * setting of this browser (stream-view-preference.mjs); nothing goes to the
395
+ * relay.
396
+ *
397
+ * @returns {string} HTML
398
+ */
399
+ export function renderStreamViewPreferenceHtml({ on = false } = {}) {
400
+ const activeKey = on === true ? 'on' : 'off';
401
+ const buttons = STREAM_VIEW_CHOICES.map(({ key, label }) => `
402
+ <button
403
+ type="button"
404
+ class="ctx-thoughts-btn ctx-stream-view-btn${key === activeKey ? ' is-active' : ''}"
405
+ data-stream-view="${key}"
406
+ aria-pressed="${key === activeKey ? 'true' : 'false'}"
407
+ >${escapeHtml(label)}</button>`).join('');
408
+ return `
409
+ <div class="ctx-thinking ctx-stream-view">
410
+ <div class="ctx-thinking-row">
411
+ <span class="ctx-thinking-label">๐Ÿงต Stream view</span>
412
+ <div class="ctx-thinking-group" role="group" aria-label="Stream view">
413
+ ${buttons}
414
+ </div>
415
+ </div>
416
+ <div class="ctx-thinking-note">Thoughts, tool calls and the reply of a turn as one list in the order they happened. Turns recorded before this version keep the grouped layout. Saved in this browser.</div>
417
+ </div>
418
+ `;
419
+ }
420
+
353
421
  /**
354
422
  * The owner's switch for a session an agent started: may its agents start
355
423
  * sessions themselves (the relay_tool's one-level rule lifted for it)?