dsh-codex-subscription 1.10.0 → 1.11.0

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/lib/client.js CHANGED
@@ -59,12 +59,32 @@ window.__ModuleLoader__.load({
59
59
  const base = prompt.trim() === "" ? translate("imageEditDefault") : prompt.trim();
60
60
  return notes.length === 0 ? base : base + "\n\n" + translate("imageRegionNotes") + "\n" + notes.join("\n");
61
61
  }
62
+ const ORIGINAL_IMAGE_ID_PATTERN = /^img_[0-9a-f]{32}$/u;
63
+ const positiveInteger = (value) => Number.isSafeInteger(value) && value > 0;
64
+ function decodeOriginalImageRef(value) {
65
+ if (value === null || typeof value !== "object" || Array.isArray(value) || typeof value.assetId !== "string" || !ORIGINAL_IMAGE_ID_PATTERN.test(value.assetId) || value.mediaType !== "image/png" || !positiveInteger(value.bytes) || value.bytes > 48 * 1024 * 1024 || !positiveInteger(value.width) || !positiveInteger(value.height) || typeof value.name !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value.name) || typeof value.sha256 !== "string" || !/^[0-9a-f]{64}$/u.test(value.sha256)) return void 0;
66
+ return {
67
+ assetId: value.assetId,
68
+ mediaType: value.mediaType,
69
+ bytes: value.bytes,
70
+ width: value.width,
71
+ height: value.height,
72
+ name: value.name,
73
+ sha256: value.sha256
74
+ };
75
+ }
76
+ function decodeImagePresentation(value) {
77
+ if (value === null || typeof value !== "object" || Array.isArray(value) || value.kind !== "codex-subscription-image" || value.schemaVersion !== 1) return void 0;
78
+ const original = decodeOriginalImageRef(value.original);
79
+ return original === void 0 ? void 0 : { original };
80
+ }
62
81
  //#endregion
63
82
  //#region src/settings-contract.js
64
83
  const SETTINGS_NAMESPACE = "codex-subscription";
65
84
  const QUICK_QUOTA_MODE_FIELD = "quickQuotaMode";
66
85
  const LEGACY_QUICK_QUOTA_FIELD = "quickQuotaVisible";
67
86
  const QUICK_QUOTA_MODE_PERCENT = "percent";
87
+ const QUICK_QUOTA_MODE_FORECAST = "forecast";
68
88
  const SEARCH_PROVIDER_FIELD = "searchProvider";
69
89
  const SEARCH_PROVIDER_AUTO = "auto";
70
90
  const SEARCH_PROVIDER_CODEX = "codex";
@@ -135,7 +155,8 @@ window.__ModuleLoader__.load({
135
155
  const normalizeQuickQuotaMode = (value, legacyVisible = false) => [
136
156
  "off",
137
157
  "percent",
138
- "bar"
158
+ "bar",
159
+ "forecast"
139
160
  ].includes(value) ? value : legacyVisible === true ? QUICK_QUOTA_MODE_PERCENT : "off";
140
161
  const supportsCodexFastMode = (modelId) => typeof modelId === "string" && (/^gpt-5\.(?:5|6)(?:$|-)/u.test(modelId) || modelId === "gpt-5.4");
141
162
  //#endregion
@@ -153,7 +174,8 @@ window.__ModuleLoader__.load({
153
174
  return {
154
175
  remainingPercent: selected.remainingPercent,
155
176
  windowSeconds: selected.windowSeconds,
156
- ...Number.isSafeInteger(selected.resetsAt) ? { resetsAt: selected.resetsAt } : {}
177
+ ...Number.isSafeInteger(selected.resetsAt) ? { resetsAt: selected.resetsAt } : {},
178
+ ...selected.forecast === void 0 ? {} : { forecast: selected.forecast }
157
179
  };
158
180
  }
159
181
  //#endregion
@@ -304,6 +326,8 @@ window.__ModuleLoader__.load({
304
326
  quickQuotaOff: "关闭",
305
327
  quickQuotaPercent: "百分比",
306
328
  quickQuotaBar: "进度条",
329
+ quickQuotaForecast: "续航预测",
330
+ quickQuotaBeta: "Beta",
307
331
  contextTitle: "上下文窗口",
308
332
  contextStandard: "标准",
309
333
  contextStandardHint: "使用模型目录默认值;官方 Agent 预设会自动管理上下文。",
@@ -315,6 +339,12 @@ window.__ModuleLoader__.load({
315
339
  contextFixed: "固定 {value}",
316
340
  contextMaximum: "范围 128000–{value}",
317
341
  quickQuotaStatus: "Codex 剩余额度 {value}%",
342
+ quickQuotaForecastStatus: "Codex 剩余额度 {value}%,按当前速度预计可用 {duration}",
343
+ quotaForecast: "按当前速度 {symbol}{duration}",
344
+ runwayDaysHours: "{days} 天 {hours} 小时",
345
+ runwayDays: "{days} 天",
346
+ runwayHours: "{hours} 小时",
347
+ runwayMinutes: "{minutes} 分钟",
318
348
  speedTitle: "速度",
319
349
  speedStandard: "标准",
320
350
  speedStandardHint: "标准速度",
@@ -346,13 +376,16 @@ window.__ModuleLoader__.load({
346
376
  imageGenerated: "已生成",
347
377
  imageFailed: "生成失败",
348
378
  imageLabel: "生成的图片",
349
- imageOpen: "查看原图",
379
+ imageOpen: "查看图片",
350
380
  imageOpenNamed: "查看 {value}",
351
381
  imageLoading: "正在加载图片…",
352
382
  imageLoadFailed: "图片加载失败,点击重试",
353
383
  imagePreview: "图片预览",
384
+ imagePreviewShort: "预览图",
354
385
  imageClosePreview: "关闭预览",
355
- imageDownload: "下载原图",
386
+ imageDownload: "下载",
387
+ imageDownloadPreparing: "正在准备原图…",
388
+ imageDownloadFailed: "下载失败,重试",
356
389
  imageZoomOut: "缩小",
357
390
  imageZoomIn: "放大",
358
391
  imageFit: "适合窗口",
@@ -458,6 +491,8 @@ window.__ModuleLoader__.load({
458
491
  quickQuotaOff: "Off",
459
492
  quickQuotaPercent: "Percent",
460
493
  quickQuotaBar: "Progress bar",
494
+ quickQuotaForecast: "Runway",
495
+ quickQuotaBeta: "Beta",
461
496
  contextTitle: "Context window",
462
497
  contextStandard: "Standard",
463
498
  contextStandardHint: "Use the model catalog default; official agent presets manage context automatically.",
@@ -469,6 +504,12 @@ window.__ModuleLoader__.load({
469
504
  contextFixed: "Fixed {value}",
470
505
  contextMaximum: "128000–{value}",
471
506
  quickQuotaStatus: "Codex quota: {value}% remaining",
507
+ quickQuotaForecastStatus: "Codex quota: {value}% remaining; about {duration} at the current pace",
508
+ quotaForecast: "At current pace {symbol}{duration}",
509
+ runwayDaysHours: "{days}d {hours}h",
510
+ runwayDays: "{days}d",
511
+ runwayHours: "{hours}h",
512
+ runwayMinutes: "{minutes}m",
472
513
  speedTitle: "Speed",
473
514
  speedStandard: "Standard",
474
515
  speedStandardHint: "Standard speed",
@@ -500,13 +541,16 @@ window.__ModuleLoader__.load({
500
541
  imageGenerated: "Generated",
501
542
  imageFailed: "Generation failed",
502
543
  imageLabel: "Generated image",
503
- imageOpen: "View original",
544
+ imageOpen: "View image",
504
545
  imageOpenNamed: "View {value}",
505
546
  imageLoading: "Loading image…",
506
547
  imageLoadFailed: "Image failed to load. Click to retry",
507
548
  imagePreview: "Image preview",
549
+ imagePreviewShort: "Preview",
508
550
  imageClosePreview: "Close preview",
509
- imageDownload: "Download original",
551
+ imageDownload: "Download",
552
+ imageDownloadPreparing: "Preparing original…",
553
+ imageDownloadFailed: "Download failed. Retry",
510
554
  imageZoomOut: "Zoom out",
511
555
  imageZoomIn: "Zoom in",
512
556
  imageFit: "Fit to window",
@@ -531,7 +575,7 @@ window.__ModuleLoader__.load({
531
575
  .codexSubscriptionNote{font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary)}
532
576
  .codexSubscriptionCard{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1);padding:14px 16px;display:flex;flex-direction:column;gap:12px}
533
577
  .codexSubscriptionUsageCard{padding:12px 14px;gap:9px}.codexSubscriptionPreferencesCard{padding:12px 14px;gap:10px}.codexSubscriptionPreference{min-height:32px;box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:12px;color:var(--dsw-alias-label-primary);font-size:13px;line-height:20px}.codexSubscriptionPreferenceCopy{display:flex;min-width:0;flex-direction:column;gap:2px}.codexSubscriptionPreferenceLabel{display:flex;align-items:center;gap:6px}
534
- .codexSubscriptionQuotaModes{display:flex;align-items:center;gap:3px;padding:2px;border-radius:9px;background:var(--dsw-alias-bg-module-platform)}.codexSubscriptionQuotaMode{position:relative;display:flex;align-items:center;justify-content:center;min-height:26px;padding:0 9px;border-radius:7px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;cursor:pointer}.codexSubscriptionQuotaMode:has(input:checked){background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);box-shadow:0 0 0 1px var(--dsw-alias-border-l3)}.codexSubscriptionQuotaMode:has(input:focus-visible){outline:2px solid var(--dsw-alias-border-l3);outline-offset:1px}.codexSubscriptionQuotaMode:has(input:disabled){cursor:not-allowed;opacity:.5}.codexSubscriptionQuotaMode input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
578
+ .codexSubscriptionQuotaModes{display:flex;align-items:center;gap:3px;padding:2px;border-radius:9px;background:var(--dsw-alias-bg-module-platform)}.codexSubscriptionQuotaMode{position:relative;display:flex;align-items:center;justify-content:center;min-height:26px;padding:0 9px;border-radius:7px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;cursor:pointer;white-space:nowrap}.codexSubscriptionQuotaMode small{margin-left:3px;font-size:9px;line-height:1;color:var(--dsw-alias-label-tertiary)}.codexSubscriptionQuotaMode:has(input:checked){background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);box-shadow:0 0 0 1px var(--dsw-alias-border-l3)}.codexSubscriptionQuotaMode:has(input:focus-visible){outline:2px solid var(--dsw-alias-border-l3);outline-offset:1px}.codexSubscriptionQuotaMode:has(input:disabled){cursor:not-allowed;opacity:.5}.codexSubscriptionQuotaMode input{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none}
535
579
  .codexSubscriptionContext{display:flex;flex-direction:column;gap:8px}.codexSubscriptionContextHead{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.codexSubscriptionContextCopy{display:flex;min-width:0;flex:1;flex-direction:column;gap:2px}.codexSubscriptionContextHint{font-size:11px;line-height:17px;color:var(--dsw-alias-label-tertiary)}.codexSubscriptionContextTrigger{height:32px;min-width:108px;display:inline-flex;align-items:center;justify-content:space-between;gap:10px;padding:0 10px 0 12px;border:0;border-radius:999px;outline:0;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;cursor:pointer}.codexSubscriptionContextTrigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.codexSubscriptionContextTrigger:focus-visible{box-shadow:0 0 0 2px var(--dsw-alias-border-l3)}.codexSubscriptionContextTrigger:disabled{color:var(--dsw-alias-label-dimmed);cursor:not-allowed}.codexSubscriptionContextTrigger svg{color:var(--dsw-alias-label-tertiary);transition:transform 120ms var(--ds-ease-in-out)}.codexSubscriptionContextTrigger[aria-expanded=true] svg{transform:rotate(180deg)}.codexSubscriptionContextModels{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l2)}.codexSubscriptionContextModel{min-height:42px;display:flex;align-items:center;justify-content:space-between;gap:12px;border-bottom:1px solid var(--dsw-alias-border-l2)}.codexSubscriptionContextModel:last-child{border-bottom:0}.codexSubscriptionContextModelCopy{display:flex;min-width:0;flex-direction:column}.codexSubscriptionContextModelCopy strong{font-size:12px;line-height:18px;font-weight:500}.codexSubscriptionContextModelCopy span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}.codexSubscriptionContextInput{width:116px}
536
580
  .codexSubscriptionSwitch{position:relative;flex:0 0 auto;width:32px;height:18px;padding:0;border:1px solid var(--dsw-alias-border-l3);border-radius:999px;background:var(--dsw-alias-bg-module-platform);cursor:pointer}.codexSubscriptionSwitch:disabled{cursor:not-allowed;opacity:.5}.codexSubscriptionSwitch[aria-checked=true]{background:var(--dsw-alias-label-secondary);border-color:var(--dsw-alias-label-secondary)}.codexSubscriptionSwitchKnob{position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:var(--dsw-alias-bg-layer-1);transition:transform 120ms var(--ds-ease-in-out)}.codexSubscriptionSwitch[aria-checked=true] .codexSubscriptionSwitchKnob{transform:translateX(14px)}
537
581
  .codexSubscriptionSearch{display:flex;flex-direction:column;gap:7px}.codexSubscriptionSearchChoices{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:6px}.codexSubscriptionSearchChoice{display:grid;grid-template-columns:14px minmax(0,1fr);align-items:center;column-gap:8px;min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);padding:9px 10px;text-align:left;cursor:pointer}.codexSubscriptionSearchChoice:has(input:disabled){cursor:not-allowed;opacity:.5}.codexSubscriptionSearchChoice:has(input:checked){border-color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2)}.codexSubscriptionSearchChoice:has(input:focus-visible){outline:2px solid var(--dsw-alias-border-l3);outline-offset:2px}.codexSubscriptionSearchInput{width:14px;height:14px;margin:0;accent-color:var(--dsw-alias-label-primary);cursor:inherit}.codexSubscriptionSearchCopy{display:block;min-width:0;pointer-events:none}.codexSubscriptionSearchCopy strong,.codexSubscriptionSearchCopy span{display:block}.codexSubscriptionSearchCopy strong{font-size:12px;line-height:18px;font-weight:500;color:var(--dsw-alias-label-secondary)}.codexSubscriptionSearchChoice:has(input:checked) strong{color:var(--dsw-alias-label-primary)}.codexSubscriptionSearchCopy span{font-size:11px;line-height:16px;color:var(--dsw-alias-label-tertiary)}.codexSubscriptionDivider{height:1px;background:var(--dsw-alias-border-l2)}
@@ -560,12 +604,13 @@ window.__ModuleLoader__.load({
560
604
  .codexModelSelectMenu{overflow:visible}
561
605
  .codexImageTool{display:flex;flex-direction:column;gap:8px;margin:4px 0;color:var(--dsw-alias-label-primary)}.codexImageToolRow{display:flex;align-items:center;min-height:24px;gap:8px;font-size:13px;line-height:20px}.codexImageToolIcon{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;color:var(--dsw-alias-label-secondary)}.codexImageToolIcon::before{content:'';width:8px;height:8px;border:1.5px solid currentColor;border-radius:3px}.codexImageTool[data-state=running] .codexImageToolIcon::before{border-radius:50%;border-right-color:transparent;animation:codexImageSpin 800ms linear infinite}.codexImageTool[data-state=error] .codexImageToolIcon::before{border-color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-state-error-primary)}.codexImageToolTitle{font-weight:500}.codexImageToolState{color:var(--dsw-alias-label-tertiary)}.codexImageToolError{margin:0 0 0 24px;font-size:12px;line-height:18px;color:var(--dsw-alias-state-error-primary)}.codexImageToolGallery{margin-left:24px}.codexGeneratedImageFrame{display:flex;align-items:center;justify-content:center;width:min(240px,100%);height:240px;padding:0;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:16px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-tertiary);cursor:pointer}.codexGeneratedImageFrame img{display:block;width:100%;height:100%;object-fit:cover}.codexGeneratedImageRetry{min-height:36px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);cursor:pointer}.codexGeneratedImageModal{width:min(920px,calc(100vw - 32px));max-height:calc(100vh - 32px)}.codexGeneratedImageModalContent{min-height:0;overflow:hidden}.codexGeneratedImageViewer{display:flex;min-width:0;flex-direction:column;gap:12px}.codexGeneratedImageStage{display:grid;place-items:center;min-height:280px;max-height:calc(100vh - 260px);overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-module-platform)}.codexGeneratedImageStage img{display:block;max-width:100%;max-height:calc(100vh - 280px);object-fit:contain;transform-origin:center;transition:transform 120ms ease}.codexGeneratedImageMeta{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.codexGeneratedImageToolbar{display:flex;align-items:center;justify-content:space-between;gap:10px;flex-wrap:wrap}.codexGeneratedImageZoom{display:flex;align-items:center;gap:6px}.codexGeneratedImageZoomValue{min-width:44px;color:var(--dsw-alias-label-secondary);font-size:12px;text-align:center}.codexGeneratedImageDownload{display:inline-flex;align-items:center;gap:6px;min-height:32px;box-sizing:border-box;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:transparent;color:var(--dsw-alias-label-primary);font-size:13px;text-decoration:none}.codexGeneratedImageDownload:hover{background:var(--dsw-alias-interactive-bg-hover)}.codexGeneratedImageGuidance{display:flex;flex-direction:column;gap:2px;padding-top:2px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}@keyframes codexImageSpin{to{transform:rotate(360deg)}}
562
606
  .codexImageBeta{padding:0 5px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:16px}
607
+ .codexImageToolGallery{display:flex;align-items:flex-start;flex-direction:column;gap:8px}
563
608
  @container (max-width:560px){.codexSubscriptionCreditRows{grid-template-columns:1fr}}
564
609
  @container (max-width:480px){.codexSubscriptionAccountRow,.codexSubscriptionSectionHead{align-items:flex-start;flex-direction:column}.codexSubscriptionActions{width:100%}.codexSubscriptionSearchChoices{grid-template-columns:1fr}}
565
610
  @media(max-width:640px){.codexSubscriptionCard{padding:14px}}
566
611
  `;
567
612
  const IMAGE_STYLE = String.raw`
568
- .codexGeneratedImageLightbox{position:fixed;inset:0;z-index:1000;display:grid;grid-template-rows:auto minmax(0,1fr) auto;background:var(--dsw-alias-bg-base,#111);color:var(--dsw-alias-label-primary);outline:0}.codexGeneratedImageTopbar{display:flex;align-items:center;justify-content:space-between;gap:20px;min-height:58px;padding:8px 14px;border-bottom:1px solid var(--dsw-alias-border-l2);background:color-mix(in srgb,var(--dsw-alias-bg-base,#111) 92%,transparent);backdrop-filter:blur(18px)}.codexGeneratedImageTopbar>div:first-child{display:flex;min-width:0;flex-direction:column}.codexGeneratedImageTopbar strong{overflow:hidden;font-size:13px;font-weight:600;line-height:20px;text-overflow:ellipsis;white-space:nowrap}.codexGeneratedImageTopbar small{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}.codexGeneratedImageActions{display:flex;align-items:center;gap:6px}.codexGeneratedImageActions button,.codexGeneratedImageActions a{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:5px;height:32px;padding:0 10px;border:1px solid transparent;border-radius:16px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;text-decoration:none;cursor:pointer}.codexGeneratedImageActions button:hover,.codexGeneratedImageActions a:hover,.codexGeneratedImageActions button.is-active{background:var(--dsw-alias-interactive-bg-hover)}.codexGeneratedImageActions button:focus-visible,.codexGeneratedImageActions a:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:1px}.codexGeneratedImageActions button:disabled{opacity:.4;cursor:default}.codexGeneratedImageActions>span{min-width:38px;color:var(--dsw-alias-label-tertiary);font-size:11px;text-align:center}.codexGeneratedImageClose{font-size:20px!important}.codexGeneratedImageCanvas{position:relative;display:grid;place-items:center;min-height:0;overflow:auto;padding:24px;background:var(--dsw-alias-bg-base,#111)}.codexGeneratedImageSurface{position:relative;display:inline-flex;max-width:calc(100vw - 48px);max-height:calc(100vh - 210px);transform-origin:center;transition:transform 120ms ease}.codexGeneratedImageSurface img{display:block;max-width:100%;max-height:calc(100vh - 210px);border-radius:12px;object-fit:contain}.codexGeneratedImageCanvas.is-annotating .codexGeneratedImageSurface{cursor:crosshair}.codexGeneratedImagePin{position:absolute;display:grid;place-items:center;width:24px;height:24px;padding:0;border:2px solid white;border-radius:50%;background:var(--dsw-alias-label-primary);box-shadow:0 1px 4px rgba(0,0,0,.35);color:var(--dsw-alias-bg-base,#111);font:inherit;font-size:11px;font-weight:700;transform:translate(-50%,-50%);cursor:pointer}.codexGeneratedImagePin.is-active{background:var(--dsw-alias-state-business-primary,#3964fe);color:white}.codexGeneratedImageAnnotateHint{position:absolute;bottom:14px;left:50%;padding:6px 10px;border-radius:14px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-3) 90%,transparent);box-shadow:var(--dsw-shadow-lv2);color:var(--dsw-alias-label-secondary);font-size:11px;transform:translateX(-50%)}.codexGeneratedImageEditDock{display:flex;flex-direction:column;gap:8px;padding:10px 14px 14px;border-top:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base))}.codexGeneratedImagePrompt,.codexGeneratedImageAnnotation{display:flex;align-items:center;gap:8px;width:min(820px,100%);margin:0 auto}.codexGeneratedImagePrompt>*:first-child,.codexGeneratedImageAnnotation>*:nth-child(2){min-width:0;flex:1}.codexGeneratedImagePrompt button{flex:none;white-space:nowrap}.codexGeneratedImageAnnotation>span{flex:none;color:var(--dsw-alias-label-secondary);font-size:12px}.codexGeneratedImageAnnotation>button{flex:none;height:28px;padding:0 8px;border:0;border-radius:14px;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:11px;cursor:pointer}.codexGeneratedImageAnnotation>button:hover{background:var(--dsw-alias-interactive-bg-hover)}.codexGeneratedImageEditError{width:min(820px,100%);margin:0 auto;color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:17px}@media(max-width:760px){.codexGeneratedImageTopbar{align-items:flex-start;flex-direction:column}.codexGeneratedImageActions{width:100%;overflow-x:auto}.codexGeneratedImageActions>span{display:none}.codexGeneratedImageCanvas{padding:12px}.codexGeneratedImageSurface{max-width:calc(100vw - 24px);max-height:calc(100vh - 270px)}.codexGeneratedImageSurface img{max-height:calc(100vh - 270px)}.codexGeneratedImagePrompt,.codexGeneratedImageAnnotation{align-items:stretch;flex-direction:column}.codexGeneratedImagePrompt button{width:100%}}
613
+ .codexGeneratedImageLightbox{position:fixed;inset:0;z-index:1000;display:grid;grid-template-rows:auto minmax(0,1fr);background:var(--dsw-alias-bg-base,#111);color:var(--dsw-alias-label-primary);outline:0}.codexGeneratedImageTopbar{display:flex;align-items:center;justify-content:space-between;gap:20px;min-height:58px;padding:8px 14px;border-bottom:1px solid var(--dsw-alias-border-l2);background:color-mix(in srgb,var(--dsw-alias-bg-base,#111) 92%,transparent);backdrop-filter:blur(18px)}.codexGeneratedImageTopbar>div:first-child{display:flex;min-width:0;flex-direction:column}.codexGeneratedImageTopbar strong{overflow:hidden;font-size:13px;font-weight:600;line-height:20px;text-overflow:ellipsis;white-space:nowrap}.codexGeneratedImageTopbar small{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}.codexGeneratedImageActions{display:flex;align-items:center;gap:6px}.codexGeneratedImageActions button,.codexGeneratedImageActions a{box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:5px;height:32px;padding:0 10px;border:1px solid transparent;border-radius:16px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;text-decoration:none;cursor:pointer}.codexGeneratedImageActions button:hover,.codexGeneratedImageActions a:hover,.codexGeneratedImageActions button.is-active{background:var(--dsw-alias-interactive-bg-hover)}.codexGeneratedImageActions button:focus-visible,.codexGeneratedImageActions a:focus-visible{outline:2px solid var(--dsw-alias-border-l3);outline-offset:1px}.codexGeneratedImageActions button:disabled{opacity:.4;cursor:default}.codexGeneratedImageActions>span{min-width:38px;color:var(--dsw-alias-label-tertiary);font-size:11px;text-align:center}.codexGeneratedImageClose{font-size:20px!important}.codexGeneratedImageCanvas{position:relative;display:grid;place-items:center;min-height:0;overflow:auto;padding:24px;background:var(--dsw-alias-bg-base,#111)}.codexGeneratedImageSurface{position:relative;display:inline-flex;max-width:calc(100vw - 48px);max-height:calc(100vh - 150px);transform-origin:center;transition:transform 120ms ease}.codexGeneratedImageSurface img{display:block;max-width:100%;max-height:calc(100vh - 150px);border-radius:12px;object-fit:contain}.codexGeneratedImageCanvas.is-annotating .codexGeneratedImageSurface{cursor:crosshair}.codexGeneratedImagePin{position:absolute;display:grid;place-items:center;width:24px;height:24px;padding:0;border:2px solid white;border-radius:50%;background:var(--dsw-alias-label-primary);box-shadow:0 1px 4px rgba(0,0,0,.35);color:var(--dsw-alias-bg-base,#111);font:inherit;font-size:11px;font-weight:700;transform:translate(-50%,-50%);cursor:pointer}.codexGeneratedImagePin.is-active{background:var(--dsw-alias-state-business-primary,#3964fe);color:white}.codexGeneratedImageAnnotateHint{position:absolute;bottom:14px;left:50%;padding:6px 10px;border-radius:14px;background:color-mix(in srgb,var(--dsw-alias-bg-layer-3) 90%,transparent);box-shadow:var(--dsw-shadow-lv2);color:var(--dsw-alias-label-secondary);font-size:11px;transform:translateX(-50%)}@media(max-width:760px){.codexGeneratedImageTopbar{align-items:flex-start;flex-direction:column}.codexGeneratedImageActions{width:100%;overflow-x:auto}.codexGeneratedImageActions>span{display:none}.codexGeneratedImageCanvas{padding:12px}.codexGeneratedImageSurface{max-width:calc(100vw - 24px);max-height:calc(100vh - 180px)}.codexGeneratedImageSurface img{max-height:calc(100vh - 180px)}}
569
614
  `;
570
615
  const IMAGE_LAYOUT_STYLE = String.raw`
571
616
  .codexGeneratedImageWorkspace{display:grid;min-height:0;grid-template-columns:minmax(0,1fr)}
@@ -605,7 +650,66 @@ window.__ModuleLoader__.load({
605
650
  return cleaned.toLowerCase().endsWith(".png") ? cleaned : `${cleaned}.png`;
606
651
  };
607
652
  const imageByteSize = (bytes) => bytes < 1024 * 1024 ? `${Math.max(.1, bytes / 1024).toLocaleString(void 0, { maximumFractionDigits: 1 })} KB` : `${(bytes / 1024 / 1024).toLocaleString(void 0, { maximumFractionDigits: 1 })} MB`;
608
- function CodexGeneratedImage({ attachment, loadImage, attachForEdit, getImageViewer, t }) {
653
+ const originalRefMatches = (left, right) => left.assetId === right.assetId && left.mediaType === right.mediaType && left.bytes === right.bytes && left.width === right.width && left.height === right.height && left.name === right.name && left.sha256 === right.sha256;
654
+ async function sha256Hex(data) {
655
+ const value = await crypto.subtle.digest("SHA-256", data);
656
+ return [...new Uint8Array(value)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
657
+ }
658
+ function decodeBase64Chunk(value) {
659
+ if (typeof value !== "string" || value.length === 0 || value.length > Math.ceil(4194304 / 3) * 4 + 8) throw new Error("Invalid original image chunk");
660
+ let decoded;
661
+ try {
662
+ decoded = atob(value);
663
+ } catch {
664
+ throw new Error("Invalid original image chunk");
665
+ }
666
+ const bytes = new Uint8Array(decoded.length);
667
+ for (let index = 0; index < decoded.length; index += 1) bytes[index] = decoded.charCodeAt(index);
668
+ return bytes;
669
+ }
670
+ async function readOriginalImage(rpc, sessionId, original) {
671
+ const parts = [];
672
+ let total = 0;
673
+ let done = false;
674
+ while (!done) {
675
+ const chunk = unwrap(await rpc.call(CHANNEL, "image/original/chunk", {
676
+ sessionId,
677
+ assetId: original.assetId,
678
+ offset: total
679
+ }));
680
+ const ref = decodeOriginalImageRef(chunk?.ref);
681
+ if (ref === void 0 || !originalRefMatches(ref, original) || chunk.offset !== total || typeof chunk.done !== "boolean") throw new Error("Original image metadata changed");
682
+ const bytes = decodeBase64Chunk(chunk.encoded);
683
+ if (bytes.byteLength === 0 || total + bytes.byteLength > original.bytes) throw new Error("Original image download is incomplete");
684
+ parts.push(bytes);
685
+ total += bytes.byteLength;
686
+ done = chunk.done;
687
+ }
688
+ if (total !== original.bytes) throw new Error("Original image download is incomplete");
689
+ const data = new Uint8Array(total);
690
+ let offset = 0;
691
+ for (const part of parts) {
692
+ data.set(part, offset);
693
+ offset += part.byteLength;
694
+ }
695
+ if (await sha256Hex(data) !== original.sha256) throw new Error("Original image integrity check failed");
696
+ return data;
697
+ }
698
+ function triggerBlobDownload(data, mediaType, filename) {
699
+ const url = URL.createObjectURL(new Blob([data], { type: mediaType }));
700
+ const anchor = document.createElement("a");
701
+ anchor.href = url;
702
+ anchor.download = filename;
703
+ anchor.rel = "noopener";
704
+ document.body.append(anchor);
705
+ try {
706
+ anchor.click();
707
+ } finally {
708
+ anchor.remove();
709
+ URL.revokeObjectURL(url);
710
+ }
711
+ }
712
+ function CodexGeneratedImage({ attachment, original, rpc, sessionId, loadImage, attachForEdit, getImageViewer, t }) {
609
713
  const [attempt, setAttempt] = (0, react.useState)(0);
610
714
  const [error, setError] = (0, react.useState)(false);
611
715
  const [open, setOpen] = (0, react.useState)(false);
@@ -614,9 +718,9 @@ window.__ModuleLoader__.load({
614
718
  const [annotationMode, setAnnotationMode] = (0, react.useState)(false);
615
719
  const [annotations, setAnnotations] = (0, react.useState)([]);
616
720
  const [selectedAnnotation, setSelectedAnnotation] = (0, react.useState)();
617
- const [prompt, setPrompt] = (0, react.useState)("");
618
721
  const [editBusy, setEditBusy] = (0, react.useState)(false);
619
722
  const [editError, setEditError] = (0, react.useState)(false);
723
+ const [downloadState, setDownloadState] = (0, react.useState)("idle");
620
724
  const triggerRef = (0, react.useRef)(null);
621
725
  const dialogRef = (0, react.useRef)(null);
622
726
  (0, react.useEffect)(() => {
@@ -673,7 +777,8 @@ window.__ModuleLoader__.load({
673
777
  }, [open]);
674
778
  const label = attachment.name ?? t("imageLabel");
675
779
  const downloadName = imageDownloadName(attachment);
676
- const imageMeta = attachment.width + " × " + attachment.height + " · " + imageByteSize(attachment.bytes);
780
+ const previewMeta = attachment.width + " × " + attachment.height + " · " + imageByteSize(attachment.bytes);
781
+ const imageMeta = original === void 0 ? previewMeta : original.width + " × " + original.height + " · " + imageByteSize(original.bytes) + " · " + t("imagePreviewShort") + " " + previewMeta;
677
782
  const addAnnotation = (event) => {
678
783
  if (!annotationMode || event.target.closest(".codexGeneratedImagePin")) return;
679
784
  const bounds = event.currentTarget.getBoundingClientRect();
@@ -692,7 +797,6 @@ window.__ModuleLoader__.load({
692
797
  setEditError(false);
693
798
  try {
694
799
  await attachForEdit(src, downloadName, buildImageEditDraft({
695
- prompt,
696
800
  annotations,
697
801
  translate: t
698
802
  }));
@@ -703,31 +807,50 @@ window.__ModuleLoader__.load({
703
807
  setEditBusy(false);
704
808
  }
705
809
  };
810
+ const downloadOriginal = async () => {
811
+ if (downloadState === "pending" || original === void 0) return;
812
+ setDownloadState("pending");
813
+ try {
814
+ triggerBlobDownload(await readOriginalImage(rpc, sessionId, original), original.mediaType, original.name);
815
+ setDownloadState("idle");
816
+ } catch {
817
+ setDownloadState("failed");
818
+ throw new Error("original image download failed");
819
+ }
820
+ };
706
821
  const openImage = () => {
707
822
  if (src === void 0) return;
708
- if ((getImageViewer?.())?.open?.({
823
+ const viewer = getImageViewer?.();
824
+ const actions = [];
825
+ actions.push({
826
+ id: "continue-editing",
827
+ label: t("imageEdit"),
828
+ pendingLabel: t("imageEditPreparing"),
829
+ errorLabel: t("imageEditFailed"),
830
+ closeOnSuccess: true,
831
+ onInvoke: ({ annotations: nextAnnotations }) => attachForEdit(src, downloadName, buildImageEditDraft({
832
+ annotations: nextAnnotations,
833
+ translate: t
834
+ }))
835
+ });
836
+ if (viewer?.open?.({
709
837
  items: [{
710
838
  id: attachment.attachmentId ?? downloadName,
711
839
  src,
712
840
  name: label,
713
841
  width: attachment.width,
714
842
  height: attachment.height,
715
- bytes: attachment.bytes
843
+ bytes: attachment.bytes,
844
+ download: original === void 0 ? void 0 : {
845
+ pendingLabel: t("imageDownloadPreparing"),
846
+ errorLabel: t("imageDownloadFailed"),
847
+ onInvoke: downloadOriginal
848
+ },
849
+ actions
716
850
  }],
717
851
  opener: triggerRef.current,
718
852
  source: "codex-generated",
719
- annotations: true,
720
- editor: {
721
- label: t("imageEdit"),
722
- busyLabel: t("imageEditPreparing"),
723
- errorLabel: t("imageEditFailed"),
724
- placeholder: t("imageEditPrompt"),
725
- onSubmit: ({ prompt: nextPrompt, annotations: nextAnnotations }) => attachForEdit(src, downloadName, buildImageEditDraft({
726
- prompt: nextPrompt,
727
- annotations: nextAnnotations,
728
- translate: t
729
- }))
730
- }
853
+ annotations: true
731
854
  }) === true) return;
732
855
  setZoom(1);
733
856
  setOpen(true);
@@ -745,151 +868,142 @@ window.__ModuleLoader__.load({
745
868
  "aria-modal": "true",
746
869
  "aria-label": t("imagePreview"),
747
870
  tabIndex: -1,
748
- children: [
749
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
750
- className: "codexGeneratedImageTopbar",
751
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: imageMeta })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
752
- className: "codexGeneratedImageActions",
753
- children: [
754
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
755
- type: "button",
756
- className: annotationMode ? "is-active" : "",
757
- "aria-pressed": annotationMode,
758
- onClick: () => setAnnotationMode((value) => !value),
759
- children: t("imageAnnotate")
760
- }),
761
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
762
- type: "button",
763
- "aria-label": t("imageZoomOut"),
764
- disabled: zoom <= .5,
765
- onClick: () => setZoom((value) => Math.max(.5, value - .25)),
766
- children: "−"
767
- }),
768
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [Math.round(zoom * 100), "%"] }),
769
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
770
- type: "button",
771
- "aria-label": t("imageZoomIn"),
772
- disabled: zoom >= 3,
773
- onClick: () => setZoom((value) => Math.min(3, value + .25)),
774
- children: "+"
775
- }),
776
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
777
- type: "button",
778
- onClick: () => setZoom(1),
779
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFullscreenOutline16, { "aria-hidden": "true" }), t("imageFit")]
780
- }),
781
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
782
- href: src,
783
- download: downloadName,
784
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutline16, { "aria-hidden": "true" }), t("imageDownload")]
785
- }),
786
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
787
- type: "button",
788
- className: "codexGeneratedImageClose",
789
- "aria-label": t("imageClosePreview"),
790
- onClick: close,
791
- children: "×"
792
- })
793
- ]
794
- })]
795
- }),
796
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
797
- className: "codexGeneratedImageWorkspace " + (annotationMode || annotations.length > 0 ? "has-comments" : ""),
798
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
799
- className: "codexGeneratedImageCanvas " + (annotationMode ? "is-annotating" : ""),
800
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
801
- className: "codexGeneratedImageSurface",
802
- onClick: addAnnotation,
803
- style: { transform: "scale(" + zoom + ")" },
804
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
805
- src,
806
- alt: label
807
- }), annotations.map((annotation, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
808
- type: "button",
809
- className: "codexGeneratedImagePin " + (annotation.id === selectedAnnotation ? "is-active" : ""),
810
- "aria-label": fill(t("imageAnnotation"), { value: index + 1 }),
811
- style: {
812
- left: annotation.x * 100 + "%",
813
- top: annotation.y * 100 + "%"
814
- },
815
- onClick: (event) => {
816
- event.stopPropagation();
817
- setSelectedAnnotation(annotation.id);
818
- },
819
- children: index + 1
820
- }, annotation.id))]
821
- }), annotationMode ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
822
- className: "codexGeneratedImageAnnotateHint",
823
- children: t("imageAnnotateHint")
824
- }) : null]
825
- }), annotationMode || annotations.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
826
- className: "codexGeneratedImageComments",
827
- "aria-label": t("imageAnnotate"),
828
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("imageAnnotate") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("imageAnnotateHint") })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
829
- className: "codexGeneratedImageCommentList",
830
- children: annotations.map((annotation, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
831
- className: annotation.id === selectedAnnotation ? "is-active" : "",
832
- onClick: () => setSelectedAnnotation(annotation.id),
833
- children: [
834
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: index + 1 }),
835
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
836
- value: annotation.note,
837
- rows: 3,
838
- "aria-label": fill(t("imageAnnotation"), { value: index + 1 }),
839
- placeholder: t("imageAnnotationPlaceholder"),
840
- onFocus: () => setSelectedAnnotation(annotation.id),
841
- onChange: (event) => {
842
- const note = event.target.value;
843
- setAnnotations((value) => value.map((item) => item.id === annotation.id ? {
844
- ...item,
845
- note
846
- } : item));
847
- }
848
- }),
849
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
850
- type: "button",
851
- "aria-label": t("imageRemoveAnnotation"),
852
- onClick: (event) => {
853
- event.stopPropagation();
854
- setAnnotations((value) => value.filter((item) => item.id !== annotation.id));
855
- if (selectedAnnotation === annotation.id) setSelectedAnnotation(void 0);
856
- },
857
- children: "×"
858
- })
859
- ]
860
- }, annotation.id))
861
- })]
862
- }) : null]
863
- }),
864
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("footer", {
865
- className: "codexGeneratedImageEditDock",
866
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
867
- className: "codexGeneratedImagePrompt",
868
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
869
- value: prompt,
870
- placeholder: t("imageEditPrompt"),
871
- onChange: (event) => setPrompt(event.target.value),
872
- onKeyDown: (event) => {
873
- if (event.key === "Enter" && !event.shiftKey) {
874
- event.preventDefault();
875
- continueEditing();
876
- }
877
- }
878
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
871
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
872
+ className: "codexGeneratedImageTopbar",
873
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: imageMeta })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
874
+ className: "codexGeneratedImageActions",
875
+ children: [
876
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
877
+ type: "button",
878
+ className: annotationMode ? "is-active" : "",
879
+ "aria-pressed": annotationMode,
880
+ onClick: () => setAnnotationMode((value) => !value),
881
+ children: t("imageAnnotate")
882
+ }),
883
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
884
+ type: "button",
885
+ "aria-label": t("imageZoomOut"),
886
+ disabled: zoom <= .5,
887
+ onClick: () => setZoom((value) => Math.max(.5, value - .25)),
888
+ children: "−"
889
+ }),
890
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [Math.round(zoom * 100), "%"] }),
891
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
892
+ type: "button",
893
+ "aria-label": t("imageZoomIn"),
894
+ disabled: zoom >= 3,
895
+ onClick: () => setZoom((value) => Math.min(3, value + .25)),
896
+ children: "+"
897
+ }),
898
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
899
+ type: "button",
900
+ onClick: () => setZoom(1),
901
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFullscreenOutline16, { "aria-hidden": "true" }), t("imageFit")]
902
+ }),
903
+ original === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
904
+ href: src,
905
+ download: downloadName,
906
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutline16, { "aria-hidden": "true" }), t("imageDownload")]
907
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
908
+ type: "button",
909
+ disabled: downloadState === "pending",
910
+ onClick: () => {
911
+ downloadOriginal().catch(() => void 0);
912
+ },
913
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutline16, { "aria-hidden": "true" }), downloadState === "pending" ? t("imageDownloadPreparing") : downloadState === "failed" ? t("imageDownloadFailed") : t("imageDownload")]
914
+ }),
915
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
879
916
  type: "button",
880
917
  disabled: editBusy,
881
918
  onClick: () => {
882
919
  continueEditing();
883
920
  },
884
- children: editBusy ? t("imageEditPreparing") : t("imageEdit")
885
- })]
886
- }), editError ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
887
- className: "codexGeneratedImageEditError",
888
- role: "alert",
889
- children: t("imageEditFailed")
921
+ children: editBusy ? t("imageEditPreparing") : editError ? t("imageEditFailed") : t("imageEdit")
922
+ }),
923
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
924
+ type: "button",
925
+ className: "codexGeneratedImageClose",
926
+ "aria-label": t("imageClosePreview"),
927
+ onClick: close,
928
+ children: "×"
929
+ })
930
+ ]
931
+ })]
932
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
933
+ className: "codexGeneratedImageWorkspace " + (annotationMode || annotations.length > 0 ? "has-comments" : ""),
934
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
935
+ className: "codexGeneratedImageCanvas " + (annotationMode ? "is-annotating" : ""),
936
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
937
+ className: "codexGeneratedImageSurface",
938
+ onClick: addAnnotation,
939
+ style: { transform: "scale(" + zoom + ")" },
940
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
941
+ src,
942
+ alt: label
943
+ }), annotations.map((annotation, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
944
+ type: "button",
945
+ className: "codexGeneratedImagePin " + (annotation.id === selectedAnnotation ? "is-active" : ""),
946
+ "aria-label": fill(t("imageAnnotation"), { value: index + 1 }),
947
+ style: {
948
+ left: annotation.x * 100 + "%",
949
+ top: annotation.y * 100 + "%"
950
+ },
951
+ onClick: (event) => {
952
+ event.stopPropagation();
953
+ setSelectedAnnotation(annotation.id);
954
+ },
955
+ children: index + 1
956
+ }, annotation.id))]
957
+ }), annotationMode ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
958
+ className: "codexGeneratedImageAnnotateHint",
959
+ children: t("imageAnnotateHint")
890
960
  }) : null]
891
- })
892
- ]
961
+ }), annotationMode || annotations.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
962
+ className: "codexGeneratedImageComments",
963
+ "aria-label": t("imageAnnotate"),
964
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", { children: t("imageAnnotate") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("imageAnnotateHint") })] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
965
+ className: "codexGeneratedImageCommentList",
966
+ children: annotations.map((annotation, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
967
+ className: annotation.id === selectedAnnotation ? "is-active" : "",
968
+ onClick: () => setSelectedAnnotation(annotation.id),
969
+ children: [
970
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: index + 1 }),
971
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
972
+ value: annotation.note,
973
+ rows: 3,
974
+ "aria-label": fill(t("imageAnnotation"), { value: index + 1 }),
975
+ placeholder: t("imageAnnotationPlaceholder"),
976
+ onFocus: () => setSelectedAnnotation(annotation.id),
977
+ onChange: (event) => {
978
+ const note = event.target.value;
979
+ setAnnotations((value) => value.map((item) => item.id === annotation.id ? {
980
+ ...item,
981
+ note
982
+ } : item));
983
+ },
984
+ onKeyDown: (event) => {
985
+ if (event.key === "Enter" && !event.shiftKey || event.key === "Escape") {
986
+ event.preventDefault();
987
+ event.stopPropagation();
988
+ setSelectedAnnotation(void 0);
989
+ }
990
+ }
991
+ }),
992
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
993
+ type: "button",
994
+ "aria-label": t("imageRemoveAnnotation"),
995
+ onClick: (event) => {
996
+ event.stopPropagation();
997
+ setAnnotations((value) => value.filter((item) => item.id !== annotation.id));
998
+ if (selectedAnnotation === annotation.id) setSelectedAnnotation(void 0);
999
+ },
1000
+ children: "×"
1001
+ })
1002
+ ]
1003
+ }, annotation.id))
1004
+ })]
1005
+ }) : null]
1006
+ })]
893
1007
  }), document.body);
894
1008
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
895
1009
  ref: triggerRef,
@@ -904,13 +1018,14 @@ window.__ModuleLoader__.load({
904
1018
  })
905
1019
  }), lightbox] });
906
1020
  }
907
- function CodexImageToolRow({ block, loadImage, attachForEdit, getImageViewer, t }) {
1021
+ function CodexImageToolRow({ block, sessionId, rpc, loadImage, attachForEdit, getImageViewer, t }) {
908
1022
  const settled = block?.kind === "tool-result";
909
1023
  const image = settled ? block.content.find((item) => item?.type === "image" && item.attachment !== void 0) : void 0;
910
1024
  const failed = settled && block.isError === true;
911
1025
  const state = !settled ? "running" : failed ? "error" : "done";
912
1026
  const status = !settled ? t("imageGenerating") : failed ? t("imageFailed") : t("imageGenerated");
913
1027
  const error = failed ? block.content.find((item) => item?.type === "text" && typeof item.text === "string")?.text : void 0;
1028
+ const original = decodeImagePresentation(block?.meta)?.original;
914
1029
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
915
1030
  className: "codexImageTool",
916
1031
  "data-state": state,
@@ -940,6 +1055,9 @@ window.__ModuleLoader__.load({
940
1055
  className: "codexImageToolGallery",
941
1056
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodexGeneratedImage, {
942
1057
  attachment: image.attachment,
1058
+ original,
1059
+ rpc,
1060
+ sessionId,
943
1061
  loadImage,
944
1062
  attachForEdit,
945
1063
  getImageViewer,
@@ -1080,6 +1198,18 @@ window.__ModuleLoader__.load({
1080
1198
  }
1081
1199
  const usePreferenceSnapshot = (preference) => (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
1082
1200
  const notifyQuickQuota = () => window.dispatchEvent(new Event(QUICK_QUOTA_REFRESH_EVENT));
1201
+ const formatRunway = (seconds, t) => {
1202
+ if (!Number.isFinite(seconds) || seconds <= 0) return void 0;
1203
+ const minutes = Math.max(1, Math.round(seconds / 60));
1204
+ const days = Math.floor(minutes / 1440);
1205
+ const hours = Math.floor(minutes % 1440 / 60);
1206
+ if (days > 0) return hours > 0 ? fill(t("runwayDaysHours"), {
1207
+ days,
1208
+ hours
1209
+ }) : fill(t("runwayDays"), { days });
1210
+ if (hours > 0) return fill(t("runwayHours"), { hours });
1211
+ return fill(t("runwayMinutes"), { minutes });
1212
+ };
1083
1213
  function useQuickQuota(rpc, enabled, model) {
1084
1214
  const [quota, setQuota] = (0, react.useState)();
1085
1215
  (0, react.useEffect)(() => {
@@ -1155,7 +1285,12 @@ window.__ModuleLoader__.load({
1155
1285
  children: [
1156
1286
  choice("off", t("quickQuotaOff")),
1157
1287
  choice(QUICK_QUOTA_MODE_PERCENT, t("quickQuotaPercent")),
1158
- choice("bar", t("quickQuotaBar"))
1288
+ choice("bar", t("quickQuotaBar")),
1289
+ choice(QUICK_QUOTA_MODE_FORECAST, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1290
+ t("quickQuotaForecast"),
1291
+ " ",
1292
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("quickQuotaBeta") })
1293
+ ] }))
1159
1294
  ]
1160
1295
  })]
1161
1296
  });
@@ -1360,11 +1495,16 @@ window.__ModuleLoader__.load({
1360
1495
  const current = (0, react.useSyncExternalStore)((listener) => directory.subscribe(listener), () => directory.getSnapshot()).current;
1361
1496
  const codex = current?.provider === "openai-codex";
1362
1497
  const quotaEnabled = preferenceSnapshot.status === "ready" && preferenceSnapshot.quickQuotaMode !== "off" && codex;
1498
+ const forecastMode = preferenceSnapshot.quickQuotaMode === QUICK_QUOTA_MODE_FORECAST;
1363
1499
  const quota = useQuickQuota(rpc, quotaEnabled, current?.model);
1364
1500
  if (!quotaEnabled || quota === void 0) return null;
1365
1501
  const value = Math.round(Number(quota.remainingPercent) * 10) / 10;
1366
1502
  const display = percent(value);
1367
- const label = fill(t("quickQuotaStatus"), { value: display });
1503
+ const duration = quota.forecast?.status === "ready" ? formatRunway(quota.forecast.runwaySeconds, t) : void 0;
1504
+ const label = duration === void 0 ? fill(t("quickQuotaStatus"), { value: display }) : fill(t("quickQuotaForecastStatus"), {
1505
+ value: display,
1506
+ duration
1507
+ });
1368
1508
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1369
1509
  className: "codexComposerQuota",
1370
1510
  role: "status",
@@ -1375,7 +1515,7 @@ window.__ModuleLoader__.load({
1375
1515
  max: 100,
1376
1516
  value,
1377
1517
  "aria-hidden": "true"
1378
- }) : `${display}%`
1518
+ }) : forecastMode && duration !== void 0 ? `${display}% · ${quota.forecast.survivesReset ? "≥" : "≈"}${duration}` : `${display}%`
1379
1519
  });
1380
1520
  }
1381
1521
  function CodexModelSelect({ locked, available, directory, load, select, preference, t }) {
@@ -2281,7 +2421,10 @@ window.__ModuleLoader__.load({
2281
2421
  }),
2282
2422
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2283
2423
  className: "codexSubscriptionLimitMeta",
2284
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: windowLabel(window.windowSeconds, t) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetTime, {
2424
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: window.forecast?.status === "ready" ? fill(t("quotaForecast"), {
2425
+ symbol: window.forecast.survivesReset ? "≥" : "≈",
2426
+ duration: formatRunway(window.forecast.runwaySeconds, t)
2427
+ }) : windowLabel(window.windowSeconds, t) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetTime, {
2285
2428
  resetsAt: window.resetsAt,
2286
2429
  t
2287
2430
  })]
@@ -2437,43 +2580,50 @@ window.__ModuleLoader__.load({
2437
2580
  t
2438
2581
  })
2439
2582
  }, CodexSection));
2440
- ctx.slots.inject("conversation.input.right", () => ctx.slots.register({
2441
- name: "conversation.input.right",
2442
- id: "codex-subscription-quota",
2443
- order: 15,
2444
- locale: NS,
2445
- inject: (sessionId) => ({
2446
- preference,
2447
- rpc: connection.rpc,
2448
- t,
2449
- directory: ctx.modelDirectories.directoryFor(sessionId).store
2450
- })
2451
- }, CodexComposerQuota));
2452
2583
  const sessions = ctx.get("sessions");
2453
- ctx.slots.inject("conversation.input.model", () => ctx.slots.register({
2454
- name: "conversation.input.model",
2455
- priority: -10,
2456
- locale: NS,
2457
- inject: (sessionId) => {
2458
- const directory = ctx.modelDirectories.directoryFor(sessionId);
2459
- const available = sessions.subagentAddress(sessionId) === void 0;
2460
- return {
2461
- available,
2462
- directory: directory.store,
2463
- load: () => {
2464
- if (available) directory.load();
2465
- },
2466
- select: (selection) => available ? directory.select(selection).then(() => true, () => false) : Promise.resolve(false),
2467
- preference
2468
- };
2469
- }
2470
- }, CodexModelSelect));
2584
+ const installDirectorySlots = (scope) => {
2585
+ const modelDirectories = scope.get("modelDirectories");
2586
+ scope.slots.inject("conversation.input.right", () => scope.slots.register({
2587
+ name: "conversation.input.right",
2588
+ id: "codex-subscription-quota",
2589
+ order: 15,
2590
+ locale: NS,
2591
+ inject: (sessionId) => ({
2592
+ preference,
2593
+ rpc: connection.rpc,
2594
+ t,
2595
+ directory: modelDirectories.directoryFor(sessionId).store
2596
+ })
2597
+ }, CodexComposerQuota));
2598
+ scope.slots.inject("conversation.input.model", () => scope.slots.register({
2599
+ name: "conversation.input.model",
2600
+ priority: -10,
2601
+ locale: NS,
2602
+ inject: (sessionId) => {
2603
+ const directory = modelDirectories.directoryFor(sessionId);
2604
+ const available = sessions.subagentAddress(sessionId) === void 0;
2605
+ return {
2606
+ available,
2607
+ directory: directory.store,
2608
+ load: () => {
2609
+ if (available) directory.load();
2610
+ },
2611
+ select: (selection) => available ? directory.select(selection).then(() => true, () => false) : Promise.resolve(false),
2612
+ preference
2613
+ };
2614
+ }
2615
+ }, CodexModelSelect));
2616
+ };
2617
+ if (ctx.get("remote.session") === void 0) installDirectorySlots(ctx);
2618
+ else ctx.inject(["remote.session"], installDirectorySlots);
2471
2619
  const conversation = ctx.get("conversation");
2472
2620
  ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
2473
2621
  name: "tool.call.toolview",
2474
2622
  key: "codex_image_generate",
2475
2623
  locale: NS,
2476
2624
  inject: (sessionId) => ({
2625
+ sessionId,
2626
+ rpc: connection.rpc,
2477
2627
  t,
2478
2628
  loadImage: (attachment) => conversation.resolveImage(sessionId, attachment),
2479
2629
  getImageViewer: () => {