@oar-sh/oar 0.9.10 → 0.9.12
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 +440 -1
- package/README.md +156 -972
- package/package.json +13 -13
- package/server/README.md +112 -25
- package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +57 -4
- package/server/claude-worker/claude-session-process.mjs +217 -11
- package/server/claude-worker/claude-session-worker.mjs +26 -1
- package/server/claude-worker/task-output-discovery.mjs +126 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +235 -14
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +8 -0
- package/server/copilot-worker/copilot-shell-output-mirror.mjs +94 -0
- package/server/copilot-worker/copilot-task-settle.mjs +96 -0
- package/server/db-schema.mjs +68 -2
- package/server/grok-worker/acp-client.mjs +2 -1
- package/server/grok-worker/acp-host-services.mjs +46 -6
- package/server/grok-worker/grok-background-tasks.mjs +241 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +3 -1
- package/server/grok-worker/grok-session-worker.mjs +33 -1
- package/server/grok-worker/grok-turn-runner.mjs +11 -0
- 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/migrations/0017-conversation-history-indexes.mjs +89 -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 +264 -16
- package/server/public/app/ask-user-view.js +17 -15
- package/server/public/app/background-tasks-view.mjs +285 -16
- package/server/public/app/bootstrap.js +253 -37
- package/server/public/app/chat-actions-menu-state.mjs +20 -10
- package/server/public/app/claude-background-command-limit-options.mjs +21 -0
- 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-payload-cache.mjs +357 -0
- package/server/public/app/conversation-turn-state.mjs +81 -0
- package/server/public/app/conversation-view.js +567 -94
- package/server/public/app/conversation-window-retention.mjs +96 -0
- 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 +1033 -82
- 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-prompt-sanitizer.mjs +11 -4
- package/server/public/app/relay-restart-error.mjs +16 -0
- package/server/public/app/settings-modal.js +150 -10
- 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 +138 -20
- package/server/public/app/store.js +121 -30
- package/server/public/app/subagent-row.mjs +3 -3
- package/server/public/app/subagent-viewer.mjs +37 -7
- package/server/public/app/task-console-render.mjs +237 -0
- package/server/public/app/task-console-watch.mjs +42 -0
- package/server/public/app/task-console.mjs +630 -0
- package/server/public/app/task-output-text.mjs +234 -0
- package/server/public/app/thinking-follow.js +0 -4
- package/server/public/app/transcript-island.mjs +73 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +230 -26
- package/server/public/app/transcript-jump-bottom.mjs +170 -22
- package/server/public/app/transcript-reading-anchor.mjs +256 -0
- package/server/public/app/transcript-reload-freshness.mjs +17 -1
- package/server/public/app/transcript-viewport-anchor.mjs +159 -0
- package/server/public/index.html +625 -21
- package/server/relay-tools.md +3 -1
- package/server/relay.mjs +2 -1
- package/server/repositories/conversation-group-repository.mjs +444 -0
- package/server/repositories/message-repository.mjs +9 -0
- package/server/repositories/question-repository.mjs +4 -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 +433 -68
- package/server/routes/preview-routes.mjs +9 -1
- package/server/routes/sessions-routes.mjs +901 -253
- package/server/server-runtime.mjs +392 -99
- package/server/services/about-service.mjs +461 -0
- package/server/services/background-task-store.mjs +365 -0
- package/server/services/background-work-view.mjs +228 -0
- package/server/services/cache-rebuild-service.mjs +30 -0
- package/server/services/cli-process-runner.mjs +4 -21
- package/server/services/conversation-history-page.mjs +199 -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/relay-prompt-sanitizer.mjs +11 -4
- package/server/services/remote-relay-dispatcher.mjs +187 -7
- package/server/services/request-boundary.mjs +183 -0
- package/server/services/session-activity-tracker.mjs +231 -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/task-output-rooms.mjs +259 -0
- package/server/services/task-output-service.mjs +562 -0
- 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/claude-background-command-limit.mjs +83 -0
- package/shared/preview-tool-core.mjs +15 -5
- package/shared/remote-relay-contract.mjs +35 -3
- package/shared/share-path-privacy.mjs +30 -0
- package/shared/task-output-text.mjs +737 -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/task-output-spool.mjs +181 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +47 -15
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BASE, TOKEN, authHeaders, updateWorkspaceRootHints, applyContextUsageBar, readContextUsageRatio, currentConvId, conversations, setCliOnline, setCloudflaredTunnelState, setActiveRuntimeSessionCount, setRuntimeSessionBindingCount, setContextIndicatorMode, setServerPlatform, setUpdateState } from './store.js';
|
|
1
|
+
import { BASE, TOKEN, CLIENT_ID, IS_SHARE_PREVIEW, SHARE_PREVIEW_CONVERSATION_ID, SHARED_CONVERSATION_TOKEN, authHeaders, updateWorkspaceRootHints, applyContextUsageBar, readContextUsageRatio, currentConvId, conversations, setCliOnline, setCloudflaredTunnelState, setActiveRuntimeSessionCount, setRuntimeSessionBindingCount, setContextIndicatorMode, setServerPlatform, setUpdateState } from './store.js';
|
|
2
2
|
|
|
3
3
|
// fetch() has no default timeout. Without one, a request issued while the app is
|
|
4
4
|
// being backgrounded can hang indefinitely on a half-open mobile connection and
|
|
@@ -71,6 +71,58 @@ export async function apiFetch(url, opts = {}) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
const SERVER_ERROR_MAX_CHARS = 300;
|
|
75
|
+
|
|
76
|
+
/** A server's error text made fit for an alert or a status line. */
|
|
77
|
+
export function readableServerError(value) {
|
|
78
|
+
const text = String(value == null ? '' : value)
|
|
79
|
+
.replace(/[\u0000-\u001f\u007f]+/g, ' ')
|
|
80
|
+
.replace(/\s+/g, ' ')
|
|
81
|
+
.trim();
|
|
82
|
+
return text.length > SERVER_ERROR_MAX_CHARS ? `${text.slice(0, SERVER_ERROR_MAX_CHARS - 1)}…` : text;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* apiFetch for the few callers that show the user why a request failed:
|
|
87
|
+
* throws an Error carrying the server's `error` text (or the HTTP status, or
|
|
88
|
+
* the network failure) instead of returning null. The error's `status` is the
|
|
89
|
+
* HTTP status the relay answered with, or 0 when no answer arrived. Paused
|
|
90
|
+
* networking still returns null, as apiFetch does.
|
|
91
|
+
*/
|
|
92
|
+
export async function apiFetchOrThrow(url, opts = {}) {
|
|
93
|
+
if (!networkRequestsEnabled && !opts.keepalive) return null;
|
|
94
|
+
let response;
|
|
95
|
+
try {
|
|
96
|
+
response = await fetch(`${BASE}${url}`, {
|
|
97
|
+
signal: requestTimeoutSignal(),
|
|
98
|
+
headers: {
|
|
99
|
+
'Content-Type': 'application/json',
|
|
100
|
+
...authHeaders(),
|
|
101
|
+
...(opts.headers || {}),
|
|
102
|
+
},
|
|
103
|
+
...opts,
|
|
104
|
+
});
|
|
105
|
+
} catch (error) {
|
|
106
|
+
noteFetchFailure(url, error);
|
|
107
|
+
const timedOut = error?.name === 'TimeoutError' || error?.name === 'AbortError';
|
|
108
|
+
throw Object.assign(
|
|
109
|
+
new Error(timedOut ? 'The relay did not answer in time.' : `The relay could not be reached (${readableServerError(toErrorMessage(error))}).`),
|
|
110
|
+
{ status: 0 },
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
noteFetchSuccess();
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
console.error('API error', response.status, url);
|
|
116
|
+
let serverText = '';
|
|
117
|
+
try {
|
|
118
|
+
const body = await response.json();
|
|
119
|
+
serverText = readableServerError(body?.error || body?.message || '');
|
|
120
|
+
} catch {}
|
|
121
|
+
throw Object.assign(new Error(serverText || `The relay answered HTTP ${response.status}.`), { status: response.status });
|
|
122
|
+
}
|
|
123
|
+
return response.json();
|
|
124
|
+
}
|
|
125
|
+
|
|
74
126
|
export async function verifyExistingSession(tokenCandidate = '') {
|
|
75
127
|
if (!networkRequestsEnabled) {
|
|
76
128
|
return {
|
|
@@ -195,7 +247,7 @@ export async function updateWorkspaceRoot(rootPath, conversationId = null) {
|
|
|
195
247
|
const endpoint = convId
|
|
196
248
|
? `/api/conversation/${encodeURIComponent(convId)}/workspace-root`
|
|
197
249
|
: '/api/workspace-root';
|
|
198
|
-
const response = await
|
|
250
|
+
const response = await apiFetchOrThrow(endpoint, {
|
|
199
251
|
method: 'POST',
|
|
200
252
|
body: JSON.stringify({ rootPath: pathValue }),
|
|
201
253
|
});
|
|
@@ -276,12 +328,14 @@ export async function updateClaudeSettings({
|
|
|
276
328
|
model = undefined,
|
|
277
329
|
enabledModels = undefined,
|
|
278
330
|
attributionMode = undefined,
|
|
331
|
+
backgroundCommandLimitMinutes = undefined,
|
|
279
332
|
} = {}) {
|
|
280
333
|
const payload = {};
|
|
281
334
|
if (typeof enabled === 'boolean') payload.enabled = enabled;
|
|
282
335
|
if (typeof model === 'string' && model.trim()) payload.model = model.trim();
|
|
283
336
|
if (Array.isArray(enabledModels)) payload.enabledModels = enabledModels;
|
|
284
337
|
if (typeof attributionMode === 'string' && attributionMode.trim()) payload.attributionMode = attributionMode.trim();
|
|
338
|
+
if (Number.isFinite(backgroundCommandLimitMinutes)) payload.backgroundCommandLimitMinutes = backgroundCommandLimitMinutes;
|
|
285
339
|
return settingsRequest('/api/settings/claude', payload, 'Failed to update Claude settings');
|
|
286
340
|
}
|
|
287
341
|
|
|
@@ -474,14 +528,14 @@ export async function updateCursorSettings({
|
|
|
474
528
|
}
|
|
475
529
|
|
|
476
530
|
export async function loadWindowsAutostartSetting() {
|
|
477
|
-
return
|
|
531
|
+
return apiFetchOrThrow('/api/settings/windows-autostart');
|
|
478
532
|
}
|
|
479
533
|
|
|
480
534
|
export async function updateWindowsAutostartSetting(mode) {
|
|
481
535
|
// Mode string ('off' | 'signin' | 'boot'); a boolean still works for the
|
|
482
536
|
// pre-mode call sites and maps onto signin/off server-side.
|
|
483
537
|
const body = typeof mode === 'boolean' ? { enabled: mode } : { mode: String(mode) };
|
|
484
|
-
return
|
|
538
|
+
return apiFetchOrThrow('/api/settings/windows-autostart', {
|
|
485
539
|
method: 'POST',
|
|
486
540
|
body: JSON.stringify(body),
|
|
487
541
|
});
|
|
@@ -520,6 +574,17 @@ export async function updateBackgroundTaskTimeoutSetting(timeoutMinutes) {
|
|
|
520
574
|
});
|
|
521
575
|
}
|
|
522
576
|
|
|
577
|
+
export async function loadTaskStallMinutesSetting() {
|
|
578
|
+
return apiFetch('/api/settings/task-stall-minutes');
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export async function updateTaskStallMinutesSetting(minutes) {
|
|
582
|
+
return apiFetch('/api/settings/task-stall-minutes', {
|
|
583
|
+
method: 'POST',
|
|
584
|
+
body: JSON.stringify({ minutes: Number(minutes) }),
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
523
588
|
export async function loadPwaAppNameSetting() {
|
|
524
589
|
return apiFetch('/api/settings/pwa-app-name');
|
|
525
590
|
}
|
|
@@ -692,9 +757,12 @@ export async function relaunchSessionWorkerWithWorkspaceRoot(conversationId, roo
|
|
|
692
757
|
// One key per user gesture, so a duplicate delivery (retried fetch, second tab,
|
|
693
758
|
// stray touch event) is provably the same request and the relay replays it
|
|
694
759
|
// instead of running a second stop/spawn cycle.
|
|
695
|
-
|
|
760
|
+
// A relaunch stops the worker and starts a new one; on Windows that alone
|
|
761
|
+
// can take longer than the default ceiling.
|
|
762
|
+
return apiFetchOrThrow(`/api/conversation/${encodeURIComponent(convId)}/relaunch-with-workspace-root`, {
|
|
696
763
|
method: 'POST',
|
|
697
764
|
body: JSON.stringify({ rootPath: pathValue, idempotencyKey: String(idempotencyKey || '') }),
|
|
765
|
+
signal: requestTimeoutSignal(LONG_REQUEST_TIMEOUT_MS),
|
|
698
766
|
});
|
|
699
767
|
}
|
|
700
768
|
|
|
@@ -878,6 +946,64 @@ async function conversationActionRequest(url) {
|
|
|
878
946
|
}
|
|
879
947
|
}
|
|
880
948
|
|
|
949
|
+
// Session groups: every call answers the relay's `{ ok, ... }`, or
|
|
950
|
+
// `{ ok: false, status, error }` when it refuses, or null when offline.
|
|
951
|
+
async function groupRequest(url, method, body = null) {
|
|
952
|
+
if (!areNetworkRequestsEnabled()) return null;
|
|
953
|
+
try {
|
|
954
|
+
const response = await fetch(`${BASE}${url}`, {
|
|
955
|
+
signal: requestTimeoutSignal(LONG_REQUEST_TIMEOUT_MS),
|
|
956
|
+
method,
|
|
957
|
+
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
|
958
|
+
body: body === null ? undefined : JSON.stringify(body),
|
|
959
|
+
});
|
|
960
|
+
const payload = await response.json().catch(() => null);
|
|
961
|
+
if (!response.ok) return { ...(payload || {}), ok: false, status: response.status };
|
|
962
|
+
noteFetchSuccess();
|
|
963
|
+
return payload || { ok: true };
|
|
964
|
+
} catch (error) {
|
|
965
|
+
noteFetchFailure(url, error);
|
|
966
|
+
return null;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
export function createConversationGroup({ name = null, conversationIds = [] } = {}) {
|
|
971
|
+
return groupRequest('/api/conversation-groups', 'POST', { name, conversationIds });
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
export function renameConversationGroup(groupId, name) {
|
|
975
|
+
return groupRequest(`/api/conversation-groups/${encodeURIComponent(String(groupId || ''))}`, 'PATCH', { name });
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
export function moveConversationToGroup(conversationId, groupId) {
|
|
979
|
+
return groupRequest(`/api/conversation/${encodeURIComponent(String(conversationId || ''))}/group`, 'PUT', { groupId });
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
/**
|
|
983
|
+
* A conversation into a group at a place in its hand order: before
|
|
984
|
+
* `beforeConversationId`, or at the end with null. `groupId: null` takes it
|
|
985
|
+
* out of its group.
|
|
986
|
+
*/
|
|
987
|
+
export function placeConversationInGroup(conversationId, { groupId = null, beforeConversationId = null } = {}) {
|
|
988
|
+
return groupRequest(`/api/conversation/${encodeURIComponent(String(conversationId || ''))}/group`, 'PUT', { groupId, beforeConversationId });
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
export function removeConversationFromGroup(conversationId) {
|
|
992
|
+
return groupRequest(`/api/conversation/${encodeURIComponent(String(conversationId || ''))}/group`, 'DELETE');
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
export function dissolveConversationGroup(groupId) {
|
|
996
|
+
return groupRequest(`/api/conversation-groups/${encodeURIComponent(String(groupId || ''))}`, 'DELETE');
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
export function archiveConversationGroup(groupId) {
|
|
1000
|
+
return groupRequest(`/api/conversation-groups/${encodeURIComponent(String(groupId || ''))}/archive`, 'POST', {});
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
export function stopConversationGroup(groupId) {
|
|
1004
|
+
return groupRequest(`/api/conversation-groups/${encodeURIComponent(String(groupId || ''))}/stop`, 'POST', { clientId: CLIENT_ID });
|
|
1005
|
+
}
|
|
1006
|
+
|
|
881
1007
|
export async function deleteConversation(id) {
|
|
882
1008
|
const convId = String(id || '').trim();
|
|
883
1009
|
if (!convId || !areNetworkRequestsEnabled()) return null;
|
|
@@ -901,14 +1027,50 @@ export async function deleteConversation(id) {
|
|
|
901
1027
|
}
|
|
902
1028
|
}
|
|
903
1029
|
|
|
1030
|
+
// Keeps the relay's `error` text on a refusal (a turn still running), so the
|
|
1031
|
+
// notice can say why.
|
|
1032
|
+
async function shareVisibilityRequest(url, method, body = null) {
|
|
1033
|
+
if (!networkRequestsEnabled) return null;
|
|
1034
|
+
try {
|
|
1035
|
+
const response = await fetch(`${BASE}${url}`, {
|
|
1036
|
+
signal: requestTimeoutSignal(),
|
|
1037
|
+
method,
|
|
1038
|
+
headers: {
|
|
1039
|
+
'Content-Type': 'application/json',
|
|
1040
|
+
...authHeaders(),
|
|
1041
|
+
},
|
|
1042
|
+
body: body === null ? undefined : JSON.stringify(body),
|
|
1043
|
+
});
|
|
1044
|
+
const result = await response.json().catch(() => null);
|
|
1045
|
+
if (!response.ok) {
|
|
1046
|
+
console.error('API error', response.status, url);
|
|
1047
|
+
return { ok: false, status: response.status, error: String(result?.error || '').trim() };
|
|
1048
|
+
}
|
|
1049
|
+
noteFetchSuccess();
|
|
1050
|
+
return result;
|
|
1051
|
+
} catch (error) {
|
|
1052
|
+
noteFetchFailure(url, error);
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
904
1057
|
export async function updateMessageShareVisibility(conversationId, messageId, hiddenFromShares) {
|
|
905
1058
|
const convId = String(conversationId || '').trim();
|
|
906
1059
|
const msgId = String(messageId || '').trim();
|
|
907
1060
|
if (!convId || !msgId) return null;
|
|
908
|
-
return
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1061
|
+
return shareVisibilityRequest(
|
|
1062
|
+
`/api/conversation/${encodeURIComponent(convId)}/message/${encodeURIComponent(msgId)}/share-visibility`,
|
|
1063
|
+
'PATCH',
|
|
1064
|
+
{ hiddenFromShares: hiddenFromShares === true },
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// Every hidden message of the conversation visible to shared viewers again,
|
|
1069
|
+
// except those of a turn still running (the answer's `skipped`).
|
|
1070
|
+
export async function unhideAllMessagesFromShares(conversationId) {
|
|
1071
|
+
const convId = String(conversationId || '').trim();
|
|
1072
|
+
if (!convId) return null;
|
|
1073
|
+
return shareVisibilityRequest(`/api/conversation/${encodeURIComponent(convId)}/share-visibility/unhide-all`, 'POST');
|
|
912
1074
|
}
|
|
913
1075
|
|
|
914
1076
|
// Pin or unpin one message. Unlike apiFetch this keeps the relay's `error`
|
|
@@ -986,6 +1148,57 @@ export async function createConversationShareLink(id) {
|
|
|
986
1148
|
});
|
|
987
1149
|
}
|
|
988
1150
|
|
|
1151
|
+
// Share management (Settings → Shared conversations, the ⋯ menu). These throw
|
|
1152
|
+
// with the relay's reason, so the caller can say why an action failed.
|
|
1153
|
+
export async function listConversationShares() {
|
|
1154
|
+
return apiFetchOrThrow('/api/shares');
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
export async function renewConversationShareLink(id) {
|
|
1158
|
+
const convId = String(id || '').trim();
|
|
1159
|
+
if (!convId) return null;
|
|
1160
|
+
return apiFetchOrThrow(`/api/conversation/${encodeURIComponent(convId)}/share/renew`, { method: 'POST' });
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
export async function stopSharingConversation(id) {
|
|
1164
|
+
const convId = String(id || '').trim();
|
|
1165
|
+
if (!convId) return null;
|
|
1166
|
+
return apiFetchOrThrow(`/api/conversation/${encodeURIComponent(convId)}/share`, { method: 'DELETE' });
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
export async function loadConversationShareOpens(id, { limit = 200 } = {}) {
|
|
1170
|
+
const convId = String(id || '').trim();
|
|
1171
|
+
if (!convId) return null;
|
|
1172
|
+
const pageSize = Math.max(1, Math.trunc(Number(limit) || 200));
|
|
1173
|
+
return apiFetchOrThrow(`/api/conversation/${encodeURIComponent(convId)}/share/opens?limit=${pageSize}`);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
export async function updateShareSettings(settings = {}) {
|
|
1177
|
+
return apiFetchOrThrow('/api/settings/shares', {
|
|
1178
|
+
method: 'POST',
|
|
1179
|
+
body: JSON.stringify(settings || {}),
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
export async function countStoredViewerIps() {
|
|
1184
|
+
return apiFetchOrThrow('/api/settings/shares/stored-viewer-ips');
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
export async function forgetStoredViewerIps() {
|
|
1188
|
+
return apiFetchOrThrow('/api/settings/shares/stored-viewer-ips', { method: 'DELETE' });
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
export async function loadSharePathPrivacySetting() {
|
|
1192
|
+
return apiFetchOrThrow('/api/settings/share-path-privacy');
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
export async function updateSharePathPrivacySetting(mode) {
|
|
1196
|
+
return apiFetchOrThrow('/api/settings/share-path-privacy', {
|
|
1197
|
+
method: 'POST',
|
|
1198
|
+
body: JSON.stringify({ mode }),
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
|
|
989
1202
|
export async function loadServerStatusEventPage({ before = null, limit = 40 } = {}) {
|
|
990
1203
|
const params = new URLSearchParams();
|
|
991
1204
|
const pageSize = Math.max(1, Math.min(100, Math.trunc(Number(limit) || 40)));
|
|
@@ -1004,9 +1217,23 @@ export async function loadServerStatusEventPage({ before = null, limit = 40 } =
|
|
|
1004
1217
|
};
|
|
1005
1218
|
}
|
|
1006
1219
|
|
|
1007
|
-
|
|
1220
|
+
// The shared reader's source: the share link's route, or for "Preview as
|
|
1221
|
+
// shared" the owner's preview route (cookie-authed, marked redactions).
|
|
1222
|
+
function sharedReaderEndpoint(shareToken, suffix = '') {
|
|
1223
|
+
if (IS_SHARE_PREVIEW) return `${BASE}/api/conversation/${encodeURIComponent(SHARE_PREVIEW_CONVERSATION_ID)}/share-preview${suffix}`;
|
|
1008
1224
|
const token = String(shareToken || '').trim().toLowerCase();
|
|
1009
|
-
|
|
1225
|
+
return token ? `${BASE}/api/shared/${encodeURIComponent(token)}${suffix}` : '';
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function sharedReaderHeaders() {
|
|
1229
|
+
return IS_SHARE_PREVIEW
|
|
1230
|
+
? { 'Content-Type': 'application/json', ...authHeaders() }
|
|
1231
|
+
: { 'Content-Type': 'application/json' };
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
export async function loadSharedConversation(shareToken, options = {}) {
|
|
1235
|
+
const endpoint = sharedReaderEndpoint(shareToken);
|
|
1236
|
+
if (!endpoint) return { ok: false, status: 400, error: 'Missing shared token' };
|
|
1010
1237
|
const params = new URLSearchParams();
|
|
1011
1238
|
const limit = Math.max(1, Math.trunc(Number(options.limit) || 0));
|
|
1012
1239
|
if (limit) params.set('limit', String(limit));
|
|
@@ -1020,11 +1247,13 @@ export async function loadSharedConversation(shareToken, options = {}) {
|
|
|
1020
1247
|
if (afterTimestamp) params.set('afterTimestamp', afterTimestamp);
|
|
1021
1248
|
const aroundMessageId = String(options.aroundMessageId || '').trim();
|
|
1022
1249
|
if (aroundMessageId) params.set('aroundMessageId', aroundMessageId);
|
|
1250
|
+
// The page's first fetch: the relay logs it as an open (polls are not).
|
|
1251
|
+
if (options.open === true) params.set('open', '1');
|
|
1023
1252
|
const query = params.toString();
|
|
1024
1253
|
try {
|
|
1025
|
-
const response = await fetch(`${
|
|
1254
|
+
const response = await fetch(`${endpoint}${query ? `?${query}` : ''}`, {
|
|
1026
1255
|
signal: requestTimeoutSignal(),
|
|
1027
|
-
headers:
|
|
1256
|
+
headers: sharedReaderHeaders(),
|
|
1028
1257
|
});
|
|
1029
1258
|
const payload = await response.json().catch(() => null);
|
|
1030
1259
|
if (!response.ok) {
|
|
@@ -1048,6 +1277,23 @@ export async function loadSharedConversation(shareToken, options = {}) {
|
|
|
1048
1277
|
}
|
|
1049
1278
|
}
|
|
1050
1279
|
|
|
1280
|
+
/** One page of a sub-agent's record, as the shared view (or its preview) may read it. */
|
|
1281
|
+
export async function loadSharedSubagentRecord(runId, { after = 0, limit = 500 } = {}) {
|
|
1282
|
+
const id = String(runId || '').trim();
|
|
1283
|
+
const endpoint = id ? sharedReaderEndpoint(SHARED_CONVERSATION_TOKEN, `/subagent/${encodeURIComponent(id)}`) : '';
|
|
1284
|
+
if (!endpoint) return null;
|
|
1285
|
+
try {
|
|
1286
|
+
const response = await fetch(`${endpoint}?after=${Number(after) || 0}&limit=${Number(limit) || 500}`, {
|
|
1287
|
+
signal: requestTimeoutSignal(),
|
|
1288
|
+
headers: sharedReaderHeaders(),
|
|
1289
|
+
});
|
|
1290
|
+
if (!response.ok) return null;
|
|
1291
|
+
return await response.json().catch(() => null);
|
|
1292
|
+
} catch {
|
|
1293
|
+
return null;
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1051
1297
|
export async function reportSharedViewerPresence(shareToken, viewerId) {
|
|
1052
1298
|
const token = String(shareToken || '').trim().toLowerCase();
|
|
1053
1299
|
const viewer = String(viewerId || '').trim();
|
|
@@ -1122,8 +1368,10 @@ export async function killSessionWorker(sdkSessionId, body = {}) {
|
|
|
1122
1368
|
});
|
|
1123
1369
|
}
|
|
1124
1370
|
|
|
1371
|
+
// The three relay maintenance calls throw on failure (apiFetchOrThrow), so
|
|
1372
|
+
// their callers can tell a refusal from a restart that cut the connection.
|
|
1125
1373
|
export async function requestRelayRestart(body = {}) {
|
|
1126
|
-
return
|
|
1374
|
+
return apiFetchOrThrow('/api/relay/shutdown', {
|
|
1127
1375
|
method: 'POST',
|
|
1128
1376
|
body: JSON.stringify({
|
|
1129
1377
|
reason: 'manual-restart',
|
|
@@ -1171,14 +1419,14 @@ export async function cancelUsageLimitPause(conversationId) {
|
|
|
1171
1419
|
}
|
|
1172
1420
|
|
|
1173
1421
|
export async function cancelRelayRestart(body = {}) {
|
|
1174
|
-
return
|
|
1422
|
+
return apiFetchOrThrow('/api/relay/shutdown/cancel', {
|
|
1175
1423
|
method: 'POST',
|
|
1176
1424
|
body: JSON.stringify({ requestedBy: 'localhost-api', ...(body || {}) }),
|
|
1177
1425
|
});
|
|
1178
1426
|
}
|
|
1179
1427
|
|
|
1180
1428
|
export async function requestQueueEmpty(body = {}) {
|
|
1181
|
-
return
|
|
1429
|
+
return apiFetchOrThrow('/api/queue/empty', {
|
|
1182
1430
|
method: 'POST',
|
|
1183
1431
|
body: JSON.stringify({
|
|
1184
1432
|
reason: 'manual-empty-queue',
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
escHtml,
|
|
6
6
|
relayQuestions,
|
|
7
7
|
relayQuestionDrafts,
|
|
8
|
+
scrollBottom,
|
|
8
9
|
} from './store.js';
|
|
9
10
|
import { loadRelayQuestions as loadRelayQuestionsApi, answerRelayQuestion, answerRelayQuestionStructured } from './api-client.js';
|
|
10
11
|
import { enqueueOutboxRequest, registerOutboxSync } from './sync-outbox.mjs';
|
|
@@ -13,6 +14,7 @@ import { schemaFieldsFromQuestion } from './question-schema-view.mjs';
|
|
|
13
14
|
import { composeMultiSelectAnswer, isMultiSelectQuestion, offersMultiSelectToggle } from './question-multi-select.mjs';
|
|
14
15
|
import { isChatInteractionHeld } from './selection-guard.mjs';
|
|
15
16
|
import { questionCardHeadLabel } from './conversation-provider-indicator.mjs';
|
|
17
|
+
import { beginOwnSend, followsTranscriptEnd, refreshJumpButton, scrollAfterOwnSend } from './transcript-jump-bottom-view.mjs';
|
|
16
18
|
|
|
17
19
|
let relayQuestionRenderHash = '';
|
|
18
20
|
let renderedRelayQuestionIds = new Set();
|
|
@@ -27,12 +29,6 @@ const relayQuestionMultiMode = new Map();
|
|
|
27
29
|
function multiModeFor(question) {
|
|
28
30
|
return question ? relayQuestionMultiMode.get(question.id) : undefined;
|
|
29
31
|
}
|
|
30
|
-
const RELAY_QUESTION_AUTO_SCROLL_THRESHOLD_PX = 80;
|
|
31
|
-
|
|
32
|
-
function distanceFromBottom(el) {
|
|
33
|
-
if (!el) return Number.POSITIVE_INFINITY;
|
|
34
|
-
return Math.max(0, el.scrollHeight - el.clientHeight - el.scrollTop);
|
|
35
|
-
}
|
|
36
32
|
|
|
37
33
|
function captureFocusedQuestionControl(messagesEl) {
|
|
38
34
|
const active = document.activeElement;
|
|
@@ -200,7 +196,8 @@ export async function openPendingQuestionFromBanner() {
|
|
|
200
196
|
return;
|
|
201
197
|
}
|
|
202
198
|
renderRelayQuestions();
|
|
203
|
-
|
|
199
|
+
// The reader asked for the question: it waits at the end.
|
|
200
|
+
scrollBottom();
|
|
204
201
|
}
|
|
205
202
|
|
|
206
203
|
// Scoped to #messages on purpose: scrollIntoView walks every ancestor
|
|
@@ -218,7 +215,9 @@ export function renderRelayQuestions() {
|
|
|
218
215
|
const el = document.getElementById('messages');
|
|
219
216
|
if (!el) return;
|
|
220
217
|
const previousScrollTop = el.scrollTop;
|
|
221
|
-
|
|
218
|
+
// A card is shown to a reader who is at the end. Anyone further up stays
|
|
219
|
+
// where they are; the jump-to-bottom button says that a question waits.
|
|
220
|
+
const shouldAutoScroll = followsTranscriptEnd();
|
|
222
221
|
const focusedControl = captureFocusedQuestionControl(el);
|
|
223
222
|
const questions = Array.from(relayQuestions.values())
|
|
224
223
|
.filter((q) => q && q.conversationId === currentConvId && q.status === 'pending')
|
|
@@ -254,6 +253,7 @@ export function renderRelayQuestions() {
|
|
|
254
253
|
if (!questions.length) {
|
|
255
254
|
renderedRelayQuestionIds = nextRenderedQuestionIds;
|
|
256
255
|
if (!shouldAutoScroll) el.scrollTop = previousScrollTop;
|
|
256
|
+
refreshJumpButton();
|
|
257
257
|
return;
|
|
258
258
|
}
|
|
259
259
|
|
|
@@ -330,19 +330,17 @@ export function renderRelayQuestions() {
|
|
|
330
330
|
el.appendChild(wrapper);
|
|
331
331
|
}
|
|
332
332
|
|
|
333
|
-
// An agent exchange whose turn asks this is shown whole, before anything
|
|
334
|
-
// scrolls (agent-exchange-fold-view.mjs).
|
|
335
|
-
window.syncAgentExchangeFolds?.();
|
|
336
333
|
renderedRelayQuestionIds = nextRenderedQuestionIds;
|
|
337
334
|
restoreFocusedQuestionControl(el, focusedControl);
|
|
338
|
-
if (
|
|
335
|
+
if (!shouldAutoScroll) {
|
|
336
|
+
el.scrollTop = previousScrollTop;
|
|
337
|
+
} else if (firstNewQuestionId) {
|
|
339
338
|
const target = el.querySelector(`.relay-question-container[data-question-id="${CSS.escape(firstNewQuestionId)}"]`);
|
|
340
339
|
scrollQuestionCardIntoMessages(el, target);
|
|
341
|
-
} else if (shouldAutoScroll) {
|
|
342
|
-
window.scrollBottom?.();
|
|
343
340
|
} else {
|
|
344
|
-
|
|
341
|
+
scrollBottom();
|
|
345
342
|
}
|
|
343
|
+
refreshJumpButton();
|
|
346
344
|
}
|
|
347
345
|
|
|
348
346
|
function fieldDomId(questionId, index) {
|
|
@@ -489,6 +487,7 @@ export async function submitRelayStructuredAnswer(questionId) {
|
|
|
489
487
|
const card = document.querySelector(`.relay-question-container[data-question-id="${questionId}"]`);
|
|
490
488
|
const controls = card ? card.querySelectorAll('button, input, select, textarea') : [];
|
|
491
489
|
controls.forEach((el) => { el.disabled = true; });
|
|
490
|
+
const send = beginOwnSend();
|
|
492
491
|
|
|
493
492
|
try {
|
|
494
493
|
const sdkSessionId = String(question?.sdkSessionId || '').trim();
|
|
@@ -513,6 +512,7 @@ export async function submitRelayStructuredAnswer(questionId) {
|
|
|
513
512
|
window.renderConvList?.();
|
|
514
513
|
window.syncComposerButtonState?.();
|
|
515
514
|
renderRelayQuestions();
|
|
515
|
+
scrollAfterOwnSend(send);
|
|
516
516
|
window.showTransientRelayNotice?.(`✅ Answer received · Agent continuing…`, 7000);
|
|
517
517
|
} catch (e) {
|
|
518
518
|
controls.forEach((el) => { el.disabled = false; });
|
|
@@ -594,6 +594,7 @@ export async function submitRelayQuestionAnswer(questionId, presetAnswer = null)
|
|
|
594
594
|
const card = document.querySelector(`.relay-question-container[data-question-id="${questionId}"]`);
|
|
595
595
|
const controls = card ? card.querySelectorAll('button, textarea') : [];
|
|
596
596
|
controls.forEach((el) => { el.disabled = true; });
|
|
597
|
+
const send = beginOwnSend();
|
|
597
598
|
|
|
598
599
|
try {
|
|
599
600
|
const question = relayQuestions.get(questionId) || null;
|
|
@@ -618,6 +619,7 @@ export async function submitRelayQuestionAnswer(questionId, presetAnswer = null)
|
|
|
618
619
|
window.renderConvList?.();
|
|
619
620
|
window.syncComposerButtonState?.();
|
|
620
621
|
renderRelayQuestions();
|
|
622
|
+
scrollAfterOwnSend(send);
|
|
621
623
|
window.showTransientRelayNotice?.(`✅ Answer received: ${answer} · Agent continuing…`, 7000);
|
|
622
624
|
} catch (e) {
|
|
623
625
|
controls.forEach((el) => { el.disabled = false; });
|