@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.
- package/CHANGELOG.md +478 -0
- package/README.md +129 -39
- package/bin/bat/oar.bat +46 -46
- package/bin/oar.js +814 -228
- package/package.json +5 -6
- package/server/README.md +306 -4
- package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
- package/server/claude-worker/claude-session-process.mjs +35 -1
- package/server/claude-worker/claude-turn-publisher.mjs +3 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +31 -4
- package/server/claude-worker/subagent-timeline-recorder.mjs +572 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +3 -1
- package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
- package/server/db-schema.mjs +44 -0
- package/server/migrations/0008-message-pins.mjs +85 -0
- package/server/migrations/0009-subagent-timeline.mjs +135 -0
- package/server/migrations/0010-subagent-timeline-pruning.mjs +89 -0
- package/server/migrations/0011-relay-event-subscriptions.mjs +125 -0
- package/server/migrations/0012-cross-relay-event-subscriptions.mjs +116 -0
- package/server/public/app/agent-exchange-fold-view.mjs +248 -0
- package/server/public/app/api-client.js +91 -0
- package/server/public/app/archive-confirm.mjs +28 -0
- package/server/public/app/ask-user-view.js +3 -0
- package/server/public/app/attachments-view.js +1024 -131
- package/server/public/app/back-guard.mjs +58 -0
- package/server/public/app/background-tasks-view.mjs +18 -26
- package/server/public/app/bootstrap.js +230 -35
- package/server/public/app/chat-actions-menu-state.mjs +116 -0
- package/server/public/app/claude-cloud-conversation-ui.js +10 -22
- package/server/public/app/composer-control-state.mjs +16 -0
- package/server/public/app/composer-keyboard.mjs +54 -0
- package/server/public/app/conversation-view.js +471 -110
- package/server/public/app/explorer-edit-state.mjs +68 -0
- package/server/public/app/external-link-policy.mjs +4 -1
- package/server/public/app/file-version.mjs +58 -0
- package/server/public/app/host-suspend-ui.js +6 -3
- package/server/public/app/journal-view.js +161 -27
- package/server/public/app/no-conversation-state.mjs +58 -0
- package/server/public/app/pinned-messages-view.mjs +253 -0
- package/server/public/app/provider-readiness.mjs +120 -0
- package/server/public/app/pwa-install.js +1 -1
- package/server/public/app/relay-board-view.js +3 -0
- package/server/public/app/relay-dot-state.mjs +8 -1
- package/server/public/app/relay-event-subscriptions-ui.js +184 -0
- package/server/public/app/relay-event-view.mjs +234 -0
- package/server/public/app/router.js +8 -2
- package/server/public/app/settings-modal.js +53 -1
- package/server/public/app/socket-handlers.js +65 -28
- package/server/public/app/store.js +25 -2
- package/server/public/app/subagent-row.mjs +138 -0
- package/server/public/app/subagent-timeline-view.mjs +186 -0
- package/server/public/app/subagent-viewer-watch.mjs +50 -0
- package/server/public/app/subagent-viewer.mjs +534 -0
- package/server/public/app/task-row-format.mjs +29 -0
- package/server/public/app/thinking-follow.js +4 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +290 -0
- package/server/public/app/transcript-jump-bottom.mjs +99 -0
- package/server/public/app/transcript-reload-freshness.mjs +25 -0
- package/server/public/app/update-ui.js +1 -1
- package/server/public/app/viewer-chat-gallery.mjs +59 -0
- package/server/public/app/viewer-controls-state.mjs +59 -0
- package/server/public/app/viewer-gallery.mjs +46 -0
- package/server/public/app/viewer-gestures.mjs +105 -0
- package/server/public/app/viewer-history.mjs +45 -0
- package/server/public/app/viewer-tap.mjs +112 -0
- package/server/public/index.html +700 -59
- package/server/repositories/message-pin-statements.mjs +39 -0
- package/server/repositories/message-repository.mjs +34 -3
- package/server/repositories/question-repository.mjs +9 -3
- package/server/repositories/remote-relay-repository.mjs +6 -2
- package/server/repositories/subagent-timeline-statements.mjs +170 -0
- package/server/routes/ask-user-routes.mjs +3 -0
- package/server/routes/messages-routes.mjs +548 -111
- package/server/routes/relay-event-routes.mjs +157 -0
- package/server/routes/remote-relay-routes.mjs +15 -0
- package/server/routes/sessions-routes.mjs +164 -24
- package/server/server-runtime.mjs +274 -62
- package/server/services/cloudflared-tunnel-service.mjs +139 -41
- package/server/services/explorer-entry-name.mjs +26 -0
- package/server/services/explorer-mutations.mjs +122 -0
- package/server/services/file-serving.mjs +155 -0
- package/server/services/message-pin-list.mjs +52 -0
- package/server/services/oar-cli-helpers.mjs +139 -4
- package/server/services/oar-cli-menu.mjs +240 -0
- package/server/services/oar-cli-wsl.mjs +129 -0
- package/server/services/preview-proxy-server.mjs +41 -23
- package/server/services/push-dispatch-service.mjs +18 -0
- package/server/services/relay-event-link-service.mjs +289 -0
- package/server/services/relay-event-service.mjs +1935 -0
- package/server/services/relay-shutdown-gate.mjs +31 -0
- package/server/services/remote-relay-caller-context.mjs +52 -7
- package/server/services/remote-relay-dispatcher.mjs +494 -2
- package/server/services/remote-relay-registry-service.mjs +11 -1
- package/server/services/safe-served-content.mjs +61 -11
- package/server/services/session-activity-tracker.mjs +487 -0
- package/server/services/session-history-refresh-service.mjs +31 -0
- package/server/services/stale-attempt-log-limiter.mjs +96 -0
- package/server/services/stale-recovery-clock-guard.mjs +57 -0
- package/server/services/stale-subagent-run-sweep.mjs +109 -0
- package/server/services/subagent-record-retention.mjs +164 -0
- package/server/services/subagent-timeline-service.mjs +328 -0
- package/server/services/subagent-viewer-rooms.mjs +119 -0
- package/server/services/upgrade-socket-guard.mjs +16 -0
- package/shared/relay-events.mjs +371 -0
- package/shared/remote-relay-contract.mjs +157 -5
- package/shared/subagent-record-retention.mjs +31 -0
- package/shared/subagent-timeline.mjs +79 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// A swipe across the viewer: one pointer, moving mostly along one axis.
|
|
2
|
+
//
|
|
3
|
+
// The axis is decided once the pointer has moved a little, and stays: a
|
|
4
|
+
// drag that started sideways is a sideways drag even if it wanders. Each
|
|
5
|
+
// axis can be switched off for the moment (sideways while an image is
|
|
6
|
+
// zoomed in, since that is a pan; downwards while text is scrolled). The
|
|
7
|
+
// content follows the finger through `onMove`; on release the drag is a
|
|
8
|
+
// swipe when it went far enough or fast enough, else it springs back.
|
|
9
|
+
|
|
10
|
+
export const SWIPE_LOCK_PX = 12;
|
|
11
|
+
export const SWIPE_THRESHOLD_PX = 80;
|
|
12
|
+
export const SWIPE_FLICK_PX_PER_MS = 0.45;
|
|
13
|
+
export const SWIPE_MIN_FLICK_PX = 30;
|
|
14
|
+
|
|
15
|
+
export function createSwipeRecognizer({
|
|
16
|
+
onStart = () => {},
|
|
17
|
+
onMove = () => {},
|
|
18
|
+
onEnd = () => {},
|
|
19
|
+
canStart = () => true,
|
|
20
|
+
horizontalEnabled = () => true,
|
|
21
|
+
verticalEnabled = () => false,
|
|
22
|
+
lockPx = SWIPE_LOCK_PX,
|
|
23
|
+
thresholdPx = SWIPE_THRESHOLD_PX,
|
|
24
|
+
flickPxPerMs = SWIPE_FLICK_PX_PER_MS,
|
|
25
|
+
minFlickPx = SWIPE_MIN_FLICK_PX,
|
|
26
|
+
} = {}) {
|
|
27
|
+
const state = { pointer: null, pointers: 0 };
|
|
28
|
+
|
|
29
|
+
const finish = (event, { cancelled = false } = {}) => {
|
|
30
|
+
const p = state.pointer;
|
|
31
|
+
state.pointer = null;
|
|
32
|
+
if (!p || !p.axis) return;
|
|
33
|
+
const dx = event ? event.clientX - p.x : 0;
|
|
34
|
+
const dy = event ? event.clientY - p.y : 0;
|
|
35
|
+
const along = p.axis === 'x' ? dx : dy;
|
|
36
|
+
// Velocity from the last two samples; a finger that paused before
|
|
37
|
+
// lifting has none.
|
|
38
|
+
const dt = event ? event.timeStamp - p.lastAt : Infinity;
|
|
39
|
+
const velocity = dt > 0 && dt < 100 && Number.isFinite(dt)
|
|
40
|
+
? ((p.axis === 'x' ? event.clientX - p.lastX : event.clientY - p.lastY) / dt)
|
|
41
|
+
: 0;
|
|
42
|
+
const farEnough = Math.abs(along) > thresholdPx;
|
|
43
|
+
const flicked = Math.abs(along) > minFlickPx && Math.abs(velocity) > flickPxPerMs
|
|
44
|
+
&& Math.sign(velocity) === Math.sign(along);
|
|
45
|
+
let swipe = null;
|
|
46
|
+
if (!cancelled && (farEnough || flicked)) {
|
|
47
|
+
swipe = p.axis === 'x' ? (along < 0 ? 'left' : 'right') : (along < 0 ? 'up' : 'down');
|
|
48
|
+
}
|
|
49
|
+
onEnd({ axis: p.axis, dx, dy, swipe, cancelled });
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
handle(event) {
|
|
54
|
+
const type = String(event?.type || '');
|
|
55
|
+
if (type === 'pointerdown') {
|
|
56
|
+
state.pointers += 1;
|
|
57
|
+
if (state.pointers > 1) {
|
|
58
|
+
// A second finger: a pinch, not a swipe.
|
|
59
|
+
finish(null, { cancelled: true });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!canStart(event)) return;
|
|
63
|
+
state.pointer = {
|
|
64
|
+
id: event.pointerId,
|
|
65
|
+
x: event.clientX, y: event.clientY,
|
|
66
|
+
lastX: event.clientX, lastY: event.clientY, lastAt: event.timeStamp,
|
|
67
|
+
axis: null,
|
|
68
|
+
};
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const p = state.pointer;
|
|
72
|
+
if (type === 'pointermove') {
|
|
73
|
+
if (!p || event.pointerId !== p.id) return;
|
|
74
|
+
const dx = event.clientX - p.x;
|
|
75
|
+
const dy = event.clientY - p.y;
|
|
76
|
+
if (!p.axis) {
|
|
77
|
+
if (Math.max(Math.abs(dx), Math.abs(dy)) < lockPx) return;
|
|
78
|
+
const axis = Math.abs(dx) >= Math.abs(dy) ? 'x' : 'y';
|
|
79
|
+
if ((axis === 'x' && !horizontalEnabled()) || (axis === 'y' && !verticalEnabled())) {
|
|
80
|
+
state.pointer = null;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
p.axis = axis;
|
|
84
|
+
onStart({ axis });
|
|
85
|
+
}
|
|
86
|
+
p.lastX = event.clientX;
|
|
87
|
+
p.lastY = event.clientY;
|
|
88
|
+
p.lastAt = event.timeStamp;
|
|
89
|
+
onMove({ axis: p.axis, dx, dy });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (type === 'pointerup' || type === 'pointercancel') {
|
|
93
|
+
state.pointers = Math.max(0, state.pointers - 1);
|
|
94
|
+
if (!p || event.pointerId !== p.id) return;
|
|
95
|
+
finish(event, { cancelled: type === 'pointercancel' });
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
/** True while a swipe is being tracked along an axis. */
|
|
99
|
+
get active() { return Boolean(state.pointer?.axis); },
|
|
100
|
+
cancel() {
|
|
101
|
+
finish(null, { cancelled: true });
|
|
102
|
+
state.pointers = 0;
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// The system back gesture closes the viewer instead of leaving the app.
|
|
2
|
+
//
|
|
3
|
+
// Opening the viewer pushes one history entry marked as ours; back pops it
|
|
4
|
+
// and the pop closes the viewer. Closing by any other means (the × button,
|
|
5
|
+
// Escape, a swipe down) pops that entry itself so history is left as it
|
|
6
|
+
// was, and the pop that follows is recognised as our own and ignored. The
|
|
7
|
+
// address never changes: the entry carries only its state.
|
|
8
|
+
|
|
9
|
+
export const VIEWER_HISTORY_MARK = 'oarFileViewer';
|
|
10
|
+
const OWN_POP_WINDOW_MS = 1500;
|
|
11
|
+
|
|
12
|
+
// `mark` names the viewer's entry: two viewers (the file viewer and the
|
|
13
|
+
// sub-agent viewer) must never take each other's entry for their own.
|
|
14
|
+
export function createViewerHistory({ history, onBack, now = () => Date.now(), mark = VIEWER_HISTORY_MARK } = {}) {
|
|
15
|
+
const state = { pushed: false, ownPopAt: 0 };
|
|
16
|
+
return {
|
|
17
|
+
get pushed() { return state.pushed; },
|
|
18
|
+
/** The viewer came up: one entry, however many files it then shows. */
|
|
19
|
+
opened() {
|
|
20
|
+
if (state.pushed) return;
|
|
21
|
+
history.pushState({ [mark]: true }, '');
|
|
22
|
+
state.pushed = true;
|
|
23
|
+
},
|
|
24
|
+
/** The viewer went away by itself: take its entry back out of history. */
|
|
25
|
+
closed() {
|
|
26
|
+
if (!state.pushed) return;
|
|
27
|
+
state.pushed = false;
|
|
28
|
+
if (history.state?.[mark]) {
|
|
29
|
+
state.ownPopAt = now();
|
|
30
|
+
history.back();
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
/** Feed every popstate here; the one answering our own back() is skipped. */
|
|
34
|
+
onPopState() {
|
|
35
|
+
if (state.ownPopAt && now() - state.ownPopAt < OWN_POP_WINDOW_MS) {
|
|
36
|
+
state.ownPopAt = 0;
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
state.ownPopAt = 0;
|
|
40
|
+
if (!state.pushed) return;
|
|
41
|
+
state.pushed = false;
|
|
42
|
+
onBack?.();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Telling a tap from everything else that happens on the viewer's content.
|
|
2
|
+
//
|
|
3
|
+
// A tap is one pointer that goes down and comes up in place, quickly. A drag
|
|
4
|
+
// (panning a zoomed image, selecting text), a pinch (a second pointer) and a
|
|
5
|
+
// long press are not taps. Neither is the first half of a double tap: the
|
|
6
|
+
// tap is reported only once the double-tap window has passed without a
|
|
7
|
+
// second pointer going down, so a double tap zooms without first flipping
|
|
8
|
+
// the controls. Taps on things that act by themselves (links, buttons, a
|
|
9
|
+
// player's own controls) are left to them.
|
|
10
|
+
|
|
11
|
+
export const TAP_MAX_MOVE_PX = 10;
|
|
12
|
+
export const TAP_MAX_DURATION_MS = 500;
|
|
13
|
+
export const DOUBLE_TAP_WINDOW_MS = 300;
|
|
14
|
+
|
|
15
|
+
const INTERACTIVE_SELECTOR = 'a, button, input, textarea, select, summary, video, audio, [contenteditable], [data-no-viewer-tap]';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Pure tap recognizer fed with pointer events of the shape
|
|
19
|
+
* `{ type, pointerId, clientX, clientY, timeStamp, target }`. Calls `onTap`
|
|
20
|
+
* with the final pointer position, `isInteractive(target)` says whether a
|
|
21
|
+
* target handles taps itself (defaults to the usual controls), and
|
|
22
|
+
* `hasSelection()` lets a text selection that ended under the pointer count
|
|
23
|
+
* as a drag rather than a tap.
|
|
24
|
+
*/
|
|
25
|
+
export function createTapRecognizer({
|
|
26
|
+
onTap,
|
|
27
|
+
isInteractive = defaultIsInteractive,
|
|
28
|
+
hasSelection = () => false,
|
|
29
|
+
setTimeoutImpl = setTimeout,
|
|
30
|
+
clearTimeoutImpl = clearTimeout,
|
|
31
|
+
} = {}) {
|
|
32
|
+
const state = {
|
|
33
|
+
active: null, // the pointer being watched: { id, x, y, at }
|
|
34
|
+
pointers: 0, // pointers currently down
|
|
35
|
+
pending: null, // timer holding a tap until the double-tap window passes
|
|
36
|
+
cancelled: false, // a second pointer joined or the pointer moved too far
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const dropPending = () => {
|
|
40
|
+
if (state.pending === null) return;
|
|
41
|
+
clearTimeoutImpl(state.pending);
|
|
42
|
+
state.pending = null;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
handle(event) {
|
|
47
|
+
const type = String(event?.type || '');
|
|
48
|
+
if (type === 'pointerdown') {
|
|
49
|
+
state.pointers += 1;
|
|
50
|
+
// A second pointer down while a tap waits to be reported: the start
|
|
51
|
+
// of a double tap (or a pinch); either way not a single tap.
|
|
52
|
+
if (state.pending !== null) {
|
|
53
|
+
dropPending();
|
|
54
|
+
state.cancelled = true;
|
|
55
|
+
state.active = null;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (state.pointers > 1) {
|
|
59
|
+
state.cancelled = true;
|
|
60
|
+
state.active = null;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (isInteractive(event.target)) {
|
|
64
|
+
state.active = null;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
state.cancelled = false;
|
|
68
|
+
state.active = { id: event.pointerId, x: event.clientX, y: event.clientY, at: event.timeStamp };
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (type === 'pointermove') {
|
|
72
|
+
if (!state.active || event.pointerId !== state.active.id) return;
|
|
73
|
+
if (Math.hypot(event.clientX - state.active.x, event.clientY - state.active.y) > TAP_MAX_MOVE_PX) {
|
|
74
|
+
state.cancelled = true;
|
|
75
|
+
state.active = null;
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (type === 'pointerup' || type === 'pointercancel') {
|
|
80
|
+
state.pointers = Math.max(0, state.pointers - 1);
|
|
81
|
+
const active = state.active;
|
|
82
|
+
if (!active || event.pointerId !== active.id) {
|
|
83
|
+
if (state.pointers === 0) state.cancelled = false;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
state.active = null;
|
|
87
|
+
if (type === 'pointercancel' || state.cancelled) { state.cancelled = false; return; }
|
|
88
|
+
if (event.timeStamp - active.at > TAP_MAX_DURATION_MS) return;
|
|
89
|
+
if (Math.hypot(event.clientX - active.x, event.clientY - active.y) > TAP_MAX_MOVE_PX) return;
|
|
90
|
+
if (hasSelection()) return;
|
|
91
|
+
const point = { clientX: event.clientX, clientY: event.clientY };
|
|
92
|
+
dropPending();
|
|
93
|
+
state.pending = setTimeoutImpl(() => {
|
|
94
|
+
state.pending = null;
|
|
95
|
+
onTap?.(point);
|
|
96
|
+
}, DOUBLE_TAP_WINDOW_MS);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
/** Forget a tap in flight, e.g. when the content is replaced. */
|
|
100
|
+
cancel() {
|
|
101
|
+
dropPending();
|
|
102
|
+
state.active = null;
|
|
103
|
+
state.cancelled = false;
|
|
104
|
+
state.pointers = 0;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function defaultIsInteractive(target) {
|
|
110
|
+
if (!target || typeof target.closest !== 'function') return false;
|
|
111
|
+
return Boolean(target.closest(INTERACTIVE_SELECTOR));
|
|
112
|
+
}
|