@oar-sh/oar 0.9.24 → 0.9.26
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/.github/extensions/web-relay/skills/reasoning-stream.mjs +3 -1
- package/CHANGELOG.md +284 -0
- package/README.md +12 -11
- package/package.json +5 -1
- package/server/claude-worker/claude-remote-relay-tool.mjs +15 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +8 -4
- package/server/claude-worker/claude-session-process.mjs +4 -0
- package/server/claude-worker/claude-turn-publisher.mjs +25 -11
- package/server/claude-worker/sdk-message-normalizer.mjs +29 -1
- package/server/copilot-worker/copilot-prompt-context.mjs +2 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +11 -9
- package/server/cursor-worker/cursor-turn-runner.mjs +3 -1
- package/server/cursor-worker/sdk-message-normalizer.mjs +13 -0
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/grok-turn-runner.mjs +3 -1
- package/server/grok-worker/sdk-message-normalizer.mjs +3 -0
- package/server/migrations/0039-media-index.mjs +167 -0
- package/server/migrations/0040-image-session-defaults.mjs +93 -0
- package/server/public/app/api-client.js +58 -0
- package/server/public/app/ask-user-view.js +2 -1
- package/server/public/app/attachments-view.js +66 -9
- package/server/public/app/bootstrap.js +85 -25
- package/server/public/app/chat-actions-menu-state.mjs +1 -0
- package/server/public/app/context-media-tab.js +143 -0
- package/server/public/app/context-usage-view.mjs +2 -2
- package/server/public/app/conversation-view.js +90 -5
- package/server/public/app/image-composer-ui.js +518 -0
- package/server/public/app/image-turn-placeholder.mjs +233 -0
- package/server/public/app/journal-view.js +12 -2
- package/server/public/app/live-activity-cap.mjs +3 -3
- package/server/public/app/markdown-math.mjs +471 -0
- package/server/public/app/media-gallery-model.mjs +202 -0
- package/server/public/app/media-gallery-ui.js +524 -0
- package/server/public/app/media-view.js +79 -0
- package/server/public/app/openai-image-composer.mjs +492 -0
- package/server/public/app/openai-image-cost-ui.js +426 -0
- package/server/public/app/openai-image-spend.mjs +443 -0
- package/server/public/app/plan-usage-view.mjs +38 -6
- package/server/public/app/pwa-install.js +25 -4
- package/server/public/app/relay-error-ctas.mjs +5 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +3 -0
- package/server/public/app/relay-restart-ui.js +2 -2
- package/server/public/app/remote-relays-settings.js +33 -6
- package/server/public/app/router.js +49 -237
- package/server/public/app/session-history-ui.js +2 -0
- package/server/public/app/settings-modal.js +4 -0
- package/server/public/app/settings-tabs.js +10 -1
- package/server/public/app/socket-handlers.js +26 -0
- package/server/public/app/store.js +2 -0
- package/server/public/app/stream-view-preference.mjs +1 -1
- package/server/public/app/tab-strip-fade.mjs +1 -1
- package/server/public/app/thoughts-preference.mjs +1 -1
- package/server/public/app/tmux-inspector-view.js +29 -16
- package/server/public/app/viewer-gallery.mjs +23 -1
- package/server/public/index.html +517 -123
- package/server/relay-tools.md +1 -0
- package/server/repositories/image-conversation-repository.mjs +69 -0
- package/server/repositories/media-repository.mjs +305 -0
- package/server/routes/media-routes.mjs +361 -0
- package/server/routes/messages-routes.mjs +1410 -360
- package/server/routes/relay-board-routes.mjs +46 -1
- package/server/routes/remote-relay-routes.mjs +1 -1
- package/server/routes/sessions-routes.mjs +361 -14
- package/server/server-runtime.mjs +21 -0
- package/server/services/database-file.mjs +6 -4
- package/server/services/generated-image-cleanup-service.mjs +2 -1
- package/server/services/generated-image-files.mjs +110 -0
- package/server/services/image-input-paths.mjs +389 -0
- package/server/services/image-operation-service.mjs +6 -2
- package/server/services/media-extract.mjs +653 -0
- package/server/services/media-index-service.mjs +221 -0
- package/server/services/openai-image-preview-turns.mjs +330 -0
- package/server/services/openai-image-spend-service.mjs +516 -0
- package/server/services/openai-image-stream.mjs +170 -0
- package/server/services/plan-usage-openai-image.mjs +148 -0
- package/server/services/relay-prompt-sanitizer.mjs +7 -0
- package/server/services/relay-restart-queue.mjs +1 -1
- package/server/services/remote-relay-caller-context.mjs +6 -0
- package/server/services/remote-relay-dispatcher.mjs +409 -23
- package/server/services/remote-relay-registry-service.mjs +10 -2
- package/server/services/session-worker-stop-service.mjs +50 -18
- package/server/services/token-usage-service.mjs +15 -0
- package/shared/image-header.mjs +90 -0
- package/shared/math-instructions.mjs +30 -0
- package/shared/openai-image-options.mjs +299 -0
- package/shared/openai-image-presets.mjs +175 -0
- package/shared/openai-image-previews.mjs +40 -0
- package/shared/openai-image-prices.mjs +291 -0
- package/shared/openai-image-spend.mjs +90 -0
- package/shared/remote-relay-contract.mjs +126 -4
- package/shared/worker-runtime/direct-openai-image.mjs +96 -50
- package/server/public/app/math-dollars.mjs +0 -89
|
@@ -81,6 +81,8 @@ import {
|
|
|
81
81
|
galleryNeighborIndex,
|
|
82
82
|
galleryNeighbors,
|
|
83
83
|
galleryPositionLabel,
|
|
84
|
+
galleryCanStep,
|
|
85
|
+
extendGallery,
|
|
84
86
|
} from './viewer-gallery.mjs';
|
|
85
87
|
import {
|
|
86
88
|
deepestExistingAncestor,
|
|
@@ -672,9 +674,10 @@ function updateFilePreviewUiState() {
|
|
|
672
674
|
if (bottomChrome) {
|
|
673
675
|
bottomChrome.hidden = !gallery;
|
|
674
676
|
document.getElementById('file-preview-position').textContent = galleryPositionLabel(gallery);
|
|
675
|
-
document.getElementById('file-preview-prev').disabled =
|
|
676
|
-
document.getElementById('file-preview-next').disabled =
|
|
677
|
+
document.getElementById('file-preview-prev').disabled = !galleryCanStep(gallery, -1);
|
|
678
|
+
document.getElementById('file-preview-next').disabled = !galleryCanStep(gallery, 1);
|
|
677
679
|
}
|
|
680
|
+
renderGalleryDetails(gallery);
|
|
678
681
|
}
|
|
679
682
|
|
|
680
683
|
/**
|
|
@@ -1231,7 +1234,7 @@ export function openUploadedAttachmentViewer(name, contentUrl, mimeType, options
|
|
|
1231
1234
|
kind: uploadPreviewKind(mimeType),
|
|
1232
1235
|
name: String(name || 'attachment'),
|
|
1233
1236
|
rawUrl: String(contentUrl || ''),
|
|
1234
|
-
size: 0,
|
|
1237
|
+
size: Number(options?.size) > 0 ? Number(options.size) : 0,
|
|
1235
1238
|
contentType: String(mimeType || '').toLowerCase(),
|
|
1236
1239
|
},
|
|
1237
1240
|
});
|
|
@@ -1249,7 +1252,7 @@ export function openGalleryItem(item, gallery) {
|
|
|
1249
1252
|
if (!item) return Promise.resolve(false);
|
|
1250
1253
|
const source = item.source || gallery?.source || 'workspace';
|
|
1251
1254
|
if (source === 'upload') {
|
|
1252
|
-
openUploadedAttachmentViewer(item.name, item.href, item.mime, { gallery });
|
|
1255
|
+
openUploadedAttachmentViewer(item.name, item.href, item.mime, { gallery, size: item.size });
|
|
1253
1256
|
return Promise.resolve(true);
|
|
1254
1257
|
}
|
|
1255
1258
|
const open = source === 'workspace' ? openWorkspaceFilePreview : openDriveFilePreview;
|
|
@@ -1335,7 +1338,9 @@ export function renderFilePreview() {
|
|
|
1335
1338
|
const kindLabel = String(payload.kind || 'text').toUpperCase();
|
|
1336
1339
|
const langLabel = payload.language ? ` · ${payload.language}` : '';
|
|
1337
1340
|
const truncatedLabel = payload.truncated ? ` · truncated to ${formatBytes(FILE_PREVIEW_MAX_BYTES)}` : '';
|
|
1338
|
-
|
|
1341
|
+
// An attachment opened by its address may come without a size: no "0 B" then.
|
|
1342
|
+
const sizeLabel = payload.size > 0 || filePreviewState.source !== 'upload' ? ` · ${formatBytes(payload.size || 0)}` : '';
|
|
1343
|
+
metaEl.textContent = `${kindLabel}${langLabel}${sizeLabel}${truncatedLabel}`;
|
|
1339
1344
|
|
|
1340
1345
|
const cardMeta = payload.size > 0 ? formatBytes(payload.size) : '';
|
|
1341
1346
|
|
|
@@ -1560,13 +1565,65 @@ export async function openWorkspaceFilePreviewFromRepo(rawPath, options = {}) {
|
|
|
1560
1565
|
|
|
1561
1566
|
/** Show the file `delta` steps along the gallery; false at either end. */
|
|
1562
1567
|
export async function stepFilePreviewGallery(delta) {
|
|
1563
|
-
|
|
1564
|
-
|
|
1568
|
+
let gallery = filePreviewState.gallery;
|
|
1569
|
+
let index = galleryNeighborIndex(gallery, delta);
|
|
1570
|
+
if (index === -1 && galleryCanStep(gallery, delta)) {
|
|
1571
|
+
// A paged gallery: the next page is fetched and the step goes on into it.
|
|
1572
|
+
let page = null;
|
|
1573
|
+
try {
|
|
1574
|
+
page = await gallery.loadMore();
|
|
1575
|
+
} catch {
|
|
1576
|
+
page = null;
|
|
1577
|
+
}
|
|
1578
|
+
if (filePreviewState.gallery !== gallery) return false;
|
|
1579
|
+
gallery = extendGallery(gallery, page || { items: [], hasMore: false });
|
|
1580
|
+
filePreviewState.gallery = gallery;
|
|
1581
|
+
index = galleryNeighborIndex(gallery, delta);
|
|
1582
|
+
if (index === -1) {
|
|
1583
|
+
renderFilePreview();
|
|
1584
|
+
return false;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1565
1587
|
if (index === -1) return false;
|
|
1566
1588
|
const next = galleryAt(gallery, index);
|
|
1567
1589
|
return openGalleryItem(next.items[index], next);
|
|
1568
1590
|
}
|
|
1569
1591
|
|
|
1592
|
+
// What a gallery says about its open item (the media gallery: session,
|
|
1593
|
+
// date, who sent it) and the buttons it adds ("Open in session", "Copy
|
|
1594
|
+
// path"), built here so the viewer markup stays the same for every gallery.
|
|
1595
|
+
function renderGalleryDetails(gallery) {
|
|
1596
|
+
const toolbar = document.querySelector('#file-preview-chrome-top .file-preview-toolbar');
|
|
1597
|
+
let caption = document.getElementById('file-preview-gallery-caption');
|
|
1598
|
+
if (!caption && toolbar) {
|
|
1599
|
+
caption = document.createElement('div');
|
|
1600
|
+
caption.id = 'file-preview-gallery-caption';
|
|
1601
|
+
caption.className = 'file-preview-meta';
|
|
1602
|
+
toolbar.insertAdjacentElement('afterend', caption);
|
|
1603
|
+
}
|
|
1604
|
+
document.querySelectorAll('[data-gallery-action]').forEach((button) => button.remove());
|
|
1605
|
+
const item = gallery?.items?.[gallery.index];
|
|
1606
|
+
const details = item && typeof gallery.describe === 'function' ? gallery.describe(item) : null;
|
|
1607
|
+
if (caption) {
|
|
1608
|
+
caption.textContent = details?.caption || '';
|
|
1609
|
+
caption.hidden = !details?.caption;
|
|
1610
|
+
}
|
|
1611
|
+
if (!toolbar || !Array.isArray(details?.actions)) return;
|
|
1612
|
+
const anchor = document.getElementById('file-preview-copy-btn');
|
|
1613
|
+
for (const action of details.actions) {
|
|
1614
|
+
const button = document.createElement('button');
|
|
1615
|
+
button.type = 'button';
|
|
1616
|
+
button.className = 'file-preview-btn';
|
|
1617
|
+
button.textContent = action.label;
|
|
1618
|
+
button.dataset.galleryAction = action.id;
|
|
1619
|
+
button.addEventListener('click', () => {
|
|
1620
|
+
viewerControls.interaction();
|
|
1621
|
+
void action.run(item);
|
|
1622
|
+
});
|
|
1623
|
+
toolbar.insertBefore(button, anchor || null);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1570
1627
|
// Preview payloads of the open file's neighbours, fetched ahead so a swipe
|
|
1571
1628
|
// lands on the file and not on "Fetching…"; an image's bytes are asked for
|
|
1572
1629
|
// too, under the very address the viewer will use. Entries are promises.
|
|
@@ -2935,8 +2992,8 @@ const viewerSwipe = createSwipeRecognizer({
|
|
|
2935
2992
|
return;
|
|
2936
2993
|
}
|
|
2937
2994
|
// Past either end the content gives a little, then holds.
|
|
2938
|
-
|
|
2939
|
-
const shift =
|
|
2995
|
+
// A page still to load counts as somewhere to go.
|
|
2996
|
+
const shift = galleryCanStep(filePreviewState.gallery, dx < 0 ? 1 : -1) ? dx : dx * 0.25;
|
|
2940
2997
|
bodyEl.style.transform = `translateX(${shift}px)`;
|
|
2941
2998
|
},
|
|
2942
2999
|
onEnd: ({ axis, swipe }) => {
|
|
@@ -161,6 +161,7 @@ import { loadKeepingLiveRows } from './transcript-reload-freshness.mjs';
|
|
|
161
161
|
import { backgroundTasksSnapshotMark } from './background-tasks-view.mjs';
|
|
162
162
|
import { newerPageReachesEnd } from './transcript-island.mjs';
|
|
163
163
|
import { initPinnedMessagesView, openPinnedMessagesModal, setPinsConversation } from './pinned-messages-view.mjs';
|
|
164
|
+
import { afterContextRender, initContextTabs, leaveContextTab, resetContextTab, wrapContextBody } from './context-media-tab.js';
|
|
164
165
|
import { initHiddenMessagesView, openHiddenMessagesModal, setHiddenMessagesConversation } from './hidden-messages-view.mjs';
|
|
165
166
|
import { bindChatSelectionGuard, chatSelectionGuard, isChatInteractionHeld } from './selection-guard.mjs';
|
|
166
167
|
import { loadRepoBrowserTree, openRepoBrowser, closeRepoBrowser, setRepoBrowserSessionInfo, resetWorkspaceRepoBrowserForRootChange } from './attachments-view.js';
|
|
@@ -221,6 +222,13 @@ import {
|
|
|
221
222
|
sessionLockNoteText,
|
|
222
223
|
sessionLockProviderKey,
|
|
223
224
|
} from './conversation-provider-indicator.mjs';
|
|
225
|
+
import { imageQualityOptions } from './openai-image-composer.mjs';
|
|
226
|
+
import { hideImageComposerControls, syncImageComposerControls } from './image-composer-ui.js';
|
|
227
|
+
import {
|
|
228
|
+
onOpenAIImageSpendChange,
|
|
229
|
+
syncChatMenuImageCost,
|
|
230
|
+
syncOpenAIImageSpendBanner,
|
|
231
|
+
} from './openai-image-cost-ui.js';
|
|
224
232
|
import {
|
|
225
233
|
initMessageSearchView,
|
|
226
234
|
openMessageSearchModal,
|
|
@@ -278,6 +286,7 @@ import { closeQuestionsPanel, initQuestionsPanel, openQuestionsPanel, setQuestio
|
|
|
278
286
|
import { questionsTwinLabel } from './questions-view.mjs';
|
|
279
287
|
import { closeRelaySchedulePanel } from './relay-schedules-ui.js';
|
|
280
288
|
import { isInboxViewActive } from './inbox-ui.js';
|
|
289
|
+
import { isMediaViewActive, leaveMediaView, toggleMediaView } from './media-view.js';
|
|
281
290
|
import { initScrollbarHover } from './scrollbar-hover.mjs';
|
|
282
291
|
import {
|
|
283
292
|
autoCompactWindowNotes,
|
|
@@ -297,11 +306,11 @@ import {
|
|
|
297
306
|
autoCompactWindowFromIndex,
|
|
298
307
|
formatAutoCompactWindowLabel,
|
|
299
308
|
} from './auto-compact-window-options.mjs';
|
|
300
|
-
import { renderPlanUsageHtml, planUsageSubtitle, normalizeUsageProvider } from './plan-usage-view.mjs';
|
|
309
|
+
import { renderPlanUsageHtml, renderPlanUsageCardHtml, planUsageSubtitle, normalizeUsageProvider } from './plan-usage-view.mjs';
|
|
301
310
|
import { revealSelectedTabs, watchTabStripFades } from './tab-strip-fade.mjs';
|
|
302
311
|
import { FONT_SCALE_CHANGE_EVENT, getUiScale, initFontScaling, updateFontScaleFromSelect } from './font-scaling.js';
|
|
303
312
|
import { initClientDiagnostics, recordStatusEvent } from './status-store.mjs';
|
|
304
|
-
import { isStatusViewActive, toggleStatusView } from './status-view.mjs';
|
|
313
|
+
import { isStatusViewActive, leaveStatusView, toggleStatusView } from './status-view.mjs';
|
|
305
314
|
import { installExternalLinkPolicy, openExternalNavigation } from './external-link-policy.mjs';
|
|
306
315
|
import {
|
|
307
316
|
initCwdPicker,
|
|
@@ -1659,13 +1668,6 @@ function modelProvidersForId(modelId, providersByModel = {}) {
|
|
|
1659
1668
|
return providers.map((provider) => String(provider || '').trim().toLowerCase()).filter(Boolean);
|
|
1660
1669
|
}
|
|
1661
1670
|
|
|
1662
|
-
function openAIImageSizesForModel(modelId = '') {
|
|
1663
|
-
const normalized = String(modelId || '').trim().toLowerCase().replace(/^openai\//, '');
|
|
1664
|
-
if (normalized.startsWith('dall-e-2')) return ['256x256', '512x512', '1024x1024'];
|
|
1665
|
-
if (normalized.startsWith('dall-e-3')) return ['1024x1024', '1792x1024', '1024x1792'];
|
|
1666
|
-
return ['auto', '1024x1024', '1536x1024', '1024x1536'];
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
1671
|
function modelVisibleForActiveProvider(modelId, activeProviderType, providersByModel = {}) {
|
|
1670
1672
|
const normalizedModelId = String(modelId || '').trim().toLowerCase();
|
|
1671
1673
|
if (!normalizedModelId) return false;
|
|
@@ -1799,8 +1801,12 @@ function selectedReasoningEffortValue() {
|
|
|
1799
1801
|
function updateReasoningSelectorForModel(modelId, preferredEffort = '', { persist = false } = {}) {
|
|
1800
1802
|
const select = document.getElementById('reasoning-effort-select');
|
|
1801
1803
|
if (!select) return;
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
+
const imageModel = isOpenAIImageModelId(modelId);
|
|
1805
|
+
select.title = imageModel ? 'Quality' : 'Reasoning effort';
|
|
1806
|
+
// An image model's Quality: the catalog's values its family takes.
|
|
1807
|
+
const options = imageModel
|
|
1808
|
+
? imageQualityOptions(modelId, reasoningOptionsForModel(modelId))
|
|
1809
|
+
: reasoningOptionsForModel(modelId);
|
|
1804
1810
|
const currentEffort = String(select.value || '').trim().toLowerCase();
|
|
1805
1811
|
const storedEffort = String(localStorage.getItem(REASONING_STORAGE_KEY) || '').trim().toLowerCase();
|
|
1806
1812
|
select.innerHTML = '';
|
|
@@ -2082,20 +2088,16 @@ function updateContextTierSelector(modelId) {
|
|
|
2082
2088
|
const select = document.getElementById('context-tier-select');
|
|
2083
2089
|
if (!select) return;
|
|
2084
2090
|
if (isOpenAIImageModelId(modelId)) {
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
option.value = size;
|
|
2091
|
-
option.textContent = size;
|
|
2092
|
-
select.appendChild(option);
|
|
2093
|
-
}
|
|
2094
|
-
select.value = imageSizes.includes(current) ? current : 'auto';
|
|
2095
|
-
select.title = 'Image size';
|
|
2091
|
+
// Size (with Custom…), Count, Format and Background: image-composer-ui.js,
|
|
2092
|
+
// from the conversation's stored values.
|
|
2093
|
+
syncImageComposerControls(modelId, currentConvId);
|
|
2094
|
+
// Today's image spend near or at the daily cap: the banner of an image chat.
|
|
2095
|
+
syncOpenAIImageSpendBanner({ imageChat: true });
|
|
2096
2096
|
updateModelPricingDetails(modelId, 'default');
|
|
2097
2097
|
return;
|
|
2098
2098
|
}
|
|
2099
|
+
hideImageComposerControls();
|
|
2100
|
+
syncOpenAIImageSpendBanner({ imageChat: false });
|
|
2099
2101
|
const tierOptions = buildContextTierOptions({
|
|
2100
2102
|
modelId,
|
|
2101
2103
|
providerType: activeComposerProviderType(),
|
|
@@ -3454,6 +3456,27 @@ function usageCardIsFresh(provider) {
|
|
|
3454
3456
|
return Number.isFinite(at) && Date.now() - at < USAGE_LIVE_FRESH_MS;
|
|
3455
3457
|
}
|
|
3456
3458
|
|
|
3459
|
+
/**
|
|
3460
|
+
* A new OpenAI image spend state (the socket event): the kept report's
|
|
3461
|
+
* 'openai' card takes it, and the open modal redraws that card alone.
|
|
3462
|
+
*/
|
|
3463
|
+
function applyImageSpendToUsageReport(state) {
|
|
3464
|
+
if (!state || !Array.isArray(lastUsageReport?.providers)) return;
|
|
3465
|
+
const index = lastUsageReport.providers.findIndex((card) => card?.imageSpend && normalizeUsageProvider(card.provider) === 'openai');
|
|
3466
|
+
if (index < 0) return;
|
|
3467
|
+
const card = { ...lastUsageReport.providers[index], imageSpend: { ...lastUsageReport.providers[index].imageSpend, ...state } };
|
|
3468
|
+
const providers = lastUsageReport.providers.slice();
|
|
3469
|
+
providers[index] = card;
|
|
3470
|
+
storeUsageReport({ ...lastUsageReport, providers });
|
|
3471
|
+
if (summaryModalState.kind !== 'usage') return;
|
|
3472
|
+
const panel = document.getElementById('summary-modal-body')?.querySelector('[data-usage-panel="openai"]');
|
|
3473
|
+
if (!panel) return;
|
|
3474
|
+
const updating = panel.hasAttribute('data-updating');
|
|
3475
|
+
panel.outerHTML = renderPlanUsageCardHtml(card, { active: !panel.hidden });
|
|
3476
|
+
if (updating) markUsageCardUpdating('openai', true);
|
|
3477
|
+
}
|
|
3478
|
+
onOpenAIImageSpendChange(applyImageSpendToUsageReport);
|
|
3479
|
+
|
|
3457
3480
|
/** The "Updating…" marker on one card, while its live read runs. */
|
|
3458
3481
|
function markUsageCardUpdating(provider, updating) {
|
|
3459
3482
|
const body = document.getElementById('summary-modal-body');
|
|
@@ -4036,11 +4059,13 @@ async function loadContextSummaryAndRender(convId) {
|
|
|
4036
4059
|
const detailText = String(payload.text || '').trim();
|
|
4037
4060
|
// The structured breakdown is the answer; the runtime's own text dump stays
|
|
4038
4061
|
// available underneath for the details it carries that categories don't.
|
|
4039
|
-
const
|
|
4062
|
+
const contextHtml = usageHtml
|
|
4040
4063
|
? `${usageHtml}${autoCompactHtml}${thinkingHtml}${thoughtsHtml}${attributionHtml}${agentSessionsHtml}${detailText
|
|
4041
4064
|
? `<details class="ctx-usage-raw"><summary>Raw details</summary><pre>${escHtml(detailText)}</pre></details>`
|
|
4042
4065
|
: ''}`
|
|
4043
4066
|
: `${autoCompactHtml}${thinkingHtml}${thoughtsHtml}${attributionHtml}${agentSessionsHtml}<pre>${escHtml(detailText || 'No context data available.')}</pre>`;
|
|
4067
|
+
// Two categories: Context (this body) and the session's Media gallery.
|
|
4068
|
+
const bodyHtml = wrapContextBody(contextHtml, settingsConversationId);
|
|
4044
4069
|
|
|
4045
4070
|
renderSummaryModalContent({
|
|
4046
4071
|
title: 'Context usage',
|
|
@@ -4049,7 +4074,11 @@ async function loadContextSummaryAndRender(convId) {
|
|
|
4049
4074
|
refresh: () => loadContextSummaryAndRender(refreshLookupId),
|
|
4050
4075
|
kind: 'context',
|
|
4051
4076
|
});
|
|
4052
|
-
setSummaryModalLeaveHandler('context', () =>
|
|
4077
|
+
setSummaryModalLeaveHandler('context', () => {
|
|
4078
|
+
commitAutoCompactDraft();
|
|
4079
|
+
leaveContextTab();
|
|
4080
|
+
});
|
|
4081
|
+
afterContextRender();
|
|
4053
4082
|
}
|
|
4054
4083
|
|
|
4055
4084
|
async function showUsage() {
|
|
@@ -4111,6 +4140,9 @@ async function showContext() {
|
|
|
4111
4140
|
});
|
|
4112
4141
|
return;
|
|
4113
4142
|
}
|
|
4143
|
+
// A fresh open shows Context first; Media is one tap away.
|
|
4144
|
+
initContextTabs();
|
|
4145
|
+
resetContextTab(convId);
|
|
4114
4146
|
if (btn) {
|
|
4115
4147
|
btn.textContent = '⏳';
|
|
4116
4148
|
btn.disabled = true;
|
|
@@ -5141,6 +5173,13 @@ function toggleChatActionsMenu() {
|
|
|
5141
5173
|
syncThemeMenuLabel();
|
|
5142
5174
|
syncAboutMenuLabel();
|
|
5143
5175
|
const willOpen = !!menu.hidden;
|
|
5176
|
+
// An image chat's cost, asked from the relay each time the menu opens.
|
|
5177
|
+
if (willOpen) {
|
|
5178
|
+
void syncChatMenuImageCost({
|
|
5179
|
+
conversationId: currentConvId,
|
|
5180
|
+
imageChat: isOpenAIImageModelId(document.getElementById('model-select')?.value || ''),
|
|
5181
|
+
}).then(() => { if (!menu.hidden) placeChatActionsMenu(); });
|
|
5182
|
+
}
|
|
5144
5183
|
menu.hidden = !willOpen;
|
|
5145
5184
|
trigger.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
|
|
5146
5185
|
if (willOpen) {
|
|
@@ -5717,6 +5756,14 @@ async function initApp() {
|
|
|
5717
5756
|
closeChatActionsMenu();
|
|
5718
5757
|
openCurrentCloudSession(showExternalLinkFallback);
|
|
5719
5758
|
});
|
|
5759
|
+
// The global Media view: every session's pictures, in the transcript's place.
|
|
5760
|
+
bindMenuAction(document.getElementById('chat-menu-media'), (event) => {
|
|
5761
|
+
event.preventDefault();
|
|
5762
|
+
event.stopPropagation();
|
|
5763
|
+
lockChatActionsMenuShield(350);
|
|
5764
|
+
closeChatActionsMenu();
|
|
5765
|
+
void window.toggleMediaView();
|
|
5766
|
+
});
|
|
5720
5767
|
const chatMenuSettingsBtn = document.getElementById('chat-menu-settings');
|
|
5721
5768
|
bindMenuAction(chatMenuSettingsBtn, (event) => {
|
|
5722
5769
|
event.preventDefault();
|
|
@@ -5990,6 +6037,7 @@ async function bootstrap() {
|
|
|
5990
6037
|
// Leaving the Inbox goes back to the conversation it was opened from.
|
|
5991
6038
|
// Opened from Status it stored none: `returnTo` is the session history stands on.
|
|
5992
6039
|
closeInboxView: ({ returnTo = '' } = {}) => { if (isInboxViewActive()) void toggleInboxView({ returnTo }); },
|
|
6040
|
+
closeMediaView: ({ returnTo = '' } = {}) => { if (isMediaViewActive()) void toggleMediaView({ returnTo }); },
|
|
5993
6041
|
closeChangeCwdMenu: () => closeChangeCwdMenu({ focusTrigger: true }),
|
|
5994
6042
|
closeGitChangesModal,
|
|
5995
6043
|
closeGitDiffViewer,
|
|
@@ -6169,9 +6217,21 @@ window.openOriginConversation = openOriginConversation;
|
|
|
6169
6217
|
// history stands on.
|
|
6170
6218
|
window.toggleStatusView = () => {
|
|
6171
6219
|
leaveInboxView();
|
|
6220
|
+
leaveMediaView();
|
|
6172
6221
|
return toggleStatusView({ returnTo: sessionHistoryConversation() });
|
|
6173
6222
|
};
|
|
6174
|
-
window.toggleInboxView = () =>
|
|
6223
|
+
window.toggleInboxView = () => {
|
|
6224
|
+
leaveMediaView();
|
|
6225
|
+
return toggleInboxView({ returnTo: sessionHistoryConversation() });
|
|
6226
|
+
};
|
|
6227
|
+
// The global Media view shares that place too.
|
|
6228
|
+
window.toggleMediaView = () => toggleMediaView({
|
|
6229
|
+
returnTo: sessionHistoryConversation(),
|
|
6230
|
+
leaveOthers: () => {
|
|
6231
|
+
leaveInboxView();
|
|
6232
|
+
leaveStatusView();
|
|
6233
|
+
},
|
|
6234
|
+
});
|
|
6175
6235
|
window.refreshConversations = refreshConversations;
|
|
6176
6236
|
window.renderConvList = renderConvList;
|
|
6177
6237
|
window.handleAttachmentInput = handleAttachmentInput;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { apiFetch } from './api-client.js';
|
|
2
|
+
import { mountMediaGallery } from './media-gallery-ui.js';
|
|
3
|
+
import { MEDIA_TEXT } from './media-gallery-model.mjs';
|
|
4
|
+
import { closeSummaryModal, escHtml, summaryModalState } from './store.js';
|
|
5
|
+
|
|
6
|
+
// The 🧠 modal's two categories: Context (what the modal always showed, and
|
|
7
|
+
// what it opens on) and Media (this session's gallery, with its count on the
|
|
8
|
+
// tab). The modal body is re-rendered as one string on every refresh, so the
|
|
9
|
+
// tab choice lives here and the gallery is mounted again into its panel.
|
|
10
|
+
|
|
11
|
+
const state = {
|
|
12
|
+
conversationId: '',
|
|
13
|
+
tab: 'context',
|
|
14
|
+
count: null,
|
|
15
|
+
gallery: null,
|
|
16
|
+
bound: false,
|
|
17
|
+
fetchJson: (url) => apiFetch(url),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** A fresh open of 🧠 starts on Context. */
|
|
21
|
+
export function resetContextTab(conversationId = '') {
|
|
22
|
+
destroyGallery();
|
|
23
|
+
state.tab = 'context';
|
|
24
|
+
state.conversationId = String(conversationId || '');
|
|
25
|
+
state.count = null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function countLabel(count) {
|
|
29
|
+
return Number.isFinite(count) ? String(count) : '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The tab strip and the two panels around the Context body. */
|
|
33
|
+
export function wrapContextBody(contextHtml, conversationId) {
|
|
34
|
+
const id = String(conversationId || '');
|
|
35
|
+
if (id !== state.conversationId) {
|
|
36
|
+
state.conversationId = id;
|
|
37
|
+
state.count = null;
|
|
38
|
+
}
|
|
39
|
+
if (!id) return contextHtml;
|
|
40
|
+
const tab = state.tab === 'media' ? 'media' : 'context';
|
|
41
|
+
const tabButton = (name, label, extra = '') => `<button type="button" class="context-tab" role="tab" data-context-tab="${name}" aria-selected="${tab === name}">${label}${extra}</button>`;
|
|
42
|
+
return `<div class="context-tab-strip" role="tablist">${tabButton('context', `🧠 ${escHtml(MEDIA_TEXT.tabContext)}`)}${tabButton('media', `🖼️ ${escHtml(MEDIA_TEXT.tabMedia)}`, `<span class="context-tab-count" data-context-media-count>${escHtml(countLabel(state.count))}</span>`)}</div>`
|
|
43
|
+
+ `<div data-context-panel="context"${tab === 'context' ? '' : ' hidden'}>${contextHtml}</div>`
|
|
44
|
+
+ `<div data-context-panel="media"${tab === 'media' ? '' : ' hidden'}></div>`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function body() {
|
|
48
|
+
return document.getElementById('summary-modal-body');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function showCount() {
|
|
52
|
+
const node = body()?.querySelector('[data-context-media-count]');
|
|
53
|
+
if (node) node.textContent = countLabel(state.count);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function openInSession(media) {
|
|
57
|
+
closeSummaryModal();
|
|
58
|
+
void window.openConversation?.(media.conversationId, {
|
|
59
|
+
aroundMessageId: media.messageId,
|
|
60
|
+
focusMessageId: media.messageId,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// One gallery per open of the modal: a re-render of the modal (Refresh, a
|
|
65
|
+
// socket-driven refresh) moves the same gallery into the new panel, with its
|
|
66
|
+
// pages and scroll; leaving the modal, or opening it for another session,
|
|
67
|
+
// destroys it (its observer and timers go with it).
|
|
68
|
+
function destroyGallery() {
|
|
69
|
+
state.gallery?.destroy();
|
|
70
|
+
state.gallery = null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function mountMediaPanel() {
|
|
74
|
+
const panel = body()?.querySelector('[data-context-panel="media"]');
|
|
75
|
+
if (!panel || panel.hidden) return;
|
|
76
|
+
if (state.gallery && state.gallery.conversationId === state.conversationId) {
|
|
77
|
+
if (state.gallery.root.parentNode !== panel) panel.replaceChildren(state.gallery.root);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
destroyGallery();
|
|
81
|
+
const gallery = mountMediaGallery(panel, {
|
|
82
|
+
conversationId: state.conversationId,
|
|
83
|
+
onOpenInSession: openInSession,
|
|
84
|
+
fetchJson: state.fetchJson,
|
|
85
|
+
raiseViewer: true,
|
|
86
|
+
});
|
|
87
|
+
state.gallery = { ...gallery, conversationId: state.conversationId };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The modal was left (closed, or replaced by another kind): the gallery goes. */
|
|
91
|
+
export function leaveContextTab() {
|
|
92
|
+
destroyGallery();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** After each render of the modal: the count (asked once per open) and the gallery when Media is showing. */
|
|
96
|
+
export function afterContextRender({ fetchJson = (url) => apiFetch(url) } = {}) {
|
|
97
|
+
state.fetchJson = fetchJson;
|
|
98
|
+
if (!state.conversationId) return;
|
|
99
|
+
if (state.count === null) {
|
|
100
|
+
const conversationId = state.conversationId;
|
|
101
|
+
void Promise.resolve(state.fetchJson(`/api/media/counts?conversationId=${encodeURIComponent(conversationId)}`))
|
|
102
|
+
.then((counts) => {
|
|
103
|
+
if (conversationId !== state.conversationId || !counts) return;
|
|
104
|
+
state.count = Number(counts.media) || 0;
|
|
105
|
+
showCount();
|
|
106
|
+
})
|
|
107
|
+
.catch(() => {});
|
|
108
|
+
} else {
|
|
109
|
+
showCount();
|
|
110
|
+
}
|
|
111
|
+
mountMediaPanel();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function selectTab(name) {
|
|
115
|
+
const root = body();
|
|
116
|
+
if (!root) return;
|
|
117
|
+
state.tab = name === 'media' ? 'media' : 'context';
|
|
118
|
+
root.querySelectorAll('[data-context-tab]').forEach((node) => {
|
|
119
|
+
node.setAttribute('aria-selected', String(node.getAttribute('data-context-tab') === state.tab));
|
|
120
|
+
});
|
|
121
|
+
root.querySelectorAll('[data-context-panel]').forEach((node) => {
|
|
122
|
+
node.hidden = node.getAttribute('data-context-panel') !== state.tab;
|
|
123
|
+
});
|
|
124
|
+
mountMediaPanel();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Once: the tab clicks, delegated on the modal body (its content is replaced on every render). */
|
|
128
|
+
export function initContextTabs() {
|
|
129
|
+
if (state.bound) return;
|
|
130
|
+
const root = body();
|
|
131
|
+
if (!root) return;
|
|
132
|
+
state.bound = true;
|
|
133
|
+
root.addEventListener('click', (event) => {
|
|
134
|
+
if (summaryModalState.kind !== 'context') return;
|
|
135
|
+
const target = event.target instanceof Element ? event.target.closest('[data-context-tab]') : null;
|
|
136
|
+
if (!target) return;
|
|
137
|
+
selectTab(target.getAttribute('data-context-tab'));
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function contextTabState() {
|
|
142
|
+
return { ...state };
|
|
143
|
+
}
|
|
@@ -356,7 +356,7 @@ const THOUGHTS_EXPANDED_CHOICES = Object.freeze([
|
|
|
356
356
|
]);
|
|
357
357
|
|
|
358
358
|
/**
|
|
359
|
-
* The mirror of Settings ›
|
|
359
|
+
* The mirror of Settings › Appearance "Expand thoughts while an agent works", for
|
|
360
360
|
* every provider. A setting of this browser, so nothing goes to the relay:
|
|
361
361
|
* the buttons write thoughts-preference.mjs and both places follow it.
|
|
362
362
|
*
|
|
@@ -390,7 +390,7 @@ const STREAM_VIEW_CHOICES = Object.freeze([
|
|
|
390
390
|
]);
|
|
391
391
|
|
|
392
392
|
/**
|
|
393
|
-
* The mirror of Settings ›
|
|
393
|
+
* The mirror of Settings › Appearance "Stream view", for every provider. A
|
|
394
394
|
* setting of this browser (stream-view-preference.mjs); nothing goes to the
|
|
395
395
|
* relay.
|
|
396
396
|
*
|