dsh-lcx-codex 0.4.2 → 0.4.3-pre.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +75 -224
  2. package/THIRD_PARTY_NOTICES.md +64 -0
  3. package/cordis.patch.yml +3 -20
  4. package/lib/auxiliary-usage.js +63 -0
  5. package/lib/client.js +1398 -167
  6. package/lib/compact-v2.js +218 -199
  7. package/lib/dsh-compat.js +294 -100
  8. package/lib/dsh-responses.js +512 -277
  9. package/lib/grok-native-search.js +391 -0
  10. package/lib/index.js +1066 -758
  11. package/lib/invocation-policy-scope.js +261 -0
  12. package/lib/json-store.js +57 -31
  13. package/lib/native-checkpoint.js +520 -194
  14. package/lib/pi-responses-runtime.js +1571 -0
  15. package/lib/responses-request.js +109 -121
  16. package/lib/responses-stream.js +1280 -447
  17. package/lib/route.js +425 -369
  18. package/lib/search-accounting.js +86 -0
  19. package/lib/search-usage.js +86 -0
  20. package/lib/service-mutex.js +73 -64
  21. package/lib/token-budget.js +176 -108
  22. package/lib/transport.js +308 -68
  23. package/lib/types/client/index.d.ts +18 -0
  24. package/lib/types/client/search-media.d.ts +16 -0
  25. package/lib/types/index.d.ts +83 -0
  26. package/lib/web-run-output.js +189 -18
  27. package/lib/web-search-alpha.js +1067 -163
  28. package/lib/web-search-capability.js +80 -65
  29. package/lib/web-search-hosted.js +321 -33
  30. package/lib/web-search-ref-store.js +145 -60
  31. package/package.json +112 -32
  32. package/ARCHITECTURE.md +0 -117
  33. package/CHANGELOG.md +0 -224
  34. package/README_EN.md +0 -277
  35. package/assets/dsh-lcx-codex-banner.jpg +0 -0
  36. package/lib/legacy-v3.js +0 -20
  37. package/lib/responses-replay.js +0 -68
  38. package/scripts/probe-alpha.mjs +0 -43
  39. package/scripts/validate-dsh-schema.mjs +0 -31
package/lib/client.js CHANGED
@@ -1,168 +1,1399 @@
1
- window.__ModuleLoader__.load({
2
- id: 'dsh-lcx-codex',
3
- factory: (require) => {
4
- const module = { exports: {} }
5
- const exports = module.exports
6
- const React = require('react')
7
- const { createSnapshotStore } = require('@deepseek-ai/dsh-client-runtime/client')
8
- const NAMESPACE = 'lcx-codex'
9
- const DEFAULTS = {
10
- enabled: false,
11
- webSearch: false,
12
- advancedHostedSearch: false,
13
- alphaSearch: false,
14
- remoteCompaction: false,
15
- fallbackToBasicCompaction: true,
16
- autoCompaction: true,
17
- webSearchTimeoutSeconds: 240,
18
- autoCompactionThresholdPercent: 90,
19
- emergencyPruneThresholdPercent: 95,
20
- provider: 'lcx',
21
- baseURL: 'https://api.lcxbot.com/v1',
22
- apiKeyEnv: 'LCX_API_KEY',
23
- model: 'gpt-5.6-sol',
24
- }
25
- const css = `.lcx-card{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;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,.lcx-field small{display:block;font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary)}.lcx-fields{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:8px}.lcx-field{display:flex;flex-direction:column;gap:4px}.lcx-input{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px;background:transparent;color:inherit}.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}`
26
- if (typeof document !== 'undefined' && !document.querySelector('style[data-plugin-css="dsh-lcx-codex"]')) {
27
- const tag = document.createElement('style')
28
- tag.dataset.pluginCss = 'dsh-lcx-codex'
29
- tag.textContent = css
30
- document.head.appendChild(tag)
31
- }
32
- const copy = {
33
- zh: {
34
- title: 'Responses / Codex 能力',
35
- desc: 'LCX 开启后从第一轮普通请求开始接管当前 GPT Responses 会话,并统一管理普通请求、工具、Native V2 压缩和续接。',
36
- enabled: '启用 LCX(接管当前 GPT Responses 会话)',
37
- enabledHelp: '切换 Claude、Gemini、DeepSeek 等非 GPT 模型前先关闭 LCX;模型切换本身不需要重启 DSH。',
38
- web: '使用 GPT Hosted Search 作为 DSH web_search 后端',
39
- webHelp: '不会新增第二个普通搜索工具;普通 web_search 自动跟随当前 Agent 的 GPT Responses 模型。',
40
- searchTimeout: 'web_search 超时(秒)',
41
- searchTimeoutHelp: '只调整 DSH 工具外层超时,不改变模型可见 schema。默认 240 秒;Hosted Search 较慢时避免 60 秒提前中断。',
42
- advanced: '启用高级 Hosted 工具(websearch_gpt_advanced)',
43
- advancedHelp: '只在需要域名过滤、位置、search context、图片等原生 Hosted 参数时使用;默认关闭以保持工具 schema 稳定。',
44
- alpha: '启用 Alpha command(websearch_alpha)',
45
- alphaHelp: '仅 capability probe 对当前 endpoint/provider/model/schema 验证通过后才真正注册。',
46
- autoCompact: '启用 Native-first 自动压缩',
47
- autoCompactHelp: '到主动阈值前不让 DSH 的 80% tool-result prune 改写历史;到主动阈值后优先 Native V2。',
48
- autoThreshold: 'Native 自动压缩阈值(%)',
49
- autoThresholdHelp: '建议 90%。允许 85–95%;262k 窗口在 90% 时仍约剩 26k tokens。',
50
- emergencyThreshold: 'DSH 紧急 prune 阈值(%)',
51
- emergencyThresholdHelp: '建议 95%。达到这里才允许 DSH tool-result-pruner 先救场;必须高于 Native 阈值。',
52
- fallback: 'Native 失败后回退 DSH basic compaction',
53
- fallbackHelp: 'Remote-first:只有 Native 请求失败才调用 basic summary,不并行双跑。',
54
- endpoint: 'Responses 地址',
55
- model: 'GPT 模型',
56
- save: '保存', discard: '放弃修改', saving: '保存中…',
57
- },
58
- en: {
59
- title: 'Responses / Codex capabilities',
60
- desc: 'When enabled, LCX owns the selected GPT Responses conversation from the first ordinary turn through tools, Native V2 compaction and continuation.',
61
- enabled: 'Enable LCX (own current GPT Responses conversation)',
62
- enabledHelp: 'Turn LCX off before switching to Claude, Gemini, DeepSeek, or another non-GPT model. Model switching itself does not require a DSH restart.',
63
- web: 'Use GPT Hosted Search as DSH web_search backend',
64
- webHelp: 'Keeps DSH web_search as the single ordinary search tool and follows the active Agent GPT Responses model.',
65
- searchTimeout: 'web_search timeout (seconds)',
66
- searchTimeoutHelp: 'Changes only the DSH tool deadline, not the model-visible schema. Default 240s to avoid premature 60s timeouts.',
67
- advanced: 'Enable advanced Hosted tool (websearch_gpt_advanced)',
68
- advancedHelp: 'Only for native Hosted controls such as domains, location, context size and image search; off by default for stable tool schemas.',
69
- alpha: 'Enable Alpha command (websearch_alpha)',
70
- alphaHelp: 'Registered only after a matching capability probe.',
71
- autoCompact: 'Enable Native-first automatic compaction',
72
- autoCompactHelp: 'Suppresses the stock 80% tool-result prune before the proactive threshold, then prefers Native V2.',
73
- autoThreshold: 'Native auto-compaction threshold (%)',
74
- autoThresholdHelp: 'Default 90%. Allowed 85–95%.',
75
- emergencyThreshold: 'DSH emergency prune threshold (%)',
76
- emergencyThresholdHelp: 'Default 95%. DSH tool-result pruning is allowed only in this emergency zone; must exceed the Native threshold.',
77
- fallback: 'Fall back to DSH basic compaction on Native failure',
78
- fallbackHelp: 'Remote-first: basic summary runs only after Native failure, never in parallel.',
79
- endpoint: 'Responses endpoint', model: 'GPT model',
80
- save: 'Save', discard: 'Discard', saving: 'Saving…',
81
- },
82
- }
83
- const lang = () => String(globalThis.navigator?.language ?? '').toLowerCase().startsWith('zh') ? 'zh' : 'en'
84
- function valueFrom(snapshot) { return { ...DEFAULTS, ...(snapshot.value ?? {}) } }
85
- class Controller {
86
- constructor(scope) {
87
- this.scope = scope; this.draft = null; this.dirty = false; this.saving = false
88
- this.store = createSnapshotStore(this.projection())
89
- this.stop = scope.subscribe(() => { if (!this.dirty) this.draft = null; this.publish() })
90
- }
91
- snapshot() { return this.scope.getSnapshot() }
92
- value() { return valueFrom(this.snapshot()) }
93
- draftValue() { return { ...this.value(), ...(this.draft ?? {}) } }
94
- field(name) { return this.draftValue()[name] }
95
- projection() {
96
- const s = this.snapshot()
97
- const bools = ['enabled','webSearch','advancedHostedSearch','alphaSearch','fallbackToBasicCompaction','autoCompaction']
98
- return {
99
- available: s.status === 'ready', writable: s.writable, dirty: this.dirty, saving: this.saving,
100
- ...Object.fromEntries(bools.map((f) => [f, { value: f === 'fallbackToBasicCompaction' || f === 'autoCompaction' ? this.field(f) !== false : Boolean(this.field(f)) }])),
101
- webSearchTimeoutSeconds: Number(this.field('webSearchTimeoutSeconds') ?? 240),
102
- autoCompactionThresholdPercent: Number(this.field('autoCompactionThresholdPercent') ?? 90),
103
- emergencyPruneThresholdPercent: Number(this.field('emergencyPruneThresholdPercent') ?? 95),
104
- baseURL: String(this.field('baseURL') ?? ''), model: String(this.field('model') ?? ''),
105
- }
106
- }
107
- publish() { this.store.set(this.projection()) }
108
- edit(field, value) { this.draft = { ...(this.draft ?? this.value()), [field]: value }; this.dirty = true; this.publish() }
109
- discard() { this.draft = null; this.dirty = false; this.publish() }
110
- async save() {
111
- if (!this.dirty || this.saving || !this.snapshot().writable) return
112
- const next = this.draftValue(), prev = this.value(); this.saving = true; this.publish()
1
+ "use strict";
2
+ (() => {
3
+ // src/client/search-media.ts
4
+ var SEARCH_MEDIA_LIMIT = 60;
5
+ var IMAGE_EXTENSION = /\.(?:jpe?g|png|webp|gif|avif)$/i;
6
+ var VIDEO_EXTENSION = /\.(?:mp4|webm|ogv)$/i;
7
+ var URL_TOKEN = /https?:\/\/[^\s<>"'`,。;:!?、“”‘’()【】《》]+/gi;
8
+ var REFERENCE_DEFINITION = /^ {0,3}\[([^\]\r\n]+)\]:[ \t]*(?:<([^>\s]+)>|(\S+))(?:[ \t]+.*)?$/gm;
9
+ var REFERENCE_IMAGE = /!\[([^\]\r\n]*)\](?:\s*\[([^\]\r\n]*)\])?/g;
10
+ var REFERENCE_LINK = /(^|[^!])\[([^\]\r\n]+)\]\s*\[([^\]\r\n]*)\]/g;
11
+ function stripFencedCode(text) {
12
+ const lines = text.split(/(?<=\n)/);
13
+ let fence = null;
14
+ return lines.map((line) => {
15
+ const match = line.match(/^ {0,3}(`{3,}|~{3,})([^\r\n]*)/);
16
+ if (fence === null) {
17
+ if (/^(?: {4}|\t)/.test(line)) return line.replace(/[^\r\n]/g, " ");
18
+ if (match === null) return line;
19
+ fence = { marker: match[1][0], length: match[1].length };
20
+ return line.replace(/[^\r\n]/g, " ");
21
+ }
22
+ const closes = match !== null && match[1][0] === fence.marker && match[1].length >= fence.length && match[2].trim() === "";
23
+ if (closes) fence = null;
24
+ return line.replace(/[^\r\n]/g, " ");
25
+ }).join("");
26
+ }
27
+ function stripInlineCode(text) {
28
+ let result = "";
29
+ for (let index = 0; index < text.length; ) {
30
+ if (text[index] !== "`") {
31
+ result += text[index++];
32
+ continue;
33
+ }
34
+ let end = index;
35
+ while (text[end] === "`") end += 1;
36
+ const marker = text.slice(index, end);
37
+ const close = text.indexOf(marker, end);
38
+ if (close === -1) {
39
+ result += marker;
40
+ index = end;
41
+ continue;
42
+ }
43
+ result += " ".repeat(close + marker.length - index);
44
+ index = close + marker.length;
45
+ }
46
+ return result;
47
+ }
48
+ function stripHtml(text) {
49
+ return text.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, " ").replace(/<!--[\s\S]*?-->/g, " ").replace(/<(https?:\/\/[^<>\s]+)>/gi, "$1").replace(/<[^>]*>/g, " ");
50
+ }
51
+ function destination(value) {
52
+ const trimmed = value.trim();
53
+ if (trimmed.startsWith("<")) {
54
+ const end = trimmed.indexOf(">", 1);
55
+ return end === -1 ? null : trimmed.slice(1, end);
56
+ }
57
+ const match = trimmed.match(/^(?:\\.|\S)+/);
58
+ return match?.[0]?.replace(/\\([()[\]])/g, "$1") ?? null;
59
+ }
60
+ function eraseInlineImages(text, onDestination) {
61
+ const chars = text.split("");
62
+ for (let index = 0; index < text.length - 2; index += 1) {
63
+ if (text[index] !== "!" || text[index + 1] !== "[") continue;
64
+ let labelEnd = index + 2;
65
+ for (; labelEnd < text.length; labelEnd += 1) {
66
+ if (text[labelEnd] === "\\") labelEnd += 1;
67
+ else if (text[labelEnd] === "]") break;
68
+ }
69
+ if (labelEnd >= text.length) continue;
70
+ let open = labelEnd + 1;
71
+ while (text[open] === " " || text[open] === " ") open += 1;
72
+ if (text[open] !== "(") continue;
73
+ let depth = 1;
74
+ let close = open + 1;
75
+ for (; close < text.length && depth > 0; close += 1) {
76
+ if (text[close] === "\\") close += 1;
77
+ else if (text[close] === "(") depth += 1;
78
+ else if (text[close] === ")") depth -= 1;
79
+ }
80
+ if (depth !== 0) continue;
81
+ const value = destination(text.slice(open + 1, close - 1));
82
+ if (value !== null) onDestination(value);
83
+ for (let offset = index; offset < close; offset += 1) chars[offset] = " ";
84
+ index = close - 1;
85
+ }
86
+ return chars.join("");
87
+ }
88
+ function collectInlineLinkDestinations(text, onDestination) {
89
+ const chars = text.split("");
90
+ for (let index = 0; index < text.length - 1; index += 1) {
91
+ if (text[index] !== "[" || text[index - 1] === "!") continue;
92
+ let labelEnd = index + 1;
93
+ for (; labelEnd < text.length; labelEnd += 1) {
94
+ if (text[labelEnd] === "\\") labelEnd += 1;
95
+ else if (text[labelEnd] === "]") break;
96
+ }
97
+ if (labelEnd >= text.length) continue;
98
+ let open = labelEnd + 1;
99
+ while (text[open] === " " || text[open] === " ") open += 1;
100
+ if (text[open] !== "(") continue;
101
+ let depth = 1;
102
+ let close = open + 1;
103
+ for (; close < text.length && depth > 0; close += 1) {
104
+ if (text[close] === "\\") close += 1;
105
+ else if (text[close] === "(") depth += 1;
106
+ else if (text[close] === ")") depth -= 1;
107
+ }
108
+ if (depth !== 0) continue;
109
+ const value = destination(text.slice(open + 1, close - 1));
110
+ if (value !== null) onDestination(value);
111
+ for (let offset = index; offset < close; offset += 1) chars[offset] = " ";
112
+ index = close - 1;
113
+ }
114
+ return chars.join("");
115
+ }
116
+ function trimUrlToken(value) {
117
+ let result = value;
118
+ const pairs = [
119
+ ["(", ")"],
120
+ ["[", "]"],
121
+ ["{", "}"]
122
+ ];
123
+ let changed = true;
124
+ while (changed && result.length > 0) {
125
+ changed = false;
126
+ const last = result.at(-1) ?? "";
127
+ if (".,;:!".includes(last)) {
128
+ result = result.slice(0, -1);
129
+ changed = true;
130
+ continue;
131
+ }
132
+ for (const [open, close] of pairs) {
133
+ if (last !== close) continue;
134
+ const opens = result.split(open).length - 1;
135
+ const closes = result.split(close).length - 1;
136
+ if (closes > opens) {
137
+ result = result.slice(0, -1);
138
+ changed = true;
139
+ }
140
+ break;
141
+ }
142
+ }
143
+ return result;
144
+ }
145
+ function isPublicIpv4(hostname) {
146
+ const parts = hostname.split(".");
147
+ if (parts.length !== 4 || parts.some((part) => !/^\d{1,3}$/.test(part))) return true;
148
+ const octets = parts.map(Number);
149
+ if (octets.some((part) => part > 255)) return false;
150
+ const [a, b] = octets;
151
+ return !(a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 0 || a === 192 && b === 168 || a === 198 && (b === 18 || b === 19) || a >= 224);
152
+ }
153
+ function isPublicHostname(value) {
154
+ const hostname = value.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.+$/, "");
155
+ if (hostname === "localhost" || hostname.endsWith(".localhost") || hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".lan") || hostname.endsWith(".home"))
156
+ return false;
157
+ if (hostname.includes(":")) {
158
+ const compact = hostname.replace(/^0+/, "");
159
+ return !(compact === "" || compact === "::" || compact === "::1" || compact.startsWith("fc") || compact.startsWith("fd") || /^fe[89ab]/.test(compact) || compact.startsWith("ff") || compact.startsWith("2001:db8:") || compact.startsWith("::ffff:"));
160
+ }
161
+ if (!isPublicIpv4(hostname)) return false;
162
+ if (/^\d+(?:\.\d+){3}$/.test(hostname)) return true;
163
+ return hostname.includes(".");
164
+ }
165
+ function publicHttpUrl(value) {
166
+ if (typeof value !== "string") return null;
167
+ let parsed;
168
+ try {
169
+ parsed = new URL(trimUrlToken(value));
170
+ } catch {
171
+ return null;
172
+ }
173
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== "" || !isPublicHostname(parsed.hostname))
174
+ return null;
175
+ return parsed;
176
+ }
177
+ function mediaItem(value) {
178
+ const parsed = publicHttpUrl(value);
179
+ 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 };
183
+ }
184
+ function referenceLabel(value) {
185
+ return value.trim().replace(/\s+/g, " ").toLowerCase();
186
+ }
187
+ function record(value) {
188
+ return value !== null && typeof value === "object" && !Array.isArray(value);
189
+ }
190
+ function structuredSearchMedia(meta, expectedTool) {
191
+ if (!record(meta) || !record(meta.lcxHostedMedia)) return [];
192
+ const owned = meta.lcxHostedMedia;
193
+ if (owned.version !== 1 || owned.tool !== expectedTool || !Array.isArray(owned.candidates)) return [];
194
+ const result = [];
195
+ const seen = /* @__PURE__ */ new Set();
196
+ for (const candidate of owned.candidates) {
197
+ if (!record(candidate) || candidate.structured !== true || candidate.kind !== "image" && candidate.kind !== "video") continue;
198
+ const url = publicHttpUrl(candidate.url);
199
+ if (!url || seen.has(url.href)) continue;
200
+ const previewUrl = publicHttpUrl(candidate.previewUrl);
201
+ const sourceUrl = publicHttpUrl(candidate.sourceUrl);
202
+ const poster = publicHttpUrl(candidate.poster);
203
+ seen.add(url.href);
204
+ result.push({
205
+ kind: candidate.kind,
206
+ url: url.href,
207
+ ...previewUrl ? { previewUrl: previewUrl.href } : {},
208
+ ...sourceUrl ? { sourceUrl: sourceUrl.href } : {},
209
+ ...poster ? { poster: poster.href } : {},
210
+ ...typeof candidate.caption === "string" && candidate.caption.trim() ? { caption: candidate.caption.trim().slice(0, 500) } : {},
211
+ structured: true
212
+ });
213
+ if (result.length === SEARCH_MEDIA_LIMIT) break;
214
+ }
215
+ return result;
216
+ }
217
+ function mergeSearchMedia(structured, fallback) {
218
+ const result = [];
219
+ const seen = /* @__PURE__ */ new Set();
220
+ for (const item of [...structured, ...fallback]) {
221
+ const key = `${item.kind}\0${item.kind === "video" ? item.url : item.url.replace(/#.*$/, "")}`;
222
+ if (seen.has(key)) continue;
223
+ seen.add(key);
224
+ result.push(item);
225
+ if (result.length === SEARCH_MEDIA_LIMIT) break;
226
+ }
227
+ return result;
228
+ }
229
+ function extractSearchMedia(text) {
230
+ let visible = stripHtml(stripInlineCode(stripFencedCode(text)));
231
+ const definitions = /* @__PURE__ */ new Map();
232
+ visible.replace(REFERENCE_DEFINITION, (_match, label, angle, bare) => {
233
+ definitions.set(referenceLabel(label), angle ?? bare);
234
+ return _match;
235
+ });
236
+ 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(/#.*$/, ""));
240
+ };
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
+ );
250
+ const candidates = [];
251
+ visible = collectInlineLinkDestinations(
252
+ visible,
253
+ (value) => candidates.push(value)
254
+ );
255
+ visible.replace(
256
+ REFERENCE_LINK,
257
+ (_match, prefix, label, explicit) => {
258
+ const value = definitions.get(referenceLabel(explicit === "" ? label : explicit));
259
+ if (value !== void 0) candidates.push(value);
260
+ return _match;
261
+ }
262
+ );
263
+ visible = visible.replace(REFERENCE_DEFINITION, " ");
264
+ for (const match of visible.matchAll(URL_TOKEN)) candidates.push(match[0]);
265
+ const result = [];
266
+ const seen = /* @__PURE__ */ new Set();
267
+ for (const candidate of candidates) {
268
+ const item = mediaItem(candidate);
269
+ if (item === null) continue;
270
+ const key = item.kind === "video" ? item.url : item.url.replace(/#.*$/, "");
271
+ if (nativeImages.has(key) || seen.has(key)) continue;
272
+ seen.add(key);
273
+ result.push(item);
274
+ if (result.length === SEARCH_MEDIA_LIMIT) break;
275
+ }
276
+ return result;
277
+ }
278
+
279
+ // src/client/inline-media.ts
280
+ var inlineMediaCss = `
281
+ [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}
311
+ .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}}
317
+ `;
318
+ var VISIBLE_MEDIA_LIMIT = 4;
319
+ var mediaKey = (item) => item.kind === "video" ? item.url : item.url.replace(/#.*$/, "");
320
+ function previewItems(items) {
321
+ const groups = /* @__PURE__ */ new Map();
322
+ for (const item of items) {
323
+ if (item.kind === "page") continue;
324
+ 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)]);
326
+ const group = groups.get(identity) ?? [];
327
+ if (!group.some((value) => value.url === item.url)) group.push(item);
328
+ groups.set(identity, group);
329
+ }
330
+ const result = /* @__PURE__ */ new Map();
331
+ 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()) {
333
+ 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;
338
+ }
339
+ var activeDialogByDocument = /* @__PURE__ */ new WeakMap();
340
+ function supportsInlineMediaDom(marker) {
341
+ const row = marker.closest('[data-chat-flow-kind="lcx-search-media"]');
342
+ const answer = row?.previousElementSibling;
343
+ 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
+ );
347
+ }
348
+ function installInlineMedia(marker, items, labels) {
349
+ if (items.length === 0 || !supportsInlineMediaDom(marker)) return () => {
350
+ };
351
+ const row = marker.closest('[data-chat-flow-kind="lcx-search-media"]');
352
+ const answer = row.previousElementSibling;
353
+ 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();
372
+ let disposed = false;
373
+ let frame = 0;
374
+ 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) {
427
+ activeDialogByDocument.get(doc)?.();
428
+ const dialog = doc.createElement("dialog");
429
+ dialog.className = "lcx-media-dialog";
430
+ dialog.setAttribute("aria-label", item.kind === "image" ? labels.image : labels.video);
431
+ const close = doc.createElement("button");
432
+ close.type = "button";
433
+ close.className = "lcx-media-close";
434
+ close.textContent = "\xD7";
435
+ 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;
446
+ const source = doc.createElement("a");
447
+ source.href = item.sourceUrl ?? item.url;
448
+ source.target = "_blank";
449
+ source.rel = "noopener noreferrer";
450
+ source.textContent = labels.source;
451
+ let closed = false;
452
+ let sourceIndex = 0;
453
+ let previous;
454
+ let next;
455
+ const cleanup = () => {
456
+ if (closed) return;
457
+ closed = true;
458
+ close.onclick = null;
459
+ dialog.oncancel = null;
460
+ dialog.onclick = null;
461
+ dialog.onkeydown = null;
462
+ previous && (previous.onclick = null);
463
+ next && (next.onclick = null);
464
+ media.onload = null;
465
+ media.onerror = null;
466
+ if (media instanceof HTMLVideoElement) {
467
+ media.pause();
468
+ media.removeAttribute("src");
469
+ media.load();
470
+ }
471
+ dialog.remove();
472
+ if (activeDialogByDocument.get(doc) === cleanup) activeDialogByDocument.delete(doc);
473
+ if (ownedClose === cleanup) ownedClose = void 0;
474
+ if (trigger.isConnected) trigger.focus({ preventScroll: true });
475
+ };
476
+ ownedClose = cleanup;
477
+ activeDialogByDocument.set(doc, cleanup);
478
+ close.onclick = cleanup;
479
+ dialog.oncancel = (event) => {
480
+ event.preventDefault();
481
+ cleanup();
482
+ };
483
+ 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
+ }
488
+ };
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;
499
+ }
500
+ cleanup();
501
+ fail();
502
+ };
503
+ dialog.append(close, media, source);
504
+ 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) => {
522
+ 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
+ }
560
+ };
561
+ select(images().findIndex((entry) => mediaKey(entry.item) === mediaKey(item)));
562
+ }
563
+ dialog.showModal();
564
+ close.focus();
565
+ if (media instanceof HTMLVideoElement) void media.play().catch(() => {
566
+ });
567
+ }
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();
586
+ };
587
+ const cleanup = () => {
588
+ button.onclick = null;
589
+ if (thumbnail) {
590
+ thumbnail.onload = null;
591
+ thumbnail.onerror = null;
592
+ }
593
+ entry.load = void 0;
594
+ };
595
+ const entry = { item, button, fail, cleanup };
596
+ const previewUrl = item.kind === "image" ? item.previewUrl ?? item.url : item.previewUrl ?? item.poster;
597
+ if (previewUrl) {
598
+ const image = doc.createElement("img");
599
+ thumbnail = image;
600
+ image.alt = "";
601
+ image.loading = "lazy";
602
+ image.decoding = "async";
603
+ 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 = "";
614
+ };
615
+ image.onload = reveal;
616
+ image.onerror = item.kind === "image" ? fail : () => {
617
+ image.remove();
618
+ reveal();
619
+ };
620
+ entry.load = () => {
621
+ image.src = previewUrl;
622
+ };
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);
637
+ }
638
+ }
639
+ button.onclick = () => open(item, button, fail);
640
+ preview.append(button);
641
+ entries.set(preview, entry);
642
+ return preview;
643
+ }
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);
653
+ }
654
+ strip.append(preview);
655
+ }
656
+ const canPreview = (anchor) => !anchor.closest("pre,code,[data-streaming],[data-turn-process-inline],.lcx-inline-media");
657
+ function scan() {
658
+ frame = 0;
659
+ 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;
113
665
  try {
114
- for (const f of Object.keys(next)) if (next[f] !== prev[f]) await this.scope.set(f, next[f])
115
- this.draft = null; this.dirty = false
116
- } finally { this.saving = false; this.publish() }
117
- }
118
- inject() { return { hooks: { lcxCard: this.store }, edit: (f,v) => this.edit(f,v), save: () => void this.save(), discard: () => this.discard() } }
119
- }
120
- function Row({ id, label, help, checked, disabled, onChange }) {
121
- return React.createElement('div', { className:'lcx-row' },
122
- React.createElement('input', { id, type:'checkbox', checked, disabled, onChange:e=>onChange(e.target.checked) }),
123
- React.createElement('label', { htmlFor:id }, label, React.createElement('small', null, help)))
124
- }
125
- function Field({ label, help, value, disabled, type='text', min, max, step, onChange }) {
126
- return React.createElement('div', { className:'lcx-field' },
127
- React.createElement('label', null, label),
128
- React.createElement('input', { className:'lcx-input', type, value, disabled, min, max, step, onChange }),
129
- help ? React.createElement('small', null, help) : null)
130
- }
131
- function Card(props) {
132
- const t = copy[lang()], s = props.useLcxCard(x=>x), [open,setOpen] = React.useState(false)
133
- if (!s.available) return null
134
- const disabled = !s.writable || s.saving
135
- const numeric = (field) => (e) => props.edit(field, Number(e.target.value))
136
- return React.createElement('li', { className:'lcx-card' },
137
- React.createElement('button', { className:'lcx-head', type:'button', onClick:()=>setOpen(!open) }, React.createElement('strong',null,t.title), React.createElement('span',null,open?'⌃':'⌄')),
138
- open ? React.createElement('div', { className:'lcx-body' },
139
- React.createElement('p', { className:'lcx-help' }, t.desc),
140
- React.createElement(Row, { id:'lcx-enabled', label:t.enabled, help:t.enabledHelp, checked:s.enabled.value, disabled, onChange:v=>props.edit('enabled',v) }),
141
- React.createElement(Row, { id:'lcx-web', label:t.web, help:t.webHelp, checked:s.webSearch.value, disabled:disabled||!s.enabled.value, onChange:v=>props.edit('webSearch',v) }),
142
- React.createElement('div', { className:'lcx-fields' },
143
- React.createElement(Field, { label:t.searchTimeout, help:t.searchTimeoutHelp, type:'number', min:30, max:600, step:30, value:s.webSearchTimeoutSeconds, disabled:disabled||!s.enabled.value||!s.webSearch.value, onChange:numeric('webSearchTimeoutSeconds') })),
144
- React.createElement(Row, { id:'lcx-advanced', label:t.advanced, help:t.advancedHelp, checked:s.advancedHostedSearch.value, disabled:disabled||!s.enabled.value||!s.webSearch.value, onChange:v=>props.edit('advancedHostedSearch',v) }),
145
- React.createElement(Row, { id:'lcx-alpha', label:t.alpha, help:t.alphaHelp, checked:s.alphaSearch.value, disabled:disabled||!s.enabled.value, onChange:v=>props.edit('alphaSearch',v) }),
146
- React.createElement(Row, { id:'lcx-auto-compact', label:t.autoCompact, help:t.autoCompactHelp, checked:s.autoCompaction.value, disabled:disabled||!s.enabled.value, onChange:v=>props.edit('autoCompaction',v) }),
147
- React.createElement('div', { className:'lcx-fields' },
148
- React.createElement(Field, { label:t.autoThreshold, help:t.autoThresholdHelp, type:'number', min:85, max:95, step:1, value:s.autoCompactionThresholdPercent, disabled:disabled||!s.enabled.value||!s.autoCompaction.value, onChange:numeric('autoCompactionThresholdPercent') }),
149
- React.createElement(Field, { label:t.emergencyThreshold, help:t.emergencyThresholdHelp, type:'number', min:90, max:99, step:1, value:s.emergencyPruneThresholdPercent, disabled:disabled||!s.enabled.value||!s.autoCompaction.value, onChange:numeric('emergencyPruneThresholdPercent') })),
150
- React.createElement(Row, { id:'lcx-fallback', label:t.fallback, help:t.fallbackHelp, checked:s.fallbackToBasicCompaction.value, disabled:disabled||!s.enabled.value, onChange:v=>props.edit('fallbackToBasicCompaction',v) }),
151
- React.createElement('div', { className:'lcx-fields' },
152
- React.createElement(Field, { label:t.endpoint, value:s.baseURL, disabled, onChange:e=>props.edit('baseURL',e.target.value) }),
153
- React.createElement(Field, { label:t.model, value:s.model, disabled, onChange:e=>props.edit('model',e.target.value) })),
154
- React.createElement('div', { className:'lcx-foot' },
155
- React.createElement('button', { disabled:!s.dirty||disabled, onClick:props.discard }, t.discard),
156
- React.createElement('button', { disabled:!s.dirty||disabled, onClick:props.save }, s.saving?t.saving:t.save))) : null)
157
- }
158
- const inject = ['slots','locale','settingsScope']
159
- function apply(ctx) {
160
- const slots = ctx.slots ?? ctx.get('slots'), svc = ctx.settingsScope ?? ctx.get('settingsScope')
161
- if (!slots || !svc) return
162
- const controller = new Controller(svc.bind({ namespace:NAMESPACE }))
163
- slots.inject('settings.plugin.item', () => slots.register({ name:'settings.plugin.item', key:NAMESPACE, inject:()=>controller.inject() }, Card))
164
- ctx.effect(() => () => controller.stop(), 'lcx-codex settings card')
165
- }
166
- exports.apply = apply; exports.inject = inject; return module.exports
167
- },
168
- })
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 });
695
+ }
696
+ updateVisibility();
697
+ }
698
+ scan();
699
+ 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
707
+ });
708
+ return () => {
709
+ if (disposed) return;
710
+ disposed = true;
711
+ observer.disconnect();
712
+ if (frame) {
713
+ cancelAnimationFrame(frame);
714
+ frame = 0;
715
+ }
716
+ 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();
724
+ };
725
+ }
726
+
727
+ // src/search-accounting.ts
728
+ var object = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
729
+ var count = (v) => typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
730
+ var zeroBuckets = () => ({ uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 });
731
+ var isSearchTool = (name) => name === "web_search" || name === "websearch_gpt_advanced";
732
+ function auxiliaryUsageOf(event, toolName) {
733
+ if (!object(event) || event.type !== "tool/result" || !object(event.data?.meta)) return [];
734
+ if (!isSearchTool(toolName)) return [];
735
+ const raw = event.data.meta.auxiliaryUsage;
736
+ if (!Array.isArray(raw)) return [];
737
+ const ids = /* @__PURE__ */ new Set(), result = [];
738
+ for (const item of raw) {
739
+ if (!object(item) || typeof item.requestId !== "string" || !item.requestId || typeof item.provider !== "string" || !item.provider || typeof item.model !== "string" || !item.model || !object(item.usage) || ids.has(item.requestId)) continue;
740
+ const u = item.usage;
741
+ if (![u.inputTokens, u.outputTokens, u.totalTokens, u.cacheReadTokens, u.cacheWriteTokens].every(count) || u.inputTokens + u.outputTokens + u.cacheReadTokens + u.cacheWriteTokens !== u.totalTokens) continue;
742
+ ids.add(item.requestId);
743
+ result.push(item);
744
+ }
745
+ return result;
746
+ }
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
+
780
+ // src/client/search-usage-ui.ts
781
+ var searchUsageDefinition = {
782
+ kind: "lcx-search-usage",
783
+ match(event) {
784
+ if (event.type === "turn/start") return { id: String(event.data.turn), role: "start" };
785
+ if (event.type === "turn/end" || event.type === "tool/result" || event.type === "tool/call") return { id: String(event.data.turn), role: "update" };
786
+ return null;
787
+ },
788
+ start(_context, match) {
789
+ if (match.event.type !== "turn/start") throw new Error("LCX billing requires a complete turn");
790
+ return { turn: match.event.data.turn, records: [], pending: {}, complete: false };
791
+ },
792
+ update(context, match) {
793
+ const event = match.event, state = context.state, pending = { ...state.pending };
794
+ if (event.type === "tool/call" && isSearchTool(event.data.name)) pending[event.data.callId] = event.data.name;
795
+ const callId = event.type === "tool/result" ? event.data.message.source.callId : void 0;
796
+ const extra = auxiliaryUsageOf(event, callId ? pending[callId] : void 0);
797
+ if (callId) delete pending[callId];
798
+ return { ...state, pending, records: [...state.records, ...extra], complete: event.type === "turn/end" };
799
+ },
800
+ publication: (match) => match.event.type === "turn/end" ? "immediate" : "none",
801
+ buildLocationData(context, scope) {
802
+ const s = context.state;
803
+ return scope === "turn" && s?.complete ? { kind: "turn", turn: s.turn, key: "lcx-search-usage", value: s.records } : null;
804
+ }
805
+ };
806
+ function installUsageSlots(slots, createElement) {
807
+ 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;
814
+ 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
+ };
859
+ });
860
+ } catch {
861
+ continue;
862
+ }
863
+ if (typeof effect === "function") cleanups.push(effect);
864
+ }
865
+ return () => {
866
+ for (const dispose of cleanups.reverse()) dispose();
867
+ };
868
+ }
869
+
870
+ // src/client/index.tsx
871
+ var SEARCH_MEDIA_KIND = "lcx-search-media";
872
+ window.__ModuleLoader__.load({
873
+ id: "dsh-lcx-codex",
874
+ factory: (require2) => {
875
+ const module = { exports: {} };
876
+ const exports = module.exports;
877
+ const React = require2("react");
878
+ const { createSnapshotStore } = require2(
879
+ "@deepseek-ai/dsh-client-store"
880
+ );
881
+ const NAMESPACE = "lcx-codex";
882
+ const mediaStore = createSnapshotStore(
883
+ { enabled: false }
884
+ );
885
+ const FIELDS = [
886
+ "enabled",
887
+ "webSearch",
888
+ "advancedHostedSearch",
889
+ "alphaSearch",
890
+ "grokNativeWebSearch",
891
+ "grokNativeXSearch",
892
+ "searchMediaPreview"
893
+ ];
894
+ const DEFAULTS = Object.fromEntries(
895
+ FIELDS.map((field) => [field, false])
896
+ );
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;
898
+ function mountCss() {
899
+ if (typeof document === "undefined") return () => {
900
+ };
901
+ if (document.querySelector('style[data-plugin-css="dsh-lcx-codex"]'))
902
+ return () => {
903
+ };
904
+ const tag = document.createElement("style");
905
+ tag.dataset.pluginCss = "dsh-lcx-codex";
906
+ tag.textContent = css;
907
+ document.head.appendChild(tag);
908
+ return () => tag.remove();
909
+ }
910
+ const copy = {
911
+ zh: {
912
+ title: "Responses / Codex \u80FD\u529B",
913
+ 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
+ enabled: "\u542F\u7528 LCX\uFF08\u63A5\u7BA1\u5F53\u524D GPT Responses \u4F1A\u8BDD\uFF09",
915
+ 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",
916
+ web: "\u4F7F\u7528 GPT Hosted Search \u4F5C\u4E3A DSH web_search \u540E\u7AEF",
917
+ webHelp: "\u4E0D\u4F1A\u65B0\u589E\u7B2C\u4E8C\u4E2A\u666E\u901A\u641C\u7D22\u5DE5\u5177\uFF1B\u666E\u901A web_search \u81EA\u52A8\u8DDF\u968F\u5F53\u524D Agent \u7684 GPT Responses route\u3002",
918
+ advanced: "\u542F\u7528\u9AD8\u7EA7 Hosted \u5DE5\u5177\uFF08websearch_gpt_advanced\uFF09",
919
+ advancedHelp: "\u53EA\u5728\u9700\u8981\u57DF\u540D\u8FC7\u6EE4\u3001\u4F4D\u7F6E\u3001search context\u3001\u56FE\u7247\u7B49\u539F\u751F Hosted \u53C2\u6570\u65F6\u4F7F\u7528\uFF1B\u9ED8\u8BA4\u5173\u95ED\u4EE5\u4FDD\u6301\u5DE5\u5177 schema \u7A33\u5B9A\u3002",
920
+ alpha: "\u542F\u7528 Alpha command\uFF08websearch_alpha\uFF09",
921
+ alphaHelp: "\u4EC5 capability probe \u5BF9\u5F53\u524D route/schema \u9A8C\u8BC1\u901A\u8FC7\u540E\u624D\u771F\u6B63\u6CE8\u518C\u3002",
922
+ 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",
925
+ mediaPlay: "\u64AD\u653E\u89C6\u9891",
926
+ mediaEnlarge: "\u653E\u5927\u56FE\u7247",
927
+ 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",
941
+ mediaOpen: "\u6253\u5F00\u539F\u59CB\u5A92\u4F53",
942
+ grokTitle: "Grok \u539F\u751F\u641C\u7D22",
943
+ 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
+ grokWeb: "\u542F\u7528\u539F\u751F Web Search",
945
+ grokWebHelp: "\u4F7F\u7528 Grok \u7684\u539F\u751F\u7F51\u9875\u641C\u7D22\u3002",
946
+ grokX: "\u542F\u7528\u539F\u751F X Search",
947
+ grokXHelp: "\u4F7F\u7528 Grok \u7684\u539F\u751F X \u641C\u7D22\uFF1B\u5355\u72EC\u5F00\u542F\u4E5F\u4E0D\u4F1A\u4F7F\u7528 DSH \u641C\u7D22\u3002",
948
+ save: "\u4FDD\u5B58",
949
+ discard: "\u653E\u5F03\u4FEE\u6539",
950
+ saving: "\u4FDD\u5B58\u4E2D\u2026",
951
+ saveError: "\u672A\u80FD\u786E\u8BA4\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\u3002\u4FEE\u6539\u5DF2\u4FDD\u7559\uFF0C\u8BF7\u91CD\u8BD5\u6216\u653E\u5F03\u4FEE\u6539\u4EE5\u67E5\u770B\u5F53\u524D\u8BBE\u7F6E\u3002"
952
+ },
953
+ en: {
954
+ title: "Responses / Codex capabilities",
955
+ desc: "LCX follows the GPT Responses route selected by the current DSH session. Model, endpoint and credentials remain DSH-owned.",
956
+ enabled: "Enable LCX (own current GPT Responses conversation)",
957
+ enabledHelp: "Applies only to GPT Responses conversations. Grok native search is controlled independently below; other non-GPT models continue through DSH normally.",
958
+ web: "Use GPT Hosted Search as DSH web_search backend",
959
+ webHelp: "Keeps DSH web_search as the single ordinary search tool and follows the active Agent GPT Responses route.",
960
+ advanced: "Enable advanced Hosted tool (websearch_gpt_advanced)",
961
+ advancedHelp: "Only for native Hosted controls such as domains, location, context size and image search; off by default for stable tool schemas.",
962
+ alpha: "Enable Alpha command (websearch_alpha)",
963
+ alphaHelp: "Registered only after a matching capability probe.",
964
+ 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",
967
+ mediaPlay: "Play video",
968
+ mediaEnlarge: "Enlarge image",
969
+ 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",
983
+ mediaOpen: "Open original media",
984
+ grokTitle: "Grok native search",
985
+ 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
+ grokWeb: "Enable native Web Search",
987
+ grokWebHelp: "Use Grok's native web search.",
988
+ grokX: "Enable native X Search",
989
+ grokXHelp: "Use Grok's native X search. Enabling it alone also avoids DSH search.",
990
+ save: "Save",
991
+ discard: "Discard",
992
+ saving: "Saving\u2026",
993
+ saveError: "Could not confirm settings were saved. Changes are kept; retry or discard to view current settings."
994
+ }
995
+ };
996
+ function valueFrom(snapshot) {
997
+ const value = snapshot.value ?? {};
998
+ return {
999
+ ...DEFAULTS,
1000
+ ...Object.fromEntries(
1001
+ FIELDS.map((field) => [field, Boolean(value[field])])
1002
+ )
1003
+ };
1004
+ }
1005
+ const hostedMediaTool = (value) => value === "web_search" || value === "websearch_gpt_advanced";
1006
+ const searchMediaDefinition = {
1007
+ kind: SEARCH_MEDIA_KIND,
1008
+ target: "chat",
1009
+ match(event) {
1010
+ if (event.type === "turn/start")
1011
+ return { id: String(event.data.turn), role: "start" };
1012
+ if (event.type === "tool/result" || event.type === "tool/call")
1013
+ return { id: String(event.data.turn), role: "update" };
1014
+ if (event.type !== "assistant/message" || event.surfaceOp !== "append" || event.data.interrupted === true)
1015
+ return null;
1016
+ const source = event.data.message.source;
1017
+ if (source.kind !== "model" || !/^(?:gpt|grok)/i.test(source.model))
1018
+ return null;
1019
+ return { id: String(event.data.turn), role: "update" };
1020
+ },
1021
+ start(_context, match) {
1022
+ if (match.event.type !== "turn/start")
1023
+ throw new Error("search media state requires turn/start");
1024
+ return {
1025
+ anchorSeq: 0,
1026
+ location: match.location,
1027
+ items: [],
1028
+ structuredItems: [],
1029
+ provider: "",
1030
+ model: "",
1031
+ pending: {},
1032
+ ready: false
1033
+ };
1034
+ },
1035
+ update(context, match) {
1036
+ if (match.event.type === "tool/call") {
1037
+ if (!hostedMediaTool(match.event.data.name)) return context.state;
1038
+ return {
1039
+ ...context.state,
1040
+ pending: {
1041
+ ...context.state.pending,
1042
+ [match.event.data.callId]: match.event.data.name
1043
+ }
1044
+ };
1045
+ }
1046
+ if (match.event.type === "tool/result") {
1047
+ const callId = match.event.data.message.source.callId;
1048
+ const tool = context.state.pending[callId];
1049
+ if (tool === void 0) return context.state;
1050
+ const pending = { ...context.state.pending };
1051
+ delete pending[callId];
1052
+ return {
1053
+ ...context.state,
1054
+ pending,
1055
+ structuredItems: mergeSearchMedia(
1056
+ context.state.structuredItems,
1057
+ structuredSearchMedia(match.event.data.meta, tool)
1058
+ )
1059
+ };
1060
+ }
1061
+ if (match.event.type !== "assistant/message") return context.state;
1062
+ const message = match.event.data.message;
1063
+ const source = message.source;
1064
+ if (source.kind !== "model") return context.state;
1065
+ const text = message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n");
1066
+ return {
1067
+ ...context.state,
1068
+ anchorSeq: match.event.seq,
1069
+ location: match.location,
1070
+ items: context.state.structuredItems.length ? context.state.structuredItems : extractSearchMedia(text),
1071
+ provider: source.provider,
1072
+ model: source.model,
1073
+ ready: true
1074
+ };
1075
+ },
1076
+ publication: (match) => match.event.type === "assistant/message" ? "immediate" : "none",
1077
+ buildViewNode(context) {
1078
+ const state = context.state;
1079
+ if (state === void 0 || !state.ready || state.items.length === 0) return null;
1080
+ const node = {
1081
+ key: context.key,
1082
+ kind: SEARCH_MEDIA_KIND,
1083
+ id: context.id,
1084
+ target: "chat",
1085
+ anchorSeq: state.anchorSeq,
1086
+ location: state.location,
1087
+ visibility: "visible",
1088
+ data: {
1089
+ items: state.items,
1090
+ provider: state.provider,
1091
+ model: state.model
1092
+ }
1093
+ };
1094
+ return node;
1095
+ }
1096
+ };
1097
+ class Controller {
1098
+ scope;
1099
+ draft;
1100
+ dirty;
1101
+ saving;
1102
+ saveError;
1103
+ store;
1104
+ stop;
1105
+ constructor(scope) {
1106
+ this.scope = scope;
1107
+ this.draft = null;
1108
+ this.dirty = false;
1109
+ this.saving = false;
1110
+ this.saveError = false;
1111
+ this.store = createSnapshotStore(this.projection());
1112
+ mediaStore.set({ enabled: this.value().searchMediaPreview });
1113
+ this.stop = scope.subscribe(() => {
1114
+ if (!this.dirty) this.draft = null;
1115
+ this.publish();
1116
+ });
1117
+ }
1118
+ snapshot() {
1119
+ return this.scope.getSnapshot();
1120
+ }
1121
+ value() {
1122
+ return valueFrom(this.snapshot());
1123
+ }
1124
+ draftValue() {
1125
+ return { ...this.value(), ...this.draft ?? {} };
1126
+ }
1127
+ projection() {
1128
+ const s = this.snapshot(), value = this.draftValue(), fields = Object.fromEntries(
1129
+ FIELDS.map((field) => [field, { value: Boolean(value[field]) }])
1130
+ );
1131
+ return {
1132
+ available: s.status === "ready",
1133
+ writable: s.writable,
1134
+ dirty: this.dirty,
1135
+ saving: this.saving,
1136
+ saveError: this.saveError,
1137
+ ...fields
1138
+ };
1139
+ }
1140
+ publish() {
1141
+ mediaStore.set({ enabled: this.value().searchMediaPreview });
1142
+ this.store.set(this.projection());
1143
+ }
1144
+ edit(field, value) {
1145
+ if (!FIELDS.includes(field) || this.saving || !this.snapshot().writable) return;
1146
+ this.draft = {
1147
+ ...this.draft,
1148
+ [field]: Boolean(value)
1149
+ };
1150
+ if (this.value()[field] === Boolean(value)) delete this.draft[field];
1151
+ this.dirty = Object.keys(this.draft).length > 0;
1152
+ this.saveError = false;
1153
+ this.publish();
1154
+ }
1155
+ discard() {
1156
+ if (this.saving) return;
1157
+ this.draft = null;
1158
+ this.dirty = false;
1159
+ this.saveError = false;
1160
+ this.publish();
1161
+ }
1162
+ async save() {
1163
+ if (!this.dirty || this.saving || !this.snapshot().writable || this.snapshot().status !== "ready") return;
1164
+ const next = this.draftValue(), prev = this.value();
1165
+ const fields = FIELDS.filter((field) => next[field] !== prev[field]);
1166
+ this.saving = true;
1167
+ this.saveError = false;
1168
+ this.publish();
1169
+ try {
1170
+ if (fields.length) {
1171
+ await this.scope.mutate(fields.map((field) => ({
1172
+ op: "set",
1173
+ path: [field],
1174
+ value: next[field]
1175
+ })));
1176
+ if (this.snapshot().status !== "ready" || fields.some((field) => this.value()[field] !== next[field]))
1177
+ throw new Error("Settings mutation was not confirmed");
1178
+ }
1179
+ this.draft = null;
1180
+ this.dirty = false;
1181
+ } catch {
1182
+ this.saveError = true;
1183
+ } finally {
1184
+ this.saving = false;
1185
+ this.publish();
1186
+ }
1187
+ }
1188
+ inject() {
1189
+ return {
1190
+ hooks: {
1191
+ lcxCard: this.store,
1192
+ mediaPreview: mediaStore
1193
+ },
1194
+ setMediaPreview: (value) => this.edit("searchMediaPreview", value),
1195
+ edit: (field, value) => this.edit(field, value),
1196
+ save: () => void this.save(),
1197
+ discard: () => this.discard()
1198
+ };
1199
+ }
1200
+ }
1201
+ function Row({
1202
+ id,
1203
+ label,
1204
+ help,
1205
+ checked,
1206
+ disabled,
1207
+ onChange
1208
+ }) {
1209
+ return React.createElement(
1210
+ "div",
1211
+ { className: "lcx-row" },
1212
+ React.createElement("input", {
1213
+ id,
1214
+ type: "checkbox",
1215
+ checked,
1216
+ disabled,
1217
+ onChange: (event) => onChange(event.target.checked)
1218
+ }),
1219
+ React.createElement(
1220
+ "label",
1221
+ { htmlFor: id },
1222
+ label,
1223
+ React.createElement("small", null, help)
1224
+ )
1225
+ );
1226
+ }
1227
+ function Card(props) {
1228
+ const t = props.t, s = props.useLcxCard((state) => state), [open, setOpen] = React.useState(false);
1229
+ if (!s.available) return null;
1230
+ const disabled = !s.writable || s.saving;
1231
+ return React.createElement(
1232
+ "li",
1233
+ { className: "lcx-card" },
1234
+ React.createElement(
1235
+ "button",
1236
+ {
1237
+ className: "lcx-head",
1238
+ type: "button",
1239
+ onClick: () => setOpen(!open)
1240
+ },
1241
+ React.createElement("strong", null, t("title")),
1242
+ React.createElement("span", null, open ? "\u2303" : "\u2304")
1243
+ ),
1244
+ open ? React.createElement(
1245
+ "div",
1246
+ { className: "lcx-body" },
1247
+ React.createElement("p", { className: "lcx-help" }, t("desc")),
1248
+ React.createElement(Row, {
1249
+ id: "lcx-media-preview",
1250
+ label: t("mediaPreview"),
1251
+ help: t("mediaPreviewHelp"),
1252
+ checked: s.searchMediaPreview.value,
1253
+ disabled,
1254
+ onChange: props.setMediaPreview
1255
+ }),
1256
+ React.createElement(Row, {
1257
+ id: "lcx-enabled",
1258
+ label: t("enabled"),
1259
+ help: t("enabledHelp"),
1260
+ checked: s.enabled.value,
1261
+ disabled,
1262
+ onChange: (value) => props.edit("enabled", value)
1263
+ }),
1264
+ React.createElement(Row, {
1265
+ id: "lcx-web",
1266
+ label: t("web"),
1267
+ help: t("webHelp"),
1268
+ checked: s.webSearch.value,
1269
+ disabled: disabled || !s.enabled.value,
1270
+ onChange: (value) => props.edit("webSearch", value)
1271
+ }),
1272
+ React.createElement(Row, {
1273
+ id: "lcx-advanced",
1274
+ label: t("advanced"),
1275
+ help: t("advancedHelp"),
1276
+ checked: s.advancedHostedSearch.value,
1277
+ disabled: disabled || !s.enabled.value || !s.webSearch.value,
1278
+ onChange: (value) => props.edit("advancedHostedSearch", value)
1279
+ }),
1280
+ React.createElement(Row, {
1281
+ id: "lcx-alpha",
1282
+ label: t("alpha"),
1283
+ help: t("alphaHelp"),
1284
+ checked: s.alphaSearch.value,
1285
+ disabled: disabled || !s.enabled.value,
1286
+ onChange: (value) => props.edit("alphaSearch", value)
1287
+ }),
1288
+ React.createElement(
1289
+ "section",
1290
+ { className: "lcx-group" },
1291
+ React.createElement("strong", null, t("grokTitle")),
1292
+ React.createElement("p", { className: "lcx-help" }, t("grokDesc")),
1293
+ React.createElement(Row, {
1294
+ id: "lcx-grok-web",
1295
+ label: t("grokWeb"),
1296
+ help: t("grokWebHelp"),
1297
+ checked: s.grokNativeWebSearch.value,
1298
+ disabled,
1299
+ onChange: (value) => props.edit("grokNativeWebSearch", value)
1300
+ }),
1301
+ React.createElement(Row, {
1302
+ id: "lcx-grok-x",
1303
+ label: t("grokX"),
1304
+ help: t("grokXHelp"),
1305
+ checked: s.grokNativeXSearch.value,
1306
+ disabled,
1307
+ onChange: (value) => props.edit("grokNativeXSearch", value)
1308
+ })
1309
+ ),
1310
+ React.createElement(
1311
+ "p",
1312
+ { role: "alert", hidden: !s.saveError },
1313
+ s.saveError ? t("saveError") : ""
1314
+ ),
1315
+ React.createElement(
1316
+ "div",
1317
+ { className: "lcx-foot" },
1318
+ React.createElement(
1319
+ "button",
1320
+ { disabled: !s.dirty || disabled, onClick: props.discard },
1321
+ t("discard")
1322
+ ),
1323
+ React.createElement(
1324
+ "button",
1325
+ { disabled: !s.dirty || disabled, onClick: props.save },
1326
+ s.saving ? t("saving") : t("save")
1327
+ )
1328
+ )
1329
+ ) : null
1330
+ );
1331
+ }
1332
+ function InlineMedia({ node, t }) {
1333
+ const [marker, setMarker] = React.useState(null);
1334
+ React.useEffect(() => {
1335
+ 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") });
1337
+ }, [marker, node.data.items, t]);
1338
+ return React.createElement("span", { ref: setMarker, "aria-hidden": true });
1339
+ }
1340
+ function MediaNode(props) {
1341
+ const enabled = props.useMediaPreview((state) => state?.enabled === true);
1342
+ return enabled ? React.createElement(InlineMedia, { node: props.node, t: props.t }) : null;
1343
+ }
1344
+ const inject = ["slots", "locale", "settingsScope", "uiConversation"];
1345
+ 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");
1347
+ 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
+ }
1352
+ ctx.effect(mountCss, "lcx-codex styles");
1353
+ for (const language of ["zh", "en"])
1354
+ ctx.effect(() => locale.register(NAMESPACE, language, copy[language]), `lcx-codex ${language} dictionary`);
1355
+ ctx.effect(
1356
+ () => uiConversation.events.register(searchMediaDefinition),
1357
+ "lcx-codex search media definition"
1358
+ );
1359
+ const installSlot = (name, label, register) => ctx.effect(() => {
1360
+ const cleanup = slots.inject(name, register);
1361
+ return () => {
1362
+ if (typeof cleanup === "function") cleanup();
1363
+ };
1364
+ }, label);
1365
+ const controller = new Controller(svc.bind({ namespace: NAMESPACE }));
1366
+ installSlot(
1367
+ "settings.plugin.item",
1368
+ "lcx-codex settings slot",
1369
+ () => slots.register(
1370
+ {
1371
+ name: "settings.plugin.item",
1372
+ key: NAMESPACE,
1373
+ locale: NAMESPACE,
1374
+ inject: () => controller.inject()
1375
+ },
1376
+ Card
1377
+ )
1378
+ );
1379
+ installSlot(
1380
+ "conversation.chat.node",
1381
+ "lcx-codex media slot",
1382
+ () => slots.register(
1383
+ {
1384
+ name: "conversation.chat.node",
1385
+ key: SEARCH_MEDIA_KIND,
1386
+ locale: NAMESPACE,
1387
+ inject: () => ({ hooks: { mediaPreview: mediaStore } })
1388
+ },
1389
+ MediaNode
1390
+ )
1391
+ );
1392
+ ctx.effect(() => () => controller.stop(), "lcx-codex settings card");
1393
+ }
1394
+ exports.apply = apply;
1395
+ exports.inject = inject;
1396
+ return module.exports;
1397
+ }
1398
+ });
1399
+ })();