@oar-sh/oar 0.9.8 → 0.9.10

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 (107) hide show
  1. package/CHANGELOG.md +478 -0
  2. package/README.md +129 -39
  3. package/bin/bat/oar.bat +46 -46
  4. package/bin/oar.js +814 -228
  5. package/package.json +5 -6
  6. package/server/README.md +306 -4
  7. package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
  8. package/server/claude-worker/claude-session-process.mjs +35 -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 +3 -1
  13. package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
  14. package/server/db-schema.mjs +44 -0
  15. package/server/migrations/0008-message-pins.mjs +85 -0
  16. package/server/migrations/0009-subagent-timeline.mjs +135 -0
  17. package/server/migrations/0010-subagent-timeline-pruning.mjs +89 -0
  18. package/server/migrations/0011-relay-event-subscriptions.mjs +125 -0
  19. package/server/migrations/0012-cross-relay-event-subscriptions.mjs +116 -0
  20. package/server/public/app/agent-exchange-fold-view.mjs +248 -0
  21. package/server/public/app/api-client.js +91 -0
  22. package/server/public/app/archive-confirm.mjs +28 -0
  23. package/server/public/app/ask-user-view.js +3 -0
  24. package/server/public/app/attachments-view.js +1024 -131
  25. package/server/public/app/back-guard.mjs +58 -0
  26. package/server/public/app/background-tasks-view.mjs +18 -26
  27. package/server/public/app/bootstrap.js +230 -35
  28. package/server/public/app/chat-actions-menu-state.mjs +116 -0
  29. package/server/public/app/claude-cloud-conversation-ui.js +10 -22
  30. package/server/public/app/composer-control-state.mjs +16 -0
  31. package/server/public/app/composer-keyboard.mjs +54 -0
  32. package/server/public/app/conversation-view.js +471 -110
  33. package/server/public/app/explorer-edit-state.mjs +68 -0
  34. package/server/public/app/external-link-policy.mjs +4 -1
  35. package/server/public/app/file-version.mjs +58 -0
  36. package/server/public/app/host-suspend-ui.js +6 -3
  37. package/server/public/app/journal-view.js +161 -27
  38. package/server/public/app/no-conversation-state.mjs +58 -0
  39. package/server/public/app/pinned-messages-view.mjs +253 -0
  40. package/server/public/app/provider-readiness.mjs +120 -0
  41. package/server/public/app/pwa-install.js +1 -1
  42. package/server/public/app/relay-board-view.js +3 -0
  43. package/server/public/app/relay-dot-state.mjs +8 -1
  44. package/server/public/app/relay-event-subscriptions-ui.js +184 -0
  45. package/server/public/app/relay-event-view.mjs +234 -0
  46. package/server/public/app/router.js +8 -2
  47. package/server/public/app/settings-modal.js +53 -1
  48. package/server/public/app/socket-handlers.js +65 -28
  49. package/server/public/app/store.js +25 -2
  50. package/server/public/app/subagent-row.mjs +138 -0
  51. package/server/public/app/subagent-timeline-view.mjs +186 -0
  52. package/server/public/app/subagent-viewer-watch.mjs +50 -0
  53. package/server/public/app/subagent-viewer.mjs +534 -0
  54. package/server/public/app/task-row-format.mjs +29 -0
  55. package/server/public/app/thinking-follow.js +4 -0
  56. package/server/public/app/transcript-jump-bottom-view.mjs +290 -0
  57. package/server/public/app/transcript-jump-bottom.mjs +99 -0
  58. package/server/public/app/transcript-reload-freshness.mjs +25 -0
  59. package/server/public/app/update-ui.js +1 -1
  60. package/server/public/app/viewer-chat-gallery.mjs +59 -0
  61. package/server/public/app/viewer-controls-state.mjs +59 -0
  62. package/server/public/app/viewer-gallery.mjs +46 -0
  63. package/server/public/app/viewer-gestures.mjs +105 -0
  64. package/server/public/app/viewer-history.mjs +45 -0
  65. package/server/public/app/viewer-tap.mjs +112 -0
  66. package/server/public/index.html +700 -59
  67. package/server/repositories/message-pin-statements.mjs +39 -0
  68. package/server/repositories/message-repository.mjs +34 -3
  69. package/server/repositories/question-repository.mjs +9 -3
  70. package/server/repositories/remote-relay-repository.mjs +6 -2
  71. package/server/repositories/subagent-timeline-statements.mjs +170 -0
  72. package/server/routes/ask-user-routes.mjs +3 -0
  73. package/server/routes/messages-routes.mjs +548 -111
  74. package/server/routes/relay-event-routes.mjs +157 -0
  75. package/server/routes/remote-relay-routes.mjs +15 -0
  76. package/server/routes/sessions-routes.mjs +164 -24
  77. package/server/server-runtime.mjs +274 -62
  78. package/server/services/cloudflared-tunnel-service.mjs +139 -41
  79. package/server/services/explorer-entry-name.mjs +26 -0
  80. package/server/services/explorer-mutations.mjs +122 -0
  81. package/server/services/file-serving.mjs +155 -0
  82. package/server/services/message-pin-list.mjs +52 -0
  83. package/server/services/oar-cli-helpers.mjs +139 -4
  84. package/server/services/oar-cli-menu.mjs +240 -0
  85. package/server/services/oar-cli-wsl.mjs +129 -0
  86. package/server/services/preview-proxy-server.mjs +41 -23
  87. package/server/services/push-dispatch-service.mjs +18 -0
  88. package/server/services/relay-event-link-service.mjs +289 -0
  89. package/server/services/relay-event-service.mjs +1935 -0
  90. package/server/services/relay-shutdown-gate.mjs +31 -0
  91. package/server/services/remote-relay-caller-context.mjs +52 -7
  92. package/server/services/remote-relay-dispatcher.mjs +494 -2
  93. package/server/services/remote-relay-registry-service.mjs +11 -1
  94. package/server/services/safe-served-content.mjs +61 -11
  95. package/server/services/session-activity-tracker.mjs +487 -0
  96. package/server/services/session-history-refresh-service.mjs +31 -0
  97. package/server/services/stale-attempt-log-limiter.mjs +96 -0
  98. package/server/services/stale-recovery-clock-guard.mjs +57 -0
  99. package/server/services/stale-subagent-run-sweep.mjs +109 -0
  100. package/server/services/subagent-record-retention.mjs +164 -0
  101. package/server/services/subagent-timeline-service.mjs +328 -0
  102. package/server/services/subagent-viewer-rooms.mjs +119 -0
  103. package/server/services/upgrade-socket-guard.mjs +16 -0
  104. package/shared/relay-events.mjs +371 -0
  105. package/shared/remote-relay-contract.mjs +157 -5
  106. package/shared/subagent-record-retention.mjs +31 -0
  107. package/shared/subagent-timeline.mjs +79 -0
@@ -0,0 +1,58 @@
1
+ // In the installed app, the system back button with nothing left to go back
2
+ // to closes the app. There is no way for a page to send itself to the
3
+ // background instead, but it can make sure that moment never comes: one
4
+ // entry marked as the floor is kept beneath the app's own entry, and when
5
+ // back lands on the floor the app's entry is pushed again at once. Back
6
+ // still closes the file viewer (which pushes its own entry above); only the
7
+ // last step out is swallowed. A browser tab keeps its back button: this is
8
+ // for the installed app alone.
9
+
10
+ export const BACK_GUARD_FLOOR = 'oarBackGuardFloor';
11
+ export const BACK_GUARD_APP = 'oarApp';
12
+ const FIRST_INTERACTION_EVENTS = ['pointerdown', 'keydown', 'touchstart'];
13
+
14
+ /**
15
+ * Lay the floor at the first touch or key, not at start-up: an entry a page
16
+ * leaves through pushState before the user has done anything is one the
17
+ * browser skips on back (its guard against pages that hijack the button),
18
+ * so a floor laid at start-up would be stepped over. Before the first
19
+ * interaction nothing a page does keeps back from leaving; from then on
20
+ * the floor holds.
21
+ */
22
+ export function armBackGuardOnFirstInteraction({ history, enabled = true, target, onFloor = null } = {}) {
23
+ if (!enabled || !history || !target) return null;
24
+ const state = { guard: null };
25
+ const arm = () => {
26
+ for (const eventName of FIRST_INTERACTION_EVENTS) target.removeEventListener(eventName, arm, true);
27
+ if (state.guard) return;
28
+ state.guard = installBackGuard({ history, enabled: true, onFloor });
29
+ target.addEventListener('popstate', () => state.guard.onPopState());
30
+ };
31
+ for (const eventName of FIRST_INTERACTION_EVENTS) target.addEventListener(eventName, arm, true);
32
+ return { get armed() { return Boolean(state.guard); } };
33
+ }
34
+
35
+ /**
36
+ * `onFloor` is told each time back lands on the floor and is swallowed.
37
+ * The entry pushed again then is pushed without a touch, so the browser
38
+ * lets the next back leave unless the user touches the app in between:
39
+ * one swallowed press, then the app closes — the pattern phones use, and
40
+ * the moment to say "press back again to close".
41
+ */
42
+ export function installBackGuard({ history, enabled = true, onFloor = null } = {}) {
43
+ if (!enabled || !history) return null;
44
+ const current = history.state && typeof history.state === 'object' ? history.state : {};
45
+ // After a reload on the app's entry the floor already lies beneath it.
46
+ if (!current[BACK_GUARD_FLOOR] && !current[BACK_GUARD_APP]) {
47
+ history.replaceState({ ...current, [BACK_GUARD_FLOOR]: true }, '');
48
+ history.pushState({ [BACK_GUARD_APP]: true }, '');
49
+ }
50
+ return {
51
+ /** Feed every popstate here. */
52
+ onPopState() {
53
+ if (!history.state?.[BACK_GUARD_FLOOR]) return;
54
+ history.pushState({ [BACK_GUARD_APP]: true }, '');
55
+ onFloor?.();
56
+ },
57
+ };
58
+ }
@@ -5,8 +5,12 @@
5
5
  // `backgroundTasks` on reload; hidden whenever the set is empty.
6
6
 
7
7
  import { apiFetch } from './api-client.js';
8
- import { humanizeModelLabel } from './model-selector-options.mjs';
8
+ import { formatTaskModelLabel, formatTaskTokens } from './task-row-format.mjs';
9
9
  import { prefixToolActivityEmoji } from './tool-activity-emoji.mjs';
10
+
11
+ // Shared with the sub-agent rows (subagent-row.mjs); re-exported for the
12
+ // transcript's finished-workflow card and the existing importers.
13
+ export { formatTaskModelLabel, formatTaskTokens };
10
14
  import {
11
15
  getPreviewsForConversation,
12
16
  renderPreviewRowsInto,
@@ -71,16 +75,6 @@ function taskKindLabel(task) {
71
75
  return label === 'Subagent' && subagentType ? subagentType : label;
72
76
  }
73
77
 
74
- // "claude-opus-5-5" → "Opus 5.5". A bare alias ("opus") is what a spawn pin
75
- // carries until the subagent's first answer names the real model.
76
- export function formatTaskModelLabel(model) {
77
- const value = String(model || '').trim();
78
- if (!value) return '';
79
- if (/^claude-/i.test(value)) return humanizeModelLabel(value);
80
- if (/^[a-z]+(\[[^\]]+\])?$/i.test(value)) return value.charAt(0).toUpperCase() + value.slice(1);
81
- return humanizeModelLabel(value);
82
- }
83
-
84
78
  // Detail line: the live tool call when the worker saw one ("🪓 Tool
85
79
  // (PowerShell): cd …", cropped to the row by CSS), else the SDK's progress
86
80
  // summary, else the bare tool name.
@@ -93,20 +87,6 @@ export function taskActivityText(task) {
93
87
  return { text: lastToolName ? `using ${lastToolName}` : '', toolCall: false };
94
88
  }
95
89
 
96
- // Compact token counts: "812 tok", "62.2k tok", "1.2M tok". Exported for the
97
- // transcript's finished-workflow card, which shares the panel's formatting.
98
- export function formatTaskTokens(totalTokens) {
99
- const tokens = Number(totalTokens);
100
- if (!Number.isFinite(tokens) || tokens <= 0) return '';
101
- if (tokens < 1000) return `${tokens} tok`;
102
- if (tokens < 1_000_000) {
103
- const thousands = tokens / 1000;
104
- return `${thousands >= 100 ? Math.round(thousands) : thousands.toFixed(1)}k tok`;
105
- }
106
- const millions = tokens / 1_000_000;
107
- return `${millions >= 100 ? Math.round(millions) : millions.toFixed(1)}M tok`;
108
- }
109
-
110
90
  function escHtml(value) {
111
91
  return String(value ?? '').replace(/[&<>"']/g, (ch) => ({
112
92
  '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
@@ -377,6 +357,16 @@ async function stopBackgroundTask(conversationId, taskId) {
377
357
  }
378
358
  }
379
359
 
360
+ // A sub-agent task opens its record in the viewer: the task's spawning
361
+ // tool_use id is the run's id. Bash tasks, monitors and workflows have no
362
+ // record, and Copilot's tasks carry no run id.
363
+ export function subagentRunIdForTask(task) {
364
+ const runId = String(task?.toolUseId || '').trim();
365
+ if (!runId) return '';
366
+ const type = String(task?.taskType || '').trim();
367
+ return type === 'local_agent' || type === 'agent' || type === 'subagent' ? runId : '';
368
+ }
369
+
380
370
  function renderTaskRow(conversationId, task) {
381
371
  const key = `${conversationId}:${task.taskId}`;
382
372
  const stopping = stopsInFlight.has(key);
@@ -403,7 +393,9 @@ function renderTaskRow(conversationId, task) {
403
393
  <div class="bg-task-row" data-task-id="${escHtml(task.taskId)}">
404
394
  ${foldButton}<span class="bg-task-icon" title="${escHtml(task.taskType || 'task')}">${taskIcon(task.taskType)}</span>
405
395
  <span class="bg-task-main">
406
- <span class="bg-task-desc">${escHtml(task.description || task.taskId)}</span>
396
+ ${subagentRunIdForTask(task)
397
+ ? `<button type="button" class="subagent-open bg-task-desc" data-subagent-open="${escHtml(subagentRunIdForTask(task))}" title="Open this sub-agent's thoughts, tool calls and reply">${escHtml(task.description || task.taskId)}</button>`
398
+ : `<span class="bg-task-desc">${escHtml(task.description || task.taskId)}</span>`}
407
399
  ${activity.text ? `<span class="bg-task-detail${activity.toolCall ? ' bg-task-toolcall' : ''}" title="${escHtml(activity.text)}">${escHtml(activity.text)}</span>` : ''}
408
400
  </span>
409
401
  <span class="bg-task-side">