dsh-lcx-codex 0.4.3 → 0.5.0-pre.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
@@ -2,8 +2,9 @@
2
2
  (() => {
3
3
  // src/client/search-media.ts
4
4
  var SEARCH_MEDIA_LIMIT = 60;
5
- var IMAGE_EXTENSION = /\.(?:jpe?g|png|webp|gif|avif)$/i;
6
5
  var VIDEO_EXTENSION = /\.(?:mp4|webm|ogv)$/i;
6
+ var IMAGE_EXTENSION = /\.(?:jpe?g|png|webp|gif|avif)$/i;
7
+ var IMAGE_CDN_HOSTS = /* @__PURE__ */ new Set(["images.unsplash.com", "plus.unsplash.com", "images.pexels.com"]);
7
8
  var URL_TOKEN = /https?:\/\/[^\s<>"'`,。;:!?、“”‘’()【】《》]+/gi;
8
9
  var REFERENCE_DEFINITION = /^ {0,3}\[([^\]\r\n]+)\]:[ \t]*(?:<([^>\s]+)>|(\S+))(?:[ \t]+.*)?$/gm;
9
10
  var REFERENCE_IMAGE = /!\[([^\]\r\n]*)\](?:\s*\[([^\]\r\n]*)\])?/g;
@@ -174,12 +175,13 @@
174
175
  return null;
175
176
  return parsed;
176
177
  }
177
- function mediaItem(value) {
178
+ function proseMediaItem(value) {
178
179
  const parsed = publicHttpUrl(value);
179
180
  if (parsed === null) return null;
180
- const imageCdn = parsed.protocol === "https:" && ["images.unsplash.com", "plus.unsplash.com", "images.pexels.com"].includes(parsed.hostname) && !/\.(svg|html?|js)$/i.test(parsed.pathname);
181
- const kind = IMAGE_EXTENSION.test(parsed.pathname) || imageCdn ? "image" : VIDEO_EXTENSION.test(parsed.pathname) ? "video" : null;
182
- return kind === null ? null : { kind, url: parsed.href };
181
+ if (VIDEO_EXTENSION.test(parsed.pathname)) return { kind: "video", url: parsed.href };
182
+ if (IMAGE_EXTENSION.test(parsed.pathname) || parsed.protocol === "https:" && IMAGE_CDN_HOSTS.has(parsed.hostname) && !/\.(?:svg|html?|js)$/i.test(parsed.pathname))
183
+ return { kind: "image", url: parsed.href };
184
+ return null;
183
185
  }
184
186
  function referenceLabel(value) {
185
187
  return value.trim().replace(/\s+/g, " ").toLowerCase();
@@ -226,7 +228,7 @@
226
228
  }
227
229
  return result;
228
230
  }
229
- function extractSearchMedia(text) {
231
+ function extractDirectMediaLinks(text) {
230
232
  let visible = stripHtml(stripInlineCode(stripFencedCode(text)));
231
233
  const definitions = /* @__PURE__ */ new Map();
232
234
  visible.replace(REFERENCE_DEFINITION, (_match, label, angle, bare) => {
@@ -234,19 +236,16 @@
234
236
  return _match;
235
237
  });
236
238
  const nativeImages = /* @__PURE__ */ new Set();
237
- const addNativeImage = (value) => {
238
- const item = mediaItem(value);
239
- if (item?.kind === "image") nativeImages.add(new URL(item.url).href.replace(/#.*$/, ""));
239
+ const addNative = (value) => {
240
+ const parsed = publicHttpUrl(value);
241
+ if (parsed) nativeImages.add(parsed.href.replace(/#.*$/, ""));
240
242
  };
241
- visible = eraseInlineImages(visible, addNativeImage);
242
- visible = visible.replace(
243
- REFERENCE_IMAGE,
244
- (_match, alt, explicit) => {
245
- const value = definitions.get(referenceLabel(explicit === void 0 || explicit === "" ? alt : explicit));
246
- if (value !== void 0) addNativeImage(value);
247
- return " ".repeat(_match.length);
248
- }
249
- );
243
+ visible = eraseInlineImages(visible, addNative);
244
+ visible = visible.replace(REFERENCE_IMAGE, (_match, alt, explicit) => {
245
+ const value = definitions.get(referenceLabel(explicit === void 0 || explicit === "" ? alt : explicit));
246
+ if (value !== void 0) addNative(value);
247
+ return " ".repeat(_match.length);
248
+ });
250
249
  const candidates = [];
251
250
  visible = collectInlineLinkDestinations(
252
251
  visible,
@@ -254,7 +253,7 @@
254
253
  );
255
254
  visible.replace(
256
255
  REFERENCE_LINK,
257
- (_match, prefix, label, explicit) => {
256
+ (_match, _prefix, label, explicit) => {
258
257
  const value = definitions.get(referenceLabel(explicit === "" ? label : explicit));
259
258
  if (value !== void 0) candidates.push(value);
260
259
  return _match;
@@ -265,10 +264,10 @@
265
264
  const result = [];
266
265
  const seen = /* @__PURE__ */ new Set();
267
266
  for (const candidate of candidates) {
268
- const item = mediaItem(candidate);
267
+ const item = proseMediaItem(candidate);
269
268
  if (item === null) continue;
270
269
  const key = item.kind === "video" ? item.url : item.url.replace(/#.*$/, "");
271
- if (nativeImages.has(key) || seen.has(key)) continue;
270
+ if (seen.has(key) || item.kind === "image" && nativeImages.has(key)) continue;
272
271
  seen.add(key);
273
272
  result.push(item);
274
273
  if (result.length === SEARCH_MEDIA_LIMIT) break;
@@ -279,179 +278,200 @@
279
278
  // src/client/inline-media.ts
280
279
  var inlineMediaCss = `
281
280
  [data-chat-flow-kind="lcx-search-media"]{display:none!important}
282
- .lcx-media-strip{display:flex;align-items:flex-start;flex-wrap:wrap;gap:8px;margin:8px 0 4px;line-height:1}
283
- .lcx-media-strip:empty{display:none}
284
- .lcx-media-strip:not(:has(.lcx-inline-media:not([hidden]))):not(:has(.lcx-media-toggle)){display:none}
285
- .lcx-media-toggle-row{flex:0 0 100%;min-width:0}
286
- .lcx-media-toggle{display:flex;align-items:center;justify-content:flex-start;gap:7px;width:fit-content;max-width:100%;min-width:0;padding:2px 0;border:0;border-radius:3px;background:transparent;color:var(--dsw-alias-label-secondary,#748096);font:12px/20px system-ui;cursor:pointer;text-align:left;transition:color .15s}
287
- .lcx-media-toggle::after{content:'';width:5px;height:5px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:translateY(-2px) rotate(45deg)}
288
- .lcx-media-toggle[aria-expanded="true"]::after{transform:translateY(1px) rotate(225deg)}
289
- .lcx-media-toggle:hover{color:var(--dsw-alias-link,#416de0);background:transparent}
290
- .lcx-inline-media{display:inline-flex;max-width:100%;line-height:1}
291
- .lcx-inline-media[hidden]{display:none}
292
- .lcx-media-thumb{display:block;position:relative;max-width:100%;padding:0;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;overflow:hidden;background:var(--dsw-alias-bg-layer-2,#f5f5f5);color:inherit;cursor:zoom-in;box-shadow:0 2px 6px #00000009;transition:box-shadow .15s,transform .15s,background .15s;font-family:inherit}
293
- .lcx-media-thumb:hover{box-shadow:0 3px 12px #00000016;transform:translateY(-1px)}
294
- .lcx-media-thumb img{display:block!important;width:auto!important;height:auto!important;max-width:144px!important;max-height:144px!important;object-fit:contain;margin:0!important;border-radius:0!important}
295
- .lcx-media-thumb:not([data-video])::after{content:'\u2197';position:absolute;right:6px;bottom:6px;display:grid;place-items:center;width:22px;height:22px;border-radius:7px;background:#14212b99;color:white;font:15px/1 system-ui;backdrop-filter:blur(6px);pointer-events:none}
296
- .lcx-media-thumb:focus-visible,.lcx-media-close:focus-visible{outline:2px solid var(--dsw-alias-link,#3478f6);outline-offset:3px}
297
- .lcx-media-thumb[data-video]{cursor:pointer;padding:7px 10px;font-size:12px;font-weight:500;display:flex;align-items:center;gap:8px;line-height:18px;border-radius:10px;color:var(--dsw-alias-link,#416de0);background:color-mix(in srgb,currentColor 7%,transparent);border-color:color-mix(in srgb,currentColor 13%,transparent);box-shadow:none}
298
- .lcx-media-thumb[data-video]:hover{background:color-mix(in srgb,currentColor 12%,transparent);box-shadow:0 2px 6px #00000008}
299
- .lcx-media-play-label{display:flex;align-items:center;gap:7px;white-space:nowrap}
300
- .lcx-media-play-label::before{content:'';width:0;height:0;border-top:4px solid transparent;border-bottom:4px solid transparent;border-left:6px solid currentColor}
301
- .lcx-media-format{font:10px/16px system-ui;letter-spacing:.04em;opacity:.7;border-left:1px solid color-mix(in srgb,currentColor 24%,transparent);padding-left:8px}
302
- .lcx-media-thumb[data-video]:has(img){padding:0}
303
- .lcx-media-thumb[data-video]:has(img) .lcx-media-play-label{position:absolute;inset:0;display:flex;justify-content:center;color:white;background:#0003;font-size:0}
304
- .lcx-media-thumb[data-video]:has(img) .lcx-media-format{display:none}
305
- .lcx-media-dialog{box-sizing:border-box;border:1px solid #ffffff24;border-radius:16px;padding:48px 16px 16px;background:#15171b;color:#fff;max-width:94vw;max-height:92vh;box-shadow:0 20px 80px #0006}
306
- .lcx-media-dialog::backdrop{background:#080b12bb;backdrop-filter:blur(5px)}
307
- .lcx-media-dialog img,.lcx-media-dialog video{display:block;object-fit:contain;max-width:86vw;max-height:74vh;width:auto;height:auto;border-radius:6px;margin:auto}
308
- .lcx-media-dialog video{width:min(800px,86vw)}
309
- .lcx-media-close{position:absolute;right:10px;top:8px;width:32px;height:32px;border:0;border-radius:50%;background:#ffffff14;color:white;font-size:23px;cursor:pointer}
310
- .lcx-media-nav{position:absolute;left:16px;top:8px;display:flex;align-items:center;gap:8px;font:12px/20px system-ui;color:#ddd}
281
+ .lcx-media{box-sizing:border-box;display:flex;flex-direction:column;gap:12px;min-width:0;max-width:100%;margin:16px 0 4px;font-family:var(--dsw-font-family,inherit);color:var(--dsw-alias-label-primary,inherit)}
282
+ .lcx-media *{box-sizing:border-box}
283
+ .lcx-media[hidden]{display:none}
284
+ .lcx-media-rail{position:relative;min-width:0;--lcx-h:136px}
285
+ .lcx-media-rail[data-count="1"]{--lcx-h:220px}
286
+ .lcx-media-rail[data-count="2"]{--lcx-h:176px}
287
+ .lcx-media-track{--lcx-fade-l:0px;--lcx-fade-r:0px;display:flex;gap:8px;margin:0;padding:0;list-style:none;overflow-x:auto;overflow-y:hidden;scroll-snap-type:x proximity;scroll-behavior:smooth;scrollbar-width:none;overscroll-behavior-x:contain;-webkit-mask-image:linear-gradient(to right,transparent 0,#000 var(--lcx-fade-l),#000 calc(100% - var(--lcx-fade-r)),transparent 100%);mask-image:linear-gradient(to right,transparent 0,#000 var(--lcx-fade-l),#000 calc(100% - var(--lcx-fade-r)),transparent 100%)}
288
+ .lcx-media-track::-webkit-scrollbar{display:none}
289
+ .lcx-media-rail[data-more-left] .lcx-media-track{--lcx-fade-l:40px}
290
+ .lcx-media-rail[data-more-right] .lcx-media-track{--lcx-fade-r:40px}
291
+ .lcx-media-tile{position:relative;flex:none;height:var(--lcx-h);aspect-ratio:var(--lcx-ar,4/3);max-width:100%;margin:0;padding:0;scroll-snap-align:start;border-radius:var(--dsw-radius-md,12px);overflow:hidden;background:var(--dsw-alias-bg-skeleton,#0000000a)}
292
+ .lcx-media-tile::after{content:"";position:absolute;inset:0;border-radius:inherit;box-shadow:inset 0 0 0 .5px var(--dsw-alias-border-l2,#0000001a);pointer-events:none}
293
+ .lcx-media-tile[data-state="loading"]{background:linear-gradient(100deg,var(--dsw-alias-bg-skeleton,#0000000a) 30%,var(--dsw-alias-interactive-bg-hover,#2631480f) 50%,var(--dsw-alias-bg-skeleton,#0000000a) 70%) 0 0/300% 100%;animation:lcx-media-shimmer 1.4s ease-in-out infinite}
294
+ @keyframes lcx-media-shimmer{from{background-position:100% 0}to{background-position:0 0}}
295
+ .lcx-media-open{position:absolute;inset:0;display:block;width:100%;height:100%;margin:0;padding:0;border:0;border-radius:inherit;background:none;color:inherit;font:inherit;cursor:zoom-in}
296
+ .lcx-media-open:disabled{cursor:default}
297
+ .lcx-media-open img{display:block;width:100%;height:100%;max-width:none;margin:0;padding:0;border:0;border-radius:0;object-fit:cover;opacity:0;transition:opacity .25s ease,transform .45s cubic-bezier(.2,.7,.2,1)}
298
+ .lcx-media-tile[data-state="loaded"] .lcx-media-open img{opacity:1}
299
+ .lcx-media-tile[data-fit="contain"] .lcx-media-open img{object-fit:contain}
300
+ .lcx-media-tile[data-state="loaded"]:hover .lcx-media-open img{transform:scale(1.04)}
301
+ .lcx-media-fail{position:absolute;inset:0;display:grid;place-items:center;color:var(--dsw-alias-label-tertiary,#8a93a6)}
302
+ .lcx-media-fail svg{width:22px;height:22px}
303
+ .lcx-media-chip{position:absolute;left:6px;bottom:6px;z-index:1;max-width:calc(100% - 12px);overflow:hidden;padding:2px 8px;border-radius:var(--dsw-radius-xs,6px);background:#00000073;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);color:#fff;font-size:11px;line-height:16px;font-weight:500;white-space:nowrap;text-overflow:ellipsis;text-decoration:none;opacity:0;transform:translateY(3px);transition:opacity .18s ease,transform .18s ease}
304
+ a.lcx-media-chip:hover{text-decoration:underline;text-underline-offset:2px}
305
+ .lcx-media-tile:hover .lcx-media-chip,.lcx-media-tile:focus-within .lcx-media-chip,.lcx-media-tile[data-state="failed"] .lcx-media-chip{opacity:1;transform:none}
306
+ .lcx-media-tile[data-state="failed"] .lcx-media-chip{background:transparent;-webkit-backdrop-filter:none;backdrop-filter:none;color:var(--dsw-alias-label-tertiary,#8a93a6)}
307
+ .lcx-media-nav{position:absolute;top:50%;z-index:2;display:grid;place-items:center;width:32px;height:32px;margin-top:-16px;padding:0;border:0;border-radius:999px;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 0 0 .5px var(--dsw-alias-border-l3,#0000001f),0 2px 10px #00000021;color:var(--dsw-alias-label-primary,#1f2430);cursor:pointer;opacity:0;transition:opacity .15s ease}
311
308
  .lcx-media-nav[hidden]{display:none}
312
- .lcx-media-nav button{width:30px;height:30px;border:0;border-radius:9px;background:#ffffff14;color:white;font-size:20px;cursor:pointer}
313
- .lcx-media-nav button:disabled{opacity:.3;cursor:default}
314
- .lcx-media-nav button:focus-visible,.lcx-media-toggle:focus-visible{outline:2px solid #648dfb;outline-offset:2px}
315
- .lcx-media-dialog a{display:block;width:fit-content;margin:12px auto 0;color:#c6d7ff;font:12px/18px system-ui;text-decoration:none}
316
- @media(prefers-reduced-motion:reduce){.lcx-media-thumb{transition:none;transform:none}}
309
+ .lcx-media-nav[data-dir="prev"]{left:8px}
310
+ .lcx-media-nav[data-dir="next"]{right:8px}
311
+ .lcx-media-nav svg{width:16px;height:16px}
312
+ .lcx-media-rail:hover .lcx-media-nav,.lcx-media-nav:focus-visible{opacity:1}
313
+ .lcx-media-videos{display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,210px),1fr));gap:8px;min-width:0}
314
+ .lcx-media-video{display:flex;align-items:center;gap:10px;width:100%;min-width:0;margin:0;padding:6px 12px 6px 6px;border:0;border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-interactive-bg-hover,#2631480f);color:inherit;font:inherit;text-align:left;cursor:pointer;transition:background .15s ease}
315
+ .lcx-media-video:hover{background:var(--dsw-alias-interactive-bg-active,#26314817)}
316
+ .lcx-media-play{flex:none;display:grid;place-items:center;width:36px;height:36px;border-radius:999px;background:var(--dsw-alias-label-primary,#1f2430);color:var(--dsw-alias-label-primary-foreground,#fff);transition:transform .15s ease}
317
+ .lcx-media-video:hover .lcx-media-play{transform:scale(1.06)}
318
+ .lcx-media-play svg{width:16px;height:16px;margin-left:2px}
319
+ .lcx-media-video-text{display:flex;flex-direction:column;min-width:0}
320
+ .lcx-media-video-title{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:13px;line-height:19px;font-weight:500;color:var(--dsw-alias-label-primary,inherit)}
321
+ .lcx-media-video-sub{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary,#8a93a6)}
322
+ .lcx-media-player{grid-column:1/-1;position:relative;width:min(100%,640px);aspect-ratio:16/9;border-radius:var(--dsw-radius-md,12px);overflow:hidden;background:#000}
323
+ .lcx-media-player video{display:block;width:100%;height:100%;max-width:none;margin:0;border:0;object-fit:contain;background:#000}
324
+ .lcx-media-player-close{position:absolute;top:8px;right:8px;z-index:1;display:grid;place-items:center;width:28px;height:28px;padding:0;border:0;border-radius:999px;background:#00000080;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);color:#fff;cursor:pointer}
325
+ .lcx-media-player-close svg,.lcx-media-close svg{width:14px;height:14px}
326
+ .lcx-media-player-fail{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;padding:16px;color:#ffffffd9;font-size:13px;line-height:20px;text-align:center}
327
+ .lcx-media-player-fail a{color:#fff;text-decoration:underline;text-underline-offset:3px}
328
+ .lcx-media-open:focus-visible,.lcx-media-chip:focus-visible,.lcx-media-nav:focus-visible,.lcx-media-video:focus-visible,.lcx-media-player-close:focus-visible,.lcx-media-close:focus-visible,.lcx-media-dialog a:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-link,#416de0));outline-offset:2px}
329
+ .lcx-media-open:focus-visible{outline-offset:-2px}
330
+ .lcx-media-dialog{position:fixed;inset:0;box-sizing:border-box;width:100vw;height:100vh;max-width:none;max-height:none;margin:0;padding:48px;border:0;overflow:hidden;background:var(--dsw-alias-bg-mask-1,#0009);-webkit-backdrop-filter:var(--dsw-mask-blur,blur(8px));backdrop-filter:var(--dsw-mask-blur,blur(8px));color:var(--dsw-alias-label-primary,inherit);font-family:var(--dsw-font-family,inherit)}
331
+ .lcx-media-dialog[open]{display:grid;place-items:center}
332
+ .lcx-media-dialog::backdrop{background:transparent}
333
+ .lcx-media-dialog-body{display:flex;flex-direction:column;align-items:center;gap:12px;min-width:0;max-width:min(100%,1600px);max-height:100%}
334
+ .lcx-media-dialog img{display:block;max-width:100%;max-height:calc(100vh - 150px);width:auto;height:auto;object-fit:contain;border-radius:var(--dsw-radius-md,12px);box-shadow:var(--dsw-elevation-prominent,0 10px 40px #0005)}
335
+ .lcx-media-dialog-fail{display:grid;place-items:center;min-width:min(320px,100%);min-height:160px;padding:24px;border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-1,#fff);color:var(--dsw-alias-label-secondary,#5a6478);font-size:13px;line-height:20px}
336
+ .lcx-media-dialog-bar{display:flex;align-items:center;gap:14px;max-width:100%;padding:6px 16px;border-radius:999px;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 0 0 .5px var(--dsw-alias-border-l3,#0000001f);font-size:12px;line-height:20px}
337
+ .lcx-media-dialog-caption{min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:var(--dsw-alias-label-secondary,#5a6478)}
338
+ .lcx-media-dialog-bar a{flex:none;color:var(--dsw-alias-link,#416de0);text-decoration:none}
339
+ .lcx-media-dialog-bar a:hover{text-decoration:underline;text-underline-offset:3px}
340
+ .lcx-media-close{position:fixed;top:20px;right:20px;z-index:1;display:grid;place-items:center;width:36px;height:36px;padding:0;border:0;border-radius:999px;background:var(--dsw-alias-bg-layer-1,#fff);box-shadow:0 0 0 .5px var(--dsw-alias-border-l3,#0000001f);color:var(--dsw-alias-label-primary,#1f2430);cursor:pointer}
341
+ @media(max-width:520px){.lcx-media-rail{--lcx-h:112px}.lcx-media-rail[data-count="1"]{--lcx-h:184px}.lcx-media-rail[data-count="2"]{--lcx-h:136px}.lcx-media-dialog{padding:16px}.lcx-media-close{top:12px;right:12px}}
342
+ @media(hover:none){.lcx-media-chip{opacity:1;transform:none}.lcx-media-nav{display:none}}
343
+ @media(prefers-reduced-motion:reduce){.lcx-media-track{scroll-behavior:auto}.lcx-media-tile[data-state="loading"]{animation:none}.lcx-media-open img,.lcx-media-chip,.lcx-media-nav,.lcx-media-video,.lcx-media-play{transition:none}.lcx-media-tile[data-state="loaded"]:hover .lcx-media-open img{transform:none}}
317
344
  `;
318
- var VISIBLE_MEDIA_LIMIT = 4;
319
- var mediaKey = (item) => item.kind === "video" ? item.url : item.url.replace(/#.*$/, "");
320
- function previewItems(items) {
345
+ var nativeMarkdownImageCss = `
346
+ [data-chat-flow-kind="assistant-step"] p>button:has(>img:only-child){display:block;width:fit-content;max-width:100%;margin:4px 0 8px}
347
+ `;
348
+ var ICON_PLAY = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5.5v13a1 1 0 0 0 1.5.86l10.5-6.5a1 1 0 0 0 0-1.72L9.5 4.64A1 1 0 0 0 8 5.5Z" fill="currentColor"/></svg>';
349
+ var ICON_CLOSE = '<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M3.5 3.5l9 9m0-9l-9 9" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>';
350
+ var ICON_BROKEN = '<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><rect x="3.5" y="4.5" width="17" height="15" rx="2.5" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M3.5 16l4.5-4.5 3 3 2.5-2.5 7 6" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><circle cx="9" cy="9" r="1.3" fill="currentColor"/></svg>';
351
+ var ICON_PREV = '<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M10 3.5 5.5 8l4.5 4.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
352
+ var ICON_NEXT = '<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false"><path d="M6 3.5 10.5 8 6 12.5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
353
+ var TILE_ASPECT_MIN = 0.5;
354
+ var TILE_ASPECT_MAX = 2.2;
355
+ var imageKey = (url) => url.replace(/#.*$/, "");
356
+ var hostOf = (url) => {
357
+ try {
358
+ return new URL(url).hostname.replace(/^www\./, "");
359
+ } catch {
360
+ return url;
361
+ }
362
+ };
363
+ var fileNameOf = (url) => {
364
+ try {
365
+ const name = decodeURIComponent(new URL(url).pathname.split("/").filter(Boolean).at(-1) ?? "");
366
+ return name || hostOf(url);
367
+ } catch {
368
+ return url;
369
+ }
370
+ };
371
+ function railImages(items) {
372
+ const seen = /* @__PURE__ */ new Set();
373
+ const result = [];
374
+ for (const item of [...items.filter((value) => value.structured === true), ...items.filter((value) => value.structured !== true)]) {
375
+ if (item.kind !== "image") continue;
376
+ const key = imageKey(item.url);
377
+ if (seen.has(key)) continue;
378
+ seen.add(key);
379
+ result.push(item);
380
+ }
381
+ return result;
382
+ }
383
+ function videoItems(items) {
321
384
  const groups = /* @__PURE__ */ new Map();
322
385
  for (const item of items) {
323
- if (item.kind === "page") continue;
386
+ if (item.kind !== "video") continue;
324
387
  const url = new URL(item.url);
325
- const identity = item.kind === "video" ? JSON.stringify(["video", url.origin, url.pathname.replace(/\.(mp4|webm|ogv)$/i, ""), url.search, url.hash]) : JSON.stringify(["image", mediaKey(item)]);
388
+ const identity = JSON.stringify([url.origin, url.pathname.replace(/\.(mp4|webm|ogv)$/i, ""), url.search, url.hash]);
326
389
  const group = groups.get(identity) ?? [];
327
390
  if (!group.some((value) => value.url === item.url)) group.push(item);
328
391
  groups.set(identity, group);
329
392
  }
330
- const result = /* @__PURE__ */ new Map();
331
393
  const priority = (url) => ({ mp4: 0, webm: 1, ogv: 2 })[new URL(url).pathname.split(".").at(-1).toLowerCase()] ?? 3;
332
- for (const group of groups.values()) {
394
+ return [...groups.values()].map((group) => {
333
395
  group.sort((a, b) => priority(a.url) - priority(b.url));
334
- const item = { ...group[0], sources: group.map((value) => value.url) };
335
- for (const value of group) result.set(mediaKey(value), item);
336
- }
337
- return result;
396
+ return { ...group[0], sources: group.map((value) => value.url) };
397
+ });
338
398
  }
339
399
  var activeDialogByDocument = /* @__PURE__ */ new WeakMap();
340
- function supportsInlineMediaDom(marker) {
400
+ function inlineMediaAnswer(marker) {
341
401
  const row = marker.closest('[data-chat-flow-kind="lcx-search-media"]');
342
- const answer = row?.previousElementSibling;
343
402
  const ElementClass = marker.ownerDocument.defaultView?.HTMLElement;
344
- return Boolean(
345
- ElementClass && answer instanceof ElementClass && row?.parentElement === answer.parentElement && answer.dataset.chatFlowKind === "assistant-step" && typeof answer.dataset.chatTurn === "string" && answer.dataset.chatTurn !== "" && answer.dataset.chatTurn === row?.dataset.chatTurn
346
- );
403
+ if (!ElementClass || !row || !row.dataset.chatTurn) return null;
404
+ const group = row.closest("[data-chat-group-key][data-step-process]");
405
+ const column = group?.parentElement;
406
+ if (!group || !(column instanceof ElementClass) || !column.hasAttribute("data-chat-flow") || group.dataset.chatTurn !== row.dataset.chatTurn) return null;
407
+ let answer = null;
408
+ for (const child of column.children) {
409
+ if (child === group) break;
410
+ if (child instanceof ElementClass && child.dataset.chatFlowKind === "assistant-step" && child.dataset.chatGroupPart === "response" && child.dataset.chatTurn === row.dataset.chatTurn) {
411
+ answer = child;
412
+ }
413
+ }
414
+ return answer;
347
415
  }
348
- function installInlineMedia(marker, items, labels) {
349
- if (items.length === 0 || !supportsInlineMediaDom(marker)) return () => {
416
+ function installInlineMedia(marker, items, labels, host = {}) {
417
+ const resolvedAnswer = inlineMediaAnswer(marker);
418
+ const images = railImages(items), videos = videoItems(items);
419
+ if (!resolvedAnswer || images.length === 0 && videos.length === 0) return () => {
350
420
  };
351
- const row = marker.closest('[data-chat-flow-kind="lcx-search-media"]');
352
- const answer = row.previousElementSibling;
421
+ const answer = resolvedAnswer;
353
422
  const doc = marker.ownerDocument;
354
- const wanted = previewItems(items.filter((item) => item.structured !== true));
355
- const structured = [...new Set(previewItems(items.filter((item) => item.structured === true)).values())];
356
- const previews = /* @__PURE__ */ new Map();
357
- const strips = /* @__PURE__ */ new Map();
358
- const entries = /* @__PURE__ */ new Map();
359
- let expanded = false;
360
- const toggle = doc.createElement("button");
361
- toggle.type = "button";
362
- toggle.className = "lcx-media-toggle";
363
- const toggleRow = doc.createElement("span");
364
- toggleRow.className = "lcx-media-toggle-row";
365
- toggleRow.append(toggle);
366
- toggle.onclick = () => {
367
- expanded = !expanded;
368
- updateVisibility();
369
- toggle.focus({ preventScroll: true });
370
- };
371
- const failed = /* @__PURE__ */ new Set();
423
+ const win = doc.defaultView;
424
+ const cleanups = [];
372
425
  let disposed = false;
373
426
  let frame = 0;
374
427
  let ownedClose;
375
- function orderedEntries() {
376
- return [...answer.querySelectorAll(".lcx-inline-media")].flatMap((preview) => {
377
- const entry = entries.get(preview);
378
- return entry ? [{ preview, ...entry }] : [];
379
- });
380
- }
381
- function updateVisibility() {
382
- const all = orderedEntries();
383
- all.forEach((entry, index) => {
384
- const hidden = !expanded && index >= VISIBLE_MEDIA_LIMIT;
385
- if (entry.preview.hidden !== hidden) entry.preview.hidden = hidden;
386
- if (!hidden) {
387
- entry.load?.();
388
- const saved = entries.get(entry.preview);
389
- if (saved) saved.load = void 0;
390
- }
391
- });
392
- if (all.length <= VISIBLE_MEDIA_LIMIT) {
393
- toggleRow.remove();
394
- return;
395
- }
396
- const text = expanded ? labels.less : labels.more.replace("{count}", String(all.length - VISIBLE_MEDIA_LIMIT));
397
- if (toggle.textContent !== text) toggle.textContent = text;
398
- const aria = String(expanded);
399
- if (toggle.getAttribute("aria-expanded") !== aria) toggle.setAttribute("aria-expanded", aria);
400
- const parent = all[expanded ? all.length - 1 : VISIBLE_MEDIA_LIMIT - 1].preview.parentElement;
401
- if (toggleRow.parentElement !== parent || toggleRow !== parent.lastElementChild) parent.append(toggleRow);
402
- }
403
- function removePreview(preview) {
404
- const strip = preview.parentElement;
405
- entries.get(preview)?.cleanup();
406
- entries.delete(preview);
407
- preview.remove();
408
- if (strip?.classList.contains("lcx-media-strip") && !strip.childElementCount) {
409
- strip.remove();
410
- for (const [block, value] of strips) if (value === strip) strips.delete(block);
411
- }
412
- }
413
- function placePreview(anchor, preview) {
414
- const block = anchor.closest("p,li,td,th,figcaption");
415
- const owner = block && answer.contains(block) ? block : anchor;
416
- let strip = strips.get(owner);
417
- if (!strip?.isConnected) {
418
- strip = doc.createElement("span");
419
- strip.className = "lcx-media-strip";
420
- if (owner === anchor) anchor.after(strip);
421
- else owner.append(strip);
422
- strips.set(owner, strip);
423
- }
424
- strip.append(preview);
425
- }
426
- function open(item, trigger, fail) {
428
+ const token = Math.random().toString(36).slice(2);
429
+ answer.dataset.lcxMediaOwner = token;
430
+ for (const stale of [...answer.children]) if (stale.classList.contains("lcx-media")) stale.remove();
431
+ const root = doc.createElement("div");
432
+ root.className = "lcx-media";
433
+ root.dataset.lcxMedia = "";
434
+ function openDialog(item, trigger, description) {
427
435
  activeDialogByDocument.get(doc)?.();
428
436
  const dialog = doc.createElement("dialog");
429
437
  dialog.className = "lcx-media-dialog";
430
- dialog.setAttribute("aria-label", item.kind === "image" ? labels.image : labels.video);
438
+ dialog.setAttribute("aria-label", labels.imageDialog);
439
+ dialog.dataset.kind = "image";
440
+ const body = doc.createElement("div");
441
+ body.className = "lcx-media-dialog-body";
431
442
  const close = doc.createElement("button");
432
443
  close.type = "button";
433
444
  close.className = "lcx-media-close";
434
- close.textContent = "\xD7";
445
+ close.innerHTML = ICON_CLOSE;
435
446
  close.setAttribute("aria-label", labels.close);
436
- let media = doc.createElement(item.kind === "image" ? "img" : "video");
437
- if (media instanceof HTMLImageElement) {
438
- media.alt = trigger.getAttribute("aria-label") ?? labels.image;
439
- media.referrerPolicy = "no-referrer";
440
- } else {
441
- media.controls = true;
442
- media.playsInline = true;
443
- media.preload = "none";
444
- }
445
- if (media instanceof HTMLVideoElement) media.src = item.url;
447
+ const bar = doc.createElement("div");
448
+ bar.className = "lcx-media-dialog-bar";
449
+ const caption = doc.createElement("span");
450
+ caption.className = "lcx-media-dialog-caption";
451
+ caption.textContent = description;
452
+ caption.title = description;
446
453
  const source = doc.createElement("a");
447
454
  source.href = item.sourceUrl ?? item.url;
448
455
  source.target = "_blank";
449
456
  source.rel = "noopener noreferrer";
450
457
  source.textContent = labels.source;
458
+ bar.append(caption, source);
459
+ let media;
451
460
  let closed = false;
452
- let sourceIndex = 0;
453
- let previous;
454
- let next;
461
+ const image = doc.createElement("img");
462
+ image.alt = description;
463
+ image.referrerPolicy = "no-referrer";
464
+ image.src = item.url;
465
+ image.onerror = () => {
466
+ if (closed) return;
467
+ const fail = doc.createElement("div");
468
+ fail.className = "lcx-media-dialog-fail";
469
+ fail.textContent = labels.failed;
470
+ media.onerror = null;
471
+ media.replaceWith(fail);
472
+ media = fail;
473
+ };
474
+ media = image;
455
475
  const cleanup = () => {
456
476
  if (closed) return;
457
477
  closed = true;
@@ -459,15 +479,7 @@
459
479
  dialog.oncancel = null;
460
480
  dialog.onclick = null;
461
481
  dialog.onkeydown = null;
462
- previous && (previous.onclick = null);
463
- next && (next.onclick = null);
464
- media.onload = null;
465
482
  media.onerror = null;
466
- if (media instanceof HTMLVideoElement) {
467
- media.pause();
468
- media.removeAttribute("src");
469
- media.load();
470
- }
471
483
  dialog.remove();
472
484
  if (activeDialogByDocument.get(doc) === cleanup) activeDialogByDocument.delete(doc);
473
485
  if (ownedClose === cleanup) ownedClose = void 0;
@@ -481,253 +493,316 @@
481
493
  cleanup();
482
494
  };
483
495
  dialog.onclick = (event) => {
484
- if (event.target === dialog) {
485
- const rect = dialog.getBoundingClientRect();
486
- if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) cleanup();
487
- }
496
+ if (event.target === dialog) cleanup();
488
497
  };
489
- media.onerror = () => {
490
- if (closed) return;
491
- if (media instanceof HTMLVideoElement && sourceIndex + 1 < item.sources.length) {
492
- sourceIndex += 1;
493
- media.src = item.sources[sourceIndex];
494
- source.href = item.sources[sourceIndex];
495
- media.load();
496
- void media.play().catch(() => {
497
- });
498
- return;
498
+ dialog.onkeydown = (event) => {
499
+ if (event.key !== "Tab") return;
500
+ const focusable = [...dialog.querySelectorAll("button:not([disabled]),a[href]")];
501
+ if (focusable.length === 0) return;
502
+ const first = focusable[0], last = focusable[focusable.length - 1];
503
+ const active = doc.activeElement;
504
+ if (event.shiftKey && active === first) {
505
+ event.preventDefault();
506
+ last.focus();
507
+ } else if (!event.shiftKey && active === last) {
508
+ event.preventDefault();
509
+ first.focus();
499
510
  }
500
- cleanup();
501
- fail();
502
511
  };
503
- dialog.append(close, media, source);
512
+ body.append(media, bar);
513
+ dialog.append(body, close);
504
514
  doc.body.append(dialog);
505
- if (item.kind === "image") {
506
- const nav = doc.createElement("span");
507
- nav.className = "lcx-media-nav";
508
- const previousButton = doc.createElement("button"), nextButton = doc.createElement("button"), counter = doc.createElement("span");
509
- previous = previousButton;
510
- next = nextButton;
511
- previousButton.type = nextButton.type = "button";
512
- previousButton.textContent = "\u2039";
513
- nextButton.textContent = "\u203A";
514
- previousButton.setAttribute("aria-label", labels.previous);
515
- nextButton.setAttribute("aria-label", labels.next);
516
- counter.setAttribute("aria-live", "polite");
517
- nav.append(previousButton, counter, nextButton);
518
- dialog.append(nav);
519
- let currentKey = mediaKey(item);
520
- const images = () => orderedEntries().filter((entry) => entry.item.kind === "image");
521
- const select = (index) => {
515
+ dialog.showModal();
516
+ close.focus();
517
+ }
518
+ function openImage(item, trigger, description) {
519
+ activeDialogByDocument.get(doc)?.();
520
+ if (host.openImage) {
521
+ let closed = false;
522
+ let dispose = null;
523
+ const cleanup = () => {
522
524
  if (closed) return;
523
- const list = images(), entry = list[index];
524
- if (!entry) return;
525
- currentKey = mediaKey(entry.item);
526
- const image = doc.createElement("img");
527
- image.alt = entry.button.getAttribute("aria-label") ?? labels.image;
528
- image.referrerPolicy = "no-referrer";
529
- image.onerror = () => {
530
- if (closed || media !== image) return;
531
- entry.fail();
532
- const remaining = images();
533
- if (!remaining.length) {
534
- cleanup();
535
- return;
536
- }
537
- select(Math.min(index, remaining.length - 1));
538
- };
539
- media.onerror = null;
540
- media.replaceWith(image);
541
- media = image;
542
- source.href = entry.item.sourceUrl ?? entry.item.url;
543
- image.src = entry.item.url;
544
- counter.textContent = `${index + 1} / ${list.length}`;
545
- previousButton.disabled = index === 0;
546
- nextButton.disabled = index === list.length - 1;
547
- nav.hidden = list.length < 2;
548
- };
549
- const move = (offset) => {
550
- const list = images();
551
- select(list.findIndex((entry) => mediaKey(entry.item) === currentKey) + offset);
552
- };
553
- previousButton.onclick = () => move(-1);
554
- nextButton.onclick = () => move(1);
555
- dialog.onkeydown = (event) => {
556
- if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
557
- event.preventDefault();
558
- move(event.key === "ArrowLeft" ? -1 : 1);
559
- }
525
+ closed = true;
526
+ dispose?.();
527
+ if (activeDialogByDocument.get(doc) === cleanup) activeDialogByDocument.delete(doc);
528
+ if (ownedClose === cleanup) ownedClose = void 0;
529
+ if (trigger.isConnected) trigger.focus({ preventScroll: true });
560
530
  };
561
- select(images().findIndex((entry) => mediaKey(entry.item) === mediaKey(item)));
531
+ dispose = host.openImage({ src: item.url, alt: description, labels: { dialog: labels.imageDialog, close: labels.close }, onClose: cleanup });
532
+ if (dispose) {
533
+ ownedClose = cleanup;
534
+ activeDialogByDocument.set(doc, cleanup);
535
+ return;
536
+ }
537
+ closed = true;
562
538
  }
563
- dialog.showModal();
564
- close.focus();
565
- if (media instanceof HTMLVideoElement) void media.play().catch(() => {
566
- });
539
+ openDialog(item, trigger, description);
567
540
  }
568
- function createPreview(item, description, forget) {
569
- const key = mediaKey(item);
570
- if (failed.has(key)) return void 0;
571
- const preview = doc.createElement("span");
572
- preview.className = "lcx-inline-media";
573
- preview.dataset.url = item.url;
574
- if (item.structured) preview.dataset.structured = "";
575
- const button = doc.createElement("button");
576
- button.type = "button";
577
- button.className = "lcx-media-thumb";
578
- const label = item.kind === "image" ? labels.image : labels.video;
579
- button.setAttribute("aria-label", label + " \xB7 " + description);
580
- let thumbnail;
581
- const fail = () => {
582
- failed.add(key);
583
- removePreview(preview);
584
- forget?.();
585
- updateVisibility();
541
+ if (images.length) {
542
+ const rail = doc.createElement("div");
543
+ rail.className = "lcx-media-rail";
544
+ rail.dataset.count = String(Math.min(images.length, 3));
545
+ const track = doc.createElement("ul");
546
+ track.className = "lcx-media-track";
547
+ track.setAttribute("role", "list");
548
+ track.setAttribute("aria-label", `${labels.heading} \xB7 ${images.length}`);
549
+ const makeNav = (dir) => {
550
+ const button = doc.createElement("button");
551
+ button.type = "button";
552
+ button.className = "lcx-media-nav";
553
+ button.dataset.dir = dir;
554
+ button.hidden = true;
555
+ button.tabIndex = -1;
556
+ button.setAttribute("aria-label", dir === "prev" ? labels.previous : labels.next);
557
+ button.innerHTML = dir === "prev" ? ICON_PREV : ICON_NEXT;
558
+ button.onclick = () => track.scrollBy({ left: (dir === "prev" ? -1 : 1) * Math.max(160, track.clientWidth * 0.8), behavior: "smooth" });
559
+ cleanups.push(() => {
560
+ button.onclick = null;
561
+ });
562
+ return button;
586
563
  };
587
- const cleanup = () => {
588
- button.onclick = null;
589
- if (thumbnail) {
590
- thumbnail.onload = null;
591
- thumbnail.onerror = null;
592
- }
593
- entry.load = void 0;
564
+ const prev = makeNav("prev"), next = makeNav("next");
565
+ const updateEdges = () => {
566
+ if (disposed) return;
567
+ const max = track.scrollWidth - track.clientWidth;
568
+ const left = track.scrollLeft > 2, right = max - track.scrollLeft > 2;
569
+ rail.toggleAttribute("data-more-left", left);
570
+ rail.toggleAttribute("data-more-right", right);
571
+ prev.hidden = !left;
572
+ next.hidden = !right;
594
573
  };
595
- const entry = { item, button, fail, cleanup };
596
- const previewUrl = item.kind === "image" ? item.previewUrl ?? item.url : item.previewUrl ?? item.poster;
597
- if (previewUrl) {
574
+ track.addEventListener("scroll", updateEdges, { passive: true });
575
+ cleanups.push(() => track.removeEventListener("scroll", updateEdges));
576
+ const Resize = win?.ResizeObserver;
577
+ if (Resize) {
578
+ const resize = new Resize(updateEdges);
579
+ resize.observe(track);
580
+ cleanups.push(() => resize.disconnect());
581
+ }
582
+ for (const item of images) {
583
+ const domain = hostOf(item.sourceUrl ?? item.url);
584
+ const description = item.caption ?? domain;
585
+ const tile = doc.createElement("li");
586
+ tile.className = "lcx-media-tile";
587
+ tile.dataset.state = "loading";
588
+ tile.dataset.url = item.url;
589
+ const open = doc.createElement("button");
590
+ open.type = "button";
591
+ open.className = "lcx-media-open";
592
+ open.setAttribute("aria-label", `${labels.image} \xB7 ${description}`);
593
+ if (item.caption) open.title = item.caption;
598
594
  const image = doc.createElement("img");
599
- thumbnail = image;
600
595
  image.alt = "";
601
596
  image.loading = "lazy";
602
597
  image.decoding = "async";
603
598
  image.referrerPolicy = "no-referrer";
604
- button.append(image);
605
- button.tabIndex = -1;
606
- button.style.visibility = "hidden";
607
- preview.style.height = "1px";
608
- preview.style.margin = "0";
609
- const reveal = () => {
610
- button.tabIndex = 0;
611
- button.style.visibility = "";
612
- preview.style.height = "";
613
- preview.style.margin = "";
599
+ image.draggable = false;
600
+ open.append(image);
601
+ let source = item.previewUrl ?? item.url;
602
+ image.onload = () => {
603
+ if (tile.dataset.state !== "loading") return;
604
+ const ratio = image.naturalWidth > 0 && image.naturalHeight > 0 ? image.naturalWidth / image.naturalHeight : 0;
605
+ if (ratio) tile.style.setProperty("--lcx-ar", String(Math.min(TILE_ASPECT_MAX, Math.max(TILE_ASPECT_MIN, ratio))));
606
+ if (ratio && (ratio > TILE_ASPECT_MAX || ratio < TILE_ASPECT_MIN)) tile.dataset.fit = "contain";
607
+ tile.dataset.state = "loaded";
608
+ updateEdges();
614
609
  };
615
- image.onload = reveal;
616
- image.onerror = item.kind === "image" ? fail : () => {
610
+ image.onerror = () => {
611
+ if (source !== item.url) {
612
+ source = item.url;
613
+ image.src = source;
614
+ return;
615
+ }
616
+ image.onload = null;
617
+ image.onerror = null;
617
618
  image.remove();
618
- reveal();
619
- };
620
- entry.load = () => {
621
- image.src = previewUrl;
619
+ if (item.structured !== true) {
620
+ open.onclick = null;
621
+ open.onfocus = null;
622
+ tile.remove();
623
+ rail.dataset.count = String(Math.min(track.children.length, 3));
624
+ if (track.children.length === 0) {
625
+ rail.remove();
626
+ if (!root.querySelector(".lcx-media-videos")) root.hidden = true;
627
+ }
628
+ updateEdges();
629
+ return;
630
+ }
631
+ tile.dataset.state = "failed";
632
+ open.disabled = true;
633
+ open.onclick = null;
634
+ open.setAttribute("aria-label", `${labels.failed} \xB7 ${description}`);
635
+ open.title = labels.failed;
636
+ const fail = doc.createElement("span");
637
+ fail.className = "lcx-media-fail";
638
+ fail.innerHTML = ICON_BROKEN;
639
+ open.append(fail);
622
640
  };
623
- }
624
- if (item.kind === "video") {
625
- button.dataset.video = "";
626
- const play = doc.createElement("span");
627
- play.className = "lcx-media-play-label";
628
- play.textContent = labels.video;
629
- button.append(play);
630
- const format = item.sources.map((value) => /\.(mp4|webm|ogv)$/i.exec(new URL(value).pathname)?.[1]).filter(Boolean).join(" / ");
631
- if (format) {
632
- const badge = doc.createElement("span");
633
- badge.className = "lcx-media-format";
634
- badge.textContent = format.toUpperCase();
635
- badge.setAttribute("aria-hidden", "true");
636
- button.append(badge);
641
+ image.src = source;
642
+ open.onclick = () => openImage(item, open, description);
643
+ open.onfocus = () => tile.scrollIntoView?.({ block: "nearest", inline: "nearest" });
644
+ const chip = item.sourceUrl ? doc.createElement("a") : doc.createElement("span");
645
+ chip.className = "lcx-media-chip";
646
+ chip.textContent = domain;
647
+ chip.title = item.caption ? `${item.caption} \xB7 ${domain}` : domain;
648
+ if (item.sourceUrl) {
649
+ const anchor = chip;
650
+ anchor.href = item.sourceUrl;
651
+ anchor.target = "_blank";
652
+ anchor.rel = "noopener noreferrer";
637
653
  }
654
+ tile.append(open, chip);
655
+ track.append(tile);
656
+ cleanups.push(() => {
657
+ open.onclick = null;
658
+ open.onfocus = null;
659
+ image.onload = null;
660
+ image.onerror = null;
661
+ });
638
662
  }
639
- button.onclick = () => open(item, button, fail);
640
- preview.append(button);
641
- entries.set(preview, entry);
642
- return preview;
663
+ rail.append(track, prev, next);
664
+ root.append(rail);
643
665
  }
644
- for (const item of structured) {
645
- const preview = createPreview(item, item.caption ?? new URL(item.url).hostname);
646
- if (!preview) continue;
647
- let strip = strips.get(answer);
648
- if (!strip) {
649
- strip = doc.createElement("span");
650
- strip.className = "lcx-media-strip";
651
- answer.append(strip);
652
- strips.set(answer, strip);
666
+ if (videos.length) {
667
+ const section = doc.createElement("div");
668
+ section.className = "lcx-media-videos";
669
+ for (const item of videos) {
670
+ const name = fileNameOf(item.url);
671
+ const format = item.sources.map((value) => /\.(mp4|webm|ogv)$/i.exec(new URL(value).pathname)?.[1]).filter(Boolean).join(" / ").toUpperCase();
672
+ const card = doc.createElement("button");
673
+ card.type = "button";
674
+ card.className = "lcx-media-video";
675
+ card.dataset.url = item.url;
676
+ card.setAttribute("aria-label", `${labels.video} \xB7 ${name}`);
677
+ const play = doc.createElement("span");
678
+ play.className = "lcx-media-play";
679
+ play.setAttribute("aria-hidden", "true");
680
+ play.innerHTML = ICON_PLAY;
681
+ const text = doc.createElement("span");
682
+ text.className = "lcx-media-video-text";
683
+ const title = doc.createElement("span");
684
+ title.className = "lcx-media-video-title";
685
+ title.textContent = name;
686
+ title.title = item.url;
687
+ const sub = doc.createElement("span");
688
+ sub.className = "lcx-media-video-sub";
689
+ sub.textContent = [hostOf(item.url), format].filter(Boolean).join(" \xB7 ");
690
+ text.append(title, sub);
691
+ card.append(play, text);
692
+ let collapse;
693
+ card.onclick = () => {
694
+ const player = doc.createElement("div");
695
+ player.className = "lcx-media-player";
696
+ player.setAttribute("role", "group");
697
+ player.setAttribute("aria-label", `${labels.dialog} \xB7 ${name}`);
698
+ const video = doc.createElement("video");
699
+ video.controls = true;
700
+ video.playsInline = true;
701
+ video.preload = "none";
702
+ const poster = item.previewUrl ?? item.poster;
703
+ if (poster) video.poster = poster;
704
+ const close = doc.createElement("button");
705
+ close.type = "button";
706
+ close.className = "lcx-media-player-close";
707
+ close.innerHTML = ICON_CLOSE;
708
+ close.setAttribute("aria-label", labels.close);
709
+ let sourceIndex = 0;
710
+ let closed = false;
711
+ video.onerror = () => {
712
+ if (closed) return;
713
+ if (sourceIndex + 1 < item.sources.length) {
714
+ sourceIndex += 1;
715
+ video.src = item.sources[sourceIndex];
716
+ video.load();
717
+ void video.play().catch(() => {
718
+ });
719
+ return;
720
+ }
721
+ video.onerror = null;
722
+ video.pause();
723
+ video.removeAttribute("src");
724
+ video.load();
725
+ video.remove();
726
+ const fail = doc.createElement("div");
727
+ fail.className = "lcx-media-player-fail";
728
+ const message = doc.createElement("span");
729
+ message.textContent = labels.videoFailed;
730
+ const link = doc.createElement("a");
731
+ link.href = item.url;
732
+ link.target = "_blank";
733
+ link.rel = "noopener noreferrer";
734
+ link.textContent = labels.source;
735
+ fail.append(message, link);
736
+ player.append(fail);
737
+ };
738
+ collapse = (refocus = true) => {
739
+ if (closed) return;
740
+ closed = true;
741
+ video.onerror = null;
742
+ close.onclick = null;
743
+ player.onkeydown = null;
744
+ video.pause();
745
+ video.removeAttribute("src");
746
+ video.load();
747
+ player.replaceWith(card);
748
+ collapse = void 0;
749
+ if (refocus && card.isConnected) card.focus({ preventScroll: true });
750
+ };
751
+ close.onclick = () => collapse?.();
752
+ player.onkeydown = (event) => {
753
+ if (event.key === "Escape") {
754
+ event.preventDefault();
755
+ collapse?.();
756
+ }
757
+ };
758
+ video.src = item.url;
759
+ player.append(video, close);
760
+ card.replaceWith(player);
761
+ video.focus({ preventScroll: true });
762
+ void video.play().catch(() => {
763
+ });
764
+ };
765
+ section.append(card);
766
+ cleanups.push(() => {
767
+ collapse?.(false);
768
+ card.onclick = null;
769
+ });
653
770
  }
654
- strip.append(preview);
771
+ root.append(section);
655
772
  }
656
- const canPreview = (anchor) => !anchor.closest("pre,code,[data-streaming],[data-turn-process-inline],.lcx-inline-media");
657
- function scan() {
773
+ answer.append(root);
774
+ const keepAttached = () => {
658
775
  frame = 0;
659
776
  if (disposed) return;
660
- const candidates = /* @__PURE__ */ new Map();
661
- const selected = /* @__PURE__ */ new Set();
662
- for (const anchor of answer.querySelectorAll("a[href]")) {
663
- if (!canPreview(anchor)) continue;
664
- let url;
665
- try {
666
- url = new URL(anchor.href);
667
- } catch {
668
- continue;
669
- }
670
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) continue;
671
- const baseItem = wanted.get(url.href.replace(/#.*$/, ""));
672
- const item = wanted.get(url.href) ?? (baseItem?.kind === "image" ? baseItem : void 0);
673
- if (!item) continue;
674
- const key = mediaKey(item);
675
- if (selected.has(key) || failed.has(key)) continue;
676
- selected.add(key);
677
- candidates.set(anchor, { item, url });
678
- }
679
- for (const [anchor, current] of previews) {
680
- if (!candidates.has(anchor) || !current.preview.isConnected || current.href !== anchor.href) {
681
- removePreview(current.preview);
682
- previews.delete(anchor);
683
- }
684
- }
685
- for (const [anchor, { item, url }] of candidates) {
686
- if (previews.has(anchor)) continue;
687
- const preview = createPreview(
688
- item,
689
- anchor.textContent?.trim() || url.hostname,
690
- () => previews.delete(anchor)
691
- );
692
- if (!preview) continue;
693
- placePreview(anchor, preview);
694
- previews.set(anchor, { preview, href: anchor.href });
777
+ if (answer.dataset.lcxMediaOwner !== token) {
778
+ root.remove();
779
+ return;
695
780
  }
696
- updateVisibility();
697
- }
698
- scan();
781
+ if (root.parentElement !== answer || answer.lastElementChild !== root) answer.append(root);
782
+ };
699
783
  const observer = new MutationObserver(() => {
700
- if (!disposed && !frame) frame = requestAnimationFrame(scan);
701
- });
702
- observer.observe(answer, {
703
- attributes: true,
704
- attributeFilter: ["data-streaming", "data-turn-process-inline", "href"],
705
- childList: true,
706
- subtree: true
784
+ if (!disposed && !frame) frame = requestAnimationFrame(keepAttached);
707
785
  });
786
+ observer.observe(answer, { childList: true });
708
787
  return () => {
709
788
  if (disposed) return;
710
- disposed = true;
711
789
  observer.disconnect();
712
790
  if (frame) {
713
791
  cancelAnimationFrame(frame);
714
792
  frame = 0;
715
793
  }
716
794
  ownedClose?.();
717
- toggle.onclick = null;
718
- toggleRow.remove();
719
- for (const preview of [...entries.keys()]) removePreview(preview);
720
- previews.clear();
721
- entries.clear();
722
- for (const strip of strips.values()) strip.remove();
723
- strips.clear();
795
+ for (const cleanup of cleanups) cleanup();
796
+ cleanups.length = 0;
797
+ disposed = true;
798
+ if (answer.dataset.lcxMediaOwner === token) delete answer.dataset.lcxMediaOwner;
799
+ root.remove();
724
800
  };
725
801
  }
726
802
 
727
803
  // src/search-accounting.ts
728
804
  var object = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
729
805
  var count = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
730
- var zeroBuckets = () => ({ uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 });
731
806
  var isSearchTool = (name) => name === "web_search" || name === "websearch_gpt_advanced";
732
807
  function auxiliaryUsageOf(event, toolName) {
733
808
  if (!object(event) || event.type !== "tool/result" || !object(event.data?.meta)) return [];
@@ -744,38 +819,6 @@
744
819
  }
745
820
  return result;
746
821
  }
747
- function addUsage(base, records) {
748
- if (!records.length) return base;
749
- const next = { ...base };
750
- for (const { usage: u } of records) {
751
- next.uncachedInputTokens += u.inputTokens;
752
- next.outputTokens += u.outputTokens;
753
- next.cacheReadTokens += u.cacheReadTokens;
754
- next.cacheWriteTokens += u.cacheWriteTokens;
755
- }
756
- if (!Object.values(next).every(count)) throw new Error("LCX search usage exceeds safe counters");
757
- return next;
758
- }
759
- function mergeBuckets(base, extra) {
760
- if (!object(base) || Object.values(extra).every((n) => n === 0)) return base;
761
- const next = { ...base };
762
- for (const key of Object.keys(extra)) {
763
- if (count(base[key])) next[key] = base[key] + extra[key];
764
- }
765
- return next;
766
- }
767
- function addTurnUsage(base, records) {
768
- if (!object(base) || !records.length || !count(base.totalTokens)) return base;
769
- const next = mergeBuckets(base, addUsage(zeroBuckets(), records));
770
- next.totalTokens = base.totalTokens + records.reduce((n, r) => n + r.usage.totalTokens, 0);
771
- delete next.reasoningTokens;
772
- if (Array.isArray(base.routes)) {
773
- const routes = /* @__PURE__ */ new Map();
774
- for (const r of [...base.routes, ...records]) routes.set(`${r.provider}\0${r.model}`, { provider: r.provider, model: r.model });
775
- next.routes = [...routes.values()];
776
- }
777
- return next;
778
- }
779
822
 
780
823
  // src/client/search-usage-ui.ts
781
824
  var searchUsageDefinition = {
@@ -803,64 +846,70 @@
803
846
  return scope === "turn" && s?.complete ? { kind: "turn", turn: s.turn, key: "lcx-search-usage", value: s.records } : null;
804
847
  }
805
848
  };
849
+ function tokenCount(value) {
850
+ return String(value);
851
+ }
852
+ function recordsTotal(records) {
853
+ let total = 0;
854
+ for (const record2 of records) {
855
+ const value = record2.usage.totalTokens;
856
+ if (!Number.isSafeInteger(value) || value < 0) continue;
857
+ total += value;
858
+ if (!Number.isSafeInteger(total)) return 0;
859
+ }
860
+ return total;
861
+ }
862
+ function bucketsTotal(value) {
863
+ if (!object(value)) return 0;
864
+ let total = 0;
865
+ for (const key of ["uncachedInputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"]) {
866
+ const count2 = value[key];
867
+ if (!Number.isSafeInteger(count2) || count2 < 0) return 0;
868
+ total += count2;
869
+ if (!Number.isSafeInteger(total)) return 0;
870
+ }
871
+ return total;
872
+ }
873
+ function usageView(createElement, scope, total, t) {
874
+ if (total <= 0) return null;
875
+ const label = t?.(scope === "turn" ? "usageTurn" : "usageSession") ?? "Search usage";
876
+ return createElement("span", {
877
+ className: `lcx-search-usage lcx-search-usage-${scope}`,
878
+ "data-lcx-search-usage": scope,
879
+ title: `${label}: ${tokenCount(total)}`
880
+ }, `${label}: ${tokenCount(total)}`);
881
+ }
882
+ function TurnTailUsage({ turn, t }, createElement) {
883
+ if (!createElement) return null;
884
+ return usageView(createElement, "turn", recordsTotal(turn.data.get("lcx-search-usage") ?? []), t);
885
+ }
886
+ function SessionUsage({ useProjection, t }, createElement) {
887
+ if (!createElement) return null;
888
+ const extra = useProjection("lcxSearchUsage");
889
+ const auxiliary = object(extra) ? extra.auxiliary : void 0;
890
+ return usageView(createElement, "session", bucketsTotal(auxiliary), t);
891
+ }
806
892
  function installUsageSlots(slots, createElement) {
893
+ const registrations = [
894
+ [
895
+ { name: "conversation.chat.turnTail", id: "lcx-search-usage-turn", order: 0, locale: "lcx-codex" },
896
+ (props) => TurnTailUsage(props, createElement)
897
+ ],
898
+ [
899
+ { name: "conversation.composer.dock", id: "lcx-search-usage-session", order: 10, locale: "lcx-codex" },
900
+ (props) => SessionUsage(props, createElement)
901
+ ]
902
+ ];
807
903
  const cleanups = [];
808
- for (const [name, key] of [
809
- ["conversation.composer.dock", "stats"],
810
- ["conversation.composer.bar", ""],
811
- ["conversation.chat.node", "turn-tail"]
812
- ]) {
813
- let effect;
904
+ for (const [name, component] of registrations) {
814
905
  try {
815
- effect = slots.inject(name, () => {
816
- let entries;
817
- try {
818
- entries = slots.entriesOfSlot(name);
819
- } catch {
820
- return;
821
- }
822
- if (!Array.isArray(entries) || entries.length === 0) return;
823
- const candidates = key === "" ? [entries[0]] : entries.filter((e) => e?.options?.id === key || e?.options?.key === key);
824
- if (candidates.length !== 1) return;
825
- const base = candidates[0], original = base?.component;
826
- if (!base || typeof original !== "function" || !base.options || typeof base.options !== "object") return;
827
- const descriptor = Object.getOwnPropertyDescriptor(base, "component");
828
- if (descriptor && descriptor.set === void 0 && descriptor.writable === false) return;
829
- function WithSearchUsage(props) {
830
- if (key === "turn-tail") {
831
- const location = props?.node?.location;
832
- const records = (location?.kind === "turn" || location?.kind === "step") && location.turn?.data?.get ? location.turn.data.get("lcx-search-usage") : void 0;
833
- if (!records?.length) return createElement(original, props);
834
- return createElement(original, { ...props, node: { ...props.node, data: { ...props.node.data, tokenUsage: addTurnUsage(props.node.data.tokenUsage, records) } } });
835
- }
836
- if (typeof props?.useProjection !== "function") return createElement(original, props);
837
- const extra = props.useProjection("lcxSearchUsage");
838
- const breakdown = props.useProjection("contextBreakdown");
839
- const useProjection = (projection) => {
840
- const value = props.useProjection(projection);
841
- if (projection === "tokenUsage" && extra?.auxiliary) return mergeBuckets(value, extra.auxiliary);
842
- if (projection === "contextPressure" && extra?.aggregateContext && object(breakdown)) {
843
- const tokens = breakdown.systemTokens + breakdown.toolsTokens + breakdown.messageTokens;
844
- if (Number.isSafeInteger(tokens) && tokens >= 0 && object(value)) return { ...value, pressureTokens: tokens, projectedTokens: tokens };
845
- }
846
- return value;
847
- };
848
- return createElement(original, { ...props, useProjection });
849
- }
850
- try {
851
- base.component = WithSearchUsage;
852
- } catch {
853
- return;
854
- }
855
- if (base.component !== WithSearchUsage) return;
856
- return () => {
857
- if (base.component === WithSearchUsage) base.component = original;
858
- };
906
+ const effect = slots.inject(name.name, () => {
907
+ const dispose = slots.register(name, component);
908
+ return typeof dispose === "function" ? dispose : void 0;
859
909
  });
910
+ if (typeof effect === "function") cleanups.push(effect);
860
911
  } catch {
861
- continue;
862
912
  }
863
- if (typeof effect === "function") cleanups.push(effect);
864
913
  }
865
914
  return () => {
866
915
  for (const dispose of cleanups.reverse()) dispose();
@@ -894,7 +943,7 @@
894
943
  const DEFAULTS = Object.fromEntries(
895
944
  FIELDS.map((field) => [field, false])
896
945
  );
897
- const css = `.lcx-card{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;list-style:none}.lcx-head{width:100%;display:flex;justify-content:space-between;padding:14px 16px;border:0;background:transparent;color:inherit}.lcx-body{border-top:1px solid var(--dsw-alias-border-l2);padding:12px 16px}.lcx-row{display:flex;gap:9px;padding:8px 0}.lcx-row small,.lcx-help{display:block;font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary)}.lcx-group{border-top:1px solid var(--dsw-alias-border-l2);margin-top:10px;padding-top:14px}.lcx-group strong{font-size:14px}.lcx-foot{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}.lcx-foot button{padding:6px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:inherit}` + inlineMediaCss;
946
+ const css = `.lcx-card{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;list-style:none}.lcx-head{width:100%;display:flex;justify-content:space-between;padding:14px 16px;border:0;background:transparent;color:inherit}.lcx-body{border-top:1px solid var(--dsw-alias-border-l2);padding:12px 16px}.lcx-row{display:flex;gap:9px;padding:8px 0}.lcx-row small,.lcx-help{display:block;font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary)}.lcx-group{border-top:1px solid var(--dsw-alias-border-l2);margin-top:10px;padding-top:14px}.lcx-group strong{font-size:14px}.lcx-foot{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}.lcx-foot button{padding:6px 12px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:inherit}.lcx-search-usage{display:inline-flex;align-items:center;gap:4px;margin:2px 0;color:var(--dsw-alias-label-tertiary);font:12px/18px system-ui}.lcx-search-usage-session{padding:0 4px}.lcx-search-usage-turn{padding:0 6px}` + inlineMediaCss + nativeMarkdownImageCss;
898
947
  function mountCss() {
899
948
  if (typeof document === "undefined") return () => {
900
949
  };
@@ -910,6 +959,7 @@
910
959
  const copy = {
911
960
  zh: {
912
961
  title: "Responses / Codex \u80FD\u529B",
962
+ summary: "GPT Responses route \u4E0E\u641C\u7D22\u80FD\u529B",
913
963
  desc: "LCX \u8DDF\u968F DSH \u5F53\u524D\u4F1A\u8BDD\u9009\u62E9\u7684 GPT Responses route\uFF1B\u6A21\u578B\u3001endpoint \u548C credential \u7EE7\u7EED\u53EA\u7531 DSH \u7BA1\u7406\u3002",
914
964
  enabled: "\u542F\u7528 LCX\uFF08\u63A5\u7BA1\u5F53\u524D GPT Responses \u4F1A\u8BDD\uFF09",
915
965
  enabledHelp: "\u6B64\u5F00\u5173\u4EC5\u63A5\u7BA1 GPT Responses \u4F1A\u8BDD\uFF1BGrok \u539F\u751F\u641C\u7D22\u7531\u4E0B\u65B9\u72EC\u7ACB\u5F00\u5173\u63A7\u5236\uFF0C\u5176\u4ED6\u975E GPT \u6A21\u578B\u7EE7\u7EED\u4F7F\u7528 DSH \u539F\u751F\u8DEF\u5F84\u3002",
@@ -920,25 +970,20 @@
920
970
  alpha: "\u542F\u7528 Alpha command\uFF08websearch_alpha\uFF09",
921
971
  alphaHelp: "\u4EC5 capability probe \u5BF9\u5F53\u524D route/schema \u9A8C\u8BC1\u901A\u8FC7\u540E\u624D\u771F\u6B63\u6CE8\u518C\u3002",
922
972
  mediaPreview: "\u641C\u7D22\u5A92\u4F53\u9884\u89C8",
923
- mediaPreviewHelp: "\u5728\u56DE\u7B54\u4E0B\u663E\u793A\u53EF\u7528\u7684\u641C\u7D22\u56FE\u7247\u6216\u76F4\u94FE\u9884\u89C8\uFF0C\u70B9\u51FB\u653E\u5927\u6216\u64AD\u653E\uFF1B\u4E0D\u53EF\u9884\u89C8\u65F6\u4FDD\u7559\u539F\u59CB\u56DE\u7B54\u4E0E\u7F51\u9875\u94FE\u63A5\u3002\u6B64\u8BBE\u7F6E\u53EA\u6539\u53D8\u754C\u9762\u663E\u793A\u3002",
924
- mediaTitle: "\u5A92\u4F53\u9884\u89C8",
973
+ mediaPreviewHelp: "\u5728\u56DE\u7B54\u4E0B\u65B9\u663E\u793A\u641C\u7D22\u8FD4\u56DE\u7684\u56FE\u7247\u3001\u56DE\u7B54\u91CC\u7684\u56FE\u7247\u76F4\u94FE\uFF0C\u4EE5\u53CA\u53EF\u76F4\u63A5\u64AD\u653E\u7684\u89C6\u9891\u76F4\u94FE\uFF1B\u70B9\u51FB\u653E\u5927\u6216\u64AD\u653E\u3002\u56DE\u7B54\u6B63\u6587\u91CC\u7684 Markdown \u56FE\u7247\u7531 DSH \u539F\u751F\u6E32\u67D3\uFF0C\u666E\u901A\u7F51\u9875\u94FE\u63A5\u4FDD\u6301\u4E3A\u94FE\u63A5\u3002\u6B64\u8BBE\u7F6E\u53EA\u6539\u53D8\u754C\u9762\u663E\u793A\u3002",
974
+ mediaTitle: "\u89C6\u9891\u9884\u89C8",
975
+ mediaImagePreview: "\u56FE\u7247\u9884\u89C8",
925
976
  mediaPlay: "\u64AD\u653E\u89C6\u9891",
926
977
  mediaEnlarge: "\u653E\u5927\u56FE\u7247",
927
978
  mediaClose: "\u5173\u95ED\u9884\u89C8",
928
- mediaMore: "\u5C55\u5F00\u5176\u4F59 {count} \u9879",
929
- mediaLess: "\u6536\u8D77\u9884\u89C8",
930
- mediaPrevious: "\u4E0A\u4E00\u5F20",
931
- mediaNext: "\u4E0B\u4E00\u5F20",
932
- mediaResolve: "\u52A0\u8F7D\u7D20\u6750\u9884\u89C8",
933
- mediaLoading: "\u6B63\u5728\u83B7\u53D6\u5A92\u4F53\u2026",
934
- mediaUnavailable: "\u6682\u4E0D\u80FD\u9884\u89C8",
935
- mediaAll: "\u5168\u90E8",
936
- mediaImages: "\u56FE\u7247",
937
- mediaVideos: "\u89C6\u9891",
938
- mediaImage: "\u56FE\u7247",
939
- mediaVideo: "\u89C6\u9891",
940
- mediaFilter: "\u5A92\u4F53\u7C7B\u578B",
979
+ mediaHeading: "\u641C\u7D22\u56FE\u7247",
980
+ mediaPrevious: "\u4E0A\u4E00\u7EC4\u56FE\u7247",
981
+ mediaNext: "\u4E0B\u4E00\u7EC4\u56FE\u7247",
982
+ mediaFailed: "\u56FE\u7247\u52A0\u8F7D\u5931\u8D25",
983
+ mediaVideoFailed: "\u89C6\u9891\u65E0\u6CD5\u64AD\u653E",
941
984
  mediaOpen: "\u6253\u5F00\u539F\u59CB\u5A92\u4F53",
985
+ usageTurn: "\u672C\u8F6E\u641C\u7D22\u7528\u91CF",
986
+ usageSession: "\u641C\u7D22\u7528\u91CF",
942
987
  grokTitle: "Grok \u539F\u751F\u641C\u7D22",
943
988
  grokDesc: "\u4F7F\u7528 DSH \u5F53\u524D\u9009\u62E9\u7684 Grok \u6A21\u578B\u53CA\u5176\u670D\u52A1\u914D\u7F6E\uFF1B\u4E0E GPT \u529F\u80FD\u5F00\u5173\u72EC\u7ACB\u3002\u5F00\u542F\u4EFB\u4E00\u641C\u7D22\u540E\uFF0CGrok \u4EC5\u4F7F\u7528\u539F\u751F\u641C\u7D22\uFF0C\u7F51\u9875\u8BFB\u53D6\u548C\u5176\u4ED6\u5DE5\u5177\u4ECD\u53EF\u7528\u3002",
944
989
  grokWeb: "\u542F\u7528\u539F\u751F Web Search",
@@ -952,6 +997,7 @@
952
997
  },
953
998
  en: {
954
999
  title: "Responses / Codex capabilities",
1000
+ summary: "GPT Responses route and search capabilities",
955
1001
  desc: "LCX follows the GPT Responses route selected by the current DSH session. Model, endpoint and credentials remain DSH-owned.",
956
1002
  enabled: "Enable LCX (own current GPT Responses conversation)",
957
1003
  enabledHelp: "Applies only to GPT Responses conversations. Grok native search is controlled independently below; other non-GPT models continue through DSH normally.",
@@ -962,25 +1008,20 @@
962
1008
  alpha: "Enable Alpha command (websearch_alpha)",
963
1009
  alphaHelp: "Registered only after a matching capability probe.",
964
1010
  mediaPreview: "Search media previews",
965
- mediaPreviewHelp: "Preview available search images or direct media links below the answer. Unavailable media leaves the original answer and links intact. This setting changes presentation only.",
966
- mediaTitle: "Media previews",
1011
+ mediaPreviewHelp: "Show images returned by search, direct image links in the answer and directly playable video links below the answer; click to enlarge or play. Markdown images in the answer are rendered natively by DSH, and ordinary page links stay links. This setting changes presentation only.",
1012
+ mediaTitle: "Video preview",
1013
+ mediaImagePreview: "Image preview",
967
1014
  mediaPlay: "Play video",
968
1015
  mediaEnlarge: "Enlarge image",
969
1016
  mediaClose: "Close preview",
970
- mediaMore: "Show {count} more",
971
- mediaLess: "Show fewer",
972
- mediaPrevious: "Previous image",
973
- mediaNext: "Next image",
974
- mediaResolve: "Load media preview",
975
- mediaLoading: "Resolving media\u2026",
976
- mediaUnavailable: "Preview unavailable",
977
- mediaAll: "All",
978
- mediaImages: "Photos",
979
- mediaVideos: "Videos",
980
- mediaImage: "Photo",
981
- mediaVideo: "Video",
982
- mediaFilter: "Media type",
1017
+ mediaHeading: "Search images",
1018
+ mediaPrevious: "Previous images",
1019
+ mediaNext: "Next images",
1020
+ mediaFailed: "Image failed to load",
1021
+ mediaVideoFailed: "Video cannot be played",
983
1022
  mediaOpen: "Open original media",
1023
+ usageTurn: "Turn search usage",
1024
+ usageSession: "Search usage",
984
1025
  grokTitle: "Grok native search",
985
1026
  grokDesc: "Uses the Grok model and provider profile currently selected in DSH, independently of the GPT switch. When either search is enabled, Grok uses native search while page reading and other tools remain available.",
986
1027
  grokWeb: "Enable native Web Search",
@@ -1067,7 +1108,12 @@
1067
1108
  ...context.state,
1068
1109
  anchorSeq: match.event.seq,
1069
1110
  location: match.location,
1070
- items: context.state.structuredItems.length ? context.state.structuredItems : extractSearchMedia(text),
1111
+ // Ownership (Issue #102): structured provider candidates + direct image/video file links
1112
+ // from the answer text. Markdown images belong to DSH and are never duplicated.
1113
+ items: mergeSearchMedia(
1114
+ context.state.structuredItems,
1115
+ extractDirectMediaLinks(text)
1116
+ ),
1071
1117
  provider: source.provider,
1072
1118
  model: source.model,
1073
1119
  ready: true
@@ -1102,12 +1148,14 @@
1102
1148
  saveError;
1103
1149
  store;
1104
1150
  stop;
1151
+ saveEpoch;
1105
1152
  constructor(scope) {
1106
1153
  this.scope = scope;
1107
1154
  this.draft = null;
1108
1155
  this.dirty = false;
1109
1156
  this.saving = false;
1110
1157
  this.saveError = false;
1158
+ this.saveEpoch = 0;
1111
1159
  this.store = createSnapshotStore(this.projection());
1112
1160
  mediaStore.set({ enabled: this.value().searchMediaPreview });
1113
1161
  this.stop = scope.subscribe(() => {
@@ -1159,10 +1207,19 @@
1159
1207
  this.saveError = false;
1160
1208
  this.publish();
1161
1209
  }
1210
+ leavePage() {
1211
+ this.saveEpoch += 1;
1212
+ this.draft = null;
1213
+ this.dirty = false;
1214
+ this.saving = false;
1215
+ this.saveError = false;
1216
+ this.publish();
1217
+ }
1162
1218
  async save() {
1163
1219
  if (!this.dirty || this.saving || !this.snapshot().writable || this.snapshot().status !== "ready") return;
1164
1220
  const next = this.draftValue(), prev = this.value();
1165
1221
  const fields = FIELDS.filter((field) => next[field] !== prev[field]);
1222
+ const saveEpoch = ++this.saveEpoch;
1166
1223
  this.saving = true;
1167
1224
  this.saveError = false;
1168
1225
  this.publish();
@@ -1173,16 +1230,20 @@
1173
1230
  path: [field],
1174
1231
  value: next[field]
1175
1232
  })));
1233
+ if (saveEpoch !== this.saveEpoch) return;
1176
1234
  if (this.snapshot().status !== "ready" || fields.some((field) => this.value()[field] !== next[field]))
1177
1235
  throw new Error("Settings mutation was not confirmed");
1178
1236
  }
1237
+ if (saveEpoch !== this.saveEpoch) return;
1179
1238
  this.draft = null;
1180
1239
  this.dirty = false;
1181
1240
  } catch {
1182
- this.saveError = true;
1241
+ if (saveEpoch === this.saveEpoch) this.saveError = true;
1183
1242
  } finally {
1184
- this.saving = false;
1185
- this.publish();
1243
+ if (saveEpoch === this.saveEpoch) {
1244
+ this.saving = false;
1245
+ this.publish();
1246
+ }
1186
1247
  }
1187
1248
  }
1188
1249
  inject() {
@@ -1194,7 +1255,8 @@
1194
1255
  setMediaPreview: (value) => this.edit("searchMediaPreview", value),
1195
1256
  edit: (field, value) => this.edit(field, value),
1196
1257
  save: () => void this.save(),
1197
- discard: () => this.discard()
1258
+ discard: () => this.discard(),
1259
+ leavePage: () => this.leavePage()
1198
1260
  };
1199
1261
  }
1200
1262
  }
@@ -1226,7 +1288,13 @@
1226
1288
  }
1227
1289
  function Card(props) {
1228
1290
  const t = props.t, s = props.useLcxCard((state) => state), [open, setOpen] = React.useState(false);
1291
+ React.useEffect(
1292
+ () => props.view === "page" ? () => props.leavePage() : void 0,
1293
+ [props.view, props.leavePage]
1294
+ );
1229
1295
  if (!s.available) return null;
1296
+ if (props.view === "summary")
1297
+ return React.createElement("span", { className: "lcx-summary" }, t("summary"));
1230
1298
  const disabled = !s.writable || s.saving;
1231
1299
  return React.createElement(
1232
1300
  "li",
@@ -1329,11 +1397,37 @@
1329
1397
  ) : null
1330
1398
  );
1331
1399
  }
1400
+ const lightboxHost = {
1401
+ openImage({ src, alt, labels, onClose }) {
1402
+ try {
1403
+ const primitives = require2("@deepseek-ai/dsh-client-ui-primitives");
1404
+ const reactDom = require2("react-dom/client");
1405
+ if (typeof primitives?.ImageLightbox !== "function" || typeof reactDom?.createRoot !== "function") return null;
1406
+ const container = document.createElement("div");
1407
+ container.dataset.lcxMediaLightbox = "";
1408
+ document.body.appendChild(container);
1409
+ const root = reactDom.createRoot(container);
1410
+ root.render(React.createElement(primitives.ImageLightbox, { src, alt, labels, onClose }));
1411
+ let done = false;
1412
+ return () => {
1413
+ if (done) return;
1414
+ done = true;
1415
+ queueMicrotask(() => {
1416
+ root.unmount();
1417
+ container.remove();
1418
+ });
1419
+ };
1420
+ } catch {
1421
+ return null;
1422
+ }
1423
+ }
1424
+ };
1332
1425
  function InlineMedia({ node, t }) {
1333
1426
  const [marker, setMarker] = React.useState(null);
1334
1427
  React.useEffect(() => {
1335
1428
  if (!marker) return;
1336
- return installInlineMedia(marker, node.data.items, { image: t("mediaEnlarge"), video: t("mediaPlay"), close: t("mediaClose"), source: t("mediaOpen"), more: t("mediaMore"), less: t("mediaLess"), previous: t("mediaPrevious"), next: t("mediaNext") });
1429
+ const labels = { image: t("mediaEnlarge"), video: t("mediaPlay"), close: t("mediaClose"), source: t("mediaOpen"), failed: t("mediaFailed"), videoFailed: t("mediaVideoFailed"), heading: t("mediaHeading"), previous: t("mediaPrevious"), next: t("mediaNext"), dialog: t("mediaTitle"), imageDialog: t("mediaImagePreview") };
1430
+ return installInlineMedia(marker, node.data.items, labels, lightboxHost);
1337
1431
  }, [marker, node.data.items, t]);
1338
1432
  return React.createElement("span", { ref: setMarker, "aria-hidden": true });
1339
1433
  }
@@ -1341,14 +1435,12 @@
1341
1435
  const enabled = props.useMediaPreview((state) => state?.enabled === true);
1342
1436
  return enabled ? React.createElement(InlineMedia, { node: props.node, t: props.t }) : null;
1343
1437
  }
1344
- const inject = ["slots", "locale", "settingsScope", "uiConversation"];
1438
+ const inject = ["slots", "locale", "configForms", "uiConversation"];
1345
1439
  function apply(ctx) {
1346
- const slots = ctx.slots ?? ctx.get("slots"), svc = ctx.settingsScope ?? ctx.get("settingsScope"), locale = ctx.locale ?? ctx.get("locale"), uiConversation = ctx.uiConversation ?? ctx.get("uiConversation");
1440
+ const slots = ctx.slots ?? ctx.get("slots"), svc = ctx.configForms ?? ctx.get("configForms"), locale = ctx.locale ?? ctx.get("locale"), uiConversation = ctx.uiConversation ?? ctx.get("uiConversation");
1347
1441
  if (!slots || !svc || !locale || !uiConversation) return;
1348
- if (typeof slots.entriesOfSlot === "function") {
1349
- ctx.effect(() => uiConversation.events.register(searchUsageDefinition), "lcx search billing data");
1350
- ctx.effect(() => installUsageSlots(slots, React.createElement), "lcx search billing slots");
1351
- }
1442
+ ctx.effect(() => uiConversation.events.register(searchUsageDefinition), "lcx search billing data");
1443
+ ctx.effect(() => installUsageSlots(slots, React.createElement), "lcx search billing slots");
1352
1444
  ctx.effect(mountCss, "lcx-codex styles");
1353
1445
  for (const language of ["zh", "en"])
1354
1446
  ctx.effect(() => locale.register(NAMESPACE, language, copy[language]), `lcx-codex ${language} dictionary`);
@@ -1362,14 +1454,14 @@
1362
1454
  if (typeof cleanup === "function") cleanup();
1363
1455
  };
1364
1456
  }, label);
1365
- const controller = new Controller(svc.bind({ namespace: NAMESPACE }));
1457
+ const controller = new Controller(svc.get(NAMESPACE));
1366
1458
  installSlot(
1367
- "settings.plugin.item",
1368
- "lcx-codex settings slot",
1459
+ "plugins.bundle.config",
1460
+ "lcx-codex plugin configuration",
1369
1461
  () => slots.register(
1370
1462
  {
1371
- name: "settings.plugin.item",
1372
- key: NAMESPACE,
1463
+ name: "plugins.bundle.config",
1464
+ key: "dsh-lcx-codex",
1373
1465
  locale: NAMESPACE,
1374
1466
  inject: () => controller.inject()
1375
1467
  },