@oar-sh/oar 0.9.20 → 0.9.21
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.
- package/CHANGELOG.md +212 -0
- package/README.md +13 -13
- package/bin/oar.js +2 -2
- package/package.json +1 -1
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +231 -1
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +22 -1
- package/server/claude-cloud-worker/claude-cloud-worker-steering.mjs +34 -0
- package/server/claude-worker/claude-session-process.mjs +7 -0
- package/server/claude-worker/claude-session-worker-main.mjs +8 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +74 -1
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +8 -1
- package/server/cursor-worker/cursor-session-worker.mjs +12 -1
- package/server/cursor-worker/cursor-worker-steering.mjs +20 -0
- package/server/grok-worker/grok-background-tasks.mjs +9 -0
- package/server/grok-worker/grok-session-worker.mjs +12 -1
- package/server/grok-worker/grok-turn-runner.mjs +3 -0
- package/server/grok-worker/grok-worker-steering.mjs +23 -0
- package/server/public/app/about-ui.js +1 -1
- package/server/public/app/about-view.mjs +19 -0
- package/server/public/app/action-confirmations.js +1 -1
- package/server/public/app/api-client.js +4 -2
- package/server/public/app/ask-user-view.js +20 -6
- package/server/public/app/attachments-view.js +1 -1
- package/server/public/app/back-guard.mjs +31 -5
- package/server/public/app/bootstrap.js +556 -68
- package/server/public/app/chat-actions-menu-state.mjs +139 -4
- package/server/public/app/chat-header-meta.mjs +27 -0
- package/server/public/app/claude-billing-toggle-ui.js +2 -1
- package/server/public/app/claude-cloud-conversation-ui.js +2 -2
- package/server/public/app/composer-settings-label.mjs +90 -0
- package/server/public/app/composer-settings-ui.js +442 -0
- package/server/public/app/context-menu.mjs +36 -5
- package/server/public/app/conversation-deep-link.mjs +15 -11
- package/server/public/app/conversation-group-drag.mjs +12 -2
- package/server/public/app/conversation-group-order.mjs +9 -2
- package/server/public/app/conversation-group-state.mjs +47 -10
- package/server/public/app/conversation-origin-ui.js +17 -1
- package/server/public/app/conversation-view.js +3 -0
- package/server/public/app/cwd-menu-placement.mjs +18 -6
- package/server/public/app/cwd-picker.js +36 -6
- package/server/public/app/emoji-view.js +1 -1
- package/server/public/app/font-scaling.js +26 -0
- package/server/public/app/header-priority-actions.mjs +234 -0
- package/server/public/app/hidden-messages-view.mjs +4 -0
- package/server/public/app/inbox-ui.js +18 -2
- package/server/public/app/journal-view.js +311 -58
- package/server/public/app/phone-landscape-ui.js +225 -0
- package/server/public/app/phone-landscape.mjs +99 -0
- package/server/public/app/phone-typing-state.mjs +104 -0
- package/server/public/app/pinned-messages-view.mjs +5 -0
- package/server/public/app/push-settings.js +2 -2
- package/server/public/app/pwa-install.js +24 -3
- package/server/public/app/relay-event-subscriptions-ui.js +71 -22
- package/server/public/app/relay-event-view.mjs +38 -0
- package/server/public/app/relay-restart-ui.js +2 -2
- package/server/public/app/relay-schedule-view.mjs +32 -6
- package/server/public/app/relay-schedules-ui.js +69 -20
- package/server/public/app/remote-relay-origin-view.mjs +41 -23
- package/server/public/app/session-history-ui.js +250 -0
- package/server/public/app/session-history.mjs +599 -0
- package/server/public/app/settings-modal.js +61 -6
- package/server/public/app/settings-tabs.js +12 -0
- package/server/public/app/share-state.js +1 -1
- package/server/public/app/shared-conversations-settings.js +2 -2
- package/server/public/app/socket-handlers.js +5 -2
- package/server/public/app/status-view.mjs +18 -2
- package/server/public/app/store.js +87 -16
- package/server/public/app/subagent-viewer.mjs +27 -2
- package/server/public/app/tab-strip-fade.mjs +101 -0
- package/server/public/app/tap-actions.js +66 -0
- package/server/public/app/usage-limit-ui.js +63 -2
- package/server/public/app/viewer-history.mjs +13 -2
- package/server/public/index.html +2249 -1590
- package/server/relay-tools.md +1 -0
- package/server/repositories/message-repository.mjs +23 -0
- package/server/repositories/session-repository.mjs +19 -0
- package/server/routes/claude-cloud-routes.mjs +19 -0
- package/server/routes/messages-routes.mjs +57 -1
- package/server/routes/sessions-routes.mjs +41 -7
- package/server/server-runtime.mjs +190 -40
- package/server/services/about-service.mjs +22 -53
- package/server/services/claude-billing-swap-service.mjs +7 -20
- package/server/services/claude-cloud-session-service.mjs +47 -5
- package/server/services/remote-relay-dispatcher.mjs +61 -14
- package/server/services/session-activity-tracker.mjs +1 -1
- package/server/services/session-launch-mutex-service.mjs +23 -0
- package/server/services/session-worker-registry-service.mjs +36 -0
- package/server/services/session-worker-websocket-service.mjs +40 -16
- package/server/services/worker-recycle-service.mjs +772 -0
- package/server/services/worker-replacement.mjs +197 -0
- package/shared/build-id.mjs +128 -0
- package/shared/worker-runtime/heartbeat.mjs +5 -0
- package/shared/worker-runtime/simple-steering-state.mjs +38 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +23 -0
- package/shared/worker-runtime/working-directory.mjs +1 -1
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// Wires session-history.mjs to the page: the popstate listener, the
|
|
2
|
+
// overlays it watches, the document title, and the hooks the open path calls
|
|
3
|
+
// (journal-view.js). Before initSessionHistory() runs (the shared reader, the
|
|
4
|
+
// unit tests) every hook here does nothing.
|
|
5
|
+
|
|
6
|
+
import { conversations } from './store.js';
|
|
7
|
+
import { closeContextMenu } from './context-menu.mjs';
|
|
8
|
+
import { isStatusViewActive, toggleStatusView } from './status-view.mjs';
|
|
9
|
+
import {
|
|
10
|
+
armVisitBaseOnFirstInteraction,
|
|
11
|
+
createGoneList,
|
|
12
|
+
createOverlayWatch,
|
|
13
|
+
createSessionHistory,
|
|
14
|
+
} from './session-history.mjs';
|
|
15
|
+
|
|
16
|
+
const PHONE_QUERY = '(max-width: 680px)';
|
|
17
|
+
|
|
18
|
+
let sessionHistory = null;
|
|
19
|
+
let baseTitle = '';
|
|
20
|
+
// The relay knew no conversation the page tried to open, and said so: the
|
|
21
|
+
// empty screen stays until the reader opens one (no question jumps in).
|
|
22
|
+
let autoOpenHeld = false;
|
|
23
|
+
|
|
24
|
+
function tabStorage() {
|
|
25
|
+
try {
|
|
26
|
+
return typeof sessionStorage === 'undefined' ? null : sessionStorage;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Kept in the tab across reloads, as its history is.
|
|
33
|
+
const goneConversationIds = createGoneList({ storage: tabStorage() });
|
|
34
|
+
|
|
35
|
+
function isPhone() {
|
|
36
|
+
try {
|
|
37
|
+
return !!window.matchMedia?.(PHONE_QUERY)?.matches;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function titleFor(id) {
|
|
44
|
+
if (!id) return baseTitle;
|
|
45
|
+
return String(conversations[id]?.title || '').trim() || baseTitle;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function setDocumentTitle(id) {
|
|
49
|
+
const next = titleFor(id);
|
|
50
|
+
if (document.title !== next) document.title = next;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const isVisible = (element) => !!element?.classList?.contains('visible');
|
|
54
|
+
const isShown = (element) => !!element && !element.hidden;
|
|
55
|
+
const byId = (id) => () => document.getElementById(id);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The Inbox's and Status's closer, told where to go back to: the session the
|
|
59
|
+
* history stands on. One view opened from the other has no session of its
|
|
60
|
+
* own to return to (the first one had already left it).
|
|
61
|
+
*/
|
|
62
|
+
function backToSession(close) {
|
|
63
|
+
if (typeof close !== 'function') return undefined;
|
|
64
|
+
return () => close({ returnTo: sessionHistory?.conversation || '' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Each overlay back closes before it switches sessions: `find()` names its
|
|
69
|
+
* element (null while there is none: some are built on first use), `isOpen`
|
|
70
|
+
* reads it. `closers` come from bootstrap.js, which holds most of them.
|
|
71
|
+
* Opened in the same moment, the earlier one in this list lies below.
|
|
72
|
+
*/
|
|
73
|
+
function overlayDescriptors(closers) {
|
|
74
|
+
return [
|
|
75
|
+
{ name: 'sidebar', find: byId('sidebar'), isOpen: (el) => isPhone() && !!el?.classList?.contains('open'), close: closers.closeSidebar },
|
|
76
|
+
// The Inbox takes the transcript's place: back leaves it for the session it came from.
|
|
77
|
+
{ name: 'inbox', find: () => document.body, isOpen: (el) => !!el?.classList?.contains('inbox-view-active'), close: backToSession(closers.closeInboxView) },
|
|
78
|
+
// Status the same way.
|
|
79
|
+
{ name: 'status', find: () => document.body, isOpen: (el) => !!el?.classList?.contains('status-view-active'), close: backToSession(closers.closeStatusView) },
|
|
80
|
+
{ name: 'composer-settings', find: byId('composer-settings'), isOpen: isShown, close: closers.closeComposerSettings },
|
|
81
|
+
{ name: 'chat-actions-menu', find: byId('chat-actions-menu'), isOpen: isShown, close: closers.closeChatActionsMenu },
|
|
82
|
+
{ name: 'summary-modal', find: byId('summary-modal'), isOpen: isVisible, close: closers.closeSummaryModal },
|
|
83
|
+
// The Change CWD dialog's list of known folders (inside the summary modal).
|
|
84
|
+
{
|
|
85
|
+
name: 'change-cwd-menu',
|
|
86
|
+
find: byId('change-cwd-menu'),
|
|
87
|
+
isOpen: (el) => isShown(el) && isVisible(document.getElementById('summary-modal')),
|
|
88
|
+
close: closers.closeChangeCwdMenu,
|
|
89
|
+
},
|
|
90
|
+
{ name: 'settings-modal', find: byId('settings-modal'), isOpen: isVisible, close: closers.closeSettingsModal },
|
|
91
|
+
{ name: 'message-search-modal', find: byId('message-search-modal'), isOpen: isVisible, close: closers.closeMessageSearchModal },
|
|
92
|
+
{ name: 'repo-browser-modal', find: byId('repo-browser-modal'), isOpen: isVisible, close: closers.closeRepoBrowser },
|
|
93
|
+
{ name: 'new-conversation-model-modal', find: byId('new-conversation-model-modal'), isOpen: isVisible, close: closers.closeNewConversationModelModal },
|
|
94
|
+
{ name: 'git-changes-modal', find: byId('git-changes-modal'), isOpen: isVisible, close: closers.closeGitChangesModal },
|
|
95
|
+
{ name: 'git-diff-modal', find: byId('git-diff-modal'), isOpen: isVisible, close: closers.closeGitDiffViewer },
|
|
96
|
+
{ name: 'tmux-inspector-modal', find: byId('tmux-inspector-modal'), isOpen: isVisible, close: closers.closeTmuxInspector },
|
|
97
|
+
{ name: 'image-annotate-modal', find: byId('image-annotate-modal'), isOpen: isVisible, close: closers.closeAnnotateEditor },
|
|
98
|
+
{ name: 'relay-watch-panel', find: byId('relay-watch-panel'), isOpen: isShown, close: closers.closeRelayWatchPanel },
|
|
99
|
+
{ name: 'relay-schedule-panel', find: byId('relay-schedule-panel'), isOpen: isShown, close: closers.closeRelaySchedulePanel },
|
|
100
|
+
{ name: 'emoji-popup', find: byId('emoji-popup'), isOpen: isVisible, close: closers.closeEmojiPicker },
|
|
101
|
+
// A row's menu (right-click, long press, ⓘ): on <body> while open.
|
|
102
|
+
{ name: 'context-menu', find: () => document.querySelector('body > .context-menu'), isOpen: (el) => !!el, close: closeContextMenu },
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Where the overlays built on first use appear: the panels and the row menu
|
|
107
|
+
// on <body>, the folder list inside the summary modal.
|
|
108
|
+
const OVERLAY_HOSTS = [
|
|
109
|
+
['body', { attributes: true, attributeFilter: ['class'], childList: true }],
|
|
110
|
+
['summary-modal', { attributes: true, attributeFilter: ['class', 'hidden'], childList: true, subtree: true }],
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
function watchOverlays(closers) {
|
|
114
|
+
const overlays = overlayDescriptors(closers)
|
|
115
|
+
.filter((descriptor) => typeof descriptor.close === 'function')
|
|
116
|
+
.map((descriptor) => ({
|
|
117
|
+
name: descriptor.name,
|
|
118
|
+
find: descriptor.find,
|
|
119
|
+
isOpen: () => {
|
|
120
|
+
const element = descriptor.find();
|
|
121
|
+
return !!element && descriptor.isOpen(element);
|
|
122
|
+
},
|
|
123
|
+
close: () => descriptor.close(),
|
|
124
|
+
}));
|
|
125
|
+
const watch = createOverlayWatch({
|
|
126
|
+
overlays,
|
|
127
|
+
onOpened: (overlay) => sessionHistory?.overlayOpened(overlay),
|
|
128
|
+
onClosed: (name) => sessionHistory?.overlayClosed(name),
|
|
129
|
+
});
|
|
130
|
+
if (typeof MutationObserver === 'function') {
|
|
131
|
+
const observed = new WeakSet();
|
|
132
|
+
let observer = null;
|
|
133
|
+
const observe = (element, options = { attributes: true, attributeFilter: ['class', 'hidden'] }) => {
|
|
134
|
+
if (!element || observed.has(element)) return;
|
|
135
|
+
observed.add(element);
|
|
136
|
+
observer.observe(element, options);
|
|
137
|
+
};
|
|
138
|
+
// An overlay's element built since: watched from now on.
|
|
139
|
+
const observeOverlays = () => {
|
|
140
|
+
for (const overlay of overlays) {
|
|
141
|
+
const element = overlay.find();
|
|
142
|
+
if (element && element !== document.body) observe(element);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
observer = new MutationObserver(() => {
|
|
146
|
+
observeOverlays();
|
|
147
|
+
watch.check();
|
|
148
|
+
});
|
|
149
|
+
for (const [id, options] of OVERLAY_HOSTS) {
|
|
150
|
+
observe(id === 'body' ? document.body : document.getElementById(id), options);
|
|
151
|
+
}
|
|
152
|
+
observeOverlays();
|
|
153
|
+
}
|
|
154
|
+
// The sidebar is an overlay on a phone only.
|
|
155
|
+
try {
|
|
156
|
+
window.matchMedia?.(PHONE_QUERY)?.addEventListener?.('change', () => watch.check());
|
|
157
|
+
} catch {}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Leaving Status goes back to the conversation it was opened from, or else
|
|
161
|
+
// to `returnTo`.
|
|
162
|
+
function closeStatusViewDefault({ returnTo = '' } = {}) {
|
|
163
|
+
if (isStatusViewActive()) void toggleStatusView({ returnTo });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function initSessionHistory({ openConversation, ...given } = {}) {
|
|
167
|
+
if (sessionHistory || typeof window === 'undefined' || !window.history) return sessionHistory;
|
|
168
|
+
const closers = { closeStatusView: closeStatusViewDefault, ...given };
|
|
169
|
+
baseTitle = document.title;
|
|
170
|
+
sessionHistory = createSessionHistory({
|
|
171
|
+
history: window.history,
|
|
172
|
+
location: window.location,
|
|
173
|
+
open: (id) => {
|
|
174
|
+
void Promise.resolve(openConversation?.(id, { history: 'none' })).catch(() => {});
|
|
175
|
+
},
|
|
176
|
+
isGone: (id) => goneConversationIds.has(id),
|
|
177
|
+
// Only where the list is an overlay: on a wide screen (a phone turned)
|
|
178
|
+
// openSidebar would un-collapse the desktop sidebar for good.
|
|
179
|
+
onList: () => { if (isPhone()) closers.openSidebar?.(); },
|
|
180
|
+
onLeaveList: () => closers.closeSidebar?.(),
|
|
181
|
+
setTitle: setDocumentTitle,
|
|
182
|
+
schedule: (fn, ms) => window.setTimeout(fn, ms),
|
|
183
|
+
cancel: (handle) => window.clearTimeout(handle),
|
|
184
|
+
});
|
|
185
|
+
window.addEventListener('popstate', () => sessionHistory.onPopState());
|
|
186
|
+
watchOverlays(closers);
|
|
187
|
+
return sessionHistory;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** After the back guard's arming, so the list entry lies above its floor. */
|
|
191
|
+
export function armSessionHistoryOnFirstInteraction(target = window) {
|
|
192
|
+
if (!sessionHistory) return null;
|
|
193
|
+
return armVisitBaseOnFirstInteraction({ target, sessionHistory, isPhone });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The entry the back guard pushes again above its floor. */
|
|
197
|
+
export function sessionHistoryEntryAgain() {
|
|
198
|
+
return sessionHistory ? sessionHistory.entryAgain() : null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** openConversation's hook: `mode` is 'push' (default), 'replace' or 'none'. */
|
|
202
|
+
export function noteConversationShown(id, mode = 'push') {
|
|
203
|
+
if (String(id || '').trim()) autoOpenHeld = false;
|
|
204
|
+
sessionHistory?.shown(id, { mode: mode || 'push' });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Nothing is open any more (the open one was deleted). */
|
|
208
|
+
export function noteConversationCleared() {
|
|
209
|
+
sessionHistory?.shown(null, { mode: 'replace' });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** A deleted conversation: back and forward step over its entries. */
|
|
213
|
+
export function noteConversationGone(id) {
|
|
214
|
+
goneConversationIds.add(id);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The relay knows no conversation `id` (deleted elsewhere, a wrong link):
|
|
219
|
+
* its entries are stepped over from now on. True when back or forward led
|
|
220
|
+
* onto it and history steps on past it; false when the caller clears the
|
|
221
|
+
* view (and with it the entry and the address).
|
|
222
|
+
*/
|
|
223
|
+
export function noteConversationMissing(id) {
|
|
224
|
+
goneConversationIds.add(id);
|
|
225
|
+
// Nothing opens by itself after the notice, until the reader opens one.
|
|
226
|
+
autoOpenHeld = true;
|
|
227
|
+
return sessionHistory ? sessionHistory.missing(id) : false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* True after an unknown conversation left the view empty and said so, until
|
|
232
|
+
* the next conversation opens: a waiting question does not open its own in
|
|
233
|
+
* the meantime (ask-user-view.js).
|
|
234
|
+
*/
|
|
235
|
+
export function isConversationAutoOpenHeld() {
|
|
236
|
+
return autoOpenHeld;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The session the history stands on: the one the Inbox or Status was opened
|
|
241
|
+
* over, though the store holds none while either shows.
|
|
242
|
+
*/
|
|
243
|
+
export function sessionHistoryConversation() {
|
|
244
|
+
return sessionHistory?.conversation || '';
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** The open conversation was renamed. */
|
|
248
|
+
export function syncSessionDocumentTitle() {
|
|
249
|
+
if (sessionHistory) setDocumentTitle(sessionHistory.conversation);
|
|
250
|
+
}
|