@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.
Files changed (92) hide show
  1. package/.github/extensions/web-relay/skills/reasoning-stream.mjs +3 -1
  2. package/CHANGELOG.md +284 -0
  3. package/README.md +12 -11
  4. package/package.json +5 -1
  5. package/server/claude-worker/claude-remote-relay-tool.mjs +15 -0
  6. package/server/claude-worker/claude-sdk-adapter.mjs +8 -4
  7. package/server/claude-worker/claude-session-process.mjs +4 -0
  8. package/server/claude-worker/claude-turn-publisher.mjs +25 -11
  9. package/server/claude-worker/sdk-message-normalizer.mjs +29 -1
  10. package/server/copilot-worker/copilot-prompt-context.mjs +2 -1
  11. package/server/copilot-worker/copilot-sdk-session-process.mjs +11 -9
  12. package/server/cursor-worker/cursor-turn-runner.mjs +3 -1
  13. package/server/cursor-worker/sdk-message-normalizer.mjs +13 -0
  14. package/server/db-schema.mjs +19 -0
  15. package/server/grok-worker/grok-turn-runner.mjs +3 -1
  16. package/server/grok-worker/sdk-message-normalizer.mjs +3 -0
  17. package/server/migrations/0039-media-index.mjs +167 -0
  18. package/server/migrations/0040-image-session-defaults.mjs +93 -0
  19. package/server/public/app/api-client.js +58 -0
  20. package/server/public/app/ask-user-view.js +2 -1
  21. package/server/public/app/attachments-view.js +66 -9
  22. package/server/public/app/bootstrap.js +85 -25
  23. package/server/public/app/chat-actions-menu-state.mjs +1 -0
  24. package/server/public/app/context-media-tab.js +143 -0
  25. package/server/public/app/context-usage-view.mjs +2 -2
  26. package/server/public/app/conversation-view.js +90 -5
  27. package/server/public/app/image-composer-ui.js +518 -0
  28. package/server/public/app/image-turn-placeholder.mjs +233 -0
  29. package/server/public/app/journal-view.js +12 -2
  30. package/server/public/app/live-activity-cap.mjs +3 -3
  31. package/server/public/app/markdown-math.mjs +471 -0
  32. package/server/public/app/media-gallery-model.mjs +202 -0
  33. package/server/public/app/media-gallery-ui.js +524 -0
  34. package/server/public/app/media-view.js +79 -0
  35. package/server/public/app/openai-image-composer.mjs +492 -0
  36. package/server/public/app/openai-image-cost-ui.js +426 -0
  37. package/server/public/app/openai-image-spend.mjs +443 -0
  38. package/server/public/app/plan-usage-view.mjs +38 -6
  39. package/server/public/app/pwa-install.js +25 -4
  40. package/server/public/app/relay-error-ctas.mjs +5 -0
  41. package/server/public/app/relay-prompt-sanitizer.mjs +3 -0
  42. package/server/public/app/relay-restart-ui.js +2 -2
  43. package/server/public/app/remote-relays-settings.js +33 -6
  44. package/server/public/app/router.js +49 -237
  45. package/server/public/app/session-history-ui.js +2 -0
  46. package/server/public/app/settings-modal.js +4 -0
  47. package/server/public/app/settings-tabs.js +10 -1
  48. package/server/public/app/socket-handlers.js +26 -0
  49. package/server/public/app/store.js +2 -0
  50. package/server/public/app/stream-view-preference.mjs +1 -1
  51. package/server/public/app/tab-strip-fade.mjs +1 -1
  52. package/server/public/app/thoughts-preference.mjs +1 -1
  53. package/server/public/app/tmux-inspector-view.js +29 -16
  54. package/server/public/app/viewer-gallery.mjs +23 -1
  55. package/server/public/index.html +517 -123
  56. package/server/relay-tools.md +1 -0
  57. package/server/repositories/image-conversation-repository.mjs +69 -0
  58. package/server/repositories/media-repository.mjs +305 -0
  59. package/server/routes/media-routes.mjs +361 -0
  60. package/server/routes/messages-routes.mjs +1410 -360
  61. package/server/routes/relay-board-routes.mjs +46 -1
  62. package/server/routes/remote-relay-routes.mjs +1 -1
  63. package/server/routes/sessions-routes.mjs +361 -14
  64. package/server/server-runtime.mjs +21 -0
  65. package/server/services/database-file.mjs +6 -4
  66. package/server/services/generated-image-cleanup-service.mjs +2 -1
  67. package/server/services/generated-image-files.mjs +110 -0
  68. package/server/services/image-input-paths.mjs +389 -0
  69. package/server/services/image-operation-service.mjs +6 -2
  70. package/server/services/media-extract.mjs +653 -0
  71. package/server/services/media-index-service.mjs +221 -0
  72. package/server/services/openai-image-preview-turns.mjs +330 -0
  73. package/server/services/openai-image-spend-service.mjs +516 -0
  74. package/server/services/openai-image-stream.mjs +170 -0
  75. package/server/services/plan-usage-openai-image.mjs +148 -0
  76. package/server/services/relay-prompt-sanitizer.mjs +7 -0
  77. package/server/services/relay-restart-queue.mjs +1 -1
  78. package/server/services/remote-relay-caller-context.mjs +6 -0
  79. package/server/services/remote-relay-dispatcher.mjs +409 -23
  80. package/server/services/remote-relay-registry-service.mjs +10 -2
  81. package/server/services/session-worker-stop-service.mjs +50 -18
  82. package/server/services/token-usage-service.mjs +15 -0
  83. package/shared/image-header.mjs +90 -0
  84. package/shared/math-instructions.mjs +30 -0
  85. package/shared/openai-image-options.mjs +299 -0
  86. package/shared/openai-image-presets.mjs +175 -0
  87. package/shared/openai-image-previews.mjs +40 -0
  88. package/shared/openai-image-prices.mjs +291 -0
  89. package/shared/openai-image-spend.mjs +90 -0
  90. package/shared/remote-relay-contract.mjs +126 -4
  91. package/shared/worker-runtime/direct-openai-image.mjs +96 -50
  92. package/server/public/app/math-dollars.mjs +0 -89
@@ -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
- const selectedReasoningEffort = String(document.getElementById('reasoning-effort-select')?.value || '').trim().toLowerCase();
3439
- const selectedContextTier = String(document.getElementById('context-tier-select')?.value || 'default').trim();
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
- const selectedReasoningEffort = String(document.getElementById('reasoning-effort-select')?.value || '').trim().toLowerCase();
4833
- const selectedContextTier = String(document.getElementById('context-tier-select')?.value || 'default').trim();
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) {