@oar-sh/oar 0.9.6 → 0.9.8
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 +285 -0
- package/README.md +222 -40
- package/bin/oar.js +320 -18
- package/package.json +1 -1
- package/server/README.md +174 -7
- package/server/claude-cloud-worker/claude-cloud-event-normalizer.mjs +340 -0
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +1325 -0
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +155 -0
- package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +18 -13
- package/server/claude-worker/claude-session-process.mjs +225 -24
- package/server/claude-worker/claude-session-worker.mjs +6 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +28 -0
- package/server/db-schema.mjs +17 -0
- package/server/migrations/0006-workspace-root-settings.mjs +67 -0
- package/server/migrations/0007-claude-cloud.mjs +82 -0
- package/server/public/app/activity-replay-state.mjs +68 -5
- package/server/public/app/agent-sessions-settings-ui.js +197 -0
- package/server/public/app/agent-sessions-settings.mjs +134 -0
- package/server/public/app/api-client.js +149 -6
- package/server/public/app/attachments-view.js +20 -1
- package/server/public/app/attribution-control.mjs +46 -0
- package/server/public/app/bootstrap.js +468 -88
- package/server/public/app/claude-cloud-conversation-ui.js +110 -0
- package/server/public/app/claude-cloud-settings-ui.js +374 -0
- package/server/public/app/claude-cloud-ui.mjs +611 -0
- package/server/public/app/composer-control-state.mjs +11 -0
- package/server/public/app/context-menu.mjs +140 -0
- package/server/public/app/context-tier-options.mjs +4 -0
- package/server/public/app/context-usage-view.mjs +127 -8
- package/server/public/app/conversation-origin-ui.js +27 -0
- package/server/public/app/conversation-provider-indicator.mjs +22 -4
- package/server/public/app/conversation-view.js +145 -11
- package/server/public/app/cwd-picker.js +12 -0
- package/server/public/app/journal-view.js +698 -28
- package/server/public/app/math-dollars.mjs +89 -0
- package/server/public/app/model-selector-options.mjs +1 -1
- package/server/public/app/plan-usage-view.mjs +36 -8
- package/server/public/app/pwa-install.js +5 -0
- package/server/public/app/relay-error-ctas.mjs +16 -2
- package/server/public/app/remote-relay-origin-view.mjs +120 -4
- package/server/public/app/router.js +49 -12
- package/server/public/app/scrollbar-hover.mjs +78 -0
- package/server/public/app/settings-modal.js +37 -3
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/socket-handlers.js +53 -1
- package/server/public/app/store.js +26 -0
- package/server/public/app/transcript-separators.mjs +48 -11
- package/server/public/app-icon-192.png +0 -0
- package/server/public/app-icon-512.png +0 -0
- package/server/public/app-icon-light-192.png +0 -0
- package/server/public/app-icon-light-512.png +0 -0
- package/server/public/app-icon-light-maskable-512.png +0 -0
- package/server/public/app-icon-light.svg +4 -0
- package/server/public/app-icon-maskable-512.png +0 -0
- package/server/public/app-icon.svg +3 -3
- package/server/public/favicon.ico +0 -0
- package/server/public/index.html +564 -20
- package/server/public/manifest.webmanifest +9 -3
- package/server/public/sw.js +6 -1
- package/server/relay-tools.md +13 -1
- package/server/repositories/remote-relay-repository.mjs +42 -1
- package/server/repositories/session-repository.mjs +47 -1
- package/server/routes/claude-cloud-routes.mjs +140 -0
- package/server/routes/git-routes.mjs +37 -0
- package/server/routes/messages-routes.mjs +123 -16
- package/server/routes/remote-relay-routes.mjs +15 -1
- package/server/routes/sessions-routes.mjs +517 -74
- package/server/server-runtime.mjs +268 -33
- package/server/services/claude-account-usage-service.mjs +176 -0
- package/server/services/claude-cloud-repo-service.mjs +408 -0
- package/server/services/claude-cloud-session-service.mjs +326 -0
- package/server/services/claude-cloud-settings-service.mjs +269 -0
- package/server/services/claude-context-usage.mjs +51 -3
- package/server/services/git-remote-service.mjs +115 -0
- package/server/services/oar-cli-helpers.mjs +67 -1
- package/server/services/plan-usage-claude-cloud.mjs +211 -0
- package/server/services/plan-usage-claude.mjs +202 -40
- package/server/services/plan-usage-contract.mjs +39 -3
- package/server/services/plan-usage-service.mjs +82 -2
- package/server/services/provider-reasoning-effort.mjs +3 -0
- package/server/services/pwa-manifest-theme.mjs +30 -0
- package/server/services/remote-relay-caller-context.mjs +49 -10
- package/server/services/remote-relay-client.mjs +9 -0
- package/server/services/remote-relay-dispatcher.mjs +383 -35
- package/server/services/remote-relay-inbound.mjs +49 -2
- package/server/services/remote-relay-loopback.mjs +245 -0
- package/server/services/remote-relay-registry-service.mjs +52 -0
- package/server/services/session-worker-launch-service.mjs +100 -15
- package/server/services/session-worker-process-service.mjs +348 -129
- package/server/services/session-worker-registry-service.mjs +18 -1
- package/server/services/session-worker-stop-service.mjs +24 -13
- package/server/services/session-worker-supervisor-service.mjs +56 -1
- package/server/services/update-install-service.mjs +9 -3
- package/server/services/usage-limit-pause-service.mjs +3 -1
- package/server/services/worker-launch-note-service.mjs +151 -0
- package/shared/claude-attribution.mjs +117 -0
- package/shared/claude-cloud/api-client.mjs +809 -0
- package/shared/claude-cloud/base-url.mjs +47 -0
- package/shared/claude-cloud/credentials.mjs +221 -0
- package/shared/claude-cloud/repo-url.mjs +111 -0
- package/shared/provider-routing.mjs +4 -2
- package/shared/remote-relay-contract.mjs +153 -22
- package/shared/remote-relay-tool-core.mjs +11 -8
- package/shared/steer-settle-failure.mjs +1 -0
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
openSummaryModal,
|
|
33
33
|
closeSummaryModal,
|
|
34
34
|
refreshSummaryModal,
|
|
35
|
+
setSummaryModalLeaveHandler,
|
|
35
36
|
syncViewportMetrics,
|
|
36
37
|
isMobileComposerViewport,
|
|
37
38
|
releaseComposerFocusAfterSend,
|
|
@@ -70,6 +71,7 @@ import {
|
|
|
70
71
|
refreshConversationHistory,
|
|
71
72
|
updateConversationTitle,
|
|
72
73
|
updateConversationPreferences,
|
|
74
|
+
updateWorkspaceRootAttribution,
|
|
73
75
|
createConversationShareLink,
|
|
74
76
|
updateDefaultSessionWorkspaceRoot,
|
|
75
77
|
scheduleContextUsageRefresh,
|
|
@@ -83,6 +85,12 @@ import {
|
|
|
83
85
|
confirmNewConversationModel,
|
|
84
86
|
closeNewConversationModelModal,
|
|
85
87
|
deleteConv,
|
|
88
|
+
archiveConv,
|
|
89
|
+
unarchiveConv,
|
|
90
|
+
toggleArchivedConversations,
|
|
91
|
+
isArchivedConversationsView,
|
|
92
|
+
refreshNewConversationCloudRepoList,
|
|
93
|
+
openOriginConversation,
|
|
86
94
|
} from './journal-view.js';
|
|
87
95
|
import {
|
|
88
96
|
loadRelayQuestions,
|
|
@@ -113,6 +121,7 @@ import {
|
|
|
113
121
|
clearImageEditTarget,
|
|
114
122
|
jumpToImageParent,
|
|
115
123
|
flushDeferredMessageRender,
|
|
124
|
+
conversationHasActiveTurn,
|
|
116
125
|
} from './conversation-view.js';
|
|
117
126
|
import { bindChatSelectionGuard, chatSelectionGuard, isChatInteractionHeld } from './selection-guard.mjs';
|
|
118
127
|
import { loadRepoBrowserTree, openRepoBrowser, closeRepoBrowser, setRepoBrowserSessionInfo, resetWorkspaceRepoBrowserForRootChange } from './attachments-view.js';
|
|
@@ -190,16 +199,19 @@ import {
|
|
|
190
199
|
registerPwaShell,
|
|
191
200
|
updatePwaAppName,
|
|
192
201
|
} from './pwa-install.js';
|
|
202
|
+
import { initScrollbarHover } from './scrollbar-hover.mjs';
|
|
193
203
|
import {
|
|
204
|
+
autoCompactWindowNotes,
|
|
194
205
|
renderAutoCompactControlHtml,
|
|
206
|
+
renderAutoCompactNotesHtml,
|
|
195
207
|
renderContextUsageHtml,
|
|
196
208
|
renderThinkingControlHtml,
|
|
197
209
|
thinkingEnabledFromKey,
|
|
198
210
|
thinkingEnabledToKey,
|
|
199
211
|
} from './context-usage-view.mjs';
|
|
212
|
+
import { renderAttributionControlHtml } from './attribution-control.mjs';
|
|
200
213
|
import {
|
|
201
214
|
autoCompactWindowFromIndex,
|
|
202
|
-
autoCompactWindowToIndex,
|
|
203
215
|
formatAutoCompactWindowLabel,
|
|
204
216
|
} from './auto-compact-window-options.mjs';
|
|
205
217
|
import { renderPlanUsageHtml, planUsageSubtitle, normalizeUsageProvider } from './plan-usage-view.mjs';
|
|
@@ -329,6 +341,22 @@ import {
|
|
|
329
341
|
openGrokLogoutConfirmation,
|
|
330
342
|
confirmGrokLogout,
|
|
331
343
|
} from './grok-auth-ui.js';
|
|
344
|
+
import {
|
|
345
|
+
getClaudeCloudSettings,
|
|
346
|
+
openClaudeCloudLoginTab,
|
|
347
|
+
refreshClaudeCloudSettingsState,
|
|
348
|
+
saveClaudeCloudSettings,
|
|
349
|
+
subscribeClaudeCloudSettings,
|
|
350
|
+
toggleClaudeCloudProvider,
|
|
351
|
+
} from './claude-cloud-settings-ui.js';
|
|
352
|
+
import { openCurrentCloudSession, syncClaudeCloudConversationUi } from './claude-cloud-conversation-ui.js';
|
|
353
|
+
import { syncConversationOriginHeader } from './conversation-origin-ui.js';
|
|
354
|
+
import {
|
|
355
|
+
previewAgentSessionsMaxWait,
|
|
356
|
+
saveAgentSessionsMaxWait,
|
|
357
|
+
toggleAgentSessions,
|
|
358
|
+
} from './agent-sessions-settings-ui.js';
|
|
359
|
+
import { CLAUDE_CLOUD_PROVIDER, claudeCloudComposerModelIds } from './claude-cloud-ui.mjs';
|
|
332
360
|
import {
|
|
333
361
|
confirmCliInstall,
|
|
334
362
|
runCliInstall,
|
|
@@ -360,6 +388,7 @@ const PROVIDER_LABELS = {
|
|
|
360
388
|
openai: 'OpenAI',
|
|
361
389
|
'openai-byok': 'OpenAI (BYOK)',
|
|
362
390
|
claude: 'Claude SDK',
|
|
391
|
+
'claude-cloud': 'Claude Cloud',
|
|
363
392
|
cursor: 'Cursor SDK',
|
|
364
393
|
grok: 'Grok',
|
|
365
394
|
'github-copilot': 'GitHub Copilot',
|
|
@@ -801,7 +830,11 @@ function initNetworkLifecycleHandling() {
|
|
|
801
830
|
window.addEventListener('pageshow', () => {
|
|
802
831
|
handleForegroundTransition('pageshow', { immediate: true });
|
|
803
832
|
});
|
|
804
|
-
window.addEventListener('pagehide', () => {
|
|
833
|
+
window.addEventListener('pagehide', (event) => {
|
|
834
|
+
// A page unloaded with the context modal still open (tab closed, app
|
|
835
|
+
// swiped away, reload) saves the window slider's draft like a modal
|
|
836
|
+
// close. A page only put aside (`persisted`) keeps its draft.
|
|
837
|
+
if (!event.persisted) commitAutoCompactDraft({ pageClosing: true });
|
|
805
838
|
// The page is going away; no grace period, suspend immediately.
|
|
806
839
|
cancelBackgroundSuspend();
|
|
807
840
|
setForegroundNetworkWorkEnabled(false);
|
|
@@ -1197,6 +1230,9 @@ function currentGrokModelLock() {
|
|
|
1197
1230
|
};
|
|
1198
1231
|
}
|
|
1199
1232
|
|
|
1233
|
+
// A cloud session's model is switched between turns like a local Claude
|
|
1234
|
+
// chat's (the worker sends set_model before the message), so there is no
|
|
1235
|
+
// cloud lock on the picker.
|
|
1200
1236
|
function currentRuntimeModelLock() {
|
|
1201
1237
|
return currentOpenAIModelLock() || currentGrokModelLock();
|
|
1202
1238
|
}
|
|
@@ -1351,6 +1387,7 @@ function normalizeModelSelectorProviderType(providerType = '') {
|
|
|
1351
1387
|
const normalized = String(providerType || '').trim().toLowerCase();
|
|
1352
1388
|
if (normalized === 'openai' || normalized === 'openai-byok') return 'openai';
|
|
1353
1389
|
if (normalized === 'claude') return 'claude';
|
|
1390
|
+
if (normalized === CLAUDE_CLOUD_PROVIDER) return CLAUDE_CLOUD_PROVIDER;
|
|
1354
1391
|
if (normalized === 'cursor') return 'cursor';
|
|
1355
1392
|
if (normalized === 'grok') return 'grok';
|
|
1356
1393
|
return 'github';
|
|
@@ -1380,18 +1417,43 @@ function modelVisibleForActiveProvider(modelId, activeProviderType, providersByM
|
|
|
1380
1417
|
const hasClaude = providers.includes('claude');
|
|
1381
1418
|
const hasCursor = providers.includes('cursor');
|
|
1382
1419
|
const hasGrok = providers.includes('grok');
|
|
1420
|
+
// Only present if a relay tags catalog rows for Claude Cloud; such a row is
|
|
1421
|
+
// then as exclusive as the other runtimes' rows.
|
|
1422
|
+
const hasClaudeCloud = providers.includes(CLAUDE_CLOUD_PROVIDER);
|
|
1383
1423
|
const hasNonExclusiveProvider = providers.some((provider) => (
|
|
1384
1424
|
provider !== 'openai-byok'
|
|
1385
1425
|
&& provider !== 'claude'
|
|
1386
1426
|
&& provider !== 'cursor'
|
|
1387
1427
|
&& provider !== 'grok'
|
|
1428
|
+
&& provider !== CLAUDE_CLOUD_PROVIDER
|
|
1388
1429
|
));
|
|
1389
1430
|
const activeProvider = normalizeModelSelectorProviderType(activeProviderType);
|
|
1390
1431
|
if (activeProvider === 'openai') return hasOpenAIByok;
|
|
1391
1432
|
if (activeProvider === 'claude') return hasClaude;
|
|
1392
1433
|
if (activeProvider === 'cursor') return hasCursor;
|
|
1393
1434
|
if (activeProvider === 'grok') return hasGrok;
|
|
1394
|
-
return !((hasOpenAIByok || hasClaude || hasCursor || hasGrok) && !hasNonExclusiveProvider);
|
|
1435
|
+
return !((hasOpenAIByok || hasClaude || hasCursor || hasGrok || hasClaudeCloud) && !hasNonExclusiveProvider);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// A Claude Cloud conversation does not pick from the shared catalog: its
|
|
1439
|
+
// models are the list of its settings tab (plain ids, no Auto), plus the model
|
|
1440
|
+
// the conversation is bound to.
|
|
1441
|
+
function buildClaudeCloudModelSelectorOptions(models, providersByModel, metadataByModel) {
|
|
1442
|
+
const conversation = currentConvId ? conversations[currentConvId] : null;
|
|
1443
|
+
const cloudModels = claudeCloudComposerModelIds({
|
|
1444
|
+
settings: getClaudeCloudSettings(),
|
|
1445
|
+
catalogModels: models,
|
|
1446
|
+
providersByModel,
|
|
1447
|
+
conversationModel: conversation?.runtimeProviderModel
|
|
1448
|
+
|| conversation?.runtime_provider_model
|
|
1449
|
+
|| conversation?.preferredModel
|
|
1450
|
+
|| '',
|
|
1451
|
+
});
|
|
1452
|
+
return buildCatalogModelOptions(cloudModels, {
|
|
1453
|
+
autoValue: AUTO_MODEL_OPTION,
|
|
1454
|
+
metadataByModel,
|
|
1455
|
+
annotateContextWindow: false,
|
|
1456
|
+
}).filter((option) => option.value.toLowerCase() !== AUTO_MODEL_OPTION);
|
|
1395
1457
|
}
|
|
1396
1458
|
|
|
1397
1459
|
// metadataByModel is a parameter because the catalog refresh builds the next
|
|
@@ -1403,6 +1465,9 @@ function buildModelSelectorOptions(
|
|
|
1403
1465
|
activeProviderType = '',
|
|
1404
1466
|
metadataByModel = modelCatalogState.modelMetadataByModel,
|
|
1405
1467
|
) {
|
|
1468
|
+
if (normalizeModelSelectorProviderType(activeProviderType) === CLAUDE_CLOUD_PROVIDER) {
|
|
1469
|
+
return buildClaudeCloudModelSelectorOptions(models, providersByModel, metadataByModel);
|
|
1470
|
+
}
|
|
1406
1471
|
const normalizedOptions = buildCatalogModelOptions(models.length ? models : [FALLBACK_MODEL], {
|
|
1407
1472
|
autoValue: AUTO_MODEL_OPTION,
|
|
1408
1473
|
metadataByModel,
|
|
@@ -1419,6 +1484,11 @@ function reasoningOptionsForModel(modelId = '') {
|
|
|
1419
1484
|
if (!isModelMetadataHealthy()) return [];
|
|
1420
1485
|
const key = String(modelId || '').trim().toLowerCase();
|
|
1421
1486
|
const provider = activeComposerProviderType();
|
|
1487
|
+
// A cloud session has no effort choice; the catalog's efforts for the same
|
|
1488
|
+
// model id belong to the local Claude and Copilot runtimes. The one value is
|
|
1489
|
+
// still listed: the (hidden) select must hold it, because a send without an
|
|
1490
|
+
// effort is refused as "model metadata missing".
|
|
1491
|
+
if (provider === CLAUDE_CLOUD_PROVIDER) return ['none'];
|
|
1422
1492
|
const providerOptions = modelCatalogState.reasoningByProvider?.[provider]?.[key];
|
|
1423
1493
|
if (Array.isArray(providerOptions)) return normalizeReasoningEffortList(providerOptions);
|
|
1424
1494
|
return normalizeReasoningEffortList(modelCatalogState.reasoningByModel?.[key] || []);
|
|
@@ -1757,7 +1827,11 @@ function updateModelPricingDetails(modelId, tier) {
|
|
|
1757
1827
|
const grid = document.getElementById('model-pricing-grid');
|
|
1758
1828
|
if (!details || !summary || !grid) return;
|
|
1759
1829
|
const metadata = modelCatalogState.modelMetadataByModel?.[modelId] || {};
|
|
1760
|
-
|
|
1830
|
+
// The catalog's prices are Copilot credits; a cloud session is billed to the
|
|
1831
|
+
// Claude account, so they would be the wrong numbers for it.
|
|
1832
|
+
const pricing = activeComposerProviderType() === CLAUDE_CLOUD_PROVIDER
|
|
1833
|
+
? null
|
|
1834
|
+
: metadata?.pricing?.[tier === 'long_context' ? 'longContext' : 'default'];
|
|
1761
1835
|
if (!pricing || typeof pricing !== 'object') {
|
|
1762
1836
|
details.open = false;
|
|
1763
1837
|
details.style.display = 'none';
|
|
@@ -1784,12 +1858,14 @@ function updateModelPricingDetails(modelId, tier) {
|
|
|
1784
1858
|
details.style.display = grid.childElementCount ? '' : 'none';
|
|
1785
1859
|
}
|
|
1786
1860
|
|
|
1787
|
-
// Relay modes each provider backend actually honors.
|
|
1788
|
-
//
|
|
1861
|
+
// Relay modes each provider backend actually honors. The local providers
|
|
1862
|
+
// support the full set — Copilot/OpenAI via prompt-context
|
|
1789
1863
|
// injection, Claude via permissionMode + system-prompt appends, Cursor via
|
|
1790
1864
|
// native plan mode plus message-text nudges for ask/autopilot — but the
|
|
1791
1865
|
// composer builds its options from this table so a provider that loses (or
|
|
1792
|
-
// gains) a mode only needs a change here.
|
|
1866
|
+
// gains) a mode only needs a change here. A Claude Cloud session runs in the
|
|
1867
|
+
// cloud's own permission mode and ignores relay modes, so it has the one
|
|
1868
|
+
// entry and its selector is hidden (body.claude-cloud-conversation).
|
|
1793
1869
|
const RELAY_MODE_LABELS = { agent: 'Agent', ask: 'Ask', plan: 'Plan', autopilot: 'Autopilot' };
|
|
1794
1870
|
const RELAY_MODES_BY_PROVIDER = {
|
|
1795
1871
|
github: ['agent', 'ask', 'plan', 'autopilot'],
|
|
@@ -1797,6 +1873,7 @@ const RELAY_MODES_BY_PROVIDER = {
|
|
|
1797
1873
|
claude: ['agent', 'ask', 'plan', 'autopilot'],
|
|
1798
1874
|
cursor: ['agent', 'ask', 'plan', 'autopilot'],
|
|
1799
1875
|
grok: ['agent', 'ask', 'plan', 'autopilot'],
|
|
1876
|
+
'claude-cloud': ['agent'],
|
|
1800
1877
|
};
|
|
1801
1878
|
|
|
1802
1879
|
function relayModesForProvider(providerType = '') {
|
|
@@ -1848,8 +1925,9 @@ async function persistCurrentConversationPreferences() {
|
|
|
1848
1925
|
// record an effort the user never picked when they only changed the mode.
|
|
1849
1926
|
const reasoningEffort = String(document.getElementById('reasoning-effort-select')?.value || '').trim().toLowerCase();
|
|
1850
1927
|
// These keys hold the last explicit choice, so they are not rolled back when
|
|
1851
|
-
// the write below fails: the user did make the choice either way.
|
|
1852
|
-
|
|
1928
|
+
// the write below fails: the user did make the choice either way. A cloud
|
|
1929
|
+
// chat has no mode to choose, so its fixed "agent" is not such a choice.
|
|
1930
|
+
if (activeComposerProviderType() !== CLAUDE_CLOUD_PROVIDER) localStorage.setItem(MODE_STORAGE_KEY, mode);
|
|
1853
1931
|
if (model) localStorage.setItem(MODEL_STORAGE_KEY, model);
|
|
1854
1932
|
if (reasoningEffort) localStorage.setItem(REASONING_STORAGE_KEY, reasoningEffort);
|
|
1855
1933
|
|
|
@@ -2337,6 +2415,9 @@ function renderModelVariantCatalogBody() {
|
|
|
2337
2415
|
// shows Claude-SDK rows exclusively, the Cursor SDK tab Cursor rows, the
|
|
2338
2416
|
// Grok tab Grok rows, and Copilot never shows rows that a different runtime
|
|
2339
2417
|
// contributed (there is no cross-runtime switching).
|
|
2418
|
+
// Claude Cloud has no rows of its own to select: its picker is fed by the
|
|
2419
|
+
// Claude Cloud settings tab, so a row tagged for it belongs to no tab.
|
|
2420
|
+
if (entryProvider === CLAUDE_CLOUD_PROVIDER) return false;
|
|
2340
2421
|
if (activeTab === 'anthropic') {
|
|
2341
2422
|
return entryProvider === 'claude';
|
|
2342
2423
|
}
|
|
@@ -2360,6 +2441,7 @@ function renderModelVariantCatalogBody() {
|
|
|
2360
2441
|
&& provider !== 'claude'
|
|
2361
2442
|
&& provider !== 'cursor'
|
|
2362
2443
|
&& provider !== 'grok'
|
|
2444
|
+
&& provider !== CLAUDE_CLOUD_PROVIDER
|
|
2363
2445
|
));
|
|
2364
2446
|
}
|
|
2365
2447
|
return true;
|
|
@@ -2786,6 +2868,66 @@ async function saveSelectedModelsFromModal() {
|
|
|
2786
2868
|
// click pins a provider so the modal's Refresh re-renders onto the same tab
|
|
2787
2869
|
// instead of snapping back. Reset on every fresh open of the modal.
|
|
2788
2870
|
let usageTabProviderOverride = null;
|
|
2871
|
+
// The last usage report, shown at once on the next open while the open tab's
|
|
2872
|
+
// provider is read live; kept across reloads in sessionStorage. `fetchedAt`
|
|
2873
|
+
// (per card id) decides whether a tab needs a live read when it is shown.
|
|
2874
|
+
const USAGE_REPORT_STORAGE_KEY = 'oar.usage.lastReport.v1';
|
|
2875
|
+
const USAGE_LIVE_FRESH_MS = 60_000;
|
|
2876
|
+
let lastUsageReport = null;
|
|
2877
|
+
let usageRefreshInFlight = null;
|
|
2878
|
+
|
|
2879
|
+
function readStoredUsageReport() {
|
|
2880
|
+
try {
|
|
2881
|
+
const parsed = JSON.parse(sessionStorage.getItem(USAGE_REPORT_STORAGE_KEY) || 'null');
|
|
2882
|
+
return parsed && Array.isArray(parsed.providers) ? parsed : null;
|
|
2883
|
+
} catch {
|
|
2884
|
+
return null;
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
function storeUsageReport(report) {
|
|
2889
|
+
lastUsageReport = report;
|
|
2890
|
+
try {
|
|
2891
|
+
sessionStorage.setItem(USAGE_REPORT_STORAGE_KEY, JSON.stringify(report));
|
|
2892
|
+
} catch {
|
|
2893
|
+
// Storage full or unavailable: the module copy still serves this page.
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
/**
|
|
2898
|
+
* The next report: cards the answer read live replace the kept ones; a card
|
|
2899
|
+
* the answer only served from its cache replaces nothing newer we hold (a
|
|
2900
|
+
* restarted relay has an empty cache and would otherwise blank good cards).
|
|
2901
|
+
*/
|
|
2902
|
+
function mergeUsageReports(kept, answer) {
|
|
2903
|
+
if (!kept || !Array.isArray(kept.providers)) return answer;
|
|
2904
|
+
const live = new Set(Array.isArray(answer?.live) ? answer.live : []);
|
|
2905
|
+
const fetchedAt = { ...(kept.fetchedAt || {}) };
|
|
2906
|
+
const keptById = new Map(kept.providers.map((card) => [normalizeUsageProvider(card?.provider), card]));
|
|
2907
|
+
const providers = (answer.providers || []).map((card) => {
|
|
2908
|
+
const id = normalizeUsageProvider(card?.provider);
|
|
2909
|
+
const previous = keptById.get(id);
|
|
2910
|
+
if (live.has(id) || !previous) {
|
|
2911
|
+
fetchedAt[id] = answer.fetchedAt?.[id] || new Date().toISOString();
|
|
2912
|
+
return card;
|
|
2913
|
+
}
|
|
2914
|
+
// Unavailable from the cache but known from before: keep what we had.
|
|
2915
|
+
return card?.status === 'unavailable' && previous.status !== 'unavailable' ? previous : card;
|
|
2916
|
+
});
|
|
2917
|
+
return { ...answer, providers, fetchedAt };
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
function usageCardIsFresh(provider) {
|
|
2921
|
+
const at = Date.parse(String(lastUsageReport?.fetchedAt?.[provider] || ''));
|
|
2922
|
+
return Number.isFinite(at) && Date.now() - at < USAGE_LIVE_FRESH_MS;
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
/** The "Updating…" marker on one card, while its live read runs. */
|
|
2926
|
+
function markUsageCardUpdating(provider, updating) {
|
|
2927
|
+
const body = document.getElementById('summary-modal-body');
|
|
2928
|
+
const card = body?.querySelector(`[data-usage-panel="${CSS.escape(provider)}"]`);
|
|
2929
|
+
if (card) card.toggleAttribute('data-updating', !!updating);
|
|
2930
|
+
}
|
|
2789
2931
|
|
|
2790
2932
|
/**
|
|
2791
2933
|
* Delegated because #summary-modal-body's innerHTML is replaced on every render
|
|
@@ -2803,7 +2945,7 @@ function initUsageTabControl() {
|
|
|
2803
2945
|
});
|
|
2804
2946
|
}
|
|
2805
2947
|
|
|
2806
|
-
/**
|
|
2948
|
+
/** DOM toggle first; a tab whose card is older than a minute is then read live. */
|
|
2807
2949
|
function selectUsageTab(root, provider) {
|
|
2808
2950
|
const target = String(provider || '').trim();
|
|
2809
2951
|
if (!target) return;
|
|
@@ -2819,6 +2961,78 @@ function selectUsageTab(root, provider) {
|
|
|
2819
2961
|
panel.hidden = panel.dataset.usagePanel !== target;
|
|
2820
2962
|
}
|
|
2821
2963
|
usageTabProviderOverride = target;
|
|
2964
|
+
if (!usageCardIsFresh(target)) {
|
|
2965
|
+
refreshUsageProvider(target).catch(() => {});
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
/**
|
|
2970
|
+
* Read one provider live and swap the result into the modal. Serialised: a
|
|
2971
|
+
* second request while one runs waits for it and is then made if still
|
|
2972
|
+
* needed (the tab may have changed).
|
|
2973
|
+
*/
|
|
2974
|
+
async function refreshUsageProvider(provider) {
|
|
2975
|
+
const target = String(provider || '').trim();
|
|
2976
|
+
if (!target) return;
|
|
2977
|
+
if (usageRefreshInFlight) {
|
|
2978
|
+
await usageRefreshInFlight.catch(() => {});
|
|
2979
|
+
if (usageCardIsFresh(target)) return;
|
|
2980
|
+
}
|
|
2981
|
+
markUsageCardUpdating(target, true);
|
|
2982
|
+
const run = (async () => {
|
|
2983
|
+
const [usageResult, authResult] = await Promise.allSettled([
|
|
2984
|
+
loadUsageSummary({ providers: [target] }),
|
|
2985
|
+
getClaudeAuthStatus(),
|
|
2986
|
+
]);
|
|
2987
|
+
if (usageResult.status === 'rejected') throw usageResult.reason;
|
|
2988
|
+
const answer = usageResult.value;
|
|
2989
|
+
if (!answer || !Array.isArray(answer.providers)) return;
|
|
2990
|
+
if (summaryModalState.kind !== 'usage') {
|
|
2991
|
+
storeUsageReport(mergeUsageReports(lastUsageReport, answer));
|
|
2992
|
+
return;
|
|
2993
|
+
}
|
|
2994
|
+
const account = authResult.status === 'fulfilled' ? claudeAccountFromAuthStatus(authResult.value) : null;
|
|
2995
|
+
renderUsageReport(mergeUsageReports(lastUsageReport, answer), account);
|
|
2996
|
+
})();
|
|
2997
|
+
usageRefreshInFlight = run;
|
|
2998
|
+
try {
|
|
2999
|
+
await run;
|
|
3000
|
+
} finally {
|
|
3001
|
+
if (usageRefreshInFlight === run) usageRefreshInFlight = null;
|
|
3002
|
+
markUsageCardUpdating(target, false);
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
/** Render a report into the open modal and keep it for the next open. */
|
|
3007
|
+
function renderUsageReport(report, account, { updating = '' } = {}) {
|
|
3008
|
+
storeUsageReport(report);
|
|
3009
|
+
const activeProvider = usageTabProviderOverride
|
|
3010
|
+
|| normalizeUsageProvider(activeComposerProviderType());
|
|
3011
|
+
const planHtml = renderPlanUsageHtml(withClaudeAccount(report, account), { activeProvider });
|
|
3012
|
+
if (!planHtml) return false;
|
|
3013
|
+
initUsageTabControl();
|
|
3014
|
+
renderSummaryModalContent({
|
|
3015
|
+
title: 'Plan usage',
|
|
3016
|
+
subtitle: `${planUsageSubtitle(report)}${updating ? ' · updating…' : ''}`,
|
|
3017
|
+
bodyHtml: planHtml,
|
|
3018
|
+
refresh: refreshOpenUsageTab,
|
|
3019
|
+
kind: 'usage',
|
|
3020
|
+
});
|
|
3021
|
+
if (updating) markUsageCardUpdating(updating, true);
|
|
3022
|
+
return true;
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
/** The modal's Refresh button: the open tab live, nothing else. */
|
|
3026
|
+
async function refreshOpenUsageTab() {
|
|
3027
|
+
const body = document.getElementById('summary-modal-body');
|
|
3028
|
+
const active = body?.querySelector('[data-usage-tab].active')?.dataset.usageTab
|
|
3029
|
+
|| usageTabProviderOverride
|
|
3030
|
+
|| normalizeUsageProvider(activeComposerProviderType());
|
|
3031
|
+
if (!lastUsageReport || !active) {
|
|
3032
|
+
await loadUsageSummaryAndRender();
|
|
3033
|
+
return;
|
|
3034
|
+
}
|
|
3035
|
+
await refreshUsageProvider(active);
|
|
2822
3036
|
}
|
|
2823
3037
|
|
|
2824
3038
|
/**
|
|
@@ -2848,11 +3062,18 @@ function withClaudeAccount(report, account) {
|
|
|
2848
3062
|
};
|
|
2849
3063
|
}
|
|
2850
3064
|
|
|
3065
|
+
/**
|
|
3066
|
+
* The first open with nothing kept: the open tab's provider is read live and
|
|
3067
|
+
* the other cards come from the relay's cache (or are read once when it has
|
|
3068
|
+
* none). With a kept report, showUsage renders it first and only refreshes.
|
|
3069
|
+
*/
|
|
2851
3070
|
async function loadUsageSummaryAndRender() {
|
|
3071
|
+
const activeProvider = usageTabProviderOverride
|
|
3072
|
+
|| normalizeUsageProvider(activeComposerProviderType());
|
|
2852
3073
|
// Usage is the point of the modal; the account line is a garnish, so the two
|
|
2853
3074
|
// are settled independently and a failing auth probe cannot blank the cards.
|
|
2854
3075
|
const [usageResult, authResult] = await Promise.allSettled([
|
|
2855
|
-
loadUsageSummary(),
|
|
3076
|
+
loadUsageSummary({ providers: activeProvider ? [activeProvider] : null }),
|
|
2856
3077
|
getClaudeAuthStatus(),
|
|
2857
3078
|
]);
|
|
2858
3079
|
if (usageResult.status === 'rejected') throw usageResult.reason;
|
|
@@ -2861,20 +3082,7 @@ async function loadUsageSummaryAndRender() {
|
|
|
2861
3082
|
const account = authResult.status === 'fulfilled'
|
|
2862
3083
|
? claudeAccountFromAuthStatus(authResult.value)
|
|
2863
3084
|
: null;
|
|
2864
|
-
|
|
2865
|
-
|| normalizeUsageProvider(activeComposerProviderType());
|
|
2866
|
-
const planHtml = renderPlanUsageHtml(withClaudeAccount(d, account), { activeProvider });
|
|
2867
|
-
if (planHtml) {
|
|
2868
|
-
initUsageTabControl();
|
|
2869
|
-
renderSummaryModalContent({
|
|
2870
|
-
title: 'Plan usage',
|
|
2871
|
-
subtitle: planUsageSubtitle(d),
|
|
2872
|
-
bodyHtml: planHtml,
|
|
2873
|
-
refresh: loadUsageSummaryAndRender,
|
|
2874
|
-
kind: 'usage',
|
|
2875
|
-
});
|
|
2876
|
-
return;
|
|
2877
|
-
}
|
|
3085
|
+
if (Array.isArray(d.providers) && renderUsageReport(mergeUsageReports(lastUsageReport, d), account)) return;
|
|
2878
3086
|
// Legacy relay (or a relay without the plan-usage service): fall back to the
|
|
2879
3087
|
// Copilot-only text summary rather than showing an empty modal.
|
|
2880
3088
|
const pct = d.premiumInteractions?.percentRemaining != null
|
|
@@ -2894,18 +3102,37 @@ async function loadUsageSummaryAndRender() {
|
|
|
2894
3102
|
// The context modal is rendered from scratch on every refresh, so the slider's
|
|
2895
3103
|
// conversation binding lives here rather than on the element.
|
|
2896
3104
|
let autoCompactControlConversationId = '';
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
//
|
|
2900
|
-
//
|
|
3105
|
+
// The save in flight, if any: saves run one after another, never side by side.
|
|
3106
|
+
let autoCompactWindowUpdateInFlight = null;
|
|
3107
|
+
// The server's last known window for the rendered control: what a draft is
|
|
3108
|
+
// compared against.
|
|
2901
3109
|
let autoCompactControlStoredWindow = null;
|
|
3110
|
+
// What the last payload said about the running session, for the notes.
|
|
3111
|
+
let autoCompactControlSession = { activeWindow: null, deferred: null, usedTokens: null };
|
|
3112
|
+
// The slider is a draft while the modal is open: moving it only updates the
|
|
3113
|
+
// label and the notes, and the value is saved when the modal is left
|
|
3114
|
+
// (setSummaryModalLeaveHandler). `window` undefined = untouched. Kept outside
|
|
3115
|
+
// the DOM so a refresh of the open modal keeps the position.
|
|
3116
|
+
let autoCompactDraft = { conversationId: '', window: undefined };
|
|
3117
|
+
|
|
3118
|
+
function autoCompactNotesOptions(draftWindow, storedWindow = autoCompactControlStoredWindow) {
|
|
3119
|
+
return {
|
|
3120
|
+
draftWindow,
|
|
3121
|
+
storedWindow,
|
|
3122
|
+
activeWindow: autoCompactControlSession.activeWindow,
|
|
3123
|
+
deferred: autoCompactControlSession.deferred,
|
|
3124
|
+
turnRunning: !!autoCompactControlConversationId && conversationHasActiveTurn(autoCompactControlConversationId),
|
|
3125
|
+
usedTokens: autoCompactControlSession.usedTokens,
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
2902
3128
|
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
if (slider) slider.value = String(autoCompactWindowToIndex(window));
|
|
3129
|
+
function trackAutoCompactDraft(slider) {
|
|
3130
|
+
const window = autoCompactWindowFromIndex(slider.value);
|
|
3131
|
+
autoCompactDraft = { conversationId: autoCompactControlConversationId, window };
|
|
2907
3132
|
const label = document.getElementById('ctx-autocompact-value');
|
|
2908
3133
|
if (label) label.textContent = formatAutoCompactWindowLabel(window);
|
|
3134
|
+
const notes = document.getElementById('ctx-autocompact-notes');
|
|
3135
|
+
if (notes) notes.innerHTML = renderAutoCompactNotesHtml(autoCompactNotesOptions(window));
|
|
2909
3136
|
}
|
|
2910
3137
|
|
|
2911
3138
|
/**
|
|
@@ -2921,18 +3148,37 @@ function initAutoCompactWindowControl() {
|
|
|
2921
3148
|
const target = event?.target;
|
|
2922
3149
|
return target && target.id === 'ctx-autocompact-slider' ? target : null;
|
|
2923
3150
|
};
|
|
2924
|
-
//
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3151
|
+
// Nothing is written while the modal is open; see commitAutoCompactDraft.
|
|
3152
|
+
for (const type of ['input', 'change']) {
|
|
3153
|
+
body.addEventListener(type, (event) => {
|
|
3154
|
+
const slider = sliderFrom(event);
|
|
3155
|
+
if (slider) trackAutoCompactDraft(slider);
|
|
3156
|
+
});
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
// Runs when the context modal is left (or re-bound to another conversation):
|
|
3161
|
+
// a draft that differs from the stored value is saved; one set back to the
|
|
3162
|
+
// stored value sends nothing.
|
|
3163
|
+
function commitAutoCompactDraft({ pageClosing = false } = {}) {
|
|
3164
|
+
const { conversationId, window } = autoCompactDraft;
|
|
3165
|
+
autoCompactDraft = { conversationId: '', window: undefined };
|
|
3166
|
+
if (!conversationId || window === undefined) return;
|
|
3167
|
+
if (conversationId === autoCompactControlConversationId && window === autoCompactControlStoredWindow) return;
|
|
3168
|
+
if (pageClosing) {
|
|
3169
|
+
// Sent at once, not queued behind an earlier save: the request has to
|
|
3170
|
+
// leave before the page does.
|
|
3171
|
+
void saveAutoCompactWindow(conversationId, window, '', { pageClosing: true });
|
|
3172
|
+
return;
|
|
3173
|
+
}
|
|
3174
|
+
const notes = autoCompactWindowNotes(autoCompactNotesOptions(window, window));
|
|
3175
|
+
const previous = autoCompactWindowUpdateInFlight || Promise.resolve();
|
|
3176
|
+
const run = previous
|
|
3177
|
+
.catch(() => {})
|
|
3178
|
+
.then(() => saveAutoCompactWindow(conversationId, window, notes.pending));
|
|
3179
|
+
autoCompactWindowUpdateInFlight = run;
|
|
3180
|
+
void run.finally(() => {
|
|
3181
|
+
if (autoCompactWindowUpdateInFlight === run) autoCompactWindowUpdateInFlight = null;
|
|
2936
3182
|
});
|
|
2937
3183
|
}
|
|
2938
3184
|
|
|
@@ -2954,6 +3200,36 @@ function setThinkingControlValue({ enabled, display }) {
|
|
|
2954
3200
|
}
|
|
2955
3201
|
}
|
|
2956
3202
|
|
|
3203
|
+
// The folder's commit attribution select in the 🧠 modal: one request per
|
|
3204
|
+
// change, then the modal is re-read so the effect text matches the relay.
|
|
3205
|
+
let attributionControlUpdateInFlight = false;
|
|
3206
|
+
function initAttributionControl() {
|
|
3207
|
+
const body = document.getElementById('summary-modal-body');
|
|
3208
|
+
if (!body || body.dataset.attributionControlBound === 'true') return;
|
|
3209
|
+
body.dataset.attributionControlBound = 'true';
|
|
3210
|
+
body.addEventListener('change', async (event) => {
|
|
3211
|
+
const select = event.target?.closest?.('#ctx-attribution-select');
|
|
3212
|
+
if (!select) return;
|
|
3213
|
+
const path = String(select.dataset.workspaceRoot || '').trim();
|
|
3214
|
+
if (!path || attributionControlUpdateInFlight) return;
|
|
3215
|
+
attributionControlUpdateInFlight = true;
|
|
3216
|
+
select.disabled = true;
|
|
3217
|
+
try {
|
|
3218
|
+
const response = await updateWorkspaceRootAttribution(path, select.value || null);
|
|
3219
|
+
if (!response || response.ok === false) throw new Error(String(response?.error || 'The relay refused the change'));
|
|
3220
|
+
showTransientRelayNotice(select.value
|
|
3221
|
+
? `Commit attribution for this folder: ${select.value}.`
|
|
3222
|
+
: 'Commit attribution for this folder follows the provider setting again.');
|
|
3223
|
+
await refreshSummaryModal();
|
|
3224
|
+
} catch (error) {
|
|
3225
|
+
showTransientRelayNotice(`Could not change the commit attribution: ${error?.message || error}`);
|
|
3226
|
+
select.disabled = false;
|
|
3227
|
+
} finally {
|
|
3228
|
+
attributionControlUpdateInFlight = false;
|
|
3229
|
+
}
|
|
3230
|
+
});
|
|
3231
|
+
}
|
|
3232
|
+
|
|
2957
3233
|
function initThinkingControl() {
|
|
2958
3234
|
const body = document.getElementById('summary-modal-body');
|
|
2959
3235
|
if (!body || body.dataset.thinkingBound === '1') return;
|
|
@@ -3009,41 +3285,32 @@ async function applyThinkingSelection(axis, key) {
|
|
|
3009
3285
|
}
|
|
3010
3286
|
}
|
|
3011
3287
|
|
|
3012
|
-
async function
|
|
3013
|
-
const convId = String(autoCompactControlConversationId || '').trim();
|
|
3014
|
-
const slider = document.getElementById('ctx-autocompact-slider');
|
|
3015
|
-
if (!convId || autoCompactWindowUpdateInFlight) {
|
|
3016
|
-
// Nothing will be written, so leave the control showing what is stored
|
|
3017
|
-
// rather than a position the server never heard about.
|
|
3018
|
-
setAutoCompactControlValue(autoCompactControlStoredWindow);
|
|
3019
|
-
return;
|
|
3020
|
-
}
|
|
3021
|
-
autoCompactWindowUpdateInFlight = true;
|
|
3022
|
-
if (slider) slider.disabled = true;
|
|
3288
|
+
async function saveAutoCompactWindow(convId, autoCompactWindow, pendingNote = '', { pageClosing = false } = {}) {
|
|
3023
3289
|
try {
|
|
3024
3290
|
// Only the window is sent: the route keeps every unmentioned preference as
|
|
3025
3291
|
// stored, so this cannot clobber the composer's model/effort choice.
|
|
3026
3292
|
const response = await updateConversationPreferences(convId, {
|
|
3027
3293
|
clientId: CLIENT_ID,
|
|
3028
3294
|
autoCompactWindow,
|
|
3029
|
-
});
|
|
3295
|
+
}, { keepalive: pageClosing });
|
|
3296
|
+
// Nobody is left to tell on a page that is going away.
|
|
3297
|
+
if (pageClosing) return;
|
|
3030
3298
|
if (!response) throw new Error('Failed to update the auto-compact window');
|
|
3031
3299
|
// The server, not the slider, decides what got stored (values snap to a
|
|
3032
|
-
// stop)
|
|
3300
|
+
// stop).
|
|
3033
3301
|
const stored = response.autoCompactWindow ?? null;
|
|
3034
|
-
autoCompactControlStoredWindow = stored;
|
|
3035
|
-
|
|
3302
|
+
if (convId === autoCompactControlConversationId) autoCompactControlStoredWindow = stored;
|
|
3303
|
+
// No note when the running session already uses this value.
|
|
3304
|
+
const when = pendingNote ? ` ${pendingNote}` : '';
|
|
3036
3305
|
showTransientRelayNotice(stored === null
|
|
3037
|
-
?
|
|
3038
|
-
: `Auto-compact window set to ${formatAutoCompactWindowLabel(stored)} tokens
|
|
3306
|
+
? `Auto-compact window back to the model default.${when}`
|
|
3307
|
+
: `Auto-compact window set to ${formatAutoCompactWindowLabel(stored)} tokens.${when}`);
|
|
3308
|
+
// A modal reopened on this conversation meanwhile shows the new value.
|
|
3309
|
+
if (summaryModalState.kind === 'context' && convId === autoCompactControlConversationId) {
|
|
3310
|
+
await refreshSummaryModal();
|
|
3311
|
+
}
|
|
3039
3312
|
} catch (error) {
|
|
3040
|
-
// The write was refused, so the rejected position is a lie: put the
|
|
3041
|
-
// control back on the last value the server confirmed.
|
|
3042
|
-
setAutoCompactControlValue(autoCompactControlStoredWindow);
|
|
3043
3313
|
alert(error?.message || 'Failed to update the auto-compact window.');
|
|
3044
|
-
} finally {
|
|
3045
|
-
autoCompactWindowUpdateInFlight = false;
|
|
3046
|
-
if (slider) slider.disabled = false;
|
|
3047
3314
|
}
|
|
3048
3315
|
}
|
|
3049
3316
|
|
|
@@ -3055,14 +3322,42 @@ async function loadContextSummaryAndRender(convId) {
|
|
|
3055
3322
|
const refreshLookupId = sessionId || trimmedConvId || null;
|
|
3056
3323
|
const providerLabel = payload.providerType === 'claude'
|
|
3057
3324
|
? 'Claude'
|
|
3058
|
-
: (payload.providerType ===
|
|
3059
|
-
? '
|
|
3060
|
-
: (payload.providerType === '
|
|
3325
|
+
: (payload.providerType === CLAUDE_CLOUD_PROVIDER
|
|
3326
|
+
? 'Claude Cloud'
|
|
3327
|
+
: (payload.providerType === 'cursor'
|
|
3328
|
+
? 'Cursor'
|
|
3329
|
+
: (payload.providerType === 'grok' ? 'Grok' : 'Copilot')));
|
|
3061
3330
|
const subtitle = sessionId
|
|
3062
3331
|
? `${providerLabel} session ${sessionId.slice(0, 8)}`
|
|
3063
3332
|
: (trimmedConvId ? `Conversation ${trimmedConvId.slice(0, 8)}` : 'No conversation selected');
|
|
3064
3333
|
|
|
3065
|
-
|
|
3334
|
+
// The session fields come with the payload when the relay knows them
|
|
3335
|
+
// (older relays send neither, and the notes fall back to draft vs stored).
|
|
3336
|
+
const sessionField = (key) => payload[key] ?? payload.contextUsage?.[key] ?? null;
|
|
3337
|
+
const modelContextLimit = sessionField('modelContextLimit');
|
|
3338
|
+
const usageHtml = renderContextUsageHtml(payload.contextUsage, {
|
|
3339
|
+
compactionWindow: payload.providerType === 'claude',
|
|
3340
|
+
modelContextLimit,
|
|
3341
|
+
});
|
|
3342
|
+
// The slider's writes need the conversation id even when the modal was
|
|
3343
|
+
// opened through an sdk_session_id.
|
|
3344
|
+
const boundConversationId = String(
|
|
3345
|
+
payload.resolvedConversationId || (payload.providerType === 'claude' ? trimmedConvId : '') || '',
|
|
3346
|
+
).trim();
|
|
3347
|
+
// A draft for another conversation is saved before this one takes over.
|
|
3348
|
+
if (autoCompactDraft.conversationId && autoCompactDraft.conversationId !== boundConversationId) {
|
|
3349
|
+
commitAutoCompactDraft();
|
|
3350
|
+
}
|
|
3351
|
+
autoCompactControlConversationId = boundConversationId;
|
|
3352
|
+
// What the server says is stored right now, refreshed every time the
|
|
3353
|
+
// control is re-rendered: the draft is compared against it.
|
|
3354
|
+
autoCompactControlStoredWindow = payload.autoCompactWindow ?? null;
|
|
3355
|
+
autoCompactControlSession = {
|
|
3356
|
+
activeWindow: sessionField('activeAutoCompactWindow'),
|
|
3357
|
+
deferred: sessionField('autoCompactWindowDeferred'),
|
|
3358
|
+
usedTokens: payload.contextUsage?.totalTokens ?? null,
|
|
3359
|
+
};
|
|
3360
|
+
const draftWindow = autoCompactDraft.conversationId === boundConversationId ? autoCompactDraft.window : undefined;
|
|
3066
3361
|
// Claude-only: no other provider exposes a compaction window (or any
|
|
3067
3362
|
// session-settings surface) to steer.
|
|
3068
3363
|
const autoCompactHtml = payload.providerType === 'claude'
|
|
@@ -3078,6 +3373,11 @@ async function loadContextSummaryAndRender(convId) {
|
|
|
3078
3373
|
? (payload.contextUsage.isAutoCompactEnabled ?? null)
|
|
3079
3374
|
: null,
|
|
3080
3375
|
maxTokens: payload.contextUsage?.maxTokens ?? null,
|
|
3376
|
+
draftWindow,
|
|
3377
|
+
activeAutoCompactWindow: autoCompactControlSession.activeWindow,
|
|
3378
|
+
autoCompactWindowDeferred: autoCompactControlSession.deferred,
|
|
3379
|
+
turnRunning: autoCompactNotesOptions(draftWindow).turnRunning,
|
|
3380
|
+
usedTokens: autoCompactControlSession.usedTokens,
|
|
3081
3381
|
})
|
|
3082
3382
|
: '';
|
|
3083
3383
|
const thinkingHtml = payload.providerType === 'claude'
|
|
@@ -3086,14 +3386,11 @@ async function loadContextSummaryAndRender(convId) {
|
|
|
3086
3386
|
thinkingDisplay: payload.thinkingDisplay ?? null,
|
|
3087
3387
|
})
|
|
3088
3388
|
: '';
|
|
3089
|
-
//
|
|
3090
|
-
//
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
// Baseline for the failure path below: what the server says is stored right
|
|
3095
|
-
// now, refreshed every time the control is re-rendered.
|
|
3096
|
-
autoCompactControlStoredWindow = payload.autoCompactWindow ?? null;
|
|
3389
|
+
// What this folder's commits say, with the folder override: Claude, and a
|
|
3390
|
+
// Claude Cloud chat that was started from a folder (no path, no row).
|
|
3391
|
+
const attributionHtml = payload.providerType === 'claude' || payload.providerType === 'claude-cloud'
|
|
3392
|
+
? renderAttributionControlHtml(payload.attribution || null)
|
|
3393
|
+
: '';
|
|
3097
3394
|
thinkingControlStored = {
|
|
3098
3395
|
enabled: payload.thinkingEnabled !== false,
|
|
3099
3396
|
display: payload.thinkingDisplay || 'summarized',
|
|
@@ -3102,10 +3399,10 @@ async function loadContextSummaryAndRender(convId) {
|
|
|
3102
3399
|
// The structured breakdown is the answer; the runtime's own text dump stays
|
|
3103
3400
|
// available underneath for the details it carries that categories don't.
|
|
3104
3401
|
const bodyHtml = usageHtml
|
|
3105
|
-
? `${usageHtml}${autoCompactHtml}${thinkingHtml}${detailText
|
|
3402
|
+
? `${usageHtml}${autoCompactHtml}${thinkingHtml}${attributionHtml}${detailText
|
|
3106
3403
|
? `<details class="ctx-usage-raw"><summary>Raw details</summary><pre>${escHtml(detailText)}</pre></details>`
|
|
3107
3404
|
: ''}`
|
|
3108
|
-
: `${autoCompactHtml}${thinkingHtml}<pre>${escHtml(detailText || 'No context data available.')}</pre>`;
|
|
3405
|
+
: `${autoCompactHtml}${thinkingHtml}${attributionHtml}<pre>${escHtml(detailText || 'No context data available.')}</pre>`;
|
|
3109
3406
|
|
|
3110
3407
|
renderSummaryModalContent({
|
|
3111
3408
|
title: 'Context usage',
|
|
@@ -3114,6 +3411,7 @@ async function loadContextSummaryAndRender(convId) {
|
|
|
3114
3411
|
refresh: () => loadContextSummaryAndRender(refreshLookupId),
|
|
3115
3412
|
kind: 'context',
|
|
3116
3413
|
});
|
|
3414
|
+
setSummaryModalLeaveHandler('context', () => commitAutoCompactDraft());
|
|
3117
3415
|
}
|
|
3118
3416
|
|
|
3119
3417
|
async function showUsage() {
|
|
@@ -3125,15 +3423,27 @@ async function showUsage() {
|
|
|
3125
3423
|
btn.textContent = '⏳';
|
|
3126
3424
|
btn.disabled = true;
|
|
3127
3425
|
}
|
|
3426
|
+
// What was shown last time is shown again at once; only the open tab's
|
|
3427
|
+
// provider is read live, and its card is swapped in when the answer comes.
|
|
3428
|
+
const kept = lastUsageReport || readStoredUsageReport();
|
|
3429
|
+
const activeProvider = normalizeUsageProvider(activeComposerProviderType());
|
|
3128
3430
|
openSummaryModal({
|
|
3129
3431
|
title: 'Plan usage',
|
|
3130
3432
|
subtitle: 'Loading…',
|
|
3131
|
-
bodyHtml: '<div class="summary-loading">Fetching plan usage
|
|
3132
|
-
refresh:
|
|
3433
|
+
bodyHtml: '<div class="summary-loading">Fetching plan usage…</div>',
|
|
3434
|
+
refresh: refreshOpenUsageTab,
|
|
3133
3435
|
kind: 'usage',
|
|
3134
3436
|
});
|
|
3135
|
-
setSummaryModalLoading(true);
|
|
3136
3437
|
try {
|
|
3438
|
+
if (kept && renderUsageReport(kept, null, { updating: activeProvider })) {
|
|
3439
|
+
if (btn) {
|
|
3440
|
+
btn.textContent = btn.id === 'chat-menu-usage' ? '📊 Check Usage' : '📊';
|
|
3441
|
+
btn.disabled = false;
|
|
3442
|
+
}
|
|
3443
|
+
await refreshUsageProvider(activeProvider);
|
|
3444
|
+
return;
|
|
3445
|
+
}
|
|
3446
|
+
setSummaryModalLoading(true);
|
|
3137
3447
|
await loadUsageSummaryAndRender();
|
|
3138
3448
|
} catch (e) {
|
|
3139
3449
|
renderSummaryModalContent({
|
|
@@ -3747,6 +4057,11 @@ function syncChatTitleControls() {
|
|
|
3747
4057
|
killBtn.disabled = sharedMode || !convId || !sdkSessionId;
|
|
3748
4058
|
killBtn.hidden = sharedMode || !convId;
|
|
3749
4059
|
}
|
|
4060
|
+
const archiveBtn = document.getElementById('chat-menu-archive');
|
|
4061
|
+
if (archiveBtn) {
|
|
4062
|
+
archiveBtn.hidden = sharedMode || !convId;
|
|
4063
|
+
archiveBtn.textContent = conversation?.archived === true ? '📂 Unarchive conversation' : '🗄 Archive conversation';
|
|
4064
|
+
}
|
|
3750
4065
|
if (editor) {
|
|
3751
4066
|
editor.hidden = !editing;
|
|
3752
4067
|
}
|
|
@@ -3762,6 +4077,11 @@ function syncChatTitleControls() {
|
|
|
3762
4077
|
if (!isStatusViewActive()) {
|
|
3763
4078
|
syncChatHeaderWorkspaceLabel();
|
|
3764
4079
|
}
|
|
4080
|
+
// Cloud line, hidden controls and the image-only attach button all follow
|
|
4081
|
+
// the conversation on screen, like the header does.
|
|
4082
|
+
syncClaudeCloudConversationUi();
|
|
4083
|
+
// "via agent": the conversation an agent on this relay started this one from.
|
|
4084
|
+
syncConversationOriginHeader();
|
|
3765
4085
|
syncChatTitleWatcherIndicator();
|
|
3766
4086
|
}
|
|
3767
4087
|
|
|
@@ -3919,12 +4239,31 @@ initTmuxInspectorView({
|
|
|
3919
4239
|
closeChatActionsMenu,
|
|
3920
4240
|
});
|
|
3921
4241
|
|
|
4242
|
+
// The composer's model list of a cloud conversation comes from the Claude
|
|
4243
|
+
// Cloud settings, which load (and change) independently of the catalog.
|
|
4244
|
+
function refreshClaudeCloudComposerModels() {
|
|
4245
|
+
if (isSharedReaderMode()) return;
|
|
4246
|
+
if (normalizeModelSelectorProviderType(activeComposerProviderType()) !== CLAUDE_CLOUD_PROVIDER) return;
|
|
4247
|
+
const select = document.getElementById('model-select');
|
|
4248
|
+
// Without a healthy catalog there is nothing to rebuild from (and a rebuild
|
|
4249
|
+
// would raise the metadata blocker); the next catalog load builds the list
|
|
4250
|
+
// with whatever settings have arrived by then.
|
|
4251
|
+
if (!select || !isModelMetadataHealthy()) return;
|
|
4252
|
+
// Dropping the scope makes syncAutoModelAvailability rebuild the options.
|
|
4253
|
+
delete select.dataset.providerScope;
|
|
4254
|
+
syncAutoModelAvailability();
|
|
4255
|
+
}
|
|
4256
|
+
|
|
3922
4257
|
function showAuthGate(error = '') {
|
|
3923
|
-
document.getElementById('startup-loading')?.remove();
|
|
3924
4258
|
document.getElementById('auth-gate').style.display = 'flex';
|
|
3925
4259
|
document.getElementById('app').classList.remove('visible');
|
|
3926
4260
|
showAuthError(error);
|
|
3927
|
-
|
|
4261
|
+
// The start screen hands its logo over to the box first (installed app);
|
|
4262
|
+
// focusing earlier would open the keyboard and move the box under it.
|
|
4263
|
+
const focusToken = () => document.getElementById('token-input')?.focus();
|
|
4264
|
+
const handover = window.__startupSplash?.toAuth?.();
|
|
4265
|
+
if (handover && window.__startupSplash.animated) void handover.then(focusToken);
|
|
4266
|
+
else focusToken();
|
|
3928
4267
|
}
|
|
3929
4268
|
|
|
3930
4269
|
// Runs before anything reads MODEL_STORAGE_KEY so the first load after an
|
|
@@ -3950,6 +4289,7 @@ async function initApp() {
|
|
|
3950
4289
|
setForegroundNetworkWorkEnabled(document.visibilityState === 'visible');
|
|
3951
4290
|
const sharedThemeBtn = document.getElementById('chat-menu-shared-theme-toggle');
|
|
3952
4291
|
initTheme();
|
|
4292
|
+
initScrollbarHover();
|
|
3953
4293
|
initThemeMenuToggle();
|
|
3954
4294
|
if (sharedThemeBtn && sharedMode) sharedThemeBtn.hidden = false;
|
|
3955
4295
|
initFontScaling();
|
|
@@ -3982,7 +4322,8 @@ async function initApp() {
|
|
|
3982
4322
|
void flushConversationDraft(currentConvId);
|
|
3983
4323
|
closeTmuxInspectorView();
|
|
3984
4324
|
});
|
|
3985
|
-
|
|
4325
|
+
// The start screen lies over the app and leaves once its animation is done.
|
|
4326
|
+
void window.__startupSplash?.finish?.();
|
|
3986
4327
|
document.getElementById('auth-gate').style.display = 'none';
|
|
3987
4328
|
document.getElementById('app').classList.add('visible');
|
|
3988
4329
|
initSidebarLayout();
|
|
@@ -4023,6 +4364,17 @@ async function initApp() {
|
|
|
4023
4364
|
});
|
|
4024
4365
|
});
|
|
4025
4366
|
}
|
|
4367
|
+
const chatMenuArchiveBtn = document.getElementById('chat-menu-archive');
|
|
4368
|
+
bindMenuAction(chatMenuArchiveBtn, (event) => {
|
|
4369
|
+
event.preventDefault();
|
|
4370
|
+
event.stopPropagation();
|
|
4371
|
+
lockChatActionsMenuShield(350);
|
|
4372
|
+
closeChatActionsMenu();
|
|
4373
|
+
const convId = String(currentConvId || '').trim();
|
|
4374
|
+
if (!convId) return;
|
|
4375
|
+
const archived = conversations[convId]?.archived === true;
|
|
4376
|
+
void (archived ? unarchiveConv(null, convId) : archiveConv(null, convId));
|
|
4377
|
+
});
|
|
4026
4378
|
const chatMenuShareConversationBtn = document.getElementById('chat-menu-share-conversation');
|
|
4027
4379
|
bindMenuAction(chatMenuShareConversationBtn, (event) => {
|
|
4028
4380
|
event.preventDefault();
|
|
@@ -4082,6 +4434,14 @@ async function initApp() {
|
|
|
4082
4434
|
closeChatActionsMenu();
|
|
4083
4435
|
openGitChangesModal();
|
|
4084
4436
|
});
|
|
4437
|
+
const chatMenuOpenCloudSessionBtn = document.getElementById('chat-menu-open-cloud-session');
|
|
4438
|
+
bindMenuAction(chatMenuOpenCloudSessionBtn, (event) => {
|
|
4439
|
+
event.preventDefault();
|
|
4440
|
+
event.stopPropagation();
|
|
4441
|
+
lockChatActionsMenuShield(350);
|
|
4442
|
+
closeChatActionsMenu();
|
|
4443
|
+
openCurrentCloudSession(showExternalLinkFallback);
|
|
4444
|
+
});
|
|
4085
4445
|
const chatMenuSettingsBtn = document.getElementById('chat-menu-settings');
|
|
4086
4446
|
bindMenuAction(chatMenuSettingsBtn, (event) => {
|
|
4087
4447
|
event.preventDefault();
|
|
@@ -4172,6 +4532,7 @@ async function initApp() {
|
|
|
4172
4532
|
}
|
|
4173
4533
|
initAutoCompactWindowControl();
|
|
4174
4534
|
initThinkingControl();
|
|
4535
|
+
initAttributionControl();
|
|
4175
4536
|
initModeSelector();
|
|
4176
4537
|
initModelSelector();
|
|
4177
4538
|
initReasoningSelector();
|
|
@@ -4190,6 +4551,11 @@ async function initApp() {
|
|
|
4190
4551
|
await refreshOpenAISettingsState();
|
|
4191
4552
|
await refreshCursorSettingsState();
|
|
4192
4553
|
await refreshGrokSettingsState();
|
|
4554
|
+
// Not awaited: only a Claude Cloud conversation needs it (its model list),
|
|
4555
|
+
// and the subscription below rebuilds that list when the answer arrives. A
|
|
4556
|
+
// relay without the provider answers nothing and nothing changes.
|
|
4557
|
+
subscribeClaudeCloudSettings(refreshClaudeCloudComposerModels);
|
|
4558
|
+
void refreshClaudeCloudSettingsState();
|
|
4193
4559
|
await refreshModelCatalog(true);
|
|
4194
4560
|
initFullscreenButton();
|
|
4195
4561
|
initInstallButton();
|
|
@@ -4311,6 +4677,9 @@ window.dismissAvailableUpdate = dismissAvailableUpdate;
|
|
|
4311
4677
|
window.dismissUpdateOutcome = dismissUpdateOutcome;
|
|
4312
4678
|
window.saveGrokSettings = saveGrokSettings;
|
|
4313
4679
|
window.toggleGrokProvider = toggleGrokProvider;
|
|
4680
|
+
window.saveClaudeCloudSettings = saveClaudeCloudSettings;
|
|
4681
|
+
window.toggleClaudeCloudProvider = toggleClaudeCloudProvider;
|
|
4682
|
+
window.openClaudeCloudLoginTab = openClaudeCloudLoginTab;
|
|
4314
4683
|
window.saveCursorSettings = saveCursorSettings;
|
|
4315
4684
|
window.removeCursorSettings = removeCursorSettings;
|
|
4316
4685
|
window.saveCursorAllowanceSettings = saveCursorAllowanceSettings;
|
|
@@ -4334,6 +4703,9 @@ window.addRemoteRelayFromForm = addRemoteRelayFromForm;
|
|
|
4334
4703
|
window.showRemoteRelayTokenField = showRemoteRelayTokenField;
|
|
4335
4704
|
window.saveRemoteRelayPublicUrl = saveRemoteRelayPublicUrl;
|
|
4336
4705
|
window.toggleRemoteRelayInbound = toggleRemoteRelayInbound;
|
|
4706
|
+
window.toggleAgentSessions = toggleAgentSessions;
|
|
4707
|
+
window.previewAgentSessionsMaxWait = previewAgentSessionsMaxWait;
|
|
4708
|
+
window.saveAgentSessionsMaxWait = saveAgentSessionsMaxWait;
|
|
4337
4709
|
window.doAuth = doAuth;
|
|
4338
4710
|
window.initApp = initApp;
|
|
4339
4711
|
window.connectSocket = connectSocket;
|
|
@@ -4370,7 +4742,12 @@ window.newConversation = newConversation;
|
|
|
4370
4742
|
window.confirmNewConversationModel = confirmNewConversationModel;
|
|
4371
4743
|
window.closeNewConversationModelModal = closeNewConversationModelModal;
|
|
4372
4744
|
window.deleteConv = deleteConv;
|
|
4745
|
+
window.archiveConv = archiveConv;
|
|
4746
|
+
window.unarchiveConv = unarchiveConv;
|
|
4747
|
+
window.toggleArchivedConversations = toggleArchivedConversations;
|
|
4748
|
+
window.refreshNewConversationCloudRepoList = refreshNewConversationCloudRepoList;
|
|
4373
4749
|
window.openConversation = openConversation;
|
|
4750
|
+
window.openOriginConversation = openOriginConversation;
|
|
4374
4751
|
window.toggleStatusView = toggleStatusView;
|
|
4375
4752
|
window.refreshConversations = refreshConversations;
|
|
4376
4753
|
window.renderConvList = renderConvList;
|
|
@@ -4428,6 +4805,9 @@ window.syncChatTitleControls = syncChatTitleControls;
|
|
|
4428
4805
|
window.closeChatActionsMenu = closeChatActionsMenu;
|
|
4429
4806
|
window.openSuspendHostConfirmation = openSuspendHostConfirmation;
|
|
4430
4807
|
window.confirmKillCurrentSession = confirmKillCurrentSession;
|
|
4808
|
+
// The conversation list's context menu reaches these for the open conversation.
|
|
4809
|
+
window.openKillSessionConfirmation = openKillSessionConfirmation;
|
|
4810
|
+
window.openChatTitleEditor = openChatTitleEditor;
|
|
4431
4811
|
window.confirmRestartWebRelay = confirmRestartWebRelay;
|
|
4432
4812
|
window.confirmSuspendHost = confirmSuspendHost;
|
|
4433
4813
|
window.cancelQueuedHostSuspend = cancelQueuedHostSuspend;
|