@oar-sh/oar 0.9.9 → 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 +561 -1
- package/README.md +162 -909
- package/bin/bat/oar.bat +46 -46
- package/package.json +14 -14
- package/server/README.md +369 -13
- package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
- package/server/claude-worker/claude-session-process.mjs +121 -9
- package/server/claude-worker/claude-session-worker.mjs +9 -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 +79 -12
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +6 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
- package/server/db-schema.mjs +95 -2
- package/server/grok-worker/acp-client.mjs +2 -1
- package/server/grok-worker/acp-host-services.mjs +4 -3
- 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/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 +187 -0
- package/server/public/app/api-client.js +312 -16
- package/server/public/app/archive-confirm.mjs +28 -0
- package/server/public/app/ask-user-view.js +17 -12
- package/server/public/app/attachments-view.js +516 -6
- package/server/public/app/background-tasks-view.mjs +18 -26
- package/server/public/app/bootstrap.js +336 -59
- package/server/public/app/chat-actions-menu-state.mjs +126 -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/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 +436 -117
- package/server/public/app/cwd-picker.js +1 -1
- package/server/public/app/explorer-edit-state.mjs +68 -0
- package/server/public/app/hidden-messages-view.mjs +312 -0
- package/server/public/app/host-suspend-ui.js +13 -4
- package/server/public/app/journal-view.js +930 -45
- package/server/public/app/no-conversation-state.mjs +58 -0
- package/server/public/app/preview-cards.mjs +117 -31
- package/server/public/app/preview-command.mjs +10 -1
- package/server/public/app/provider-readiness.mjs +120 -0
- package/server/public/app/relay-board-view.js +19 -3
- 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/relay-restart-error.mjs +16 -0
- package/server/public/app/settings-modal.js +93 -9
- 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 +137 -40
- package/server/public/app/store.js +115 -32
- 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 +558 -0
- package/server/public/app/task-row-format.mjs +29 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +464 -0
- package/server/public/app/transcript-jump-bottom.mjs +210 -0
- package/server/public/app/transcript-reload-freshness.mjs +25 -0
- package/server/public/app/transcript-viewport-anchor.mjs +140 -0
- package/server/public/app/update-ui.js +1 -1
- package/server/public/app/viewer-history.mjs +5 -3
- package/server/public/index.html +774 -12
- 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 +30 -3
- package/server/repositories/question-repository.mjs +9 -3
- package/server/repositories/remote-relay-repository.mjs +6 -2
- 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/repositories/subagent-timeline-statements.mjs +170 -0
- package/server/routes/about-routes.mjs +15 -0
- package/server/routes/ask-user-routes.mjs +3 -0
- package/server/routes/cache-routes.mjs +4 -0
- package/server/routes/conversation-group-routes.mjs +171 -0
- package/server/routes/messages-routes.mjs +698 -114
- package/server/routes/preview-routes.mjs +9 -1
- package/server/routes/relay-event-routes.mjs +157 -0
- package/server/routes/remote-relay-routes.mjs +15 -0
- package/server/routes/sessions-routes.mjs +843 -193
- package/server/server-runtime.mjs +559 -160
- 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/explorer-entry-name.mjs +26 -0
- package/server/services/explorer-mutations.mjs +122 -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 +49 -27
- 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/push-dispatch-service.mjs +18 -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-link-service.mjs +289 -0
- package/server/services/relay-event-service.mjs +1952 -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 +551 -7
- package/server/services/remote-relay-registry-service.mjs +11 -1
- package/server/services/request-boundary.mjs +183 -0
- package/server/services/session-activity-tracker.mjs +695 -0
- package/server/services/session-history-refresh-service.mjs +7 -0
- 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/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/status-event-service.mjs +7 -3
- 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/tunnel-worker-path-guard.mjs +24 -41
- package/server/services/upgrade-socket-guard.mjs +16 -0
- 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/relay-events.mjs +371 -0
- package/shared/remote-relay-contract.mjs +160 -6
- package/shared/share-path-privacy.mjs +30 -0
- package/shared/subagent-record-retention.mjs +31 -0
- package/shared/subagent-timeline.mjs +79 -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,13 +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 {
|
|
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';
|
|
56
64
|
import {
|
|
57
65
|
verifyExistingSession,
|
|
58
66
|
verifyToken,
|
|
@@ -91,6 +99,8 @@ import {
|
|
|
91
99
|
isArchivedConversationsView,
|
|
92
100
|
refreshNewConversationCloudRepoList,
|
|
93
101
|
openOriginConversation,
|
|
102
|
+
openConversationGroupPicker,
|
|
103
|
+
toggleConversationGroupFold,
|
|
94
104
|
} from './journal-view.js';
|
|
95
105
|
import {
|
|
96
106
|
loadRelayQuestions,
|
|
@@ -103,6 +113,7 @@ import {
|
|
|
103
113
|
renderMessages,
|
|
104
114
|
appendMessage,
|
|
105
115
|
compactCurrentConversation,
|
|
116
|
+
renderNoConversationState,
|
|
106
117
|
sendMessage,
|
|
107
118
|
handleKey,
|
|
108
119
|
updateComposerSlashMenu,
|
|
@@ -124,13 +135,17 @@ import {
|
|
|
124
135
|
conversationHasActiveTurn,
|
|
125
136
|
focusConversationMessageById,
|
|
126
137
|
syncRenderedPinState,
|
|
138
|
+
syncRenderedShareVisibility,
|
|
127
139
|
isConversationWindowAtTail,
|
|
128
140
|
getTranscriptJumpEpoch,
|
|
141
|
+
getTranscriptLiveRowEpoch,
|
|
129
142
|
} from './conversation-view.js';
|
|
143
|
+
import { loadKeepingLiveRows } from './transcript-reload-freshness.mjs';
|
|
130
144
|
import { initPinnedMessagesView, setPinsConversation } from './pinned-messages-view.mjs';
|
|
145
|
+
import { initHiddenMessagesView, setHiddenMessagesConversation } from './hidden-messages-view.mjs';
|
|
131
146
|
import { bindChatSelectionGuard, chatSelectionGuard, isChatInteractionHeld } from './selection-guard.mjs';
|
|
132
147
|
import { loadRepoBrowserTree, openRepoBrowser, closeRepoBrowser, setRepoBrowserSessionInfo, resetWorkspaceRepoBrowserForRootChange } from './attachments-view.js';
|
|
133
|
-
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 } from './attachments-view.js';
|
|
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';
|
|
134
149
|
import { initEmojiPicker, toggleEmojiPicker } from './emoji-view.js';
|
|
135
150
|
import { initSlashAutocomplete } from './slash-autocomplete.mjs';
|
|
136
151
|
import { initMentionAutocomplete } from './mention-autocomplete.mjs';
|
|
@@ -173,6 +188,8 @@ import {
|
|
|
173
188
|
openMessageSearchModal,
|
|
174
189
|
closeMessageSearchModal,
|
|
175
190
|
} from './message-search-view.js';
|
|
191
|
+
import { initSubagentViewer } from './subagent-viewer.mjs';
|
|
192
|
+
import { watchPreviewMarks } from './share-preview-marks.mjs';
|
|
176
193
|
import {
|
|
177
194
|
initGitChangesView,
|
|
178
195
|
openGitChangesModal,
|
|
@@ -274,6 +291,7 @@ import {
|
|
|
274
291
|
updateTheme,
|
|
275
292
|
openSettingsModal,
|
|
276
293
|
closeSettingsModal,
|
|
294
|
+
isSuspendHostActionVisible,
|
|
277
295
|
syncSuspendHostVisibility,
|
|
278
296
|
updateShowSuspendHostSetting,
|
|
279
297
|
syncDefaultSessionWorkspaceRootInput,
|
|
@@ -309,6 +327,7 @@ import {
|
|
|
309
327
|
updateTurnCeilingSetting,
|
|
310
328
|
previewBackgroundTaskTimeoutSetting,
|
|
311
329
|
updateBackgroundTaskTimeoutSetting,
|
|
330
|
+
updateSubagentRecordRetentionSetting,
|
|
312
331
|
} from './settings-modal.js';
|
|
313
332
|
import {
|
|
314
333
|
togglePushOnThisDevice,
|
|
@@ -356,14 +375,24 @@ import {
|
|
|
356
375
|
subscribeClaudeCloudSettings,
|
|
357
376
|
toggleClaudeCloudProvider,
|
|
358
377
|
} from './claude-cloud-settings-ui.js';
|
|
359
|
-
import { openCurrentCloudSession, syncClaudeCloudConversationUi } from './claude-cloud-conversation-ui.js';
|
|
378
|
+
import { isCurrentConversationClaudeCloud, openCurrentCloudSession, syncClaudeCloudConversationUi } from './claude-cloud-conversation-ui.js';
|
|
379
|
+
import {
|
|
380
|
+
CHAT_ACTIONS_MENU_ENTRY_IDS,
|
|
381
|
+
CHAT_ACTIONS_MENU_META_IDS,
|
|
382
|
+
resolveChatActionsMenuState,
|
|
383
|
+
} from './chat-actions-menu-state.mjs';
|
|
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';
|
|
360
388
|
import { syncConversationOriginHeader } from './conversation-origin-ui.js';
|
|
389
|
+
import { loadRelayEventSubscriptions, onRelayEventSubscriptionsChanged, syncRelayEventHeader } from './relay-event-subscriptions-ui.js';
|
|
361
390
|
import {
|
|
362
391
|
previewAgentSessionsMaxWait,
|
|
363
392
|
saveAgentSessionsMaxWait,
|
|
364
393
|
toggleAgentSessions,
|
|
365
394
|
} from './agent-sessions-settings-ui.js';
|
|
366
|
-
import { CLAUDE_CLOUD_PROVIDER, claudeCloudComposerModelIds } from './claude-cloud-ui.mjs';
|
|
395
|
+
import { CLAUDE_CLOUD_PROVIDER, buildCloudLineModel, claudeCloudComposerModelIds } from './claude-cloud-ui.mjs';
|
|
367
396
|
import {
|
|
368
397
|
confirmCliInstall,
|
|
369
398
|
runCliInstall,
|
|
@@ -377,6 +406,7 @@ import {
|
|
|
377
406
|
dismissAvailableUpdate,
|
|
378
407
|
dismissUpdateOutcome,
|
|
379
408
|
} from './update-ui.js';
|
|
409
|
+
import { shouldAutofocusComposerOnLoad } from './composer-keyboard.mjs';
|
|
380
410
|
|
|
381
411
|
const MODEL_STORAGE_KEY = 'copilot_selected_model';
|
|
382
412
|
// The New Chat modal used to keep its own model key, so a selection made there
|
|
@@ -442,6 +472,8 @@ let liveConversationPollInFlight = false;
|
|
|
442
472
|
let sharedViewerIdFallback = '';
|
|
443
473
|
let sharedConversationRenderKey = '';
|
|
444
474
|
let sharedConversationLastError = '';
|
|
475
|
+
// The share was stopped or renewed while the page was open: the reader stops.
|
|
476
|
+
let sharedConversationEnded = false;
|
|
445
477
|
let networkLifecycleBound = false;
|
|
446
478
|
let foregroundRecoveryTimer = null;
|
|
447
479
|
let foregroundRecoveryInFlight = false;
|
|
@@ -860,14 +892,14 @@ function initNetworkLifecycleHandling() {
|
|
|
860
892
|
}
|
|
861
893
|
|
|
862
894
|
function syncThemeMenuLabel() {
|
|
863
|
-
const button = document.getElementById('chat-menu-
|
|
895
|
+
const button = document.getElementById('chat-menu-theme-toggle');
|
|
864
896
|
if (!button) return;
|
|
865
897
|
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
|
866
898
|
button.textContent = isLight ? '🌙 Night mode' : '☀️ Day mode';
|
|
867
899
|
}
|
|
868
900
|
|
|
869
901
|
function initThemeMenuToggle() {
|
|
870
|
-
const themeBtn = document.getElementById('chat-menu-
|
|
902
|
+
const themeBtn = document.getElementById('chat-menu-theme-toggle');
|
|
871
903
|
if (!themeBtn) return;
|
|
872
904
|
themeBtn.hidden = false;
|
|
873
905
|
syncThemeMenuLabel();
|
|
@@ -924,7 +956,7 @@ function applySharedReaderUi() {
|
|
|
924
956
|
const modeSelect = document.getElementById('mode-select');
|
|
925
957
|
const chatMenuBtn = document.getElementById('chat-actions-menu-btn');
|
|
926
958
|
const chatMenu = document.getElementById('chat-actions-menu');
|
|
927
|
-
const
|
|
959
|
+
const themeBtn = document.getElementById('chat-menu-theme-toggle');
|
|
928
960
|
const installBtn = document.getElementById('install-btn');
|
|
929
961
|
const activeElement = document.activeElement;
|
|
930
962
|
if (chatMenu && activeElement instanceof Element && chatMenu.contains(activeElement)) {
|
|
@@ -941,7 +973,7 @@ function applySharedReaderUi() {
|
|
|
941
973
|
if (input) {
|
|
942
974
|
input.readOnly = true;
|
|
943
975
|
input.disabled = true;
|
|
944
|
-
input.placeholder = 'Shared conversation (read-only)';
|
|
976
|
+
input.placeholder = IS_SHARE_PREVIEW ? 'Preview as shared (read-only)' : 'Shared conversation (read-only)';
|
|
945
977
|
}
|
|
946
978
|
disableSharedControl(sendBtn);
|
|
947
979
|
disableSharedControl(attachBtn);
|
|
@@ -953,19 +985,23 @@ function applySharedReaderUi() {
|
|
|
953
985
|
if (modelSelect) modelSelect.hidden = true;
|
|
954
986
|
if (effortSelect) effortSelect.hidden = true;
|
|
955
987
|
if (modeSelect) modeSelect.hidden = true;
|
|
956
|
-
|
|
988
|
+
// One source for the menu: the resolver's shared branch (Day/Night only,
|
|
989
|
+
// trigger usable). The loop below stays as a guard for entries the resolver
|
|
990
|
+
// does not know.
|
|
991
|
+
applyChatActionsMenuState(resolveChatActionsMenuState({ sharedMode: true }));
|
|
992
|
+
if (chatMenu && themeBtn) {
|
|
957
993
|
for (const child of Array.from(chatMenu.children)) {
|
|
958
|
-
if (child !==
|
|
994
|
+
if (child !== themeBtn) hideSharedMenuEntry(child);
|
|
959
995
|
}
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
996
|
+
themeBtn.hidden = false;
|
|
997
|
+
themeBtn.removeAttribute('aria-hidden');
|
|
998
|
+
themeBtn.disabled = false;
|
|
999
|
+
themeBtn.tabIndex = 0;
|
|
964
1000
|
syncThemeMenuLabel();
|
|
965
1001
|
} else if (chatMenuBtn) {
|
|
966
1002
|
chatMenuBtn.hidden = true;
|
|
967
1003
|
}
|
|
968
|
-
if (chatMenuBtn && !
|
|
1004
|
+
if (chatMenuBtn && !themeBtn) chatMenuBtn.hidden = true;
|
|
969
1005
|
}
|
|
970
1006
|
|
|
971
1007
|
function syncChatTitleWatcherIndicator() {
|
|
@@ -1006,18 +1042,50 @@ function resolveStableScrollTopForLiveRefresh(messagesEl, capturedScrollTop) {
|
|
|
1006
1042
|
return captured;
|
|
1007
1043
|
}
|
|
1008
1044
|
|
|
1009
|
-
|
|
1010
|
-
|
|
1045
|
+
// Whether a reload keeps the reader at the end: as it was when the reload was
|
|
1046
|
+
// asked for, unless they have moved while it was on its way (scrolled up, or
|
|
1047
|
+
// jumped back to the end); then as it is now.
|
|
1048
|
+
function resolveLiveRefreshAtBottom(messagesEl, capturedScrollTop, capturedAtBottom) {
|
|
1049
|
+
const current = Number(messagesEl?.scrollTop);
|
|
1050
|
+
if (!Number.isFinite(current) || Math.abs(current - Number(capturedScrollTop)) <= 2) return capturedAtBottom;
|
|
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.');
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
async function refreshSharedConversation({ open = false } = {}) {
|
|
1068
|
+
if (sharedConversationPollInFlight || sharedConversationEnded) return;
|
|
1011
1069
|
const sharedToken = resolveSharedTokenFromLocation();
|
|
1012
|
-
if (!sharedToken) return;
|
|
1070
|
+
if (!sharedToken && !IS_SHARE_PREVIEW) return;
|
|
1013
1071
|
sharedConversationPollInFlight = true;
|
|
1014
1072
|
const requestSeq = ++sharedConversationRequestSeq;
|
|
1015
1073
|
const messagesEl = document.getElementById('messages');
|
|
1016
|
-
const
|
|
1074
|
+
const wasAtBottom = isMessagesNearBottom();
|
|
1017
1075
|
const savedScrollTop = messagesEl?.scrollTop || 0;
|
|
1018
1076
|
try {
|
|
1019
|
-
|
|
1077
|
+
// The reader has no socket today, so nothing moves the count; the guard
|
|
1078
|
+
// keeps a rebuild from dropping a live row should one ever arrive.
|
|
1079
|
+
const { response, stale } = await loadKeepingLiveRows(
|
|
1080
|
+
() => loadSharedConversation(sharedToken, { limit: 120, open }),
|
|
1081
|
+
getTranscriptLiveRowEpoch,
|
|
1082
|
+
);
|
|
1083
|
+
if (response?.status === 404) {
|
|
1084
|
+
endSharedConversationReader();
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1020
1087
|
if (requestSeq < sharedConversationAppliedSeq) return;
|
|
1088
|
+
if (stale) return;
|
|
1021
1089
|
if (!response?.ok) {
|
|
1022
1090
|
const message = String(response?.error || 'Could not load shared conversation.').trim();
|
|
1023
1091
|
if (message && message !== sharedConversationLastError) {
|
|
@@ -1050,6 +1118,7 @@ async function refreshSharedConversation() {
|
|
|
1050
1118
|
return;
|
|
1051
1119
|
}
|
|
1052
1120
|
sharedConversationRenderKey = renderKey;
|
|
1121
|
+
const preserveBottom = resolveLiveRefreshAtBottom(messagesEl, savedScrollTop, wasAtBottom);
|
|
1053
1122
|
const stableSavedScrollTop = resolveStableScrollTopForLiveRefresh(messagesEl, savedScrollTop);
|
|
1054
1123
|
applyLoadedConversationState(convId, response, {
|
|
1055
1124
|
restoreScroll: !preserveBottom,
|
|
@@ -1065,15 +1134,29 @@ async function refreshSharedConversation() {
|
|
|
1065
1134
|
|
|
1066
1135
|
async function pulseSharedViewerPresence() {
|
|
1067
1136
|
const sharedToken = resolveSharedTokenFromLocation();
|
|
1068
|
-
if (!sharedToken) return;
|
|
1137
|
+
if (!sharedToken || sharedConversationEnded) return;
|
|
1069
1138
|
await reportSharedViewerPresence(sharedToken, sharedViewerId()).catch(() => {});
|
|
1070
1139
|
}
|
|
1071
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
|
+
|
|
1072
1153
|
async function initSharedConversationReader() {
|
|
1073
1154
|
applySharedReaderUi();
|
|
1074
|
-
|
|
1155
|
+
initSharePreviewUi();
|
|
1156
|
+
await refreshSharedConversation({ open: true });
|
|
1075
1157
|
await pulseSharedViewerPresence();
|
|
1076
1158
|
stopSharedModeTimers();
|
|
1159
|
+
if (sharedConversationEnded) return;
|
|
1077
1160
|
sharedConversationPollTimer = setInterval(() => {
|
|
1078
1161
|
if (!shouldRunForegroundNetworkWork()) return;
|
|
1079
1162
|
void refreshSharedConversation();
|
|
@@ -1090,6 +1173,12 @@ function showAuthError(msg) {
|
|
|
1090
1173
|
|
|
1091
1174
|
function resolveAuthErrorMessage(result = null) {
|
|
1092
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
|
+
}
|
|
1093
1182
|
if (result?.status === 403) return 'Access denied';
|
|
1094
1183
|
if (result?.status > 0) return `Authentication failed (${result.status})`;
|
|
1095
1184
|
return String(result?.error || 'Could not reach the relay').trim() || 'Could not reach the relay';
|
|
@@ -1382,6 +1471,8 @@ function syncComposerPlaceholder() {
|
|
|
1382
1471
|
if (!input) return;
|
|
1383
1472
|
if (isSharedReaderMode()) return;
|
|
1384
1473
|
if (imageEditTarget) return;
|
|
1474
|
+
// The composer is locked until a provider is ready; its text says why.
|
|
1475
|
+
if (input.dataset.setupLocked === '1') return;
|
|
1385
1476
|
const rawValue = String(document.getElementById('model-select')?.value || '');
|
|
1386
1477
|
const { baseModelId } = splitModelVariantId(rawValue);
|
|
1387
1478
|
input.placeholder = composerPlaceholderFor({
|
|
@@ -1616,6 +1707,8 @@ function updateModelCatalogState(payload) {
|
|
|
1616
1707
|
// Shared readers never load the catalog, so this would only surface
|
|
1617
1708
|
// "metadata unavailable" warnings for a picker they cannot see.
|
|
1618
1709
|
if (isSharedReaderMode()) return;
|
|
1710
|
+
// The catalog's `source` is the Copilot readiness signal (provider-readiness.mjs).
|
|
1711
|
+
updateProviderReadinessInputs({ catalog: payload && typeof payload === 'object' ? payload : null });
|
|
1619
1712
|
const select = document.getElementById('model-select');
|
|
1620
1713
|
if (!select) return;
|
|
1621
1714
|
const models = Array.isArray(payload?.models)
|
|
@@ -2180,6 +2273,8 @@ function initModeSelector() {
|
|
|
2180
2273
|
function refreshModelCatalog(force = false) {
|
|
2181
2274
|
return loadModelCatalog().then((r) => {
|
|
2182
2275
|
if (!r) {
|
|
2276
|
+
// A failed load leaves provider readiness unknown: no setup wall on a blip.
|
|
2277
|
+
updateProviderReadinessInputs({ catalog: null });
|
|
2183
2278
|
if (isModelMetadataHealthy(modelCatalogState) || isModelMetadataHealthy(lastHealthyModelCatalogState)) {
|
|
2184
2279
|
setModelBanner('⚠️ Could not refresh model metadata; using last known good catalog.');
|
|
2185
2280
|
syncModelMetadataBlocker();
|
|
@@ -3630,24 +3725,35 @@ async function pollAuthenticatedCurrentConversationLive() {
|
|
|
3630
3725
|
liveConversationPollInFlight = true;
|
|
3631
3726
|
try {
|
|
3632
3727
|
const messagesEl = document.getElementById('messages');
|
|
3633
|
-
const
|
|
3728
|
+
const wasAtBottom = isMessagesNearBottom();
|
|
3634
3729
|
const savedScrollTop = messagesEl?.scrollTop || 0;
|
|
3635
3730
|
const savedLoadedCount = loadConversationLoadedMessageCount(currentId);
|
|
3636
3731
|
const requestLimit = Math.max(20, getConversationLoadedMessageCount() || 0, savedLoadedCount || 0);
|
|
3637
3732
|
const jumpEpoch = getTranscriptJumpEpoch();
|
|
3638
|
-
const response = await
|
|
3733
|
+
const { response, stale } = await loadKeepingLiveRows(
|
|
3734
|
+
() => loadConversation(currentId, { limit: requestLimit }),
|
|
3735
|
+
getTranscriptLiveRowEpoch,
|
|
3736
|
+
);
|
|
3639
3737
|
if (!response) return;
|
|
3640
3738
|
if (String(currentConvId || '').trim() !== currentId) return;
|
|
3641
3739
|
// The user jumped into the history while this request was on its way: the
|
|
3642
3740
|
// answer is the end of the conversation as it was asked for before that,
|
|
3643
3741
|
// and would overwrite the window they jumped to. The next round asks again.
|
|
3644
3742
|
if (getTranscriptJumpEpoch() !== jumpEpoch) return;
|
|
3743
|
+
// Rows kept arriving through every request: the page has them, the answer
|
|
3744
|
+
// does not. The next round asks again.
|
|
3745
|
+
if (stale) return;
|
|
3746
|
+
const preserveBottom = resolveLiveRefreshAtBottom(messagesEl, savedScrollTop, wasAtBottom);
|
|
3645
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);
|
|
3646
3751
|
applyLoadedConversationState(currentId, response, {
|
|
3647
3752
|
restoreScroll: !preserveBottom,
|
|
3648
3753
|
savedScrollTop: preserveBottom ? null : stableSavedScrollTop,
|
|
3649
3754
|
followLiveUpdates: preserveBottom,
|
|
3650
3755
|
});
|
|
3756
|
+
if (reading) restoreReadingBlock(messagesEl, reading);
|
|
3651
3757
|
} finally {
|
|
3652
3758
|
liveConversationPollInFlight = false;
|
|
3653
3759
|
}
|
|
@@ -3664,6 +3770,7 @@ function startLiveConversationPolling() {
|
|
|
3664
3770
|
const transcriptResizeKeeper = createTranscriptResizeKeeper({
|
|
3665
3771
|
getElement: () => document.getElementById('messages'),
|
|
3666
3772
|
});
|
|
3773
|
+
onTranscriptEndLeft(() => transcriptResizeKeeper.recordEndLeft());
|
|
3667
3774
|
|
|
3668
3775
|
function setupViewportTracking() {
|
|
3669
3776
|
syncViewportMetrics();
|
|
@@ -3793,6 +3900,19 @@ function initMessageScrollPersistence() {
|
|
|
3793
3900
|
}, { passive: true });
|
|
3794
3901
|
}
|
|
3795
3902
|
|
|
3903
|
+
// The jump-to-bottom button on a window in the middle of the history: the
|
|
3904
|
+
// end of the conversation replaces it, scrolled to the bottom.
|
|
3905
|
+
async function loadCurrentConversationEnd() {
|
|
3906
|
+
const currentId = String(currentConvId || '').trim();
|
|
3907
|
+
if (!currentId) return;
|
|
3908
|
+
const jumpEpoch = getTranscriptJumpEpoch();
|
|
3909
|
+
const response = await loadConversation(currentId, { limit: 20 });
|
|
3910
|
+
if (!response || String(currentConvId || '').trim() !== currentId) return;
|
|
3911
|
+
// A jump into the history while it was on its way wins.
|
|
3912
|
+
if (getTranscriptJumpEpoch() !== jumpEpoch) return;
|
|
3913
|
+
applyLoadedConversationState(currentId, response, { restoreScroll: false });
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3796
3916
|
async function runConversationHistoryRefresh({ source = 'menu' } = {}) {
|
|
3797
3917
|
const currentId = String(currentConvId || '').trim();
|
|
3798
3918
|
if (!currentId) {
|
|
@@ -3851,9 +3971,8 @@ async function refreshCurrentView() {
|
|
|
3851
3971
|
const capturedVersion = ++refreshViewVersion;
|
|
3852
3972
|
const jumpEpoch = getTranscriptJumpEpoch();
|
|
3853
3973
|
const messagesEl = document.getElementById('messages');
|
|
3854
|
-
const
|
|
3974
|
+
const wasAtBottom = isMessagesNearBottom();
|
|
3855
3975
|
const scrollTop = messagesEl?.scrollTop || 0;
|
|
3856
|
-
const stableSavedScrollTop = resolveStableScrollTopForLiveRefresh(messagesEl, scrollTop);
|
|
3857
3976
|
|
|
3858
3977
|
await refreshConversations();
|
|
3859
3978
|
|
|
@@ -3869,13 +3988,22 @@ async function refreshCurrentView() {
|
|
|
3869
3988
|
|
|
3870
3989
|
const savedLoadedCount = loadConversationLoadedMessageCount(currentId);
|
|
3871
3990
|
const requestLimit = Math.max(20, getConversationLoadedMessageCount() || 0, savedLoadedCount || 0);
|
|
3872
|
-
const r = await
|
|
3991
|
+
const { response: r, stale } = await loadKeepingLiveRows(
|
|
3992
|
+
() => loadConversation(currentId, { limit: requestLimit }),
|
|
3993
|
+
getTranscriptLiveRowEpoch,
|
|
3994
|
+
);
|
|
3873
3995
|
if (capturedVersion < refreshViewVersion) return;
|
|
3874
3996
|
if (String(currentConvId || '').trim() !== currentId) return;
|
|
3875
3997
|
// The user jumped into the history, before this refresh or while it was on
|
|
3876
3998
|
// its way: title, tasks, pins and the rest are refreshed, the transcript
|
|
3877
|
-
// and its scroll position are left as they are.
|
|
3878
|
-
|
|
3999
|
+
// and its scroll position are left as they are. So too when rows kept
|
|
4000
|
+
// arriving through every request: the page has rows the answer lacks.
|
|
4001
|
+
const keepTranscript = getTranscriptJumpEpoch() !== jumpEpoch || transcriptIsInHistory(currentId) || stale;
|
|
4002
|
+
const preserveBottom = resolveLiveRefreshAtBottom(messagesEl, scrollTop, wasAtBottom);
|
|
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;
|
|
3879
4007
|
if (r) {
|
|
3880
4008
|
applyLoadedConversationState(currentId, r, {
|
|
3881
4009
|
restoreScroll: !preserveBottom,
|
|
@@ -3883,6 +4011,9 @@ async function refreshCurrentView() {
|
|
|
3883
4011
|
followLiveUpdates: preserveBottom && !keepTranscript,
|
|
3884
4012
|
keepTranscript,
|
|
3885
4013
|
});
|
|
4014
|
+
// At once, and again below once the cards are back on the page.
|
|
4015
|
+
restoreReadingBlock(messagesEl, reading);
|
|
4016
|
+
restoredAt = { scrollTop: messagesEl?.scrollTop };
|
|
3886
4017
|
} else {
|
|
3887
4018
|
setRepoBrowserSessionInfo('', '');
|
|
3888
4019
|
restoreInFlightThinking(null);
|
|
@@ -3891,7 +4022,12 @@ async function refreshCurrentView() {
|
|
|
3891
4022
|
await loadRelayBoards();
|
|
3892
4023
|
scheduleContextUsageRefresh(currentId, 0);
|
|
3893
4024
|
if (messagesEl && !keepTranscript && String(currentConvId || '').trim() === currentId) {
|
|
3894
|
-
|
|
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) {
|
|
3895
4031
|
messagesEl.scrollTop = stableSavedScrollTop;
|
|
3896
4032
|
}
|
|
3897
4033
|
saveConversationScrollTop(currentId, messagesEl.scrollTop);
|
|
@@ -3951,7 +4087,7 @@ function getChatTitleElements() {
|
|
|
3951
4087
|
return {
|
|
3952
4088
|
wrap: document.getElementById('chat-title-wrap'),
|
|
3953
4089
|
title: document.getElementById('chat-title'),
|
|
3954
|
-
|
|
4090
|
+
menuBtn: document.getElementById('chat-actions-menu-btn'),
|
|
3955
4091
|
editor: document.getElementById('chat-title-editor'),
|
|
3956
4092
|
input: document.getElementById('chat-title-input'),
|
|
3957
4093
|
saveBtn: document.getElementById('chat-title-save-btn'),
|
|
@@ -3968,12 +4104,68 @@ function closeChatActionsMenu() {
|
|
|
3968
4104
|
if (backdrop && !window.__chatActionsMenuShieldTimer) backdrop.classList.remove('visible');
|
|
3969
4105
|
}
|
|
3970
4106
|
|
|
4107
|
+
// Writes one resolver decision to the DOM, the way the shared view hides its
|
|
4108
|
+
// entries: `hidden` alone for the text chips, plus `disabled`, `aria-hidden`
|
|
4109
|
+
// and the tab order for the buttons.
|
|
4110
|
+
function applyChatActionsMenuState(state) {
|
|
4111
|
+
const trigger = document.getElementById('chat-actions-menu-btn');
|
|
4112
|
+
if (trigger && state?.trigger) {
|
|
4113
|
+
trigger.hidden = !!state.trigger.hidden;
|
|
4114
|
+
trigger.disabled = !!state.trigger.disabled;
|
|
4115
|
+
}
|
|
4116
|
+
const entries = state?.entries || {};
|
|
4117
|
+
for (const id of CHAT_ACTIONS_MENU_ENTRY_IDS) {
|
|
4118
|
+
const element = document.getElementById(id);
|
|
4119
|
+
const decision = entries[id];
|
|
4120
|
+
if (!element || !decision) continue;
|
|
4121
|
+
element.hidden = !!decision.hidden;
|
|
4122
|
+
if (CHAT_ACTIONS_MENU_META_IDS.includes(id)) continue;
|
|
4123
|
+
if (decision.hidden) {
|
|
4124
|
+
element.setAttribute('aria-hidden', 'true');
|
|
4125
|
+
if ('disabled' in element) element.disabled = true;
|
|
4126
|
+
if ('tabIndex' in element) element.tabIndex = -1;
|
|
4127
|
+
continue;
|
|
4128
|
+
}
|
|
4129
|
+
element.removeAttribute('aria-hidden');
|
|
4130
|
+
if ('disabled' in element) element.disabled = !!decision.disabled;
|
|
4131
|
+
if ('tabIndex' in element) element.tabIndex = 0;
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
// The ⋯ menu as it should look right now: the trigger and every entry,
|
|
4136
|
+
// decided in chat-actions-menu-state.mjs from the conversation on screen.
|
|
4137
|
+
function syncChatActionsMenuState() {
|
|
4138
|
+
const convId = String(currentConvId || '').trim();
|
|
4139
|
+
const conversation = convId ? (conversations[convId] || null) : null;
|
|
4140
|
+
const isCloud = !!convId && isCurrentConversationClaudeCloud();
|
|
4141
|
+
const state = resolveChatActionsMenuState({
|
|
4142
|
+
sharedMode: isSharedReaderMode(),
|
|
4143
|
+
conversationId: convId,
|
|
4144
|
+
conversation,
|
|
4145
|
+
sdkSessionId: String(conversation?.sdkSessionId || '').trim(),
|
|
4146
|
+
isCloud,
|
|
4147
|
+
cloudSessionUrl: isCloud ? buildCloudLineModel(conversation?.cloud)?.sessionUrl || null : null,
|
|
4148
|
+
editing: !!convId && chatTitleEditingConversationId === convId,
|
|
4149
|
+
suspendVisible: isSuspendHostActionVisible(),
|
|
4150
|
+
hasActiveShare: hasActiveShare(convId),
|
|
4151
|
+
});
|
|
4152
|
+
applyChatActionsMenuState(state);
|
|
4153
|
+
return state;
|
|
4154
|
+
}
|
|
4155
|
+
|
|
4156
|
+
// The menu follows the relay's share list (made, renewed, stopped anywhere).
|
|
4157
|
+
subscribeShares(() => syncChatActionsMenuState());
|
|
4158
|
+
|
|
3971
4159
|
function toggleChatActionsMenu() {
|
|
3972
4160
|
const menu = document.getElementById('chat-actions-menu');
|
|
3973
4161
|
const trigger = document.getElementById('chat-actions-menu-btn');
|
|
3974
|
-
if (!menu || !trigger
|
|
4162
|
+
if (!menu || !trigger) return;
|
|
4163
|
+
// The entries match the moment of the tap, as Refresh history already does.
|
|
4164
|
+
syncChatActionsMenuState();
|
|
4165
|
+
if (trigger.hidden || trigger.disabled) return;
|
|
3975
4166
|
syncRefreshHistoryMenuState();
|
|
3976
4167
|
syncThemeMenuLabel();
|
|
4168
|
+
syncAboutMenuLabel();
|
|
3977
4169
|
const willOpen = !!menu.hidden;
|
|
3978
4170
|
menu.hidden = !willOpen;
|
|
3979
4171
|
trigger.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
|
|
@@ -4050,12 +4242,13 @@ function lockChatActionsMenuShield(ms = 300) {
|
|
|
4050
4242
|
|
|
4051
4243
|
|
|
4052
4244
|
function syncChatTitleControls() {
|
|
4053
|
-
const { title,
|
|
4245
|
+
const { title, editor, input } = getChatTitleElements();
|
|
4054
4246
|
const convId = String(currentConvId || '').trim();
|
|
4055
|
-
// No conversation open (deleted, new chat): its 📍
|
|
4056
|
-
if (!convId)
|
|
4057
|
-
|
|
4058
|
-
|
|
4247
|
+
// No conversation open (deleted, new chat): its 📍 and 🙈 buttons go with it.
|
|
4248
|
+
if (!convId) {
|
|
4249
|
+
setPinsConversation(null);
|
|
4250
|
+
setHiddenMessagesConversation(null);
|
|
4251
|
+
}
|
|
4059
4252
|
const conversation = convId ? (conversations[convId] || null) : null;
|
|
4060
4253
|
const sdkSessionId = String(conversation?.sdkSessionId || '').trim();
|
|
4061
4254
|
if (title) {
|
|
@@ -4079,17 +4272,11 @@ function syncChatTitleControls() {
|
|
|
4079
4272
|
const editing = convId && chatTitleEditingConversationId === convId;
|
|
4080
4273
|
document.body.classList.toggle('chat-title-editing', !!editing);
|
|
4081
4274
|
if (title) title.hidden = editing;
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
}
|
|
4086
|
-
if (killBtn) {
|
|
4087
|
-
killBtn.disabled = sharedMode || !convId || !sdkSessionId;
|
|
4088
|
-
killBtn.hidden = sharedMode || !convId;
|
|
4089
|
-
}
|
|
4275
|
+
// The ⋯ trigger and its entries: relay-wide ones stay with no conversation
|
|
4276
|
+
// open, so Settings is always one tap away.
|
|
4277
|
+
syncChatActionsMenuState();
|
|
4090
4278
|
const archiveBtn = document.getElementById('chat-menu-archive');
|
|
4091
4279
|
if (archiveBtn) {
|
|
4092
|
-
archiveBtn.hidden = sharedMode || !convId;
|
|
4093
4280
|
archiveBtn.textContent = conversation?.archived === true ? '📂 Unarchive conversation' : '🗄 Archive conversation';
|
|
4094
4281
|
}
|
|
4095
4282
|
if (editor) {
|
|
@@ -4112,19 +4299,21 @@ function syncChatTitleControls() {
|
|
|
4112
4299
|
syncClaudeCloudConversationUi();
|
|
4113
4300
|
// "via agent": the conversation an agent on this relay started this one from.
|
|
4114
4301
|
syncConversationOriginHeader();
|
|
4302
|
+
// Relay events: "watching N sessions" on a watcher, "watched" on a watched one.
|
|
4303
|
+
syncRelayEventHeader();
|
|
4115
4304
|
syncChatTitleWatcherIndicator();
|
|
4116
4305
|
}
|
|
4117
4306
|
|
|
4118
4307
|
function openChatTitleEditor() {
|
|
4119
4308
|
const convId = String(currentConvId || '').trim();
|
|
4120
4309
|
if (!convId) return;
|
|
4121
|
-
const { title,
|
|
4310
|
+
const { title, menuBtn, editor, input } = getChatTitleElements();
|
|
4122
4311
|
closeChatActionsMenu();
|
|
4123
4312
|
const currentTitle = String(conversations[convId]?.title || title?.textContent || convId).trim() || convId;
|
|
4124
4313
|
chatTitleEditingConversationId = convId;
|
|
4125
4314
|
document.body.classList.add('chat-title-editing');
|
|
4126
4315
|
if (title) title.hidden = true;
|
|
4127
|
-
if (
|
|
4316
|
+
if (menuBtn) menuBtn.disabled = true;
|
|
4128
4317
|
if (editor) editor.hidden = false;
|
|
4129
4318
|
if (input) {
|
|
4130
4319
|
input.maxLength = CHAT_TITLE_MAX_LENGTH;
|
|
@@ -4143,11 +4332,11 @@ function openChatTitleEditor() {
|
|
|
4143
4332
|
function closeChatTitleEditor() {
|
|
4144
4333
|
chatTitleEditingConversationId = null;
|
|
4145
4334
|
document.body.classList.remove('chat-title-editing');
|
|
4146
|
-
const { title,
|
|
4335
|
+
const { title, editor, input } = getChatTitleElements();
|
|
4147
4336
|
closeChatActionsMenu();
|
|
4148
4337
|
if (editor) editor.hidden = true;
|
|
4149
4338
|
if (title) title.hidden = !String(currentConvId || '').trim();
|
|
4150
|
-
|
|
4339
|
+
// The trigger is decided by the syncChatTitleControls() call below.
|
|
4151
4340
|
if (input) {
|
|
4152
4341
|
const convId = String(currentConvId || '').trim();
|
|
4153
4342
|
input.value = convId ? String(conversations[convId]?.title || title?.textContent || convId) : '';
|
|
@@ -4317,19 +4506,19 @@ async function initApp() {
|
|
|
4317
4506
|
appSharedMode = sharedMode;
|
|
4318
4507
|
initNetworkLifecycleHandling();
|
|
4319
4508
|
setForegroundNetworkWorkEnabled(document.visibilityState === 'visible');
|
|
4320
|
-
const sharedThemeBtn = document.getElementById('chat-menu-shared-theme-toggle');
|
|
4321
4509
|
initTheme();
|
|
4322
4510
|
initScrollbarHover();
|
|
4323
4511
|
initThemeMenuToggle();
|
|
4324
|
-
if (sharedThemeBtn && sharedMode) sharedThemeBtn.hidden = false;
|
|
4325
4512
|
initFontScaling();
|
|
4326
4513
|
if (!sharedMode) {
|
|
4327
4514
|
clearLegacyKnownCwdHistoryStorage();
|
|
4328
4515
|
}
|
|
4329
4516
|
syncPwaVersionMenuEntry();
|
|
4330
4517
|
syncQueueStatusMenuEntry();
|
|
4518
|
+
if (!sharedMode) syncAboutMenuLabel();
|
|
4331
4519
|
if (!sharedMode) {
|
|
4332
4520
|
syncSuspendHostVisibility();
|
|
4521
|
+
syncScrollOnSendToggle();
|
|
4333
4522
|
initHostSuspendUi();
|
|
4334
4523
|
initUsageLimitUi();
|
|
4335
4524
|
}
|
|
@@ -4394,6 +4583,16 @@ async function initApp() {
|
|
|
4394
4583
|
});
|
|
4395
4584
|
});
|
|
4396
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
|
+
});
|
|
4397
4596
|
const chatMenuArchiveBtn = document.getElementById('chat-menu-archive');
|
|
4398
4597
|
bindMenuAction(chatMenuArchiveBtn, (event) => {
|
|
4399
4598
|
event.preventDefault();
|
|
@@ -4424,10 +4623,35 @@ async function initApp() {
|
|
|
4424
4623
|
}
|
|
4425
4624
|
const copied = await copyTextToClipboard(shareUrl);
|
|
4426
4625
|
showTransientRelayNotice(copied ? 'Share link copied to clipboard.' : shareUrl);
|
|
4626
|
+
void refreshShares().catch(() => {});
|
|
4427
4627
|
}).catch((error) => {
|
|
4428
4628
|
showTransientRelayNotice(error?.message || 'Could not create share link.');
|
|
4429
4629
|
});
|
|
4430
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
|
+
});
|
|
4431
4655
|
const chatMenuRefreshHistoryBtn = document.getElementById('chat-menu-refresh-history');
|
|
4432
4656
|
if (chatMenuRefreshHistoryBtn && !chatMenuRefreshHistoryBtn.hidden && !chatMenuRefreshHistoryBtn.disabled) {
|
|
4433
4657
|
bindMenuAction(chatMenuRefreshHistoryBtn, (event) => {
|
|
@@ -4480,6 +4704,14 @@ async function initApp() {
|
|
|
4480
4704
|
closeChatActionsMenu();
|
|
4481
4705
|
openSettingsModal();
|
|
4482
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
|
+
});
|
|
4483
4715
|
const chatMenuRestartRelayBtn = document.getElementById('chat-menu-restart-relay');
|
|
4484
4716
|
bindMenuAction(chatMenuRestartRelayBtn, (event) => {
|
|
4485
4717
|
event.preventDefault();
|
|
@@ -4556,6 +4788,8 @@ async function initApp() {
|
|
|
4556
4788
|
initConversationHistoryLazyLoading();
|
|
4557
4789
|
initBubbleActionHandlers();
|
|
4558
4790
|
initMessageScrollPersistence();
|
|
4791
|
+
initTranscriptJumpToBottom();
|
|
4792
|
+
initSubagentViewer();
|
|
4559
4793
|
syncChatTitleControls();
|
|
4560
4794
|
await initSharedConversationReader();
|
|
4561
4795
|
return;
|
|
@@ -4578,14 +4812,25 @@ async function initApp() {
|
|
|
4578
4812
|
const status = await refreshWorkspaceRootHints();
|
|
4579
4813
|
syncQueueStatusMenuEntry(status);
|
|
4580
4814
|
setSessionWorkerStatesFromStatusPayload(status?.sessionWorker || null);
|
|
4581
|
-
await refreshOpenAISettingsState();
|
|
4582
|
-
await refreshCursorSettingsState();
|
|
4583
|
-
await refreshGrokSettingsState();
|
|
4815
|
+
const openAISettings = await refreshOpenAISettingsState();
|
|
4816
|
+
const cursorSettings = await refreshCursorSettingsState();
|
|
4817
|
+
const grokSettings = await refreshGrokSettingsState();
|
|
4818
|
+
// Loaded here for provider readiness: whether any provider can run a chat
|
|
4819
|
+
// decides what the empty screen, New Chat and the composer offer.
|
|
4820
|
+
const claudeSettings = await refreshClaudeSettingsState();
|
|
4821
|
+
updateProviderReadinessInputs({
|
|
4822
|
+
openai: openAISettings || null,
|
|
4823
|
+
cursor: cursorSettings || null,
|
|
4824
|
+
grok: grokSettings || null,
|
|
4825
|
+
claude: claudeSettings || null,
|
|
4826
|
+
});
|
|
4584
4827
|
// Not awaited: only a Claude Cloud conversation needs it (its model list),
|
|
4585
4828
|
// and the subscription below rebuilds that list when the answer arrives. A
|
|
4586
4829
|
// relay without the provider answers nothing and nothing changes.
|
|
4587
4830
|
subscribeClaudeCloudSettings(refreshClaudeCloudComposerModels);
|
|
4588
|
-
void refreshClaudeCloudSettingsState()
|
|
4831
|
+
void refreshClaudeCloudSettingsState().then((state) => {
|
|
4832
|
+
updateProviderReadinessInputs({ claudeCloud: state || null });
|
|
4833
|
+
});
|
|
4589
4834
|
await refreshModelCatalog(true);
|
|
4590
4835
|
initFullscreenButton();
|
|
4591
4836
|
initInstallButton();
|
|
@@ -4599,15 +4844,25 @@ async function initApp() {
|
|
|
4599
4844
|
initConversationHistoryLazyLoading();
|
|
4600
4845
|
initBubbleActionHandlers();
|
|
4601
4846
|
initMessageScrollPersistence();
|
|
4847
|
+
initTranscriptJumpToBottom({ loadEnd: loadCurrentConversationEnd });
|
|
4602
4848
|
initComposerAttachmentInput();
|
|
4603
4849
|
initMessageSearchView({ openConversation });
|
|
4850
|
+
initSubagentViewer();
|
|
4604
4851
|
initPinnedMessagesView({
|
|
4605
4852
|
openConversation,
|
|
4606
4853
|
focusMessage: focusConversationMessageById,
|
|
4607
4854
|
syncRenderedPinState,
|
|
4608
4855
|
});
|
|
4856
|
+
initHiddenMessagesView({ syncRenderedShareVisibility });
|
|
4609
4857
|
initGitChangesView();
|
|
4610
4858
|
syncChatTitleControls();
|
|
4859
|
+
// A provider turned on or off (a Settings save, a catalog change) redraws
|
|
4860
|
+
// the empty screen and the composer without a reload.
|
|
4861
|
+
subscribeProviderReadiness(() => {
|
|
4862
|
+
renderNoConversationState();
|
|
4863
|
+
syncComposerButtonState();
|
|
4864
|
+
syncComposerPlaceholder();
|
|
4865
|
+
});
|
|
4611
4866
|
connectSocket();
|
|
4612
4867
|
startRelayConnectionWatchdog();
|
|
4613
4868
|
startDeviceVisibilityHeartbeat();
|
|
@@ -4619,6 +4874,9 @@ async function initApp() {
|
|
|
4619
4874
|
// Browsers without Background Sync replay the outbox from the page instead.
|
|
4620
4875
|
void initOutboxFallbackReplay();
|
|
4621
4876
|
await loadConversations();
|
|
4877
|
+
// The sidebar's "watched" badges follow the subscriptions.
|
|
4878
|
+
onRelayEventSubscriptionsChanged(() => renderConvList());
|
|
4879
|
+
void loadRelayEventSubscriptions();
|
|
4622
4880
|
const pushConversationId = consumePushConversationDeepLink();
|
|
4623
4881
|
if (pushConversationId) {
|
|
4624
4882
|
await openConversation(pushConversationId).catch(() => {});
|
|
@@ -4626,7 +4884,19 @@ async function initApp() {
|
|
|
4626
4884
|
await loadRelayQuestions(currentConvId);
|
|
4627
4885
|
await loadRelayBoards();
|
|
4628
4886
|
updateCompactButton();
|
|
4629
|
-
|
|
4887
|
+
// Nothing opened (fresh client, fresh relay, a last conversation that is
|
|
4888
|
+
// gone): the static markup gives way to the real empty screen.
|
|
4889
|
+
renderNoConversationState();
|
|
4890
|
+
syncComposerButtonState();
|
|
4891
|
+
syncComposerPlaceholder();
|
|
4892
|
+
// A phone shows no keyboard for this focus, but the next tap that keeps it
|
|
4893
|
+
// (the transcript's ⌄) would raise one.
|
|
4894
|
+
if (shouldAutofocusComposerOnLoad({
|
|
4895
|
+
coarsePointer: !!window.matchMedia?.('(pointer: coarse)')?.matches,
|
|
4896
|
+
phoneLayout: isMobileComposerViewport(),
|
|
4897
|
+
})) {
|
|
4898
|
+
document.getElementById('msg-input').focus();
|
|
4899
|
+
}
|
|
4630
4900
|
}
|
|
4631
4901
|
|
|
4632
4902
|
async function doAuth() {
|
|
@@ -4647,7 +4917,9 @@ async function doAuth() {
|
|
|
4647
4917
|
async function bootstrap() {
|
|
4648
4918
|
if (ensureTrailingSlashPath()) return;
|
|
4649
4919
|
const sharedMode = isSharedReaderMode();
|
|
4650
|
-
|
|
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) {
|
|
4651
4923
|
navigator.serviceWorker.getRegistrations()
|
|
4652
4924
|
.then((registrations) => Promise.all(registrations.map((registration) => registration.unregister().catch(() => false))))
|
|
4653
4925
|
.catch(() => {});
|
|
@@ -4734,11 +5006,13 @@ window.saveGrokAllowanceSettings = saveGrokAllowanceSettings;
|
|
|
4734
5006
|
window.resetGrokAllowanceAccounting = resetGrokAllowanceAccounting;
|
|
4735
5007
|
window.toggleCursorProvider = toggleCursorProvider;
|
|
4736
5008
|
window.updateShowSuspendHostSetting = updateShowSuspendHostSetting;
|
|
5009
|
+
window.updateScrollOnSendSetting = (enabled) => updateScrollOnSendSetting(enabled);
|
|
4737
5010
|
window.updateWindowsAutostartMode = updateWindowsAutostartMode;
|
|
4738
5011
|
window.previewTurnCeilingSetting = previewTurnCeilingSetting;
|
|
4739
5012
|
window.updateTurnCeilingSetting = updateTurnCeilingSetting;
|
|
4740
5013
|
window.previewBackgroundTaskTimeoutSetting = previewBackgroundTaskTimeoutSetting;
|
|
4741
5014
|
window.updateBackgroundTaskTimeoutSetting = updateBackgroundTaskTimeoutSetting;
|
|
5015
|
+
window.updateSubagentRecordRetentionSetting = updateSubagentRecordRetentionSetting;
|
|
4742
5016
|
window.togglePushOnThisDevice = togglePushOnThisDevice;
|
|
4743
5017
|
window.updatePushPreferencesFromControls = updatePushPreferencesFromControls;
|
|
4744
5018
|
window.openSettingsModal = openSettingsModal;
|
|
@@ -4787,6 +5061,8 @@ window.confirmNewConversationModel = confirmNewConversationModel;
|
|
|
4787
5061
|
window.closeNewConversationModelModal = closeNewConversationModelModal;
|
|
4788
5062
|
window.deleteConv = deleteConv;
|
|
4789
5063
|
window.archiveConv = archiveConv;
|
|
5064
|
+
window.openConversationGroupPicker = openConversationGroupPicker;
|
|
5065
|
+
window.toggleConversationGroupFold = toggleConversationGroupFold;
|
|
4790
5066
|
window.unarchiveConv = unarchiveConv;
|
|
4791
5067
|
window.toggleArchivedConversations = toggleArchivedConversations;
|
|
4792
5068
|
window.refreshNewConversationCloudRepoList = refreshNewConversationCloudRepoList;
|
|
@@ -4815,6 +5091,7 @@ window.toggleRepoBrowserHidden = toggleRepoBrowserHidden;
|
|
|
4815
5091
|
window.toggleRepoBrowserHeavy = toggleRepoBrowserHeavy;
|
|
4816
5092
|
window.focusRepoTree = focusRepoTree;
|
|
4817
5093
|
window.setRepoCurrentPath = setRepoCurrentPath;
|
|
5094
|
+
window.startRepoNewFolder = startRepoNewFolder;
|
|
4818
5095
|
window.confirmRepoBrowserCwdPick = confirmRepoBrowserCwdPick;
|
|
4819
5096
|
window.toggleEmojiPicker = toggleEmojiPicker;
|
|
4820
5097
|
window.submitRelayQuestionChoice = submitRelayQuestionChoice;
|