@oar-sh/oar 0.9.7 → 0.9.9
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 +344 -0
- package/README.md +89 -40
- package/bin/oar.js +1023 -135
- package/package.json +4 -5
- package/server/README.md +20 -1
- package/server/claude-worker/claude-sdk-adapter.mjs +0 -11
- package/server/claude-worker/claude-session-process.mjs +196 -28
- package/server/claude-worker/sdk-message-normalizer.mjs +28 -0
- package/server/db-schema.mjs +8 -0
- package/server/migrations/0008-message-pins.mjs +85 -0
- package/server/public/app/activity-replay-state.mjs +68 -5
- package/server/public/app/api-client.js +36 -2
- package/server/public/app/attachments-view.js +508 -125
- package/server/public/app/back-guard.mjs +58 -0
- package/server/public/app/bootstrap.js +170 -59
- package/server/public/app/context-usage-view.mjs +127 -8
- package/server/public/app/conversation-view.js +172 -32
- package/server/public/app/external-link-policy.mjs +4 -1
- package/server/public/app/file-version.mjs +58 -0
- package/server/public/app/journal-view.js +16 -2
- package/server/public/app/pinned-messages-view.mjs +253 -0
- package/server/public/app/pwa-install.js +6 -1
- package/server/public/app/relay-dot-state.mjs +8 -1
- package/server/public/app/router.js +8 -2
- package/server/public/app/scrollbar-hover.mjs +78 -0
- package/server/public/app/settings-modal.js +4 -1
- package/server/public/app/socket-handlers.js +8 -1
- package/server/public/app/store.js +32 -0
- package/server/public/app/transcript-separators.mjs +48 -11
- 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 +43 -0
- package/server/public/app/viewer-tap.mjs +112 -0
- package/server/public/app-icon-192.png +0 -0
- package/server/public/app-icon-512.png +0 -0
- package/server/public/app-icon-light-192.png +0 -0
- package/server/public/app-icon-light-512.png +0 -0
- package/server/public/app-icon-light-maskable-512.png +0 -0
- package/server/public/app-icon-light.svg +4 -0
- package/server/public/app-icon-maskable-512.png +0 -0
- package/server/public/app-icon.svg +3 -3
- package/server/public/favicon.ico +0 -0
- package/server/public/index.html +487 -67
- package/server/public/manifest.webmanifest +9 -3
- package/server/public/sw.js +6 -1
- package/server/repositories/message-pin-statements.mjs +39 -0
- package/server/repositories/message-repository.mjs +5 -0
- package/server/routes/messages-routes.mjs +36 -62
- package/server/routes/sessions-routes.mjs +107 -26
- package/server/server-runtime.mjs +39 -8
- package/server/services/claude-context-usage.mjs +51 -3
- package/server/services/cloudflared-tunnel-service.mjs +139 -41
- package/server/services/file-serving.mjs +155 -0
- package/server/services/message-pin-list.mjs +52 -0
- package/server/services/oar-cli-helpers.mjs +205 -4
- package/server/services/oar-cli-menu.mjs +240 -0
- package/server/services/oar-cli-wsl.mjs +129 -0
- package/server/services/pwa-manifest-theme.mjs +30 -0
- package/server/services/safe-served-content.mjs +61 -11
- package/server/services/session-history-refresh-service.mjs +24 -0
- package/server/services/session-worker-registry-service.mjs +18 -1
- package/server/services/update-install-service.mjs +9 -3
|
@@ -18,10 +18,44 @@ export function normalizeRelayActivityEntry(item) {
|
|
|
18
18
|
// A compaction boundary is published as an ordinary activity row carrying
|
|
19
19
|
// structured metadata. The transcript promotes it to a full-width break row
|
|
20
20
|
// (and drops it from the bubble's tool-activity list), so both sides ask here.
|
|
21
|
+
// Every compaction entry answers here, whatever its state: the live bubble
|
|
22
|
+
// never shows any of them as prose.
|
|
21
23
|
export function isCompactBoundaryActivityEntry(item) {
|
|
22
24
|
return normalizeRelayActivityEntry(item)?.metadata?.kind === 'compact_boundary';
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
// The live list of one turn keeps its newest ordinary lines only, but every
|
|
28
|
+
// structured row a line of the transcript is built from: the turn's
|
|
29
|
+
// compaction entries and the session-restart note would otherwise scroll out
|
|
30
|
+
// of a busy turn and take their line with them until the next reload.
|
|
31
|
+
export const LIVE_ACTIVITY_CAP = 24;
|
|
32
|
+
function isKeptLiveActivityEntry(item) {
|
|
33
|
+
const kind = normalizeRelayActivityEntry(item)?.metadata?.kind;
|
|
34
|
+
return kind === 'compact_boundary' || kind === 'compact_window_respawn';
|
|
35
|
+
}
|
|
36
|
+
export function capLiveActivityEntries(items, cap = LIVE_ACTIVITY_CAP) {
|
|
37
|
+
const list = Array.isArray(items) ? items : [];
|
|
38
|
+
let ordinary = list.reduce((count, item) => count + (isKeptLiveActivityEntry(item) ? 0 : 1), 0);
|
|
39
|
+
if (ordinary <= cap) return list;
|
|
40
|
+
return list.filter((item) => {
|
|
41
|
+
if (isKeptLiveActivityEntry(item)) return true;
|
|
42
|
+
ordinary -= 1;
|
|
43
|
+
return ordinary < cap;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A compaction is published in up to two steps on the same message: a
|
|
48
|
+
// `pending` entry when it starts, then either the boundary (no `state`, token
|
|
49
|
+
// counts) or a `cancelled` entry when it ended without a result.
|
|
50
|
+
// → 'pending' | 'cancelled' | 'boundary' | null (not a compaction entry).
|
|
51
|
+
export function compactionEntryState(item) {
|
|
52
|
+
const metadata = normalizeRelayActivityEntry(item)?.metadata;
|
|
53
|
+
if (metadata?.kind !== 'compact_boundary') return null;
|
|
54
|
+
const state = String(metadata.state || '').trim().toLowerCase();
|
|
55
|
+
if (state === 'pending' || state === 'cancelled') return state;
|
|
56
|
+
return 'boundary';
|
|
57
|
+
}
|
|
58
|
+
|
|
25
59
|
function toTokenCount(value) {
|
|
26
60
|
// `Number(null)` is 0, not NaN: without this an omitted post_tokens (every
|
|
27
61
|
// real auto-compaction) would read as a compaction down to zero tokens.
|
|
@@ -34,19 +68,36 @@ function toTokenCount(value) {
|
|
|
34
68
|
// than once only the LAST boundary is promoted. This returns that entry by
|
|
35
69
|
// identity, so the render path can keep the others visible as prose instead
|
|
36
70
|
// of dropping them (they would otherwise vanish from the transcript).
|
|
37
|
-
|
|
71
|
+
//
|
|
72
|
+
// The last compaction entry decides: a `pending` one is promoted only while
|
|
73
|
+
// the message is still `processing` (a finished or failed message never shows
|
|
74
|
+
// a dangling start). A dangling start or a `cancelled` end draws no line of
|
|
75
|
+
// its own, so the line falls back to an earlier boundary of the same message
|
|
76
|
+
// when there is one.
|
|
77
|
+
export function promotedCompactBoundaryEntry(items, { processing = false } = {}) {
|
|
38
78
|
const list = Array.isArray(items) ? items : [];
|
|
79
|
+
let sawLast = false;
|
|
39
80
|
for (let index = list.length - 1; index >= 0; index -= 1) {
|
|
40
|
-
|
|
81
|
+
const state = compactionEntryState(list[index]);
|
|
82
|
+
if (!state) continue;
|
|
83
|
+
if (!sawLast) {
|
|
84
|
+
sawLast = true;
|
|
85
|
+
if (state === 'pending' && processing) return list[index];
|
|
86
|
+
}
|
|
87
|
+
if (state === 'boundary') return list[index];
|
|
41
88
|
}
|
|
42
89
|
return null;
|
|
43
90
|
}
|
|
44
91
|
|
|
45
92
|
// The last compaction recorded against one message's activities, as
|
|
46
|
-
// { preTokens, postTokens } (either may be null when the SDK omitted it)
|
|
47
|
-
|
|
48
|
-
|
|
93
|
+
// { preTokens, postTokens } (either may be null when the SDK omitted it), or
|
|
94
|
+
// { pending: true, preTokens: null, postTokens: null } while it runs.
|
|
95
|
+
export function compactBoundaryFromActivities(items, options = {}) {
|
|
96
|
+
const promoted = promotedCompactBoundaryEntry(items, options);
|
|
49
97
|
if (!promoted) return null;
|
|
98
|
+
if (compactionEntryState(promoted) === 'pending') {
|
|
99
|
+
return { pending: true, preTokens: null, postTokens: null };
|
|
100
|
+
}
|
|
50
101
|
const metadata = normalizeRelayActivityEntry(promoted).metadata;
|
|
51
102
|
return {
|
|
52
103
|
preTokens: toTokenCount(metadata.preTokens),
|
|
@@ -54,6 +105,18 @@ export function compactBoundaryFromActivities(items) {
|
|
|
54
105
|
};
|
|
55
106
|
}
|
|
56
107
|
|
|
108
|
+
// Activity rows the message bubble lists as prose: everything except the
|
|
109
|
+
// promoted boundary (it is the break row) and every pending/cancelled step
|
|
110
|
+
// (transient state, never prose).
|
|
111
|
+
export function visibleActivityEntries(items, promoted) {
|
|
112
|
+
const list = Array.isArray(items) ? items : [];
|
|
113
|
+
return list.filter((item) => {
|
|
114
|
+
if (item === promoted) return false;
|
|
115
|
+
const state = compactionEntryState(item);
|
|
116
|
+
return state !== 'pending' && state !== 'cancelled';
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
57
120
|
// Head-cap that never drops structured rows. Activity lists are capped from
|
|
58
121
|
// the front (the start of a turn is the interesting part), but a
|
|
59
122
|
// metadata-bearing row carries transcript structure rather than prose, and a
|
|
@@ -46,7 +46,9 @@ export function areNetworkRequestsEnabled() {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export async function apiFetch(url, opts = {}) {
|
|
49
|
-
|
|
49
|
+
// A keepalive request is a page's last word; it goes out even when the
|
|
50
|
+
// page's regular network work is already suspended.
|
|
51
|
+
if (!networkRequestsEnabled && !opts.keepalive) return null;
|
|
50
52
|
try {
|
|
51
53
|
const response = await fetch(`${BASE}${url}`, {
|
|
52
54
|
signal: requestTimeoutSignal(),
|
|
@@ -898,6 +900,36 @@ export async function updateMessageShareVisibility(conversationId, messageId, hi
|
|
|
898
900
|
});
|
|
899
901
|
}
|
|
900
902
|
|
|
903
|
+
// Pin or unpin one message. Unlike apiFetch this keeps the relay's `error`
|
|
904
|
+
// text on a refusal (the pin limit), so the notice can say why.
|
|
905
|
+
export async function updateMessagePin(conversationId, messageId, pinned) {
|
|
906
|
+
const convId = String(conversationId || '').trim();
|
|
907
|
+
const msgId = String(messageId || '').trim();
|
|
908
|
+
if (!convId || !msgId || !networkRequestsEnabled) return null;
|
|
909
|
+
const url = `/api/conversation/${encodeURIComponent(convId)}/message/${encodeURIComponent(msgId)}/pin`;
|
|
910
|
+
try {
|
|
911
|
+
const response = await fetch(`${BASE}${url}`, {
|
|
912
|
+
signal: requestTimeoutSignal(),
|
|
913
|
+
method: 'PATCH',
|
|
914
|
+
headers: {
|
|
915
|
+
'Content-Type': 'application/json',
|
|
916
|
+
...authHeaders(),
|
|
917
|
+
},
|
|
918
|
+
body: JSON.stringify({ pinned: pinned === true }),
|
|
919
|
+
});
|
|
920
|
+
const result = await response.json().catch(() => null);
|
|
921
|
+
if (!response.ok) {
|
|
922
|
+
console.error('API error', response.status, url);
|
|
923
|
+
return { ok: false, status: response.status, error: String(result?.error || '').trim() };
|
|
924
|
+
}
|
|
925
|
+
noteFetchSuccess();
|
|
926
|
+
return result;
|
|
927
|
+
} catch (error) {
|
|
928
|
+
noteFetchFailure(url, error);
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
901
933
|
export async function updateConversationTitle(id, title) {
|
|
902
934
|
const convId = String(id || '').trim();
|
|
903
935
|
if (!convId) return null;
|
|
@@ -924,12 +956,14 @@ export async function updateWorkspaceRootAttribution(path, attributionMode) {
|
|
|
924
956
|
});
|
|
925
957
|
}
|
|
926
958
|
|
|
927
|
-
|
|
959
|
+
// `keepalive` lets the request outlive the page (a save made while it closes).
|
|
960
|
+
export async function updateConversationPreferences(id, preferences = {}, { keepalive = false } = {}) {
|
|
928
961
|
const convId = String(id || '').trim();
|
|
929
962
|
if (!convId) return null;
|
|
930
963
|
return apiFetch(`/api/conversation/${convId}/preferences`, {
|
|
931
964
|
method: 'PATCH',
|
|
932
965
|
body: JSON.stringify(preferences || {}),
|
|
966
|
+
...(keepalive ? { keepalive: true } : {}),
|
|
933
967
|
});
|
|
934
968
|
}
|
|
935
969
|
|