@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
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// The placeholder of a running OpenAI image turn, inside the live bubble
|
|
2
|
+
// (#thinking-indicator): an elapsed timer, the size, quality, count and
|
|
3
|
+
// model, and the newest partial of each image as the relay streams them
|
|
4
|
+
// (image_started, image_preview, image_preview_reset; owner pages only).
|
|
5
|
+
//
|
|
6
|
+
// It lives in the live bubble, so it goes with it: never stored, never in a
|
|
7
|
+
// share view (the relay sends shares no such event, and IS_SHARED_VIEW
|
|
8
|
+
// ignores them all the same), and never in the media gallery (which reads
|
|
9
|
+
// `.msg-bubble img`). The image area is sized by the size's aspect ratio from
|
|
10
|
+
// the start, so a partial arriving does not move the transcript.
|
|
11
|
+
|
|
12
|
+
import { BASE, IS_SHARED_VIEW, currentConvId } from './store.js';
|
|
13
|
+
import {
|
|
14
|
+
formatImageElapsed,
|
|
15
|
+
imageAspectRatio,
|
|
16
|
+
imagePlaceholderDetails,
|
|
17
|
+
reduceImageTurnState,
|
|
18
|
+
} from './openai-image-composer.mjs';
|
|
19
|
+
|
|
20
|
+
const LIVE_BUBBLE_ID = 'thinking-indicator';
|
|
21
|
+
const TICK_MS = 1000;
|
|
22
|
+
|
|
23
|
+
// The open conversation's image turn, or null.
|
|
24
|
+
let turn = null;
|
|
25
|
+
let timer = null;
|
|
26
|
+
|
|
27
|
+
function openConversationId() {
|
|
28
|
+
return String(currentConvId || '').trim();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function liveBubbleFor(state) {
|
|
32
|
+
if (typeof document === 'undefined' || !state) return null;
|
|
33
|
+
const bubble = document.getElementById(LIVE_BUBBLE_ID);
|
|
34
|
+
if (!bubble) return null;
|
|
35
|
+
if (String(bubble.dataset.conversationId || '') !== state.conversationId) return null;
|
|
36
|
+
const bubbleMessageId = String(bubble.dataset.messageId || '').trim();
|
|
37
|
+
if (bubbleMessageId && bubbleMessageId !== state.messageId) return null;
|
|
38
|
+
return bubble;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function stopTimer() {
|
|
42
|
+
if (timer) clearInterval(timer);
|
|
43
|
+
timer = null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ensureTimer() {
|
|
47
|
+
if (timer || typeof setInterval !== 'function') return;
|
|
48
|
+
timer = setInterval(tick, TICK_MS);
|
|
49
|
+
timer?.unref?.();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function tick() {
|
|
53
|
+
const node = typeof document === 'undefined' ? null : document.querySelector('[data-image-placeholder]');
|
|
54
|
+
if (!node || !turn) {
|
|
55
|
+
stopTimer();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const clock = node.querySelector('[data-image-placeholder-timer]');
|
|
59
|
+
const text = formatImageElapsed(Date.now() - (turn.startedAtMs ?? Date.now()));
|
|
60
|
+
if (clock && clock.textContent !== text) clock.textContent = text;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function removePlaceholderNodes() {
|
|
64
|
+
if (typeof document === 'undefined') return;
|
|
65
|
+
for (const node of document.querySelectorAll('[data-image-placeholder]')) node.remove();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildPlaceholder(state) {
|
|
69
|
+
const root = document.createElement('div');
|
|
70
|
+
root.className = 'image-turn-placeholder';
|
|
71
|
+
root.setAttribute('data-image-placeholder', '');
|
|
72
|
+
root.setAttribute('role', 'status');
|
|
73
|
+
root.innerHTML = `
|
|
74
|
+
<div class="image-turn-placeholder-meta">
|
|
75
|
+
<span class="image-turn-placeholder-label">Making the image</span>
|
|
76
|
+
<span class="image-turn-placeholder-timer" data-image-placeholder-timer>0:00</span>
|
|
77
|
+
<span class="image-turn-placeholder-retry" data-image-placeholder-retry hidden>trying again</span>
|
|
78
|
+
</div>
|
|
79
|
+
<div class="image-turn-placeholder-details" data-image-placeholder-details></div>
|
|
80
|
+
<div class="image-turn-placeholder-grid" data-image-placeholder-grid></div>`;
|
|
81
|
+
root.dataset.messageId = state.messageId;
|
|
82
|
+
return root;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function syncGrid(grid, state) {
|
|
86
|
+
const count = Math.max(1, Number(state.count) || 1);
|
|
87
|
+
grid.dataset.count = String(count);
|
|
88
|
+
const ratio = imageAspectRatio(state.size);
|
|
89
|
+
while (grid.children.length > count) grid.lastElementChild.remove();
|
|
90
|
+
for (let index = 0; index < count; index += 1) {
|
|
91
|
+
let tile = grid.children[index];
|
|
92
|
+
if (!tile) {
|
|
93
|
+
tile = document.createElement('div');
|
|
94
|
+
tile.className = 'image-turn-placeholder-tile';
|
|
95
|
+
tile.dataset.imageIndex = String(index);
|
|
96
|
+
grid.appendChild(tile);
|
|
97
|
+
}
|
|
98
|
+
if (tile.style.aspectRatio !== ratio) tile.style.aspectRatio = ratio;
|
|
99
|
+
const partial = state.partials?.[index] || null;
|
|
100
|
+
let img = tile.querySelector('img[data-image-placeholder-preview]');
|
|
101
|
+
if (!partial) {
|
|
102
|
+
img?.remove();
|
|
103
|
+
tile.classList.remove('has-preview');
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (!img) {
|
|
107
|
+
img = document.createElement('img');
|
|
108
|
+
img.setAttribute('data-image-placeholder-preview', '');
|
|
109
|
+
img.dataset.imageIndex = String(index);
|
|
110
|
+
img.alt = count > 1 ? `Preview of image ${index + 1}` : 'Preview of the image';
|
|
111
|
+
img.decoding = 'async';
|
|
112
|
+
tile.appendChild(img);
|
|
113
|
+
}
|
|
114
|
+
if (img.getAttribute('src') !== partial.url) img.setAttribute('src', partial.url);
|
|
115
|
+
img.dataset.partialIndex = String(partial.partialIndex);
|
|
116
|
+
tile.classList.add('has-preview');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Draws the open conversation's image turn into its live bubble, or takes
|
|
122
|
+
* the placeholder away when there is none (or the bubble is another turn's).
|
|
123
|
+
* Called after each event and whenever the live bubble is (re)built.
|
|
124
|
+
*/
|
|
125
|
+
export function renderImageTurnPlaceholder() {
|
|
126
|
+
if (typeof document === 'undefined') return;
|
|
127
|
+
const state = turn && turn.conversationId === openConversationId() ? turn : null;
|
|
128
|
+
const bubble = liveBubbleFor(state);
|
|
129
|
+
if (!state || !bubble) {
|
|
130
|
+
removePlaceholderNodes();
|
|
131
|
+
if (!state) stopTimer();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const host = bubble.querySelector('.thinking-bubble') || bubble;
|
|
135
|
+
let node = host.querySelector('[data-image-placeholder]');
|
|
136
|
+
if (node && node.dataset.messageId !== state.messageId) {
|
|
137
|
+
node.remove();
|
|
138
|
+
node = null;
|
|
139
|
+
}
|
|
140
|
+
if (!node) {
|
|
141
|
+
node = buildPlaceholder(state);
|
|
142
|
+
const header = host.querySelector('.thinking-bubble-header');
|
|
143
|
+
if (header) header.after(node);
|
|
144
|
+
else host.prepend(node);
|
|
145
|
+
}
|
|
146
|
+
node.dataset.attempt = String(state.attempt);
|
|
147
|
+
const details = node.querySelector('[data-image-placeholder-details]');
|
|
148
|
+
const detailText = imagePlaceholderDetails(state);
|
|
149
|
+
if (details && details.textContent !== detailText) details.textContent = detailText;
|
|
150
|
+
const retry = node.querySelector('[data-image-placeholder-retry]');
|
|
151
|
+
if (retry) retry.hidden = !state.retrying;
|
|
152
|
+
const grid = node.querySelector('[data-image-placeholder-grid]');
|
|
153
|
+
if (grid) syncGrid(grid, state);
|
|
154
|
+
tick();
|
|
155
|
+
ensureTimer();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function accept(payload) {
|
|
159
|
+
if (IS_SHARED_VIEW || !payload || typeof payload !== 'object') return false;
|
|
160
|
+
const conversationId = String(payload.conversationId || '').trim();
|
|
161
|
+
return !!conversationId && conversationId === openConversationId();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function apply(type, payload) {
|
|
165
|
+
if (!accept(payload)) return;
|
|
166
|
+
// Another turn of the open conversation replaces the one held.
|
|
167
|
+
const held = turn && turn.conversationId === payload.conversationId ? turn : null;
|
|
168
|
+
turn = reduceImageTurnState(held, type, payload, { base: BASE });
|
|
169
|
+
renderImageTurnPlaceholder();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** `image_started` { conversationId, messageId, attempt, size, quality, count, model, startedAt }. */
|
|
173
|
+
export function handleImageStarted(payload) {
|
|
174
|
+
apply('started', payload);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** `image_preview` { conversationId, messageId, attempt, imageIndex, partialIndex, contentUrl, … }. */
|
|
178
|
+
export function handleImagePreview(payload) {
|
|
179
|
+
apply('preview', payload);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** `image_preview_reset` { conversationId, messageId, attempt, reason }. */
|
|
183
|
+
export function handleImagePreviewReset(payload) {
|
|
184
|
+
if (!accept(payload)) return;
|
|
185
|
+
if (!turn || turn.messageId !== String(payload.messageId || '').trim()) return;
|
|
186
|
+
apply('reset', payload);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* A running image turn the page learns of from the conversation's in-flight
|
|
191
|
+
* snapshot (opened or reconnected mid-turn): the timer runs from the turn's
|
|
192
|
+
* own start. What the relay sent live for the same turn is kept; `image`
|
|
193
|
+
* (optional) carries the turn's size, quality, count, model, attempt and
|
|
194
|
+
* startedAt, and `previews` its newest partials, when the snapshot has them.
|
|
195
|
+
*/
|
|
196
|
+
export function noteInFlightImageTurn({ conversationId, messageId, startedAt, model = '', image = null } = {}) {
|
|
197
|
+
if (IS_SHARED_VIEW) return;
|
|
198
|
+
const convId = String(conversationId || '').trim();
|
|
199
|
+
const msgId = String(messageId || '').trim();
|
|
200
|
+
if (!convId || !msgId || convId !== openConversationId()) return;
|
|
201
|
+
const held = turn && turn.conversationId === convId && turn.messageId === msgId ? turn : null;
|
|
202
|
+
const info = image && typeof image === 'object' ? image : {};
|
|
203
|
+
// What the live events said wins over the snapshot.
|
|
204
|
+
let next = held || reduceImageTurnState(null, 'started', {
|
|
205
|
+
conversationId: convId,
|
|
206
|
+
messageId: msgId,
|
|
207
|
+
attempt: info.attempt,
|
|
208
|
+
size: info.size,
|
|
209
|
+
quality: info.quality,
|
|
210
|
+
count: info.count,
|
|
211
|
+
model: info.model || model,
|
|
212
|
+
startedAt: info.startedAt || startedAt,
|
|
213
|
+
}, { base: BASE });
|
|
214
|
+
for (const preview of Array.isArray(info.previews) ? info.previews : []) {
|
|
215
|
+
next = reduceImageTurnState(next, 'preview', { ...preview, conversationId: convId, messageId: msgId }, { base: BASE });
|
|
216
|
+
}
|
|
217
|
+
turn = next;
|
|
218
|
+
renderImageTurnPlaceholder();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The turn ended or the page left it: the placeholder goes. */
|
|
222
|
+
export function clearImageTurnPlaceholder(messageId = '') {
|
|
223
|
+
const id = String(messageId || '').trim();
|
|
224
|
+
if (id && turn && turn.messageId !== id) return;
|
|
225
|
+
turn = null;
|
|
226
|
+
removePlaceholderNodes();
|
|
227
|
+
stopTimer();
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** For tests: the held turn state. */
|
|
231
|
+
export function imageTurnPlaceholderState() {
|
|
232
|
+
return turn;
|
|
233
|
+
}
|
|
@@ -126,12 +126,14 @@ import {
|
|
|
126
126
|
} from './new-conversation-model-choice.mjs';
|
|
127
127
|
import { buildCatalogModelOptions } from './model-selector-options.mjs';
|
|
128
128
|
import { isReasoningOffUnsupported, reasoningEffortOptionLabel, reasoningEffortOptionTitle } from './reasoning-effort-labels.mjs';
|
|
129
|
+
import { rememberImageComposerValues } from './image-composer-ui.js';
|
|
129
130
|
import {
|
|
130
131
|
conversationProviderIndicatorKey,
|
|
131
132
|
conversationProviderIndicatorLabel,
|
|
132
133
|
} from './conversation-provider-indicator.mjs';
|
|
133
134
|
import { leaveStatusView } from './status-view.mjs';
|
|
134
135
|
import { leaveInboxView } from './inbox-ui.js';
|
|
136
|
+
import { leaveMediaView } from './media-view.js';
|
|
135
137
|
import {
|
|
136
138
|
CLAUDE_CLOUD_PROVIDER,
|
|
137
139
|
buildCloudSourceWarnings,
|
|
@@ -1330,7 +1332,8 @@ function showConversationUnknown(id, { lastOpenedBefore = '' } = {}) {
|
|
|
1330
1332
|
|
|
1331
1333
|
export async function openConversation(id, options = {}) {
|
|
1332
1334
|
// The Inbox owns the transcript like the status view does.
|
|
1333
|
-
const
|
|
1335
|
+
const didLeaveMediaView = leaveMediaView();
|
|
1336
|
+
const didLeaveInboxView = leaveInboxView() || didLeaveMediaView;
|
|
1334
1337
|
const didLeaveStatusView = leaveStatusView() || didLeaveInboxView;
|
|
1335
1338
|
document.getElementById('input-area')?.removeAttribute('hidden');
|
|
1336
1339
|
const previousConversationId = String(currentConvId || '').trim();
|
|
@@ -1844,9 +1847,14 @@ async function populateNewConversationModelSelect(providerType = 'github') {
|
|
|
1844
1847
|
const cloudDefault = claudeCloudDefaultModel(newConversationClaudeCloudSettingsCache, eligibleModels);
|
|
1845
1848
|
if (cloudDefault) target.value = cloudDefault;
|
|
1846
1849
|
} else {
|
|
1850
|
+
// An OpenAI Image chat starts on the relay's image default (its OpenAI
|
|
1851
|
+
// settings name it) unless the last model used is an image one it offers.
|
|
1852
|
+
const providerDefault = normalizedProvider === 'claude-api'
|
|
1853
|
+
? newConversationClaudeApiSettingsCache?.model
|
|
1854
|
+
: (normalizedProvider === 'openai-image' ? newConversationOpenAISettingsCache?.imageDefaultModel : '');
|
|
1847
1855
|
const preselected = preselectNewConversationModel(Array.from(target.options, (option) => option.value), {
|
|
1848
1856
|
storedModel,
|
|
1849
|
-
providerDefault
|
|
1857
|
+
providerDefault,
|
|
1850
1858
|
allowAuto: normalizedProvider !== 'openai-image',
|
|
1851
1859
|
});
|
|
1852
1860
|
if (preselected) target.value = preselected;
|
|
@@ -2561,6 +2569,8 @@ async function openBootstrappedConversation({
|
|
|
2561
2569
|
// default model and has no effort, so it must not become what the next
|
|
2562
2570
|
// local chat starts with.
|
|
2563
2571
|
if (selectedProvider !== CLAUDE_CLOUD_PROVIDER) rememberBootstrappedSelection(payload);
|
|
2572
|
+
// The chosen size becomes the new chat's own, read when its composer is built.
|
|
2573
|
+
if (selectedProvider === 'openai-image' && selectedSize) rememberImageComposerValues(conversationId, { size: selectedSize });
|
|
2564
2574
|
await refreshConversations();
|
|
2565
2575
|
await openConversation(conversationId);
|
|
2566
2576
|
if (selectedProvider === 'openai-image') {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// "Tool lines shown while an agent works" (Settings ›
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// "Tool lines shown while an agent works" (Settings › Appearance): how many
|
|
2
|
+
// of its newest tool lines each agent of a running turn keeps in the live
|
|
3
|
+
// bubble, the main agent and every sub-agent apart.
|
|
4
4
|
// A setting of this browser, kept in localStorage like the scroll-on-send
|
|
5
5
|
// one. The slider runs from 10 to 500 by 10 and has one more step at the far
|
|
6
6
|
// end, no limit (stored as '0'). The document and the storage are passed in
|