@oar-sh/oar 0.9.20 → 0.9.21
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 +212 -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 +22 -1
- package/server/claude-cloud-worker/claude-cloud-worker-steering.mjs +34 -0
- package/server/claude-worker/claude-session-process.mjs +7 -0
- package/server/claude-worker/claude-session-worker-main.mjs +8 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +74 -1
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +8 -1
- package/server/cursor-worker/cursor-session-worker.mjs +12 -1
- package/server/cursor-worker/cursor-worker-steering.mjs +20 -0
- package/server/grok-worker/grok-background-tasks.mjs +9 -0
- package/server/grok-worker/grok-session-worker.mjs +12 -1
- package/server/grok-worker/grok-turn-runner.mjs +3 -0
- package/server/grok-worker/grok-worker-steering.mjs +23 -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/api-client.js +4 -2
- package/server/public/app/ask-user-view.js +20 -6
- package/server/public/app/attachments-view.js +1 -1
- package/server/public/app/back-guard.mjs +31 -5
- package/server/public/app/bootstrap.js +556 -68
- package/server/public/app/chat-actions-menu-state.mjs +139 -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/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 +3 -0
- 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 +234 -0
- package/server/public/app/hidden-messages-view.mjs +4 -0
- package/server/public/app/inbox-ui.js +18 -2
- package/server/public/app/journal-view.js +311 -58
- 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/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/remote-relay-origin-view.mjs +41 -23
- package/server/public/app/session-history-ui.js +250 -0
- package/server/public/app/session-history.mjs +599 -0
- package/server/public/app/settings-modal.js +61 -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 +5 -2
- package/server/public/app/status-view.mjs +18 -2
- package/server/public/app/store.js +87 -16
- package/server/public/app/subagent-viewer.mjs +27 -2
- package/server/public/app/tab-strip-fade.mjs +101 -0
- package/server/public/app/tap-actions.js +66 -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 +2249 -1590
- package/server/relay-tools.md +1 -0
- package/server/repositories/message-repository.mjs +23 -0
- package/server/repositories/session-repository.mjs +19 -0
- package/server/routes/claude-cloud-routes.mjs +19 -0
- package/server/routes/messages-routes.mjs +57 -1
- package/server/routes/sessions-routes.mjs +41 -7
- package/server/server-runtime.mjs +190 -40
- 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/remote-relay-dispatcher.mjs +61 -14
- package/server/services/session-activity-tracker.mjs +1 -1
- 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-websocket-service.mjs +40 -16
- package/server/services/worker-recycle-service.mjs +772 -0
- package/server/services/worker-replacement.mjs +197 -0
- package/shared/build-id.mjs +128 -0
- package/shared/worker-runtime/heartbeat.mjs +5 -0
- package/shared/worker-runtime/simple-steering-state.mjs +38 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +23 -0
- package/shared/worker-runtime/working-directory.mjs +1 -1
|
@@ -107,6 +107,7 @@ import {
|
|
|
107
107
|
openOriginConversation,
|
|
108
108
|
openConversationGroupPicker,
|
|
109
109
|
toggleConversationGroupFold,
|
|
110
|
+
toggleAllConversationGroupFolds,
|
|
110
111
|
isConversationWorking,
|
|
111
112
|
} from './journal-view.js';
|
|
112
113
|
import {
|
|
@@ -153,8 +154,8 @@ import {
|
|
|
153
154
|
import { loadKeepingLiveRows } from './transcript-reload-freshness.mjs';
|
|
154
155
|
import { backgroundTasksSnapshotMark } from './background-tasks-view.mjs';
|
|
155
156
|
import { newerPageReachesEnd } from './transcript-island.mjs';
|
|
156
|
-
import { initPinnedMessagesView, setPinsConversation } from './pinned-messages-view.mjs';
|
|
157
|
-
import { initHiddenMessagesView, setHiddenMessagesConversation } from './hidden-messages-view.mjs';
|
|
157
|
+
import { initPinnedMessagesView, openPinnedMessagesModal, setPinsConversation } from './pinned-messages-view.mjs';
|
|
158
|
+
import { initHiddenMessagesView, openHiddenMessagesModal, setHiddenMessagesConversation } from './hidden-messages-view.mjs';
|
|
158
159
|
import { bindChatSelectionGuard, chatSelectionGuard, isChatInteractionHeld } from './selection-guard.mjs';
|
|
159
160
|
import { loadRepoBrowserTree, openRepoBrowser, closeRepoBrowser, setRepoBrowserSessionInfo, resetWorkspaceRepoBrowserForRootChange } from './attachments-view.js';
|
|
160
161
|
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 +238,20 @@ import {
|
|
|
237
238
|
isInstalledAppMode,
|
|
238
239
|
} from './pwa-install.js';
|
|
239
240
|
import { armBackGuardOnFirstInteraction } from './back-guard.mjs';
|
|
241
|
+
import {
|
|
242
|
+
armSessionHistoryOnFirstInteraction,
|
|
243
|
+
initSessionHistory,
|
|
244
|
+
sessionHistoryConversation,
|
|
245
|
+
sessionHistoryEntryAgain,
|
|
246
|
+
syncSessionDocumentTitle,
|
|
247
|
+
} from './session-history-ui.js';
|
|
248
|
+
// What back closes, beyond the closers imported elsewhere in this file.
|
|
249
|
+
import { closeAnnotateEditor } from './annotate-editor.js';
|
|
250
|
+
import { closeEmojiPicker } from './emoji-view.js';
|
|
251
|
+
import { closeChangeCwdMenu } from './cwd-picker.js';
|
|
252
|
+
import { closeRelayWatchPanel } from './relay-event-subscriptions-ui.js';
|
|
253
|
+
import { closeRelaySchedulePanel } from './relay-schedules-ui.js';
|
|
254
|
+
import { isInboxViewActive } from './inbox-ui.js';
|
|
240
255
|
import { initScrollbarHover } from './scrollbar-hover.mjs';
|
|
241
256
|
import {
|
|
242
257
|
autoCompactWindowNotes,
|
|
@@ -255,7 +270,8 @@ import {
|
|
|
255
270
|
formatAutoCompactWindowLabel,
|
|
256
271
|
} from './auto-compact-window-options.mjs';
|
|
257
272
|
import { renderPlanUsageHtml, planUsageSubtitle, normalizeUsageProvider } from './plan-usage-view.mjs';
|
|
258
|
-
import {
|
|
273
|
+
import { revealSelectedTabs, watchTabStripFades } from './tab-strip-fade.mjs';
|
|
274
|
+
import { FONT_SCALE_CHANGE_EVENT, getUiScale, initFontScaling, updateFontScaleFromSelect } from './font-scaling.js';
|
|
259
275
|
import { initClientDiagnostics, recordStatusEvent } from './status-store.mjs';
|
|
260
276
|
import { isStatusViewActive, toggleStatusView } from './status-view.mjs';
|
|
261
277
|
import { installExternalLinkPolicy, openExternalNavigation } from './external-link-policy.mjs';
|
|
@@ -384,6 +400,8 @@ import {
|
|
|
384
400
|
import { applyPendingActionsFromStatus, initHostSuspendUi } from './host-suspend-ui.js';
|
|
385
401
|
import { applyUsageLimitFromStatus, initUsageLimitUi } from './usage-limit-ui.js';
|
|
386
402
|
import { initClaudeBillingToggle, renderClaudeBillingToggle } from './claude-billing-toggle-ui.js';
|
|
403
|
+
import { closeComposerSettings, initComposerSettings, scheduleComposerSettingsSync } from './composer-settings-ui.js';
|
|
404
|
+
import { initPhoneLandscape } from './phone-landscape-ui.js';
|
|
387
405
|
import {
|
|
388
406
|
startClaudeRelogin,
|
|
389
407
|
submitClaudeLoginCodeFromInput,
|
|
@@ -411,16 +429,43 @@ import {
|
|
|
411
429
|
import { isCurrentConversationClaudeCloud, openCurrentCloudSession, syncClaudeCloudConversationUi } from './claude-cloud-conversation-ui.js';
|
|
412
430
|
import {
|
|
413
431
|
CHAT_ACTIONS_MENU_ENTRY_IDS,
|
|
432
|
+
CHAT_ACTIONS_MENU_META_CHIP_IDS,
|
|
414
433
|
CHAT_ACTIONS_MENU_META_IDS,
|
|
434
|
+
chatActionsMenuMetaChipLabels,
|
|
435
|
+
resolveChatActionsMenuPlacement,
|
|
415
436
|
resolveChatActionsMenuState,
|
|
416
437
|
} from './chat-actions-menu-state.mjs';
|
|
438
|
+
import {
|
|
439
|
+
HEADER_ACTIONS_CHANGE_EVENT,
|
|
440
|
+
HEADER_BANNER_OWN_ROW_QUERY,
|
|
441
|
+
HEADER_PRIORITY_ACTIONS,
|
|
442
|
+
HEADER_TITLE_MIN_REM,
|
|
443
|
+
fitHeaderPriorityActions,
|
|
444
|
+
headerPriorityMenuIdsFor,
|
|
445
|
+
headerReservedWidth,
|
|
446
|
+
resolveHeaderActionAvailability,
|
|
447
|
+
resolveQuestionBannerMode,
|
|
448
|
+
} from './header-priority-actions.mjs';
|
|
449
|
+
import { PHONE_LANDSCAPE_ATTRIBUTE, PHONE_LANDSCAPE_CHANGE_EVENT } from './phone-landscape.mjs';
|
|
417
450
|
import { subscribeProviderReadiness, updateProviderReadinessInputs } from './provider-readiness.mjs';
|
|
418
451
|
import { hasActiveShare, refreshShares, subscribeShares } from './share-state.js';
|
|
419
452
|
import { openStopSharingConfirmation } from './shared-conversations-settings.js';
|
|
420
453
|
import { showAbout, syncAboutMenuLabel } from './about-ui.js';
|
|
421
|
-
import { syncConversationOriginHeader } from './conversation-origin-ui.js';
|
|
422
|
-
import {
|
|
423
|
-
|
|
454
|
+
import { conversationOriginMenuEntry, syncConversationOriginHeader } from './conversation-origin-ui.js';
|
|
455
|
+
import {
|
|
456
|
+
loadRelayEventSubscriptions,
|
|
457
|
+
onRelayEventSubscriptionsChanged,
|
|
458
|
+
openRelayWatchPanel,
|
|
459
|
+
relayWatchingChipText,
|
|
460
|
+
syncRelayEventHeader,
|
|
461
|
+
} from './relay-event-subscriptions-ui.js';
|
|
462
|
+
import {
|
|
463
|
+
loadRelaySchedules,
|
|
464
|
+
onRelaySchedulesChanged,
|
|
465
|
+
openRelaySchedulePanel,
|
|
466
|
+
relayScheduleChipText,
|
|
467
|
+
syncRelayScheduleHeader,
|
|
468
|
+
} from './relay-schedules-ui.js';
|
|
424
469
|
import { leaveInboxView, startInbox, toggleInboxView } from './inbox-ui.js';
|
|
425
470
|
import {
|
|
426
471
|
previewAgentSessionsMaxWait,
|
|
@@ -816,8 +861,9 @@ function initPushNotificationClientHooks() {
|
|
|
816
861
|
});
|
|
817
862
|
}
|
|
818
863
|
|
|
819
|
-
// ?push_conv=<id> (a notification tap) and ?conv=<id> (
|
|
820
|
-
//
|
|
864
|
+
// ?push_conv=<id> (a notification tap, consumed) and ?conv=<id> (the open
|
|
865
|
+
// session's address: a reload, a copied link, another relay's provenance
|
|
866
|
+
// badge) open that conversation at load.
|
|
821
867
|
function consumePushConversationDeepLink() {
|
|
822
868
|
return consumeConversationDeepLink();
|
|
823
869
|
}
|
|
@@ -931,27 +977,58 @@ function initNetworkLifecycleHandling() {
|
|
|
931
977
|
}
|
|
932
978
|
|
|
933
979
|
function syncThemeMenuLabel() {
|
|
980
|
+
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
|
934
981
|
const button = document.getElementById('chat-menu-theme-toggle');
|
|
935
|
-
if (
|
|
982
|
+
if (button) button.textContent = isLight ? '🌙 Night mode' : '☀️ Day mode';
|
|
983
|
+
// The header twin: the glyph alone, the words in its tooltip.
|
|
984
|
+
const inline = document.getElementById('theme-toggle-btn');
|
|
985
|
+
if (inline) {
|
|
986
|
+
const label = isLight ? 'Night mode' : 'Day mode';
|
|
987
|
+
inline.textContent = isLight ? '🌙' : '☀️';
|
|
988
|
+
inline.title = label;
|
|
989
|
+
inline.setAttribute('aria-label', label);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
function toggleThemeFromHeader() {
|
|
936
994
|
const isLight = document.documentElement.getAttribute('data-theme') === 'light';
|
|
937
|
-
|
|
995
|
+
updateTheme(isLight ? 'dark' : 'light');
|
|
996
|
+
syncThemeMenuLabel();
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// One priority+ action (header-priority-actions.mjs): its header button and
|
|
1000
|
+
// its ︙ twin run the same handler. A header button that already carries its
|
|
1001
|
+
// own binding (an inline onclick, a view module's listener) passes
|
|
1002
|
+
// `inline: false`, and only the twin is bound here.
|
|
1003
|
+
function bindHeaderPriorityAction(key, run, { inline = true } = {}) {
|
|
1004
|
+
const action = HEADER_PRIORITY_ACTIONS.find((entry) => entry.key === key);
|
|
1005
|
+
if (!action) return;
|
|
1006
|
+
const inlineBtn = inline ? document.getElementById(action.inlineId) : null;
|
|
1007
|
+
if (inlineBtn && inlineBtn.dataset.headerActionBound !== '1') {
|
|
1008
|
+
inlineBtn.dataset.headerActionBound = '1';
|
|
1009
|
+
inlineBtn.addEventListener('click', (event) => {
|
|
1010
|
+
event.preventDefault();
|
|
1011
|
+
event.stopPropagation();
|
|
1012
|
+
run(event);
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
const twin = document.getElementById(action.menuId);
|
|
1016
|
+
if (twin && twin.dataset.headerActionBound !== '1') {
|
|
1017
|
+
twin.dataset.headerActionBound = '1';
|
|
1018
|
+
bindMenuAction(twin, (event) => {
|
|
1019
|
+
event.preventDefault();
|
|
1020
|
+
event.stopPropagation();
|
|
1021
|
+
lockChatActionsMenuShield(350);
|
|
1022
|
+
closeChatActionsMenu();
|
|
1023
|
+
run(event);
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
938
1026
|
}
|
|
939
1027
|
|
|
940
1028
|
function initThemeMenuToggle() {
|
|
941
|
-
const themeBtn = document.getElementById('chat-menu-theme-toggle');
|
|
942
|
-
if (!themeBtn) return;
|
|
943
|
-
themeBtn.hidden = false;
|
|
944
1029
|
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
|
-
});
|
|
1030
|
+
// Day/Night is the shared view's one action too, so it is bound in both.
|
|
1031
|
+
bindHeaderPriorityAction('theme', () => toggleThemeFromHeader());
|
|
955
1032
|
}
|
|
956
1033
|
|
|
957
1034
|
function disableSharedControl(element, {
|
|
@@ -1025,18 +1102,16 @@ function applySharedReaderUi() {
|
|
|
1025
1102
|
if (effortSelect) effortSelect.hidden = true;
|
|
1026
1103
|
if (modeSelect) modeSelect.hidden = true;
|
|
1027
1104
|
// One source for the menu: the resolver's shared branch (Day/Night only,
|
|
1028
|
-
//
|
|
1029
|
-
// does not know
|
|
1105
|
+
// in the header or in ︙, whichever fits). The loop stays as a guard for
|
|
1106
|
+
// entries the resolver does not know; the sync after it decides Day/Night
|
|
1107
|
+
// and the trigger.
|
|
1030
1108
|
applyChatActionsMenuState(resolveChatActionsMenuState({ sharedMode: true }));
|
|
1031
1109
|
if (chatMenu && themeBtn) {
|
|
1032
1110
|
for (const child of Array.from(chatMenu.children)) {
|
|
1033
1111
|
if (child !== themeBtn) hideSharedMenuEntry(child);
|
|
1034
1112
|
}
|
|
1035
|
-
themeBtn.hidden = false;
|
|
1036
|
-
themeBtn.removeAttribute('aria-hidden');
|
|
1037
|
-
themeBtn.disabled = false;
|
|
1038
|
-
themeBtn.tabIndex = 0;
|
|
1039
1113
|
syncThemeMenuLabel();
|
|
1114
|
+
syncChatActionsMenuState();
|
|
1040
1115
|
} else if (chatMenuBtn) {
|
|
1041
1116
|
chatMenuBtn.hidden = true;
|
|
1042
1117
|
}
|
|
@@ -1460,6 +1535,8 @@ function syncSessionLockNote({ pinnedModel = '' } = {}) {
|
|
|
1460
1535
|
}
|
|
1461
1536
|
// The billing toggle sits under the note and follows the same chat.
|
|
1462
1537
|
renderClaudeBillingToggle();
|
|
1538
|
+
// The composer's label shows the lock and the model.
|
|
1539
|
+
scheduleComposerSettingsSync();
|
|
1463
1540
|
}
|
|
1464
1541
|
|
|
1465
1542
|
function applyModelMetadataHardFail(message = '') {
|
|
@@ -2223,6 +2300,8 @@ function applyConversationPreferences({
|
|
|
2223
2300
|
suppressConversationPreferenceSync = false;
|
|
2224
2301
|
}
|
|
2225
2302
|
syncModeSelectPlanOnlyHint();
|
|
2303
|
+
// Values set here fire no change event: the composer's label reads them.
|
|
2304
|
+
scheduleComposerSettingsSync();
|
|
2226
2305
|
// Nothing is written to the shared "last used" storage here. Applying a
|
|
2227
2306
|
// conversation's stored preferences is not a choice: it runs on every open,
|
|
2228
2307
|
// on the ~1s live poll, on a catalog refresh and on another client's edit,
|
|
@@ -2887,7 +2966,7 @@ function renderModelVariantCatalogBody() {
|
|
|
2887
2966
|
}
|
|
2888
2967
|
const visibleProviderOrder = providerOrder.filter((providerKey) => providerBelongsToTab(providerKey, modelVariantCatalogProviderTab));
|
|
2889
2968
|
const providerTabButtons = `
|
|
2890
|
-
<div class="summary-tab-strip" style="display:flex;flex-wrap:wrap;gap:
|
|
2969
|
+
<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
2970
|
<button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'copilot' ? ' active' : ''}" data-model-provider-tab="copilot">Copilot</button>
|
|
2892
2971
|
<button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'openai' ? ' active' : ''}" data-model-provider-tab="openai" ${hasOpenAITab ? '' : 'disabled'}>OpenAI</button>
|
|
2893
2972
|
<button type="button" class="summary-btn model-provider-tab${modelVariantCatalogProviderTab === 'anthropic' ? ' active' : ''}" data-model-provider-tab="anthropic" ${hasAnthropicTab ? '' : 'disabled'}>Claude SDK</button>
|
|
@@ -3006,7 +3085,7 @@ function renderModelVariantCatalogBody() {
|
|
|
3006
3085
|
`;
|
|
3007
3086
|
}).join('');
|
|
3008
3087
|
const bodyHtml = `
|
|
3009
|
-
<div style="display:flex;flex-direction:column;gap:
|
|
3088
|
+
<div style="display:flex;flex-direction:column;gap:calc(5px * var(--ui-scale))">
|
|
3010
3089
|
${providerTabButtons}
|
|
3011
3090
|
<div style="font-size:0.8rem;color:var(--muted)">
|
|
3012
3091
|
Saved globally for this relay. Refreshed: <strong>${escHtml(refreshedLabel)}</strong>
|
|
@@ -3018,12 +3097,12 @@ function renderModelVariantCatalogBody() {
|
|
|
3018
3097
|
Each model shows all supported reasoning efforts above its selectable variants.
|
|
3019
3098
|
</div>
|
|
3020
3099
|
${warnings.map((line) => `<div style="font-size:0.78rem;color:var(--warn,#f7c873)">${line}</div>`).join('')}
|
|
3021
|
-
<div style="display:grid;gap:
|
|
3100
|
+
<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
3101
|
</div>
|
|
3023
3102
|
`;
|
|
3024
3103
|
renderSummaryModalContent({
|
|
3025
3104
|
title: '🤗 Select Models',
|
|
3026
|
-
subtitle:
|
|
3105
|
+
subtitle: "Choose model variants shown under Model in the composer's ⚙ settings",
|
|
3027
3106
|
bodyHtml,
|
|
3028
3107
|
refresh: async () => {
|
|
3029
3108
|
const refreshed = await refreshModelVariantCatalog();
|
|
@@ -3277,6 +3356,7 @@ function selectUsageTab(root, provider) {
|
|
|
3277
3356
|
for (const panel of root.querySelectorAll('[data-usage-panel]')) {
|
|
3278
3357
|
panel.hidden = panel.dataset.usagePanel !== target;
|
|
3279
3358
|
}
|
|
3359
|
+
revealSelectedTabs(root);
|
|
3280
3360
|
usageTabProviderOverride = target;
|
|
3281
3361
|
if (!usageCardIsFresh(target)) {
|
|
3282
3362
|
refreshUsageProvider(target).catch(() => {});
|
|
@@ -3336,6 +3416,10 @@ function renderUsageReport(report, account, { updating = '' } = {}) {
|
|
|
3336
3416
|
kind: 'usage',
|
|
3337
3417
|
});
|
|
3338
3418
|
if (updating) markUsageCardUpdating(updating, true);
|
|
3419
|
+
// A new strip each render: its selected tab in view, its fade marked.
|
|
3420
|
+
watchTabStripFades();
|
|
3421
|
+
const body = document.getElementById('summary-modal-body');
|
|
3422
|
+
if (body) revealSelectedTabs(body);
|
|
3339
3423
|
return true;
|
|
3340
3424
|
}
|
|
3341
3425
|
|
|
@@ -3820,11 +3904,15 @@ async function showUsage() {
|
|
|
3820
3904
|
// A fresh open follows the current session's provider again; only a click
|
|
3821
3905
|
// inside the modal pins a tab (for the Refresh button's sake).
|
|
3822
3906
|
usageTabProviderOverride = null;
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3907
|
+
// The header button and its ︙ twin both show the wait.
|
|
3908
|
+
const usageButtons = ['chat-menu-usage', 'usage-btn'].map((id) => document.getElementById(id)).filter(Boolean);
|
|
3909
|
+
const setUsageBusy = (busy) => {
|
|
3910
|
+
for (const button of usageButtons) {
|
|
3911
|
+
button.textContent = busy ? '⏳' : (button.id === 'chat-menu-usage' ? '📊 Check Usage' : '📊');
|
|
3912
|
+
button.disabled = busy;
|
|
3913
|
+
}
|
|
3914
|
+
};
|
|
3915
|
+
setUsageBusy(true);
|
|
3828
3916
|
// What was shown last time is shown again at once; only the open tab's
|
|
3829
3917
|
// provider is read live, and its card is swapped in when the answer comes.
|
|
3830
3918
|
const kept = lastUsageReport || readStoredUsageReport();
|
|
@@ -3838,10 +3926,7 @@ async function showUsage() {
|
|
|
3838
3926
|
});
|
|
3839
3927
|
try {
|
|
3840
3928
|
if (kept && renderUsageReport(kept, null, { updating: activeProvider })) {
|
|
3841
|
-
|
|
3842
|
-
btn.textContent = btn.id === 'chat-menu-usage' ? '📊 Check Usage' : '📊';
|
|
3843
|
-
btn.disabled = false;
|
|
3844
|
-
}
|
|
3929
|
+
setUsageBusy(false);
|
|
3845
3930
|
await refreshUsageProvider(activeProvider);
|
|
3846
3931
|
return;
|
|
3847
3932
|
}
|
|
@@ -3856,10 +3941,7 @@ async function showUsage() {
|
|
|
3856
3941
|
kind: 'usage',
|
|
3857
3942
|
});
|
|
3858
3943
|
} finally {
|
|
3859
|
-
|
|
3860
|
-
btn.textContent = btn.id === 'chat-menu-usage' ? '📊 Check Usage' : '📊';
|
|
3861
|
-
btn.disabled = false;
|
|
3862
|
-
}
|
|
3944
|
+
setUsageBusy(false);
|
|
3863
3945
|
}
|
|
3864
3946
|
}
|
|
3865
3947
|
|
|
@@ -3913,7 +3995,7 @@ function renderSessionInstructionDocs(docs) {
|
|
|
3913
3995
|
return '<div class="summary-loading">No session instruction files were found.</div>';
|
|
3914
3996
|
}
|
|
3915
3997
|
|
|
3916
|
-
return `<div style="display:flex;flex-direction:column;gap:
|
|
3998
|
+
return `<div style="display:flex;flex-direction:column;gap:calc(6px * var(--ui-scale))">${
|
|
3917
3999
|
items.map((doc) => {
|
|
3918
4000
|
const title = String(doc?.title || doc?.sessionId || 'Session instructions').trim();
|
|
3919
4001
|
const name = String(doc?.name || '').trim();
|
|
@@ -3923,13 +4005,13 @@ function renderSessionInstructionDocs(docs) {
|
|
|
3923
4005
|
const updatedAt = String(doc?.updatedAt || '').trim();
|
|
3924
4006
|
const content = escHtml(String(doc?.content || '').trim());
|
|
3925
4007
|
return `
|
|
3926
|
-
<details open style="border:1px solid var(--border);border-radius:10px;background:var(--bg3);padding:
|
|
4008
|
+
<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
4009
|
<summary style="cursor:pointer;font-weight:600;outline:none">${escHtml(title)}</summary>
|
|
3928
|
-
<div style="margin-top:
|
|
4010
|
+
<div style="margin-top:calc(3px * var(--ui-scale));font-size:0.78rem;color:var(--muted)">
|
|
3929
4011
|
${escHtml(summary || sessionId)}
|
|
3930
4012
|
${updatedAt ? ` · ${escHtml(updatedAt)}` : ''}
|
|
3931
4013
|
</div>
|
|
3932
|
-
<pre style="margin-top:
|
|
4014
|
+
<pre style="margin-top:calc(5px * var(--ui-scale));white-space:pre-wrap;word-break:break-word">${content}</pre>
|
|
3933
4015
|
</details>
|
|
3934
4016
|
`;
|
|
3935
4017
|
}).join('')
|
|
@@ -4501,12 +4583,321 @@ function applyChatActionsMenuState(state) {
|
|
|
4501
4583
|
}
|
|
4502
4584
|
}
|
|
4503
4585
|
|
|
4504
|
-
//
|
|
4586
|
+
// ── Priority+ header actions ──
|
|
4587
|
+
// Pins, hidden, search, context, Check Usage, Day/Night and fullscreen sit in
|
|
4588
|
+
// the header while it has room and fall back into ︙ in reverse priority
|
|
4589
|
+
// order (header-priority-actions.mjs decides; this measures and applies).
|
|
4590
|
+
// The twin ids the last fit put into ︙; null before the header was measured,
|
|
4591
|
+
// which keeps the menu's old reach (Day/Night, Check Usage).
|
|
4592
|
+
let headerPriorityMenuIds = null;
|
|
4593
|
+
// The inputs the last full fit measured with (headerFitSignature); a pass
|
|
4594
|
+
// whose inputs are the same keeps the placement and measures nothing.
|
|
4595
|
+
let headerPriorityFitSignature = '';
|
|
4596
|
+
|
|
4597
|
+
function headerPriorityActionAvailability() {
|
|
4598
|
+
const pins = document.getElementById('pinned-messages-btn');
|
|
4599
|
+
const hiddenBtn = document.getElementById('hidden-messages-btn');
|
|
4600
|
+
const fullscreen = document.getElementById('fullscreen-btn');
|
|
4601
|
+
return resolveHeaderActionAvailability({
|
|
4602
|
+
sharedMode: isSharedReaderMode(),
|
|
4603
|
+
pinCount: Number(pins?.dataset.count || 0),
|
|
4604
|
+
hiddenCount: Number(hiddenBtn?.dataset.count || 0),
|
|
4605
|
+
fullscreenAvailable: fullscreen?.dataset.headerAvailable === '1',
|
|
4606
|
+
});
|
|
4607
|
+
}
|
|
4608
|
+
|
|
4609
|
+
function cssPixels(value) {
|
|
4610
|
+
const number = parseFloat(value);
|
|
4611
|
+
return Number.isFinite(number) ? number : 0;
|
|
4612
|
+
}
|
|
4613
|
+
|
|
4614
|
+
function isHeaderBannerOnOwnRow() {
|
|
4615
|
+
return typeof window.matchMedia === 'function' && window.matchMedia(HEADER_BANNER_OWN_ROW_QUERY).matches;
|
|
4616
|
+
}
|
|
4617
|
+
|
|
4618
|
+
// The question banner's two widths, measured with its compact chip shown
|
|
4619
|
+
// and then its full text: the chip ("❓ N", what the fit reserves) and the
|
|
4620
|
+
// least the full text needs ("❓ N open questions" plus padding and border;
|
|
4621
|
+
// the "latest in …" tail may end in an ellipsis). Zero when it does not show.
|
|
4622
|
+
// Leaves the banner in its full form; the caller sets the mode.
|
|
4623
|
+
function measureQuestionBanner(banner) {
|
|
4624
|
+
if (!banner) return { visible: false, compact: 0, fullMin: 0 };
|
|
4625
|
+
banner.removeAttribute('data-compact');
|
|
4626
|
+
if (!banner.offsetWidth) return { visible: false, compact: 0, fullMin: 0 };
|
|
4627
|
+
const count = banner.querySelector('.pending-question-count');
|
|
4628
|
+
const style = getComputedStyle(banner);
|
|
4629
|
+
const frame = cssPixels(style.paddingLeft) + cssPixels(style.paddingRight)
|
|
4630
|
+
+ cssPixels(style.borderLeftWidth) + cssPixels(style.borderRightWidth);
|
|
4631
|
+
// Rounded up: a fraction short would cut the last letter.
|
|
4632
|
+
const fullMin = Math.ceil((count ? count.getBoundingClientRect().width : banner.scrollWidth) + frame);
|
|
4633
|
+
banner.setAttribute('data-compact', '');
|
|
4634
|
+
const compact = Math.ceil(banner.getBoundingClientRect().width);
|
|
4635
|
+
banner.removeAttribute('data-compact');
|
|
4636
|
+
return { visible: true, compact, fullMin };
|
|
4637
|
+
}
|
|
4638
|
+
|
|
4639
|
+
// The width of everything on the header's first row that never overflows
|
|
4640
|
+
// (headerReservedWidth): its padding, ☰, the title's minimum, ︙, the
|
|
4641
|
+
// question banner's compact chip while it shares that row, and the flex gaps
|
|
4642
|
+
// between them. Read on every full pass: all of it moves with --font-scale.
|
|
4643
|
+
function headerFixedWidth(header, bannerCompactWidth) {
|
|
4644
|
+
const style = getComputedStyle(header);
|
|
4645
|
+
const rootFont = cssPixels(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
4646
|
+
return headerReservedWidth({
|
|
4647
|
+
padding: cssPixels(style.paddingLeft) + cssPixels(style.paddingRight),
|
|
4648
|
+
titleMin: HEADER_TITLE_MIN_REM * rootFont,
|
|
4649
|
+
toggle: document.getElementById('sidebar-toggle')?.offsetWidth || 0,
|
|
4650
|
+
banner: bannerCompactWidth,
|
|
4651
|
+
menu: document.getElementById('chat-actions-menu-wrap')?.offsetWidth || 0,
|
|
4652
|
+
gap: cssPixels(style.columnGap || style.gap),
|
|
4653
|
+
// On a phone the banner has a row of its own (the ≤680px header CSS).
|
|
4654
|
+
bannerOwnRow: isHeaderBannerOnOwnRow(),
|
|
4655
|
+
});
|
|
4656
|
+
}
|
|
4657
|
+
|
|
4658
|
+
// Everything the fit depends on, in one string: the header's width, the
|
|
4659
|
+
// title, the banner, the counts and availability, the theme (the glyphs'
|
|
4660
|
+
// widths), the scale and the layout. Cheap to read: no element is shown or
|
|
4661
|
+
// hidden for it.
|
|
4662
|
+
function headerFitSignature(header, availability) {
|
|
4663
|
+
const root = document.documentElement;
|
|
4664
|
+
const rootStyle = getComputedStyle(root);
|
|
4665
|
+
const title = document.getElementById('chat-title');
|
|
4666
|
+
const banner = document.getElementById('pending-question-banner');
|
|
4667
|
+
return JSON.stringify([
|
|
4668
|
+
header.clientWidth,
|
|
4669
|
+
rootStyle.fontSize,
|
|
4670
|
+
rootStyle.getPropertyValue('--ui-scale'),
|
|
4671
|
+
root.getAttribute('data-theme'),
|
|
4672
|
+
root.hasAttribute(PHONE_LANDSCAPE_ATTRIBUTE),
|
|
4673
|
+
isHeaderBannerOnOwnRow(),
|
|
4674
|
+
document.body.classList.contains('chat-title-editing'),
|
|
4675
|
+
title?.hidden, title?.textContent,
|
|
4676
|
+
banner?.className, banner?.hidden, banner?.textContent,
|
|
4677
|
+
document.getElementById('sidebar-toggle')?.hidden,
|
|
4678
|
+
document.getElementById('chat-actions-menu-btn')?.hidden,
|
|
4679
|
+
document.getElementById('pinned-messages-btn')?.dataset.count,
|
|
4680
|
+
document.getElementById('hidden-messages-btn')?.dataset.count,
|
|
4681
|
+
document.getElementById('fullscreen-btn')?.textContent,
|
|
4682
|
+
document.getElementById('theme-toggle-btn')?.textContent,
|
|
4683
|
+
availability,
|
|
4684
|
+
]);
|
|
4685
|
+
}
|
|
4686
|
+
|
|
4687
|
+
// The last placement, minus whatever is no longer available. Measures nothing.
|
|
4688
|
+
function keepHeaderPriorityPlacement(availability) {
|
|
4689
|
+
for (const action of HEADER_PRIORITY_ACTIONS) {
|
|
4690
|
+
const inline = document.getElementById(action.inlineId);
|
|
4691
|
+
if (inline && !availability[action.key]) inline.hidden = true;
|
|
4692
|
+
}
|
|
4693
|
+
if (Array.isArray(headerPriorityMenuIds)) {
|
|
4694
|
+
const availableMenuIds = new Set(headerPriorityMenuIdsFor(Object.keys(availability).filter((key) => availability[key])));
|
|
4695
|
+
headerPriorityMenuIds = headerPriorityMenuIds.filter((id) => availableMenuIds.has(id));
|
|
4696
|
+
}
|
|
4697
|
+
return headerPriorityMenuIds;
|
|
4698
|
+
}
|
|
4699
|
+
|
|
4700
|
+
// Shows inline what fits and hides the rest; returns the twin ids for ︙.
|
|
4701
|
+
// A header that cannot be measured (hidden, not laid out) keeps the last
|
|
4702
|
+
// placement, minus whatever is no longer available. Unless `force`, a pass
|
|
4703
|
+
// whose inputs are those of the last one keeps its placement too: the hide
|
|
4704
|
+
// and measure below runs only when something it reads has changed.
|
|
4705
|
+
function syncHeaderPriorityFit({ force = false } = {}) {
|
|
4706
|
+
const availability = headerPriorityActionAvailability();
|
|
4707
|
+
const header = document.getElementById('chat-header');
|
|
4708
|
+
const actions = document.getElementById('chat-header-actions');
|
|
4709
|
+
const measurable = !!(header && actions && header.clientWidth && typeof getComputedStyle === 'function');
|
|
4710
|
+
if (!measurable) return keepHeaderPriorityPlacement(availability);
|
|
4711
|
+
const signature = headerFitSignature(header, availability);
|
|
4712
|
+
if (!force && signature === headerPriorityFitSignature && Array.isArray(headerPriorityMenuIds)) {
|
|
4713
|
+
return keepHeaderPriorityPlacement(availability);
|
|
4714
|
+
}
|
|
4715
|
+
headerPriorityFitSignature = signature;
|
|
4716
|
+
// Un-hide every available button before measuring: a hidden one is 0 wide.
|
|
4717
|
+
// All of this runs in one task, so nothing is painted in between.
|
|
4718
|
+
const widths = {};
|
|
4719
|
+
for (const action of HEADER_PRIORITY_ACTIONS) {
|
|
4720
|
+
const inline = document.getElementById(action.inlineId);
|
|
4721
|
+
if (!inline) continue;
|
|
4722
|
+
inline.hidden = !availability[action.key];
|
|
4723
|
+
if (availability[action.key]) widths[action.key] = inline.offsetWidth;
|
|
4724
|
+
}
|
|
4725
|
+
const gap = cssPixels(getComputedStyle(actions).columnGap || getComputedStyle(actions).gap);
|
|
4726
|
+
// The question banner sits between the title and the actions. The fit
|
|
4727
|
+
// reserves its compact chip only, so an action goes into ︙ before the
|
|
4728
|
+
// banner could push it; then the banner takes what the row has left.
|
|
4729
|
+
const banner = document.getElementById('pending-question-banner');
|
|
4730
|
+
const phoneOwnRow = isHeaderBannerOnOwnRow();
|
|
4731
|
+
const phoneLandscape = document.documentElement.hasAttribute(PHONE_LANDSCAPE_ATTRIBUTE);
|
|
4732
|
+
const bannerSize = measureQuestionBanner(banner);
|
|
4733
|
+
const reserved = headerFixedWidth(header, phoneOwnRow ? 0 : bannerSize.compact);
|
|
4734
|
+
// Where ☰, the title's minimum, the chip and ︙ alone overflow the first
|
|
4735
|
+
// row (a narrow #main at a large zoom), the banner takes a row of its own,
|
|
4736
|
+
// as on a phone, and the fit no longer reserves it.
|
|
4737
|
+
const ownRow = phoneOwnRow || resolveQuestionBannerMode({
|
|
4738
|
+
visible: bannerSize.visible,
|
|
4739
|
+
phoneLandscape,
|
|
4740
|
+
room: header.clientWidth - reserved + bannerSize.compact,
|
|
4741
|
+
compact: bannerSize.compact,
|
|
4742
|
+
}) === 'row';
|
|
4743
|
+
const available = header.clientWidth - (ownRow ? headerFixedWidth(header, 0) : reserved);
|
|
4744
|
+
const fit = fitHeaderPriorityActions({
|
|
4745
|
+
widths,
|
|
4746
|
+
gap,
|
|
4747
|
+
available,
|
|
4748
|
+
availability,
|
|
4749
|
+
});
|
|
4750
|
+
const inlineKeys = new Set(fit.inline);
|
|
4751
|
+
for (const action of HEADER_PRIORITY_ACTIONS) {
|
|
4752
|
+
const inline = document.getElementById(action.inlineId);
|
|
4753
|
+
if (inline) inline.hidden = !inlineKeys.has(action.key);
|
|
4754
|
+
}
|
|
4755
|
+
if (banner) {
|
|
4756
|
+
const headerStyle = getComputedStyle(header);
|
|
4757
|
+
const mode = resolveQuestionBannerMode({
|
|
4758
|
+
visible: bannerSize.visible,
|
|
4759
|
+
ownRow,
|
|
4760
|
+
phoneLandscape,
|
|
4761
|
+
room: available - fit.used + bannerSize.compact,
|
|
4762
|
+
compact: bannerSize.compact,
|
|
4763
|
+
contentWidth: header.clientWidth - cssPixels(headerStyle.paddingLeft) - cssPixels(headerStyle.paddingRight),
|
|
4764
|
+
fullMin: bannerSize.fullMin,
|
|
4765
|
+
});
|
|
4766
|
+
banner.toggleAttribute('data-compact', mode === 'compact');
|
|
4767
|
+
// The phone's header CSS places the banner on its own row by itself; a
|
|
4768
|
+
// wider header needs this attribute for it.
|
|
4769
|
+
banner.toggleAttribute('data-own-row', mode === 'row' && !phoneOwnRow);
|
|
4770
|
+
}
|
|
4771
|
+
headerPriorityMenuIds = headerPriorityMenuIdsFor(fit.menu);
|
|
4772
|
+
// Measured at least once (the e2e helpers wait for it).
|
|
4773
|
+
actions.dataset.headerFit = '1';
|
|
4774
|
+
return headerPriorityMenuIds;
|
|
4775
|
+
}
|
|
4776
|
+
|
|
4777
|
+
// The pins and hidden twins carry the count their header button shows.
|
|
4778
|
+
function syncHeaderPriorityTwinLabels() {
|
|
4779
|
+
const label = (buttonId, twinId, text, icon) => {
|
|
4780
|
+
const twin = document.getElementById(twinId);
|
|
4781
|
+
if (!twin) return;
|
|
4782
|
+
const count = Number(document.getElementById(buttonId)?.dataset.count || 0);
|
|
4783
|
+
twin.textContent = count ? `${icon} ${text} (${count})` : `${icon} ${text}`;
|
|
4784
|
+
};
|
|
4785
|
+
label('pinned-messages-btn', 'chat-menu-pins', 'Pinned messages', '📍');
|
|
4786
|
+
label('hidden-messages-btn', 'chat-menu-hidden', 'Hidden from shared viewers', '🙈');
|
|
4787
|
+
}
|
|
4788
|
+
|
|
4789
|
+
// At most one fit per frame. A pass asked for by an observer of one of the
|
|
4790
|
+
// fit's inputs (`force`) measures; one asked for by a header sync (every list
|
|
4791
|
+
// paint ends in one) measures only if headerFitSignature says something
|
|
4792
|
+
// changed. Either way it runs before the next paint.
|
|
4793
|
+
let headerPriorityFrame = 0;
|
|
4794
|
+
let headerPriorityForce = false;
|
|
4795
|
+
function scheduleHeaderPrioritySync({ force = false } = {}) {
|
|
4796
|
+
if (force) headerPriorityForce = true;
|
|
4797
|
+
if (headerPriorityFrame) return;
|
|
4798
|
+
headerPriorityFrame = requestAnimationFrame(() => {
|
|
4799
|
+
headerPriorityFrame = 0;
|
|
4800
|
+
const forced = headerPriorityForce;
|
|
4801
|
+
headerPriorityForce = false;
|
|
4802
|
+
syncChatActionsMenuState({ fit: forced ? 'force' : 'now' });
|
|
4803
|
+
// The header moved or changed size: the open menu follows it.
|
|
4804
|
+
if (forced) placeChatActionsMenu();
|
|
4805
|
+
});
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
// Every reason the fit can change: the header's own box (window, drawer,
|
|
4809
|
+
// font scale), the title, the banner, the theme (the glyph's width), a view's
|
|
4810
|
+
// counts or fullscreen verdict (HEADER_ACTIONS_CHANGE_EVENT), the font scale
|
|
4811
|
+
// and the phone turning. Conversation switches reach it through
|
|
4812
|
+
// syncChatTitleControls.
|
|
4813
|
+
function initHeaderPriorityActions({ sharedMode = false } = {}) {
|
|
4814
|
+
if (!sharedMode) {
|
|
4815
|
+
bindHeaderPriorityAction('pins', () => openPinnedMessagesModal(), { inline: false });
|
|
4816
|
+
bindHeaderPriorityAction('hidden', () => openHiddenMessagesModal(), { inline: false });
|
|
4817
|
+
bindHeaderPriorityAction('search', () => openMessageSearchModal(), { inline: false });
|
|
4818
|
+
bindHeaderPriorityAction('context', () => { void showContext(); }, { inline: false });
|
|
4819
|
+
bindHeaderPriorityAction('usage', () => {
|
|
4820
|
+
showUsage().catch((error) => {
|
|
4821
|
+
alert(error?.message || 'Failed to load usage');
|
|
4822
|
+
});
|
|
4823
|
+
});
|
|
4824
|
+
bindHeaderPriorityAction('fullscreen', () => { void toggleFullscreen(); }, { inline: false });
|
|
4825
|
+
}
|
|
4826
|
+
if (window.__headerPriorityObserversBound) return;
|
|
4827
|
+
window.__headerPriorityObserversBound = true;
|
|
4828
|
+
const forced = () => scheduleHeaderPrioritySync({ force: true });
|
|
4829
|
+
const header = document.getElementById('chat-header');
|
|
4830
|
+
if (header && typeof ResizeObserver === 'function') {
|
|
4831
|
+
new ResizeObserver(forced).observe(header);
|
|
4832
|
+
} else {
|
|
4833
|
+
window.addEventListener('resize', forced);
|
|
4834
|
+
}
|
|
4835
|
+
// The open ︙ menu stays inside the window (a turn, the keyboard).
|
|
4836
|
+
window.addEventListener('resize', () => placeChatActionsMenu());
|
|
4837
|
+
if (typeof MutationObserver === 'function') {
|
|
4838
|
+
new MutationObserver(forced)
|
|
4839
|
+
.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
|
4840
|
+
const title = document.getElementById('chat-title');
|
|
4841
|
+
if (title) {
|
|
4842
|
+
new MutationObserver(forced)
|
|
4843
|
+
.observe(title, { childList: true, characterData: true, subtree: true });
|
|
4844
|
+
}
|
|
4845
|
+
// The pending-question banner shares the first row on a wide screen: it
|
|
4846
|
+
// takes room when it shows (on a phone it has a row of its own).
|
|
4847
|
+
const banner = document.getElementById('pending-question-banner');
|
|
4848
|
+
if (banner) {
|
|
4849
|
+
new MutationObserver(forced)
|
|
4850
|
+
.observe(banner, { attributes: true, attributeFilter: ['class', 'hidden'], childList: true, characterData: true, subtree: true });
|
|
4851
|
+
}
|
|
4852
|
+
}
|
|
4853
|
+
window.addEventListener(HEADER_ACTIONS_CHANGE_EVENT, forced);
|
|
4854
|
+
window.addEventListener(FONT_SCALE_CHANGE_EVENT, forced);
|
|
4855
|
+
window.addEventListener(PHONE_LANDSCAPE_CHANGE_EVENT, forced);
|
|
4856
|
+
document.addEventListener('fullscreenchange', forced);
|
|
4857
|
+
syncChatActionsMenuState({ fit: 'force' });
|
|
4858
|
+
}
|
|
4859
|
+
|
|
4860
|
+
// The meta line's chips the conversation on screen has, for their ︙
|
|
4861
|
+
// stand-ins in phone landscape (where that line is hidden), with the labels.
|
|
4862
|
+
function chatActionsMenuMetaChips(convId) {
|
|
4863
|
+
if (!convId || isSharedReaderMode()) return { chips: {}, labels: null };
|
|
4864
|
+
const watching = relayWatchingChipText(convId);
|
|
4865
|
+
const schedules = relayScheduleChipText(convId);
|
|
4866
|
+
const origin = conversationOriginMenuEntry();
|
|
4867
|
+
return {
|
|
4868
|
+
chips: { watching: !!watching, schedules: !!schedules, origin: !!origin },
|
|
4869
|
+
labels: chatActionsMenuMetaChipLabels({ watching, origin: origin?.label || '' }),
|
|
4870
|
+
};
|
|
4871
|
+
}
|
|
4872
|
+
|
|
4873
|
+
function syncChatActionsMenuMetaChipLabels(labels) {
|
|
4874
|
+
if (!labels) return;
|
|
4875
|
+
for (const [chip, id] of Object.entries(CHAT_ACTIONS_MENU_META_CHIP_IDS)) {
|
|
4876
|
+
const entry = document.getElementById(id);
|
|
4877
|
+
if (entry && labels[chip] && entry.textContent !== labels[chip]) entry.textContent = labels[chip];
|
|
4878
|
+
}
|
|
4879
|
+
}
|
|
4880
|
+
|
|
4881
|
+
// The ︙ menu as it should look right now: the trigger and every entry,
|
|
4505
4882
|
// decided in chat-actions-menu-state.mjs from the conversation on screen.
|
|
4506
|
-
|
|
4883
|
+
// `fit`: 'schedule' (the default) keeps the twins the last fit chose and asks
|
|
4884
|
+
// for a fit before the next paint; 'now' fits first if any of its inputs
|
|
4885
|
+
// changed; 'force' fits first whatever.
|
|
4886
|
+
function syncChatActionsMenuState({ fit = 'schedule' } = {}) {
|
|
4507
4887
|
const convId = String(currentConvId || '').trim();
|
|
4508
4888
|
const conversation = convId ? (conversations[convId] || null) : null;
|
|
4509
4889
|
const isCloud = !!convId && isCurrentConversationClaudeCloud();
|
|
4890
|
+
let headerMenuIds;
|
|
4891
|
+
if (fit === 'now' || fit === 'force') {
|
|
4892
|
+
headerMenuIds = syncHeaderPriorityFit({ force: fit === 'force' });
|
|
4893
|
+
} else {
|
|
4894
|
+
headerMenuIds = keepHeaderPriorityPlacement(headerPriorityActionAvailability());
|
|
4895
|
+
scheduleHeaderPrioritySync();
|
|
4896
|
+
}
|
|
4897
|
+
syncHeaderPriorityTwinLabels();
|
|
4898
|
+
const metaChips = chatActionsMenuMetaChips(convId);
|
|
4899
|
+
syncChatActionsMenuMetaChipLabels(metaChips.labels);
|
|
4900
|
+
const fullscreenBtn = document.getElementById('fullscreen-btn');
|
|
4510
4901
|
const state = resolveChatActionsMenuState({
|
|
4511
4902
|
sharedMode: isSharedReaderMode(),
|
|
4512
4903
|
conversationId: convId,
|
|
@@ -4517,20 +4908,60 @@ function syncChatActionsMenuState() {
|
|
|
4517
4908
|
editing: !!convId && chatTitleEditingConversationId === convId,
|
|
4518
4909
|
suspendVisible: isSuspendHostActionVisible(),
|
|
4519
4910
|
hasActiveShare: hasActiveShare(convId),
|
|
4911
|
+
headerMenuIds,
|
|
4912
|
+
// As the header button: disabled where the browser cannot go fullscreen.
|
|
4913
|
+
fullscreenSupported: !fullscreenBtn?.disabled,
|
|
4914
|
+
phoneLandscape: document.documentElement.hasAttribute(PHONE_LANDSCAPE_ATTRIBUTE),
|
|
4915
|
+
metaChips: metaChips.chips,
|
|
4520
4916
|
});
|
|
4917
|
+
const trigger = document.getElementById('chat-actions-menu-btn');
|
|
4918
|
+
const triggerWasHidden = !!trigger?.hidden;
|
|
4521
4919
|
applyChatActionsMenuState(state);
|
|
4920
|
+
// ︙ coming or going changes the room the fit has (the shared view's menu
|
|
4921
|
+
// empties once Day/Night fits): one more pass settles it.
|
|
4922
|
+
if (trigger && !!trigger.hidden !== triggerWasHidden) scheduleHeaderPrioritySync({ force: true });
|
|
4522
4923
|
return state;
|
|
4523
4924
|
}
|
|
4524
4925
|
|
|
4525
4926
|
// The menu follows the relay's share list (made, renewed, stopped anywhere).
|
|
4526
4927
|
subscribeShares(() => syncChatActionsMenuState());
|
|
4527
4928
|
|
|
4929
|
+
// Keeps the open ︙ menu inside the viewport and #main (never off-screen,
|
|
4930
|
+
// never under the sidebar) and caps its height at what is left below it,
|
|
4931
|
+
// where it scrolls (resolveChatActionsMenuPlacement decides).
|
|
4932
|
+
function placeChatActionsMenu() {
|
|
4933
|
+
const menu = document.getElementById('chat-actions-menu');
|
|
4934
|
+
if (!menu || menu.hidden) return;
|
|
4935
|
+
// Measured as CSS places it; where the reader scrolled to stays.
|
|
4936
|
+
const scrollTop = menu.scrollTop;
|
|
4937
|
+
menu.style.right = '';
|
|
4938
|
+
menu.style.maxWidth = '';
|
|
4939
|
+
menu.style.maxHeight = '';
|
|
4940
|
+
const rect = menu.getBoundingClientRect();
|
|
4941
|
+
if (!rect.width && !rect.height) return;
|
|
4942
|
+
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
4943
|
+
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
4944
|
+
const main = document.getElementById('main')?.getBoundingClientRect?.();
|
|
4945
|
+
const bounds = {
|
|
4946
|
+
left: Math.max(0, main && main.width ? main.left : 0),
|
|
4947
|
+
right: Math.min(viewportWidth, main && main.width ? main.right : viewportWidth),
|
|
4948
|
+
bottom: Math.min(viewportHeight, main && main.height ? main.bottom : viewportHeight),
|
|
4949
|
+
};
|
|
4950
|
+
// 8px from the edges and a 120px floor at 100 %, scaled with the zoom.
|
|
4951
|
+
const scale = getUiScale();
|
|
4952
|
+
const placement = resolveChatActionsMenuPlacement({ rect, bounds, margin: 8 * scale, minHeight: 120 * scale });
|
|
4953
|
+
if (placement.right) menu.style.right = `${placement.right}px`;
|
|
4954
|
+
if (placement.maxWidth !== null) menu.style.maxWidth = `${placement.maxWidth}px`;
|
|
4955
|
+
menu.style.maxHeight = `${placement.maxHeight}px`;
|
|
4956
|
+
if (scrollTop) menu.scrollTop = scrollTop;
|
|
4957
|
+
}
|
|
4958
|
+
|
|
4528
4959
|
function toggleChatActionsMenu() {
|
|
4529
4960
|
const menu = document.getElementById('chat-actions-menu');
|
|
4530
4961
|
const trigger = document.getElementById('chat-actions-menu-btn');
|
|
4531
4962
|
if (!menu || !trigger) return;
|
|
4532
4963
|
// The entries match the moment of the tap, as Refresh history already does.
|
|
4533
|
-
syncChatActionsMenuState();
|
|
4964
|
+
syncChatActionsMenuState({ fit: 'now' });
|
|
4534
4965
|
if (trigger.hidden || trigger.disabled) return;
|
|
4535
4966
|
syncRefreshHistoryMenuState();
|
|
4536
4967
|
syncThemeMenuLabel();
|
|
@@ -4538,6 +4969,10 @@ function toggleChatActionsMenu() {
|
|
|
4538
4969
|
const willOpen = !!menu.hidden;
|
|
4539
4970
|
menu.hidden = !willOpen;
|
|
4540
4971
|
trigger.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
|
|
4972
|
+
if (willOpen) {
|
|
4973
|
+
placeChatActionsMenu();
|
|
4974
|
+
menu.scrollTop = 0;
|
|
4975
|
+
}
|
|
4541
4976
|
}
|
|
4542
4977
|
|
|
4543
4978
|
function isConversationLocallyProcessing(conversation) {
|
|
@@ -4644,12 +5079,14 @@ function syncChatTitleControls() {
|
|
|
4644
5079
|
const editing = convId && chatTitleEditingConversationId === convId;
|
|
4645
5080
|
document.body.classList.toggle('chat-title-editing', !!editing);
|
|
4646
5081
|
if (title) title.hidden = editing;
|
|
4647
|
-
// The
|
|
4648
|
-
// open, so Settings is always one tap away.
|
|
5082
|
+
// The ︙ trigger and its entries: relay-wide ones stay with no conversation
|
|
5083
|
+
// open, so Settings is always one tap away. The header's fit is only asked
|
|
5084
|
+
// for here (once per frame at most, and it measures only when its inputs
|
|
5085
|
+
// changed): every list paint ends in this call.
|
|
4649
5086
|
syncChatActionsMenuState();
|
|
4650
5087
|
const archiveBtn = document.getElementById('chat-menu-archive');
|
|
4651
5088
|
if (archiveBtn) {
|
|
4652
|
-
archiveBtn.textContent = conversation?.archived === true ? '📂 Unarchive conversation' : '
|
|
5089
|
+
archiveBtn.textContent = conversation?.archived === true ? '📂 Unarchive conversation' : '🗄️ Archive conversation';
|
|
4653
5090
|
}
|
|
4654
5091
|
if (editor) {
|
|
4655
5092
|
editor.hidden = !editing;
|
|
@@ -4731,6 +5168,7 @@ function applyConversationTitleUpdate(conversationId, title, updatedAt) {
|
|
|
4731
5168
|
if (currentConvId === id) {
|
|
4732
5169
|
const titleEl = document.getElementById('chat-title');
|
|
4733
5170
|
if (titleEl) titleEl.textContent = nextTitle;
|
|
5171
|
+
syncSessionDocumentTitle();
|
|
4734
5172
|
}
|
|
4735
5173
|
renderConvList();
|
|
4736
5174
|
}
|
|
@@ -4907,6 +5345,11 @@ async function initApp() {
|
|
|
4907
5345
|
renderConvList();
|
|
4908
5346
|
},
|
|
4909
5347
|
});
|
|
5348
|
+
// The ⚙ panel and the label row above the message box.
|
|
5349
|
+
initComposerSettings({ isSharedReaderMode, showContext: () => showContext() });
|
|
5350
|
+
// A phone turned sideways: its typing mode folds the label row and the
|
|
5351
|
+
// task line into floating buttons while the keyboard is up.
|
|
5352
|
+
initPhoneLandscape({ isSharedReaderMode });
|
|
4910
5353
|
}
|
|
4911
5354
|
setupViewportTracking();
|
|
4912
5355
|
bindChatSelectionGuard();
|
|
@@ -4938,24 +5381,37 @@ async function initApp() {
|
|
|
4938
5381
|
event.stopPropagation();
|
|
4939
5382
|
toggleChatActionsMenu();
|
|
4940
5383
|
});
|
|
5384
|
+
// Header buttons and their ︙ twins (Check Usage among them), and the
|
|
5385
|
+
// passes that decide which of them fit.
|
|
5386
|
+
initHeaderPriorityActions({ sharedMode });
|
|
4941
5387
|
if (!sharedMode) {
|
|
4942
5388
|
const chatMenuEditBtn = document.getElementById('chat-menu-edit-title');
|
|
4943
|
-
|
|
4944
|
-
bindMenuAction(chatMenuUsageBtn, (event) => {
|
|
5389
|
+
bindMenuAction(chatMenuEditBtn, (event) => {
|
|
4945
5390
|
event.preventDefault();
|
|
4946
5391
|
event.stopPropagation();
|
|
4947
5392
|
lockChatActionsMenuShield(350);
|
|
4948
5393
|
closeChatActionsMenu();
|
|
4949
|
-
|
|
4950
|
-
alert(error?.message || 'Failed to load usage');
|
|
4951
|
-
});
|
|
5394
|
+
openChatTitleEditor();
|
|
4952
5395
|
});
|
|
4953
|
-
|
|
5396
|
+
// Phone landscape hides the meta line: these stand in for its chips and
|
|
5397
|
+
// do what a tap on the chip does. The lists open under ︙, the chip
|
|
5398
|
+
// having no box to hang from.
|
|
5399
|
+
const metaChipAction = (id, run) => bindMenuAction(document.getElementById(id), (event) => {
|
|
4954
5400
|
event.preventDefault();
|
|
4955
5401
|
event.stopPropagation();
|
|
4956
5402
|
lockChatActionsMenuShield(350);
|
|
4957
5403
|
closeChatActionsMenu();
|
|
4958
|
-
|
|
5404
|
+
run();
|
|
5405
|
+
});
|
|
5406
|
+
metaChipAction(CHAT_ACTIONS_MENU_META_CHIP_IDS.watching, () => {
|
|
5407
|
+
openRelayWatchPanel({ anchor: document.getElementById('chat-actions-menu-btn') });
|
|
5408
|
+
});
|
|
5409
|
+
metaChipAction(CHAT_ACTIONS_MENU_META_CHIP_IDS.schedules, () => {
|
|
5410
|
+
openRelaySchedulePanel({ anchor: document.getElementById('chat-actions-menu-btn') });
|
|
5411
|
+
});
|
|
5412
|
+
metaChipAction(CHAT_ACTIONS_MENU_META_CHIP_IDS.origin, () => {
|
|
5413
|
+
const origin = conversationOriginMenuEntry();
|
|
5414
|
+
if (origin) void openOriginConversation(null, { dataset: { originConversationId: origin.conversationId } });
|
|
4959
5415
|
});
|
|
4960
5416
|
const chatMenuCompactBtn = document.getElementById('chat-menu-compact');
|
|
4961
5417
|
if (chatMenuCompactBtn && !chatMenuCompactBtn.hidden && !chatMenuCompactBtn.disabled) {
|
|
@@ -5267,7 +5723,9 @@ async function initApp() {
|
|
|
5267
5723
|
initPushNotificationClientHooks();
|
|
5268
5724
|
// Browsers without Background Sync replay the outbox from the page instead.
|
|
5269
5725
|
void initOutboxFallbackReplay();
|
|
5270
|
-
|
|
5726
|
+
// Read before anything opens: the address then follows the open session.
|
|
5727
|
+
const pushConversationId = consumePushConversationDeepLink();
|
|
5728
|
+
await loadConversations({ openLast: !pushConversationId });
|
|
5271
5729
|
// The sidebar's "watched" badges follow the subscriptions.
|
|
5272
5730
|
onRelayEventSubscriptionsChanged(() => renderConvList());
|
|
5273
5731
|
void loadRelayEventSubscriptions();
|
|
@@ -5276,9 +5734,8 @@ async function initApp() {
|
|
|
5276
5734
|
void loadRelaySchedules();
|
|
5277
5735
|
// The Inbox's count, and the cards of questions that don't block.
|
|
5278
5736
|
startInbox();
|
|
5279
|
-
const pushConversationId = consumePushConversationDeepLink();
|
|
5280
5737
|
if (pushConversationId) {
|
|
5281
|
-
await openConversation(pushConversationId).catch(() => {});
|
|
5738
|
+
await openConversation(pushConversationId, { history: 'replace' }).catch(() => {});
|
|
5282
5739
|
}
|
|
5283
5740
|
await loadRelayQuestions(currentConvId);
|
|
5284
5741
|
await loadRelayBoards();
|
|
@@ -5333,6 +5790,31 @@ async function bootstrap() {
|
|
|
5333
5790
|
}
|
|
5334
5791
|
const urlToken = getTokenFromUrl();
|
|
5335
5792
|
if (urlToken) stripTokenFromUrl();
|
|
5793
|
+
// Back and forward walk the sessions opened, and back closes an open
|
|
5794
|
+
// overlay first (session-history.mjs).
|
|
5795
|
+
initSessionHistory({
|
|
5796
|
+
openConversation,
|
|
5797
|
+
openSidebar,
|
|
5798
|
+
closeSidebar,
|
|
5799
|
+
closeComposerSettings,
|
|
5800
|
+
closeChatActionsMenu,
|
|
5801
|
+
closeSummaryModal,
|
|
5802
|
+
closeSettingsModal,
|
|
5803
|
+
closeMessageSearchModal,
|
|
5804
|
+
closeRepoBrowser,
|
|
5805
|
+
closeNewConversationModelModal,
|
|
5806
|
+
// Leaving the Inbox goes back to the conversation it was opened from.
|
|
5807
|
+
// Opened from Status it stored none: `returnTo` is the session history stands on.
|
|
5808
|
+
closeInboxView: ({ returnTo = '' } = {}) => { if (isInboxViewActive()) void toggleInboxView({ returnTo }); },
|
|
5809
|
+
closeChangeCwdMenu: () => closeChangeCwdMenu({ focusTrigger: true }),
|
|
5810
|
+
closeGitChangesModal,
|
|
5811
|
+
closeGitDiffViewer,
|
|
5812
|
+
closeTmuxInspector: closeTmuxInspectorView,
|
|
5813
|
+
closeAnnotateEditor,
|
|
5814
|
+
closeRelayWatchPanel,
|
|
5815
|
+
closeRelaySchedulePanel,
|
|
5816
|
+
closeEmojiPicker,
|
|
5817
|
+
});
|
|
5336
5818
|
// In the installed app the system back button must never close the app;
|
|
5337
5819
|
// a tab keeps its own back button. After the token is gone from the
|
|
5338
5820
|
// address, so the entries the guard lays down carry the clean one.
|
|
@@ -5341,7 +5823,10 @@ async function bootstrap() {
|
|
|
5341
5823
|
enabled: isInstalledAppMode(),
|
|
5342
5824
|
target: window,
|
|
5343
5825
|
onFloor: () => showTransientRelayNotice('Press back again to close the app', 2500),
|
|
5826
|
+
entryAgain: sessionHistoryEntryAgain,
|
|
5344
5827
|
});
|
|
5828
|
+
// Armed after the guard: on a phone the session list lies above its floor.
|
|
5829
|
+
armSessionHistoryOnFirstInteraction(window);
|
|
5345
5830
|
const persistedToken = consumeLegacyPersistedAuthToken();
|
|
5346
5831
|
const bootstrapToken = String(urlToken || persistedToken || '').trim();
|
|
5347
5832
|
const existingSession = await verifyExistingSession(bootstrapToken);
|
|
@@ -5480,17 +5965,20 @@ window.deleteConv = deleteConv;
|
|
|
5480
5965
|
window.archiveConv = archiveConv;
|
|
5481
5966
|
window.openConversationGroupPicker = openConversationGroupPicker;
|
|
5482
5967
|
window.toggleConversationGroupFold = toggleConversationGroupFold;
|
|
5968
|
+
window.toggleAllConversationGroupFolds = toggleAllConversationGroupFolds;
|
|
5483
5969
|
window.unarchiveConv = unarchiveConv;
|
|
5484
5970
|
window.toggleArchivedConversations = toggleArchivedConversations;
|
|
5485
5971
|
window.refreshNewConversationCloudRepoList = refreshNewConversationCloudRepoList;
|
|
5486
5972
|
window.openConversation = openConversation;
|
|
5487
5973
|
window.openOriginConversation = openOriginConversation;
|
|
5488
5974
|
// The Inbox and Status share the transcript's place: opening one leaves the other.
|
|
5975
|
+
// Closed by its button, one opened from the other goes back to the session
|
|
5976
|
+
// history stands on.
|
|
5489
5977
|
window.toggleStatusView = () => {
|
|
5490
5978
|
leaveInboxView();
|
|
5491
|
-
return toggleStatusView();
|
|
5979
|
+
return toggleStatusView({ returnTo: sessionHistoryConversation() });
|
|
5492
5980
|
};
|
|
5493
|
-
window.toggleInboxView = toggleInboxView;
|
|
5981
|
+
window.toggleInboxView = () => toggleInboxView({ returnTo: sessionHistoryConversation() });
|
|
5494
5982
|
window.refreshConversations = refreshConversations;
|
|
5495
5983
|
window.renderConvList = renderConvList;
|
|
5496
5984
|
window.handleAttachmentInput = handleAttachmentInput;
|