@oar-sh/oar 0.9.10 → 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.
- package/CHANGELOG.md +314 -1
- package/README.md +156 -972
- package/package.json +13 -13
- package/server/README.md +95 -22
- package/server/claude-worker/claude-session-process.mjs +89 -11
- package/server/claude-worker/claude-session-worker.mjs +9 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +76 -11
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +6 -0
- package/server/db-schema.mjs +59 -2
- package/server/grok-worker/acp-client.mjs +2 -1
- package/server/grok-worker/acp-host-services.mjs +4 -3
- package/server/migrations/0013-previews.mjs +130 -0
- package/server/migrations/0014-conversation-groups.mjs +89 -0
- package/server/migrations/0015-conversation-group-position.mjs +76 -0
- package/server/migrations/0016-share-access-log.mjs +110 -0
- package/server/public/app/about-ui.js +73 -0
- package/server/public/app/about-view.mjs +234 -0
- package/server/public/app/action-confirmations.js +31 -14
- package/server/public/app/agent-exchange-fold-view.mjs +40 -101
- package/server/public/app/api-client.js +251 -16
- package/server/public/app/ask-user-view.js +17 -15
- package/server/public/app/bootstrap.js +158 -32
- package/server/public/app/chat-actions-menu-state.mjs +20 -10
- package/server/public/app/context-menu.mjs +90 -6
- package/server/public/app/conversation-group-drag.mjs +191 -0
- package/server/public/app/conversation-group-order.mjs +265 -0
- package/server/public/app/conversation-group-state.mjs +100 -0
- package/server/public/app/conversation-list-drag.mjs +449 -0
- package/server/public/app/conversation-turn-state.mjs +81 -0
- package/server/public/app/conversation-view.js +136 -93
- package/server/public/app/cwd-picker.js +1 -1
- package/server/public/app/hidden-messages-view.mjs +312 -0
- package/server/public/app/host-suspend-ui.js +7 -1
- package/server/public/app/journal-view.js +787 -22
- package/server/public/app/preview-cards.mjs +117 -31
- package/server/public/app/preview-command.mjs +10 -1
- package/server/public/app/relay-board-view.js +19 -6
- package/server/public/app/relay-restart-error.mjs +16 -0
- package/server/public/app/settings-modal.js +40 -8
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/share-format.mjs +49 -0
- package/server/public/app/share-preview-marks.mjs +167 -0
- package/server/public/app/share-state.js +109 -0
- package/server/public/app/shared-conversations-settings.js +480 -0
- package/server/public/app/socket-handlers.js +80 -14
- package/server/public/app/store.js +96 -30
- package/server/public/app/subagent-row.mjs +3 -3
- package/server/public/app/subagent-viewer.mjs +30 -6
- package/server/public/app/thinking-follow.js +0 -4
- package/server/public/app/transcript-jump-bottom-view.mjs +194 -20
- package/server/public/app/transcript-jump-bottom.mjs +132 -21
- package/server/public/app/transcript-viewport-anchor.mjs +140 -0
- package/server/public/index.html +386 -15
- package/server/relay-tools.md +2 -1
- package/server/relay.mjs +2 -1
- package/server/repositories/conversation-group-repository.mjs +444 -0
- package/server/repositories/message-repository.mjs +1 -0
- package/server/repositories/session-repository.mjs +12 -37
- package/server/repositories/share-repository.mjs +322 -0
- package/server/repositories/share-visibility-statements.mjs +10 -0
- package/server/routes/about-routes.mjs +15 -0
- package/server/routes/cache-routes.mjs +4 -0
- package/server/routes/conversation-group-routes.mjs +171 -0
- package/server/routes/messages-routes.mjs +184 -65
- package/server/routes/preview-routes.mjs +9 -1
- package/server/routes/sessions-routes.mjs +771 -193
- package/server/server-runtime.mjs +316 -100
- package/server/services/about-service.mjs +461 -0
- package/server/services/background-task-store.mjs +128 -0
- package/server/services/background-work-view.mjs +228 -0
- package/server/services/cache-rebuild-service.mjs +30 -0
- package/server/services/delete-archive-service.mjs +4 -1
- package/server/services/detached-worker-watch-service.mjs +150 -0
- package/server/services/hidden-message-list.mjs +21 -0
- package/server/services/host-suspend-command.mjs +2 -1
- package/server/services/message-pin-list.mjs +1 -1
- package/server/services/preview-health-probe.mjs +8 -1
- package/server/services/preview-proxy-server.mjs +8 -4
- package/server/services/preview-registry-service.mjs +676 -26
- package/server/services/preview-static-handler.mjs +40 -7
- package/server/services/preview-store.mjs +274 -0
- package/server/services/rate-limit-service.mjs +12 -1
- package/server/services/relay-auth.mjs +183 -0
- package/server/services/relay-cli-launcher-service.mjs +3 -1
- package/server/services/relay-event-service.mjs +17 -0
- package/server/services/remote-relay-dispatcher.mjs +57 -5
- package/server/services/request-boundary.mjs +183 -0
- package/server/services/session-activity-tracker.mjs +227 -19
- package/server/services/session-worker-launch-service.mjs +190 -6
- package/server/services/session-worker-process-service.mjs +10 -6
- package/server/services/share-access-log-service.mjs +123 -0
- package/server/services/share-path-redaction.mjs +424 -0
- package/server/services/status-event-service.mjs +7 -3
- package/server/services/tunnel-worker-path-guard.mjs +24 -41
- package/server/services/windows-autostart-service.mjs +6 -1
- package/server/services/windows-boot-autostart-service.mjs +207 -23
- package/shared/preview-tool-core.mjs +15 -5
- package/shared/remote-relay-contract.mjs +3 -1
- package/shared/share-path-privacy.mjs +30 -0
- package/shared/windows-system-tools.mjs +106 -0
- package/shared/worker-runtime/background-task-digest.mjs +30 -0
- package/shared/worker-runtime/heartbeat.mjs +24 -0
- package/shared/worker-runtime/kept-posts.mjs +83 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +47 -15
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// "Preview as shared": the relay marks, in the payload of its preview routes,
|
|
2
|
+
// every redaction (U+E000 … U+E001, the text a viewer gets in between) and
|
|
3
|
+
// every leftover that still looks like a path (U+E002 … U+E003). See
|
|
4
|
+
// server/services/share-path-redaction.mjs. This module turns the marks into
|
|
5
|
+
// highlights wherever the transcript put the text: reply markdown, thoughts,
|
|
6
|
+
// tool lines, code blocks, the sub-agent viewer. A highlight can be split by
|
|
7
|
+
// the elements a renderer put inside it (syntax highlighting); each piece is
|
|
8
|
+
// marked on its own. Marks inside attributes (a title, a link) are dropped.
|
|
9
|
+
|
|
10
|
+
export const PREVIEW_MARK_CHARS = Object.freeze({
|
|
11
|
+
redactedStart: '',
|
|
12
|
+
redactedEnd: '',
|
|
13
|
+
suspectStart: '',
|
|
14
|
+
suspectEnd: '',
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const ANY_MARK_RE = /[-]/;
|
|
18
|
+
const ALL_MARKS_RE = /[-]/g;
|
|
19
|
+
const BLOCK_SELECTOR = 'p, li, pre, div, td, th, h1, h2, h3, h4, h5, h6, blockquote, summary';
|
|
20
|
+
const MARK_KINDS = {
|
|
21
|
+
'': { open: 'redacted' },
|
|
22
|
+
'': { close: 'redacted' },
|
|
23
|
+
'': { open: 'suspect' },
|
|
24
|
+
'': { close: 'suspect' },
|
|
25
|
+
};
|
|
26
|
+
const CLASS_BY_KIND = {
|
|
27
|
+
redacted: 'share-preview-redacted',
|
|
28
|
+
suspect: 'share-preview-suspect',
|
|
29
|
+
};
|
|
30
|
+
const TITLE_BY_KIND = {
|
|
31
|
+
redacted: 'Hidden from shared viewers: they see this instead',
|
|
32
|
+
suspect: 'Still looks like a path: shared viewers see it as it is',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** The text without any mark. */
|
|
36
|
+
export function stripPreviewMarks(text) {
|
|
37
|
+
return String(text ?? '').replace(ALL_MARKS_RE, '');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The pieces of a text between marks, each with the highlight it stands in
|
|
42
|
+
* (null for plain text). `state` carries an open highlight from the text
|
|
43
|
+
* before (a highlight split across nodes) and is updated in place.
|
|
44
|
+
*/
|
|
45
|
+
export function splitPreviewMarks(text, state = { kind: null }) {
|
|
46
|
+
const pieces = [];
|
|
47
|
+
let buffer = '';
|
|
48
|
+
const flush = () => {
|
|
49
|
+
if (buffer) pieces.push({ text: buffer, kind: state.kind });
|
|
50
|
+
buffer = '';
|
|
51
|
+
};
|
|
52
|
+
for (const ch of String(text ?? '')) {
|
|
53
|
+
const mark = MARK_KINDS[ch];
|
|
54
|
+
if (!mark) {
|
|
55
|
+
buffer += ch;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
flush();
|
|
59
|
+
if (mark.open) state.kind = mark.open;
|
|
60
|
+
else if (mark.close && state.kind === mark.close) state.kind = null;
|
|
61
|
+
}
|
|
62
|
+
flush();
|
|
63
|
+
return pieces;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function markElement(doc, piece) {
|
|
67
|
+
const element = doc.createElement('mark');
|
|
68
|
+
element.className = CLASS_BY_KIND[piece.kind];
|
|
69
|
+
element.title = TITLE_BY_KIND[piece.kind];
|
|
70
|
+
element.textContent = piece.text;
|
|
71
|
+
return element;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function rewriteTextNode(node, state) {
|
|
75
|
+
const value = node.nodeValue || '';
|
|
76
|
+
if (!ANY_MARK_RE.test(value) && !state.kind) return;
|
|
77
|
+
const pieces = splitPreviewMarks(value, state);
|
|
78
|
+
const doc = node.ownerDocument;
|
|
79
|
+
const fragment = doc.createDocumentFragment();
|
|
80
|
+
for (const piece of pieces) {
|
|
81
|
+
fragment.appendChild(piece.kind ? markElement(doc, piece) : doc.createTextNode(piece.text));
|
|
82
|
+
}
|
|
83
|
+
node.parentNode?.replaceChild(fragment, node);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function stripAttributes(root) {
|
|
87
|
+
const elements = root.nodeType === 1 ? [root, ...root.querySelectorAll('*')] : [...(root.querySelectorAll?.('*') || [])];
|
|
88
|
+
for (const element of elements) {
|
|
89
|
+
for (const attribute of [...(element.attributes || [])]) {
|
|
90
|
+
if (ANY_MARK_RE.test(attribute.value)) element.setAttribute(attribute.name, stripPreviewMarks(attribute.value));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Turn every mark under `root` into a highlight. Idempotent. */
|
|
96
|
+
export function applyPreviewMarks(root) {
|
|
97
|
+
if (!root) return;
|
|
98
|
+
if (root.nodeType === 3) {
|
|
99
|
+
if (root.parentElement) applyPreviewMarks(root.parentElement);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const doc = root.ownerDocument || root;
|
|
103
|
+
if (ANY_MARK_RE.test(root.textContent || '')) {
|
|
104
|
+
const walker = doc.createTreeWalker(root, 4 /* NodeFilter.SHOW_TEXT */, {
|
|
105
|
+
acceptNode(node) {
|
|
106
|
+
const tag = String(node.parentElement?.tagName || '').toUpperCase();
|
|
107
|
+
// Mark characters are never part of a script or a style; leave them.
|
|
108
|
+
return tag === 'SCRIPT' || tag === 'STYLE' ? 2 /* FILTER_REJECT */ : 1 /* FILTER_ACCEPT */;
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
const nodes = [];
|
|
112
|
+
while (walker.nextNode()) nodes.push(walker.currentNode);
|
|
113
|
+
// An open highlight runs on in document order; a block element ends it.
|
|
114
|
+
let state = { kind: null };
|
|
115
|
+
let block = null;
|
|
116
|
+
for (const node of nodes) {
|
|
117
|
+
const nodeBlock = node.parentElement?.closest?.(BLOCK_SELECTOR) || null;
|
|
118
|
+
if (nodeBlock !== block) {
|
|
119
|
+
state = { kind: null };
|
|
120
|
+
block = nodeBlock;
|
|
121
|
+
}
|
|
122
|
+
rewriteTextNode(node, state);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
stripAttributes(root);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Keep marking whatever the page renders under `root` (the transcript is
|
|
130
|
+
* rebuilt as the preview polls, the sub-agent viewer fills in). Only what
|
|
131
|
+
* changed is looked at. Returns a stop function.
|
|
132
|
+
*/
|
|
133
|
+
export function watchPreviewMarks(root, { schedule = (fn) => requestAnimationFrame(fn) } = {}) {
|
|
134
|
+
if (!root || typeof MutationObserver !== 'function') return () => {};
|
|
135
|
+
const changed = new Set();
|
|
136
|
+
let pending = false;
|
|
137
|
+
let observer = null;
|
|
138
|
+
const observe = () => observer.observe(root, {
|
|
139
|
+
childList: true, subtree: true, characterData: true, attributes: true,
|
|
140
|
+
});
|
|
141
|
+
const run = () => {
|
|
142
|
+
pending = false;
|
|
143
|
+
observer.disconnect();
|
|
144
|
+
try {
|
|
145
|
+
for (const node of changed) {
|
|
146
|
+
if (node.isConnected) applyPreviewMarks(node);
|
|
147
|
+
}
|
|
148
|
+
changed.clear();
|
|
149
|
+
if (ANY_MARK_RE.test(document.title || '')) document.title = stripPreviewMarks(document.title);
|
|
150
|
+
} finally {
|
|
151
|
+
observe();
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
observer = new MutationObserver((records) => {
|
|
155
|
+
for (const record of records) {
|
|
156
|
+
if (record.type === 'childList') record.addedNodes.forEach((node) => changed.add(node));
|
|
157
|
+
else if (record.type === 'characterData') changed.add(record.target);
|
|
158
|
+
else if (record.type === 'attributes' && ANY_MARK_RE.test(record.target.getAttribute?.(record.attributeName) || '')) changed.add(record.target);
|
|
159
|
+
}
|
|
160
|
+
if (pending || !changed.size) return;
|
|
161
|
+
pending = true;
|
|
162
|
+
schedule(run);
|
|
163
|
+
});
|
|
164
|
+
applyPreviewMarks(root);
|
|
165
|
+
observe();
|
|
166
|
+
return () => observer.disconnect();
|
|
167
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// The relay's active public shares, as it last reported them: one entry per
|
|
2
|
+
// shared conversation (Settings → Shared conversations) and whether the open
|
|
3
|
+
// conversation has one (the ⋯ menu's "Stop sharing"). Read from GET /api/shares
|
|
4
|
+
// when the socket connects and again on every share event; no DOM here.
|
|
5
|
+
|
|
6
|
+
import { listConversationShares } from './api-client.js';
|
|
7
|
+
|
|
8
|
+
let shares = [];
|
|
9
|
+
let settings = { storeViewerIp: true, accessLog: true };
|
|
10
|
+
let loaded = false;
|
|
11
|
+
let refreshInFlight = null;
|
|
12
|
+
let refreshAgain = false;
|
|
13
|
+
const listeners = new Set();
|
|
14
|
+
|
|
15
|
+
function notify() {
|
|
16
|
+
for (const listener of listeners) {
|
|
17
|
+
try { listener(); } catch (error) { console.error('share listener failed', error); }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeShare(entry) {
|
|
22
|
+
const conversationId = String(entry?.conversationId || '').trim();
|
|
23
|
+
if (!conversationId) return null;
|
|
24
|
+
const count = (value) => (Number.isFinite(Number(value)) ? Math.max(0, Math.trunc(Number(value))) : 0);
|
|
25
|
+
return {
|
|
26
|
+
conversationId,
|
|
27
|
+
title: String(entry?.title || '').trim(),
|
|
28
|
+
archived: entry?.archived === true,
|
|
29
|
+
shareUrl: String(entry?.shareUrl || '').trim(),
|
|
30
|
+
sharedSince: entry?.sharedSince || null,
|
|
31
|
+
renewedAt: entry?.renewedAt || null,
|
|
32
|
+
lastOpenedAt: entry?.lastOpenedAt || null,
|
|
33
|
+
openCount: count(entry?.openCount),
|
|
34
|
+
viewerCount: count(entry?.viewerCount),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Takes a GET /api/shares body (or the same shape). */
|
|
39
|
+
export function applySharesSnapshot(payload) {
|
|
40
|
+
if (!payload || typeof payload !== 'object') return;
|
|
41
|
+
shares = (Array.isArray(payload.shares) ? payload.shares : []).map(normalizeShare).filter(Boolean);
|
|
42
|
+
settings = {
|
|
43
|
+
storeViewerIp: payload.storeViewerIp !== false,
|
|
44
|
+
accessLog: payload.accessLog !== false,
|
|
45
|
+
};
|
|
46
|
+
loaded = true;
|
|
47
|
+
notify();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function applyShareSettings(next) {
|
|
51
|
+
if (!next || typeof next.storeViewerIp !== 'boolean') return;
|
|
52
|
+
settings = { ...settings, storeViewerIp: next.storeViewerIp };
|
|
53
|
+
notify();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A share this page just stopped: gone before the relay's event arrives. */
|
|
57
|
+
export function forgetShare(conversationId) {
|
|
58
|
+
const id = String(conversationId || '').trim();
|
|
59
|
+
const next = shares.filter((share) => share.conversationId !== id);
|
|
60
|
+
if (next.length === shares.length) return;
|
|
61
|
+
shares = next;
|
|
62
|
+
notify();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getShares() {
|
|
66
|
+
return shares.slice();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getShareSettings() {
|
|
70
|
+
return { ...settings };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function sharesLoaded() {
|
|
74
|
+
return loaded;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function hasActiveShare(conversationId) {
|
|
78
|
+
const id = String(conversationId || '').trim();
|
|
79
|
+
return !!id && shares.some((share) => share.conversationId === id);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Reads the list again. Calls during a read are folded into one more read
|
|
84
|
+
* after it, so the last event always wins. Rejects with the relay's reason.
|
|
85
|
+
*/
|
|
86
|
+
export async function refreshShares() {
|
|
87
|
+
if (refreshInFlight) {
|
|
88
|
+
refreshAgain = true;
|
|
89
|
+
return refreshInFlight;
|
|
90
|
+
}
|
|
91
|
+
refreshInFlight = (async () => {
|
|
92
|
+
try {
|
|
93
|
+
do {
|
|
94
|
+
refreshAgain = false;
|
|
95
|
+
const payload = await listConversationShares();
|
|
96
|
+
if (payload) applySharesSnapshot(payload);
|
|
97
|
+
} while (refreshAgain);
|
|
98
|
+
} finally {
|
|
99
|
+
refreshInFlight = null;
|
|
100
|
+
}
|
|
101
|
+
})();
|
|
102
|
+
return refreshInFlight;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function subscribeShares(listener) {
|
|
106
|
+
if (typeof listener !== 'function') return () => {};
|
|
107
|
+
listeners.add(listener);
|
|
108
|
+
return () => listeners.delete(listener);
|
|
109
|
+
}
|