@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
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+
3
+ // Session groups, made and changed by hand in the sidebar: a new group from
4
+ // one or more conversations, a name, a conversation moved in or out (or
5
+ // dragged to a place inside a group), and a group dissolved. Archiving a group lives with the archive route
6
+ // (sessions-routes.mjs) and stopping one with the turn controls
7
+ // (messages-routes.mjs), next to the code they reuse.
8
+ //
9
+ // The owner's action only: every route is behind `auth`, and a request that
10
+ // arrived from a paired relay is refused, as the remote_relay tool's own
11
+ // route refuses calls from other relays. The tool reads groups, never
12
+ // changes them.
13
+
14
+ import { readRemoteRelayRequest } from '../services/remote-relay-inbound.mjs';
15
+ import { GROUP_NAME_MAX } from '../repositories/conversation-group-repository.mjs';
16
+ import { resolveConversationTitle } from './sessions-routes.mjs';
17
+
18
+ const resolveConversationTitleText = (title) => resolveConversationTitle({ title, titleSource: 'auto' });
19
+
20
+ export function registerConversationGroupRoutes(app, deps) {
21
+ const {
22
+ auth,
23
+ io,
24
+ conversationGroups = null,
25
+ remoteRelayInbound = null,
26
+ } = deps;
27
+
28
+ function refuseRemote(req, res) {
29
+ if (!readRemoteRelayRequest(req, remoteRelayInbound).remote) return false;
30
+ res.status(403).json({ error: 'Session groups are managed by this relay\'s owner only.' });
31
+ return true;
32
+ }
33
+
34
+ function unavailable(res) {
35
+ return res.status(503).json({ ok: false, error: 'Session groups are not available on this relay (its storage for them is missing).' });
36
+ }
37
+
38
+ // What the page can apply in place: the group's summary (null once
39
+ // dissolved), the named conversations' membership, the group's order (head
40
+ // first) and the positions of its members and of the named conversations.
41
+ function emitChanged(groupId, conversationIds = []) {
42
+ const ids = [...new Set(conversationIds.map((id) => String(id || '').trim()).filter(Boolean))];
43
+ const group = groupId ? conversationGroups.describeGroup(groupId, { headTitle: resolveConversationTitleText }) : null;
44
+ const order = group ? conversationGroups.orderOf(groupId) : [];
45
+ io.emit('conversation_group_changed', {
46
+ groupId: groupId || null,
47
+ conversationIds: ids,
48
+ group,
49
+ memberships: conversationGroups.membershipsOf(ids),
50
+ order,
51
+ positions: conversationGroups.positionsOf([...order, ...ids]),
52
+ });
53
+ }
54
+
55
+ function groupPayload(group) {
56
+ if (!group) return null;
57
+ return {
58
+ id: group.id,
59
+ name: group.name || null,
60
+ parentConversationId: group.parentConversationId || null,
61
+ members: conversationGroups.membersOf(group.id).map((member) => member.id),
62
+ };
63
+ }
64
+
65
+ // POST /api/conversation-groups { name?, conversationIds } — a group by hand.
66
+ app.post('/api/conversation-groups', auth, (req, res) => {
67
+ if (refuseRemote(req, res)) return;
68
+ if (!conversationGroups?.enabled) return unavailable(res);
69
+ const ids = (Array.isArray(req.body?.conversationIds) ? req.body.conversationIds : [])
70
+ .map((id) => String(id || '').trim())
71
+ .filter(Boolean);
72
+ if (!ids.length) return res.status(400).json({ ok: false, error: 'conversationIds must name at least one conversation' });
73
+ const name = req.body?.name == null ? null : String(req.body.name);
74
+ if (name && name.trim().length > GROUP_NAME_MAX) {
75
+ return res.status(400).json({ ok: false, error: `A group name has at most ${GROUP_NAME_MAX} characters` });
76
+ }
77
+ // The rows leave the groups they were in (a drop from a view that had
78
+ // not caught up yet): those groups are announced too.
79
+ const previousGroupIds = new Map();
80
+ for (const id of ids) {
81
+ const previous = conversationGroups.groupOfConversation(id)?.id;
82
+ if (!previous) continue;
83
+ if (!previousGroupIds.has(previous)) previousGroupIds.set(previous, []);
84
+ previousGroupIds.get(previous).push(id);
85
+ }
86
+ const group = conversationGroups.createGroup({ name, conversationIds: ids });
87
+ const payload = groupPayload(group);
88
+ if (!payload.members.length) {
89
+ conversationGroups.deleteIfEmpty(group.id);
90
+ return res.status(404).json({ ok: false, error: 'None of the conversations exists' });
91
+ }
92
+ emitChanged(group.id, ids);
93
+ for (const [previous, movedIds] of previousGroupIds) emitChanged(previous, movedIds);
94
+ return res.json({ ok: true, group: payload });
95
+ });
96
+
97
+ // PATCH /api/conversation-groups/:id { name } — rename; empty returns to the head's title.
98
+ app.patch('/api/conversation-groups/:id', auth, (req, res) => {
99
+ if (refuseRemote(req, res)) return;
100
+ if (!conversationGroups?.enabled) return unavailable(res);
101
+ const groupId = String(req.params.id || '').trim();
102
+ if (!conversationGroups.getGroup(groupId)) return res.status(404).json({ ok: false, error: 'Group not found' });
103
+ if (!('name' in (req.body || {}))) return res.status(400).json({ ok: false, error: 'name is required' });
104
+ const name = req.body.name == null ? '' : String(req.body.name);
105
+ if (name.trim().length > GROUP_NAME_MAX) {
106
+ return res.status(400).json({ ok: false, error: `A group name has at most ${GROUP_NAME_MAX} characters` });
107
+ }
108
+ const group = conversationGroups.renameGroup(groupId, name);
109
+ emitChanged(groupId, []);
110
+ return res.json({ ok: true, group: groupPayload(group) });
111
+ });
112
+
113
+ // PUT /api/conversation/:id/group { groupId, beforeConversationId? } — move
114
+ // a conversation into a group. `groupId: null` takes it out (as DELETE).
115
+ // With `beforeConversationId` in the body (null: to the end) it is a drag:
116
+ // the row lands before that one and the group gets a hand order; without
117
+ // it the row joins at the end of a hand-ordered group, as the menu does.
118
+ app.put('/api/conversation/:id/group', auth, (req, res) => {
119
+ if (refuseRemote(req, res)) return;
120
+ if (!conversationGroups?.enabled) return unavailable(res);
121
+ const body = req.body || {};
122
+ const conversationId = String(req.params.id || '').trim();
123
+ const idOrNull = (value) => value === null || typeof value === 'string';
124
+ if (!idOrNull(body.groupId ?? null)) return res.status(400).json({ ok: false, error: 'groupId must be a string or null' });
125
+ if ('beforeConversationId' in body && !idOrNull(body.beforeConversationId)) {
126
+ return res.status(400).json({ ok: false, error: 'beforeConversationId must be a string or null' });
127
+ }
128
+ const leaving = 'groupId' in body && body.groupId === null;
129
+ const groupId = leaving ? '' : String(body.groupId || '').trim();
130
+ if (!groupId && !leaving) return res.status(400).json({ ok: false, error: 'groupId is required' });
131
+ const placing = 'beforeConversationId' in body;
132
+ const result = placing
133
+ ? conversationGroups.moveConversation(conversationId, {
134
+ groupId: groupId || null,
135
+ beforeConversationId: body.beforeConversationId == null ? null : String(body.beforeConversationId).trim() || null,
136
+ })
137
+ : conversationGroups.setMembership(conversationId, groupId || null);
138
+ if (!result.ok) return res.status(404).json({ ok: false, error: result.error });
139
+ if (result.changed && groupId) emitChanged(groupId, [conversationId]);
140
+ if (result.changed && result.previousGroupId && result.previousGroupId !== groupId) emitChanged(result.previousGroupId, [conversationId]);
141
+ return res.json({
142
+ ok: true,
143
+ groupId: groupId || null,
144
+ previousGroupId: result.previousGroupId || null,
145
+ changed: result.changed === true,
146
+ ...(placing ? { order: result.order || [] } : {}),
147
+ });
148
+ });
149
+
150
+ // DELETE /api/conversation/:id/group — move a conversation out of its group.
151
+ app.delete('/api/conversation/:id/group', auth, (req, res) => {
152
+ if (refuseRemote(req, res)) return;
153
+ if (!conversationGroups?.enabled) return unavailable(res);
154
+ const conversationId = String(req.params.id || '').trim();
155
+ const result = conversationGroups.setMembership(conversationId, null);
156
+ if (!result.ok) return res.status(404).json({ ok: false, error: result.error });
157
+ if (result.changed) emitChanged(result.previousGroupId, [conversationId]);
158
+ return res.json({ ok: true, previousGroupId: result.previousGroupId || null, changed: result.changed === true });
159
+ });
160
+
161
+ // DELETE /api/conversation-groups/:id — dissolve: members stay, ungrouped.
162
+ app.delete('/api/conversation-groups/:id', auth, (req, res) => {
163
+ if (refuseRemote(req, res)) return;
164
+ if (!conversationGroups?.enabled) return unavailable(res);
165
+ const groupId = String(req.params.id || '').trim();
166
+ const members = conversationGroups.membersOf(groupId).map((member) => member.id);
167
+ if (!conversationGroups.dissolveGroup(groupId)) return res.status(404).json({ ok: false, error: 'Group not found' });
168
+ emitChanged(groupId, members);
169
+ return res.json({ ok: true, members });
170
+ });
171
+ }