dsh-codex-subscription 1.10.0 → 1.11.1

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
@@ -296,6 +318,7 @@ window.__ModuleLoader__.load({
296
318
  resetTooEarly: "请等待冷静期结束后再确认。",
297
319
  resetAcknowledgeRequired: "请先确认已了解这次操作可能消耗重置次数。",
298
320
  resetAccountChanged: "登录账号已变更,请重新开始。",
321
+ resetUncertain: "服务端返回结果不确定。请再次确认,插件会复用同一个请求,不会另外发起一次重置。",
299
322
  creditsNote: "仅显示 Codex 为此账户或工作区实际返回的额外 Credits、消费上限或额度重置次数;三者不是同一项。",
300
323
  creditsUsed: "已用 {used} / {limit} credits",
301
324
  spendReached: "Credits 月度消费上限已用尽。",
@@ -304,6 +327,8 @@ window.__ModuleLoader__.load({
304
327
  quickQuotaOff: "关闭",
305
328
  quickQuotaPercent: "百分比",
306
329
  quickQuotaBar: "进度条",
330
+ quickQuotaForecast: "续航预测",
331
+ quickQuotaBeta: "Beta",
307
332
  contextTitle: "上下文窗口",
308
333
  contextStandard: "标准",
309
334
  contextStandardHint: "使用模型目录默认值;官方 Agent 预设会自动管理上下文。",
@@ -315,6 +340,12 @@ window.__ModuleLoader__.load({
315
340
  contextFixed: "固定 {value}",
316
341
  contextMaximum: "范围 128000–{value}",
317
342
  quickQuotaStatus: "Codex 剩余额度 {value}%",
343
+ quickQuotaForecastStatus: "Codex 剩余额度 {value}%,按当前速度预计可用 {duration}",
344
+ quotaForecast: "按当前速度 {symbol}{duration}",
345
+ runwayDaysHours: "{days} 天 {hours} 小时",
346
+ runwayDays: "{days} 天",
347
+ runwayHours: "{hours} 小时",
348
+ runwayMinutes: "{minutes} 分钟",
318
349
  speedTitle: "速度",
319
350
  speedStandard: "标准",
320
351
  speedStandardHint: "标准速度",
@@ -346,13 +377,16 @@ window.__ModuleLoader__.load({
346
377
  imageGenerated: "已生成",
347
378
  imageFailed: "生成失败",
348
379
  imageLabel: "生成的图片",
349
- imageOpen: "查看原图",
380
+ imageOpen: "查看图片",
350
381
  imageOpenNamed: "查看 {value}",
351
382
  imageLoading: "正在加载图片…",
352
383
  imageLoadFailed: "图片加载失败,点击重试",
353
384
  imagePreview: "图片预览",
385
+ imagePreviewShort: "预览图",
354
386
  imageClosePreview: "关闭预览",
355
- imageDownload: "下载原图",
387
+ imageDownload: "下载",
388
+ imageDownloadPreparing: "正在准备原图…",
389
+ imageDownloadFailed: "下载失败,重试",
356
390
  imageZoomOut: "缩小",
357
391
  imageZoomIn: "放大",
358
392
  imageFit: "适合窗口",
@@ -450,6 +484,7 @@ window.__ModuleLoader__.load({
450
484
  resetTooEarly: "Wait for the cooldown before confirming.",
451
485
  resetAcknowledgeRequired: "Confirm that you understand this may consume a reset.",
452
486
  resetAccountChanged: "The signed-in account changed. Start again.",
487
+ resetUncertain: "The server result is uncertain. Confirm again to check the same request; the plugin will not start a separate reset.",
453
488
  creditsNote: "Shows only extra Credits, spending caps, or quota resets returned for this account or workspace; these are separate items.",
454
489
  creditsUsed: "{used} / {limit} credits used",
455
490
  spendReached: "The monthly Credits spending cap has been reached.",
@@ -458,6 +493,8 @@ window.__ModuleLoader__.load({
458
493
  quickQuotaOff: "Off",
459
494
  quickQuotaPercent: "Percent",
460
495
  quickQuotaBar: "Progress bar",
496
+ quickQuotaForecast: "Runway",
497
+ quickQuotaBeta: "Beta",
461
498
  contextTitle: "Context window",
462
499
  contextStandard: "Standard",
463
500
  contextStandardHint: "Use the model catalog default; official agent presets manage context automatically.",
@@ -469,6 +506,12 @@ window.__ModuleLoader__.load({
469
506
  contextFixed: "Fixed {value}",
470
507
  contextMaximum: "128000–{value}",
471
508
  quickQuotaStatus: "Codex quota: {value}% remaining",
509
+ quickQuotaForecastStatus: "Codex quota: {value}% remaining; about {duration} at the current pace",
510
+ quotaForecast: "At current pace {symbol}{duration}",
511
+ runwayDaysHours: "{days}d {hours}h",
512
+ runwayDays: "{days}d",
513
+ runwayHours: "{hours}h",
514
+ runwayMinutes: "{minutes}m",
472
515
  speedTitle: "Speed",
473
516
  speedStandard: "Standard",
474
517
  speedStandardHint: "Standard speed",
@@ -500,13 +543,16 @@ window.__ModuleLoader__.load({
500
543
  imageGenerated: "Generated",
501
544
  imageFailed: "Generation failed",
502
545
  imageLabel: "Generated image",
503
- imageOpen: "View original",
546
+ imageOpen: "View image",
504
547
  imageOpenNamed: "View {value}",
505
548
  imageLoading: "Loading image…",
506
549
  imageLoadFailed: "Image failed to load. Click to retry",
507
550
  imagePreview: "Image preview",
551
+ imagePreviewShort: "Preview",
508
552
  imageClosePreview: "Close preview",
509
- imageDownload: "Download original",
553
+ imageDownload: "Download",
554
+ imageDownloadPreparing: "Preparing original…",
555
+ imageDownloadFailed: "Download failed. Retry",
510
556
  imageZoomOut: "Zoom out",
511
557
  imageZoomIn: "Zoom in",
512
558
  imageFit: "Fit to window",
@@ -531,7 +577,7 @@ window.__ModuleLoader__.load({
531
577
  .codexSubscriptionNote{font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary)}
532
578
  .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
579
  .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}
580
+ .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
581
  .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
582
  .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
583
  .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 +606,13 @@ window.__ModuleLoader__.load({
560
606
  .codexModelSelectMenu{overflow:visible}
561
607
  .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
608
  .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}
609
+ .codexImageToolGallery{display:flex;align-items:flex-start;flex-direction:column;gap:8px}
563
610
  @container (max-width:560px){.codexSubscriptionCreditRows{grid-template-columns:1fr}}
564
611
  @container (max-width:480px){.codexSubscriptionAccountRow,.codexSubscriptionSectionHead{align-items:flex-start;flex-direction:column}.codexSubscriptionActions{width:100%}.codexSubscriptionSearchChoices{grid-template-columns:1fr}}
565
612
  @media(max-width:640px){.codexSubscriptionCard{padding:14px}}
566
613
  `;
567
614
  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%}}
615
+ .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
616
  `;
570
617
  const IMAGE_LAYOUT_STYLE = String.raw`
571
618
  .codexGeneratedImageWorkspace{display:grid;min-height:0;grid-template-columns:minmax(0,1fr)}
@@ -605,7 +652,66 @@ window.__ModuleLoader__.load({
605
652
  return cleaned.toLowerCase().endsWith(".png") ? cleaned : `${cleaned}.png`;
606
653
  };
607
654
  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 }) {
655
+ 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;
656
+ async function sha256Hex(data) {
657
+ const value = await crypto.subtle.digest("SHA-256", data);
658
+ return [...new Uint8Array(value)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
659
+ }
660
+ function decodeBase64Chunk(value) {
661
+ if (typeof value !== "string" || value.length === 0 || value.length > Math.ceil(4194304 / 3) * 4 + 8) throw new Error("Invalid original image chunk");
662
+ let decoded;
663
+ try {
664
+ decoded = atob(value);
665
+ } catch {
666
+ throw new Error("Invalid original image chunk");
667
+ }
668
+ const bytes = new Uint8Array(decoded.length);
669
+ for (let index = 0; index < decoded.length; index += 1) bytes[index] = decoded.charCodeAt(index);
670
+ return bytes;
671
+ }
672
+ async function readOriginalImage(rpc, sessionId, original) {
673
+ const parts = [];
674
+ let total = 0;
675
+ let done = false;
676
+ while (!done) {
677
+ const chunk = unwrap(await rpc.call(CHANNEL, "image/original/chunk", {
678
+ sessionId,
679
+ assetId: original.assetId,
680
+ offset: total
681
+ }));
682
+ const ref = decodeOriginalImageRef(chunk?.ref);
683
+ if (ref === void 0 || !originalRefMatches(ref, original) || chunk.offset !== total || typeof chunk.done !== "boolean") throw new Error("Original image metadata changed");
684
+ const bytes = decodeBase64Chunk(chunk.encoded);
685
+ if (bytes.byteLength === 0 || total + bytes.byteLength > original.bytes) throw new Error("Original image download is incomplete");
686
+ parts.push(bytes);
687
+ total += bytes.byteLength;
688
+ done = chunk.done;
689
+ }
690
+ if (total !== original.bytes) throw new Error("Original image download is incomplete");
691
+ const data = new Uint8Array(total);
692
+ let offset = 0;
693
+ for (const part of parts) {
694
+ data.set(part, offset);
695
+ offset += part.byteLength;
696
+ }
697
+ if (await sha256Hex(data) !== original.sha256) throw new Error("Original image integrity check failed");
698
+ return data;
699
+ }
700
+ function triggerBlobDownload(data, mediaType, filename) {
701
+ const url = URL.createObjectURL(new Blob([data], { type: mediaType }));
702
+ const anchor = document.createElement("a");
703
+ anchor.href = url;
704
+ anchor.download = filename;
705
+ anchor.rel = "noopener";
706
+ document.body.append(anchor);
707
+ try {
708
+ anchor.click();
709
+ } finally {
710
+ anchor.remove();
711
+ URL.revokeObjectURL(url);
712
+ }
713
+ }
714
+ function CodexGeneratedImage({ attachment, original, rpc, sessionId, loadImage, attachForEdit, getImageViewer, t }) {
609
715
  const [attempt, setAttempt] = (0, react.useState)(0);
610
716
  const [error, setError] = (0, react.useState)(false);
611
717
  const [open, setOpen] = (0, react.useState)(false);
@@ -614,9 +720,9 @@ window.__ModuleLoader__.load({
614
720
  const [annotationMode, setAnnotationMode] = (0, react.useState)(false);
615
721
  const [annotations, setAnnotations] = (0, react.useState)([]);
616
722
  const [selectedAnnotation, setSelectedAnnotation] = (0, react.useState)();
617
- const [prompt, setPrompt] = (0, react.useState)("");
618
723
  const [editBusy, setEditBusy] = (0, react.useState)(false);
619
724
  const [editError, setEditError] = (0, react.useState)(false);
725
+ const [downloadState, setDownloadState] = (0, react.useState)("idle");
620
726
  const triggerRef = (0, react.useRef)(null);
621
727
  const dialogRef = (0, react.useRef)(null);
622
728
  (0, react.useEffect)(() => {
@@ -673,7 +779,8 @@ window.__ModuleLoader__.load({
673
779
  }, [open]);
674
780
  const label = attachment.name ?? t("imageLabel");
675
781
  const downloadName = imageDownloadName(attachment);
676
- const imageMeta = attachment.width + " × " + attachment.height + " · " + imageByteSize(attachment.bytes);
782
+ const previewMeta = attachment.width + " × " + attachment.height + " · " + imageByteSize(attachment.bytes);
783
+ const imageMeta = original === void 0 ? previewMeta : original.width + " × " + original.height + " · " + imageByteSize(original.bytes) + " · " + t("imagePreviewShort") + " " + previewMeta;
677
784
  const addAnnotation = (event) => {
678
785
  if (!annotationMode || event.target.closest(".codexGeneratedImagePin")) return;
679
786
  const bounds = event.currentTarget.getBoundingClientRect();
@@ -692,7 +799,6 @@ window.__ModuleLoader__.load({
692
799
  setEditError(false);
693
800
  try {
694
801
  await attachForEdit(src, downloadName, buildImageEditDraft({
695
- prompt,
696
802
  annotations,
697
803
  translate: t
698
804
  }));
@@ -703,31 +809,50 @@ window.__ModuleLoader__.load({
703
809
  setEditBusy(false);
704
810
  }
705
811
  };
812
+ const downloadOriginal = async () => {
813
+ if (downloadState === "pending" || original === void 0) return;
814
+ setDownloadState("pending");
815
+ try {
816
+ triggerBlobDownload(await readOriginalImage(rpc, sessionId, original), original.mediaType, original.name);
817
+ setDownloadState("idle");
818
+ } catch {
819
+ setDownloadState("failed");
820
+ throw new Error("original image download failed");
821
+ }
822
+ };
706
823
  const openImage = () => {
707
824
  if (src === void 0) return;
708
- if ((getImageViewer?.())?.open?.({
825
+ const viewer = getImageViewer?.();
826
+ const actions = [];
827
+ actions.push({
828
+ id: "continue-editing",
829
+ label: t("imageEdit"),
830
+ pendingLabel: t("imageEditPreparing"),
831
+ errorLabel: t("imageEditFailed"),
832
+ closeOnSuccess: true,
833
+ onInvoke: ({ annotations: nextAnnotations }) => attachForEdit(src, downloadName, buildImageEditDraft({
834
+ annotations: nextAnnotations,
835
+ translate: t
836
+ }))
837
+ });
838
+ if (viewer?.open?.({
709
839
  items: [{
710
840
  id: attachment.attachmentId ?? downloadName,
711
841
  src,
712
842
  name: label,
713
843
  width: attachment.width,
714
844
  height: attachment.height,
715
- bytes: attachment.bytes
845
+ bytes: attachment.bytes,
846
+ download: original === void 0 ? void 0 : {
847
+ pendingLabel: t("imageDownloadPreparing"),
848
+ errorLabel: t("imageDownloadFailed"),
849
+ onInvoke: downloadOriginal
850
+ },
851
+ actions
716
852
  }],
717
853
  opener: triggerRef.current,
718
854
  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
- }
855
+ annotations: true
731
856
  }) === true) return;
732
857
  setZoom(1);
733
858
  setOpen(true);
@@ -745,151 +870,142 @@ window.__ModuleLoader__.load({
745
870
  "aria-modal": "true",
746
871
  "aria-label": t("imagePreview"),
747
872
  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, {
873
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
874
+ className: "codexGeneratedImageTopbar",
875
+ 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", {
876
+ className: "codexGeneratedImageActions",
877
+ children: [
878
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
879
+ type: "button",
880
+ className: annotationMode ? "is-active" : "",
881
+ "aria-pressed": annotationMode,
882
+ onClick: () => setAnnotationMode((value) => !value),
883
+ children: t("imageAnnotate")
884
+ }),
885
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
886
+ type: "button",
887
+ "aria-label": t("imageZoomOut"),
888
+ disabled: zoom <= .5,
889
+ onClick: () => setZoom((value) => Math.max(.5, value - .25)),
890
+ children: "−"
891
+ }),
892
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [Math.round(zoom * 100), "%"] }),
893
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
894
+ type: "button",
895
+ "aria-label": t("imageZoomIn"),
896
+ disabled: zoom >= 3,
897
+ onClick: () => setZoom((value) => Math.min(3, value + .25)),
898
+ children: "+"
899
+ }),
900
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
901
+ type: "button",
902
+ onClick: () => setZoom(1),
903
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFullscreenOutline16, { "aria-hidden": "true" }), t("imageFit")]
904
+ }),
905
+ original === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("a", {
906
+ href: src,
907
+ download: downloadName,
908
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutline16, { "aria-hidden": "true" }), t("imageDownload")]
909
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
910
+ type: "button",
911
+ disabled: downloadState === "pending",
912
+ onClick: () => {
913
+ downloadOriginal().catch(() => void 0);
914
+ },
915
+ 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")]
916
+ }),
917
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
879
918
  type: "button",
880
919
  disabled: editBusy,
881
920
  onClick: () => {
882
921
  continueEditing();
883
922
  },
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")
923
+ children: editBusy ? t("imageEditPreparing") : editError ? t("imageEditFailed") : t("imageEdit")
924
+ }),
925
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
926
+ type: "button",
927
+ className: "codexGeneratedImageClose",
928
+ "aria-label": t("imageClosePreview"),
929
+ onClick: close,
930
+ children: "×"
931
+ })
932
+ ]
933
+ })]
934
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
935
+ className: "codexGeneratedImageWorkspace " + (annotationMode || annotations.length > 0 ? "has-comments" : ""),
936
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("main", {
937
+ className: "codexGeneratedImageCanvas " + (annotationMode ? "is-annotating" : ""),
938
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
939
+ className: "codexGeneratedImageSurface",
940
+ onClick: addAnnotation,
941
+ style: { transform: "scale(" + zoom + ")" },
942
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
943
+ src,
944
+ alt: label
945
+ }), annotations.map((annotation, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
946
+ type: "button",
947
+ className: "codexGeneratedImagePin " + (annotation.id === selectedAnnotation ? "is-active" : ""),
948
+ "aria-label": fill(t("imageAnnotation"), { value: index + 1 }),
949
+ style: {
950
+ left: annotation.x * 100 + "%",
951
+ top: annotation.y * 100 + "%"
952
+ },
953
+ onClick: (event) => {
954
+ event.stopPropagation();
955
+ setSelectedAnnotation(annotation.id);
956
+ },
957
+ children: index + 1
958
+ }, annotation.id))]
959
+ }), annotationMode ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
960
+ className: "codexGeneratedImageAnnotateHint",
961
+ children: t("imageAnnotateHint")
890
962
  }) : null]
891
- })
892
- ]
963
+ }), annotationMode || annotations.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("aside", {
964
+ className: "codexGeneratedImageComments",
965
+ "aria-label": t("imageAnnotate"),
966
+ 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", {
967
+ className: "codexGeneratedImageCommentList",
968
+ children: annotations.map((annotation, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
969
+ className: annotation.id === selectedAnnotation ? "is-active" : "",
970
+ onClick: () => setSelectedAnnotation(annotation.id),
971
+ children: [
972
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: index + 1 }),
973
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
974
+ value: annotation.note,
975
+ rows: 3,
976
+ "aria-label": fill(t("imageAnnotation"), { value: index + 1 }),
977
+ placeholder: t("imageAnnotationPlaceholder"),
978
+ onFocus: () => setSelectedAnnotation(annotation.id),
979
+ onChange: (event) => {
980
+ const note = event.target.value;
981
+ setAnnotations((value) => value.map((item) => item.id === annotation.id ? {
982
+ ...item,
983
+ note
984
+ } : item));
985
+ },
986
+ onKeyDown: (event) => {
987
+ if (event.key === "Enter" && !event.shiftKey || event.key === "Escape") {
988
+ event.preventDefault();
989
+ event.stopPropagation();
990
+ setSelectedAnnotation(void 0);
991
+ }
992
+ }
993
+ }),
994
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
995
+ type: "button",
996
+ "aria-label": t("imageRemoveAnnotation"),
997
+ onClick: (event) => {
998
+ event.stopPropagation();
999
+ setAnnotations((value) => value.filter((item) => item.id !== annotation.id));
1000
+ if (selectedAnnotation === annotation.id) setSelectedAnnotation(void 0);
1001
+ },
1002
+ children: "×"
1003
+ })
1004
+ ]
1005
+ }, annotation.id))
1006
+ })]
1007
+ }) : null]
1008
+ })]
893
1009
  }), document.body);
894
1010
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
895
1011
  ref: triggerRef,
@@ -904,13 +1020,14 @@ window.__ModuleLoader__.load({
904
1020
  })
905
1021
  }), lightbox] });
906
1022
  }
907
- function CodexImageToolRow({ block, loadImage, attachForEdit, getImageViewer, t }) {
1023
+ function CodexImageToolRow({ block, sessionId, rpc, loadImage, attachForEdit, getImageViewer, t }) {
908
1024
  const settled = block?.kind === "tool-result";
909
1025
  const image = settled ? block.content.find((item) => item?.type === "image" && item.attachment !== void 0) : void 0;
910
1026
  const failed = settled && block.isError === true;
911
1027
  const state = !settled ? "running" : failed ? "error" : "done";
912
1028
  const status = !settled ? t("imageGenerating") : failed ? t("imageFailed") : t("imageGenerated");
913
1029
  const error = failed ? block.content.find((item) => item?.type === "text" && typeof item.text === "string")?.text : void 0;
1030
+ const original = decodeImagePresentation(block?.meta)?.original;
914
1031
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
915
1032
  className: "codexImageTool",
916
1033
  "data-state": state,
@@ -940,6 +1057,9 @@ window.__ModuleLoader__.load({
940
1057
  className: "codexImageToolGallery",
941
1058
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CodexGeneratedImage, {
942
1059
  attachment: image.attachment,
1060
+ original,
1061
+ rpc,
1062
+ sessionId,
943
1063
  loadImage,
944
1064
  attachForEdit,
945
1065
  getImageViewer,
@@ -1080,6 +1200,18 @@ window.__ModuleLoader__.load({
1080
1200
  }
1081
1201
  const usePreferenceSnapshot = (preference) => (0, react.useSyncExternalStore)(preference.subscribe, preference.getSnapshot);
1082
1202
  const notifyQuickQuota = () => window.dispatchEvent(new Event(QUICK_QUOTA_REFRESH_EVENT));
1203
+ const formatRunway = (seconds, t) => {
1204
+ if (!Number.isFinite(seconds) || seconds <= 0) return void 0;
1205
+ const minutes = Math.max(1, Math.round(seconds / 60));
1206
+ const days = Math.floor(minutes / 1440);
1207
+ const hours = Math.floor(minutes % 1440 / 60);
1208
+ if (days > 0) return hours > 0 ? fill(t("runwayDaysHours"), {
1209
+ days,
1210
+ hours
1211
+ }) : fill(t("runwayDays"), { days });
1212
+ if (hours > 0) return fill(t("runwayHours"), { hours });
1213
+ return fill(t("runwayMinutes"), { minutes });
1214
+ };
1083
1215
  function useQuickQuota(rpc, enabled, model) {
1084
1216
  const [quota, setQuota] = (0, react.useState)();
1085
1217
  (0, react.useEffect)(() => {
@@ -1155,7 +1287,12 @@ window.__ModuleLoader__.load({
1155
1287
  children: [
1156
1288
  choice("off", t("quickQuotaOff")),
1157
1289
  choice(QUICK_QUOTA_MODE_PERCENT, t("quickQuotaPercent")),
1158
- choice("bar", t("quickQuotaBar"))
1290
+ choice("bar", t("quickQuotaBar")),
1291
+ choice(QUICK_QUOTA_MODE_FORECAST, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1292
+ t("quickQuotaForecast"),
1293
+ " ",
1294
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("small", { children: t("quickQuotaBeta") })
1295
+ ] }))
1159
1296
  ]
1160
1297
  })]
1161
1298
  });
@@ -1360,11 +1497,16 @@ window.__ModuleLoader__.load({
1360
1497
  const current = (0, react.useSyncExternalStore)((listener) => directory.subscribe(listener), () => directory.getSnapshot()).current;
1361
1498
  const codex = current?.provider === "openai-codex";
1362
1499
  const quotaEnabled = preferenceSnapshot.status === "ready" && preferenceSnapshot.quickQuotaMode !== "off" && codex;
1500
+ const forecastMode = preferenceSnapshot.quickQuotaMode === QUICK_QUOTA_MODE_FORECAST;
1363
1501
  const quota = useQuickQuota(rpc, quotaEnabled, current?.model);
1364
1502
  if (!quotaEnabled || quota === void 0) return null;
1365
1503
  const value = Math.round(Number(quota.remainingPercent) * 10) / 10;
1366
1504
  const display = percent(value);
1367
- const label = fill(t("quickQuotaStatus"), { value: display });
1505
+ const duration = quota.forecast?.status === "ready" ? formatRunway(quota.forecast.runwaySeconds, t) : void 0;
1506
+ const label = duration === void 0 ? fill(t("quickQuotaStatus"), { value: display }) : fill(t("quickQuotaForecastStatus"), {
1507
+ value: display,
1508
+ duration
1509
+ });
1368
1510
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1369
1511
  className: "codexComposerQuota",
1370
1512
  role: "status",
@@ -1375,7 +1517,7 @@ window.__ModuleLoader__.load({
1375
1517
  max: 100,
1376
1518
  value,
1377
1519
  "aria-hidden": "true"
1378
- }) : `${display}%`
1520
+ }) : forecastMode && duration !== void 0 ? `${display}% · ${quota.forecast.survivesReset ? "≥" : "≈"}${duration}` : `${display}%`
1379
1521
  });
1380
1522
  }
1381
1523
  function CodexModelSelect({ locked, available, directory, load, select, preference, t }) {
@@ -2170,7 +2312,8 @@ window.__ModuleLoader__.load({
2170
2312
  ["This quota reset is already in progress", "resetInProgress"],
2171
2313
  ["Wait before confirming this quota reset", "resetTooEarly"],
2172
2314
  ["You must acknowledge that this may consume one quota reset", "resetAcknowledgeRequired"],
2173
- ["The signed-in ChatGPT account changed", "resetAccountChanged"]
2315
+ ["The signed-in ChatGPT account changed", "resetAccountChanged"],
2316
+ ["Quota reset result is uncertain; retry this confirmation to check the same request", "resetUncertain"]
2174
2317
  ])).get(error instanceof Error ? error.message : "") ?? "resetFailed");
2175
2318
  }
2176
2319
  function UsageCard({ rpc, t, signedIn, resetKey }) {
@@ -2281,7 +2424,10 @@ window.__ModuleLoader__.load({
2281
2424
  }),
2282
2425
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2283
2426
  className: "codexSubscriptionLimitMeta",
2284
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: windowLabel(window.windowSeconds, t) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetTime, {
2427
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: window.forecast?.status === "ready" ? fill(t("quotaForecast"), {
2428
+ symbol: window.forecast.survivesReset ? "≥" : "≈",
2429
+ duration: formatRunway(window.forecast.runwaySeconds, t)
2430
+ }) : windowLabel(window.windowSeconds, t) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ResetTime, {
2285
2431
  resetsAt: window.resetsAt,
2286
2432
  t
2287
2433
  })]
@@ -2437,43 +2583,50 @@ window.__ModuleLoader__.load({
2437
2583
  t
2438
2584
  })
2439
2585
  }, 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
2586
  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));
2587
+ const installDirectorySlots = (scope) => {
2588
+ const modelDirectories = scope.get("modelDirectories");
2589
+ scope.slots.inject("conversation.input.right", () => scope.slots.register({
2590
+ name: "conversation.input.right",
2591
+ id: "codex-subscription-quota",
2592
+ order: 15,
2593
+ locale: NS,
2594
+ inject: (sessionId) => ({
2595
+ preference,
2596
+ rpc: connection.rpc,
2597
+ t,
2598
+ directory: modelDirectories.directoryFor(sessionId).store
2599
+ })
2600
+ }, CodexComposerQuota));
2601
+ scope.slots.inject("conversation.input.model", () => scope.slots.register({
2602
+ name: "conversation.input.model",
2603
+ priority: -10,
2604
+ locale: NS,
2605
+ inject: (sessionId) => {
2606
+ const directory = modelDirectories.directoryFor(sessionId);
2607
+ const available = sessions.subagentAddress(sessionId) === void 0;
2608
+ return {
2609
+ available,
2610
+ directory: directory.store,
2611
+ load: () => {
2612
+ if (available) directory.load();
2613
+ },
2614
+ select: (selection) => available ? directory.select(selection).then(() => true, () => false) : Promise.resolve(false),
2615
+ preference
2616
+ };
2617
+ }
2618
+ }, CodexModelSelect));
2619
+ };
2620
+ if (ctx.get("remote.session") === void 0) installDirectorySlots(ctx);
2621
+ else ctx.inject(["remote.session"], installDirectorySlots);
2471
2622
  const conversation = ctx.get("conversation");
2472
2623
  ctx.slots.inject("tool.call.toolview", () => ctx.slots.register({
2473
2624
  name: "tool.call.toolview",
2474
2625
  key: "codex_image_generate",
2475
2626
  locale: NS,
2476
2627
  inject: (sessionId) => ({
2628
+ sessionId,
2629
+ rpc: connection.rpc,
2477
2630
  t,
2478
2631
  loadImage: (attachment) => conversation.resolveImage(sessionId, attachment),
2479
2632
  getImageViewer: () => {