@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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
BASE,
|
|
2
3
|
CLIENT_ID,
|
|
3
4
|
currentConvId,
|
|
4
5
|
conversations,
|
|
@@ -24,7 +25,7 @@ import {
|
|
|
24
25
|
showTransientRelayNotice,
|
|
25
26
|
applyContextUsageBar,
|
|
26
27
|
scrollBottom,
|
|
27
|
-
|
|
28
|
+
isMessagesNearBottom,
|
|
28
29
|
setHistoryRefreshInFlight,
|
|
29
30
|
isHistoryRefreshInFlight,
|
|
30
31
|
setSummaryModalLoading,
|
|
@@ -46,14 +47,20 @@ import {
|
|
|
46
47
|
resolveConversationUiState,
|
|
47
48
|
summaryModalState,
|
|
48
49
|
IS_SHARED_VIEW,
|
|
50
|
+
IS_SHARE_PREVIEW,
|
|
49
51
|
SHARED_CONVERSATION_TOKEN,
|
|
50
52
|
getConversationWatcherCount,
|
|
51
53
|
generateId,
|
|
52
54
|
imageEditTarget,
|
|
53
55
|
hasPendingUserMessageForConversation,
|
|
54
56
|
} from './store.js';
|
|
55
|
-
import {
|
|
56
|
-
|
|
57
|
+
import {
|
|
58
|
+
captureReadingBlock,
|
|
59
|
+
createTranscriptResizeKeeper,
|
|
60
|
+
restoreReadingBlock,
|
|
61
|
+
} from './transcript-viewport-anchor.mjs';
|
|
62
|
+
import { initTranscriptJumpToBottom, onTranscriptEndLeft } from './transcript-jump-bottom-view.mjs';
|
|
63
|
+
import { syncScrollOnSendToggle, updateScrollOnSendSetting } from './transcript-jump-bottom.mjs';
|
|
57
64
|
import {
|
|
58
65
|
verifyExistingSession,
|
|
59
66
|
verifyToken,
|
|
@@ -92,6 +99,8 @@ import {
|
|
|
92
99
|
isArchivedConversationsView,
|
|
93
100
|
refreshNewConversationCloudRepoList,
|
|
94
101
|
openOriginConversation,
|
|
102
|
+
openConversationGroupPicker,
|
|
103
|
+
toggleConversationGroupFold,
|
|
95
104
|
} from './journal-view.js';
|
|
96
105
|
import {
|
|
97
106
|
loadRelayQuestions,
|
|
@@ -126,13 +135,14 @@ import {
|
|
|
126
135
|
conversationHasActiveTurn,
|
|
127
136
|
focusConversationMessageById,
|
|
128
137
|
syncRenderedPinState,
|
|
138
|
+
syncRenderedShareVisibility,
|
|
129
139
|
isConversationWindowAtTail,
|
|
130
140
|
getTranscriptJumpEpoch,
|
|
131
141
|
getTranscriptLiveRowEpoch,
|
|
132
|
-
syncAgentExchangeFoldsNow,
|
|
133
142
|
} from './conversation-view.js';
|
|
134
143
|
import { loadKeepingLiveRows } from './transcript-reload-freshness.mjs';
|
|
135
144
|
import { initPinnedMessagesView, setPinsConversation } from './pinned-messages-view.mjs';
|
|
145
|
+
import { initHiddenMessagesView, setHiddenMessagesConversation } from './hidden-messages-view.mjs';
|
|
136
146
|
import { bindChatSelectionGuard, chatSelectionGuard, isChatInteractionHeld } from './selection-guard.mjs';
|
|
137
147
|
import { loadRepoBrowserTree, openRepoBrowser, closeRepoBrowser, setRepoBrowserSessionInfo, resetWorkspaceRepoBrowserForRootChange } from './attachments-view.js';
|
|
138
148
|
import { handleAttachmentInput, retryAttachmentUpload, openAnnotateEditorForPending, openAnnotateEditorForPreview, handleComposerPaste, handleComposerDrop, refreshComposerAttachmentWarning, removeAttachment, clearAttachments, openUploadedAttachmentViewer, setFilePreviewMode, toggleFilePreviewHtml, copyFilePreviewImage, closeFilePreview, goBackFilePreview, openWorkspaceFilePreview, openWorkspaceFilePreviewFromRepo, setRepoBrowserRoot, setRepoBrowserViewMode, toggleRepoBrowserHidden, toggleRepoBrowserHeavy, refreshRepoBrowser, focusRepoTree, setRepoCurrentPath, confirmRepoBrowserCwdPick, startRepoNewFolder } from './attachments-view.js';
|
|
@@ -179,6 +189,7 @@ import {
|
|
|
179
189
|
closeMessageSearchModal,
|
|
180
190
|
} from './message-search-view.js';
|
|
181
191
|
import { initSubagentViewer } from './subagent-viewer.mjs';
|
|
192
|
+
import { watchPreviewMarks } from './share-preview-marks.mjs';
|
|
182
193
|
import {
|
|
183
194
|
initGitChangesView,
|
|
184
195
|
openGitChangesModal,
|
|
@@ -371,6 +382,9 @@ import {
|
|
|
371
382
|
resolveChatActionsMenuState,
|
|
372
383
|
} from './chat-actions-menu-state.mjs';
|
|
373
384
|
import { subscribeProviderReadiness, updateProviderReadinessInputs } from './provider-readiness.mjs';
|
|
385
|
+
import { hasActiveShare, refreshShares, subscribeShares } from './share-state.js';
|
|
386
|
+
import { openStopSharingConfirmation } from './shared-conversations-settings.js';
|
|
387
|
+
import { showAbout, syncAboutMenuLabel } from './about-ui.js';
|
|
374
388
|
import { syncConversationOriginHeader } from './conversation-origin-ui.js';
|
|
375
389
|
import { loadRelayEventSubscriptions, onRelayEventSubscriptionsChanged, syncRelayEventHeader } from './relay-event-subscriptions-ui.js';
|
|
376
390
|
import {
|
|
@@ -458,6 +472,8 @@ let liveConversationPollInFlight = false;
|
|
|
458
472
|
let sharedViewerIdFallback = '';
|
|
459
473
|
let sharedConversationRenderKey = '';
|
|
460
474
|
let sharedConversationLastError = '';
|
|
475
|
+
// The share was stopped or renewed while the page was open: the reader stops.
|
|
476
|
+
let sharedConversationEnded = false;
|
|
461
477
|
let networkLifecycleBound = false;
|
|
462
478
|
let foregroundRecoveryTimer = null;
|
|
463
479
|
let foregroundRecoveryInFlight = false;
|
|
@@ -876,14 +892,14 @@ function initNetworkLifecycleHandling() {
|
|
|
876
892
|
}
|
|
877
893
|
|
|
878
894
|
function syncThemeMenuLabel() {
|
|
879
|
-
const button = document.getElementById('chat-menu-
|
|
895
|
+
const button = document.getElementById('chat-menu-theme-toggle');
|
|
880
896
|
if (!button) return;
|
|
881
897
|
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
|
882
898
|
button.textContent = isLight ? '🌙 Night mode' : '☀️ Day mode';
|
|
883
899
|
}
|
|
884
900
|
|
|
885
901
|
function initThemeMenuToggle() {
|
|
886
|
-
const themeBtn = document.getElementById('chat-menu-
|
|
902
|
+
const themeBtn = document.getElementById('chat-menu-theme-toggle');
|
|
887
903
|
if (!themeBtn) return;
|
|
888
904
|
themeBtn.hidden = false;
|
|
889
905
|
syncThemeMenuLabel();
|
|
@@ -940,7 +956,7 @@ function applySharedReaderUi() {
|
|
|
940
956
|
const modeSelect = document.getElementById('mode-select');
|
|
941
957
|
const chatMenuBtn = document.getElementById('chat-actions-menu-btn');
|
|
942
958
|
const chatMenu = document.getElementById('chat-actions-menu');
|
|
943
|
-
const
|
|
959
|
+
const themeBtn = document.getElementById('chat-menu-theme-toggle');
|
|
944
960
|
const installBtn = document.getElementById('install-btn');
|
|
945
961
|
const activeElement = document.activeElement;
|
|
946
962
|
if (chatMenu && activeElement instanceof Element && chatMenu.contains(activeElement)) {
|
|
@@ -957,7 +973,7 @@ function applySharedReaderUi() {
|
|
|
957
973
|
if (input) {
|
|
958
974
|
input.readOnly = true;
|
|
959
975
|
input.disabled = true;
|
|
960
|
-
input.placeholder = 'Shared conversation (read-only)';
|
|
976
|
+
input.placeholder = IS_SHARE_PREVIEW ? 'Preview as shared (read-only)' : 'Shared conversation (read-only)';
|
|
961
977
|
}
|
|
962
978
|
disableSharedControl(sendBtn);
|
|
963
979
|
disableSharedControl(attachBtn);
|
|
@@ -973,19 +989,19 @@ function applySharedReaderUi() {
|
|
|
973
989
|
// trigger usable). The loop below stays as a guard for entries the resolver
|
|
974
990
|
// does not know.
|
|
975
991
|
applyChatActionsMenuState(resolveChatActionsMenuState({ sharedMode: true }));
|
|
976
|
-
if (chatMenu &&
|
|
992
|
+
if (chatMenu && themeBtn) {
|
|
977
993
|
for (const child of Array.from(chatMenu.children)) {
|
|
978
|
-
if (child !==
|
|
994
|
+
if (child !== themeBtn) hideSharedMenuEntry(child);
|
|
979
995
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
996
|
+
themeBtn.hidden = false;
|
|
997
|
+
themeBtn.removeAttribute('aria-hidden');
|
|
998
|
+
themeBtn.disabled = false;
|
|
999
|
+
themeBtn.tabIndex = 0;
|
|
984
1000
|
syncThemeMenuLabel();
|
|
985
1001
|
} else if (chatMenuBtn) {
|
|
986
1002
|
chatMenuBtn.hidden = true;
|
|
987
1003
|
}
|
|
988
|
-
if (chatMenuBtn && !
|
|
1004
|
+
if (chatMenuBtn && !themeBtn) chatMenuBtn.hidden = true;
|
|
989
1005
|
}
|
|
990
1006
|
|
|
991
1007
|
function syncChatTitleWatcherIndicator() {
|
|
@@ -1032,25 +1048,42 @@ function resolveStableScrollTopForLiveRefresh(messagesEl, capturedScrollTop) {
|
|
|
1032
1048
|
function resolveLiveRefreshAtBottom(messagesEl, capturedScrollTop, capturedAtBottom) {
|
|
1033
1049
|
const current = Number(messagesEl?.scrollTop);
|
|
1034
1050
|
if (!Number.isFinite(current) || Math.abs(current - Number(capturedScrollTop)) <= 2) return capturedAtBottom;
|
|
1035
|
-
return
|
|
1051
|
+
return isMessagesNearBottom();
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// A stopped or renewed share answers 404. The page stops polling at once and
|
|
1055
|
+
// keeps nothing of the conversation on screen; one neutral line stays.
|
|
1056
|
+
function endSharedConversationReader() {
|
|
1057
|
+
if (sharedConversationEnded) return;
|
|
1058
|
+
sharedConversationEnded = true;
|
|
1059
|
+
stopSharedModeTimers();
|
|
1060
|
+
document.getElementById('messages')?.replaceChildren();
|
|
1061
|
+
const titleEl = document.getElementById('chat-title');
|
|
1062
|
+
if (titleEl) titleEl.textContent = 'Shared conversation';
|
|
1063
|
+
sharedConversationLastError = '';
|
|
1064
|
+
setModelBanner('This shared conversation is not available.');
|
|
1036
1065
|
}
|
|
1037
1066
|
|
|
1038
|
-
async function refreshSharedConversation() {
|
|
1039
|
-
if (sharedConversationPollInFlight) return;
|
|
1067
|
+
async function refreshSharedConversation({ open = false } = {}) {
|
|
1068
|
+
if (sharedConversationPollInFlight || sharedConversationEnded) return;
|
|
1040
1069
|
const sharedToken = resolveSharedTokenFromLocation();
|
|
1041
|
-
if (!sharedToken) return;
|
|
1070
|
+
if (!sharedToken && !IS_SHARE_PREVIEW) return;
|
|
1042
1071
|
sharedConversationPollInFlight = true;
|
|
1043
1072
|
const requestSeq = ++sharedConversationRequestSeq;
|
|
1044
1073
|
const messagesEl = document.getElementById('messages');
|
|
1045
|
-
const wasAtBottom =
|
|
1074
|
+
const wasAtBottom = isMessagesNearBottom();
|
|
1046
1075
|
const savedScrollTop = messagesEl?.scrollTop || 0;
|
|
1047
1076
|
try {
|
|
1048
1077
|
// The reader has no socket today, so nothing moves the count; the guard
|
|
1049
1078
|
// keeps a rebuild from dropping a live row should one ever arrive.
|
|
1050
1079
|
const { response, stale } = await loadKeepingLiveRows(
|
|
1051
|
-
() => loadSharedConversation(sharedToken, { limit: 120 }),
|
|
1080
|
+
() => loadSharedConversation(sharedToken, { limit: 120, open }),
|
|
1052
1081
|
getTranscriptLiveRowEpoch,
|
|
1053
1082
|
);
|
|
1083
|
+
if (response?.status === 404) {
|
|
1084
|
+
endSharedConversationReader();
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1054
1087
|
if (requestSeq < sharedConversationAppliedSeq) return;
|
|
1055
1088
|
if (stale) return;
|
|
1056
1089
|
if (!response?.ok) {
|
|
@@ -1101,15 +1134,29 @@ async function refreshSharedConversation() {
|
|
|
1101
1134
|
|
|
1102
1135
|
async function pulseSharedViewerPresence() {
|
|
1103
1136
|
const sharedToken = resolveSharedTokenFromLocation();
|
|
1104
|
-
if (!sharedToken) return;
|
|
1137
|
+
if (!sharedToken || sharedConversationEnded) return;
|
|
1105
1138
|
await reportSharedViewerPresence(sharedToken, sharedViewerId()).catch(() => {});
|
|
1106
1139
|
}
|
|
1107
1140
|
|
|
1141
|
+
// "Preview as shared": the owner's own page, so it says what it is and what
|
|
1142
|
+
// the highlights mean; the marks come with the preview payload.
|
|
1143
|
+
function initSharePreviewUi() {
|
|
1144
|
+
if (!IS_SHARE_PREVIEW) return;
|
|
1145
|
+
document.body.classList.add('share-preview-mode');
|
|
1146
|
+
const banner = document.getElementById('share-preview-banner');
|
|
1147
|
+
if (banner) {
|
|
1148
|
+
banner.textContent = 'Preview as shared: this is what a share link shows. Green marks what viewers get instead of a path; amber marks what still looks like one. Free text is filtered on a best-effort basis.';
|
|
1149
|
+
}
|
|
1150
|
+
watchPreviewMarks(document.body);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1108
1153
|
async function initSharedConversationReader() {
|
|
1109
1154
|
applySharedReaderUi();
|
|
1110
|
-
|
|
1155
|
+
initSharePreviewUi();
|
|
1156
|
+
await refreshSharedConversation({ open: true });
|
|
1111
1157
|
await pulseSharedViewerPresence();
|
|
1112
1158
|
stopSharedModeTimers();
|
|
1159
|
+
if (sharedConversationEnded) return;
|
|
1113
1160
|
sharedConversationPollTimer = setInterval(() => {
|
|
1114
1161
|
if (!shouldRunForegroundNetworkWork()) return;
|
|
1115
1162
|
void refreshSharedConversation();
|
|
@@ -1126,6 +1173,12 @@ function showAuthError(msg) {
|
|
|
1126
1173
|
|
|
1127
1174
|
function resolveAuthErrorMessage(result = null) {
|
|
1128
1175
|
if (result?.status === 401) return 'Invalid token';
|
|
1176
|
+
if (result?.status === 429) {
|
|
1177
|
+
const seconds = Number(result?.payload?.retryAfterSeconds);
|
|
1178
|
+
return Number.isFinite(seconds) && seconds > 0
|
|
1179
|
+
? `Too many failed sign-in attempts. Try again in ${Math.ceil(seconds)} seconds.`
|
|
1180
|
+
: 'Too many failed sign-in attempts. Try again in a minute.';
|
|
1181
|
+
}
|
|
1129
1182
|
if (result?.status === 403) return 'Access denied';
|
|
1130
1183
|
if (result?.status > 0) return `Authentication failed (${result.status})`;
|
|
1131
1184
|
return String(result?.error || 'Could not reach the relay').trim() || 'Could not reach the relay';
|
|
@@ -3672,7 +3725,7 @@ async function pollAuthenticatedCurrentConversationLive() {
|
|
|
3672
3725
|
liveConversationPollInFlight = true;
|
|
3673
3726
|
try {
|
|
3674
3727
|
const messagesEl = document.getElementById('messages');
|
|
3675
|
-
const wasAtBottom =
|
|
3728
|
+
const wasAtBottom = isMessagesNearBottom();
|
|
3676
3729
|
const savedScrollTop = messagesEl?.scrollTop || 0;
|
|
3677
3730
|
const savedLoadedCount = loadConversationLoadedMessageCount(currentId);
|
|
3678
3731
|
const requestLimit = Math.max(20, getConversationLoadedMessageCount() || 0, savedLoadedCount || 0);
|
|
@@ -3692,11 +3745,15 @@ async function pollAuthenticatedCurrentConversationLive() {
|
|
|
3692
3745
|
if (stale) return;
|
|
3693
3746
|
const preserveBottom = resolveLiveRefreshAtBottom(messagesEl, savedScrollTop, wasAtBottom);
|
|
3694
3747
|
const stableSavedScrollTop = resolveStableScrollTopForLiveRefresh(messagesEl, savedScrollTop);
|
|
3748
|
+
// The rows are rebuilt: a reader who is not at the end keeps the text
|
|
3749
|
+
// they read where it is, whatever the rows above it measure now.
|
|
3750
|
+
const reading = preserveBottom ? null : captureReadingBlock(messagesEl);
|
|
3695
3751
|
applyLoadedConversationState(currentId, response, {
|
|
3696
3752
|
restoreScroll: !preserveBottom,
|
|
3697
3753
|
savedScrollTop: preserveBottom ? null : stableSavedScrollTop,
|
|
3698
3754
|
followLiveUpdates: preserveBottom,
|
|
3699
3755
|
});
|
|
3756
|
+
if (reading) restoreReadingBlock(messagesEl, reading);
|
|
3700
3757
|
} finally {
|
|
3701
3758
|
liveConversationPollInFlight = false;
|
|
3702
3759
|
}
|
|
@@ -3713,6 +3770,7 @@ function startLiveConversationPolling() {
|
|
|
3713
3770
|
const transcriptResizeKeeper = createTranscriptResizeKeeper({
|
|
3714
3771
|
getElement: () => document.getElementById('messages'),
|
|
3715
3772
|
});
|
|
3773
|
+
onTranscriptEndLeft(() => transcriptResizeKeeper.recordEndLeft());
|
|
3716
3774
|
|
|
3717
3775
|
function setupViewportTracking() {
|
|
3718
3776
|
syncViewportMetrics();
|
|
@@ -3913,7 +3971,7 @@ async function refreshCurrentView() {
|
|
|
3913
3971
|
const capturedVersion = ++refreshViewVersion;
|
|
3914
3972
|
const jumpEpoch = getTranscriptJumpEpoch();
|
|
3915
3973
|
const messagesEl = document.getElementById('messages');
|
|
3916
|
-
const wasAtBottom =
|
|
3974
|
+
const wasAtBottom = isMessagesNearBottom();
|
|
3917
3975
|
const scrollTop = messagesEl?.scrollTop || 0;
|
|
3918
3976
|
|
|
3919
3977
|
await refreshConversations();
|
|
@@ -3943,6 +4001,9 @@ async function refreshCurrentView() {
|
|
|
3943
4001
|
const keepTranscript = getTranscriptJumpEpoch() !== jumpEpoch || transcriptIsInHistory(currentId) || stale;
|
|
3944
4002
|
const preserveBottom = resolveLiveRefreshAtBottom(messagesEl, scrollTop, wasAtBottom);
|
|
3945
4003
|
const stableSavedScrollTop = resolveStableScrollTopForLiveRefresh(messagesEl, scrollTop);
|
|
4004
|
+
// As in the live poll: the text the reader is at stays where it is.
|
|
4005
|
+
const reading = preserveBottom || keepTranscript ? null : captureReadingBlock(messagesEl);
|
|
4006
|
+
let restoredAt = null;
|
|
3946
4007
|
if (r) {
|
|
3947
4008
|
applyLoadedConversationState(currentId, r, {
|
|
3948
4009
|
restoreScroll: !preserveBottom,
|
|
@@ -3950,6 +4011,9 @@ async function refreshCurrentView() {
|
|
|
3950
4011
|
followLiveUpdates: preserveBottom && !keepTranscript,
|
|
3951
4012
|
keepTranscript,
|
|
3952
4013
|
});
|
|
4014
|
+
// At once, and again below once the cards are back on the page.
|
|
4015
|
+
restoreReadingBlock(messagesEl, reading);
|
|
4016
|
+
restoredAt = { scrollTop: messagesEl?.scrollTop };
|
|
3953
4017
|
} else {
|
|
3954
4018
|
setRepoBrowserSessionInfo('', '');
|
|
3955
4019
|
restoreInFlightThinking(null);
|
|
@@ -3958,7 +4022,12 @@ async function refreshCurrentView() {
|
|
|
3958
4022
|
await loadRelayBoards();
|
|
3959
4023
|
scheduleContextUsageRefresh(currentId, 0);
|
|
3960
4024
|
if (messagesEl && !keepTranscript && String(currentConvId || '').trim() === currentId) {
|
|
3961
|
-
|
|
4025
|
+
// Two requests later: a reader who has scrolled, pressed the jump
|
|
4026
|
+
// button or sent something meanwhile is where they want to be.
|
|
4027
|
+
const readerMoved = !restoredAt
|
|
4028
|
+
|| Math.abs(Number(messagesEl.scrollTop) - Number(restoredAt.scrollTop)) > 2;
|
|
4029
|
+
if (!preserveBottom && !readerMoved && !restoreReadingBlock(messagesEl, reading)
|
|
4030
|
+
&& Number.isFinite(stableSavedScrollTop) && stableSavedScrollTop >= 0) {
|
|
3962
4031
|
messagesEl.scrollTop = stableSavedScrollTop;
|
|
3963
4032
|
}
|
|
3964
4033
|
saveConversationScrollTop(currentId, messagesEl.scrollTop);
|
|
@@ -4078,11 +4147,15 @@ function syncChatActionsMenuState() {
|
|
|
4078
4147
|
cloudSessionUrl: isCloud ? buildCloudLineModel(conversation?.cloud)?.sessionUrl || null : null,
|
|
4079
4148
|
editing: !!convId && chatTitleEditingConversationId === convId,
|
|
4080
4149
|
suspendVisible: isSuspendHostActionVisible(),
|
|
4150
|
+
hasActiveShare: hasActiveShare(convId),
|
|
4081
4151
|
});
|
|
4082
4152
|
applyChatActionsMenuState(state);
|
|
4083
4153
|
return state;
|
|
4084
4154
|
}
|
|
4085
4155
|
|
|
4156
|
+
// The menu follows the relay's share list (made, renewed, stopped anywhere).
|
|
4157
|
+
subscribeShares(() => syncChatActionsMenuState());
|
|
4158
|
+
|
|
4086
4159
|
function toggleChatActionsMenu() {
|
|
4087
4160
|
const menu = document.getElementById('chat-actions-menu');
|
|
4088
4161
|
const trigger = document.getElementById('chat-actions-menu-btn');
|
|
@@ -4092,6 +4165,7 @@ function toggleChatActionsMenu() {
|
|
|
4092
4165
|
if (trigger.hidden || trigger.disabled) return;
|
|
4093
4166
|
syncRefreshHistoryMenuState();
|
|
4094
4167
|
syncThemeMenuLabel();
|
|
4168
|
+
syncAboutMenuLabel();
|
|
4095
4169
|
const willOpen = !!menu.hidden;
|
|
4096
4170
|
menu.hidden = !willOpen;
|
|
4097
4171
|
trigger.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
|
|
@@ -4170,8 +4244,11 @@ function lockChatActionsMenuShield(ms = 300) {
|
|
|
4170
4244
|
function syncChatTitleControls() {
|
|
4171
4245
|
const { title, editor, input } = getChatTitleElements();
|
|
4172
4246
|
const convId = String(currentConvId || '').trim();
|
|
4173
|
-
// No conversation open (deleted, new chat): its 📍
|
|
4174
|
-
if (!convId)
|
|
4247
|
+
// No conversation open (deleted, new chat): its 📍 and 🙈 buttons go with it.
|
|
4248
|
+
if (!convId) {
|
|
4249
|
+
setPinsConversation(null);
|
|
4250
|
+
setHiddenMessagesConversation(null);
|
|
4251
|
+
}
|
|
4175
4252
|
const conversation = convId ? (conversations[convId] || null) : null;
|
|
4176
4253
|
const sdkSessionId = String(conversation?.sdkSessionId || '').trim();
|
|
4177
4254
|
if (title) {
|
|
@@ -4429,19 +4506,19 @@ async function initApp() {
|
|
|
4429
4506
|
appSharedMode = sharedMode;
|
|
4430
4507
|
initNetworkLifecycleHandling();
|
|
4431
4508
|
setForegroundNetworkWorkEnabled(document.visibilityState === 'visible');
|
|
4432
|
-
const sharedThemeBtn = document.getElementById('chat-menu-shared-theme-toggle');
|
|
4433
4509
|
initTheme();
|
|
4434
4510
|
initScrollbarHover();
|
|
4435
4511
|
initThemeMenuToggle();
|
|
4436
|
-
if (sharedThemeBtn && sharedMode) sharedThemeBtn.hidden = false;
|
|
4437
4512
|
initFontScaling();
|
|
4438
4513
|
if (!sharedMode) {
|
|
4439
4514
|
clearLegacyKnownCwdHistoryStorage();
|
|
4440
4515
|
}
|
|
4441
4516
|
syncPwaVersionMenuEntry();
|
|
4442
4517
|
syncQueueStatusMenuEntry();
|
|
4518
|
+
if (!sharedMode) syncAboutMenuLabel();
|
|
4443
4519
|
if (!sharedMode) {
|
|
4444
4520
|
syncSuspendHostVisibility();
|
|
4521
|
+
syncScrollOnSendToggle();
|
|
4445
4522
|
initHostSuspendUi();
|
|
4446
4523
|
initUsageLimitUi();
|
|
4447
4524
|
}
|
|
@@ -4506,6 +4583,16 @@ async function initApp() {
|
|
|
4506
4583
|
});
|
|
4507
4584
|
});
|
|
4508
4585
|
}
|
|
4586
|
+
// "Session group…": the picker for the open conversation, reachable on
|
|
4587
|
+
// the phone with the drawer closed.
|
|
4588
|
+
bindMenuAction(document.getElementById('chat-menu-group'), (event) => {
|
|
4589
|
+
event.preventDefault();
|
|
4590
|
+
event.stopPropagation();
|
|
4591
|
+
lockChatActionsMenuShield(350);
|
|
4592
|
+
closeChatActionsMenu();
|
|
4593
|
+
const convId = String(currentConvId || '').trim();
|
|
4594
|
+
if (convId) openConversationGroupPicker(convId);
|
|
4595
|
+
});
|
|
4509
4596
|
const chatMenuArchiveBtn = document.getElementById('chat-menu-archive');
|
|
4510
4597
|
bindMenuAction(chatMenuArchiveBtn, (event) => {
|
|
4511
4598
|
event.preventDefault();
|
|
@@ -4536,10 +4623,35 @@ async function initApp() {
|
|
|
4536
4623
|
}
|
|
4537
4624
|
const copied = await copyTextToClipboard(shareUrl);
|
|
4538
4625
|
showTransientRelayNotice(copied ? 'Share link copied to clipboard.' : shareUrl);
|
|
4626
|
+
void refreshShares().catch(() => {});
|
|
4539
4627
|
}).catch((error) => {
|
|
4540
4628
|
showTransientRelayNotice(error?.message || 'Could not create share link.');
|
|
4541
4629
|
});
|
|
4542
4630
|
});
|
|
4631
|
+
// "Stop sharing": only shown while the open conversation has a live link.
|
|
4632
|
+
bindMenuAction(document.getElementById('chat-menu-stop-sharing'), (event) => {
|
|
4633
|
+
event.preventDefault();
|
|
4634
|
+
event.stopPropagation();
|
|
4635
|
+
lockChatActionsMenuShield(350);
|
|
4636
|
+
closeChatActionsMenu();
|
|
4637
|
+
const convId = String(currentConvId || '').trim();
|
|
4638
|
+
if (!convId) return;
|
|
4639
|
+
openStopSharingConfirmation(convId, conversations[convId]?.title || '');
|
|
4640
|
+
});
|
|
4641
|
+
const chatMenuPreviewSharedBtn = document.getElementById('chat-menu-preview-shared');
|
|
4642
|
+
bindMenuAction(chatMenuPreviewSharedBtn, (event) => {
|
|
4643
|
+
event.preventDefault();
|
|
4644
|
+
event.stopPropagation();
|
|
4645
|
+
lockChatActionsMenuShield(350);
|
|
4646
|
+
closeChatActionsMenu();
|
|
4647
|
+
const convId = String(currentConvId || '').trim();
|
|
4648
|
+
if (!convId) {
|
|
4649
|
+
showTransientRelayNotice('Select a conversation first.');
|
|
4650
|
+
return;
|
|
4651
|
+
}
|
|
4652
|
+
// Never creates a share link: the preview reads through the owner's route.
|
|
4653
|
+
window.open(`${BASE}/share-preview/${encodeURIComponent(convId)}`, '_blank', 'noopener');
|
|
4654
|
+
});
|
|
4543
4655
|
const chatMenuRefreshHistoryBtn = document.getElementById('chat-menu-refresh-history');
|
|
4544
4656
|
if (chatMenuRefreshHistoryBtn && !chatMenuRefreshHistoryBtn.hidden && !chatMenuRefreshHistoryBtn.disabled) {
|
|
4545
4657
|
bindMenuAction(chatMenuRefreshHistoryBtn, (event) => {
|
|
@@ -4592,6 +4704,14 @@ async function initApp() {
|
|
|
4592
4704
|
closeChatActionsMenu();
|
|
4593
4705
|
openSettingsModal();
|
|
4594
4706
|
});
|
|
4707
|
+
const chatMenuAboutBtn = document.getElementById('chat-menu-about');
|
|
4708
|
+
bindMenuAction(chatMenuAboutBtn, (event) => {
|
|
4709
|
+
event.preventDefault();
|
|
4710
|
+
event.stopPropagation();
|
|
4711
|
+
lockChatActionsMenuShield(350);
|
|
4712
|
+
closeChatActionsMenu();
|
|
4713
|
+
void showAbout();
|
|
4714
|
+
});
|
|
4595
4715
|
const chatMenuRestartRelayBtn = document.getElementById('chat-menu-restart-relay');
|
|
4596
4716
|
bindMenuAction(chatMenuRestartRelayBtn, (event) => {
|
|
4597
4717
|
event.preventDefault();
|
|
@@ -4669,6 +4789,7 @@ async function initApp() {
|
|
|
4669
4789
|
initBubbleActionHandlers();
|
|
4670
4790
|
initMessageScrollPersistence();
|
|
4671
4791
|
initTranscriptJumpToBottom();
|
|
4792
|
+
initSubagentViewer();
|
|
4672
4793
|
syncChatTitleControls();
|
|
4673
4794
|
await initSharedConversationReader();
|
|
4674
4795
|
return;
|
|
@@ -4732,6 +4853,7 @@ async function initApp() {
|
|
|
4732
4853
|
focusMessage: focusConversationMessageById,
|
|
4733
4854
|
syncRenderedPinState,
|
|
4734
4855
|
});
|
|
4856
|
+
initHiddenMessagesView({ syncRenderedShareVisibility });
|
|
4735
4857
|
initGitChangesView();
|
|
4736
4858
|
syncChatTitleControls();
|
|
4737
4859
|
// A provider turned on or off (a Settings save, a catalog change) redraws
|
|
@@ -4795,7 +4917,9 @@ async function doAuth() {
|
|
|
4795
4917
|
async function bootstrap() {
|
|
4796
4918
|
if (ensureTrailingSlashPath()) return;
|
|
4797
4919
|
const sharedMode = isSharedReaderMode();
|
|
4798
|
-
|
|
4920
|
+
// The preview runs in the owner's own browser: their app's service worker
|
|
4921
|
+
// (and its push subscription) stays.
|
|
4922
|
+
if (sharedMode && !IS_SHARE_PREVIEW && navigator.serviceWorker?.getRegistrations) {
|
|
4799
4923
|
navigator.serviceWorker.getRegistrations()
|
|
4800
4924
|
.then((registrations) => Promise.all(registrations.map((registration) => registration.unregister().catch(() => false))))
|
|
4801
4925
|
.catch(() => {});
|
|
@@ -4882,6 +5006,7 @@ window.saveGrokAllowanceSettings = saveGrokAllowanceSettings;
|
|
|
4882
5006
|
window.resetGrokAllowanceAccounting = resetGrokAllowanceAccounting;
|
|
4883
5007
|
window.toggleCursorProvider = toggleCursorProvider;
|
|
4884
5008
|
window.updateShowSuspendHostSetting = updateShowSuspendHostSetting;
|
|
5009
|
+
window.updateScrollOnSendSetting = (enabled) => updateScrollOnSendSetting(enabled);
|
|
4885
5010
|
window.updateWindowsAutostartMode = updateWindowsAutostartMode;
|
|
4886
5011
|
window.previewTurnCeilingSetting = previewTurnCeilingSetting;
|
|
4887
5012
|
window.updateTurnCeilingSetting = updateTurnCeilingSetting;
|
|
@@ -4936,6 +5061,8 @@ window.confirmNewConversationModel = confirmNewConversationModel;
|
|
|
4936
5061
|
window.closeNewConversationModelModal = closeNewConversationModelModal;
|
|
4937
5062
|
window.deleteConv = deleteConv;
|
|
4938
5063
|
window.archiveConv = archiveConv;
|
|
5064
|
+
window.openConversationGroupPicker = openConversationGroupPicker;
|
|
5065
|
+
window.toggleConversationGroupFold = toggleConversationGroupFold;
|
|
4939
5066
|
window.unarchiveConv = unarchiveConv;
|
|
4940
5067
|
window.toggleArchivedConversations = toggleArchivedConversations;
|
|
4941
5068
|
window.refreshNewConversationCloudRepoList = refreshNewConversationCloudRepoList;
|
|
@@ -4977,7 +5104,6 @@ window.setRelayQuestionMultiMode = setRelayQuestionMultiMode;
|
|
|
4977
5104
|
window.handleRelayQuestionKey = handleRelayQuestionKey;
|
|
4978
5105
|
window.openPendingQuestionFromBanner = openPendingQuestionFromBanner;
|
|
4979
5106
|
window.submitRelayBoardAction = submitRelayBoardAction;
|
|
4980
|
-
window.syncAgentExchangeFolds = syncAgentExchangeFoldsNow;
|
|
4981
5107
|
window.compactCurrentConversation = compactCurrentConversation;
|
|
4982
5108
|
window.sendMessage = sendMessage;
|
|
4983
5109
|
window.syncComposerButtonState = syncComposerButtonState;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// The header's ⋯ menu: one decision for its trigger and for every entry, with
|
|
2
2
|
// or without a conversation open. No DOM here; bootstrap.js applies the result.
|
|
3
3
|
//
|
|
4
|
-
// The menu holds two kinds of entries. Relay-wide ones (Settings,
|
|
5
|
-
// relay, Select Models, Check Usage, the queue, Day/Night, the PWA
|
|
6
|
-
// are shown whenever the app is signed in, so Settings can be reached
|
|
7
|
-
// fresh install, where no conversation exists yet. Per-conversation ones
|
|
8
|
-
// title, Share,
|
|
9
|
-
// Open on claude.ai) are hidden, not
|
|
10
|
-
// screen offers what can be done there
|
|
4
|
+
// The menu holds two kinds of entries. Relay-wide ones (Settings, About OAR,
|
|
5
|
+
// Restart web relay, Select Models, Check Usage, the queue, Day/Night, the PWA
|
|
6
|
+
// version) are shown whenever the app is signed in, so Settings can be reached
|
|
7
|
+
// on a fresh install, where no conversation exists yet. Per-conversation ones
|
|
8
|
+
// (Edit title, Share, Stop sharing, Preview as shared, Archive, Inspect tmux,
|
|
9
|
+
// Change CWD, Git changes, Kill session, Open on claude.ai) are hidden, not
|
|
10
|
+
// greyed, while nothing is open: an empty screen offers what can be done there
|
|
11
|
+
// and nothing else.
|
|
11
12
|
//
|
|
12
13
|
// The shared read-only view is its own branch: Day/Night only, whatever the
|
|
13
14
|
// conversation state, and the trigger stays usable even when the shared
|
|
@@ -26,11 +27,14 @@ export const CHAT_ACTIONS_MENU_ENTRY_IDS = Object.freeze([
|
|
|
26
27
|
'chat-menu-pwa-version',
|
|
27
28
|
'chat-menu-queue-status',
|
|
28
29
|
'chat-menu-edit-title',
|
|
29
|
-
'chat-menu-
|
|
30
|
+
'chat-menu-theme-toggle',
|
|
30
31
|
'chat-menu-usage',
|
|
31
32
|
'chat-menu-compact',
|
|
32
33
|
'chat-menu-share-conversation',
|
|
34
|
+
'chat-menu-stop-sharing',
|
|
35
|
+
'chat-menu-preview-shared',
|
|
33
36
|
'chat-menu-archive',
|
|
37
|
+
'chat-menu-group',
|
|
34
38
|
'chat-menu-refresh-history',
|
|
35
39
|
'chat-menu-inspect-tmux',
|
|
36
40
|
'chat-menu-select-models',
|
|
@@ -38,6 +42,7 @@ export const CHAT_ACTIONS_MENU_ENTRY_IDS = Object.freeze([
|
|
|
38
42
|
'chat-menu-git-changes',
|
|
39
43
|
'chat-menu-open-cloud-session',
|
|
40
44
|
'chat-menu-settings',
|
|
45
|
+
'chat-menu-about',
|
|
41
46
|
'chat-menu-restart-relay',
|
|
42
47
|
'chat-menu-suspend-host',
|
|
43
48
|
'chat-menu-kill-session',
|
|
@@ -59,6 +64,7 @@ const hidden = () => ({ hidden: true, disabled: true });
|
|
|
59
64
|
* @param {string|null} [input.cloudSessionUrl] its claude.ai session link, if reported
|
|
60
65
|
* @param {boolean} [input.editing] its title is being edited
|
|
61
66
|
* @param {boolean} [input.suspendVisible] the "show Suspend host" setting
|
|
67
|
+
* @param {boolean} [input.hasActiveShare] it has a public share link
|
|
62
68
|
* @returns {{ trigger: { hidden: boolean, disabled: boolean }, entries: Record<string, { hidden: boolean, disabled: boolean }> }}
|
|
63
69
|
*/
|
|
64
70
|
export function resolveChatActionsMenuState({
|
|
@@ -70,11 +76,12 @@ export function resolveChatActionsMenuState({
|
|
|
70
76
|
cloudSessionUrl = null,
|
|
71
77
|
editing = false,
|
|
72
78
|
suspendVisible = false,
|
|
79
|
+
hasActiveShare = false,
|
|
73
80
|
} = {}) {
|
|
74
81
|
const entries = {};
|
|
75
82
|
if (sharedMode) {
|
|
76
83
|
for (const id of CHAT_ACTIONS_MENU_ENTRY_IDS) entries[id] = hidden();
|
|
77
|
-
entries['chat-menu-
|
|
84
|
+
entries['chat-menu-theme-toggle'] = shown();
|
|
78
85
|
return { trigger: { hidden: false, disabled: false }, entries };
|
|
79
86
|
}
|
|
80
87
|
|
|
@@ -87,10 +94,13 @@ export function resolveChatActionsMenuState({
|
|
|
87
94
|
|
|
88
95
|
for (const id of CHAT_ACTIONS_MENU_ENTRY_IDS) entries[id] = shown();
|
|
89
96
|
for (const id of ALWAYS_HIDDEN_IDS) entries[id] = hidden();
|
|
90
|
-
entries['chat-menu-shared-theme-toggle'] = hidden();
|
|
91
97
|
entries['chat-menu-edit-title'] = perConversation();
|
|
92
98
|
entries['chat-menu-share-conversation'] = perConversation();
|
|
99
|
+
// Only a conversation with a live link has something to stop.
|
|
100
|
+
entries['chat-menu-stop-sharing'] = hasConversation && hasActiveShare ? shown() : hidden();
|
|
101
|
+
entries['chat-menu-preview-shared'] = perConversation();
|
|
93
102
|
entries['chat-menu-archive'] = perConversation();
|
|
103
|
+
entries['chat-menu-group'] = perConversation();
|
|
94
104
|
entries['chat-menu-inspect-tmux'] = perConversation();
|
|
95
105
|
entries['chat-menu-git-changes'] = perConversation();
|
|
96
106
|
// A cloud session has no working directory on the relay host to change.
|