@oar-sh/oar 0.9.24 → 0.9.25
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 +142 -0
- package/README.md +12 -11
- package/package.json +3 -1
- package/server/claude-worker/claude-remote-relay-tool.mjs +15 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +29 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +11 -9
- package/server/cursor-worker/sdk-message-normalizer.mjs +13 -0
- package/server/db-schema.mjs +19 -0
- 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/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/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/relay-error-ctas.mjs +5 -0
- package/server/public/app/session-history-ui.js +2 -0
- package/server/public/app/settings-modal.js +4 -0
- package/server/public/app/socket-handlers.js +26 -0
- package/server/public/app/tab-strip-fade.mjs +1 -1
- package/server/public/app/viewer-gallery.mjs +23 -1
- package/server/public/index.html +383 -7
- 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/sessions-routes.mjs +361 -14
- package/server/server-runtime.mjs +19 -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/remote-relay-caller-context.mjs +6 -0
- package/server/services/remote-relay-dispatcher.mjs +386 -22
- package/server/services/remote-relay-registry-service.mjs +3 -1
- package/server/services/token-usage-service.mjs +15 -0
- package/shared/image-header.mjs +90 -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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Migration: an image session's own defaults, and a turn's options.
|
|
5
|
+
*
|
|
6
|
+
* `image_sessions.defaults_json`: the size, quality, count, output format and
|
|
7
|
+
* background an image session's turns use unless a turn names its own
|
|
8
|
+
* (relay_tool update_session `image`, the conversation settings route).
|
|
9
|
+
* JSON as shared/openai-image-options.mjs validates it; NULL means none.
|
|
10
|
+
*
|
|
11
|
+
* `queue.image_options_json`: the options of one image turn as the send
|
|
12
|
+
* settled them, for a turn without an image operation (the worker sends them
|
|
13
|
+
* with its direct request). NULL for every other turn.
|
|
14
|
+
*
|
|
15
|
+
* The image tables are created by migrateImageConversationSchema
|
|
16
|
+
* (repositories/image-conversation-repository.mjs), which this migration
|
|
17
|
+
* calls first, additively, so a database whose tables are missing gets them
|
|
18
|
+
* before the column. A database without the queue table (no relay's) is
|
|
19
|
+
* left alone.
|
|
20
|
+
*
|
|
21
|
+
* Additive and idempotent. Safe to run on every boot. Rolling back needs no
|
|
22
|
+
* data change: older code ignores the column.
|
|
23
|
+
*
|
|
24
|
+
* Usage: node server/migrations/0040-image-session-defaults.mjs [path/to/oar.db]
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { fileURLToPath } from 'url';
|
|
28
|
+
import path from 'path';
|
|
29
|
+
import process from 'process';
|
|
30
|
+
import Database from 'better-sqlite3';
|
|
31
|
+
import { cliDbPath } from '../services/database-file.mjs';
|
|
32
|
+
import { migrateImageConversationSchema } from '../repositories/image-conversation-repository.mjs';
|
|
33
|
+
|
|
34
|
+
export function hasImageSessionDefaultsColumn(db) {
|
|
35
|
+
try {
|
|
36
|
+
return db.prepare(`PRAGMA table_info(image_sessions)`).all().some((column) => column.name === 'defaults_json');
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function tableExists(db, name) {
|
|
43
|
+
return !!db.prepare(`SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?`).get(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function hasQueueImageOptionsColumn(db) {
|
|
47
|
+
try {
|
|
48
|
+
return db.prepare(`PRAGMA table_info(queue)`).all().some((column) => column.name === 'image_options_json');
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function migrateImageSessionDefaults(db) {
|
|
55
|
+
// The image schema reads the queue; a database without one is no relay's.
|
|
56
|
+
if (!tableExists(db, 'queue')) return { applied: false, columnsAdded: 0 };
|
|
57
|
+
migrateImageConversationSchema(db);
|
|
58
|
+
let columnsAdded = 0;
|
|
59
|
+
if (!hasImageSessionDefaultsColumn(db)) {
|
|
60
|
+
db.exec(`ALTER TABLE image_sessions ADD COLUMN defaults_json TEXT`);
|
|
61
|
+
columnsAdded += 1;
|
|
62
|
+
}
|
|
63
|
+
if (!hasQueueImageOptionsColumn(db)) {
|
|
64
|
+
db.exec(`ALTER TABLE queue ADD COLUMN image_options_json TEXT`);
|
|
65
|
+
columnsAdded += 1;
|
|
66
|
+
}
|
|
67
|
+
return { applied: columnsAdded > 0, columnsAdded };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function migrate(dbPath, options = {}) {
|
|
71
|
+
const db = new Database(dbPath, options);
|
|
72
|
+
try {
|
|
73
|
+
return migrateImageSessionDefaults(db);
|
|
74
|
+
} finally {
|
|
75
|
+
db.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
|
80
|
+
if (isMain) {
|
|
81
|
+
try {
|
|
82
|
+
// The given path, else the database in server/data (oar.db, else the
|
|
83
|
+
// name before 0.9.20); never a new one.
|
|
84
|
+
const dbPath = cliDbPath(process.argv[2], new URL('..', import.meta.url));
|
|
85
|
+
const result = migrate(dbPath);
|
|
86
|
+
console.log(`Migration 0040 on ${dbPath}`);
|
|
87
|
+
console.log(result.applied ? ` image session defaults, turn options: columns added ${result.columnsAdded}` : ' skipped: nothing to change');
|
|
88
|
+
process.exit(0);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.error(`Migration 0040 failed: ${err?.message || err}`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BASE, TOKEN, CLIENT_ID, IS_SHARE_PREVIEW, SHARE_PREVIEW_CONVERSATION_ID, SHARED_CONVERSATION_TOKEN, authHeaders, updateWorkspaceRootHints, applyContextUsageBar, readContextUsageRatio, currentConvId, conversations, setCliOnline, setCloudflaredTunnelState, setActiveRuntimeSessionCount, setRuntimeSessionBindingCount, setContextIndicatorMode, setServerPlatform, setUpdateState } from './store.js';
|
|
2
2
|
import { liveActivityCap } from './live-activity-cap.mjs';
|
|
3
|
+
import { OPENAI_IMAGE_PRESETS_PATH, OPENAI_IMAGE_SETTINGS_FIELDS, OPENAI_IMAGE_USAGE_PATH } from './openai-image-spend.mjs';
|
|
3
4
|
|
|
4
5
|
// fetch() has no default timeout. Without one, a request issued while the app is
|
|
5
6
|
// being backgrounded can hang indefinitely on a half-open mobile connection and
|
|
@@ -543,6 +544,63 @@ export async function updateOpenAISettings({
|
|
|
543
544
|
return settingsRequest('/api/settings/openai', payload, 'Failed to update OpenAI settings');
|
|
544
545
|
}
|
|
545
546
|
|
|
547
|
+
/**
|
|
548
|
+
* The daily image cap and today's override (POST /api/settings/openai with
|
|
549
|
+
* only these fields). Throws with the relay's words: a paired relay's
|
|
550
|
+
* request is refused (403).
|
|
551
|
+
*/
|
|
552
|
+
export async function updateOpenAIImageSpendSettings({ imageDailyCapUsd = undefined, allowImagesToday = undefined } = {}) {
|
|
553
|
+
const payload = {};
|
|
554
|
+
if (imageDailyCapUsd !== undefined) payload[OPENAI_IMAGE_SETTINGS_FIELDS.dailyCapUsd] = Number(imageDailyCapUsd);
|
|
555
|
+
if (typeof allowImagesToday === 'boolean') payload[OPENAI_IMAGE_SETTINGS_FIELDS.allowToday] = allowImagesToday;
|
|
556
|
+
return settingsRequest('/api/settings/openai', payload, 'Failed to update the image cap');
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/** Today's image spend; with a conversation, that chat's cost too. Null on any failure. */
|
|
560
|
+
export async function loadOpenAIImageUsage({ conversationId = '' } = {}) {
|
|
561
|
+
const id = String(conversationId || '').trim();
|
|
562
|
+
return quietGet(id ? `${OPENAI_IMAGE_USAGE_PATH}?conversationId=${encodeURIComponent(id)}` : OPENAI_IMAGE_USAGE_PATH);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// The presets routes: the relay's `error` and `code` ride on the thrown Error.
|
|
566
|
+
async function imagePresetsRequest(method, path, body = null) {
|
|
567
|
+
if (!networkRequestsEnabled) return null;
|
|
568
|
+
let response;
|
|
569
|
+
try {
|
|
570
|
+
response = await fetch(`${BASE}${path}`, {
|
|
571
|
+
signal: requestTimeoutSignal(),
|
|
572
|
+
method,
|
|
573
|
+
headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
|
574
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
575
|
+
});
|
|
576
|
+
} catch (error) {
|
|
577
|
+
noteFetchFailure(path, error);
|
|
578
|
+
throw Object.assign(new Error(`The relay could not be reached (${readableServerError(toErrorMessage(error))}).`), { status: 0, code: null });
|
|
579
|
+
}
|
|
580
|
+
noteFetchSuccess();
|
|
581
|
+
const result = await response.json().catch(() => null);
|
|
582
|
+
if (!response.ok) {
|
|
583
|
+
throw Object.assign(
|
|
584
|
+
new Error(readableServerError(result?.error || '') || `The relay answered HTTP ${response.status}.`),
|
|
585
|
+
{ status: response.status, code: result?.code || null },
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
return result;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** `{ presets }` (built-ins and own), or null when the relay has none to give. */
|
|
592
|
+
export async function loadOpenAIImagePresets() {
|
|
593
|
+
return quietGet(OPENAI_IMAGE_PRESETS_PATH);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export async function saveOpenAIImagePreset(preset, { replace = false } = {}) {
|
|
597
|
+
return imagePresetsRequest('PUT', OPENAI_IMAGE_PRESETS_PATH, { preset, ...(replace ? { replace: true } : {}) });
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
export async function deleteOpenAIImagePreset(name) {
|
|
601
|
+
return imagePresetsRequest('DELETE', `${OPENAI_IMAGE_PRESETS_PATH}/${encodeURIComponent(String(name || ''))}`);
|
|
602
|
+
}
|
|
603
|
+
|
|
546
604
|
export async function loadCursorSettings() {
|
|
547
605
|
return apiFetch('/api/settings/cursor');
|
|
548
606
|
}
|
|
@@ -131,7 +131,8 @@ export async function loadRelayQuestions(conversationId) {
|
|
|
131
131
|
// after a link the relay did not know: its notice keeps the screen empty
|
|
132
132
|
// until the reader opens something.
|
|
133
133
|
const readingAView = document.body?.classList?.contains('inbox-view-active')
|
|
134
|
-
|| document.body?.classList?.contains('status-view-active')
|
|
134
|
+
|| document.body?.classList?.contains('status-view-active')
|
|
135
|
+
|| document.body?.classList?.contains('media-view-active');
|
|
135
136
|
if (!conversationId && pending.length && !readingAView && !isConversationAutoOpenHeld()) {
|
|
136
137
|
const latest = pending
|
|
137
138
|
.slice()
|
|
@@ -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;
|