@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,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
|
+
}
|
|
@@ -60,6 +60,12 @@ import { buildWorkflowRunCard } from './background-tasks-view.mjs';
|
|
|
60
60
|
import { mergeLiveSubagentRun, subagentRowsMarkup } from './subagent-row.mjs';
|
|
61
61
|
import { rememberSubagentRuns, syncSubagentRowClock } from './subagent-viewer.mjs';
|
|
62
62
|
import { isMessagePinned, toggleMessagePin } from './pinned-messages-view.mjs';
|
|
63
|
+
import { clearImageTurnPlaceholder, noteInFlightImageTurn, renderImageTurnPlaceholder } from './image-turn-placeholder.mjs';
|
|
64
|
+
import { isOpenAIImageModelId } from './conversation-provider-indicator.mjs';
|
|
65
|
+
import { buildImageSendOptions, imageEffortForSend } from './openai-image-composer.mjs';
|
|
66
|
+
import { readImageComposerSendInput, flagImageComposerSizeError } from './image-composer-ui.js';
|
|
67
|
+
import { focusOpenAIImageCapInput, refreshOpenAIImageSpendState } from './openai-image-cost-ui.js';
|
|
68
|
+
import { OPENAI_IMAGE_DAILY_CAP_REACHED_CODE } from './openai-image-spend.mjs';
|
|
63
69
|
import { isMessageHiddenFromShares, setConversationHiddenMessages } from './hidden-messages-view.mjs';
|
|
64
70
|
import { parsePreviewCommand, runPreviewCommand } from './preview-command.mjs';
|
|
65
71
|
import { buildTranscriptPreviewCard } from './preview-cards.mjs';
|
|
@@ -2014,6 +2020,7 @@ export function showThinking(messageId = null, autoScroll = true) {
|
|
|
2014
2020
|
stopBtn.classList.toggle('stopping', stopping);
|
|
2015
2021
|
}
|
|
2016
2022
|
anchorThinkingBubble(existing, nextMessageId || existing.dataset.messageId);
|
|
2023
|
+
renderImageTurnPlaceholder();
|
|
2017
2024
|
syncThinkingFollowButtons(existing, getThinkingFollowMode());
|
|
2018
2025
|
observeThinkingBubble(existing);
|
|
2019
2026
|
if (getThinkingFollowMode()) applyThinkingFollow();
|
|
@@ -2049,6 +2056,8 @@ export function showThinking(messageId = null, autoScroll = true) {
|
|
|
2049
2056
|
<div class="subagent-bubbles-container" data-subagent-bubbles-root="1"></div>
|
|
2050
2057
|
</div>`;
|
|
2051
2058
|
anchorThinkingBubble(div, nextMessageId);
|
|
2059
|
+
// An image turn's placeholder (timer, size, partials) belongs in it.
|
|
2060
|
+
renderImageTurnPlaceholder();
|
|
2052
2061
|
syncThinkingCompaction(div);
|
|
2053
2062
|
renderThinkingThoughts();
|
|
2054
2063
|
renderThinkingStream();
|
|
@@ -2064,6 +2073,7 @@ export function showThinking(messageId = null, autoScroll = true) {
|
|
|
2064
2073
|
export function removeThinking() {
|
|
2065
2074
|
thinkingMessageId = null;
|
|
2066
2075
|
lastInFlightSnapshotKey = '';
|
|
2076
|
+
clearImageTurnPlaceholder();
|
|
2067
2077
|
takeThinkingBubbleOff();
|
|
2068
2078
|
}
|
|
2069
2079
|
|
|
@@ -2323,6 +2333,31 @@ export function renderThinkingActivities() {
|
|
|
2323
2333
|
}
|
|
2324
2334
|
}
|
|
2325
2335
|
|
|
2336
|
+
// An image turn running when the page opens the conversation (or reconnects)
|
|
2337
|
+
// gets its placeholder from the snapshot: the timer from the turn's own start,
|
|
2338
|
+
// the model the conversation runs. `inFlight.image` (size, quality, count,
|
|
2339
|
+
// model, attempt, startedAt, previews), when the relay sends it, fills in the
|
|
2340
|
+
// rest.
|
|
2341
|
+
function noteInFlightImageTurnFromSnapshot(inFlight, messageId) {
|
|
2342
|
+
const conversation = currentConvId ? conversations[currentConvId] : null;
|
|
2343
|
+
const model = String(
|
|
2344
|
+
inFlight?.image?.model
|
|
2345
|
+
|| inFlight?.model
|
|
2346
|
+
|| conversation?.runtimeProviderModel
|
|
2347
|
+
|| conversation?.runtime_provider_model
|
|
2348
|
+
|| conversation?.preferredModel
|
|
2349
|
+
|| '',
|
|
2350
|
+
).trim();
|
|
2351
|
+
if (!inFlight?.image && !isOpenAIImageModelId(model)) return;
|
|
2352
|
+
noteInFlightImageTurn({
|
|
2353
|
+
conversationId: currentConvId,
|
|
2354
|
+
messageId,
|
|
2355
|
+
startedAt: inFlight?.processingAt || inFlight?.timestamp || null,
|
|
2356
|
+
model,
|
|
2357
|
+
image: inFlight?.image || null,
|
|
2358
|
+
});
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2326
2361
|
export function restoreInFlightThinking(inFlight, autoScroll = true) {
|
|
2327
2362
|
const messageId = String(inFlight?.messageId || '').trim();
|
|
2328
2363
|
const status = String(inFlight?.status || '').trim().toLowerCase();
|
|
@@ -2347,6 +2382,7 @@ export function restoreInFlightThinking(inFlight, autoScroll = true) {
|
|
|
2347
2382
|
return;
|
|
2348
2383
|
}
|
|
2349
2384
|
lastInFlightSnapshotKey = snapshotKey;
|
|
2385
|
+
noteInFlightImageTurnFromSnapshot(inFlight, messageId);
|
|
2350
2386
|
// The snapshot may have been read before the newest live frame was stored.
|
|
2351
2387
|
const liveStreamSeq = relayStreamStateByMessageId.get(messageId)?.seq || 0;
|
|
2352
2388
|
const liveStreamText = relayStreamTextByMessageId.get(messageId);
|
|
@@ -3435,13 +3471,26 @@ async function resendStoppedSteer(conversationId, markerId) {
|
|
|
3435
3471
|
try {
|
|
3436
3472
|
if (!(await validateSelectedConversationBeforeSend(conversationKey))) return;
|
|
3437
3473
|
const selectedModel = document.getElementById('model-select')?.value || '';
|
|
3438
|
-
|
|
3439
|
-
|
|
3474
|
+
let selectedReasoningEffort = String(document.getElementById('reasoning-effort-select')?.value || '').trim().toLowerCase();
|
|
3475
|
+
let selectedContextTier = String(document.getElementById('context-tier-select')?.value || 'default').trim();
|
|
3440
3476
|
const selectedMode = document.getElementById('mode-select')?.value || 'agent';
|
|
3441
3477
|
if (!selectedReasoningEffort) {
|
|
3442
3478
|
showTransientRelayNotice('Select a reasoning effort after refreshing model metadata.');
|
|
3443
3479
|
return;
|
|
3444
3480
|
}
|
|
3481
|
+
// As a send from the composer: an image model's options go along.
|
|
3482
|
+
let selectedImageOptions;
|
|
3483
|
+
if (isOpenAIImageModelId(selectedModel)) {
|
|
3484
|
+
const built = buildImageSendOptions({ model: selectedModel, ...readImageComposerSendInput() });
|
|
3485
|
+
if (!built.ok) {
|
|
3486
|
+
if (built.field === 'size') flagImageComposerSizeError(built.error);
|
|
3487
|
+
showTransientRelayNotice(built.error);
|
|
3488
|
+
return;
|
|
3489
|
+
}
|
|
3490
|
+
selectedImageOptions = built.image;
|
|
3491
|
+
selectedContextTier = built.contextTier;
|
|
3492
|
+
selectedReasoningEffort = imageEffortForSend(selectedModel, selectedReasoningEffort);
|
|
3493
|
+
}
|
|
3445
3494
|
// As a send from the composer (loadEndForOwnSend): the Resend goes to
|
|
3446
3495
|
// the end.
|
|
3447
3496
|
if (isCurrentConversation(conversationKey)) await loadEndForOwnSend();
|
|
@@ -3458,6 +3507,8 @@ async function resendStoppedSteer(conversationId, markerId) {
|
|
|
3458
3507
|
}, ownSendFollows(send), clientMessageId, true);
|
|
3459
3508
|
bubbleAppended = true;
|
|
3460
3509
|
}
|
|
3510
|
+
// A refusal the user can act on (the daily image cap) says why.
|
|
3511
|
+
let refusal = '';
|
|
3461
3512
|
const r = await sendMessageApi({
|
|
3462
3513
|
messageId: clientMessageId,
|
|
3463
3514
|
clientId: CLIENT_ID,
|
|
@@ -3466,14 +3517,20 @@ async function resendStoppedSteer(conversationId, markerId) {
|
|
|
3466
3517
|
reasoningEffort: selectedReasoningEffort,
|
|
3467
3518
|
contextTier: selectedContextTier,
|
|
3468
3519
|
relayMode: selectedMode,
|
|
3520
|
+
image: selectedImageOptions,
|
|
3469
3521
|
conversationId: conversationKey,
|
|
3470
3522
|
attachments,
|
|
3471
3523
|
resendOfMessageId: String(sourceNode?.dataset?.messageId || '').trim() || undefined,
|
|
3524
|
+
}, {
|
|
3525
|
+
onRefused: (reason, payload) => {
|
|
3526
|
+
refusal = reason;
|
|
3527
|
+
if (payload?.code === OPENAI_IMAGE_DAILY_CAP_REACHED_CODE) void refreshOpenAIImageSpendState();
|
|
3528
|
+
},
|
|
3472
3529
|
});
|
|
3473
3530
|
if (!r) {
|
|
3474
3531
|
if (!hasSendEchoArrived(clientMessageId)) {
|
|
3475
3532
|
dropOptimisticBubble();
|
|
3476
|
-
showTransientRelayNotice('Could not resend the message. Please try again.');
|
|
3533
|
+
showTransientRelayNotice(refusal || 'Could not resend the message. Please try again.', refusal ? 8000 : undefined);
|
|
3477
3534
|
return;
|
|
3478
3535
|
}
|
|
3479
3536
|
sent = true;
|
|
@@ -3764,6 +3821,10 @@ export function runRelayErrorCta(cta) {
|
|
|
3764
3821
|
openSettingsModal('providers', 'claude-api');
|
|
3765
3822
|
focusClaudeApiCapInput();
|
|
3766
3823
|
}
|
|
3824
|
+
if (cta === 'open-openai-image-cap') {
|
|
3825
|
+
openSettingsModal('providers', 'openai');
|
|
3826
|
+
focusOpenAIImageCapInput();
|
|
3827
|
+
}
|
|
3767
3828
|
if (cta === 'open-claude-api-billing') window.open(CLAUDE_API_BILLING_URL, '_blank', 'noopener');
|
|
3768
3829
|
if (cta === 'open-claude-api-limits') window.open(CLAUDE_API_LIMITS_URL, '_blank', 'noopener');
|
|
3769
3830
|
// The chat this note is in, not a chat opened since: the bubble is on screen.
|
|
@@ -4117,6 +4178,8 @@ export function renderNoConversationState() {
|
|
|
4117
4178
|
if (String(currentConvId || '').trim()) return false;
|
|
4118
4179
|
const el = getMessagesElement();
|
|
4119
4180
|
if (!el || el.querySelector('.status-view')) return false;
|
|
4181
|
+
// The global Media view holds the transcript's place with nothing open.
|
|
4182
|
+
if (document.body.classList.contains('media-view-active')) return false;
|
|
4120
4183
|
const existing = el.querySelector('.empty-state');
|
|
4121
4184
|
// A transcript with rows in it belongs to something else (a render in flight).
|
|
4122
4185
|
if (!existing && el.children.length) return false;
|
|
@@ -4168,6 +4231,9 @@ export function refreshStreamViewLayout() {
|
|
|
4168
4231
|
export function renderMessages(msgs, scroll = true, meta = {}) {
|
|
4169
4232
|
const el = getMessagesElement();
|
|
4170
4233
|
if (!el) return false;
|
|
4234
|
+
// The global Media view holds the transcript's place with nothing open: a
|
|
4235
|
+
// refresh (socket reconnect, CLI back online) must not clear it.
|
|
4236
|
+
if (!String(currentConvId || '').trim() && document.body.classList.contains('media-view-active')) return false;
|
|
4171
4237
|
lastRenderMessagesArgs = { msgs, meta };
|
|
4172
4238
|
const ordered = sortConversationMessages(msgs || []);
|
|
4173
4239
|
const snapshotKey = buildMessageSnapshotKey(ordered, meta);
|
|
@@ -4829,10 +4895,25 @@ export async function sendMessage() {
|
|
|
4829
4895
|
const targetConversationId = String(currentConvId || '').trim() || null;
|
|
4830
4896
|
const draftAttachments = selectedAttachments.slice();
|
|
4831
4897
|
const selectedModel = document.getElementById('model-select').value || '';
|
|
4832
|
-
|
|
4833
|
-
|
|
4898
|
+
let selectedReasoningEffort = String(document.getElementById('reasoning-effort-select')?.value || '').trim().toLowerCase();
|
|
4899
|
+
let selectedContextTier = String(document.getElementById('context-tier-select')?.value || 'default').trim();
|
|
4834
4900
|
const selectedMode = document.getElementById('mode-select').value || 'agent';
|
|
4835
4901
|
const draftImageEditTarget = imageEditTarget;
|
|
4902
|
+
// An image model's Size, Quality, Count, Format and Background go as the
|
|
4903
|
+
// turn's `image` options, checked for the model before anything is sent:
|
|
4904
|
+
// a custom size still being typed (or one the model refuses) stops here.
|
|
4905
|
+
let selectedImageOptions;
|
|
4906
|
+
if (isOpenAIImageModelId(selectedModel)) {
|
|
4907
|
+
const built = buildImageSendOptions({ model: selectedModel, ...readImageComposerSendInput() });
|
|
4908
|
+
if (!built.ok) {
|
|
4909
|
+
if (built.field === 'size') flagImageComposerSizeError(built.error);
|
|
4910
|
+
showTransientRelayNotice(built.error);
|
|
4911
|
+
return;
|
|
4912
|
+
}
|
|
4913
|
+
selectedImageOptions = built.image;
|
|
4914
|
+
selectedContextTier = built.contextTier;
|
|
4915
|
+
selectedReasoningEffort = imageEffortForSend(selectedModel, selectedReasoningEffort);
|
|
4916
|
+
}
|
|
4836
4917
|
let composerConversationId = targetConversationId;
|
|
4837
4918
|
const viewingSendConversation = () => String(currentConvId || '').trim() === String(composerConversationId || '').trim();
|
|
4838
4919
|
if (!(await validateSelectedConversationBeforeSend(targetConversationId))) {
|
|
@@ -4899,6 +4980,7 @@ export async function sendMessage() {
|
|
|
4899
4980
|
reasoningEffort: selectedReasoningEffort,
|
|
4900
4981
|
contextTier: selectedContextTier,
|
|
4901
4982
|
relayMode: selectedMode,
|
|
4983
|
+
image: selectedImageOptions,
|
|
4902
4984
|
conversationId: targetConversationId || undefined,
|
|
4903
4985
|
newConversation: isNew || undefined,
|
|
4904
4986
|
// The billing this page shows for a Claude chat: the relay refuses the
|
|
@@ -4926,6 +5008,9 @@ export async function sendMessage() {
|
|
|
4926
5008
|
if (payload?.code === 'CLAUDE_BILLING_CHANGED' && targetConversationId) {
|
|
4927
5009
|
noteConversationBilling(targetConversationId, payload.billing);
|
|
4928
5010
|
}
|
|
5011
|
+
// The daily image cap stopped the send: the draft stays (below),
|
|
5012
|
+
// the relay's words show, and the image chat's banner learns why.
|
|
5013
|
+
if (payload?.code === OPENAI_IMAGE_DAILY_CAP_REACHED_CODE) void refreshOpenAIImageSpendState();
|
|
4929
5014
|
},
|
|
4930
5015
|
});
|
|
4931
5016
|
if (!r) {
|