@oar-sh/oar 0.9.20 → 0.9.22
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 +325 -0
- package/README.md +13 -13
- package/bin/oar.js +2 -2
- package/package.json +1 -1
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +231 -1
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +31 -2
- package/server/claude-cloud-worker/claude-cloud-worker-steering.mjs +34 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +149 -4
- package/server/claude-worker/claude-session-process.mjs +16 -4
- package/server/claude-worker/claude-session-worker-main.mjs +18 -2
- package/server/copilot-worker/copilot-sdk-session-process.mjs +75 -1
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +18 -2
- package/server/cursor-worker/cursor-session-worker.mjs +21 -2
- package/server/cursor-worker/cursor-worker-steering.mjs +20 -0
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/grok-background-tasks.mjs +10 -0
- package/server/grok-worker/grok-session-worker.mjs +22 -2
- package/server/grok-worker/grok-turn-runner.mjs +4 -0
- package/server/grok-worker/grok-worker-steering.mjs +23 -0
- package/server/migrations/0037-relay-asks-queue-id.mjs +66 -0
- package/server/migrations/0038-turn-seq.mjs +92 -0
- package/server/public/app/about-ui.js +1 -1
- package/server/public/app/about-view.mjs +19 -0
- package/server/public/app/action-confirmations.js +1 -1
- package/server/public/app/activity-replay-state.mjs +160 -42
- package/server/public/app/api-client.js +11 -2
- package/server/public/app/ask-user-view.js +24 -9
- package/server/public/app/attachments-view.js +1 -1
- package/server/public/app/back-guard.mjs +31 -5
- package/server/public/app/bootstrap.js +618 -72
- package/server/public/app/chat-actions-menu-state.mjs +141 -4
- package/server/public/app/chat-header-meta.mjs +27 -0
- package/server/public/app/claude-billing-toggle-ui.js +2 -1
- package/server/public/app/claude-cloud-conversation-ui.js +2 -2
- package/server/public/app/composer-settings-label.mjs +90 -0
- package/server/public/app/composer-settings-ui.js +442 -0
- package/server/public/app/context-menu.mjs +36 -5
- package/server/public/app/context-usage-view.mjs +68 -0
- package/server/public/app/conversation-deep-link.mjs +15 -11
- package/server/public/app/conversation-group-drag.mjs +12 -2
- package/server/public/app/conversation-group-order.mjs +9 -2
- package/server/public/app/conversation-group-state.mjs +47 -10
- package/server/public/app/conversation-origin-ui.js +17 -1
- package/server/public/app/conversation-view.js +309 -49
- package/server/public/app/cwd-menu-placement.mjs +18 -6
- package/server/public/app/cwd-picker.js +36 -6
- package/server/public/app/emoji-view.js +1 -1
- package/server/public/app/font-scaling.js +26 -0
- package/server/public/app/header-priority-actions.mjs +238 -0
- package/server/public/app/held-details.mjs +202 -0
- package/server/public/app/hidden-messages-view.mjs +4 -0
- package/server/public/app/inbox-ui.js +88 -81
- package/server/public/app/inbox-view.mjs +35 -19
- package/server/public/app/journal-view.js +328 -58
- package/server/public/app/live-activity-cap.mjs +103 -0
- package/server/public/app/phone-landscape-ui.js +225 -0
- package/server/public/app/phone-landscape.mjs +99 -0
- package/server/public/app/phone-typing-state.mjs +104 -0
- package/server/public/app/pinned-messages-view.mjs +5 -0
- package/server/public/app/push-settings.js +2 -2
- package/server/public/app/pwa-install.js +24 -3
- package/server/public/app/questions-panel-ui.js +634 -0
- package/server/public/app/questions-stub-view.mjs +40 -0
- package/server/public/app/questions-view.mjs +89 -0
- package/server/public/app/relay-board-view.js +12 -2
- package/server/public/app/relay-event-subscriptions-ui.js +71 -22
- package/server/public/app/relay-event-view.mjs +38 -0
- package/server/public/app/relay-restart-ui.js +2 -2
- package/server/public/app/relay-schedule-view.mjs +32 -6
- package/server/public/app/relay-schedules-ui.js +69 -20
- package/server/public/app/relay-thoughts.mjs +15 -1
- package/server/public/app/remote-relay-origin-view.mjs +41 -23
- package/server/public/app/session-history-ui.js +251 -0
- package/server/public/app/session-history.mjs +599 -0
- package/server/public/app/settings-modal.js +67 -6
- package/server/public/app/settings-tabs.js +12 -0
- package/server/public/app/share-state.js +1 -1
- package/server/public/app/shared-conversations-settings.js +2 -2
- package/server/public/app/socket-handlers.js +47 -13
- package/server/public/app/status-view.mjs +18 -2
- package/server/public/app/store.js +87 -16
- package/server/public/app/stream-view-preference.mjs +60 -0
- package/server/public/app/subagent-fold.mjs +6 -3
- package/server/public/app/subagent-timeline-view.mjs +12 -5
- package/server/public/app/subagent-viewer.mjs +35 -8
- package/server/public/app/tab-strip-fade.mjs +101 -0
- package/server/public/app/tap-actions.js +66 -0
- package/server/public/app/thoughts-preference.mjs +79 -0
- package/server/public/app/turn-stream-view.mjs +124 -0
- package/server/public/app/usage-limit-ui.js +63 -2
- package/server/public/app/viewer-history.mjs +13 -2
- package/server/public/index.html +2603 -1767
- package/server/relay-tools.md +1 -0
- package/server/repositories/message-repository.mjs +33 -0
- package/server/repositories/question-repository.mjs +51 -7
- package/server/repositories/relay-turn-rows.mjs +56 -0
- package/server/repositories/session-repository.mjs +19 -0
- package/server/routes/ask-user-routes.mjs +34 -5
- package/server/routes/claude-cloud-routes.mjs +19 -0
- package/server/routes/messages-routes.mjs +92 -15
- package/server/routes/relay-ask-routes.mjs +15 -5
- package/server/routes/sessions-routes.mjs +117 -12
- package/server/server-runtime.mjs +367 -170
- package/server/services/about-service.mjs +22 -53
- package/server/services/claude-billing-swap-service.mjs +7 -20
- package/server/services/claude-cloud-session-service.mjs +47 -5
- package/server/services/cloudflared-tunnel-service.mjs +7 -0
- package/server/services/idle-timers.mjs +184 -0
- package/server/services/queue-wake-hook.mjs +145 -0
- package/server/services/queue-waker.mjs +127 -0
- package/server/services/relay-ask-service.mjs +64 -5
- package/server/services/relay-event-service.mjs +4 -1
- package/server/services/remote-relay-dispatcher.mjs +84 -18
- package/server/services/session-activity-tracker.mjs +1 -1
- package/server/services/session-history-refresh-service.mjs +4 -0
- package/server/services/session-launch-mutex-service.mjs +23 -0
- package/server/services/session-worker-registry-service.mjs +36 -0
- package/server/services/session-worker-supervisor-service.mjs +36 -0
- package/server/services/session-worker-websocket-service.mjs +183 -31
- package/server/services/ssh-tunnel-manager-service.mjs +7 -0
- package/server/services/worker-recycle-service.mjs +772 -0
- package/server/services/worker-replacement.mjs +197 -0
- package/shared/ask-user-bridge.mjs +9 -6
- package/shared/build-id.mjs +128 -0
- package/shared/relay-asks.mjs +10 -0
- package/shared/relay-events.mjs +1 -0
- package/shared/worker-runtime/heartbeat.mjs +31 -0
- package/shared/worker-runtime/simple-steering-state.mjs +38 -0
- package/shared/worker-runtime/worker-keepalive.mjs +108 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +44 -10
- package/shared/worker-runtime/working-directory.mjs +1 -1
|
@@ -63,6 +63,9 @@ import {
|
|
|
63
63
|
} from './transcript-viewport-anchor.mjs';
|
|
64
64
|
import { initTranscriptJumpToBottom, onTranscriptEndLeft } from './transcript-jump-bottom-view.mjs';
|
|
65
65
|
import { syncScrollOnSendToggle, updateScrollOnSendSetting } from './transcript-jump-bottom.mjs';
|
|
66
|
+
import { isThoughtsExpanded, syncThoughtsExpandedToggle, updateThoughtsExpandedSetting } from './thoughts-preference.mjs';
|
|
67
|
+
import { previewLiveActivityCap, syncLiveActivityCapSlider, updateLiveActivityCap } from './live-activity-cap.mjs';
|
|
68
|
+
import { STREAM_VIEW_CHANGE_EVENT, isStreamViewEnabled, syncStreamViewToggle, updateStreamViewSetting } from './stream-view-preference.mjs';
|
|
66
69
|
import { previewWindowRetention, syncWindowRetentionSlider, updateWindowRetention } from './conversation-window-retention.mjs';
|
|
67
70
|
import { conversationPayloadCache, mergeReloadIntoWindow } from './conversation-payload-cache.mjs';
|
|
68
71
|
import {
|
|
@@ -93,7 +96,7 @@ import {
|
|
|
93
96
|
reportSharedViewerPresence,
|
|
94
97
|
setNetworkRequestsEnabled,
|
|
95
98
|
} from './api-client.js';
|
|
96
|
-
import { loadConversations, refreshConversations, openConversation, renderConvList, applyLoadedConversationState, initConversationListLazyLoading, initConversationFilter } from './journal-view.js';
|
|
99
|
+
import { loadConversations, refreshConversations, openConversation, renderConvList, applyLoadedConversationState, heldDetailsStore, initConversationListLazyLoading, initConversationFilter } from './journal-view.js';
|
|
97
100
|
import {
|
|
98
101
|
newConversation,
|
|
99
102
|
confirmNewConversationModel,
|
|
@@ -107,6 +110,7 @@ import {
|
|
|
107
110
|
openOriginConversation,
|
|
108
111
|
openConversationGroupPicker,
|
|
109
112
|
toggleConversationGroupFold,
|
|
113
|
+
toggleAllConversationGroupFolds,
|
|
110
114
|
isConversationWorking,
|
|
111
115
|
} from './journal-view.js';
|
|
112
116
|
import {
|
|
@@ -149,12 +153,13 @@ import {
|
|
|
149
153
|
getHeldBackLiveRowIds,
|
|
150
154
|
captureConversationReadingAnchor,
|
|
151
155
|
canMergeIntoTranscript,
|
|
156
|
+
refreshStreamViewLayout,
|
|
152
157
|
} from './conversation-view.js';
|
|
153
158
|
import { loadKeepingLiveRows } from './transcript-reload-freshness.mjs';
|
|
154
159
|
import { backgroundTasksSnapshotMark } from './background-tasks-view.mjs';
|
|
155
160
|
import { newerPageReachesEnd } from './transcript-island.mjs';
|
|
156
|
-
import { initPinnedMessagesView, setPinsConversation } from './pinned-messages-view.mjs';
|
|
157
|
-
import { initHiddenMessagesView, setHiddenMessagesConversation } from './hidden-messages-view.mjs';
|
|
161
|
+
import { initPinnedMessagesView, openPinnedMessagesModal, setPinsConversation } from './pinned-messages-view.mjs';
|
|
162
|
+
import { initHiddenMessagesView, openHiddenMessagesModal, setHiddenMessagesConversation } from './hidden-messages-view.mjs';
|
|
158
163
|
import { bindChatSelectionGuard, chatSelectionGuard, isChatInteractionHeld } from './selection-guard.mjs';
|
|
159
164
|
import { loadRepoBrowserTree, openRepoBrowser, closeRepoBrowser, setRepoBrowserSessionInfo, resetWorkspaceRepoBrowserForRootChange } from './attachments-view.js';
|
|
160
165
|
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';
|
|
@@ -237,6 +242,22 @@ import {
|
|
|
237
242
|
isInstalledAppMode,
|
|
238
243
|
} from './pwa-install.js';
|
|
239
244
|
import { armBackGuardOnFirstInteraction } from './back-guard.mjs';
|
|
245
|
+
import {
|
|
246
|
+
armSessionHistoryOnFirstInteraction,
|
|
247
|
+
initSessionHistory,
|
|
248
|
+
sessionHistoryConversation,
|
|
249
|
+
sessionHistoryEntryAgain,
|
|
250
|
+
syncSessionDocumentTitle,
|
|
251
|
+
} from './session-history-ui.js';
|
|
252
|
+
// What back closes, beyond the closers imported elsewhere in this file.
|
|
253
|
+
import { closeAnnotateEditor } from './annotate-editor.js';
|
|
254
|
+
import { closeEmojiPicker } from './emoji-view.js';
|
|
255
|
+
import { closeChangeCwdMenu } from './cwd-picker.js';
|
|
256
|
+
import { closeRelayWatchPanel } from './relay-event-subscriptions-ui.js';
|
|
257
|
+
import { closeQuestionsPanel, initQuestionsPanel, openQuestionsPanel, setQuestionsConversation } from './questions-panel-ui.js';
|
|
258
|
+
import { questionsTwinLabel } from './questions-view.mjs';
|
|
259
|
+
import { closeRelaySchedulePanel } from './relay-schedules-ui.js';
|
|
260
|
+
import { isInboxViewActive } from './inbox-ui.js';
|
|
240
261
|
import { initScrollbarHover } from './scrollbar-hover.mjs';
|
|
241
262
|
import {
|
|
242
263
|
autoCompactWindowNotes,
|
|
@@ -246,6 +267,8 @@ import {
|
|
|
246
267
|
renderAutoCompactNotesHtml,
|
|
247
268
|
renderContextUsageHtml,
|
|
248
269
|
renderThinkingControlHtml,
|
|
270
|
+
renderThoughtsPreferenceHtml,
|
|
271
|
+
renderStreamViewPreferenceHtml,
|
|
249
272
|
thinkingEnabledFromKey,
|
|
250
273
|
thinkingEnabledToKey,
|
|
251
274
|
} from './context-usage-view.mjs';
|
|
@@ -255,7 +278,8 @@ import {
|
|
|
255
278
|
formatAutoCompactWindowLabel,
|
|
256
279
|
} from './auto-compact-window-options.mjs';
|
|
257
280
|
import { renderPlanUsageHtml, planUsageSubtitle, normalizeUsageProvider } from './plan-usage-view.mjs';
|
|
258
|
-
import {
|
|
281
|
+
import { revealSelectedTabs, watchTabStripFades } from './tab-strip-fade.mjs';
|
|
282
|
+
import { FONT_SCALE_CHANGE_EVENT, getUiScale, initFontScaling, updateFontScaleFromSelect } from './font-scaling.js';
|
|
259
283
|
import { initClientDiagnostics, recordStatusEvent } from './status-store.mjs';
|
|
260
284
|
import { isStatusViewActive, toggleStatusView } from './status-view.mjs';
|
|
261
285
|
import { installExternalLinkPolicy, openExternalNavigation } from './external-link-policy.mjs';
|
|
@@ -384,6 +408,8 @@ import {
|
|
|
384
408
|
import { applyPendingActionsFromStatus, initHostSuspendUi } from './host-suspend-ui.js';
|
|
385
409
|
import { applyUsageLimitFromStatus, initUsageLimitUi } from './usage-limit-ui.js';
|
|
386
410
|
import { initClaudeBillingToggle, renderClaudeBillingToggle } from './claude-billing-toggle-ui.js';
|
|
411
|
+
import { closeComposerSettings, initComposerSettings, scheduleComposerSettingsSync } from './composer-settings-ui.js';
|
|
412
|
+
import { initPhoneLandscape } from './phone-landscape-ui.js';
|
|
387
413
|
import {
|
|
388
414
|
startClaudeRelogin,
|
|
389
415
|
submitClaudeLoginCodeFromInput,
|
|
@@ -411,16 +437,43 @@ import {
|
|
|
411
437
|
import { isCurrentConversationClaudeCloud, openCurrentCloudSession, syncClaudeCloudConversationUi } from './claude-cloud-conversation-ui.js';
|
|
412
438
|
import {
|
|
413
439
|
CHAT_ACTIONS_MENU_ENTRY_IDS,
|
|
440
|
+
CHAT_ACTIONS_MENU_META_CHIP_IDS,
|
|
414
441
|
CHAT_ACTIONS_MENU_META_IDS,
|
|
442
|
+
chatActionsMenuMetaChipLabels,
|
|
443
|
+
resolveChatActionsMenuPlacement,
|
|
415
444
|
resolveChatActionsMenuState,
|
|
416
445
|
} from './chat-actions-menu-state.mjs';
|
|
446
|
+
import {
|
|
447
|
+
HEADER_ACTIONS_CHANGE_EVENT,
|
|
448
|
+
HEADER_BANNER_OWN_ROW_QUERY,
|
|
449
|
+
HEADER_PRIORITY_ACTIONS,
|
|
450
|
+
HEADER_TITLE_MIN_REM,
|
|
451
|
+
fitHeaderPriorityActions,
|
|
452
|
+
headerPriorityMenuIdsFor,
|
|
453
|
+
headerReservedWidth,
|
|
454
|
+
resolveHeaderActionAvailability,
|
|
455
|
+
resolveQuestionBannerMode,
|
|
456
|
+
} from './header-priority-actions.mjs';
|
|
457
|
+
import { PHONE_LANDSCAPE_ATTRIBUTE, PHONE_LANDSCAPE_CHANGE_EVENT } from './phone-landscape.mjs';
|
|
417
458
|
import { subscribeProviderReadiness, updateProviderReadinessInputs } from './provider-readiness.mjs';
|
|
418
459
|
import { hasActiveShare, refreshShares, subscribeShares } from './share-state.js';
|
|
419
460
|
import { openStopSharingConfirmation } from './shared-conversations-settings.js';
|
|
420
461
|
import { showAbout, syncAboutMenuLabel } from './about-ui.js';
|
|
421
|
-
import { syncConversationOriginHeader } from './conversation-origin-ui.js';
|
|
422
|
-
import {
|
|
423
|
-
|
|
462
|
+
import { conversationOriginMenuEntry, syncConversationOriginHeader } from './conversation-origin-ui.js';
|
|
463
|
+
import {
|
|
464
|
+
loadRelayEventSubscriptions,
|
|
465
|
+
onRelayEventSubscriptionsChanged,
|
|
466
|
+
openRelayWatchPanel,
|
|
467
|
+
relayWatchingChipText,
|
|
468
|
+
syncRelayEventHeader,
|
|
469
|
+
} from './relay-event-subscriptions-ui.js';
|
|
470
|
+
import {
|
|
471
|
+
loadRelaySchedules,
|
|
472
|
+
onRelaySchedulesChanged,
|
|
473
|
+
openRelaySchedulePanel,
|
|
474
|
+
relayScheduleChipText,
|
|
475
|
+
syncRelayScheduleHeader,
|
|
476
|
+
} from './relay-schedules-ui.js';
|
|
424
477
|
import { leaveInboxView, startInbox, toggleInboxView } from './inbox-ui.js';
|
|
425
478
|
import {
|
|
426
479
|
previewAgentSessionsMaxWait,
|
|
@@ -816,8 +869,9 @@ function initPushNotificationClientHooks() {
|
|
|
816
869
|
});
|
|
817
870
|
}
|
|
818
871
|
|
|
819
|
-
// ?push_conv=<id> (a notification tap) and ?conv=<id> (
|
|
820
|
-
//
|
|
872
|
+
// ?push_conv=<id> (a notification tap, consumed) and ?conv=<id> (the open
|
|
873
|
+
// session's address: a reload, a copied link, another relay's provenance
|
|
874
|
+
// badge) open that conversation at load.
|
|
821
875
|
function consumePushConversationDeepLink() {
|
|
822
876
|
return consumeConversationDeepLink();
|
|
823
877
|
}
|
|
@@ -931,27 +985,58 @@ function initNetworkLifecycleHandling() {
|
|
|
931
985
|
}
|
|
932
986
|
|
|
933
987
|
function syncThemeMenuLabel() {
|
|
988
|
+
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
|
934
989
|
const button = document.getElementById('chat-menu-theme-toggle');
|
|
935
|
-
if (
|
|
990
|
+
if (button) button.textContent = isLight ? '🌙 Night mode' : '☀️ Day mode';
|
|
991
|
+
// The header twin: the glyph alone, the words in its tooltip.
|
|
992
|
+
const inline = document.getElementById('theme-toggle-btn');
|
|
993
|
+
if (inline) {
|
|
994
|
+
const label = isLight ? 'Night mode' : 'Day mode';
|
|
995
|
+
inline.textContent = isLight ? '🌙' : '☀️';
|
|
996
|
+
inline.title = label;
|
|
997
|
+
inline.setAttribute('aria-label', label);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function toggleThemeFromHeader() {
|
|
936
1002
|
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
|
937
|
-
|
|
1003
|
+
updateTheme(isLight ? 'dark' : 'light');
|
|
1004
|
+
syncThemeMenuLabel();
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// One priority+ action (header-priority-actions.mjs): its header button and
|
|
1008
|
+
// its ︙ twin run the same handler. A header button that already carries its
|
|
1009
|
+
// own binding (an inline onclick, a view module's listener) passes
|
|
1010
|
+
// `inline: false`, and only the twin is bound here.
|
|
1011
|
+
function bindHeaderPriorityAction(key, run, { inline = true } = {}) {
|
|
1012
|
+
const action = HEADER_PRIORITY_ACTIONS.find((entry) => entry.key === key);
|
|
1013
|
+
if (!action) return;
|
|
1014
|
+
const inlineBtn = inline ? document.getElementById(action.inlineId) : null;
|
|
1015
|
+
if (inlineBtn && inlineBtn.dataset.headerActionBound !== '1') {
|
|
1016
|
+
inlineBtn.dataset.headerActionBound = '1';
|
|
1017
|
+
inlineBtn.addEventListener('click', (event) => {
|
|
1018
|
+
event.preventDefault();
|
|
1019
|
+
event.stopPropagation();
|
|
1020
|
+
run(event);
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
const twin = document.getElementById(action.menuId);
|
|
1024
|
+
if (twin && twin.dataset.headerActionBound !== '1') {
|
|
1025
|
+
twin.dataset.headerActionBound = '1';
|
|
1026
|
+
bindMenuAction(twin, (event) => {
|
|
1027
|
+
event.preventDefault();
|
|
1028
|
+
event.stopPropagation();
|
|
1029
|
+
lockChatActionsMenuShield(350);
|
|
1030
|
+
closeChatActionsMenu();
|
|
1031
|
+
run(event);
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
938
1034
|
}
|
|
939
1035
|
|
|
940
1036
|
function initThemeMenuToggle() {
|
|
941
|
-
const themeBtn = document.getElementById('chat-menu-theme-toggle');
|
|
942
|
-
if (!themeBtn) return;
|
|
943
|
-
themeBtn.hidden = false;
|
|
944
1037
|
syncThemeMenuLabel();
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
bindMenuAction(themeBtn, (event) => {
|
|
948
|
-
event.preventDefault();
|
|
949
|
-
event.stopPropagation();
|
|
950
|
-
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
|
951
|
-
updateTheme(isLight ? 'dark' : 'light');
|
|
952
|
-
syncThemeMenuLabel();
|
|
953
|
-
closeChatActionsMenu();
|
|
954
|
-
});
|
|
1038
|
+
// Day/Night is the shared view's one action too, so it is bound in both.
|
|
1039
|
+
bindHeaderPriorityAction('theme', () => toggleThemeFromHeader());
|
|
955
1040
|
}
|
|
956
1041
|
|
|
957
1042
|
function disableSharedControl(element, {
|
|
@@ -1025,18 +1110,16 @@ function applySharedReaderUi() {
|
|
|
1025
1110
|
if (effortSelect) effortSelect.hidden = true;
|
|
1026
1111
|
if (modeSelect) modeSelect.hidden = true;
|
|
1027
1112
|
// One source for the menu: the resolver's shared branch (Day/Night only,
|
|
1028
|
-
//
|
|
1029
|
-
// does not know
|
|
1113
|
+
// in the header or in ︙, whichever fits). The loop stays as a guard for
|
|
1114
|
+
// entries the resolver does not know; the sync after it decides Day/Night
|
|
1115
|
+
// and the trigger.
|
|
1030
1116
|
applyChatActionsMenuState(resolveChatActionsMenuState({ sharedMode: true }));
|
|
1031
1117
|
if (chatMenu && themeBtn) {
|
|
1032
1118
|
for (const child of Array.from(chatMenu.children)) {
|
|
1033
1119
|
if (child !== themeBtn) hideSharedMenuEntry(child);
|
|
1034
1120
|
}
|
|
1035
|
-
themeBtn.hidden = false;
|
|
1036
|
-
themeBtn.removeAttribute('aria-hidden');
|
|
1037
|
-
themeBtn.disabled = false;
|
|
1038
|
-
themeBtn.tabIndex = 0;
|
|
1039
1121
|
syncThemeMenuLabel();
|
|
1122
|
+
syncChatActionsMenuState();
|
|
1040
1123
|
} else if (chatMenuBtn) {
|
|
1041
1124
|
chatMenuBtn.hidden = true;
|
|
1042
1125
|
}
|
|
@@ -1460,6 +1543,8 @@ function syncSessionLockNote({ pinnedModel = '' } = {}) {
|
|
|
1460
1543
|
}
|
|
1461
1544
|
// The billing toggle sits under the note and follows the same chat.
|
|
1462
1545
|
renderClaudeBillingToggle();
|
|
1546
|
+
// The composer's label shows the lock and the model.
|
|
1547
|
+
scheduleComposerSettingsSync();
|
|
1463
1548
|
}
|
|
1464
1549
|
|
|
1465
1550
|
function applyModelMetadataHardFail(message = '') {
|
|
@@ -2223,6 +2308,8 @@ function applyConversationPreferences({
|
|
|
2223
2308
|
suppressConversationPreferenceSync = false;
|
|
2224
2309
|
}
|
|
2225
2310
|
syncModeSelectPlanOnlyHint();
|
|
2311
|
+
// Values set here fire no change event: the composer's label reads them.
|
|
2312
|
+
scheduleComposerSettingsSync();
|
|
2226
2313
|
// Nothing is written to the shared "last used" storage here. Applying a
|
|
2227
2314
|
// conversation's stored preferences is not a choice: it runs on every open,
|
|
2228
2315
|
// on the ~1s live poll, on a catalog refresh and on another client's edit,
|
|
@@ -2887,7 +2974,7 @@ function renderModelVariantCatalogBody() {
|
|
|
2887
2974
|
}
|
|
2888
2975
|
const visibleProviderOrder = providerOrder.filter((providerKey) => providerBelongsToTab(providerKey, modelVariantCatalogProviderTab));
|
|
2889
2976
|
const providerTabButtons = `
|
|
2890
|
-
<div class="summary-tab-strip" style="display:flex;flex-wrap:wrap;gap:
|
|
2977
|
+
<div class="summary-tab-strip" style="display:flex;flex-wrap:wrap;gap:calc(4px * var(--ui-scale)) calc(5px * var(--ui-scale));align-items:center;">
|
|
2891
2978
|
<button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'copilot' ? ' active' : ''}" data-model-provider-tab="copilot">Copilot</button>
|
|
2892
2979
|
<button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'openai' ? ' active' : ''}" data-model-provider-tab="openai" ${hasOpenAITab ? '' : 'disabled'}>OpenAI</button>
|
|
2893
2980
|
<button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'anthropic' ? ' active' : ''}" data-model-provider-tab="anthropic" ${hasAnthropicTab ? '' : 'disabled'}>Claude SDK</button>
|
|
@@ -3006,7 +3093,7 @@ function renderModelVariantCatalogBody() {
|
|
|
3006
3093
|
`;
|
|
3007
3094
|
}).join('');
|
|
3008
3095
|
const bodyHtml = `
|
|
3009
|
-
<div style="display:flex;flex-direction:column;gap:
|
|
3096
|
+
<div style="display:flex;flex-direction:column;gap:calc(5px * var(--ui-scale))">
|
|
3010
3097
|
${providerTabButtons}
|
|
3011
3098
|
<div style="font-size:0.8rem;color:var(--muted)">
|
|
3012
3099
|
Saved globally for this relay. Refreshed: <strong>${escHtml(refreshedLabel)}</strong>
|
|
@@ -3018,12 +3105,12 @@ function renderModelVariantCatalogBody() {
|
|
|
3018
3105
|
Each model shows all supported reasoning efforts above its selectable variants.
|
|
3019
3106
|
</div>
|
|
3020
3107
|
${warnings.map((line) => `<div style="font-size:0.78rem;color:var(--warn,#f7c873)">${line}</div>`).join('')}
|
|
3021
|
-
<div style="display:grid;gap:
|
|
3108
|
+
<div style="display:grid;gap:calc(5px * var(--ui-scale))">${groupsHtml || '<div style="color:var(--muted)">No models discovered yet. Click Refresh.</div>'}</div>
|
|
3022
3109
|
</div>
|
|
3023
3110
|
`;
|
|
3024
3111
|
renderSummaryModalContent({
|
|
3025
3112
|
title: '🤗 Select Models',
|
|
3026
|
-
subtitle:
|
|
3113
|
+
subtitle: "Choose model variants shown under Model in the composer's ⚙ settings",
|
|
3027
3114
|
bodyHtml,
|
|
3028
3115
|
refresh: async () => {
|
|
3029
3116
|
const refreshed = await refreshModelVariantCatalog();
|
|
@@ -3277,6 +3364,7 @@ function selectUsageTab(root, provider) {
|
|
|
3277
3364
|
for (const panel of root.querySelectorAll('[data-usage-panel]')) {
|
|
3278
3365
|
panel.hidden = panel.dataset.usagePanel !== target;
|
|
3279
3366
|
}
|
|
3367
|
+
revealSelectedTabs(root);
|
|
3280
3368
|
usageTabProviderOverride = target;
|
|
3281
3369
|
if (!usageCardIsFresh(target)) {
|
|
3282
3370
|
refreshUsageProvider(target).catch(() => {});
|
|
@@ -3336,6 +3424,10 @@ function renderUsageReport(report, account, { updating = '' } = {}) {
|
|
|
3336
3424
|
kind: 'usage',
|
|
3337
3425
|
});
|
|
3338
3426
|
if (updating) markUsageCardUpdating(updating, true);
|
|
3427
|
+
// A new strip each render: its selected tab in view, its fade marked.
|
|
3428
|
+
watchTabStripFades();
|
|
3429
|
+
const body = document.getElementById('summary-modal-body');
|
|
3430
|
+
if (body) revealSelectedTabs(body);
|
|
3339
3431
|
return true;
|
|
3340
3432
|
}
|
|
3341
3433
|
|
|
@@ -3618,6 +3710,18 @@ function initThinkingControl() {
|
|
|
3618
3710
|
if (!body || body.dataset.thinkingBound === '1') return;
|
|
3619
3711
|
body.dataset.thinkingBound = '1';
|
|
3620
3712
|
body.addEventListener('click', (event) => {
|
|
3713
|
+
// 🧵 Stream view and 💭 Thoughts: settings of this browser, nothing goes
|
|
3714
|
+
// to the relay. (The stream view's buttons share the thoughts' look.)
|
|
3715
|
+
const streamViewBtn = event.target?.closest?.('.ctx-stream-view-btn');
|
|
3716
|
+
if (streamViewBtn && body.contains(streamViewBtn)) {
|
|
3717
|
+
updateStreamViewSetting(streamViewBtn.dataset.streamView === 'on');
|
|
3718
|
+
return;
|
|
3719
|
+
}
|
|
3720
|
+
const thoughtsBtn = event.target?.closest?.('.ctx-thoughts-btn[data-thoughts-expanded]');
|
|
3721
|
+
if (thoughtsBtn && body.contains(thoughtsBtn)) {
|
|
3722
|
+
updateThoughtsExpandedSetting(thoughtsBtn.dataset.thoughtsExpanded !== 'off');
|
|
3723
|
+
return;
|
|
3724
|
+
}
|
|
3621
3725
|
const btn = event.target?.closest?.('.ctx-thinking-btn');
|
|
3622
3726
|
if (!btn || !body.contains(btn)) return;
|
|
3623
3727
|
void applyThinkingSelection(btn.dataset.thinkingAxis, btn.dataset.thinkingValue);
|
|
@@ -3777,6 +3881,10 @@ async function loadContextSummaryAndRender(convId) {
|
|
|
3777
3881
|
thinkingDisplay: payload.thinkingDisplay ?? null,
|
|
3778
3882
|
})
|
|
3779
3883
|
: '';
|
|
3884
|
+
// Every provider streams thoughts somewhere; the row mirrors the Settings
|
|
3885
|
+
// toggle and is this browser's own.
|
|
3886
|
+
const thoughtsHtml = renderThoughtsPreferenceHtml({ expanded: isThoughtsExpanded() })
|
|
3887
|
+
+ renderStreamViewPreferenceHtml({ on: isStreamViewEnabled() });
|
|
3780
3888
|
// What this folder's commits say, with the folder override: Claude (on
|
|
3781
3889
|
// either billing), and a Claude Cloud chat that was started from a folder
|
|
3782
3890
|
// (no path, no row).
|
|
@@ -3801,10 +3909,10 @@ async function loadContextSummaryAndRender(convId) {
|
|
|
3801
3909
|
// The structured breakdown is the answer; the runtime's own text dump stays
|
|
3802
3910
|
// available underneath for the details it carries that categories don't.
|
|
3803
3911
|
const bodyHtml = usageHtml
|
|
3804
|
-
? `${usageHtml}${autoCompactHtml}${thinkingHtml}${attributionHtml}${agentSessionsHtml}${detailText
|
|
3912
|
+
? `${usageHtml}${autoCompactHtml}${thinkingHtml}${thoughtsHtml}${attributionHtml}${agentSessionsHtml}${detailText
|
|
3805
3913
|
? `<details class="ctx-usage-raw"><summary>Raw details</summary><pre>${escHtml(detailText)}</pre></details>`
|
|
3806
3914
|
: ''}`
|
|
3807
|
-
: `${autoCompactHtml}${thinkingHtml}${attributionHtml}${agentSessionsHtml}<pre>${escHtml(detailText || 'No context data available.')}</pre>`;
|
|
3915
|
+
: `${autoCompactHtml}${thinkingHtml}${thoughtsHtml}${attributionHtml}${agentSessionsHtml}<pre>${escHtml(detailText || 'No context data available.')}</pre>`;
|
|
3808
3916
|
|
|
3809
3917
|
renderSummaryModalContent({
|
|
3810
3918
|
title: 'Context usage',
|
|
@@ -3820,11 +3928,15 @@ async function showUsage() {
|
|
|
3820
3928
|
// A fresh open follows the current session's provider again; only a click
|
|
3821
3929
|
// inside the modal pins a tab (for the Refresh button's sake).
|
|
3822
3930
|
usageTabProviderOverride = null;
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3931
|
+
// The header button and its ︙ twin both show the wait.
|
|
3932
|
+
const usageButtons = ['chat-menu-usage', 'usage-btn'].map((id) => document.getElementById(id)).filter(Boolean);
|
|
3933
|
+
const setUsageBusy = (busy) => {
|
|
3934
|
+
for (const button of usageButtons) {
|
|
3935
|
+
button.textContent = busy ? '⏳' : (button.id === 'chat-menu-usage' ? '📊 Check Usage' : '📊');
|
|
3936
|
+
button.disabled = busy;
|
|
3937
|
+
}
|
|
3938
|
+
};
|
|
3939
|
+
setUsageBusy(true);
|
|
3828
3940
|
// What was shown last time is shown again at once; only the open tab's
|
|
3829
3941
|
// provider is read live, and its card is swapped in when the answer comes.
|
|
3830
3942
|
const kept = lastUsageReport || readStoredUsageReport();
|
|
@@ -3838,10 +3950,7 @@ async function showUsage() {
|
|
|
3838
3950
|
});
|
|
3839
3951
|
try {
|
|
3840
3952
|
if (kept && renderUsageReport(kept, null, { updating: activeProvider })) {
|
|
3841
|
-
|
|
3842
|
-
btn.textContent = btn.id === 'chat-menu-usage' ? '📊 Check Usage' : '📊';
|
|
3843
|
-
btn.disabled = false;
|
|
3844
|
-
}
|
|
3953
|
+
setUsageBusy(false);
|
|
3845
3954
|
await refreshUsageProvider(activeProvider);
|
|
3846
3955
|
return;
|
|
3847
3956
|
}
|
|
@@ -3856,10 +3965,7 @@ async function showUsage() {
|
|
|
3856
3965
|
kind: 'usage',
|
|
3857
3966
|
});
|
|
3858
3967
|
} finally {
|
|
3859
|
-
|
|
3860
|
-
btn.textContent = btn.id === 'chat-menu-usage' ? '📊 Check Usage' : '📊';
|
|
3861
|
-
btn.disabled = false;
|
|
3862
|
-
}
|
|
3968
|
+
setUsageBusy(false);
|
|
3863
3969
|
}
|
|
3864
3970
|
}
|
|
3865
3971
|
|
|
@@ -3913,7 +4019,7 @@ function renderSessionInstructionDocs(docs) {
|
|
|
3913
4019
|
return '<div class="summary-loading">No session instruction files were found.</div>';
|
|
3914
4020
|
}
|
|
3915
4021
|
|
|
3916
|
-
return `<div style="display:flex;flex-direction:column;gap:
|
|
4022
|
+
return `<div style="display:flex;flex-direction:column;gap:calc(6px * var(--ui-scale))">${
|
|
3917
4023
|
items.map((doc) => {
|
|
3918
4024
|
const title = String(doc?.title || doc?.sessionId || 'Session instructions').trim();
|
|
3919
4025
|
const name = String(doc?.name || '').trim();
|
|
@@ -3923,13 +4029,13 @@ function renderSessionInstructionDocs(docs) {
|
|
|
3923
4029
|
const updatedAt = String(doc?.updatedAt || '').trim();
|
|
3924
4030
|
const content = escHtml(String(doc?.content || '').trim());
|
|
3925
4031
|
return `
|
|
3926
|
-
<details open style="border:1px solid var(--border);border-radius:10px;background:var(--bg3);padding:
|
|
4032
|
+
<details open style="border:1px solid var(--border);border-radius:calc(10px * var(--ui-scale));background:var(--bg3);padding:calc(5px * var(--ui-scale)) calc(7px * var(--ui-scale))">
|
|
3927
4033
|
<summary style="cursor:pointer;font-weight:600;outline:none">${escHtml(title)}</summary>
|
|
3928
|
-
<div style="margin-top:
|
|
4034
|
+
<div style="margin-top:calc(3px * var(--ui-scale));font-size:0.78rem;color:var(--muted)">
|
|
3929
4035
|
${escHtml(summary || sessionId)}
|
|
3930
4036
|
${updatedAt ? ` · ${escHtml(updatedAt)}` : ''}
|
|
3931
4037
|
</div>
|
|
3932
|
-
<pre style="margin-top:
|
|
4038
|
+
<pre style="margin-top:calc(5px * var(--ui-scale));white-space:pre-wrap;word-break:break-word">${content}</pre>
|
|
3933
4039
|
</details>
|
|
3934
4040
|
`;
|
|
3935
4041
|
}).join('')
|
|
@@ -4048,12 +4154,17 @@ async function pollAuthenticatedCurrentConversationLive() {
|
|
|
4048
4154
|
const requestLimit = Math.max(20, getConversationLoadedMessageCount() || 0, savedLoadedCount || 0);
|
|
4049
4155
|
const tasksMark = backgroundTasksSnapshotMark(currentId);
|
|
4050
4156
|
const jumpEpoch = getTranscriptJumpEpoch();
|
|
4157
|
+
// The finished replies this page already holds go out without their
|
|
4158
|
+
// tool lines and thoughts (held-details.mjs); the answer gets them back
|
|
4159
|
+
// from the store, or is dropped if it names one the store lacks.
|
|
4160
|
+
const heldDetails = heldDetailsStore.param(currentId);
|
|
4051
4161
|
const { response, stale } = await loadKeepingLiveRows(
|
|
4052
|
-
() => loadConversation(currentId, { limit: requestLimit }),
|
|
4162
|
+
() => loadConversation(currentId, { limit: requestLimit, heldDetails }),
|
|
4053
4163
|
getTranscriptLiveRowEpoch,
|
|
4054
4164
|
);
|
|
4055
4165
|
if (!response) return;
|
|
4056
4166
|
if (String(currentConvId || '').trim() !== currentId) return;
|
|
4167
|
+
if (!heldDetailsStore.restore(currentId, response)) return;
|
|
4057
4168
|
// The user jumped into the history while this request was on its way: the
|
|
4058
4169
|
// answer is the end of the conversation as it was asked for before that,
|
|
4059
4170
|
// and would overwrite the window they jumped to. The next round asks again.
|
|
@@ -4073,6 +4184,7 @@ async function pollAuthenticatedCurrentConversationLive() {
|
|
|
4073
4184
|
followLiveUpdates: preserveBottom,
|
|
4074
4185
|
mergeFrom: merge?.mergeFrom || null,
|
|
4075
4186
|
backgroundTasksAsOf: tasksMark,
|
|
4187
|
+
fromPoll: true,
|
|
4076
4188
|
});
|
|
4077
4189
|
// A merge does not scroll as the full render does: a reader at the end
|
|
4078
4190
|
// is taken to the new end here.
|
|
@@ -4501,12 +4613,337 @@ function applyChatActionsMenuState(state) {
|
|
|
4501
4613
|
}
|
|
4502
4614
|
}
|
|
4503
4615
|
|
|
4504
|
-
//
|
|
4616
|
+
// ── Priority+ header actions ──
|
|
4617
|
+
// Pins, hidden, search, context, Check Usage, Day/Night and fullscreen sit in
|
|
4618
|
+
// the header while it has room and fall back into ︙ in reverse priority
|
|
4619
|
+
// order (header-priority-actions.mjs decides; this measures and applies).
|
|
4620
|
+
// The twin ids the last fit put into ︙; null before the header was measured,
|
|
4621
|
+
// which keeps the menu's old reach (Day/Night, Check Usage).
|
|
4622
|
+
let headerPriorityMenuIds = null;
|
|
4623
|
+
// The inputs the last full fit measured with (headerFitSignature); a pass
|
|
4624
|
+
// whose inputs are the same keeps the placement and measures nothing.
|
|
4625
|
+
let headerPriorityFitSignature = '';
|
|
4626
|
+
|
|
4627
|
+
function headerPriorityActionAvailability() {
|
|
4628
|
+
const pins = document.getElementById('pinned-messages-btn');
|
|
4629
|
+
const hiddenBtn = document.getElementById('hidden-messages-btn');
|
|
4630
|
+
const fullscreen = document.getElementById('fullscreen-btn');
|
|
4631
|
+
return resolveHeaderActionAvailability({
|
|
4632
|
+
sharedMode: isSharedReaderMode(),
|
|
4633
|
+
questionCount: Number(document.getElementById('questions-btn')?.dataset.count || 0),
|
|
4634
|
+
pinCount: Number(pins?.dataset.count || 0),
|
|
4635
|
+
hiddenCount: Number(hiddenBtn?.dataset.count || 0),
|
|
4636
|
+
fullscreenAvailable: fullscreen?.dataset.headerAvailable === '1',
|
|
4637
|
+
});
|
|
4638
|
+
}
|
|
4639
|
+
|
|
4640
|
+
function cssPixels(value) {
|
|
4641
|
+
const number = parseFloat(value);
|
|
4642
|
+
return Number.isFinite(number) ? number : 0;
|
|
4643
|
+
}
|
|
4644
|
+
|
|
4645
|
+
function isHeaderBannerOnOwnRow() {
|
|
4646
|
+
return typeof window.matchMedia === 'function' && window.matchMedia(HEADER_BANNER_OWN_ROW_QUERY).matches;
|
|
4647
|
+
}
|
|
4648
|
+
|
|
4649
|
+
// The question banner's two widths, measured with its compact chip shown
|
|
4650
|
+
// and then its full text: the chip ("❓ N", what the fit reserves) and the
|
|
4651
|
+
// least the full text needs ("❓ N open questions" plus padding and border;
|
|
4652
|
+
// the "latest in …" tail may end in an ellipsis). Zero when it does not show.
|
|
4653
|
+
// Leaves the banner in its full form; the caller sets the mode.
|
|
4654
|
+
function measureQuestionBanner(banner) {
|
|
4655
|
+
if (!banner) return { visible: false, compact: 0, fullMin: 0 };
|
|
4656
|
+
banner.removeAttribute('data-compact');
|
|
4657
|
+
if (!banner.offsetWidth) return { visible: false, compact: 0, fullMin: 0 };
|
|
4658
|
+
const count = banner.querySelector('.pending-question-count');
|
|
4659
|
+
const style = getComputedStyle(banner);
|
|
4660
|
+
const frame = cssPixels(style.paddingLeft) + cssPixels(style.paddingRight)
|
|
4661
|
+
+ cssPixels(style.borderLeftWidth) + cssPixels(style.borderRightWidth);
|
|
4662
|
+
// Rounded up: a fraction short would cut the last letter.
|
|
4663
|
+
const fullMin = Math.ceil((count ? count.getBoundingClientRect().width : banner.scrollWidth) + frame);
|
|
4664
|
+
banner.setAttribute('data-compact', '');
|
|
4665
|
+
const compact = Math.ceil(banner.getBoundingClientRect().width);
|
|
4666
|
+
banner.removeAttribute('data-compact');
|
|
4667
|
+
return { visible: true, compact, fullMin };
|
|
4668
|
+
}
|
|
4669
|
+
|
|
4670
|
+
// The width of everything on the header's first row that never overflows
|
|
4671
|
+
// (headerReservedWidth): its padding, ☰, the title's minimum, ︙, the
|
|
4672
|
+
// question banner's compact chip while it shares that row, and the flex gaps
|
|
4673
|
+
// between them. Read on every full pass: all of it moves with --font-scale.
|
|
4674
|
+
function headerFixedWidth(header, bannerCompactWidth) {
|
|
4675
|
+
const style = getComputedStyle(header);
|
|
4676
|
+
const rootFont = cssPixels(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
4677
|
+
return headerReservedWidth({
|
|
4678
|
+
padding: cssPixels(style.paddingLeft) + cssPixels(style.paddingRight),
|
|
4679
|
+
titleMin: HEADER_TITLE_MIN_REM * rootFont,
|
|
4680
|
+
toggle: document.getElementById('sidebar-toggle')?.offsetWidth || 0,
|
|
4681
|
+
banner: bannerCompactWidth,
|
|
4682
|
+
menu: document.getElementById('chat-actions-menu-wrap')?.offsetWidth || 0,
|
|
4683
|
+
gap: cssPixels(style.columnGap || style.gap),
|
|
4684
|
+
// On a phone the banner has a row of its own (the ≤680px header CSS).
|
|
4685
|
+
bannerOwnRow: isHeaderBannerOnOwnRow(),
|
|
4686
|
+
});
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4689
|
+
// Everything the fit depends on, in one string: the header's width, the
|
|
4690
|
+
// title, the banner, the counts and availability, the theme (the glyphs'
|
|
4691
|
+
// widths), the scale and the layout. Cheap to read: no element is shown or
|
|
4692
|
+
// hidden for it.
|
|
4693
|
+
function headerFitSignature(header, availability) {
|
|
4694
|
+
const root = document.documentElement;
|
|
4695
|
+
const rootStyle = getComputedStyle(root);
|
|
4696
|
+
const title = document.getElementById('chat-title');
|
|
4697
|
+
const banner = document.getElementById('pending-question-banner');
|
|
4698
|
+
return JSON.stringify([
|
|
4699
|
+
header.clientWidth,
|
|
4700
|
+
rootStyle.fontSize,
|
|
4701
|
+
rootStyle.getPropertyValue('--ui-scale'),
|
|
4702
|
+
root.getAttribute('data-theme'),
|
|
4703
|
+
root.hasAttribute(PHONE_LANDSCAPE_ATTRIBUTE),
|
|
4704
|
+
isHeaderBannerOnOwnRow(),
|
|
4705
|
+
document.body.classList.contains('chat-title-editing'),
|
|
4706
|
+
title?.hidden, title?.textContent,
|
|
4707
|
+
banner?.className, banner?.hidden, banner?.textContent,
|
|
4708
|
+
document.getElementById('sidebar-toggle')?.hidden,
|
|
4709
|
+
document.getElementById('chat-actions-menu-btn')?.hidden,
|
|
4710
|
+
document.getElementById('questions-btn')?.dataset.count,
|
|
4711
|
+
document.getElementById('questions-btn')?.dataset.urgent,
|
|
4712
|
+
document.getElementById('pinned-messages-btn')?.dataset.count,
|
|
4713
|
+
document.getElementById('hidden-messages-btn')?.dataset.count,
|
|
4714
|
+
document.getElementById('fullscreen-btn')?.textContent,
|
|
4715
|
+
document.getElementById('theme-toggle-btn')?.textContent,
|
|
4716
|
+
availability,
|
|
4717
|
+
]);
|
|
4718
|
+
}
|
|
4719
|
+
|
|
4720
|
+
// The last placement, minus whatever is no longer available. Measures nothing.
|
|
4721
|
+
function keepHeaderPriorityPlacement(availability) {
|
|
4722
|
+
for (const action of HEADER_PRIORITY_ACTIONS) {
|
|
4723
|
+
const inline = document.getElementById(action.inlineId);
|
|
4724
|
+
if (inline && !availability[action.key]) inline.hidden = true;
|
|
4725
|
+
}
|
|
4726
|
+
if (Array.isArray(headerPriorityMenuIds)) {
|
|
4727
|
+
const availableMenuIds = new Set(headerPriorityMenuIdsFor(Object.keys(availability).filter((key) => availability[key])));
|
|
4728
|
+
headerPriorityMenuIds = headerPriorityMenuIds.filter((id) => availableMenuIds.has(id));
|
|
4729
|
+
}
|
|
4730
|
+
return headerPriorityMenuIds;
|
|
4731
|
+
}
|
|
4732
|
+
|
|
4733
|
+
// Shows inline what fits and hides the rest; returns the twin ids for ︙.
|
|
4734
|
+
// A header that cannot be measured (hidden, not laid out) keeps the last
|
|
4735
|
+
// placement, minus whatever is no longer available. Unless `force`, a pass
|
|
4736
|
+
// whose inputs are those of the last one keeps its placement too: the hide
|
|
4737
|
+
// and measure below runs only when something it reads has changed.
|
|
4738
|
+
function syncHeaderPriorityFit({ force = false } = {}) {
|
|
4739
|
+
const availability = headerPriorityActionAvailability();
|
|
4740
|
+
const header = document.getElementById('chat-header');
|
|
4741
|
+
const actions = document.getElementById('chat-header-actions');
|
|
4742
|
+
const measurable = !!(header && actions && header.clientWidth && typeof getComputedStyle === 'function');
|
|
4743
|
+
if (!measurable) return keepHeaderPriorityPlacement(availability);
|
|
4744
|
+
const signature = headerFitSignature(header, availability);
|
|
4745
|
+
if (!force && signature === headerPriorityFitSignature && Array.isArray(headerPriorityMenuIds)) {
|
|
4746
|
+
return keepHeaderPriorityPlacement(availability);
|
|
4747
|
+
}
|
|
4748
|
+
headerPriorityFitSignature = signature;
|
|
4749
|
+
// Un-hide every available button before measuring: a hidden one is 0 wide.
|
|
4750
|
+
// All of this runs in one task, so nothing is painted in between.
|
|
4751
|
+
const widths = {};
|
|
4752
|
+
for (const action of HEADER_PRIORITY_ACTIONS) {
|
|
4753
|
+
const inline = document.getElementById(action.inlineId);
|
|
4754
|
+
if (!inline) continue;
|
|
4755
|
+
inline.hidden = !availability[action.key];
|
|
4756
|
+
if (availability[action.key]) widths[action.key] = inline.offsetWidth;
|
|
4757
|
+
}
|
|
4758
|
+
const gap = cssPixels(getComputedStyle(actions).columnGap || getComputedStyle(actions).gap);
|
|
4759
|
+
// The question banner sits between the title and the actions. The fit
|
|
4760
|
+
// reserves its compact chip only, so an action goes into ︙ before the
|
|
4761
|
+
// banner could push it; then the banner takes what the row has left.
|
|
4762
|
+
const banner = document.getElementById('pending-question-banner');
|
|
4763
|
+
const phoneOwnRow = isHeaderBannerOnOwnRow();
|
|
4764
|
+
const phoneLandscape = document.documentElement.hasAttribute(PHONE_LANDSCAPE_ATTRIBUTE);
|
|
4765
|
+
const bannerSize = measureQuestionBanner(banner);
|
|
4766
|
+
const reserved = headerFixedWidth(header, phoneOwnRow ? 0 : bannerSize.compact);
|
|
4767
|
+
// Where ☰, the title's minimum, the chip and ︙ alone overflow the first
|
|
4768
|
+
// row (a narrow #main at a large zoom), the banner takes a row of its own,
|
|
4769
|
+
// as on a phone, and the fit no longer reserves it.
|
|
4770
|
+
const ownRow = phoneOwnRow || resolveQuestionBannerMode({
|
|
4771
|
+
visible: bannerSize.visible,
|
|
4772
|
+
phoneLandscape,
|
|
4773
|
+
room: header.clientWidth - reserved + bannerSize.compact,
|
|
4774
|
+
compact: bannerSize.compact,
|
|
4775
|
+
}) === 'row';
|
|
4776
|
+
const available = header.clientWidth - (ownRow ? headerFixedWidth(header, 0) : reserved);
|
|
4777
|
+
const fit = fitHeaderPriorityActions({
|
|
4778
|
+
widths,
|
|
4779
|
+
gap,
|
|
4780
|
+
available,
|
|
4781
|
+
availability,
|
|
4782
|
+
});
|
|
4783
|
+
const inlineKeys = new Set(fit.inline);
|
|
4784
|
+
for (const action of HEADER_PRIORITY_ACTIONS) {
|
|
4785
|
+
const inline = document.getElementById(action.inlineId);
|
|
4786
|
+
if (inline) inline.hidden = !inlineKeys.has(action.key);
|
|
4787
|
+
}
|
|
4788
|
+
if (banner) {
|
|
4789
|
+
const headerStyle = getComputedStyle(header);
|
|
4790
|
+
const mode = resolveQuestionBannerMode({
|
|
4791
|
+
visible: bannerSize.visible,
|
|
4792
|
+
ownRow,
|
|
4793
|
+
phoneLandscape,
|
|
4794
|
+
room: available - fit.used + bannerSize.compact,
|
|
4795
|
+
compact: bannerSize.compact,
|
|
4796
|
+
contentWidth: header.clientWidth - cssPixels(headerStyle.paddingLeft) - cssPixels(headerStyle.paddingRight),
|
|
4797
|
+
fullMin: bannerSize.fullMin,
|
|
4798
|
+
});
|
|
4799
|
+
banner.toggleAttribute('data-compact', mode === 'compact');
|
|
4800
|
+
// The phone's header CSS places the banner on its own row by itself; a
|
|
4801
|
+
// wider header needs this attribute for it.
|
|
4802
|
+
banner.toggleAttribute('data-own-row', mode === 'row' && !phoneOwnRow);
|
|
4803
|
+
}
|
|
4804
|
+
headerPriorityMenuIds = headerPriorityMenuIdsFor(fit.menu);
|
|
4805
|
+
// Measured at least once (the e2e helpers wait for it).
|
|
4806
|
+
actions.dataset.headerFit = '1';
|
|
4807
|
+
return headerPriorityMenuIds;
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4810
|
+
// The pins and hidden twins carry the count their header button shows.
|
|
4811
|
+
function syncHeaderPriorityTwinLabels() {
|
|
4812
|
+
const label = (buttonId, twinId, text, icon) => {
|
|
4813
|
+
const twin = document.getElementById(twinId);
|
|
4814
|
+
if (!twin) return;
|
|
4815
|
+
const count = Number(document.getElementById(buttonId)?.dataset.count || 0);
|
|
4816
|
+
twin.textContent = count ? `${icon} ${text} (${count})` : `${icon} ${text}`;
|
|
4817
|
+
};
|
|
4818
|
+
const questions = document.getElementById('questions-btn');
|
|
4819
|
+
const questionsTwin = document.getElementById('chat-menu-questions');
|
|
4820
|
+
if (questionsTwin) {
|
|
4821
|
+
questionsTwin.textContent = questionsTwinLabel({
|
|
4822
|
+
pending: Number(questions?.dataset.count || 0),
|
|
4823
|
+
urgent: Number(questions?.dataset.urgent || 0),
|
|
4824
|
+
});
|
|
4825
|
+
}
|
|
4826
|
+
label('pinned-messages-btn', 'chat-menu-pins', 'Pinned messages', '📍');
|
|
4827
|
+
label('hidden-messages-btn', 'chat-menu-hidden', 'Hidden from shared viewers', '🙈');
|
|
4828
|
+
}
|
|
4829
|
+
|
|
4830
|
+
// At most one fit per frame. A pass asked for by an observer of one of the
|
|
4831
|
+
// fit's inputs (`force`) measures; one asked for by a header sync (every list
|
|
4832
|
+
// paint ends in one) measures only if headerFitSignature says something
|
|
4833
|
+
// changed. Either way it runs before the next paint.
|
|
4834
|
+
let headerPriorityFrame = 0;
|
|
4835
|
+
let headerPriorityForce = false;
|
|
4836
|
+
function scheduleHeaderPrioritySync({ force = false } = {}) {
|
|
4837
|
+
if (force) headerPriorityForce = true;
|
|
4838
|
+
if (headerPriorityFrame) return;
|
|
4839
|
+
headerPriorityFrame = requestAnimationFrame(() => {
|
|
4840
|
+
headerPriorityFrame = 0;
|
|
4841
|
+
const forced = headerPriorityForce;
|
|
4842
|
+
headerPriorityForce = false;
|
|
4843
|
+
syncChatActionsMenuState({ fit: forced ? 'force' : 'now' });
|
|
4844
|
+
// The header moved or changed size: the open menu follows it.
|
|
4845
|
+
if (forced) placeChatActionsMenu();
|
|
4846
|
+
});
|
|
4847
|
+
}
|
|
4848
|
+
|
|
4849
|
+
// Every reason the fit can change: the header's own box (window, drawer,
|
|
4850
|
+
// font scale), the title, the banner, the theme (the glyph's width), a view's
|
|
4851
|
+
// counts or fullscreen verdict (HEADER_ACTIONS_CHANGE_EVENT), the font scale
|
|
4852
|
+
// and the phone turning. Conversation switches reach it through
|
|
4853
|
+
// syncChatTitleControls.
|
|
4854
|
+
function initHeaderPriorityActions({ sharedMode = false } = {}) {
|
|
4855
|
+
if (!sharedMode) {
|
|
4856
|
+
// 📓's own button toggles the list under it (questions-panel-ui.js); the
|
|
4857
|
+
// twin opens it under ︙.
|
|
4858
|
+
bindHeaderPriorityAction('questions', () => {
|
|
4859
|
+
openQuestionsPanel({ anchor: document.getElementById('chat-actions-menu-btn') });
|
|
4860
|
+
}, { inline: false });
|
|
4861
|
+
bindHeaderPriorityAction('pins', () => openPinnedMessagesModal(), { inline: false });
|
|
4862
|
+
bindHeaderPriorityAction('hidden', () => openHiddenMessagesModal(), { inline: false });
|
|
4863
|
+
bindHeaderPriorityAction('search', () => openMessageSearchModal(), { inline: false });
|
|
4864
|
+
bindHeaderPriorityAction('context', () => { void showContext(); }, { inline: false });
|
|
4865
|
+
bindHeaderPriorityAction('usage', () => {
|
|
4866
|
+
showUsage().catch((error) => {
|
|
4867
|
+
alert(error?.message || 'Failed to load usage');
|
|
4868
|
+
});
|
|
4869
|
+
});
|
|
4870
|
+
bindHeaderPriorityAction('fullscreen', () => { void toggleFullscreen(); }, { inline: false });
|
|
4871
|
+
}
|
|
4872
|
+
if (window.__headerPriorityObserversBound) return;
|
|
4873
|
+
window.__headerPriorityObserversBound = true;
|
|
4874
|
+
const forced = () => scheduleHeaderPrioritySync({ force: true });
|
|
4875
|
+
const header = document.getElementById('chat-header');
|
|
4876
|
+
if (header && typeof ResizeObserver === 'function') {
|
|
4877
|
+
new ResizeObserver(forced).observe(header);
|
|
4878
|
+
} else {
|
|
4879
|
+
window.addEventListener('resize', forced);
|
|
4880
|
+
}
|
|
4881
|
+
// The open ︙ menu stays inside the window (a turn, the keyboard).
|
|
4882
|
+
window.addEventListener('resize', () => placeChatActionsMenu());
|
|
4883
|
+
if (typeof MutationObserver === 'function') {
|
|
4884
|
+
new MutationObserver(forced)
|
|
4885
|
+
.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
4886
|
+
const title = document.getElementById('chat-title');
|
|
4887
|
+
if (title) {
|
|
4888
|
+
new MutationObserver(forced)
|
|
4889
|
+
.observe(title, { childList: true, characterData: true, subtree: true });
|
|
4890
|
+
}
|
|
4891
|
+
// The pending-question banner shares the first row on a wide screen: it
|
|
4892
|
+
// takes room when it shows (on a phone it has a row of its own).
|
|
4893
|
+
const banner = document.getElementById('pending-question-banner');
|
|
4894
|
+
if (banner) {
|
|
4895
|
+
new MutationObserver(forced)
|
|
4896
|
+
.observe(banner, { attributes: true, attributeFilter: ['class', 'hidden'], childList: true, characterData: true, subtree: true });
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
window.addEventListener(HEADER_ACTIONS_CHANGE_EVENT, forced);
|
|
4900
|
+
window.addEventListener(FONT_SCALE_CHANGE_EVENT, forced);
|
|
4901
|
+
window.addEventListener(PHONE_LANDSCAPE_CHANGE_EVENT, forced);
|
|
4902
|
+
document.addEventListener('fullscreenchange', forced);
|
|
4903
|
+
syncChatActionsMenuState({ fit: 'force' });
|
|
4904
|
+
}
|
|
4905
|
+
|
|
4906
|
+
// The meta line's chips the conversation on screen has, for their ︙
|
|
4907
|
+
// stand-ins in phone landscape (where that line is hidden), with the labels.
|
|
4908
|
+
function chatActionsMenuMetaChips(convId) {
|
|
4909
|
+
if (!convId || isSharedReaderMode()) return { chips: {}, labels: null };
|
|
4910
|
+
const watching = relayWatchingChipText(convId);
|
|
4911
|
+
const schedules = relayScheduleChipText(convId);
|
|
4912
|
+
const origin = conversationOriginMenuEntry();
|
|
4913
|
+
return {
|
|
4914
|
+
chips: { watching: !!watching, schedules: !!schedules, origin: !!origin },
|
|
4915
|
+
labels: chatActionsMenuMetaChipLabels({ watching, origin: origin?.label || '' }),
|
|
4916
|
+
};
|
|
4917
|
+
}
|
|
4918
|
+
|
|
4919
|
+
function syncChatActionsMenuMetaChipLabels(labels) {
|
|
4920
|
+
if (!labels) return;
|
|
4921
|
+
for (const [chip, id] of Object.entries(CHAT_ACTIONS_MENU_META_CHIP_IDS)) {
|
|
4922
|
+
const entry = document.getElementById(id);
|
|
4923
|
+
if (entry && labels[chip] && entry.textContent !== labels[chip]) entry.textContent = labels[chip];
|
|
4924
|
+
}
|
|
4925
|
+
}
|
|
4926
|
+
|
|
4927
|
+
// The ︙ menu as it should look right now: the trigger and every entry,
|
|
4505
4928
|
// decided in chat-actions-menu-state.mjs from the conversation on screen.
|
|
4506
|
-
|
|
4929
|
+
// `fit`: 'schedule' (the default) keeps the twins the last fit chose and asks
|
|
4930
|
+
// for a fit before the next paint; 'now' fits first if any of its inputs
|
|
4931
|
+
// changed; 'force' fits first whatever.
|
|
4932
|
+
function syncChatActionsMenuState({ fit = 'schedule' } = {}) {
|
|
4507
4933
|
const convId = String(currentConvId || '').trim();
|
|
4508
4934
|
const conversation = convId ? (conversations[convId] || null) : null;
|
|
4509
4935
|
const isCloud = !!convId && isCurrentConversationClaudeCloud();
|
|
4936
|
+
let headerMenuIds;
|
|
4937
|
+
if (fit === 'now' || fit === 'force') {
|
|
4938
|
+
headerMenuIds = syncHeaderPriorityFit({ force: fit === 'force' });
|
|
4939
|
+
} else {
|
|
4940
|
+
headerMenuIds = keepHeaderPriorityPlacement(headerPriorityActionAvailability());
|
|
4941
|
+
scheduleHeaderPrioritySync();
|
|
4942
|
+
}
|
|
4943
|
+
syncHeaderPriorityTwinLabels();
|
|
4944
|
+
const metaChips = chatActionsMenuMetaChips(convId);
|
|
4945
|
+
syncChatActionsMenuMetaChipLabels(metaChips.labels);
|
|
4946
|
+
const fullscreenBtn = document.getElementById('fullscreen-btn');
|
|
4510
4947
|
const state = resolveChatActionsMenuState({
|
|
4511
4948
|
sharedMode: isSharedReaderMode(),
|
|
4512
4949
|
conversationId: convId,
|
|
@@ -4517,20 +4954,60 @@ function syncChatActionsMenuState() {
|
|
|
4517
4954
|
editing: !!convId && chatTitleEditingConversationId === convId,
|
|
4518
4955
|
suspendVisible: isSuspendHostActionVisible(),
|
|
4519
4956
|
hasActiveShare: hasActiveShare(convId),
|
|
4957
|
+
headerMenuIds,
|
|
4958
|
+
// As the header button: disabled where the browser cannot go fullscreen.
|
|
4959
|
+
fullscreenSupported: !fullscreenBtn?.disabled,
|
|
4960
|
+
phoneLandscape: document.documentElement.hasAttribute(PHONE_LANDSCAPE_ATTRIBUTE),
|
|
4961
|
+
metaChips: metaChips.chips,
|
|
4520
4962
|
});
|
|
4963
|
+
const trigger = document.getElementById('chat-actions-menu-btn');
|
|
4964
|
+
const triggerWasHidden = !!trigger?.hidden;
|
|
4521
4965
|
applyChatActionsMenuState(state);
|
|
4966
|
+
// ︙ coming or going changes the room the fit has (the shared view's menu
|
|
4967
|
+
// empties once Day/Night fits): one more pass settles it.
|
|
4968
|
+
if (trigger && !!trigger.hidden !== triggerWasHidden) scheduleHeaderPrioritySync({ force: true });
|
|
4522
4969
|
return state;
|
|
4523
4970
|
}
|
|
4524
4971
|
|
|
4525
4972
|
// The menu follows the relay's share list (made, renewed, stopped anywhere).
|
|
4526
4973
|
subscribeShares(() => syncChatActionsMenuState());
|
|
4527
4974
|
|
|
4975
|
+
// Keeps the open ︙ menu inside the viewport and #main (never off-screen,
|
|
4976
|
+
// never under the sidebar) and caps its height at what is left below it,
|
|
4977
|
+
// where it scrolls (resolveChatActionsMenuPlacement decides).
|
|
4978
|
+
function placeChatActionsMenu() {
|
|
4979
|
+
const menu = document.getElementById('chat-actions-menu');
|
|
4980
|
+
if (!menu || menu.hidden) return;
|
|
4981
|
+
// Measured as CSS places it; where the reader scrolled to stays.
|
|
4982
|
+
const scrollTop = menu.scrollTop;
|
|
4983
|
+
menu.style.right = '';
|
|
4984
|
+
menu.style.maxWidth = '';
|
|
4985
|
+
menu.style.maxHeight = '';
|
|
4986
|
+
const rect = menu.getBoundingClientRect();
|
|
4987
|
+
if (!rect.width && !rect.height) return;
|
|
4988
|
+
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
4989
|
+
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
4990
|
+
const main = document.getElementById('main')?.getBoundingClientRect?.();
|
|
4991
|
+
const bounds = {
|
|
4992
|
+
left: Math.max(0, main && main.width ? main.left : 0),
|
|
4993
|
+
right: Math.min(viewportWidth, main && main.width ? main.right : viewportWidth),
|
|
4994
|
+
bottom: Math.min(viewportHeight, main && main.height ? main.bottom : viewportHeight),
|
|
4995
|
+
};
|
|
4996
|
+
// 8px from the edges and a 120px floor at 100 %, scaled with the zoom.
|
|
4997
|
+
const scale = getUiScale();
|
|
4998
|
+
const placement = resolveChatActionsMenuPlacement({ rect, bounds, margin: 8 * scale, minHeight: 120 * scale });
|
|
4999
|
+
if (placement.right) menu.style.right = `${placement.right}px`;
|
|
5000
|
+
if (placement.maxWidth !== null) menu.style.maxWidth = `${placement.maxWidth}px`;
|
|
5001
|
+
menu.style.maxHeight = `${placement.maxHeight}px`;
|
|
5002
|
+
if (scrollTop) menu.scrollTop = scrollTop;
|
|
5003
|
+
}
|
|
5004
|
+
|
|
4528
5005
|
function toggleChatActionsMenu() {
|
|
4529
5006
|
const menu = document.getElementById('chat-actions-menu');
|
|
4530
5007
|
const trigger = document.getElementById('chat-actions-menu-btn');
|
|
4531
5008
|
if (!menu || !trigger) return;
|
|
4532
5009
|
// The entries match the moment of the tap, as Refresh history already does.
|
|
4533
|
-
syncChatActionsMenuState();
|
|
5010
|
+
syncChatActionsMenuState({ fit: 'now' });
|
|
4534
5011
|
if (trigger.hidden || trigger.disabled) return;
|
|
4535
5012
|
syncRefreshHistoryMenuState();
|
|
4536
5013
|
syncThemeMenuLabel();
|
|
@@ -4538,6 +5015,10 @@ function toggleChatActionsMenu() {
|
|
|
4538
5015
|
const willOpen = !!menu.hidden;
|
|
4539
5016
|
menu.hidden = !willOpen;
|
|
4540
5017
|
trigger.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
|
|
5018
|
+
if (willOpen) {
|
|
5019
|
+
placeChatActionsMenu();
|
|
5020
|
+
menu.scrollTop = 0;
|
|
5021
|
+
}
|
|
4541
5022
|
}
|
|
4542
5023
|
|
|
4543
5024
|
function isConversationLocallyProcessing(conversation) {
|
|
@@ -4617,6 +5098,7 @@ function syncChatTitleControls() {
|
|
|
4617
5098
|
if (!convId) {
|
|
4618
5099
|
setPinsConversation(null);
|
|
4619
5100
|
setHiddenMessagesConversation(null);
|
|
5101
|
+
setQuestionsConversation(null);
|
|
4620
5102
|
}
|
|
4621
5103
|
const conversation = convId ? (conversations[convId] || null) : null;
|
|
4622
5104
|
const sdkSessionId = String(conversation?.sdkSessionId || '').trim();
|
|
@@ -4644,12 +5126,14 @@ function syncChatTitleControls() {
|
|
|
4644
5126
|
const editing = convId && chatTitleEditingConversationId === convId;
|
|
4645
5127
|
document.body.classList.toggle('chat-title-editing', !!editing);
|
|
4646
5128
|
if (title) title.hidden = editing;
|
|
4647
|
-
// The
|
|
4648
|
-
// open, so Settings is always one tap away.
|
|
5129
|
+
// The ︙ trigger and its entries: relay-wide ones stay with no conversation
|
|
5130
|
+
// open, so Settings is always one tap away. The header's fit is only asked
|
|
5131
|
+
// for here (once per frame at most, and it measures only when its inputs
|
|
5132
|
+
// changed): every list paint ends in this call.
|
|
4649
5133
|
syncChatActionsMenuState();
|
|
4650
5134
|
const archiveBtn = document.getElementById('chat-menu-archive');
|
|
4651
5135
|
if (archiveBtn) {
|
|
4652
|
-
archiveBtn.textContent = conversation?.archived === true ? '📂 Unarchive conversation' : '
|
|
5136
|
+
archiveBtn.textContent = conversation?.archived === true ? '📂 Unarchive conversation' : '🗄️ Archive conversation';
|
|
4653
5137
|
}
|
|
4654
5138
|
if (editor) {
|
|
4655
5139
|
editor.hidden = !editing;
|
|
@@ -4731,6 +5215,7 @@ function applyConversationTitleUpdate(conversationId, title, updatedAt) {
|
|
|
4731
5215
|
if (currentConvId === id) {
|
|
4732
5216
|
const titleEl = document.getElementById('chat-title');
|
|
4733
5217
|
if (titleEl) titleEl.textContent = nextTitle;
|
|
5218
|
+
syncSessionDocumentTitle();
|
|
4734
5219
|
}
|
|
4735
5220
|
renderConvList();
|
|
4736
5221
|
}
|
|
@@ -4895,6 +5380,9 @@ async function initApp() {
|
|
|
4895
5380
|
if (!sharedMode) {
|
|
4896
5381
|
syncSuspendHostVisibility();
|
|
4897
5382
|
syncScrollOnSendToggle();
|
|
5383
|
+
syncThoughtsExpandedToggle();
|
|
5384
|
+
syncLiveActivityCapSlider();
|
|
5385
|
+
syncStreamViewToggle();
|
|
4898
5386
|
syncWindowRetentionSlider();
|
|
4899
5387
|
initHostSuspendUi();
|
|
4900
5388
|
initUsageLimitUi();
|
|
@@ -4907,6 +5395,11 @@ async function initApp() {
|
|
|
4907
5395
|
renderConvList();
|
|
4908
5396
|
},
|
|
4909
5397
|
});
|
|
5398
|
+
// The ⚙ panel and the label row above the message box.
|
|
5399
|
+
initComposerSettings({ isSharedReaderMode, showContext: () => showContext() });
|
|
5400
|
+
// A phone turned sideways: its typing mode folds the label row and the
|
|
5401
|
+
// task line into floating buttons while the keyboard is up.
|
|
5402
|
+
initPhoneLandscape({ isSharedReaderMode });
|
|
4910
5403
|
}
|
|
4911
5404
|
setupViewportTracking();
|
|
4912
5405
|
bindChatSelectionGuard();
|
|
@@ -4938,24 +5431,37 @@ async function initApp() {
|
|
|
4938
5431
|
event.stopPropagation();
|
|
4939
5432
|
toggleChatActionsMenu();
|
|
4940
5433
|
});
|
|
5434
|
+
// Header buttons and their ︙ twins (Check Usage among them), and the
|
|
5435
|
+
// passes that decide which of them fit.
|
|
5436
|
+
initHeaderPriorityActions({ sharedMode });
|
|
4941
5437
|
if (!sharedMode) {
|
|
4942
5438
|
const chatMenuEditBtn = document.getElementById('chat-menu-edit-title');
|
|
4943
|
-
|
|
4944
|
-
bindMenuAction(chatMenuUsageBtn, (event) => {
|
|
5439
|
+
bindMenuAction(chatMenuEditBtn, (event) => {
|
|
4945
5440
|
event.preventDefault();
|
|
4946
5441
|
event.stopPropagation();
|
|
4947
5442
|
lockChatActionsMenuShield(350);
|
|
4948
5443
|
closeChatActionsMenu();
|
|
4949
|
-
|
|
4950
|
-
alert(error?.message || 'Failed to load usage');
|
|
4951
|
-
});
|
|
5444
|
+
openChatTitleEditor();
|
|
4952
5445
|
});
|
|
4953
|
-
|
|
5446
|
+
// Phone landscape hides the meta line: these stand in for its chips and
|
|
5447
|
+
// do what a tap on the chip does. The lists open under ︙, the chip
|
|
5448
|
+
// having no box to hang from.
|
|
5449
|
+
const metaChipAction = (id, run) => bindMenuAction(document.getElementById(id), (event) => {
|
|
4954
5450
|
event.preventDefault();
|
|
4955
5451
|
event.stopPropagation();
|
|
4956
5452
|
lockChatActionsMenuShield(350);
|
|
4957
5453
|
closeChatActionsMenu();
|
|
4958
|
-
|
|
5454
|
+
run();
|
|
5455
|
+
});
|
|
5456
|
+
metaChipAction(CHAT_ACTIONS_MENU_META_CHIP_IDS.watching, () => {
|
|
5457
|
+
openRelayWatchPanel({ anchor: document.getElementById('chat-actions-menu-btn') });
|
|
5458
|
+
});
|
|
5459
|
+
metaChipAction(CHAT_ACTIONS_MENU_META_CHIP_IDS.schedules, () => {
|
|
5460
|
+
openRelaySchedulePanel({ anchor: document.getElementById('chat-actions-menu-btn') });
|
|
5461
|
+
});
|
|
5462
|
+
metaChipAction(CHAT_ACTIONS_MENU_META_CHIP_IDS.origin, () => {
|
|
5463
|
+
const origin = conversationOriginMenuEntry();
|
|
5464
|
+
if (origin) void openOriginConversation(null, { dataset: { originConversationId: origin.conversationId } });
|
|
4959
5465
|
});
|
|
4960
5466
|
const chatMenuCompactBtn = document.getElementById('chat-menu-compact');
|
|
4961
5467
|
if (chatMenuCompactBtn && !chatMenuCompactBtn.hidden && !chatMenuCompactBtn.disabled) {
|
|
@@ -5267,7 +5773,9 @@ async function initApp() {
|
|
|
5267
5773
|
initPushNotificationClientHooks();
|
|
5268
5774
|
// Browsers without Background Sync replay the outbox from the page instead.
|
|
5269
5775
|
void initOutboxFallbackReplay();
|
|
5270
|
-
|
|
5776
|
+
// Read before anything opens: the address then follows the open session.
|
|
5777
|
+
const pushConversationId = consumePushConversationDeepLink();
|
|
5778
|
+
await loadConversations({ openLast: !pushConversationId });
|
|
5271
5779
|
// The sidebar's "watched" badges follow the subscriptions.
|
|
5272
5780
|
onRelayEventSubscriptionsChanged(() => renderConvList());
|
|
5273
5781
|
void loadRelayEventSubscriptions();
|
|
@@ -5276,9 +5784,9 @@ async function initApp() {
|
|
|
5276
5784
|
void loadRelaySchedules();
|
|
5277
5785
|
// The Inbox's count, and the cards of questions that don't block.
|
|
5278
5786
|
startInbox();
|
|
5279
|
-
|
|
5787
|
+
initQuestionsPanel();
|
|
5280
5788
|
if (pushConversationId) {
|
|
5281
|
-
await openConversation(pushConversationId).catch(() => {});
|
|
5789
|
+
await openConversation(pushConversationId, { history: 'replace' }).catch(() => {});
|
|
5282
5790
|
}
|
|
5283
5791
|
await loadRelayQuestions(currentConvId);
|
|
5284
5792
|
await loadRelayBoards();
|
|
@@ -5333,6 +5841,32 @@ async function bootstrap() {
|
|
|
5333
5841
|
}
|
|
5334
5842
|
const urlToken = getTokenFromUrl();
|
|
5335
5843
|
if (urlToken) stripTokenFromUrl();
|
|
5844
|
+
// Back and forward walk the sessions opened, and back closes an open
|
|
5845
|
+
// overlay first (session-history.mjs).
|
|
5846
|
+
initSessionHistory({
|
|
5847
|
+
openConversation,
|
|
5848
|
+
openSidebar,
|
|
5849
|
+
closeSidebar,
|
|
5850
|
+
closeComposerSettings,
|
|
5851
|
+
closeChatActionsMenu,
|
|
5852
|
+
closeSummaryModal,
|
|
5853
|
+
closeSettingsModal,
|
|
5854
|
+
closeMessageSearchModal,
|
|
5855
|
+
closeRepoBrowser,
|
|
5856
|
+
closeNewConversationModelModal,
|
|
5857
|
+
// Leaving the Inbox goes back to the conversation it was opened from.
|
|
5858
|
+
// Opened from Status it stored none: `returnTo` is the session history stands on.
|
|
5859
|
+
closeInboxView: ({ returnTo = '' } = {}) => { if (isInboxViewActive()) void toggleInboxView({ returnTo }); },
|
|
5860
|
+
closeChangeCwdMenu: () => closeChangeCwdMenu({ focusTrigger: true }),
|
|
5861
|
+
closeGitChangesModal,
|
|
5862
|
+
closeGitDiffViewer,
|
|
5863
|
+
closeTmuxInspector: closeTmuxInspectorView,
|
|
5864
|
+
closeAnnotateEditor,
|
|
5865
|
+
closeRelayWatchPanel,
|
|
5866
|
+
closeQuestionsPanel,
|
|
5867
|
+
closeRelaySchedulePanel,
|
|
5868
|
+
closeEmojiPicker,
|
|
5869
|
+
});
|
|
5336
5870
|
// In the installed app the system back button must never close the app;
|
|
5337
5871
|
// a tab keeps its own back button. After the token is gone from the
|
|
5338
5872
|
// address, so the entries the guard lays down carry the clean one.
|
|
@@ -5341,7 +5875,10 @@ async function bootstrap() {
|
|
|
5341
5875
|
enabled: isInstalledAppMode(),
|
|
5342
5876
|
target: window,
|
|
5343
5877
|
onFloor: () => showTransientRelayNotice('Press back again to close the app', 2500),
|
|
5878
|
+
entryAgain: sessionHistoryEntryAgain,
|
|
5344
5879
|
});
|
|
5880
|
+
// Armed after the guard: on a phone the session list lies above its floor.
|
|
5881
|
+
armSessionHistoryOnFirstInteraction(window);
|
|
5345
5882
|
const persistedToken = consumeLegacyPersistedAuthToken();
|
|
5346
5883
|
const bootstrapToken = String(urlToken || persistedToken || '').trim();
|
|
5347
5884
|
const existingSession = await verifyExistingSession(bootstrapToken);
|
|
@@ -5415,6 +5952,12 @@ window.resetGrokAllowanceAccounting = resetGrokAllowanceAccounting;
|
|
|
5415
5952
|
window.toggleCursorProvider = toggleCursorProvider;
|
|
5416
5953
|
window.updateShowSuspendHostSetting = updateShowSuspendHostSetting;
|
|
5417
5954
|
window.updateScrollOnSendSetting = (enabled) => updateScrollOnSendSetting(enabled);
|
|
5955
|
+
window.updateThoughtsExpandedSetting = (expanded) => updateThoughtsExpandedSetting(expanded);
|
|
5956
|
+
window.updateStreamViewSetting = (on) => updateStreamViewSetting(on);
|
|
5957
|
+
// The transcript and the live bubble take the other layout at once.
|
|
5958
|
+
window.addEventListener(STREAM_VIEW_CHANGE_EVENT, () => refreshStreamViewLayout());
|
|
5959
|
+
window.previewLiveActivityCap = (position) => previewLiveActivityCap(position);
|
|
5960
|
+
window.updateLiveActivityCap = (position) => updateLiveActivityCap(position);
|
|
5418
5961
|
window.previewConversationWindowRetention = (position) => previewWindowRetention(position);
|
|
5419
5962
|
// A lower cap cuts the kept windows down now; the open conversation's is cut
|
|
5420
5963
|
// when it is left.
|
|
@@ -5480,17 +6023,20 @@ window.deleteConv = deleteConv;
|
|
|
5480
6023
|
window.archiveConv = archiveConv;
|
|
5481
6024
|
window.openConversationGroupPicker = openConversationGroupPicker;
|
|
5482
6025
|
window.toggleConversationGroupFold = toggleConversationGroupFold;
|
|
6026
|
+
window.toggleAllConversationGroupFolds = toggleAllConversationGroupFolds;
|
|
5483
6027
|
window.unarchiveConv = unarchiveConv;
|
|
5484
6028
|
window.toggleArchivedConversations = toggleArchivedConversations;
|
|
5485
6029
|
window.refreshNewConversationCloudRepoList = refreshNewConversationCloudRepoList;
|
|
5486
6030
|
window.openConversation = openConversation;
|
|
5487
6031
|
window.openOriginConversation = openOriginConversation;
|
|
5488
6032
|
// The Inbox and Status share the transcript's place: opening one leaves the other.
|
|
6033
|
+
// Closed by its button, one opened from the other goes back to the session
|
|
6034
|
+
// history stands on.
|
|
5489
6035
|
window.toggleStatusView = () => {
|
|
5490
6036
|
leaveInboxView();
|
|
5491
|
-
return toggleStatusView();
|
|
6037
|
+
return toggleStatusView({ returnTo: sessionHistoryConversation() });
|
|
5492
6038
|
};
|
|
5493
|
-
window.toggleInboxView = toggleInboxView;
|
|
6039
|
+
window.toggleInboxView = () => toggleInboxView({ returnTo: sessionHistoryConversation() });
|
|
5494
6040
|
window.refreshConversations = refreshConversations;
|
|
5495
6041
|
window.renderConvList = renderConvList;
|
|
5496
6042
|
window.handleAttachmentInput = handleAttachmentInput;
|